diff --git a/pytrustnfe/Servidores.py b/pytrustnfe/Servidores.py index 01726bc..0ddc8d4 100644 --- a/pytrustnfe/Servidores.py +++ b/pytrustnfe/Servidores.py @@ -374,8 +374,8 @@ UFBA = { UFCE = { NFE_AMBIENTE_PRODUCAO: { 'servidor': 'nfe.sefaz.ce.gov.br', - WS_NFE_AUTORIZACAO: 'nfe2/services/NfeRecepcao2', - WS_NFE_RET_AUTORIZACAO: 'nfe2/services/NfeRetRecepcao2', + WS_NFE_AUTORIZACAO: 'nfe2/services/NfeAutorizacao', + WS_NFE_RET_AUTORIZACAO: 'nfe2/services/NfeRetAutorizacao', WS_NFE_INUTILIZACAO: 'nfe2/services/NfeInutilizacao2', WS_NFE_CONSULTA: 'nfe2/services/NfeConsulta2', WS_NFE_SITUACAO: 'nfe2/services/NfeStatusServico2', @@ -385,8 +385,8 @@ UFCE = { }, NFE_AMBIENTE_HOMOLOGACAO: { 'servidor': 'nfeh.sefaz.ce.gov.br', - WS_NFE_AUTORIZACAO: 'nfe2/services/NfeRecepcao2', - WS_NFE_RET_AUTORIZACAO: 'nfe2/services/NfeRetRecepcao2', + WS_NFE_AUTORIZACAO: 'nfe2/services/NfeAutorizacao', + WS_NFE_RET_AUTORIZACAO: 'nfe2/services/NfeRetAutorizacao', WS_NFE_INUTILIZACAO: 'nfe2/services/NfeInutilizacao2', WS_NFE_CONSULTA: 'nfe2/services/NfeConsulta2', WS_NFE_SITUACAO: 'nfe2/services/NfeStatusServico2', diff --git a/pytrustnfe/client.py b/pytrustnfe/client.py index 03adbd2..e6e67e3 100644 --- a/pytrustnfe/client.py +++ b/pytrustnfe/client.py @@ -45,19 +45,20 @@ class HttpClient(object): def _headers(self, action, send_raw): if send_raw: return { - 'Content-type': 'text/xml; charset=utf-8; action="http://www.portalfiscal.inf.br/nfe/wsdl/%s"' % action, + 'Content-type': 'text/xml; charset=utf-8;', + 'SOAPAction': "http://www.portalfiscal.inf.br/nfe/wsdl/%s" % action, 'Accept': 'application/soap+xml; charset=utf-8', } return { - 'Content-type': 'application/soap+xml; charset=utf-8; action="http://www.portalfiscal.inf.br/nfe/wsdl/%s"' % action, - 'Accept': 'application/soap+xml; charset=utf-8', + 'Content-type': 'application/soap+xml; charset=utf-8;', + 'SOAPAction': 'http://www.portalfiscal.inf.br/nfe/wsdl/%s' % action, } def post_soap(self, xml_soap, cabecalho, send_raw): header = self._headers(cabecalho.soap_action, send_raw) urllib3.disable_warnings(category=InsecureRequestWarning) - res = requests.post(self.url, data=xml_soap, + res = requests.post(self.url, data=xml_soap.encode('utf-8'), cert=(self.cert_path, self.key_path), verify=False, headers=header) return res.text diff --git a/pytrustnfe/nfe/__init__.py b/pytrustnfe/nfe/__init__.py index e5824b0..ec1de27 100644 --- a/pytrustnfe/nfe/__init__.py +++ b/pytrustnfe/nfe/__init__.py @@ -19,19 +19,21 @@ from pytrustnfe.exceptions import NFeValidationException def _build_header(method, **kwargs): action = { - 'NfeAutorizacao': ('NfeAutorizacao', '3.10'), - 'NfeRetAutorizacao': ('NfeRetAutorizacao', '3.10'), - 'NfeConsultaCadastro': ('CadConsultaCadastro2', '2.00'), - 'NfeInutilizacao': ('NfeInutilizacao2', '3.10'), - 'RecepcaoEventoCancelamento': ('RecepcaoEvento', '1.00'), - 'RecepcaoEventoCarta': ('RecepcaoEvento', '1.00'), - 'NFeDistribuicaoDFe': ('NFeDistribuicaoDFe/nfeDistDFeInteresse', - '1.00'), - 'RecepcaoEventoManifesto': ('RecepcaoEvento', '1.00'), + 'NfeAutorizacao': ('NfeAutorizacao', '3.10', 'NfeAutorizacao/nfeAutorizacaoLote'), + 'NfeRetAutorizacao': ('NfeRetAutorizacao', '3.10', 'NfeRetAutorizacao/nfeRetAutorizacaoLote'), + 'NfeConsultaCadastro': ('CadConsultaCadastro2', '2.00', 'CadConsultaCadastro2/consultaCadastro2'), + 'NfeInutilizacao': ('NfeInutilizacao2', '3.10', 'NfeInutilizacao2/nfeInutilizacaoNF2'), + 'RecepcaoEventoCancelamento': ('RecepcaoEvento', '1.00', 'RecepcaoEvento/nfeRecepcaoEvento'), + 'RecepcaoEventoCarta': ('RecepcaoEvento', '1.00', 'RecepcaoEvento/nfeRecepcaoEvento'), + 'NFeDistribuicaoDFe': ('NFeDistribuicaoDFe/nfeDistDFeInteresse', '1.00', 'NFeDistribuicaoDFe/nfeDistDFeInteresse'), + 'RecepcaoEventoManifesto': ('RecepcaoEvento', '1.00', 'RecepcaoEvento/nfeRecepcaoEvento'), + } + vals = { + 'estado': kwargs['estado'], + 'method': action[method][0], + 'soap_action': action[method][2], + 'versao': action[method][1] } - vals = {'estado': kwargs['estado'], - 'soap_action': action[method][0], - 'versao': action[method][1]} return CabecalhoSoap(**vals) diff --git a/pytrustnfe/nfe/comunicacao.py b/pytrustnfe/nfe/comunicacao.py index df6ba53..34c550e 100644 --- a/pytrustnfe/nfe/comunicacao.py +++ b/pytrustnfe/nfe/comunicacao.py @@ -12,9 +12,9 @@ from ..xml import sanitize_response def _soap_xml(body, cabecalho): xml = '' xml += '' - xml += '' + xml += '' xml += '' + cabecalho.estado + '' + cabecalho.versao + '' - xml += '' + xml += '' xml += body xml += '' return xml.rstrip('\n') diff --git a/pytrustnfe/nfe/danfe.py b/pytrustnfe/nfe/danfe.py index 001810a..6561538 100644 --- a/pytrustnfe/nfe/danfe.py +++ b/pytrustnfe/nfe/danfe.py @@ -20,6 +20,9 @@ from reportlab.lib.styles import ParagraphStyle from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase.ttfonts import TTFont +import pytz +from datetime import datetime, timedelta + def chunks(cString, nLen): for start in range(0, len(cString), nLen): @@ -36,10 +39,43 @@ def format_cnpj_cpf(value): return cValue -def getdateUTC(cDateUTC): - cDt = cDateUTC[0:10].split('-') +def getdateByTimezone(cDateUTC, timezone=None): + + ''' + Esse método trata a data recebida de acordo com o timezone do + usuário. O seu retorno é dividido em duas partes: + 1) A data em si; + 2) As horas; + :param cDateUTC: string contendo as informações da data + :param timezone: timezone do usuário do sistema + :return: data e hora convertidos para a timezone do usuário + ''' + + # Aqui cortamos a informação do timezone da string (+03:00) + dt = cDateUTC[0:19] + + # Verificamos se a string está completa (data + hora + timezone) + if timezone and len(cDateUTC) == 25: + + # tz irá conter informações da timezone contida em cDateUTC + tz = cDateUTC[19:25] + tz = int(tz.split(':')[0]) + + dt = datetime.strptime(dt, '%Y-%m-%dT%H:%M:%S') + + # dt agora será convertido para o horario em UTC + dt = dt - timedelta(hours=tz) + + # tzinfo passará a apontar para + dt = pytz.utc.localize(dt) + + # valor de dt é convertido para a timezone do usuário + dt = timezone.normalize(dt) + dt = dt.strftime('%Y-%m-%dT%H:%M:%S') + + cDt = dt[0:10].split('-') cDt.reverse() - return '/'.join(cDt), cDateUTC[11:16] + return '/'.join(cDt), dt[11:16] def format_number(cNumber): @@ -74,7 +110,8 @@ def get_image(path, width=1 * cm): class danfe(object): def __init__(self, sizepage=A4, list_xml=None, recibo=True, - orientation='portrait', logo=None, cce_xml=None): + orientation='portrait', logo=None, cce_xml=None, + timezone=None): path = os.path.join(os.path.dirname(__file__), 'fonts') pdfmetrics.registerFont( @@ -114,8 +151,8 @@ class danfe(object): self.Page = 1 # Calculando total linhas usadas para descrições dos itens - # Com bloco fatura, apenas 29 linhas para itens na primeira folha - nNr_Lin_Pg_1 = 34 if oXML_cobr is None else 30 + # Com bloco fatura, apenas 25 linhas para itens na primeira folha + nNr_Lin_Pg_1 = 30 if oXML_cobr is None else 26 # [ rec_ini , rec_fim , lines , limit_lines ] oPaginator = [[0, 0, 0, nNr_Lin_Pg_1]] el_det = oXML.findall(".//{http://www.portalfiscal.inf.br/nfe}det") @@ -154,13 +191,13 @@ class danfe(object): self.NrPages = len(oPaginator) # Calculando nr. páginas if recibo: - self.recibo_entrega(oXML=oXML) + self.recibo_entrega(oXML=oXML, timezone=timezone) - self.ide_emit(oXML=oXML) - self.destinatario(oXML=oXML) + self.ide_emit(oXML=oXML, timezone=timezone) + self.destinatario(oXML=oXML, timezone=timezone) if oXML_cobr is not None: - self.faturas(oXML=oXML_cobr) + self.faturas(oXML=oXML_cobr, timezone=timezone) self.impostos(oXML=oXML) self.transportes(oXML=oXML) @@ -172,7 +209,7 @@ class danfe(object): # Gera o restante das páginas do XML for oPag in oPaginator[1:]: self.newpage() - self.ide_emit(oXML=oXML) + self.ide_emit(oXML=oXML, timezone=timezone) self.produtos(oXML=oXML, el_det=el_det, oPaginator=oPag, list_desc=list_desc, nHeight=77, list_cod_prod=list_cod_prod) @@ -180,17 +217,19 @@ class danfe(object): self.newpage() if cce_xml: for xml in cce_xml: - self._generate_cce(cce_xml=xml, oXML=oXML) + self._generate_cce(cce_xml=xml, oXML=oXML, timezone=timezone) self.newpage() self.canvas.save() - def ide_emit(self, oXML=None): + def ide_emit(self, oXML=None, timezone=None): elem_infNFe = oXML.find( ".//{http://www.portalfiscal.inf.br/nfe}infNFe") elem_protNFe = oXML.find( ".//{http://www.portalfiscal.inf.br/nfe}protNFe") elem_emit = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}emit") elem_ide = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}ide") + elem_evento = oXML.find( + ".//{http://www.portalfiscal.inf.br/nfe}infEvento") cChave = elem_infNFe.attrib.get('Id')[3:] barcode128 = code128.Code128( @@ -260,7 +299,8 @@ class danfe(object): self.stringcenter(self.nLeft + 116.5 + nW_Rect, self.nlin + 19.5, ' '.join(chunks(cChave, 4))) # Chave self.canvas.setFont('NimbusSanL-Regu', 8) - cDt, cHr = getdateUTC(tagtext(oNode=elem_protNFe, cTag='dhRecbto')) + cDt, cHr = getdateByTimezone( + tagtext(oNode=elem_protNFe, cTag='dhRecbto'), timezone) cProtocolo = tagtext(oNode=elem_protNFe, cTag='nProt') cDt = cProtocolo + ' - ' + cDt + ' ' + cHr nW_Rect = (self.width - self.nLeft - self.nRight - 110) / 2 @@ -281,9 +321,9 @@ class danfe(object): # Razão Social emitente P = Paragraph(tagtext(oNode=elem_emit, cTag='xNome'), styleN) - w, h = P.wrap(55 * mm, 50 * mm) + w, h = P.wrap(55 * mm, 40 * mm) P.drawOn(self.canvas, (self.nLeft + 30) * mm, - (self.height - self.nlin - 12) * mm) + (self.height - self.nlin - ((5*h + 12)/12)) * mm) if self.logo: img = get_image(self.logo, width=2 * cm) @@ -318,9 +358,18 @@ class danfe(object): self.string(self.nLeft + 65, 449, 'SEM VALOR FISCAL') self.canvas.restoreState() + # Cancelado + if tagtext(oNode=elem_evento, cTag='cStat') == '135': + self.canvas.saveState() + self.canvas.rotate(45) + self.canvas.setFont('NimbusSanL-Bold', 60) + self.canvas.setFillColorRGB(1, 0.2, 0.2) + self.string(self.nLeft + 80, 275, 'CANCELADO') + self.canvas.restoreState() + self.nlin += 48 - def destinatario(self, oXML=None): + def destinatario(self, oXML=None, timezone=None): elem_ide = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}ide") elem_dest = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}dest") nMr = self.width - self.nRight @@ -364,9 +413,11 @@ class danfe(object): else: cnpj_cpf = format_cnpj_cpf(tagtext(oNode=elem_dest, cTag='CPF')) self.string(nMr - 69, self.nlin + 7.5, cnpj_cpf) - cDt, cHr = getdateUTC(tagtext(oNode=elem_ide, cTag='dhEmi')) + cDt, cHr = getdateByTimezone(tagtext(oNode=elem_ide, cTag='dhEmi'), + timezone) self.string(nMr - 24, self.nlin + 7.7, cDt + ' ' + cHr) - cDt, cHr = getdateUTC(tagtext(oNode=elem_ide, cTag='dhSaiEnt')) + cDt, cHr = getdateByTimezone( + tagtext(oNode=elem_ide, cTag='dhSaiEnt'), timezone) self.string(nMr - 24, self.nlin + 14.3, cDt + ' ' + cHr) # Dt saída cEnd = tagtext(oNode=elem_dest, cTag='xLgr') + ', ' + tagtext( oNode=elem_dest, cTag='nro') @@ -386,7 +437,7 @@ class danfe(object): self.nlin += 24 # Nr linhas ocupadas pelo bloco - def faturas(self, oXML=None): + def faturas(self, oXML=None, timezone=None): nMr = self.width - self.nRight @@ -419,7 +470,8 @@ class danfe(object): line_iter = iter(oXML[1:10]) # Salta elemt 1 e considera os próximos 9 for oXML_dup in line_iter: - cDt, cHr = getdateUTC(tagtext(oNode=oXML_dup, cTag='dVenc')) + cDt, cHr = getdateByTimezone(tagtext(oNode=oXML_dup, cTag='dVenc'), + timezone) self.string(self.nLeft + nCol + 1, self.nlin + nLin, tagtext(oNode=oXML_dup, cTag='nDup')) self.string(self.nLeft + nCol + 17, self.nlin + nLin, cDt) @@ -747,7 +799,7 @@ obsCont[@xCampo='NomeVendedor']") P.drawOn(self.canvas, (self.nLeft + 1) * mm, altura - h) self.nlin += 36 - def recibo_entrega(self, oXML=None): + def recibo_entrega(self, oXML=None, timezone=None): el_ide = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}ide") el_dest = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}dest") el_total = oXML.find(".//{http://www.portalfiscal.inf.br/nfe}total") @@ -779,7 +831,8 @@ obsCont[@xCampo='NomeVendedor']") self.string(self.width - self.nRight - nW + 2, self.nlin + 14, "SÉRIE %s" % (tagtext(oNode=el_ide, cTag='serie'))) - cDt, cHr = getdateUTC(tagtext(oNode=el_ide, cTag='dhEmi')) + cDt, cHr = getdateByTimezone( + tagtext(oNode=el_ide, cTag='dhEmi'), timezone) cTotal = format_number(tagtext(oNode=el_total, cTag='vNF')) cEnd = tagtext(oNode=el_dest, cTag='xNome') + ' - ' @@ -846,7 +899,7 @@ obsCont[@xCampo='NomeVendedor']") self.oPDF_IO.close() fileObj.write(pdf_out) - def _generate_cce(self, cce_xml=None, oXML=None): + def _generate_cce(self, cce_xml=None, oXML=None, timezone=None): self.canvas.setLineWidth(.2) # labels @@ -885,8 +938,8 @@ obsCont[@xCampo='NomeVendedor']") self.string(82, 24, cnpj) chave_acesso = tagtext(oNode=elem_infNFe, cTag='chNFe') self.string(82, 30, chave_acesso) - data_correcao = getdateUTC(tagtext( - oNode=elem_infNFe, cTag='dhEvento')) + data_correcao = getdateByTimezone(tagtext( + oNode=elem_infNFe, cTag='dhEvento'), timezone) data_correcao = data_correcao[0] + " " + data_correcao[1] self.string(82, 36, data_correcao) cce_id = elem_infNFe.values()[0] diff --git a/pytrustnfe/nfe/templates/RecepcaoEventoCarta.xml b/pytrustnfe/nfe/templates/RecepcaoEventoCarta.xml index 377fb96..ced435c 100644 --- a/pytrustnfe/nfe/templates/RecepcaoEventoCarta.xml +++ b/pytrustnfe/nfe/templates/RecepcaoEventoCarta.xml @@ -12,9 +12,9 @@ {{ nSeqEvento }} 1.00 - Carta de Correção + Carta de Correcao {{ xCorrecao|normalize|escape }} - A Carta de Correção é disciplinada pelo § 1º-A do art. 7º do Convênio S/N, de 15 de dezembro de 1970 e pode ser utilizada para regularização de erro ocorrido na emissão de documento fiscal, desde que o erro não esteja relacionado com: I - as variáveis que determinam o valor do imposto tais como: base de cálculo, alíquota, diferença de preço, quantidade, valor da operação ou da prestação; II - a correção de dados cadastrais que implique mudança do remetente ou do destinatário; III - a data de emissão ou de saída. + A Carta de Correcao e disciplinada pelo paragrafo 1o-A do art. 7o do Convenio S/N, de 15 de dezembro de 1970 e pode ser utilizada para regularizacao de erro ocorrido na emissao de documento fiscal, desde que o erro nao esteja relacionado com: I - as variaveis que determinam o valor do imposto tais como: base de calculo, aliquota, diferenca de preco, quantidade, valor da operacao ou da prestacao; II - a correcao de dados cadastrais que implique mudanca do remetente ou do destinatario; III - a data de emissao ou de saida. diff --git a/pytrustnfe/nfse/bh/__init__.py b/pytrustnfe/nfse/bh/__init__.py new file mode 100644 index 0000000..c5f246b --- /dev/null +++ b/pytrustnfe/nfse/bh/__init__.py @@ -0,0 +1,82 @@ +# © 2018 Danimar Ribeiro, Trustcode +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +import os +from lxml import etree +from requests import Session +from zeep import Client +from zeep.transports import Transport + +from pytrustnfe.certificado import extract_cert_and_key_from_pfx, save_cert_key +from pytrustnfe.xml import render_xml, sanitize_response +from pytrustnfe.nfse.bh.assinatura import Assinatura + + +def _render(certificado, method, **kwargs): + path = os.path.join(os.path.dirname(__file__), 'templates') + xml_send = render_xml(path, '%s.xml' % method, True, **kwargs) + + reference = '' + if method == 'GerarNfse': + reference = 'rps:%s' % kwargs['rps']['numero'] + ref_lote = 'lote%s' % kwargs['rps']['numero_lote'] + elif method == 'CancelarNfse': + reference = 'Cancelamento_NF%s' % kwargs['cancelamento']['numero_nfse'] + + signer = Assinatura(certificado.pfx, certificado.password) + xml_send = signer.assina_xml(xml_send, reference) + xml_send = signer.assina_xml(etree.fromstring(xml_send), ref_lote) + return xml_send.encode('utf-8') + + +def _send(certificado, method, **kwargs): + base_url = '' + if kwargs['ambiente'] == 'producao': + base_url = 'https://bhissdigital.pbh.gov.br/bhiss-ws/nfse?wsdl' + else: + base_url = 'https://bhisshomologa.pbh.gov.br/bhiss-ws/nfse?wsdl' + + xml_send = kwargs["xml"].decode('utf-8') + xml_cabecalho = '\ + \ + 1.00' + + cert, key = extract_cert_and_key_from_pfx( + certificado.pfx, certificado.password) + cert, key = save_cert_key(cert, key) + + session = Session() + session.cert = (cert, key) + session.verify = False + transport = Transport(session=session) + + client = Client(base_url, transport=transport) + + response = client.service[method](xml_cabecalho, xml_send) + + response, obj = sanitize_response(response.encode('utf-8')) + return { + 'sent_xml': str(xml_send), + 'received_xml': str(response), + 'object': obj + } + + +def xml_gerar_nfse(certificado, **kwargs): + return _render(certificado, 'GerarNfse', **kwargs) + + +def gerar_nfse(certificado, **kwargs): + if "xml" not in kwargs: + kwargs['xml'] = xml_gerar_nfse(certificado, **kwargs) + return _send(certificado, 'GerarNfse', **kwargs) + + +def xml_cancelar_nfse(certificado, **kwargs): + return _render(certificado, 'CancelarNfse', **kwargs) + + +def cancelar_nfse(certificado, **kwargs): + if "xml" not in kwargs: + kwargs['xml'] = xml_cancelar_nfse(certificado, **kwargs) + return _send(certificado, 'CancelarNfse', **kwargs) diff --git a/pytrustnfe/nfse/bh/assinatura.py b/pytrustnfe/nfse/bh/assinatura.py new file mode 100644 index 0000000..1831379 --- /dev/null +++ b/pytrustnfe/nfse/bh/assinatura.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# © 2016 Danimar Ribeiro, Trustcode +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +import signxml +from lxml import etree +from pytrustnfe.certificado import extract_cert_and_key_from_pfx +from signxml import XMLSigner + + +class Assinatura(object): + + def __init__(self, arquivo, senha): + self.arquivo = arquivo + self.senha = senha + + def assina_xml(self, xml_element, reference): + cert, key = extract_cert_and_key_from_pfx(self.arquivo, self.senha) + + for element in xml_element.iter("*"): + if element.text is not None and not element.text.strip(): + element.text = None + + signer = XMLSigner( + method=signxml.methods.enveloped, signature_algorithm="rsa-sha1", + digest_algorithm='sha1', + c14n_algorithm='http://www.w3.org/TR/2001/REC-xml-c14n-20010315') + + ns = {} + ns[None] = signer.namespaces['ds'] + signer.namespaces = ns + + ref_uri = ('#%s' % reference) if reference else None + signed_root = signer.sign( + xml_element, key=key.encode(), cert=cert.encode(), + reference_uri=ref_uri) + if reference: + element_signed = signed_root.find(".//*[@Id='%s']" % reference) + signature = signed_root.find(".//*[@URI='#%s']" % reference).getparent().getparent() + + if element_signed is not None and signature is not None: + parent = element_signed.getparent() + parent.append(signature) + return etree.tostring(signed_root, encoding=str) diff --git a/pytrustnfe/nfse/bh/templates/CancelarNfse.xml b/pytrustnfe/nfse/bh/templates/CancelarNfse.xml new file mode 100644 index 0000000..137897e --- /dev/null +++ b/pytrustnfe/nfse/bh/templates/CancelarNfse.xml @@ -0,0 +1,13 @@ + + + + + {{ cancelamento.numero_nfse }} + {{ cancelamento.cnpj_prestador }} + {{ cancelamento.inscricao_municipal }} + {{ cancelamento.cidade }} + + 1 + + + diff --git a/pytrustnfe/nfse/bh/templates/GerarNfse.xml b/pytrustnfe/nfse/bh/templates/GerarNfse.xml new file mode 100644 index 0000000..6b35d4d --- /dev/null +++ b/pytrustnfe/nfse/bh/templates/GerarNfse.xml @@ -0,0 +1,11 @@ + + + {{ rps.numero_lote }} + {{ rps.prestador.cnpj }} + {{ rps.prestador.inscricao_municipal }} + 1 + + {% include 'Rps.xml' %} + + + diff --git a/pytrustnfe/nfse/bh/templates/Rps.xml b/pytrustnfe/nfse/bh/templates/Rps.xml new file mode 100644 index 0000000..0dfd0cf --- /dev/null +++ b/pytrustnfe/nfse/bh/templates/Rps.xml @@ -0,0 +1,91 @@ + + + + {{ rps.numero }} + {{ rps.serie }} + {{ rps.tipo_rps }} + + {{ rps.data_emissao }} + {{ rps.natureza_operacao }} + {{ rps.regime_tributacao }} + {{ rps.optante_simples }} + {{ rps.incentivador_cultural }} + {{ rps.status }} + + {{ rps.numero_substituido }} + {{ rps.serie_substituido }} + {{ rps.tipo_substituido }} + + + + {{ rps.valor_servico }} + {{ rps.valor_deducao }} + {{ rps.valor_pis }} + {{ rps.valor_cofins }} + {{ rps.valor_inss }} + {{ rps.valor_ir }} + {{ rps.valor_csll }} + {{ rps.iss_retido }} + {{ rps.valor_iss }} + {{ rps.valor_iss_retido }} + {{ rps.outras_retencoes }} + {{ rps.base_calculo }} + {{ rps.aliquota_issqn }} + {{ rps.valor_liquido_nfse }} + {{ rps.desconto_incondicionado }} + {{ rps.desconto_condicionado }} + + {{ rps.codigo_servico }} + {{ rps.cnae_servico }} + {{ rps.codigo_tributacao_municipio }} + {{ rps.descricao }} + {{ rps.codigo_municipio }} + + + {{ rps.prestador.cnpj }} + {{ rps.prestador.inscricao_municipal }} + + + + + {% if rps.tomador.cnpj_cpf|length == 14 %} + {{ rps.tomador.cnpj_cpf }} + {% endif %} + {% if rps.tomador.cnpj_cpf|length == 11 %} + {{ rps.tomador.cnpj_cpf }} + {% endif %} + + {{ rps.tomador.inscricao_municipal }} + + {{ rps.tomador.razao_social }} + + {{ rps.tomador.logradouro }} + {{ rps.tomador.numero }} + {{ rps.tomador.complemento }} + {{ rps.tomador.bairro }} + {{ rps.tomador.cidade }} + {{ rps.tomador.uf }} + {{ rps.tomador.cep }} + + + {{ rps.tomador.telefone }} + {{ rps.tomador.email }} + + + {% if rps.intermediario is defined -%} + + {{ rps.intermediario.razao_social }} + + {{ rps.intermediario.cnpj }} + + {{ rps.intermediario.inscricao_municipal }} + + {% endif %} + {% if rps.construcao_civil is defined -%} + + {{ rps.construcao_civil.codigo_obra }} + {{ rps.construcao_civil.art }} + + {% endif %} + + diff --git a/pytrustnfe/nfse/floripa/templates/processar_nota.xml b/pytrustnfe/nfse/floripa/templates/processar_nota.xml index 9fa0aba..4fb97e8 100644 --- a/pytrustnfe/nfse/floripa/templates/processar_nota.xml +++ b/pytrustnfe/nfse/floripa/templates/processar_nota.xml @@ -17,8 +17,9 @@ {% for item in rps.itens_servico -%} {{ item.aliquota }} + {{ item.base_calculo }} {{ item.cst_servico }} - {{ item.descricao|normalize|escape }} + {{ item.name|normalize|escape }} {{ item.cnae }} {{ item.quantidade }} {{ item.valor_total }} diff --git a/pytrustnfe/nfse/ginfes/__init__.py b/pytrustnfe/nfse/ginfes/__init__.py index 80e969b..fb27677 100644 --- a/pytrustnfe/nfse/ginfes/__init__.py +++ b/pytrustnfe/nfse/ginfes/__init__.py @@ -3,9 +3,12 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). import os -import suds +from requests import Session +from zeep import Client +from zeep.transports import Transport +from requests.packages.urllib3 import disable_warnings + from pytrustnfe.xml import render_xml, sanitize_response -from pytrustnfe.client import get_authenticated_client from pytrustnfe.certificado import extract_cert_and_key_from_pfx, save_cert_key from pytrustnfe.nfe.assinatura import Assinatura @@ -33,19 +36,21 @@ def _send(certificado, method, **kwargs): cert, key = extract_cert_and_key_from_pfx( certificado.pfx, certificado.password) cert, key = save_cert_key(cert, key) - client = get_authenticated_client(base_url, cert, key) - try: - xml_send = kwargs['xml'] - header = '3' #noqa - response = getattr(client.service, method)(header, xml_send) - except suds.WebFault as e: - return { - 'sent_xml': xml_send, - 'received_xml': e.fault.faultstring, - 'object': None - } - - response, obj = sanitize_response(response) + + header = '3' #noqa + + disable_warnings() + session = Session() + session.cert = (cert, key) + session.verify = False + transport = Transport(session=session) + + client = Client(base_url, transport=transport) + + xml_send = kwargs['xml'] + response = client.service[method](header, xml_send) + + response, obj = sanitize_response(response.encode('utf-8')) return { 'sent_xml': xml_send, 'received_xml': response, diff --git a/pytrustnfe/nfse/imperial/__init__.py b/pytrustnfe/nfse/imperial/__init__.py index e57091c..c21b085 100644 --- a/pytrustnfe/nfse/imperial/__init__.py +++ b/pytrustnfe/nfse/imperial/__init__.py @@ -27,7 +27,7 @@ def _send(certificado, method, **kwargs): response = client.post_soap(soap, 'NFeaction/AWS_NFE.%s' % method) response, obj = sanitize_response(response.encode('utf-8')) return { - 'sent_xml': xml_send, + 'sent_xml': xml_send.decode(), 'received_xml': response.decode(), 'object': obj } diff --git a/pytrustnfe/utils.py b/pytrustnfe/utils.py index 4eff0a0..b5964ae 100644 --- a/pytrustnfe/utils.py +++ b/pytrustnfe/utils.py @@ -13,6 +13,7 @@ class CabecalhoSoap(object): def __init__(self, **kwargs): self.versao = kwargs.pop('versao', '') self.estado = kwargs.pop('estado', '') + self.method = kwargs.pop('method', '') self.soap_action = kwargs.pop('soap_action', '') @@ -92,7 +93,18 @@ def gerar_nfeproc(envio, recibo): nfe = _find_node(docEnvio, "NFe") protocolo = _find_node(docRecibo, "protNFe") if nfe is None or protocolo is None: - return '' + return b'' root.append(nfe) root.append(protocolo) return ET.tostring(root) + + +def gerar_nfeproc_cancel(nfe_proc, cancelamento): + docEnvio = ET.fromstring(nfe_proc) + docCancel = ET.fromstring(cancelamento) + + ev_cancelamento = _find_node(docCancel, "retEvento") + if ev_cancelamento is None: + return b'' + docEnvio.append(ev_cancelamento) + return ET.tostring(docEnvio) diff --git a/requirements.txt b/requirements.txt index abb0836..63f4786 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,3 +14,5 @@ xmlsec >= 1.3.3 reportlab pytest pytest-cov +pytz +zeep diff --git a/setup.py b/setup.py index c7e626b..c5686a4 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages -VERSION = "0.9.12" +VERSION = "0.9.21" setup( @@ -37,6 +37,7 @@ later (LGPLv2+)', 'nfse/imperial/templates/*xml', 'nfse/floripa/templates/*xml', 'nfse/carioca/templates/*xml', + 'nfse/bh/templates/*xml', 'xml/schemas/*xsd', ]}, url='https://github.com/danimaribeiro/PyTrustNFe', @@ -45,11 +46,14 @@ later (LGPLv2+)', long_description=open('README.md', 'r').read(), install_requires=[ 'Jinja2 >= 2.8', + 'pyOpenSSL >= 16.0.0, < 18', 'signxml >= 2.4.0', 'lxml >= 3.5.0, < 5', 'suds-jurko >= 0.6', 'suds-jurko-requests >= 1.2', - 'reportlab' + 'reportlab', + 'pytz', + 'zeep', ], tests_require=[ 'pytest',