Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Serialização do grupo de pagamentos #318

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,5 @@ target/
db.*

TODO*

venv
50 changes: 30 additions & 20 deletions pynfe/entidades/notafiscal.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,26 +55,6 @@ class NotaFiscal(Entidade):
# Removido na NF-e 4.00
# forma_pagamento = int()

# - Tipo de 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 o campo Forma de Pagamento
deve ser preenchido com 90=Sem Pagamento.
01=Dinheiro
02=Cheque
03=Cartão de Crédito
04=Cartão de Débito
05=Crédito Loja
10=Vale Alimentação
11=Vale Refeição
12=Vale Presente
13=Vale Combustível
14=Duplicata Mercantil
90= Sem pagamento
99=Outros
"""
tipo_pagamento = int()

# - Forma de emissao (obrigatorio - seleciona de lista) - NF_FORMAS_EMISSAO
forma_emissao = str()

Expand Down Expand Up @@ -381,6 +361,11 @@ class NotaFiscal(Entidade):
# - Processo Referenciado (lista 1 para * / ManyToManyField)
processos_referenciados = None

# - pagamentos
pagamentos = list()
# valor do troco
valor_troco = Decimal()

def __init__(self, *args, **kwargs):
self.autorizados_baixar_xml = []
self.notas_fiscais_referenciadas = []
Expand All @@ -390,12 +375,19 @@ def __init__(self, *args, **kwargs):
self.observacoes_contribuinte = []
self.processos_referenciados = []
self.responsavel_tecnico = []
self.pagamentos = []

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

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

def adicionar_pagamento(self, **kwargs):
"""Adiciona uma instancia de Responsavel Tecnico"""
obj = NotaFiscalPagamentos(**kwargs)
self.pagamentos.append(obj)
return obj

def adicionar_autorizados_baixar_xml(self, **kwargs):
obj = AutorizadosBaixarXML(**kwargs)
self.autorizados_baixar_xml.append(obj)
Expand Down Expand Up @@ -1165,3 +1157,21 @@ class NotaFiscalResponsavelTecnico(Entidade):

class AutorizadosBaixarXML(Entidade):
CPFCNPJ = str()

class NotaFiscalPagamentos(Entidade):
# forma de pagamento flag: FORMAS_PAGAMENTO
t_pag = str()
# descrição da forma de pagametno
x_pag = str()
# valor
v_pag = Decimal()
# tipo de integracao: '', '1' integrado, '2' - não integrado
tp_integra = str()
# CNPJ da Credenciadora de cartão de crédito e/ou débito
cnpj = str()
# Bandeira da operadora de cartão de crédito e/ou débito flag: BANDEIRA_CARTAO
t_band = int()
# Número de autorização da operação cartão de crédito e/ou débito
c_aut = str()
# Indicador da Forma de Pagamento: 0=à Vista, 1=à Prazo
ind_pag = int()
71 changes: 38 additions & 33 deletions pynfe/processamento/serializacao.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
VERSAO_QRCODE,
)
from pynfe.utils.webservices import MDFE, NFCE
import pynfe.utils.xml_writer as xmlw


class Serializacao(object):
Expand Down Expand Up @@ -1330,6 +1331,35 @@ def _serializar_responsavel_tecnico(
else:
return raiz

def _serializar_pagamentos(self, pagamentos: list(), finalidade_emissao='', valor_troco = 0.00, retorna_string=True):
pag = etree.Element('pag')
if (finalidade_emissao in [3, 4]):
detpag = etree.SubElement(pag, "detPag")
etree.SubElement(detpag, "tPag").text = "90"
etree.SubElement(detpag, "vPag").text = "{:.2f}".format(0)
else:
for item in pagamentos:
det = etree.Element("detPag")
xmlw.write_txt(det, "indPag", item.ind_pag, False)
xmlw.write_txt(det, "tPag", item.t_pag, True)
xmlw.write_txt(det, 'xPag', item.x_pag, False)
xmlw.write_float(det, 'vPag', item.v_pag, True, 2, 2)
if item.tp_integra:
card = etree.SubElement(det, "card")
xmlw.write_txt(card, "tpIntegra", item.tp_integra, True)
xmlw.write_txt(card, "CNPJ", item.cnpj, False)
xmlw.write_txt(card, "tBand", item.t_band, False)
xmlw.write_txt(card, "cAut", item.c_aut, False)
pag.append(det)

# troco
xmlw.write_float(pag, 'vTroco', valor_troco, False, 2, 2)

if retorna_string:
return etree.tostring(pag, encoding="unicode", pretty_print=False)
else:
return pag

def _serializar_nota_fiscal(
self, nota_fiscal, tag_raiz="infNFe", retorna_string=True
):
Expand Down Expand Up @@ -1699,39 +1729,14 @@ def _serializar_nota_fiscal(
""" 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
o campo Forma de Pagamento deve ser preenchido com 90=Sem Pagamento. """
pag = etree.SubElement(raiz, "pag")
detpag = etree.SubElement(pag, "detPag")
if (
str(nota_fiscal.finalidade_emissao) == "3"
or str(nota_fiscal.finalidade_emissao) == "4"
):
etree.SubElement(detpag, "tPag").text = "90"
etree.SubElement(detpag, "vPag").text = "{:.2f}".format(0)
else:
etree.SubElement(detpag, "tPag").text = str(
nota_fiscal.tipo_pagamento
).zfill(2)
etree.SubElement(detpag, "vPag").text = "{:.2f}".format(
nota_fiscal.totais_icms_total_nota
)
if nota_fiscal.tipo_pagamento == 3 or nota_fiscal.tipo_pagamento == 4:
cartao = etree.SubElement(detpag, "card")
""" Tipo de Integração do processo de pagamento com
o sistema de automação da empresa:
1=Pagamento integrado com o sistema de automação da empresa
2= Pagamento não integrado com o sistema de automação da empresa
"""
etree.SubElement(cartao, "tpIntegra").text = "2"
# etree.SubElement(cartao, 'CNPJ').text = ''
# # Informar o CNPJ da Credenciadora de cartão de crédito / débito
# etree.SubElement(cartao, 'tBand').text = ''
# # 01=Visa 02=Mastercard 03=American Express 04=Sorocred
# 05=Diners Club 06=Elo 07=Hipercard 08=Aura 09=Caba 99=Outros
# etree.SubElement(cartao, 'cAut').text = ''
# # Identifica o número da autorização da transação da operação
# com cartão de crédito e/ou débito
# troco
# etree.SubElement(pag, 'vTroco').text = str('')
Comment on lines -1702 to -1734
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Teria como fazer essa alteração não ser uma breaking change? Como verificar se existe o tipo_pagamento preenchido, garantindo que todos que utilizam atualmente a PyNFE possam migrar gradativamente.
De preferencia, pode também botar um warning no console avisando que usar o tipo_pagamento ficará depreciado e que usaremos somente o adicionar_pagamento.
Dessa forma também nos testes devemos ter por enquanto uma validação que garanta que a forma antiga e nova funcionam.

raiz.append(
self._serializar_pagamentos(
pagamentos=nota_fiscal.pagamentos,
finalidade_emissao=nota_fiscal.finalidade_emissao,
valor_troco=nota_fiscal.valor_troco,
retorna_string=False
)
)

# Informações adicionais
if (
Expand Down
66 changes: 66 additions & 0 deletions pynfe/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import os
from unicodedata import normalize
from signxml import XMLSigner
from typing import Literal
from decimal import Decimal

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

def check_deprecated_methods(self):
pass

def is_empty(value):
"""
Verifica se um valor está vazio.

