From aae4a4524ed537a0b8b12f151888ff66effca5f9 Mon Sep 17 00:00:00 2001 From: Bruno Santos Date: Tue, 24 Jun 2025 23:16:29 -0300 Subject: [PATCH 1/3] Serializacao: Adequecao de campos de quantidade ao TDec_1104v --- pynfe/processamento/serializacao.py | 16 +++++- tests/test_nfce_serializacao.py | 77 +++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 2 deletions(-) diff --git a/pynfe/processamento/serializacao.py b/pynfe/processamento/serializacao.py index 7ab4042e..5bcce1b9 100644 --- a/pynfe/processamento/serializacao.py +++ b/pynfe/processamento/serializacao.py @@ -5,6 +5,7 @@ import warnings from datetime import datetime +from decimal import Decimal import pynfe.utils.xml_writer as xmlw from pynfe.entidades import Manifesto, NotaFiscal @@ -268,6 +269,13 @@ def _serializar_autorizados_baixar_xml( return etree.tostring(raiz, encoding="unicode", pretty_print=True) else: return raiz + + def _formatarQuantidade(self, quantidade: Decimal) -> str: + return ( + str(quantidade.quantize(Decimal("1.0000")).normalize()) + if quantidade % 1 != 0 + else str(int(quantidade)) + ) def _serializar_produto_servico( self, produto_servico, modelo, tag_raiz="det", retorna_string=True @@ -290,7 +298,9 @@ def _serializar_produto_servico( etree.SubElement(prod, "cBenef").text = produto_servico.cbenef etree.SubElement(prod, "CFOP").text = produto_servico.cfop etree.SubElement(prod, "uCom").text = produto_servico.unidade_comercial - etree.SubElement(prod, "qCom").text = str(produto_servico.quantidade_comercial or 0) + etree.SubElement(prod, "qCom").text = self._formatarQuantidade( + produto_servico.quantidade_comercial or 0 + ) etree.SubElement(prod, "vUnCom").text = str("{:.10f}").format( produto_servico.valor_unitario_comercial or 0 ) @@ -308,7 +318,9 @@ def _serializar_produto_servico( ) etree.SubElement(prod, "cEANTrib").text = produto_servico.ean_tributavel etree.SubElement(prod, "uTrib").text = produto_servico.unidade_tributavel - etree.SubElement(prod, "qTrib").text = str(produto_servico.quantidade_tributavel) + etree.SubElement(prod, "qTrib").text = self._formatarQuantidade( + produto_servico.quantidade_tributavel + ) etree.SubElement(prod, "vUnTrib").text = "{:.10f}".format( produto_servico.valor_unitario_tributavel or 0 ) diff --git a/tests/test_nfce_serializacao.py b/tests/test_nfce_serializacao.py index f9109e80..3ab40419 100644 --- a/tests/test_nfce_serializacao.py +++ b/tests/test_nfce_serializacao.py @@ -484,6 +484,83 @@ def test_codigo_numerico_aleatorio(self): self.assertEqual(antigo_codigo, self.notafiscal.codigo_numerico_aleatorio) self.assertEqual(chave_nfce, self.xml[0].attrib["Id"]) + + # Preenche as classes do pynfe + self.emitente = self.preenche_emitente() + self.cliente = self.preenche_destinatario() + self.preenche_notafiscal_produto() + + def test_notafiscal_formatador_de_quantidade(self): + emitente = self.preenche_emitente() + cliente = self.preenche_destinatario() + + utc = datetime.timezone.utc + data_emissao = datetime.datetime(2021, 1, 14, 12, 0, 0, tzinfo=utc) + data_saida_entrada = datetime.datetime(2021, 1, 14, 13, 10, 20, tzinfo=utc) + + notafiscal = NotaFiscal( + emitente, + cliente, + uf="PR", + natureza_operacao="VENDA", + modelo=55, + serie="1", + numero_nf="222", + data_emissao=data_emissao, + data_saida_entrada=data_saida_entrada, + tipo_documento=1, + municipio="4118402", + tipo_impressao_danfe=1, + forma_emissao="1", + cliente_final=1, + indicador_destino=1, + indicador_presencial=1, + finalidade_emissao="1", + processo_emissao="0", + transporte_modalidade_frete=1, + informacoes_adicionais_interesse_fisco="Teste quantidade decimal", + totais_tributos_aproximado=Decimal("1.23"), + valor_troco=Decimal("0.00"), + ) + + notafiscal.adicionar_produto_servico( + codigo="001", + descricao="Produto Decimal", + ncm="12345678", + cfop="5102", + unidade_comercial="UN", + quantidade_comercial=Decimal("1.123456"), + valor_unitario_comercial=Decimal("10.00"), + valor_total_bruto=Decimal("11.23"), + unidade_tributavel="UN", + quantidade_tributavel=Decimal("1.123456"), + valor_unitario_tributavel=Decimal("10.00"), + ean="SEM GTIN", + ean_tributavel="SEM GTIN", + ind_total=1, + icms_modalidade="00", + icms_origem=0, + pis_modalidade="99", + cofins_modalidade="99", + pis_valor_base_calculo=Decimal("0.00"), + pis_aliquota_percentual=Decimal("0.00"), + pis_valor=Decimal("0.00"), + cofins_valor_base_calculo=Decimal("0.00"), + cofins_aliquota_percentual=Decimal("0.00"), + cofins_valor=Decimal("0.00"), + valor_tributos_aprox="1.23", + informacoes_adicionais="Teste de casas decimais", + ) + + notafiscal.adicionar_pagamento(t_pag="01", x_pag="Dinheiro", v_pag=11.23, ind_pag=0) + + xml = self.serializa_nfe() + + qCom = xml.xpath("//ns:det/ns:prod/ns:qCom", namespaces=self.ns)[0].text + qTrib = xml.xpath("//ns:det/ns:prod/ns:qTrib", namespaces=self.ns)[0].text + + self.assertEqual(qCom, "1.1235") + self.assertEqual(qTrib, "1.1235") if __name__ == "__main__": unittest.main() From 1335dbf227e4292a271eedfea814ecc1cdfda2d6 Mon Sep 17 00:00:00 2001 From: Bruno Santos Date: Wed, 18 Feb 2026 12:16:39 -0300 Subject: [PATCH 2/3] =?UTF-8?q?Corre=C3=A7=C3=A3o=20de=20lint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pynfe/processamento/serializacao.py | 2 +- tests/test_nfce_serializacao.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pynfe/processamento/serializacao.py b/pynfe/processamento/serializacao.py index 5bcce1b9..74cafeb1 100644 --- a/pynfe/processamento/serializacao.py +++ b/pynfe/processamento/serializacao.py @@ -269,7 +269,7 @@ def _serializar_autorizados_baixar_xml( return etree.tostring(raiz, encoding="unicode", pretty_print=True) else: return raiz - + def _formatarQuantidade(self, quantidade: Decimal) -> str: return ( str(quantidade.quantize(Decimal("1.0000")).normalize()) diff --git a/tests/test_nfce_serializacao.py b/tests/test_nfce_serializacao.py index 3ab40419..220e385a 100644 --- a/tests/test_nfce_serializacao.py +++ b/tests/test_nfce_serializacao.py @@ -484,7 +484,6 @@ def test_codigo_numerico_aleatorio(self): self.assertEqual(antigo_codigo, self.notafiscal.codigo_numerico_aleatorio) self.assertEqual(chave_nfce, self.xml[0].attrib["Id"]) - # Preenche as classes do pynfe self.emitente = self.preenche_emitente() self.cliente = self.preenche_destinatario() @@ -562,5 +561,6 @@ def test_notafiscal_formatador_de_quantidade(self): self.assertEqual(qCom, "1.1235") self.assertEqual(qTrib, "1.1235") + if __name__ == "__main__": unittest.main() From 03bb2901419d2195e1d3d04ed5c06ebd838e85f2 Mon Sep 17 00:00:00 2001 From: Bruno Santos Date: Wed, 18 Feb 2026 15:40:31 -0300 Subject: [PATCH 3/3] =?UTF-8?q?Teste=20para=20validar=20fun=C3=A7=C3=A3o?= =?UTF-8?q?=20formatar=20quantidade?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_nfce_serializacao.py | 77 --------- ...t_nfce_serializacao_formatar_quantidade.py | 153 ++++++++++++++++++ 2 files changed, 153 insertions(+), 77 deletions(-) create mode 100644 tests/test_nfce_serializacao_formatar_quantidade.py diff --git a/tests/test_nfce_serializacao.py b/tests/test_nfce_serializacao.py index 220e385a..f9109e80 100644 --- a/tests/test_nfce_serializacao.py +++ b/tests/test_nfce_serializacao.py @@ -484,83 +484,6 @@ def test_codigo_numerico_aleatorio(self): self.assertEqual(antigo_codigo, self.notafiscal.codigo_numerico_aleatorio) self.assertEqual(chave_nfce, self.xml[0].attrib["Id"]) - # Preenche as classes do pynfe - self.emitente = self.preenche_emitente() - self.cliente = self.preenche_destinatario() - self.preenche_notafiscal_produto() - - def test_notafiscal_formatador_de_quantidade(self): - emitente = self.preenche_emitente() - cliente = self.preenche_destinatario() - - utc = datetime.timezone.utc - data_emissao = datetime.datetime(2021, 1, 14, 12, 0, 0, tzinfo=utc) - data_saida_entrada = datetime.datetime(2021, 1, 14, 13, 10, 20, tzinfo=utc) - - notafiscal = NotaFiscal( - emitente, - cliente, - uf="PR", - natureza_operacao="VENDA", - modelo=55, - serie="1", - numero_nf="222", - data_emissao=data_emissao, - data_saida_entrada=data_saida_entrada, - tipo_documento=1, - municipio="4118402", - tipo_impressao_danfe=1, - forma_emissao="1", - cliente_final=1, - indicador_destino=1, - indicador_presencial=1, - finalidade_emissao="1", - processo_emissao="0", - transporte_modalidade_frete=1, - informacoes_adicionais_interesse_fisco="Teste quantidade decimal", - totais_tributos_aproximado=Decimal("1.23"), - valor_troco=Decimal("0.00"), - ) - - notafiscal.adicionar_produto_servico( - codigo="001", - descricao="Produto Decimal", - ncm="12345678", - cfop="5102", - unidade_comercial="UN", - quantidade_comercial=Decimal("1.123456"), - valor_unitario_comercial=Decimal("10.00"), - valor_total_bruto=Decimal("11.23"), - unidade_tributavel="UN", - quantidade_tributavel=Decimal("1.123456"), - valor_unitario_tributavel=Decimal("10.00"), - ean="SEM GTIN", - ean_tributavel="SEM GTIN", - ind_total=1, - icms_modalidade="00", - icms_origem=0, - pis_modalidade="99", - cofins_modalidade="99", - pis_valor_base_calculo=Decimal("0.00"), - pis_aliquota_percentual=Decimal("0.00"), - pis_valor=Decimal("0.00"), - cofins_valor_base_calculo=Decimal("0.00"), - cofins_aliquota_percentual=Decimal("0.00"), - cofins_valor=Decimal("0.00"), - valor_tributos_aprox="1.23", - informacoes_adicionais="Teste de casas decimais", - ) - - notafiscal.adicionar_pagamento(t_pag="01", x_pag="Dinheiro", v_pag=11.23, ind_pag=0) - - xml = self.serializa_nfe() - - qCom = xml.xpath("//ns:det/ns:prod/ns:qCom", namespaces=self.ns)[0].text - qTrib = xml.xpath("//ns:det/ns:prod/ns:qTrib", namespaces=self.ns)[0].text - - self.assertEqual(qCom, "1.1235") - self.assertEqual(qTrib, "1.1235") - if __name__ == "__main__": unittest.main() diff --git a/tests/test_nfce_serializacao_formatar_quantidade.py b/tests/test_nfce_serializacao_formatar_quantidade.py new file mode 100644 index 00000000..537ecebe --- /dev/null +++ b/tests/test_nfce_serializacao_formatar_quantidade.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python +# *-* encoding: utf8 *-* + +import datetime +import unittest +from decimal import Decimal + +from pynfe.entidades.emitente import Emitente +from pynfe.entidades.fonte_dados import _fonte_dados +from pynfe.entidades.notafiscal import NotaFiscal +from pynfe.processamento.assinatura import AssinaturaA1 +from pynfe.processamento.serializacao import SerializacaoXML +from pynfe.processamento.validacao import Validacao +from pynfe.utils.flags import ( + CODIGO_BRASIL, + NAMESPACE_NFE, + NAMESPACE_SIG, + XSD_FOLDER_NFE, + XSD_NFE, + XSD_NFE_PROCESSADA, +) + + +class SerializacaoNFeTestCase(unittest.TestCase): + def setUp(self): + self.certificado = "./tests/certificado.pfx" + self.senha = bytes("123456", "utf-8") + self.uf = "pr" + self.homologacao = True + + self.ns = {"ns": NAMESPACE_NFE} + self.ns_sig = {"ns": NAMESPACE_SIG} + + self.validacao = Validacao() + self.xsd_procNFe = self.validacao.get_xsd( + xsd_file=XSD_NFE_PROCESSADA, xsd_folder=XSD_FOLDER_NFE + ) + self.xsd_nfe = self.validacao.get_xsd(xsd_file=XSD_NFE, xsd_folder=XSD_FOLDER_NFE) + + def preenche_emitente(self): + self.emitente = Emitente( + razao_social="NF-E EMITIDA EM AMBIENTE DE HOMOLOGACAO - SEM VALOR FISCAL", + nome_fantasia="Nome Fantasia da Empresa", + cnpj="99999999000199", # cnpj apenas números + codigo_de_regime_tributario="3", # 1 para simples nacional ou 3 para normal + inscricao_estadual="9999999999", # numero de IE da empresa + inscricao_municipal="12345", + cnae_fiscal="9999999", # cnae apenas números + endereco_logradouro="Rua da Paz", + endereco_numero="666", + endereco_bairro="Sossego", + endereco_municipio="Paranavaí", + endereco_uf="PR", + endereco_cep="87704000", + endereco_pais=CODIGO_BRASIL, + ) + return self.emitente + + def preenche_notafiscal_produto(self, quantidade): + utc = datetime.timezone.utc + data_emissao = datetime.datetime(2021, 1, 14, 12, 0, 0, tzinfo=utc) + data_saida_entrada = datetime.datetime(2021, 1, 14, 13, 10, 20, tzinfo=utc) + + self.notafiscal = NotaFiscal( + emitente=self.emitente, + cliente=None, + uf="PR", + natureza_operacao="VENDA", # venda, compra, transferência, devolução, etc + forma_pagamento=0, # 0=Pagamento à vista; 1=Pagamento a prazo; 2=Outros. + modelo=65, # 55=NF-e; 65=NFC-e + serie="1", + numero_nf="111", # Número do Documento Fiscal. + data_emissao=data_emissao, + data_saida_entrada=data_saida_entrada, + tipo_documento=1, # 0=entrada; 1=saida + municipio="4118402", # Código IBGE do Município + tipo_impressao_danfe=1, # 1=DANFE normal + forma_emissao="1", # 1=Emissão normal (não em contingência); + cliente_final=1, # 0=Normal;1=Consumidor final; + indicador_destino=1, + indicador_presencial=1, + finalidade_emissao="1", # 1=NF-e normal + processo_emissao="0", # 0=Emissão de NF-e com aplicativo do contribuinte; + transporte_modalidade_frete=1, + informacoes_adicionais_interesse_fisco="Mensagem complementar", + totais_tributos_aproximado=Decimal("1.01"), + valor_troco=Decimal("3.00"), + ) + + self.notafiscal.adicionar_produto_servico( + codigo="000328", # id do produto + descricao="Produto teste", + ncm="99999999", + # cest='0100100', # NT2015/003 + ean="1234567890121", + cfop="5102", + unidade_comercial="UN", + quantidade_comercial=Decimal(quantidade), + valor_unitario_comercial=Decimal("0.00"), + valor_total_bruto=Decimal("0.00"), + unidade_tributavel="UN", + quantidade_tributavel=Decimal(quantidade), + valor_unitario_tributavel=Decimal("0.00"), + ean_tributavel="SEM GTIN", + ind_total=1, + icms_modalidade="00", + icms_origem=0, + icms_csosn="", + pis_modalidade="51", + cofins_modalidade="51", + pis_valor_base_calculo=Decimal("0.00"), + pis_aliquota_percentual=Decimal("0.00"), + pis_valor=Decimal("0.00"), + cofins_valor_base_calculo=Decimal("0.00"), + cofins_aliquota_percentual=Decimal("0.00"), + cofins_valor=Decimal("0.00"), + valor_tributos_aprox="1.01", + ) + + def serializa_nfe(self): + serializador = SerializacaoXML( + fonte_dados=_fonte_dados, homologacao=self.homologacao, so_cpf=True + ) + return serializador.exportar() + + def assina_xml(self): + a1 = AssinaturaA1(self.certificado, self.senha) + return a1.assinar(self.xml) + + def validacao_com_xsd_do_xml_gerado_sem_processar(self): + self.validacao.validar_etree( + xml_doc=self.xml_assinado, xsd_file=self.xsd_nfe, use_assert=True + ) + + def test_notafiscal_produto_com_vnf_e_vprod_com_duas_casas_decimais(self): + quantidade = 1.123456 + # Preenche as classes do pynfe + self.emitente = self.preenche_emitente() + self.notafiscal = self.preenche_notafiscal_produto(quantidade) + + # Serializa e assina o XML + self.xml = self.serializa_nfe() + self.xml_assinado = self.assina_xml() + + qCom = self.xml_assinado.xpath("//ns:det/ns:prod/ns:qCom", namespaces=self.ns)[0].text + qTrib = self.xml_assinado.xpath("//ns:det/ns:prod/ns:qTrib", namespaces=self.ns)[0].text + + self.assertEqual(qCom, "1.1235") + self.assertEqual(qTrib, "1.1235") + + +if __name__ == "__main__": + unittest.main()