Skip to content

Commit

Permalink
consulta por periodo (emitidas e recebidas)
Browse files Browse the repository at this point in the history
  • Loading branch information
brunovcosta committed Nov 14, 2024
1 parent d748e55 commit e0c08e9
Show file tree
Hide file tree
Showing 9 changed files with 233 additions and 57 deletions.
2 changes: 1 addition & 1 deletion abstra_notas/nfse/sp/sao_paulo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ A integração é feita atraves da [Nota do Milhão](https://notadomilhao.prefei
- [Cancelar nota](/abstra_notas/nfse/sp/sao_paulo/exemplos/cancelamento_nfe.py)
- [Consultar CNPJ](/abstra_notas/nfse/sp/sao_paulo/exemplos/consulta_cnpj.py)
- [Consultar nota emitida (por RPS)](/abstra_notas/nfse/sp/sao_paulo/exemplos/consultar_nota.py)
- Consultar notas emitidas (por lote) (Em breve)
- Consultar notas emitidas (por período) (Em breve)
- Consultar notas emitidas (por lote) (Em breve)
- Consultar notas recebidas (Em breve)
5 changes: 4 additions & 1 deletion abstra_notas/nfse/sp/sao_paulo/cliente.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from .envio_rps import EnvioRPS, RetornoEnvioRps, EnvioLoteRPS, RetornoEnvioRpsLote
from .consulta_cnpj import ConsultaCNPJ, RetornoConsultaCNPJ
from .cancelamento_nfe import CancelamentoNFe, RetornoCancelamentoNFe
from .consulta import ConsultaNFe, RetornoConsulta
from .consulta import ConsultaNFe, RetornoConsulta, ConsultaNFePeriodo


class Cliente:
Expand Down Expand Up @@ -60,6 +60,9 @@ def cancelar_nota(self, pedido: CancelamentoNFe) -> RetornoCancelamentoNFe:
def consultar_nota(self, pedido: ConsultaNFe) -> RetornoConsulta:
return RetornoConsulta.ler_xml(self.executar(pedido))

def consultar_notas_periodo(self, pedido: ConsultaNFePeriodo) -> RetornoConsulta:
return RetornoConsulta.ler_xml(self.executar(pedido))


class ClienteMock(Cliente):
erro: bool
Expand Down
140 changes: 105 additions & 35 deletions abstra_notas/nfse/sp/sao_paulo/consulta.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@
from lxml.etree import ElementBase, fromstring
from datetime import date
from dateutil.parser import parse
from .validacoes import normalizar_inscricao_municipal, normalizar_codigo_verificacao
from abstra_notas.validacoes.data import normalizar_data
from abstra_notas.validacoes.cpfcnpj import normalizar_cpf_ou_cnpj, cpf_ou_cnpj


def find_text(xml: ElementBase, xpath: str) -> Optional[str]:
element = xml.find(xpath)
return element.text if element is not None else None
return element.text.strip() if element is not None and element.text else None


def optional_int(arg: Optional[str]) -> Optional[int]:
Expand All @@ -32,6 +35,10 @@ def optional_bool(arg: Optional[str]) -> Optional[bool]:
return False


def optional_centavos(arg: Optional[str]) -> Optional[int]:
return int(optional_float(arg) * 100) if arg is not None else None


def optional_date(arg: Optional[str]) -> Optional[date]:
return parse(arg).date() if arg is not None else None

Expand All @@ -58,10 +65,10 @@ class RetornoNFe:
opcao_simples: int
codigo_servico: int
aliquota_servicos: float
valor_iss: int
valor_credito: float
iss_retido: bool
discriminacao: str
valor_iss_centavos: int
valor_credito_centavos: int
valor_servicos_centavos: int
valor_deducoes_centavos: Optional[int] = None
valor_pis_centavos: Optional[int] = None
Expand Down Expand Up @@ -142,19 +149,24 @@ def ler_xml(xml: ElementBase):
status_nfe=find_text(xml, ".//StatusNFe"),
tributacao_nfe=find_text(xml, ".//TributacaoNFe"),
opcao_simples=optional_int(find_text(xml, ".//OpcaoSimples")),
valor_servicos_centavos=optional_int(find_text(xml, ".//ValorServicos")),
valor_deducoes_centavos=optional_int(find_text(xml, ".//ValorDeducoes")),
valor_pis_centavos=optional_int(find_text(xml, ".//ValorPIS")),
valor_cofins_centavos=optional_int(find_text(xml, ".//ValorCOFINS")),
valor_inss_centavos=optional_int(find_text(xml, ".//ValorINSS")),
valor_ir_centavos=optional_int(find_text(xml, ".//ValorIR")),
valor_csll_centavos=optional_int(find_text(xml, ".//ValorCSLL")),
valor_servicos_centavos=optional_centavos(
find_text(xml, ".//ValorServicos")
),
valor_deducoes_centavos=optional_centavos(
find_text(xml, ".//ValorDeducoes")
),
valor_pis_centavos=optional_centavos(find_text(xml, ".//ValorPIS")),
valor_cofins_centavos=optional_centavos(find_text(xml, ".//ValorCOFINS")),
valor_inss_centavos=optional_centavos(find_text(xml, ".//ValorINSS")),
valor_ir_centavos=optional_centavos(find_text(xml, ".//ValorIR")),
valor_csll_centavos=optional_centavos(find_text(xml, ".//ValorCSLL")),
valor_iss_centavos=optional_centavos(find_text(xml, ".//ValorISS")),
valor_credito_centavos=optional_centavos(find_text(xml, ".//ValorCredito")),
codigo_servico=optional_int(find_text(xml, ".//CodigoServico")),
aliquota_servicos=optional_float(find_text(xml, ".//AliquotaServicos")),
valor_iss=optional_int(find_text(xml, ".//ValorISS")),
valor_credito=optional_float(find_text(xml, ".//ValorCredito")),
iss_retido=optional_bool(find_text(xml, ".//ISSRetido")),
cpf_cnpj_tomador=find_text(xml, ".//CPFCNPJTomador"),
cpf_cnpj_tomador=find_text(xml, ".//CPFCNPJTomador/CPF")
or find_text(xml, ".//CPFCNPJTomador/CNPJ"),
razao_social_tomador=find_text(xml, ".//RazaoSocialTomador"),
tipo_logradouro_tomador=find_text(xml, ".//EnderecoTomador/TipoLogradouro"),
logradouro_tomador=find_text(xml, ".//EnderecoTomador/Logradouro"),
Expand All @@ -180,6 +192,7 @@ def ler_xml(xml: ElementBase):
lista_nfe = []
for nfe_xml in xml.findall(".//NFe"):
lista_nfe.append(RetornoNFe.ler_xml(nfe_xml))

return RetornoConsulta(lista_nfe=lista_nfe)
else:
raise Erro(
Expand All @@ -201,28 +214,15 @@ class ConsultaNFe(Pedido, Remessa):
"""

def __post_init__(self):
if isinstance(self.chave_nfe_inscricao_prestador, int):
self.chave_nfe_inscricao_prestador = str(self.chave_nfe_inscricao_prestador)
self.chave_nfe_inscricao_prestador = self.chave_nfe_inscricao_prestador.zfill(8)
assert (
len(self.chave_nfe_inscricao_prestador) == 8
), f"A inscrição do prestador deve ter 8 caracteres. Recebido: {self.chave_nfe_inscricao_prestador}"

if isinstance(self.chave_rps_inscricao_prestador, int):
self.chave_rps_inscricao_prestador = str(self.chave_rps_inscricao_prestador)
self.chave_rps_inscricao_prestador = self.chave_rps_inscricao_prestador.zfill(8)
assert (
len(self.chave_rps_inscricao_prestador) == 8
), f"A inscrição do prestador deve ter 8 caracteres. Recebido: {self.chave_rps_inscricao_prestador}"

self.chave_nfe_codigo_verificacao = "".join(
filter(str.isalnum, self.chave_nfe_codigo_verificacao)
).upper()
assert (
self.chave_nfe_codigo_verificacao is None
or isinstance(self.chave_nfe_codigo_verificacao, str)
and len(self.chave_nfe_codigo_verificacao) == 8
), f"O código de verificação deve ter 8 caracteres. Recebido: {self.chave_nfe_codigo_verificacao}"
self.chave_nfe_inscricao_prestador = normalizar_inscricao_municipal(
self.chave_nfe_inscricao_prestador
)
self.chave_rps_inscricao_prestador = normalizar_inscricao_municipal(
self.chave_rps_inscricao_prestador
)
self.chave_nfe_codigo_verificacao = normalizar_codigo_verificacao(
self.chave_nfe_codigo_verificacao, optional=True
)

def gerar_xml(self, assinador: Assinador):
xml = self.template.render(
Expand All @@ -240,3 +240,73 @@ def gerar_xml(self, assinador: Assinador):
@property
def classe_retorno(self):
return RetornoConsulta.__name__


@dataclass
class ConsultaNFePeriodo(Pedido, Remessa):
"""
Consultar notas fiscais eletrônicas emitidas e/ou recebidas em um período.
Ao menos um dos parâmetros `recebidas_por` ou `inscricao_municipal` deve ser informado.
"""

data_inicio: date
data_fim: date

inscricao_municipal: Optional[str] = None
"""
Parâmetro opcional. (Inscrição Municipal, formatação opcional, ex: 12345678)
Caso seja informado, a consulta será feita apenas para as notas fiscais emitidas pelo remetente informado.
"""

recebidas_por: Optional[str] = None
"""
Parâmetro opcional. (CPF ou CNPJ, formatação opcional, ex: 12345678901 ou 12345678000123)
Caso seja informado, a consulta será feita apenas para as notas fiscais recebidas pelo tomador informado.
Caso não seja informado, a consulta será feita para todas as notas fiscais emitidas pelo remetente.
"""

pagina: int = 1

def __post_init__(self):
self.inscricao_municipal = normalizar_inscricao_municipal(
self.inscricao_municipal, optional=True
)
self.data_inicio = normalizar_data(self.data_inicio)
self.data_fim = normalizar_data(self.data_fim)
self.recebidas_por = normalizar_cpf_ou_cnpj(self.recebidas_por, optional=True)
self.remetente = normalizar_cpf_ou_cnpj(self.remetente)
self.pagina = int(self.pagina)

if self.inscricao_municipal is None and self.recebidas_por is None:
raise ValueError(
"Ao menos um dos parâmetros `recebidas_por` ou `inscricao_municipal` deve ser informado."
)
elif self.inscricao_municipal is not None and self.recebidas_por is not None:
raise ValueError(
"Os parâmetros `recebidas_por` e `inscricao_municipal` são mutuamente exclusivos."
)

@property
def recebidas_por_tipo(self):
return cpf_ou_cnpj(self.recebidas_por, optional=True)

def gerar_xml(self, assinador: Assinador):
xml = self.template.render(
remetente=self.remetente,
remetente_tipo=self.remetente_tipo,
inscricao_municipal=self.inscricao_municipal,
data_inicio=self.data_inicio,
data_fim=self.data_fim,
pagina=self.pagina,
recebidas_por=self.recebidas_por,
recebidas_por_tipo=self.recebidas_por_tipo,
)
return fromstring(xml.encode("utf-8"))

@property
def classe_retorno(self):
return RetornoConsulta.__name__
41 changes: 41 additions & 0 deletions abstra_notas/nfse/sp/sao_paulo/consulta_nfe_periodo_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from unittest import TestCase
from .consulta import ConsultaNFePeriodo
from .cliente import ClienteMock
from abstra_notas.assinatura import AssinadorMock
from datetime import date


class ConsultaTest(TestCase):
def test_recebidas(self):
assinador = AssinadorMock()
self.maxDiff = None

pedido = ConsultaNFePeriodo(
remetente="75.551.583/0001-48",
data_fim=date(2015, 1, 28),
data_inicio=date(2015, 1, 28),
pagina=1,
recebidas_por="04.151.050/0001-20",
)

assinador.assinar_xml(pedido.gerar_xml(assinador=assinador))

cliente = ClienteMock()
cliente.consultar_notas_periodo(pedido)

def test_emitidas(self):
assinador = AssinadorMock()
self.maxDiff = None

pedido = ConsultaNFePeriodo(
remetente="75.551.583/0001-48",
data_fim=date(2015, 1, 28),
data_inicio=date(2015, 1, 28),
pagina=1,
inscricao_municipal="12345678",
)

assinador.assinar_xml(pedido.gerar_xml(assinador=assinador))

cliente = ClienteMock()
cliente.consultar_notas_periodo(pedido)
37 changes: 19 additions & 18 deletions abstra_notas/nfse/sp/sao_paulo/consulta_nfe_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ def test_exemplo(self):

cliente = ClienteMock()
resultado = cliente.consultar_nota(pedido)
self.maxDiff = None
self.assertEqual(
resultado,
RetornoConsulta(
Expand Down Expand Up @@ -59,17 +60,17 @@ def test_exemplo(self):
status_nfe="N",
tributacao_nfe="T",
opcao_simples=4,
valor_servicos=20500,
valor_deducoes=5000,
valor_pis=10,
valor_cofins=10,
valor_inss=10,
valor_ir=10,
valor_csll=10,
valor_servicos_centavos=20500_00,
valor_deducoes_centavos=5000_00,
valor_pis_centavos=10_00,
valor_cofins_centavos=10_00,
valor_inss_centavos=10_00,
valor_ir_centavos=10_00,
valor_csll_centavos=10_00,
valor_iss_centavos=0,
valor_credito_centavos=139_50,
codigo_servico=7617,
aliquota_servicos=0.0,
valor_iss=0,
valor_credito=139.5,
iss_retido=False,
cpf_cnpj_tomador="12345678909",
razao_social_tomador="JOAO TESTE",
Expand Down Expand Up @@ -108,17 +109,17 @@ def test_exemplo(self):
status_nfe="N",
tributacao_nfe="T",
opcao_simples=4,
valor_servicos=20501,
valor_deducoes=5000,
valor_pis=10,
valor_cofins=10,
valor_inss=10,
valor_ir=10,
valor_csll=10,
valor_servicos_centavos=20501_00,
valor_deducoes_centavos=5000_00,
valor_pis_centavos=10_00,
valor_cofins_centavos=10_00,
valor_inss_centavos=10_00,
valor_ir_centavos=10_00,
valor_csll_centavos=10_00,
valor_credito_centavos=139_50,
valor_iss_centavos=0,
codigo_servico=7617,
aliquota_servicos=0.0,
valor_iss=0,
valor_credito=139.5,
iss_retido=False,
cpf_cnpj_tomador="12345678909",
razao_social_tomador="JOAO TESTE",
Expand Down
23 changes: 23 additions & 0 deletions abstra_notas/nfse/sp/sao_paulo/templates/ConsultaNFePeriodo.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<p1:PedidoConsultaNFePeriodo xmlns:p1="http://www.prefeitura.sp.gov.br/nfe" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Cabecalho Versao="1">
<CPFCNPJRemetente>
<{{remetente_tipo}}>{{remetente}}</{{remetente_tipo}}>
</CPFCNPJRemetente>
<CPFCNPJ>
{% if recebidas_por %}
<{{recebidas_por_tipo}}>{{ recebidas_por }}</{{recebidas_por_tipo}}>
{% else %}
<{{remetente_tipo}}>{{remetente}}</{{remetente_tipo}}>
{% endif %}
</CPFCNPJ>
{% if inscricao_municipal %}
<Inscricao>
{{inscricao_municipal}}
</Inscricao>
{% endif %}
<dtInicio>{{data_inicio}}</dtInicio>
<dtFim>{{data_fim}}</dtFim>
<NumeroPagina>{{pagina}}</NumeroPagina>
</Cabecalho>
</p1:PedidoConsultaNFePeriodo>
20 changes: 20 additions & 0 deletions abstra_notas/nfse/sp/sao_paulo/validacoes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
def normalizar_inscricao_municipal(inscricao_municipal, optional=False):
if inscricao_municipal is None and optional:
return None
if isinstance(inscricao_municipal, int):
inscricao_municipal = str(inscricao_municipal)
inscricao_municipal = inscricao_municipal.zfill(8)
assert (
len(inscricao_municipal) == 8
), f"A inscrição deve ter 8 caracteres. Recebido: {inscricao_municipal}"
return inscricao_municipal


def normalizar_codigo_verificacao(codigo, optional=False):
if codigo is None and optional:
return None
codigo = "".join(filter(str.isalnum, codigo)).upper()
assert (
codigo is None or isinstance(codigo, str) and len(codigo) == 8
), f"O código de verificação deve ter 8 caracteres. Recebido: {codigo}"
return codigo
8 changes: 6 additions & 2 deletions abstra_notas/validacoes/cpfcnpj.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
from typing import Literal


def cpf_ou_cnpj(valor: str) -> Literal["CPF", "CNPJ"]:
def cpf_ou_cnpj(valor: str, optional=False) -> Literal["CPF", "CNPJ", None]:
if valor is None and optional:
return None
if cpf_valido(valor):
return "CPF"
elif cnpj_valido(valor):
Expand All @@ -12,7 +14,9 @@ def cpf_ou_cnpj(valor: str) -> Literal["CPF", "CNPJ"]:
raise ValueError("Valor não é um CPF ou CNPJ válido.")


def normalizar_cpf_ou_cnpj(valor: str):
def normalizar_cpf_ou_cnpj(valor: str, optional=False) -> str:
if valor is None and optional:
return None
if cpf_valido(valor):
return normalizar_cpf(valor)
elif cnpj_valido(valor):
Expand Down
Loading

0 comments on commit e0c08e9

Please sign in to comment.