Parameters:
- value: O valor a ser verificado.

Returns:
- True se o valor estiver vazio, False caso contrário.
"""
if value is None:
return True
elif isinstance(value, (int, float, Decimal)) and value == Decimal(0):
# Verifica se o valor numérico é igual a zero.
return True
elif isinstance(value, str) and not value.strip():
# Verifica se a string está vazia ou contém apenas espaços em branco.
return True
elif isinstance(value, (list, tuple, dict)) and not value:
# Verifica se a lista, tupla ou dicionário está vazio.
return True
else:
return False


def truncar_valor(float_number: float, decimal_places: int, suprimir_zeros: bool = False):
multiplier = 10**decimal_places
result = str(int(float_number * multiplier) / multiplier)
if suprimir_zeros:
result = result.rstrip("0").rstrip(".")
return result


def arredondar_valor(value: float, decimal_places: int, suprimir_zeros: bool = False):
f = f"%.{decimal_places}f"
result = f % value
if suprimir_zeros:
result = result.rstrip("0").rstrip(".")
return result


def ajustar_valor(
value: float, decimal_places: int = 2, min_decimal_places: int = 2, tipo: Literal["ROUND", "TRUNC"] = "ROUND", decimal_separator: str = "."
):
value = 0 if value is None else value

formated_value: str = "0"
supress_zeros = min_decimal_places < decimal_places

if tipo == "ROUND":
formated_value = arredondar_valor(value, decimal_places, supress_zeros)
else:
formated_value = truncar_valor(value, decimal_places, supress_zeros)

pi, sep, dec = list(formated_value.partition("."))

# preenche com zeros a direita até a quantidade minima
if min_decimal_places:
dec = dec.ljust(min_decimal_places, "0")
# se não tem decimais não haverá separator
sep = decimal_separator if dec else ""

return f"{pi}{sep}{dec}".replace(".", decimal_separator)
48 changes: 48 additions & 0 deletions pynfe/utils/flags.py
Original file line number Diff line number Diff line change
Expand Up @@ -601,3 +601,51 @@
"AN": "91",
"EX": "99",
}

BANDEIRA_CARTAO = (
("01", "Visa"),
("02", "MasterCard"),
("03", "AmericanExpress"),
("04", "Sorocred"),
("05", "DinersClub"),
("06", "Elo"),
("07", "Hipercard"),
("08", "Aura"),
("09", "Cabal"),
("10", "Alelo"),
("11", "BanesCard"),
("12", "CalCard"),
("13", "Credz"),
("14", "Discover"),
("15", "GoodCard"),
("16", "GrenCard"),
("17", "Hiper"),
("18", "JcB"),
("19", "Mais"),
("20", "MaxVan"),
("21", "Policard"),
("22", "RedeCompras"),
("23", "Sodexo"),
("24", "ValeCard"),
("25", "Verocheque"),
("26", "VR"),
("27", "Ticket"),
("99", "Outros"),
)

FORMAS_PAGAMENTO = (
("01", "Dinheiro"),
("02", "Cheque"),
("03", "Cartao Credito"),
("04", "Cartao Debito"),
("05", "Credito Loja"),
("10", "Vale Alimentacao"),
("11", "Vale Refeicao"),
("12", "Vale Presente"),
("13", "Vale Combustivel"),
("14", "Duplicata Mercantil"),
("15", "Boleto Bancario"),
("17", "Pagamento Instantaneo"),
("90", "Sem Pagamento"),
("99", "Outro"),
)
82 changes: 82 additions & 0 deletions pynfe/utils/xml_writer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
from lxml import etree
from decimal import Decimal
from . import ajustar_valor, is_empty
from typing import Literal, Any

__MIN_LEN_ERROR = 'Tamanho do campo {} é menor que o mímino permitido "{}"'
__MAX_LEN_ERROR = 'Tamanho do campo {} é maior que o mímino permitido "{}"'

def write_txt(root: etree.ElementTree, tag_name, value: str, required: bool, min_len=0, max_len=0):
"""
:param root: XML root
:param tag_name: Nome da TAG a ser escrita (str)
:param value: Valor a ser escrito na TAG (str)
:param min_len: comprimento minimo
:param max-len: comprimento maximo
:param required: Se `True` e esta vazio, escreve a TAG vazia, do contrario não precisa gerar a TAG
"""
tag_value = str(value).strip()

# retorna sem gerar nada se a TAG não é obrigatoria
if is_empty(tag_value) and not required:
return

if len(tag_value) < min_len:
raise Exception(__MIN_LEN_ERROR.format(len(tag_value), min_len))
if max_len > 0 and len(tag_value) > max_len:
raise Exception(__MAX_LEN_ERROR.format(len(tag_value), max_len))

etree.SubElement(root, tag_name).text = tag_value

def write_float(root: etree.ElementTree, tag_name: str, value: Decimal, required: bool, decimal_places = 2, min_decimals = 0, iat: Literal["ROUND", "TRUNC"]="ROUND"):
"""
:param root: XML root
:param tag_name: Nome da TAG a ser escrita (str)
:param value: Valor a ser escrito na TAG (str)
:param decimal_places: casas decimais
:param min_decimals: numero minimo de casas decimais
:param required: Se `True` e esta vazio, escreve a TAG vazia, do contrario não precisa gerar a TAG (considera 0 como vazio)
:param iat: indice de arredondamento/truncamento (default: ROUND)
"""

# retorna sem gerar nada se a TAG não é obrigatoria
if is_empty(Decimal(value or '0')):
if not required:
return
raise Exception(f"{tag_name} - Valor requerido e não informado")

tag_value = ajustar_valor(
value=value.__float__(),
decimal_places=decimal_places,
min_decimal_places=min_decimals,
tipo=iat
)

etree.SubElement(root, tag_name).text = tag_value

def write_int(root: etree.ElementTree, tag_name: str, value: int, required: bool):
"""
:param root: XML root
:param tag_name: Nome da TAG a ser escrita (str)
:param value: Valor a ser escrito na TAG (str)
:param required: Se `True` e esta vazio, escreve a TAG vazia, do contrario não precisa gerar a TAG (considera 0 como vazio)
:param zero_is_empty: se `True` e o valor for zero será tratado como vazio
"""

# retorna sem gerar nada se a TAG não é obrigatoria
if is_empty(value):
if not required:
return
raise Exception(f"{tag_name} - Valor requerido e não informado")

etree.SubElement(root, tag_name).text = int(value)


def write_tag(root: etree.ElementTree, tag_name: str, value: Any, required: bool):
# retorna sem gerar nada se a TAG não é obrigatoria
if is_empty(value):
if not required:
return
raise Exception(f"{tag_name} - Valor requerido e não informado")

etree.SubElement(root, tag_name).text = str(value)
Loading
Loading