diff --git a/PyNFSe/cli/__init__.py b/PyNFSe/cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/PyNFSe/cli/cadastro/__init__.py b/PyNFSe/cli/cadastro/__init__.py new file mode 100644 index 0000000..2c9e132 --- /dev/null +++ b/PyNFSe/cli/cadastro/__init__.py @@ -0,0 +1,6 @@ +from .ambiente import ambiente, retornar_ambiente +from .configuracao import configuracao, retornar_configuracao +from .lote_rps import lote_rps +from .prestador import prestador, retornar_prestador +from .servico import servico +from .tomador import tomador, retornar_tomador diff --git a/PyNFSe/cli/cadastro/ambiente.py b/PyNFSe/cli/cadastro/ambiente.py new file mode 100644 index 0000000..42cc283 --- /dev/null +++ b/PyNFSe/cli/cadastro/ambiente.py @@ -0,0 +1,27 @@ +import json + +import click + +from PyNFSe.cli import constants +from PyNFSe.utils.entidades import Ambiente + + +def ambiente(ambiente): + click.echo('### Configuração ambiente {}'.format(ambiente)) + amb = Ambiente() + + amb.numero_rps = click.prompt('Número inicial do rps', type=int, default=1) + amb.numero_lote = click.prompt('Número inicial do lote rps', type=int, default=1) + amb.certificado = click.prompt('Caminho para o certificado', type=str) + amb.senha = click.prompt('Senha certificado', type=str) + + return amb + + +def retornar_ambiente(producao): + if producao: + ambiente = constants.JSON_AMBIENTE_PRODUCAO + else: + ambiente = constants.JSON_AMBIENTE_HOMOLOGACAO + with open(ambiente) as file: + return Ambiente(**json.load(file)) diff --git a/PyNFSe/cli/cadastro/configuracao.py b/PyNFSe/cli/cadastro/configuracao.py new file mode 100644 index 0000000..66b4c92 --- /dev/null +++ b/PyNFSe/cli/cadastro/configuracao.py @@ -0,0 +1,55 @@ +import json + +import click + +from PyNFSe.utils.entidades import Configuracao +from PyNFSe.cli import constants + + +def configuracao(): + conf = Configuracao() + + conf.simples = int(click.prompt('Simples Nacional (1 - Sim / 2 - Não)', type=click.Choice(['1', '2']))) + conf.incentivo = int( + click.prompt('Incentivador Cultural (1 - Sim / 2 - Não)', type=click.Choice(['1', '2']))) + + conf.iss_retido = int(click.prompt('ISS Retido padrão (1 - Sim / 2 - Não)', type=click.Choice(['1', '2']))) + conf.regime_especial = int(click.prompt('Regime Especial (0 para ver as opções)', + type=click.Choice(['0', '1', '2', '3', '4']))) + while conf.regime_especial == 0: + click.echo( + '1 - ME Municipal\n' + '2 - Estimativa\n' + '3 - Sociedade de profissionais\n' + '4 - Cooperativa\n' + ) + conf.regime_especial = int(click.prompt('Regime Especial (0 para ver as opções)', + type=click.Choice(['0', '1', '2', '3', '4']))) + + conf.natureza_operacao = int(click.prompt('Natureza da Operação (0 para ver as opções)', + type=click.Choice(['0', '1', '2', '3', '4', '5', '6']))) + while conf.natureza_operacao == 0: + click.echo( + '1 - Tributação no município\n' + '2 - Tributação fora do município\n' + '3 - Isenção\n' + '4 - Imune\n' + '5 –Exigibilidade suspensa por decisão judicial\n' + '6 - Exigibilidade suspensa por procedimento administrativo\n' + ) + conf.natureza_operacao = int(click.prompt('Natureza da Operação (0 para ver as opções)', + type=click.Choice(['0', '1', '2', '3', '4', '5', '6']))) + + conf.codigo_municipio = click.prompt('Código do município padrão', type=str) + conf.item_lista = click.prompt('Código de item da lista de serviço padrão', type=str) + conf.codigo_tributacao_municipio = click.prompt('Código de Tributação padrão', type=str, + default=conf.item_lista) + conf.codigo_cnae = click.prompt('Código CNAE padrão', type=int) + conf.aliquota = click.prompt('Aliquota padrão') + + return conf + + +def retornar_configuracao(): + with open(constants.JSON_CONFIGURACAO) as file: + return Configuracao(**json.load(file)) diff --git a/PyNFSe/cli/cadastro/lote_rps.py b/PyNFSe/cli/cadastro/lote_rps.py new file mode 100644 index 0000000..d67b5b5 --- /dev/null +++ b/PyNFSe/cli/cadastro/lote_rps.py @@ -0,0 +1,29 @@ +from datetime import datetime + +from PyNFSe.utils.entidades import LoteRPS, RPS + + +def lote_rps(prestador, tomador, servico, ambiente, configuracao): + rps = RPS() + lote = LoteRPS() + + rps.identificador = 'N{}'.format(ambiente.numero_rps) + rps.data_emissao = datetime.today() + rps.servico = servico.__dict__ + rps.prestador = prestador.__dict__ + rps.tomador = tomador.__dict__ + rps.simples = configuracao.simples + rps.incentivo = configuracao.incentivo + rps.numero = ambiente.numero_rps + rps.serie = 'A1' + rps.tipo = '1' + rps.natureza_operacao = configuracao.natureza_operacao + rps.regime_especial = configuracao.regime_especial + + lote.identificador = 'L{}'.format(ambiente.numero_lote) + lote.numero_lote = ambiente.numero_lote + lote.cnpj = prestador.cnpj + lote.inscricao_municipal = prestador.inscricao_municipal + lote.lista_rps = [rps.__dict__] + + return lote diff --git a/PyNFSe/cli/cadastro/prestador.py b/PyNFSe/cli/cadastro/prestador.py new file mode 100644 index 0000000..52fb644 --- /dev/null +++ b/PyNFSe/cli/cadastro/prestador.py @@ -0,0 +1,20 @@ +import json + +import click + +from PyNFSe.utils.entidades import Prestador +from PyNFSe.cli import constants + + +def prestador(): + click.echo('### Cadastro do Prestador ###') + prestador = Prestador() + prestador.cnpj = click.prompt('Informe o CNPJ', type=str) + prestador.inscricao_municipal = click.prompt('Informe a Inscrição Municipal', type=str) + + return prestador + + +def retornar_prestador(): + with open(constants.JSON_PRESTADOR) as file: + return Prestador(**json.load(file)) diff --git a/PyNFSe/cli/cadastro/servico.py b/PyNFSe/cli/cadastro/servico.py new file mode 100644 index 0000000..a9851e2 --- /dev/null +++ b/PyNFSe/cli/cadastro/servico.py @@ -0,0 +1,20 @@ +from decimal import Decimal + +import click + +from PyNFSe.cli import constants +from PyNFSe.utils.entidades import Servico + + +def servico(): + servico = Servico() + servico.valor_servico = Decimal(click.prompt('Valor serviço')) + servico.iss_retido = int(click.prompt('ISS Retido (1 - Sim / 2 - Não)', type=click.Choice(['1', '2']), default=constants.ISS_RETIDO)) + servico.item_lista = click.prompt('Código de item da lista de serviço', default=constants.ITEM_LISTA) + servico.discriminacao = click.prompt('Discriminação do serviço prestado') + servico.codigo_municipio = click.prompt('Código do município', default=constants.CODIGO_MUNICIPIO) + servico.codigo_cnae = click.prompt('Código CNAE', type=int, default=constants.CODIGO_CNAE) + servico.codigo_tributacao_municipio = click.prompt('Código de Tributação', default=servico.item_lista) + servico.aliquota = Decimal(click.prompt('Aliquota', default=constants.ALIQUOTA)) + + return servico diff --git a/PyNFSe/cli/cadastro/tomador.py b/PyNFSe/cli/cadastro/tomador.py new file mode 100644 index 0000000..a62d6d0 --- /dev/null +++ b/PyNFSe/cli/cadastro/tomador.py @@ -0,0 +1,50 @@ +import json + +import click + +from PyNFSe.cli import constants +from PyNFSe.utils.entidades import Tomador + + +def tomador(): + tomador = Tomador() + tomador.tipo_documento = click.prompt('Tipo Documento', type=click.Choice(['cpf', 'cnpj']), default='cnpj').upper() + tomador.numero_documento = click.prompt('Número Documento') + tomador.razao_social = click.prompt('Razão Social') + + if tomador.tipo_documento == 'CNPJ': + tomador.inscricao_municipal = click.prompt('Inscrição Municipal (0 para em branco)') + if tomador.inscricao_municipal == '0': + tomador.inscricao_municipal = None + + tomador.endereco = click.prompt('Endereço (Apenas a rua)') + tomador.endereco_numero = click.prompt('Número endereço') + + tomador.endereco_complemento = click.prompt('Complemento endereço (0 para em branco)') + if tomador.endereco_complemento == '0': + tomador.endereco_complemento = None + + tomador.bairro = click.prompt('Bairro') + tomador.codigo_municipio = click.prompt('Código Município', default=constants.CODIGO_MUNICIPIO) + tomador.uf = click.prompt('UF') + tomador.cep = click.prompt('CEP') + + tomador.telefone = click.prompt('Telefone (0 para em branco)') + if tomador.telefone == '0': + tomador.telefone = None + + tomador.email = click.prompt('Email (0 para em branco)') + if tomador.email == '0': + tomador.email = None + + return tomador + + +def retornar_tomador(): + numero_doc = click.prompt('Numero documento') + with open(constants.JSON_TOMADORES, mode='r') as file: + clientes = json.load(file) + try: + return Tomador(**clientes[numero_doc]) + except KeyError: + click.echo('{} não cadastrado'.format(numero_doc)) diff --git a/PyNFSe/cli/cli.py b/PyNFSe/cli/cli.py new file mode 100644 index 0000000..d2772f2 --- /dev/null +++ b/PyNFSe/cli/cli.py @@ -0,0 +1,96 @@ +import click +import json +import os + + +from PyNFSe.cli import constants +from PyNFSe.cli import helpers +from PyNFSe.cli import cadastro + + +@click.group() +def cli(): + pass + + +@cli.command() +def configurar_cli(): + helpers.criar_diretorio(constants.DIR_PYNFSE) + + if helpers.criar_arquivo_json(constants.JSON_PRESTADOR): + prestador = cadastro.prestador() + helpers.salvar_arquivo_json(constants.JSON_PRESTADOR, prestador) + + if helpers.criar_arquivo_json(constants.JSON_CONFIGURACAO): + configuracao = cadastro.configuracao() + helpers.salvar_arquivo_json(constants.JSON_CONFIGURACAO, configuracao) + + if helpers.criar_arquivo_json(constants.JSON_AMBIENTE_PRODUCAO): + ambiente = cadastro.ambiente('Produção') + helpers.salvar_arquivo_json(constants.JSON_AMBIENTE_PRODUCAO, ambiente) + + ambiente_teste = click.prompt('Deseja cadastar ambiente homologação (s/n)', type=click.Choice(['s', 'n']), + default='s') + if ambiente_teste == 's': + if helpers.criar_arquivo_json(constants.JSON_AMBIENTE_HOMOLOGACAO): + ambiente = cadastro.ambiente('Homologação') + helpers.salvar_arquivo_json(constants.JSON_AMBIENTE_HOMOLOGACAO, ambiente) + + helpers.criar_arquivo_json(constants.JSON_TOMADORES) + + +@cli.command() +def cadastrar_tomador(): + if not os.path.exists(constants.DIR_PYNFSE): + print('pynfse não configurado. Execute "python pynfse.py configurar_cli" e siga os passos.') + return + + tomador = cadastro.tomador() + + with open(constants.JSON_TOMADORES, mode='r') as file: + json_file = json.load(file) + + with open(constants.JSON_TOMADORES, mode='w') as file: + json_file[tomador.numero_documento] = tomador.__dict__ + json.dump(json_file, file, ensure_ascii=False) + + +@cli.command() +@click.option('--producao', is_flag=True) +def emitir_nfse(producao): + + ambiente = cadastro.retornar_ambiente(producao) + configuracao = cadastro.retornar_configuracao() + prestador = cadastro.retornar_prestador() + tomador = cadastro.retornar_tomador() + while not tomador: + tomador = cadastro.retornar_tomador() + + servico = cadastro.servico() + + servico.__init__() + + lote = cadastro.lote_rps(prestador, tomador, servico, ambiente, configuracao) + + retorno = helpers.enviar_lote(lote.__dict__, ambiente, producao) + + + ambiente.numero_lote += 1 + ambiente.numero_rps += 1 + + if producao: + helpers.salvar_arquivo_json(constants.JSON_AMBIENTE_PRODUCAO, ambiente) + else: + helpers.salvar_arquivo_json(constants.JSON_AMBIENTE_HOMOLOGACAO, ambiente) + + print(retorno) + + +@cli.command() +def listar_cliente(): + with open(constants.JSON_TOMADORES, mode='r') as file: + tomadores = json.load(file) + + for num_doc in tomadores: + tomador = tomadores[num_doc] + print('{0} - {1}'.format(tomador['razao_social'], tomador['numero_documento'])) diff --git a/PyNFSe/cli/constants.py b/PyNFSe/cli/constants.py new file mode 100644 index 0000000..7689c85 --- /dev/null +++ b/PyNFSe/cli/constants.py @@ -0,0 +1,25 @@ +import json +import os + + +def _retorna_parametro(parametro): + try: + with open(JSON_CONFIGURACAO, mode='r') as file: + return json.load(file)[parametro] + except FileNotFoundError: + return None + + + +HOME_FOLDER = os.path.expanduser('~') +DIR_PYNFSE = os.path.join(HOME_FOLDER, '.pynfse') +JSON_PRESTADOR = os.path.join(DIR_PYNFSE, 'prestador.json') +JSON_CONFIGURACAO = os.path.join(DIR_PYNFSE, 'configuracao.json') +JSON_AMBIENTE_PRODUCAO = os.path.join(DIR_PYNFSE, 'ambiente_producao.json') +JSON_AMBIENTE_HOMOLOGACAO = os.path.join(DIR_PYNFSE, 'ambiente_homologacao.json') +JSON_TOMADORES = os.path.join(DIR_PYNFSE, 'clientes.json') +ISS_RETIDO = _retorna_parametro('iss_retido') +CODIGO_MUNICIPIO = _retorna_parametro('codigo_municipio') +ITEM_LISTA = _retorna_parametro('item_lista') +CODIGO_CNAE = _retorna_parametro('codigo_cnae') +ALIQUOTA = _retorna_parametro('aliquota') diff --git a/PyNFSe/cli/helpers.py b/PyNFSe/cli/helpers.py new file mode 100644 index 0000000..ddd3e1d --- /dev/null +++ b/PyNFSe/cli/helpers.py @@ -0,0 +1,41 @@ +import json +import os + +from PyNFSe.nfse.pr.curitiba import NFSeCuritiba + + +def home_folder(): + return os.path.expanduser('~') + + +def criar_diretorio(directory): + path = os.path.join(home_folder(), directory) + + try: + os.mkdir(path) + print('Diretório {} criado'.format(directory)) + except FileExistsError: + print('O diretório {} já existe'.format(directory)) + + +def criar_arquivo_json(filename): + if not os.path.exists(filename): + with open(filename, mode='w') as file: + json.dump({}, file) + print('Arquivo {} criado'.format(filename)) + return True + else: + print('O arquivo {} já existe'.format(filename)) + return False + + +def salvar_arquivo_json(arquivo_json, obj): + with open(arquivo_json, mode='w') as file: + json.dump(obj.__dict__, file, ensure_ascii=False) + + +def enviar_lote(lote_rps, ambiente, producao): + ambiente = ambiente + cliente = NFSeCuritiba(ambiente.certificado, ambiente.senha, producao=producao) + + return cliente.recepcionar_lote_rps(lote_rps) diff --git a/PyNFSe/utils/entidades/__init__.py b/PyNFSe/utils/entidades/__init__.py index 6302666..652c6f1 100755 --- a/PyNFSe/utils/entidades/__init__.py +++ b/PyNFSe/utils/entidades/__init__.py @@ -4,3 +4,4 @@ from .prestador import Prestador from .rps import RPS from .pedido_cancelamento import PedidoCancelamentoNFSe +from .configuracao import Ambiente, Configuracao \ No newline at end of file diff --git a/PyNFSe/utils/entidades/base.py b/PyNFSe/utils/entidades/base.py index 2c8e968..361cdc1 100755 --- a/PyNFSe/utils/entidades/base.py +++ b/PyNFSe/utils/entidades/base.py @@ -1,3 +1,5 @@ +import json + class Entidade(object): def __init__(self, **kwargs): diff --git a/PyNFSe/utils/entidades/configuracao.py b/PyNFSe/utils/entidades/configuracao.py new file mode 100644 index 0000000..afc7ef8 --- /dev/null +++ b/PyNFSe/utils/entidades/configuracao.py @@ -0,0 +1,23 @@ +from decimal import Decimal + +from .base import Entidade + + +class Configuracao(Entidade): + simples = int() + incentivo = int() + regime_especial = int() + natureza_operacao = int() + codigo_municipio = str() + iss_retido = int() + item_lista = str() + codigo_cnae = int() + codigo_tributacao_municipio = str() + aliquota = Decimal() + + +class Ambiente(Entidade): + numero_rps = int() + numero_lote = int() + certificado = str() + senha = str() diff --git a/PyNFSe/utils/entidades/tomador.py b/PyNFSe/utils/entidades/tomador.py index 6620d43..f4de1de 100755 --- a/PyNFSe/utils/entidades/tomador.py +++ b/PyNFSe/utils/entidades/tomador.py @@ -18,4 +18,4 @@ class Tomador(Entidade): cep = str() telefone = str() - email = str() \ No newline at end of file + email = str() diff --git a/pynfse.py b/pynfse.py new file mode 100644 index 0000000..34a73c5 --- /dev/null +++ b/pynfse.py @@ -0,0 +1,5 @@ +from PyNFSe.cli.cli import cli + + +if __name__ == '__main__': + cli() diff --git a/requirements.txt b/requirements.txt index 7c45516..ad3b554 100755 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,5 @@ requests==2.11.1 lxml==3.6.4 PyXB==1.2.5 zeep==1.0.0 -signxml==2.2.3 \ No newline at end of file +signxml==2.2.3 +click==6.7 \ No newline at end of file