Copies the 6 pure fiscal modules (domains, chave_acesso, tpag, presets, resolver, xml_builder) from sowai-auto's backend/app/modules/fiscal/ into this standalone lib, with only the two mechanical changes required to drop the app.* dependency: import prefix rename, and TaxRule (ORM) -> TaxRuleLike (structural Protocol) in the resolver. No other line changes -- byte-level parity with the auto is the point. Ports the pure test suites (domains, chave_acesso, resolver with a local FakeTaxRule satisfying TaxRuleLike, xml_builder) plus a new test_presets_data covering the PRESETS dict directly (the auto's equivalent test exercises the apply-preset HTTP/DB flow, which isn't part of the extracted pure core). 55 tests pass locally via `uv run pytest`, no Postgres/k8s required.
304 lines
9.2 KiB
Python
304 lines
9.2 KiB
Python
"""1b.1 Task 5: `xml_builder.build_nfe` -- montagem PURA do XML da NF-e 55
|
|
(NT 2025.002 v1.40). Todos os testes montam `DadosEmissao` em memória
|
|
(nenhum banco, nenhum I/O) e inspecionam o objeto `Nfe` resultante
|
|
diretamente OU sua serialização via `xsdata.formats.dataclass.serializers.
|
|
XmlSerializer` (o mesmo serializador que `emissao.py`, Task 6, usará antes
|
|
de assinar)."""
|
|
import uuid
|
|
from decimal import Decimal
|
|
|
|
import pytest
|
|
from xsdata.formats.dataclass.serializers import XmlSerializer
|
|
from xsdata.formats.dataclass.serializers.config import SerializerConfig
|
|
|
|
from sowai_fiscal.resolver import FiscalResult, TributoLinha
|
|
from sowai_fiscal.xml_builder import (
|
|
TEXTO_HOMOLOGACAO,
|
|
DadosEmissao,
|
|
DestinatarioData,
|
|
EmitenteData,
|
|
ItemData,
|
|
PagamentoData,
|
|
build_nfe,
|
|
)
|
|
|
|
_NFE_NAMESPACE = "http://www.portalfiscal.inf.br/nfe"
|
|
|
|
|
|
def _emitente(**overrides) -> EmitenteData:
|
|
base = dict(
|
|
cnpj="12345678000190",
|
|
razao_social="EMPRESA TESTE LTDA",
|
|
nome_fantasia="Empresa Teste",
|
|
ie="1234567890",
|
|
crt="1",
|
|
address_street="Rua Teste",
|
|
address_number="100",
|
|
address_complement=None,
|
|
address_district="Centro",
|
|
address_city="Curitiba",
|
|
address_state="PR",
|
|
address_zip="80000000",
|
|
address_city_ibge_code="4106902",
|
|
)
|
|
base.update(overrides)
|
|
return EmitenteData(**base)
|
|
|
|
|
|
def _destinatario(**overrides) -> DestinatarioData:
|
|
base = dict(
|
|
nome="CLIENTE TESTE LTDA",
|
|
cnpj="99887766000155",
|
|
cpf=None,
|
|
indicador_ie="9",
|
|
address_street="Av Cliente",
|
|
address_number="50",
|
|
address_district="Bairro",
|
|
address_city="Curitiba",
|
|
address_state="PR",
|
|
address_zip="80000000",
|
|
address_city_ibge_code="4106902",
|
|
)
|
|
base.update(overrides)
|
|
return DestinatarioData(**base)
|
|
|
|
|
|
def _tributo(tax_domain: str, *, cst=None, csosn=None, aliquota=None, valor="0.00", base_calc="100.00") -> TributoLinha:
|
|
return TributoLinha(
|
|
tax_domain=tax_domain,
|
|
cst=cst,
|
|
csosn=csosn,
|
|
base_calc=Decimal(base_calc),
|
|
base_calc_percent=Decimal("100"),
|
|
aliquota=Decimal(aliquota) if aliquota is not None else None,
|
|
valor=Decimal(valor),
|
|
mva=None,
|
|
aliquota_st=None,
|
|
fcp_percent=None,
|
|
codigo_beneficio=None,
|
|
rule_id=uuid.uuid4(),
|
|
)
|
|
|
|
|
|
def _fiscal_result(cfop: str, *, csosn="102", cst=None, tributos=None, origem="0") -> FiscalResult:
|
|
return FiscalResult(
|
|
cfop=cfop,
|
|
cst=cst,
|
|
csosn=csosn,
|
|
origem=origem,
|
|
consumidor_final=True,
|
|
indicador_ie="9",
|
|
tributos=tributos if tributos is not None else [
|
|
_tributo("pis", cst="08"),
|
|
_tributo("cofins", cst="08"),
|
|
],
|
|
)
|
|
|
|
|
|
def _item(cfop: str, **overrides) -> ItemData:
|
|
base = dict(
|
|
codigo="PC001",
|
|
descricao="Filtro de oleo",
|
|
ncm="84212300",
|
|
cfop=cfop,
|
|
unidade_comercial="UN",
|
|
unidade_tributavel="UN",
|
|
quantidade=Decimal("2"),
|
|
valor_unitario=Decimal("50.00"),
|
|
fiscal_result=_fiscal_result(cfop),
|
|
)
|
|
base.update(overrides)
|
|
return ItemData(**base)
|
|
|
|
|
|
def _pagamento(**overrides) -> PagamentoData:
|
|
base = dict(tpag="01", valor=Decimal("100.00"))
|
|
base.update(overrides)
|
|
return PagamentoData(**base)
|
|
|
|
|
|
def _dados(**overrides) -> DadosEmissao:
|
|
base = dict(
|
|
emitente=_emitente(),
|
|
itens=[_item("5102")],
|
|
pagamento=_pagamento(),
|
|
ambiente="homologacao",
|
|
chave_acesso="4" * 44,
|
|
numero=1014,
|
|
serie=1,
|
|
cnf="87654321",
|
|
dh_emi="2026-07-16T10:00:00-03:00",
|
|
uf_destino_tipo="interna",
|
|
destinatario=_destinatario(),
|
|
)
|
|
base.update(overrides)
|
|
return DadosEmissao(**base)
|
|
|
|
|
|
def _render(nfe) -> str:
|
|
config = SerializerConfig(xml_declaration=False, indent=None)
|
|
return XmlSerializer(config=config).render(nfe, ns_map={None: _NFE_NAMESPACE})
|
|
|
|
|
|
# --- venda Padrão intra-PR ---------------------------------------------
|
|
|
|
|
|
def test_venda_padrao_intra_estado_cfop_5102_csosn_102_e_origem_do_item():
|
|
dados = _dados(uf_destino_tipo="interna", itens=[_item("5102", fiscal_result=_fiscal_result("5102", csosn="102", origem="1"))])
|
|
|
|
nfe = build_nfe(dados)
|
|
|
|
det = nfe.infNFe.det[0]
|
|
assert det.prod.CFOP == "5102"
|
|
assert det.imposto.ICMS.ICMSSN102.CSOSN == "102"
|
|
assert det.imposto.ICMS.ICMSSN102.orig == "1" # origem SEMPRE do item, nunca de regra
|
|
assert nfe.infNFe.ide.idDest == "1"
|
|
|
|
|
|
def test_totais_batem_com_a_soma_dos_itens_decimal_2_casas():
|
|
itens = [
|
|
_item("5102", quantidade=Decimal("2"), valor_unitario=Decimal("50.00")),
|
|
_item("5102", quantidade=Decimal("3"), valor_unitario=Decimal("10.005")),
|
|
]
|
|
dados = _dados(itens=itens)
|
|
|
|
nfe = build_nfe(dados)
|
|
|
|
# 2*50.00 = 100.00 ; 3*10.005 = 30.015 -> arredonda p/ 30.02 (ROUND_HALF_UP)
|
|
assert nfe.infNFe.total.ICMSTot.vProd == "130.02"
|
|
assert nfe.infNFe.total.ICMSTot.vNF == "130.02"
|
|
# Decimal com 2 casas sempre (nunca "100.0" ou "100")
|
|
assert nfe.infNFe.total.ICMSTot.vProd.count(".") == 1
|
|
assert len(nfe.infNFe.total.ICMSTot.vProd.split(".")[1]) == 2
|
|
|
|
|
|
# --- interestadual --------------------------------------------------------
|
|
|
|
|
|
def test_venda_interestadual_cfop_6102_iddest_2():
|
|
dados = _dados(
|
|
uf_destino_tipo="interestadual",
|
|
itens=[_item("6102", fiscal_result=_fiscal_result("6102", csosn="102"))],
|
|
ind_final="1",
|
|
destinatario=_destinatario(indicador_ie="1", address_state="SP"),
|
|
)
|
|
|
|
nfe = build_nfe(dados)
|
|
|
|
det = nfe.infNFe.det[0]
|
|
assert det.prod.CFOP == "6102"
|
|
assert nfe.infNFe.ide.idDest == "2"
|
|
assert nfe.infNFe.ide.indFinal == "1"
|
|
assert nfe.infNFe.dest.indIEDest == "1"
|
|
|
|
|
|
# --- pagamento -------------------------------------------------------------
|
|
|
|
|
|
def test_pag_detpag_tpag_do_pagamento():
|
|
dados = _dados(pagamento=_pagamento(tpag="03", valor=Decimal("55.50")))
|
|
|
|
nfe = build_nfe(dados)
|
|
|
|
det_pag = nfe.infNFe.pag.detPag[0]
|
|
assert det_pag.tPag == "03"
|
|
assert det_pag.vPag == "55.50"
|
|
|
|
|
|
def test_pag_tpag_17_para_pix():
|
|
dados = _dados(pagamento=_pagamento(tpag="17", valor=Decimal("100.00")))
|
|
|
|
nfe = build_nfe(dados)
|
|
|
|
assert nfe.infNFe.pag.detPag[0].tPag == "17"
|
|
|
|
|
|
# --- homologação -------------------------------------------------------------
|
|
|
|
|
|
def test_homologacao_forca_xnome_do_destinatario_para_texto_oficial():
|
|
dados = _dados(ambiente="homologacao", destinatario=_destinatario(nome="Cliente Real Ltda"))
|
|
|
|
nfe = build_nfe(dados)
|
|
|
|
assert nfe.infNFe.dest.xNome == TEXTO_HOMOLOGACAO
|
|
assert nfe.infNFe.ide.tpAmb == "2"
|
|
|
|
|
|
def test_producao_mantem_xnome_real_do_destinatario():
|
|
dados = _dados(ambiente="producao", destinatario=_destinatario(nome="Cliente Real Ltda"))
|
|
|
|
nfe = build_nfe(dados)
|
|
|
|
assert nfe.infNFe.dest.xNome == "Cliente Real Ltda"
|
|
assert nfe.infNFe.ide.tpAmb == "1"
|
|
|
|
|
|
# --- grupo UB (IBS/CBS) -----------------------------------------------------
|
|
|
|
|
|
def test_grupo_ibscbs_ausente_sem_linhas_ibs_cbs_crt1_2026():
|
|
dados = _dados(itens=[_item("5102", fiscal_result=_fiscal_result("5102"))])
|
|
|
|
nfe = build_nfe(dados)
|
|
|
|
assert nfe.infNFe.det[0].imposto.IBSCBS is None
|
|
|
|
|
|
def test_grupo_ibscbs_presente_com_linhas_ibs_cbs():
|
|
fr = _fiscal_result(
|
|
"5102",
|
|
tributos=[
|
|
_tributo("pis", cst="08"),
|
|
_tributo("cofins", cst="08"),
|
|
_tributo("ibs", aliquota="0.1000", valor="10.00", base_calc="100.00"),
|
|
_tributo("cbs", aliquota="0.9000", valor="90.00", base_calc="100.00"),
|
|
],
|
|
)
|
|
dados = _dados(itens=[_item("5102", fiscal_result=fr)])
|
|
|
|
nfe = build_nfe(dados)
|
|
|
|
ibscbs = nfe.infNFe.det[0].imposto.IBSCBS
|
|
assert ibscbs is not None
|
|
assert ibscbs.gIBSCBS.gIBSUF.vIBSUF == "10.00"
|
|
assert ibscbs.gIBSCBS.gCBS.vCBS == "90.00"
|
|
|
|
|
|
# --- serialização -----------------------------------------------------------
|
|
|
|
|
|
def test_serializacao_gera_xml_bem_formado_com_namespace_correto():
|
|
dados = _dados()
|
|
|
|
nfe = build_nfe(dados)
|
|
xml = _render(nfe)
|
|
|
|
assert xml.startswith(f'<NFe xmlns="{_NFE_NAMESPACE}">')
|
|
assert f'Id="NFe{dados.chave_acesso}"' in xml
|
|
assert "<infNFe" in xml and "</infNFe>" in xml
|
|
# bem-formado: um parser XML real não estoura
|
|
from lxml import etree
|
|
|
|
etree.fromstring(xml.encode("utf-8"))
|
|
|
|
|
|
def test_unsupported_icms_situation_raises_clear_error():
|
|
from sowai_fiscal.xml_builder import UnsupportedIcmsSituationError
|
|
|
|
dados = _dados(itens=[_item("5102", fiscal_result=_fiscal_result("5102", csosn="201"))])
|
|
|
|
with pytest.raises(UnsupportedIcmsSituationError):
|
|
build_nfe(dados)
|
|
|
|
|
|
def test_origem_none_levanta_valueerror_sem_fallback_silencioso():
|
|
"""I3 (review Opus, 2026-07-16): origem None chegando no builder é erro
|
|
de PROGRAMAÇÃO (a validação de completude da emissão barra peça sem
|
|
`icms_origem` com 409 antes) -- jamais um default '0' silencioso, que
|
|
emitiria origem nacional para peça importada (imposto errado)."""
|
|
dados = _dados(itens=[_item("5102", fiscal_result=_fiscal_result("5102", origem=None))])
|
|
|
|
with pytest.raises(ValueError, match="origem"):
|
|
build_nfe(dados)
|