"""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'') assert f'Id="NFe{dados.chave_acesso}"' in xml assert "" 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)