Skip to content

Commit 7e1e6d5

Browse files
authored
Grupo de pagamentos (#332)
* Grupo de pagamentos * Avoid 0 that is not None but False * tipo_pagamento as default None * Use the new method on test_nfce_serializacao
1 parent 52470db commit 7e1e6d5

33 files changed

+460
-50
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,5 @@ target/
7777
db.*
7878

7979
TODO*
80+
81+
venv

pynfe/entidades/base.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ class CampoDeprecated(object):
1010
anterior: str
1111
novo: str
1212
motivo: str
13+
apenas_warning: bool = False
1314

1415

1516
class Entidade(object):
@@ -53,6 +54,12 @@ def __setattr__(self, name, value):
5354
)
5455
setattr(self, campo_deprecado.novo, value)
5556
return
57+
if campo_deprecado.apenas_warning:
58+
warnings.warn(
59+
f"O campo {campo_deprecado.anterior} foi deprecado e será removido em versões futuras. "
60+
f"Motivo: {campo_deprecado.motivo}",
61+
DeprecationWarning,
62+
)
5663
else:
5764
raise AttributeError(
5865
f"O campo {campo_deprecado.anterior} foi deprecado e removido."

pynfe/entidades/notafiscal.py

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,16 @@
1212

1313

1414
class NotaFiscal(Entidade):
15+
# campos deprecados
16+
campos_deprecados = [
17+
CampoDeprecated(
18+
"tipo_pagamento",
19+
novo=None,
20+
motivo="Por favor utilize os grupos de pagamento pela função adicionar_pagamento.",
21+
apenas_warning=True,
22+
),
23+
]
24+
1525
status = NF_STATUS[0]
1626

1727
# Código numérico aleatório que compõe a chave de acesso
@@ -73,7 +83,7 @@ class NotaFiscal(Entidade):
7383
90= Sem pagamento
7484
99=Outros
7585
"""
76-
tipo_pagamento = int()
86+
tipo_pagamento = None
7787

7888
# - Forma de emissao (obrigatorio - seleciona de lista) - NF_FORMAS_EMISSAO
7989
forma_emissao = str()
@@ -381,6 +391,11 @@ class NotaFiscal(Entidade):
381391
# - Processo Referenciado (lista 1 para * / ManyToManyField)
382392
processos_referenciados = None
383393

394+
# - pagamentos
395+
pagamentos = list()
396+
# valor do troco
397+
valor_troco = Decimal()
398+
384399
def __init__(self, *args, **kwargs):
385400
self.autorizados_baixar_xml = []
386401
self.notas_fiscais_referenciadas = []
@@ -390,12 +405,19 @@ def __init__(self, *args, **kwargs):
390405
self.observacoes_contribuinte = []
391406
self.processos_referenciados = []
392407
self.responsavel_tecnico = []
408+
self.pagamentos = []
393409

394410
super(NotaFiscal, self).__init__(*args, **kwargs)
395411

396412
def __str__(self):
397413
return " ".join([str(self.modelo), self.serie, self.numero_nf])
398414

415+
def adicionar_pagamento(self, **kwargs):
416+
"""Adiciona uma instancia de Responsavel Tecnico"""
417+
obj = NotaFiscalPagamentos(**kwargs)
418+
self.pagamentos.append(obj)
419+
return obj
420+
399421
def adicionar_autorizados_baixar_xml(self, **kwargs):
400422
obj = AutorizadosBaixarXML(**kwargs)
401423
self.autorizados_baixar_xml.append(obj)
@@ -551,12 +573,6 @@ def identificador_unico(self):
551573

552574

553575
class NotaFiscalReferenciada(Entidade):
554-
# Campos depreciados
555-
campos_deprecados = [
556-
CampoDeprecated("fcp_percentual", "fcp_aliquota", "Consistencia de nomes"),
557-
CampoDeprecated("fcp_st_percentual", "fcp_st_aliquota", "Consistencia de nomes"),
558-
]
559-
560576
# - Tipo (seleciona de lista) - NF_REFERENCIADA_TIPOS
561577
tipo = str()
562578

@@ -588,6 +604,11 @@ class NotaFiscalReferenciada(Entidade):
588604

589605

590606
class NotaFiscalProduto(Entidade):
607+
# Campos depreciados
608+
campos_deprecados = [
609+
CampoDeprecated("fcp_percentual", "fcp_aliquota", "Consistencia de nomes"),
610+
CampoDeprecated("fcp_st_percentual", "fcp_st_aliquota", "Consistencia de nomes"),
611+
]
591612
# - Dados
592613
# - Codigo (obrigatorio)
593614
codigo = str()
@@ -1210,3 +1231,22 @@ class NotaFiscalResponsavelTecnico(Entidade):
12101231

12111232
class AutorizadosBaixarXML(Entidade):
12121233
CPFCNPJ = str()
1234+
1235+
1236+
class NotaFiscalPagamentos(Entidade):
1237+
# forma de pagamento flag: FORMAS_PAGAMENTO
1238+
t_pag = str()
1239+
# descrição da forma de pagametno
1240+
x_pag = str()
1241+
# valor
1242+
v_pag = Decimal()
1243+
# tipo de integracao: '', '1' integrado, '2' - não integrado
1244+
tp_integra = str()
1245+
# CNPJ da Credenciadora de cartão de crédito e/ou débito
1246+
cnpj = str()
1247+
# Bandeira da operadora de cartão de crédito e/ou débito flag: BANDEIRA_CARTAO
1248+
t_band = int()
1249+
# Número de autorização da operação cartão de crédito e/ou débito
1250+
c_aut = str()
1251+
# Indicador da Forma de Pagamento: 0=à Vista, 1=à Prazo
1252+
ind_pag = int()

pynfe/processamento/serializacao.py

Lines changed: 88 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22
import base64
33
import hashlib
44
import re
5+
import warnings
6+
57
from datetime import datetime
68

9+
import pynfe.utils.xml_writer as xmlw
710
from pynfe.entidades import Manifesto, NotaFiscal
811
from pynfe.utils import (
912
etree,
@@ -1341,6 +1344,71 @@ def _serializar_responsavel_tecnico(
13411344
else:
13421345
return raiz
13431346

1347+
def _serializar_pagamentos_antigo_deprecado(self, tipo_pagamento, finalidade_emissao, totais_icms_total_nota):
1348+
pag = etree.Element('pag')
1349+
detpag = etree.SubElement(pag, "detPag")
1350+
if (
1351+
str(finalidade_emissao) == "3"
1352+
or str(finalidade_emissao) == "4"
1353+
):
1354+
etree.SubElement(detpag, "tPag").text = "90"
1355+
etree.SubElement(detpag, "vPag").text = "{:.2f}".format(0)
1356+
else:
1357+
etree.SubElement(detpag, "tPag").text = str(
1358+
tipo_pagamento
1359+
).zfill(2)
1360+
etree.SubElement(detpag, "vPag").text = "{:.2f}".format(
1361+
totais_icms_total_nota
1362+
)
1363+
if tipo_pagamento == 3 or tipo_pagamento == 4:
1364+
cartao = etree.SubElement(detpag, "card")
1365+
""" Tipo de Integração do processo de pagamento com
1366+
o sistema de automação da empresa:
1367+
1=Pagamento integrado com o sistema de automação da empresa
1368+
2= Pagamento não integrado com o sistema de automação da empresa
1369+
"""
1370+
etree.SubElement(cartao, "tpIntegra").text = "2"
1371+
# etree.SubElement(cartao, 'CNPJ').text = ''
1372+
# # Informar o CNPJ da Credenciadora de cartão de crédito / débito
1373+
# etree.SubElement(cartao, 'tBand').text = ''
1374+
# # 01=Visa 02=Mastercard 03=American Express 04=Sorocred
1375+
# 05=Diners Club 06=Elo 07=Hipercard 08=Aura 09=Caba 99=Outros
1376+
# etree.SubElement(cartao, 'cAut').text = ''
1377+
# # Identifica o número da autorização da transação da operação
1378+
# com cartão de crédito e/ou débito
1379+
# troco
1380+
# etree.SubElement(pag, 'vTroco').text = str('')
1381+
return pag
1382+
1383+
def _serializar_pagamentos(self, pagamentos: list(), finalidade_emissao='', valor_troco = 0.00, retorna_string=True):
1384+
pag = etree.Element('pag')
1385+
if (finalidade_emissao in [3, 4]):
1386+
detpag = etree.SubElement(pag, "detPag")
1387+
etree.SubElement(detpag, "tPag").text = "90"
1388+
etree.SubElement(detpag, "vPag").text = "{:.2f}".format(0)
1389+
else:
1390+
for item in pagamentos:
1391+
det = etree.Element("detPag")
1392+
xmlw.write_txt(det, "indPag", item.ind_pag, False)
1393+
xmlw.write_txt(det, "tPag", item.t_pag, True)
1394+
xmlw.write_txt(det, 'xPag', item.x_pag, False)
1395+
xmlw.write_float(det, 'vPag', item.v_pag, True, 2, 2)
1396+
if item.tp_integra:
1397+
card = etree.SubElement(det, "card")
1398+
xmlw.write_txt(card, "tpIntegra", item.tp_integra, True)
1399+
xmlw.write_txt(card, "CNPJ", item.cnpj, False)
1400+
xmlw.write_txt(card, "tBand", item.t_band, False)
1401+
xmlw.write_txt(card, "cAut", item.c_aut, False)
1402+
pag.append(det)
1403+
1404+
# troco
1405+
xmlw.write_float(pag, 'vTroco', valor_troco, False, 2, 2)
1406+
1407+
if retorna_string:
1408+
return etree.tostring(pag, encoding="unicode", pretty_print=False)
1409+
else:
1410+
return pag
1411+
13441412
def _serializar_nota_fiscal(
13451413
self, nota_fiscal, tag_raiz="infNFe", retorna_string=True
13461414
):
@@ -1710,39 +1778,28 @@ def _serializar_nota_fiscal(
17101778
""" Obrigatório o preenchimento do Grupo Informações de Pagamento para NF-e e NFC-e.
17111779
Para as notas com finalidade de Ajuste ou Devolução
17121780
o campo Forma de Pagamento deve ser preenchido com 90=Sem Pagamento. """
1713-
pag = etree.SubElement(raiz, "pag")
1714-
detpag = etree.SubElement(pag, "detPag")
1715-
if (
1716-
str(nota_fiscal.finalidade_emissao) == "3"
1717-
or str(nota_fiscal.finalidade_emissao) == "4"
1718-
):
1719-
etree.SubElement(detpag, "tPag").text = "90"
1720-
etree.SubElement(detpag, "vPag").text = "{:.2f}".format(0)
1781+
if (nota_fiscal.tipo_pagamento is not None):
1782+
warnings.warn(
1783+
"O campo 'tipo_pagamento' está obsoleto e será removido em versões futuras. "
1784+
"Utilize o campo 'pagamentos' em seu lugar.",
1785+
DeprecationWarning,
1786+
)
1787+
raiz.append(
1788+
self._serializar_pagamentos_antigo_deprecado(
1789+
tipo_pagamento=nota_fiscal.tipo_pagamento,
1790+
finalidade_emissao=nota_fiscal.finalidade_emissao,
1791+
totais_icms_total_nota=nota_fiscal.totais_icms_total_nota,
1792+
)
1793+
)
17211794
else:
1722-
etree.SubElement(detpag, "tPag").text = str(
1723-
nota_fiscal.tipo_pagamento
1724-
).zfill(2)
1725-
etree.SubElement(detpag, "vPag").text = "{:.2f}".format(
1726-
nota_fiscal.totais_icms_total_nota
1795+
raiz.append(
1796+
self._serializar_pagamentos(
1797+
pagamentos=nota_fiscal.pagamentos,
1798+
finalidade_emissao=nota_fiscal.finalidade_emissao,
1799+
valor_troco=nota_fiscal.valor_troco,
1800+
retorna_string=False
1801+
)
17271802
)
1728-
if nota_fiscal.tipo_pagamento == 3 or nota_fiscal.tipo_pagamento == 4:
1729-
cartao = etree.SubElement(detpag, "card")
1730-
""" Tipo de Integração do processo de pagamento com
1731-
o sistema de automação da empresa:
1732-
1=Pagamento integrado com o sistema de automação da empresa
1733-
2= Pagamento não integrado com o sistema de automação da empresa
1734-
"""
1735-
etree.SubElement(cartao, "tpIntegra").text = "2"
1736-
# etree.SubElement(cartao, 'CNPJ').text = ''
1737-
# # Informar o CNPJ da Credenciadora de cartão de crédito / débito
1738-
# etree.SubElement(cartao, 'tBand').text = ''
1739-
# # 01=Visa 02=Mastercard 03=American Express 04=Sorocred
1740-
# 05=Diners Club 06=Elo 07=Hipercard 08=Aura 09=Caba 99=Outros
1741-
# etree.SubElement(cartao, 'cAut').text = ''
1742-
# # Identifica o número da autorização da transação da operação
1743-
# com cartão de crédito e/ou débito
1744-
# troco
1745-
# etree.SubElement(pag, 'vTroco').text = str('')
17461803

17471804
# Informações adicionais
17481805
if (

pynfe/utils/__init__.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import os
55
from unicodedata import normalize
66
from signxml import XMLSigner
7+
from typing import Literal
8+
from decimal import Decimal
79

810
try:
911
from lxml import etree # noqa: F401
@@ -178,3 +180,67 @@ def __init__(self, method, signature_algorithm, digest_algorithm, c14n_algorithm
178180

179181
def check_deprecated_methods(self):
180182
pass
183+
184+
def is_empty(value):
185+
"""
186+
Verifica se um valor está vazio.
187+
188+
Parameters:
189+
- value: O valor a ser verificado.
190+
191+
Returns:
192+
- True se o valor estiver vazio, False caso contrário.
193+
"""
194+
if value is None:
195+
return True
196+
elif isinstance(value, (int, float, Decimal)) and value == Decimal(0):
197+
# Verifica se o valor numérico é igual a zero.
198+
return True
199+
elif isinstance(value, str) and not value.strip():
200+
# Verifica se a string está vazia ou contém apenas espaços em branco.
201+
return True
202+
elif isinstance(value, (list, tuple, dict)) and not value:
203+
# Verifica se a lista, tupla ou dicionário está vazio.
204+
return True
205+
else:
206+
return False
207+
208+
209+
def truncar_valor(float_number: float, decimal_places: int, suprimir_zeros: bool = False):
210+
multiplier = 10**decimal_places
211+
result = str(int(float_number * multiplier) / multiplier)
212+
if suprimir_zeros:
213+
result = result.rstrip("0").rstrip(".")
214+
return result
215+
216+
217+
def arredondar_valor(value: float, decimal_places: int, suprimir_zeros: bool = False):
218+
f = f"%.{decimal_places}f"
219+
result = f % value
220+
if suprimir_zeros:
221+
result = result.rstrip("0").rstrip(".")
222+
return result
223+
224+
225+
def ajustar_valor(
226+
value: float, decimal_places: int = 2, min_decimal_places: int = 2, tipo: Literal["ROUND", "TRUNC"] = "ROUND", decimal_separator: str = "."
227+
):
228+
value = 0 if value is None else value
229+
230+
formated_value: str = "0"
231+
supress_zeros = min_decimal_places < decimal_places
232+
233+
if tipo == "ROUND":
234+
formated_value = arredondar_valor(value, decimal_places, supress_zeros)
235+
else:
236+
formated_value = truncar_valor(value, decimal_places, supress_zeros)
237+
238+
pi, sep, dec = list(formated_value.partition("."))
239+
240+
# preenche com zeros a direita até a quantidade minima
241+
if min_decimal_places:
242+
dec = dec.ljust(min_decimal_places, "0")
243+
# se não tem decimais não haverá separator
244+
sep = decimal_separator if dec else ""
245+
246+
return f"{pi}{sep}{dec}".replace(".", decimal_separator)

0 commit comments

Comments
 (0)