Repository: TadaSoftware/PyNFe

Issue:
Gerar o grupo de informações de Cobrança <cob></cobr>
Não estou conseguindo gerar o grupo de informçaões de cobrança da Nota fiscal (Parcelas).

Exemplo do grupo de tags que precisa conter no XML
![image](https://github.com/user-attachments/assets/322eb8fd-a08f-4cba-b142-7484a70135e2)

Já tentei de várias formas mas não consigo gerar as informações do grupo de tags <cobr></cobr>

```python
emitente = Emitente(
            razao_social='EMITENTE TESTE',
            nome_fantasia='TESTE EMITENTE',
            cnpj='99999999999999',           # cnpj apenas números
            codigo_de_regime_tributario='1', # 1 para simples nacional ou 3 para normal
            inscricao_estadual='999999999', # numero de IE da empresa
            inscricao_municipal='',
            cnae_fiscal='9999999',           # cnae apenas números
            endereco_logradouro='RUA A ',
            endereco_numero='121',
            endereco_bairro='ENDEREÇO TESTE',
            endereco_municipio='SALVADOR',
            endereco_uf='BA',
            endereco_cep='42000000',
            endereco_pais=CODIGO_BRASIL
            )
            
            #dados do Cliente:
            cliente = Cliente(
            razao_social='NF-E EMITIDA EM AMBIENTE DE HOMOLOGACAO - SEM VALOR FISCAL',
            tipo_documento = 'CNPJ', #dadosCliente[0]['tipo_documento'], 
            email = 'email@email.com.br', 
            numero_documento = dadosCliente[0]['numero_documento'], 
            indicador_ie = '1',
            inscricao_estadual = '99999999',
            endereco_logradouro = 'ENDERECO TESTE', 
            endereco_numero = '999',  
            endereco_complemento = 'Complemento Endereço',
            endereco_bairro = 'Salvador, 
            endereco_municipio = 'Bahia', 
            endereco_uf = 'BA', 
            endereco_cep = '42000000', 
            endereco_pais = CODIGO_BRASIL,
            endereco_telefone = '7199999999'
            )
            parcelas = retParcelamentoNota(numeroNfe,registroAtual, registroFinal)
    
            dadosNfe = retornaDadosNfe(registroAtual, registroFinal)
    
            # Dados da NFE
            nota_fiscal = NotaFiscal(
            emitente=emitente,
            cliente=cliente,
            uf=uf.upper(),
            natureza_operacao='VENDA', # venda, compra, transferência, devolução, etc
            forma_pagamento=2,  # 0=Pagamento à vista; 1=Pagamento a prazo; 2=Outros.
            tipo_pagamento=14,
            modelo=55,                 # 55=NF-e; 65=NFC-e
            serie='1',
            numero_nf = numeroNfe,           # Número do Documento Fiscal.
            data_emissao=datetime.now(),
            data_saida_entrada=datetime.now(),
            tipo_documento=1,          # 0=entrada; 1=saida
            municipio='2905701',       # Código IBGE do Município 
            tipo_impressao_danfe=1,    # 0=Sem geração de DANFE;1=DANFE normal, Retrato;2=DANFE normal Paisagem;3=DANFE Simplificado;4=DANFE NFC-e;
            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;2=NF-e complementar;3=NF-e de ajuste;4=Devolução de mercadoria.
            processo_emissao='0',      #0=Emissão de NF-e com aplicativo do contribuinte;
            transporte_modalidade_frete=1,
            informacoes_adicionais_interesse_fisco='NF-e Venda Loja Lirios FSA para Armazem Cosméticos',
            totais_tributos_aproximado=Decimal("{:.2f}".format(dadosNfe[0]['totais_tributos_aproximado'])),
            pagamentos=[1,2]
            )
    
            # Preenchendo os Itens da nFE
            qtdItem = 0
            itens = retItensNotaFiscal(registroAtual, registroFinal)
    
            for item in itens:
                nota_fiscal.adicionar_produto_servico(
                    codigo=item['Controle'],         #'2034',                           # id do produto
                    descricao=item['Descricao'],    #'LAPIS PRETO COM APONTADOR COD. 2034 VIVAI  PC/ COM 12',
                    ncm=item['CodIpi'],             #'33049100',
                    #cest='0100100',                # NT2015/003
                    cfop='5102',
                    unidade_comercial=item['unidade'],                          #'UN',
                    ean = item['EAN'] ,          #'7899956862304',
                    ean_tributavel=item['ean_tributavel'],                      #'7899956862304',
                    quantidade_comercial=Decimal("{:.2f}".format(item['Quantidade'])), #Decimal('12'),        # 12 unidades
                    valor_unitario_comercial=Decimal("{:.2f}".format(item['Vlr Unitario'])), # Decimal('9.75'),  # preço unitário
                    valor_total_bruto=Decimal("{:.2f}".format(item['Total'])), #Decimal('117.00'),       # preço total
                    unidade_tributavel=item['unidade'],#'UN',
                    quantidade_tributavel=Decimal("{:.2f}".format(item['Quantidade'])), #Decimal('12'),
                    valor_unitario_tributavel=Decimal("{:.2f}".format(item['Vlr Unitario'])), #Decimal('9.75'),
                    ind_total=1,
                    # numero_pedido='12345',                   # xPed
                    # numero_item='123456',                    # nItemPed
                    icms_modalidade='102',
                    icms_origem=item['icms_origem'],                                    #0,
                    icms_csosn=item['CST'],                                             #'400',
                    pis_modalidade=item['pis_modalidade'],                              #'07',
                    cofins_modalidade=item['cofins_modalidade'],                        #'07',
                    valor_tributos_aprox=Decimal("{:.2f}".format(item['Total']))        #'21.06'
                    )
                qtdItem += 1
         
            # Preenchendo os dados de Resp. Técnico do sistema
            nota_fiscal.adicionar_responsavel_tecnico(
            cnpj='47672512000168',
            contato='Asjsandro',
            email='asjsandro@gmail.com',
            fone='71999081709'
            )

            #Preenchendo a tag de Parcelas 
            nota_fiscal.adicionar_duplicata(
                numero = str(parcelas[0]['Doc/Pedido']).zfill(10),
                data_vencimento = parcelas[0]['Doc/Pedido'],
                valor = Decimal("{:.2f}".format(parcelas[0]['Doc/Pedido']))
            )

            nota_fiscal.adicionar_pagamento(
                t_pag = '15',
                x_pag = 'BOLETO BANCARIO',
                v_pag = Decimal("{:.2f}".format(parcelas[0]['Valor R$'])),
                tp_integra = '2',
                ind_pag = 1
            )
            # Preenchendo a tag de Auroziradores para baixar o XML
            autxml_lista = ['13937073000156',#SEFAZ-BA
                            '47672512000168', #Emitente
                            '92683886500'] #Destinatário
        
            for index, item in enumerate(autxml_lista, 1):
                nota_fiscal.adicionar_autorizados_baixar_xml(CPFCNPJ=item)
        
            # serialização
            serializador = SerializacaoXML(_fonte_dados, homologacao=homologacao)
            nfe = serializador.exportar()

            # assinatura
            a1 = AssinaturaA1(certificado, senha)
            xml = a1.assinar(nfe)
            conSefaz = ComunicacaoSefaz(uf, certificado, senha, homologacao)
            envio = conSefaz.autorizacao(modelo='nfe', nota_fiscal=xml, ind_sinc=0) #retorna uma tupla com o código do retorno (0,1) o numero do recibo e o XML Enviado

             # Obtendo o retorno do envio da NFE para a Sefaz.
            if envio[0] == 0:
                print(f'NF-e: {numeroNfe} Enviada com Sucesso Sucesso!')
                print('Aguardando 20s para consultar a NFe na Sefaz')
                print(f'Numero do Recibo/Protocolo: {envio[1]}')
                
                # Contagem Regressiva de 20s
                for contagem in range(20,0,-1):
                    print(f"\b\b\b{contagem}", end="", flush=True)
                    time.sleep(1)
                print('')

                response = conSefaz.consulta_recibo(modelo='nfe', numero=envio[1]) # nfe ou nfce
                infProt = response.text
                ns = {'ns':NAMESPACE_NFE}
                
                retRecibo = etree.fromstring(response.text.encode('utf-8')) # SEFAZ SP utilizar envio.content
                statusLote = retRecibo[0][0].xpath('ns:retConsReciNFe/ns:cStat', namespaces=ns)[0].text
                motivoLote = retRecibo[0][0].xpath('ns:retConsReciNFe/ns:xMotivo', namespaces=ns)[0].text
                
                if statusLote == '104':
                    print(f'Lote Processado!')
                    print(80 * '-')
                    print('Status Lote: ', statusLote)
                    print('Motivo Lote: ', motivoLote)
                    print(80 * '-')

                    chNFe = retRecibo[0][0].xpath('ns:retConsReciNFe/ns:protNFe/ns:infProt/ns:chNFe', namespaces=ns)[0].text
                    statusNfe = retRecibo[0][0].xpath('ns:retConsReciNFe/ns:protNFe/ns:infProt/ns:cStat', namespaces=ns)[0].text
                    prot_nfe = retRecibo[0][0].xpath('ns:retConsReciNFe/ns:protNFe/ns:infProt/ns:nProt', namespaces=ns)[0].text
                    xMotivo_nfe = retRecibo[0][0].xpath('ns:retConsReciNFe/ns:protNFe/ns:infProt/ns:xMotivo', namespaces=ns)[0].text
                    if statusNfe == '100':
                        print(f'Nota fiscal Autorizada')
                        print(80 * '-')
                        print('Status NFe: ', statusNfe)
                        print('Motivo: ', xMotivo_nfe)
                        print('Protocolo: ', prot_nfe)
                        print('Chave Nfe: ', chNFe)
                        print(80 * '-')
                        infnfe = etree.tostring(envio[2])
                        raiz = etree.Element('nfeProc', xmlns=NAMESPACE_NFE, versao=VERSAO_PADRAO)
                        aut = infProt[infProt.find('<protNFe'):infProt.find('</protNFe>') + 10]
                        raiz.append(etree.fromstring(infnfe))
                        raiz.append(etree.fromstring(aut))
                        xxx = etree.tostring(raiz)
                        xxx = xxx.decode("utf-8")
                        tree = ET.XML(etree.tostring(envio[2]))
                        filename = datetime.now().strftime(f"Nfe-{numeroNfe}_AUTORIZADA_%d%m%Y"+"-"+"%H%M%S")+'.xml'
                        folder = r'./xmls/'
                        with open(folder + filename, "wb") as f:
                            print('Salvando arquivo XML no diretório /xmls/')
                            f.write(str(xxx).encode())
                            print(f'Arquivo: {filename} salvo com Sucesso! - {qtdItem} Itens')
                    else:
                        print(80 * '-')
                        print(f'NFe REJEITADA!!!')
                        print('Status NFe: ', statusNfe)
                        print('Motivo: ', xMotivo_nfe)
                        print('Protocolo: ', prot_nfe)
                        print('Chave Nfe: ', chNFe)
                        print(80 * '-')
                        infnfe = etree.tostring(envio[2])
                        raiz = etree.Element('nfeProc', xmlns=NAMESPACE_NFE, versao=VERSAO_PADRAO)
                        aut = infProt[infProt.find('<protNFe'):infProt.find('</protNFe>') + 10]
                        raiz.append(etree.fromstring(infnfe))
                        raiz.append(etree.fromstring(aut))
                        xxx = etree.tostring(raiz)
                        xxx = xxx.decode("utf-8")
                        tree = ET.XML(etree.tostring(envio[2]))
                        filename = datetime.now().strftime(f"Nfe-{numeroNfe}_REJEITADA_%d%m%Y"+"-"+"%H%M%S")+'.xml'
                        folder = r'./xmls/'
                        with open(folder + filename, "wb") as f:
                            print('Salvando arquivo XML no diretório /xmls/')
                            f.write(str(xxx).encode())
                            print(f'Arquivo: {filename} salvo com Sucesso! - {qtdItem} Itens')
                        (80 * '-')
                else:
                    print(80 * '-')
                    print(f'Nfe: {numeroNfe} NÃO AUTORIZADA!')
                    infnfe = etree.tostring(envio[2])
                    raiz = etree.Element('nfeProc', xmlns=NAMESPACE_NFE, versao=VERSAO_PADRAO)
                    aut = retRecibo[retRecibo.find('<protNFe'):retRecibo.find('</protNFe>') + 10]
                    raiz.append(etree.fromstring(infnfe))
                    raiz.append(etree.fromstring(aut))
                    xxx = etree.tostring(raiz)
                    xxx = xxx.decode("utf-8")
                    tree = ET.XML(etree.tostring(envio[2]))
                    filename = datetime.now().strftime(f"Nfe-{numeroNfe}- NAO_AUTORIZADA_%d%m%Y"+"-"+"%H%M%S")+'.xml'
                    folder = r'./xmls/'
                    with open(folder + filename, "wb") as f:
                        print('Salvando arquivo XML no diretório /xmls/')
                        f.write(str(xxx).encode())
                        print(f'Arquivo: {filename} salvo com Sucesso! - {qtdItem} Itens')
                    print(80 * '-')
            else:
                print('-' * 80)
                print(f'Nfe: {numeroNfe} não processada')
                print('Erro no Envio!!!')
                print(envio[1].text) # resposta
                print('Retorno:')
                print(etree.tostring(envio[2], encoding="unicode")) # nfe
                print('-' * 80)

            registroAtual += registroPorVez
            registroFinal += registroPorVez
            numeroNfe += 1
            input('Press Any key for continue!')
        else:
            print('Notas geradas com sucesso!')
            print('Programa Finalizado!!!!!')
```

Hints from issue comments:
@G3nilson: Também estou com este problema, conseguiu resolver @asjsandro?@leogregianin: É necessário implementar o grupo de informações de cobrança com faturas e duplicatas.@felps-dev: Temos os métodos mas infelizmente ainda não foi implementado a serialização dos grupos no [serializacao.py](https://github.com/TadaSoftware/PyNFe/blob/main/pynfe/processamento/serializacao.py)@leogregianin: Pull request #381 @leogregianin: Informar os dados de Fatura na classe NotaFiscal:
```
     self.notafiscal = NotaFiscal(
            emitente=self.emitente,
            cliente=self.cliente,
            ...
            fatura_numero="12345",
            fatura_valor_original=Decimal("117.00"),
            fatura_valor_desconto=Decimal("0.00"),
            fatura_valor_liquido=Decimal("117.00"),
```

Adicionar as duplicatas na função própria, pois é 0-N:

```
        self.notafiscal.adicionar_duplicata(
            numero='1',
            data_vencimento=datetime.datetime(2025, 1, 27, 12, 0, 0, tzinfo=utc),
            valor=Decimal("39.00"),
        )


```


Solution patch:
diff --git a/pynfe/processamento/serializacao.py b/pynfe/processamento/serializacao.py
index eb43771..5c094d5 100644
--- a/pynfe/processamento/serializacao.py
+++ b/pynfe/processamento/serializacao.py
@@ -1788,6 +1788,34 @@ class SerializacaoXML(Serializacao):
                                     lacres, "nLacre"
                                 ).text = lacre.numero_lacre
 
+        # Cobrança
+        if (nota_fiscal.fatura_numero) or (nota_fiscal.duplicatas and len(nota_fiscal.duplicatas) > 0):
+            cobr = etree.SubElement(raiz, "cobr")
+
+            # Fatura 0-1
+            if nota_fiscal.fatura_numero:
+                fat = etree.SubElement(cobr, "fat")
+                etree.SubElement(fat, "nFat").text = nota_fiscal.fatura_numero
+                etree.SubElement(fat, "vOrig").text = "{:.2f}".format(
+                    nota_fiscal.fatura_valor_original
+                )
+                etree.SubElement(fat, "vDesc").text = "{:.2f}".format(
+                    nota_fiscal.fatura_valor_desconto
+                )
+                etree.SubElement(fat, "vLiq").text = "{:.2f}".format(
+                    nota_fiscal.fatura_valor_liquido
+                )
+
+            # Duplicata 0-N
+            if nota_fiscal.duplicatas and len(nota_fiscal.duplicatas) > 0:
+                for num, item in enumerate(nota_fiscal.duplicatas):
+                    dup = etree.SubElement(cobr, "dup")
+                    etree.SubElement(dup, "nDup").text = item.numero
+                    etree.SubElement(dup, "dVenc").text = item.data_vencimento.strftime(
+                        "%Y-%m-%d"
+                    )
+                    etree.SubElement(dup, "vDup").text = "{:.2f}".format(item.valor)
+
         # Pagamento
         """ Obrigatório o preenchimento do Grupo Informações de Pagamento para NF-e e NFC-e.
         Para as notas com finalidade de Ajuste ou Devolução


Test patch:
diff --git a/tests/test_nfe_serializacao_geral.py b/tests/test_nfe_serializacao_geral.py
index 19312d8..7be591b 100644
--- a/tests/test_nfe_serializacao_geral.py
+++ b/tests/test_nfe_serializacao_geral.py
@@ -113,6 +113,10 @@ class SerializacaoNFeTestCase(unittest.TestCase):
             transporte_modalidade_frete=1,
             informacoes_adicionais_interesse_fisco="Mensagem complementar",
             totais_tributos_aproximado=Decimal("21.06"),
+            fatura_numero="12345",
+            fatura_valor_original=Decimal("117.00"),
+            fatura_valor_desconto=Decimal("0.00"),
+            fatura_valor_liquido=Decimal("117.00"),
         )
 
         self.notafiscal.adicionar_produto_servico(
@@ -163,7 +167,25 @@ class SerializacaoNFeTestCase(unittest.TestCase):
             email="pynfe@pynfe.io",
             fone="11912341234",
         )
-        
+
+        self.notafiscal.adicionar_duplicata(
+            numero='1',
+            data_vencimento=datetime.datetime(2025, 1, 27, 12, 0, 0, tzinfo=utc),
+            valor=Decimal("39.00"),
+        )
+
+        self.notafiscal.adicionar_duplicata(
+            numero='1',
+            data_vencimento=datetime.datetime(2025, 1, 28, 12, 0, 0, tzinfo=utc),
+            valor=Decimal("39.00"),
+        )
+
+        self.notafiscal.adicionar_duplicata(
+            numero='1',
+            data_vencimento=datetime.datetime(2025, 1, 29, 12, 0, 0, tzinfo=utc),
+            valor=Decimal("39.00"),
+        )
+
         self.notafiscal.adicionar_pagamento(
             t_pag="03",
             x_pag="Cartao Credito",
@@ -396,6 +418,68 @@ class SerializacaoNFeTestCase(unittest.TestCase):
         self.assertEqual(chave_1, "12345678901234567890123456789012345678900001")
         self.assertEqual(chave_2, "12345678901234567890123456789012345678900002")
 
+    def fatura_test(self):
+        numero_fatura = self.xml_assinado.xpath("//ns:cobr/ns:fat/ns:nFat", namespaces=self.ns)[
+            0
+        ].text
+        valor_original = self.xml_assinado.xpath("//ns:cobr/ns:fat/ns:vOrig", namespaces=self.ns)[
+            0
+        ].text
+        valor_desconto = self.xml_assinado.xpath("//ns:cobr/ns:fat/ns:vDesc", namespaces=self.ns)[
+            0
+        ].text
+        valor_liquido = self.xml_assinado.xpath("//ns:cobr/ns:fat/ns:vLiq", namespaces=self.ns)[
+            0
+        ].text
+
+        self.assertEqual(numero_fatura, "12345")
+        self.assertEqual(valor_original, "117.00")
+        self.assertEqual(valor_desconto, "0.00")
+        self.assertEqual(valor_liquido, "117.00")
+
+    def duplicatas_test(self):
+        numero_duplicata_1 = self.xml_assinado.xpath(
+            "//ns:cobr/ns:dup/ns:nDup", namespaces=self.ns
+        )[0].text
+        data_vencimento_1 = self.xml_assinado.xpath(
+            "//ns:cobr/ns:dup/ns:dVenc", namespaces=self.ns
+        )[0].text
+        valor_duplicata_1 = self.xml_assinado.xpath(
+            "//ns:cobr/ns:dup/ns:vDup", namespaces=self.ns
+        )[0].text
+
+        numero_duplicata_2 = self.xml_assinado.xpath(
+            "//ns:cobr/ns:dup/ns:nDup", namespaces=self.ns
+        )[1].text
+        data_vencimento_2 = self.xml_assinado.xpath(
+            "//ns:cobr/ns:dup/ns:dVenc", namespaces=self.ns
+        )[1].text
+        valor_duplicata_2 = self.xml_assinado.xpath(
+            "//ns:cobr/ns:dup/ns:vDup", namespaces=self.ns
+        )[1].text
+
+        numero_duplicata_3 = self.xml_assinado.xpath(
+            "//ns:cobr/ns:dup/ns:nDup", namespaces=self.ns
+        )[2].text
+        data_vencimento_3 = self.xml_assinado.xpath(
+            "//ns:cobr/ns:dup/ns:dVenc", namespaces=self.ns
+        )[2].text
+        valor_duplicata_3 = self.xml_assinado.xpath(
+            "//ns:cobr/ns:dup/ns:vDup", namespaces=self.ns
+        )[2].text
+
+        self.assertEqual(numero_duplicata_1, "1")
+        self.assertEqual(data_vencimento_1, "2025-01-27")
+        self.assertEqual(valor_duplicata_1, "39.00")
+
+        self.assertEqual(numero_duplicata_2, "1")
+        self.assertEqual(data_vencimento_2, "2025-01-28")
+        self.assertEqual(valor_duplicata_2, "39.00")
+
+        self.assertEqual(numero_duplicata_3, "1")
+        self.assertEqual(data_vencimento_3, "2025-01-29")
+        self.assertEqual(valor_duplicata_3, "39.00")
+
     def responsavel_tecnico_test(self):
         cnpj = self.xml_assinado.xpath("//ns:infRespTec/ns:CNPJ", namespaces=self.ns)[
             0
@@ -437,6 +521,8 @@ class SerializacaoNFeTestCase(unittest.TestCase):
         self.dados_emitente_test()
         self.dados_destinatario_test()
         self.notas_referenciadas_test()
+        self.fatura_test()
+        self.duplicatas_test()
         self.responsavel_tecnico_test()
         self.digestvalue_da_assinatura_test()