diff --git a/l10n_br_nfe/models/document.py b/l10n_br_nfe/models/document.py index f7d961939647..e8e696d023fa 100644 --- a/l10n_br_nfe/models/document.py +++ b/l10n_br_nfe/models/document.py @@ -6,6 +6,7 @@ import logging import re import string +import threading from datetime import datetime from erpbrasil.base.fiscal import cnpj_cpf @@ -13,9 +14,11 @@ from erpbrasil.edoc.pdf import base from erpbrasil.transmissao import TransmissaoSOAP from lxml import etree +from nfelib.nfe.bindings.v4_0.leiaute_nfe_v4_00 import TnfeProc from nfelib.nfe.bindings.v4_0.nfe_v4_00 import Nfe from nfelib.nfe.ws.edoc_legacy import NFCeAdapter as edoc_nfce, NFeAdapter as edoc_nfe from requests import Session +from xsdata.formats.dataclass.parsers import XmlParser from xsdata.models.datatype import XmlDateTime from odoo import _, api, fields @@ -27,7 +30,6 @@ CANCELADO, CANCELADO_DENTRO_PRAZO, CANCELADO_FORA_PRAZO, - CONTINGENCIA, DENEGADO, DOCUMENT_ISSUER_COMPANY, EVENT_ENV_HML, @@ -38,6 +40,7 @@ MODELO_FISCAL_NFCE, MODELO_FISCAL_NFE, PROCESSADOR_OCA, + SERVICO_PARALIZADO, SITUACAO_EDOC_A_ENVIAR, SITUACAO_EDOC_AUTORIZADA, SITUACAO_EDOC_CANCELADA, @@ -59,6 +62,7 @@ ) PRODUCT_CODE_FISCAL_DOCUMENT_TYPES = ["55", "01"] +NFE_XML_NAMESPACE = {"nfe": "http://www.portalfiscal.inf.br/nfe"} _logger = logging.getLogger(__name__) @@ -868,11 +872,13 @@ def _processador(self): certificado = self.env.company._get_br_ecertificate() session = Session() session.verify = False + params = { "transmissao": TransmissaoSOAP(certificado, session), "uf": self.company_id.state_id.ibge_code, "versao": self.nfe_version, "ambiente": self.nfe_environment, + "envio_sincrono": self.env.user.company_id.nfe_synchronous_processing, } if self.document_type == MODELO_FISCAL_NFCE: @@ -904,7 +910,13 @@ def _document_export(self, pretty_print=True): xml_file = processador.render_edoc_xsdata(edoc, pretty_print=pretty_print)[ 0 ] - _logger.debug(xml_file) + # Delete previous authorization events in draft + if ( + record.authorization_event_id + and record.authorization_event_id.state == "draft" + ): + record.sudo().authorization_event_id.unlink() + event_id = self.event_ids.create_event_save_xml( company_id=self.company_id, environment=( @@ -919,51 +931,70 @@ def _document_export(self, pretty_print=True): self._valida_xml(xml_assinado) return result - def atualiza_status_nfe(self, processo): + def _nfe_update_status_and_save_data(self, process): self.ensure_one() + force_change_status = False + response = process.resposta - if hasattr(processo, "protocolo"): - infProt = processo.protocolo.infProt + if hasattr(process, "protocolo"): + inf_prot = process.protocolo.infProt else: - infProt = processo.resposta.protNFe.infProt - - # TODO: Verificar a consulta de notas - # if not infProt.chNFe == self.key: - # self = self.search([ - # ('key', '=', infProt.chNFe) - # ]) - if infProt.cStat in AUTORIZADO: - state = SITUACAO_EDOC_AUTORIZADA - elif infProt.cStat in DENEGADO: - state = SITUACAO_EDOC_DENEGADA + inf_prot = process.resposta.protNFe.infProt + + nfe_proc_xml = getattr(process, "processo_xml", None) + if nfe_proc_xml: + nfe_proc_xml = nfe_proc_xml.decode() + self._nfe_save_protocol(inf_prot, nfe_proc_xml) + + # Para o webservice de consulta NFe, verifica-se o status na resposta + # principal. Isso é crucial porque, para NF-es canceladas, o status atual não + # reflete o do protocolo de autorização. + if process.webservice == "nfeConsultaNF": + c_stat = response.cStat + x_motivo = response.xMotivo + force_change_status = True else: - state = SITUACAO_EDOC_REJEITADA - if self.authorization_event_id and infProt.nProt: - if type(infProt.dhRecbto) is datetime: - protocol_date = fields.Datetime.to_string(infProt.dhRecbto) - # When the bidding comes from xsdata, the date comes as XmlDateTime - elif type(infProt.dhRecbto) is XmlDateTime: - dt = infProt.dhRecbto.to_datetime() - protocol_date = fields.Datetime.to_string(dt) - else: - protocol_date = fields.Datetime.to_string( - datetime.fromisoformat(infProt.dhRecbto) - ) + c_stat = inf_prot.cStat + x_motivo = inf_prot.xMotivo - self.authorization_event_id.set_done( - status_code=infProt.cStat, - response=infProt.xMotivo, - protocol_date=protocol_date, - protocol_number=infProt.nProt, - file_response_xml=processo.processo_xml.decode("utf-8"), - ) - self.write( + # update document + self.update( { - "status_code": infProt.cStat, - "status_name": infProt.xMotivo, + "status_code": c_stat, + "status_name": x_motivo, } ) - self._change_state(state) + + # change state + state_map = { + **dict.fromkeys(AUTORIZADO, SITUACAO_EDOC_AUTORIZADA), + **dict.fromkeys(DENEGADO, SITUACAO_EDOC_DENEGADA), + **dict.fromkeys(CANCELADO, SITUACAO_EDOC_CANCELADA), + } + state = state_map.get(c_stat, SITUACAO_EDOC_REJEITADA) + self._change_state(state, force_change_status) + + def _nfe_save_protocol(self, inf_prot, nfe_proc_xml=None): + if not self.authorization_event_id: + # TODO: create new event. + pass + if type(inf_prot.dhRecbto) is datetime: + protocol_date = fields.Datetime.to_string(inf_prot.dhRecbto) + # When the bidding comes from xsdata, the date comes as XmlDateTime + elif type(inf_prot.dhRecbto) is XmlDateTime: + dt = inf_prot.dhRecbto.to_datetime() + protocol_date = fields.Datetime.to_string(dt) + else: + protocol_date = fields.Datetime.to_string( + datetime.fromisoformat(inf_prot.dhRecbto) + ) + self.authorization_event_id.set_done( + status_code=inf_prot.cStat, + response=inf_prot.xMotivo, + protocol_date=protocol_date, + protocol_number=inf_prot.nProt, + file_response_xml=nfe_proc_xml, + ) def _valida_xml(self, xml_file): self.ensure_one() @@ -1023,57 +1054,175 @@ def _generate_key(self): ) record.document_key = chave_edoc.chave - def _eletronic_document_send(self): + def _nfe_consult_receipt(self): + self.ensure_one() + processor = self._processador() + # Consult receipt and process the response + rec_num = self.authorization_event_id.lot_receipt_number + receipt_process = processor.consulta_recibo(numero=rec_num) + if receipt_process.resposta.cStat == "104": # Batch Processed (Lote Processado) + self._nfe_response_add_proc(receipt_process) + return receipt_process + + def _nfe_response_add_proc(self, ws_response_process): + """ + Monta o nfe proc e adicona ao objeto da resposta do webservice + """ + xml_soap = ws_response_process.retorno.content + tree_soap = etree.fromstring(xml_soap) + # TODO verificar se quando é sincrono funciona tbm. + prot_nfe_element = tree_soap.xpath( + "//nfe:protNFe", namespaces=NFE_XML_NAMESPACE + )[0] + proc_nfe_xml = self._nfe_create_proc(prot_nfe_element) + if proc_nfe_xml: + # nem sempre é possivel criar o nfeProc. + parser = XmlParser() + nfe_proc = parser.from_string(proc_nfe_xml.decode(), TnfeProc) + ws_response_process.processo = nfe_proc + ws_response_process.processo_xml = proc_nfe_xml + + def _nfe_create_proc(self, prot_nfe_element): + self.ensure_one() + + if not self.send_file_id.datas: + _logger.info( + "NF-e data not found when trying to assemble the " + "xml with the authorization protocol (nfeProc)" + ) + return None + + processor = self._processador() + nfe_send_xml = base64.b64decode(self.send_file_id.datas) + tree_envi_nfe = etree.fromstring(nfe_send_xml) + element_nfe = tree_envi_nfe.xpath("//nfe:NFe", namespaces=NFE_XML_NAMESPACE)[0] + proc_nfe_xml = processor.monta_nfe_proc( + nfe=element_nfe, prot_nfe=prot_nfe_element + ) + return proc_nfe_xml + + def _document_status(self): + self.ensure_one() + status = super()._document_status() + if filter_processador_edoc_nfe(self): + status = self.check_nfe_status_in_sefaz() + return status + + def check_nfe_status_in_sefaz(self): + """ + Checks the status and protocol of an NF-e against SEFAZ's database. + It updates the NF-e status and saves the data if the NF-e is found + with specific status codes. + Returns the response status message. + """ + + def _is_nfe_found(c_stat): + """ + Determines if the NF-e is registered in SEFAZ by analyzing the status code: + - 100: NF-e authorized - found and valid. + - 101: NF-e cancellation approved - found but cancelled. + - 110: NF-e use denied - present but restricted. + Returns True for these codes, indicating the NF-e's registration in SEFAZ. + """ + return c_stat in ["100", "101", "110"] + + nfe_manager = self._processador() + check_response = nfe_manager.consulta_documento(chave=self.document_key) + status = check_response.resposta.xMotivo + + if _is_nfe_found(check_response.resposta.cStat): + if not self.authorization_file_id: + # There's no need to assemble and persist the NFe file (nfeproc) + # if it is already saved. + self._nfe_response_add_proc(check_response) + # Updates the information if it is inconsistent in the system. + self._nfe_update_status_and_save_data(check_response) + return status + + def _prepare_nfce_send(self): + self.ensure_one() self._prepare_payments_for_nfce() + self.nfe40_infNFeSupl = self.env["l10n_br_fiscal.document.supplement"].create( + { + "nfe40_qrCode": self.get_nfce_qrcode(), + "nfe40_urlChave": self.get_nfce_qrcode_url(), + } + ) + def _eletronic_document_send(self): super()._eletronic_document_send() for record in self.filtered(filter_processador_edoc_nfe): if record.xml_error_message: - return - + return # Skip + if record.state_edoc not in ["enviada", "a_enviar"]: + return # Skip if record.document_type == MODELO_FISCAL_NFCE: - record.nfe40_infNFeSupl = self.env[ - "l10n_br_fiscal.document.supplement" - ].create( - { - "nfe40_qrCode": self.get_nfce_qrcode(), - "nfe40_urlChave": self.get_nfce_qrcode_url(), - } - ) - - processador = record._processador() - for edoc in record.serialize(): - processo = None - for p in processador.processar_documento(edoc): - processo = p - if processo.webservice == "nfeAutorizacaoLote": - record.authorization_event_id._save_event_file( - processo.envio_xml.decode("utf-8"), "xml" - ) - - if processo.resposta.cStat in LOTE_PROCESSADO + ["100"]: - record.atualiza_status_nfe(processo) - - elif processo.resposta.cStat in DENEGADO: - record._change_state(SITUACAO_EDOC_DENEGADA) - record.write( - { - "status_code": processo.resposta.cStat, - "status_name": processo.resposta.xMotivo, - } - ) - - elif processo.resposta.cStat in CONTINGENCIA: - record._process_document_in_contingency() + record._prepare_nfce_send() + if record.state_edoc == "enviada": + record._nfe_consult_receipt() + if record.state_edoc == "a_enviar": + record._nfe_send_for_authorization() + + def _nfe_send_for_authorization(self): + """ + Serialize and send a NFe for authorizaion + """ + serialized_nfe = self.serialize()[0] + nfe_manager = self._processador() + authorization_response = None + for service_response in nfe_manager.processar_documento(serialized_nfe): + if service_response.webservice not in [ + "nfeAutorizacaoLote", + "nfeRetAutorizacaoLote", + ]: + continue + if service_response.webservice == "nfeAutorizacaoLote": + if service_response.resposta.cStat in SERVICO_PARALIZADO: + self._process_document_in_contingency() + return + if service_response.resposta.infRec: + self._nfe_process_send_asynchronous(service_response) + + # Commit to secure receipt info for future queries. + in_testing = getattr(threading.current_thread(), "testing", False) + if not in_testing: + self.env.cr.commit() + + continue + authorization_response = service_response + if authorization_response: + self._nfe_process_authorization(authorization_response) + + def _nfe_process_send_asynchronous(self, send_process): + self.authorization_event_id._save_event_file( + send_process.envio_xml.decode("utf-8"), "xml" + ) + self.authorization_event_id.lot_receipt_number = ( + send_process.resposta.infRec.nRec + ) + self.state_edoc = "enviada" + # self.env.cr.commit() - else: - record._change_state(SITUACAO_EDOC_REJEITADA) - record.write( - { - "status_code": processo.resposta.cStat, - "status_name": processo.resposta.xMotivo, - } - ) + def _nfe_process_authorization(self, authorization_process): + self.ensure_one() + if authorization_process.resposta.cStat in LOTE_PROCESSADO + ["100"]: + self._nfe_update_status_and_save_data(authorization_process) + elif authorization_process.resposta.cStat in DENEGADO: + self._change_state(SITUACAO_EDOC_DENEGADA) + self.write( + { + "status_code": authorization_process.resposta.cStat, + "status_name": authorization_process.resposta.xMotivo, + } + ) + else: + self._change_state(SITUACAO_EDOC_REJEITADA) + self.write( + { + "status_code": authorization_process.resposta.cStat, + "status_name": authorization_process.resposta.xMotivo, + } + ) def view_pdf(self): if not self.filtered(filter_processador_edoc_nfe): diff --git a/l10n_br_nfe/models/res_company.py b/l10n_br_nfe/models/res_company.py index f3c5dd41c969..5a2021585e5f 100644 --- a/l10n_br_nfe/models/res_company.py +++ b/l10n_br_nfe/models/res_company.py @@ -64,6 +64,19 @@ class ResCompany(spec_models.SpecModel): default=NFE_ENVIRONMENT_DEFAULT, ) + nfe_synchronous_processing = fields.Boolean( + help=( + "When enabled, this option configures the system to transmit the " + "NFe (Electronic Invoice) using a synchronous method instead of an " + "asynchronous one. This means that the system will wait for an immediate " + "response from the tax authority's system (SEFAZ) upon submission of the " + "NFe, providing quicker feedback on the submission status. Before " + "activating this option, please ensure that the SEFAZ in your state " + "supports synchronous processing for NFe submissions. Failure to verify " + "compatibility may result in transmission errors or rejections." + ), + ) + nfe_transmission = fields.Selection( selection=NFE_TRANSMISSIONS, string="Transmission Type", diff --git a/l10n_br_nfe/models/res_config_settings.py b/l10n_br_nfe/models/res_config_settings.py index 297ba18f6a25..325059794f5e 100644 --- a/l10n_br_nfe/models/res_config_settings.py +++ b/l10n_br_nfe/models/res_config_settings.py @@ -25,6 +25,11 @@ class ResConfigSettings(models.TransientModel): readonly=False, ) + nfe_synchronous_processing = fields.Boolean( + related="company_id.nfe_synchronous_processing", + readonly=False, + ) + nfe_danfe_layout = fields.Selection( string="NFe Layout", related="company_id.nfe_danfe_layout", diff --git a/l10n_br_nfe/tests/mocks/retConsSitNFe/autorizado.xml b/l10n_br_nfe/tests/mocks/retConsSitNFe/autorizado.xml new file mode 100644 index 000000000000..b9ec2c2008ac --- /dev/null +++ b/l10n_br_nfe/tests/mocks/retConsSitNFe/autorizado.xml @@ -0,0 +1,34 @@ + + + + + + 2 + sefaz_mocked + 100 + Autorizado o uso da NF-e + 26 + 2020-02-03T10:31:52-03:00 + 26200124494200000106550010000010111352744151 + + + 2 + sefaz_mocked.00.07.211 + 26200124494200000106550010000010111352744151 + 2020-01-13T14:20:52-03:00 + 126200000020426 + YRn2TFuteCw8/KW0mwxQBQGurlI= + 100 + Autorizado o uso da NF-e + + + + + + diff --git a/l10n_br_nfe/tests/mocks/retConsSitNFe/cancelado.xml b/l10n_br_nfe/tests/mocks/retConsSitNFe/cancelado.xml new file mode 100644 index 000000000000..851710ff895e --- /dev/null +++ b/l10n_br_nfe/tests/mocks/retConsSitNFe/cancelado.xml @@ -0,0 +1,34 @@ + + + + + + 2 + sefaz_mocked + 101 + Cancelamento de NF-e homologado + 26 + 2020-02-03T10:31:52-03:00 + 26200124494200000106550010000010111352744151 + + + 2 + 2.2.21 + 103 + Lote recebido com sucesso + 12 + 43060992665611012850550070000081711388781007 + 1969-12-31T21:00:01.000-03:00 + 143060000295038 + + + + + + diff --git a/l10n_br_nfe/tests/test_nfce.py b/l10n_br_nfe/tests/test_nfce.py index 6550ec06eeaf..fb840ce543d3 100644 --- a/l10n_br_nfe/tests/test_nfce.py +++ b/l10n_br_nfe/tests/test_nfce.py @@ -133,7 +133,9 @@ def test_atualiza_status_nfce(self): mock_autorizada.protocolo.infProt.xMotivo = "TESTE AUTORIZADO" mock_autorizada.protocolo.infProt.dhRecbto = datetime.now() mock_autorizada.processo_xml = b"dummy" - self.document_id.atualiza_status_nfe(mock_autorizada) + mock_autorizada.resposta = mock.MagicMock() + mock_autorizada.webservice = "dummy_service" + self.document_id._nfe_update_status_and_save_data(mock_autorizada) self.assertEqual(self.document_id.state_edoc, SITUACAO_EDOC_AUTORIZADA) self.assertEqual(self.document_id.status_code, AUTORIZADO[0]) diff --git a/l10n_br_nfe/views/res_company_view.xml b/l10n_br_nfe/views/res_company_view.xml index 4b18dbb1c01a..325f6bacac20 100644 --- a/l10n_br_nfe/views/res_company_view.xml +++ b/l10n_br_nfe/views/res_company_view.xml @@ -13,6 +13,11 @@ + + + + + + + + + Enables synchronous processing in the transmission of the NFe. + + + DANFE Print Layout