diff --git a/l10n_br_account_nfe/models/document.py b/l10n_br_account_nfe/models/document.py index 1e70fe075d96..daadf9786e0f 100644 --- a/l10n_br_account_nfe/models/document.py +++ b/l10n_br_account_nfe/models/document.py @@ -157,8 +157,8 @@ def _check_fiscal_payment_mode(self): ) ) - def _process_document_in_contingency(self): - super()._process_document_in_contingency() + def _update_nfce_for_offline_contingency(self): + super()._update_nfce_for_offline_contingency() if self.move_ids: copy_invoice = self.move_ids[0].copy() diff --git a/l10n_br_account_nfe/tests/test_nfce_contingency.py b/l10n_br_account_nfe/tests/test_nfce_contingency.py index 07321e3e59ef..cfa0f812be87 100644 --- a/l10n_br_account_nfe/tests/test_nfce_contingency.py +++ b/l10n_br_account_nfe/tests/test_nfce_contingency.py @@ -63,6 +63,6 @@ def prepare_account_move_nfce(self): self.document_move_id.fiscal_document_id = self.document_id.id def test_nfce_contingencia(self): - self.document_id._process_document_in_contingency() + self.document_id._update_nfce_for_offline_contingency() self.assertIn(self.document_move_id, self.document_id.move_ids) diff --git a/l10n_br_fiscal/constants/fiscal.py b/l10n_br_fiscal/constants/fiscal.py index f572f436b869..fcd0cd468d59 100644 --- a/l10n_br_fiscal/constants/fiscal.py +++ b/l10n_br_fiscal/constants/fiscal.py @@ -351,11 +351,11 @@ ] AUTORIZADO = ("100", "150") -DENEGADO = ("110", "301", "302") +DENEGADO = ("110", "301", "302", "303") LOTE_RECEBIDO = ["103"] LOTE_PROCESSADO = ["104"] LOTE_EM_PROCESSAMENTO = ["105"] -CONTINGENCIA = ("108", "109") +SERVICO_PARALIZADO = ("108", "109") CANCELAMENTO_HOMOLOGADO = ["101", "151"] diff --git a/l10n_br_fiscal/models/document_event.py b/l10n_br_fiscal/models/document_event.py index d613ce041e9f..f6bd5231d20d 100644 --- a/l10n_br_fiscal/models/document_event.py +++ b/l10n_br_fiscal/models/document_event.py @@ -69,7 +69,7 @@ def _compute_display_name(self): type = fields.Selection( selection=[ ("-1", "Exception"), - ("0", "Envio Lote"), + ("0", "Autorização de Uso"), ("1", "Consulta Recibo"), ("2", "Cancelamento"), ("3", "Inutilização"), @@ -185,6 +185,13 @@ def _compute_display_name(self): protocol_number = fields.Char() + lot_receipt_number = fields.Char( + help=( + "In asynchronous processing, a lot receipt number is generated, " + "which is used for later consultation." + ), + ) + state = fields.Selection( selection=[ ("draft", _("Draft")), @@ -300,7 +307,7 @@ def _save_event_file( ) if authorization: - # Nâo deletamos um aquivo de autorização já + # Não deletamos um aquivo de autorização já # Existente por segurança self.file_response_id = False self.file_response_id = attachment_id @@ -312,7 +319,8 @@ def _save_event_file( def set_done( self, status_code, response, protocol_date, protocol_number, file_response_xml ): - self._save_event_file(file_response_xml, "xml", authorization=True) + if file_response_xml: + self._save_event_file(file_response_xml, "xml", authorization=True) self.write( { "state": "done", diff --git a/l10n_br_fiscal/models/document_workflow.py b/l10n_br_fiscal/models/document_workflow.py index 9491baa45bfc..8ee3f9d966e7 100644 --- a/l10n_br_fiscal/models/document_workflow.py +++ b/l10n_br_fiscal/models/document_workflow.py @@ -199,7 +199,7 @@ def _after_change_state(self, old_state, new_state): self._generates_subsequent_operations() - def _change_state(self, new_state): + def _change_state(self, new_state, force_change=False): """Método para alterar o estado do documento fiscal, mantendo a integridade do workflow da invoice. @@ -215,7 +215,9 @@ def _change_state(self, new_state): for record in self: old_state = record.state_edoc - if not record._avaliable_transition(old_state, new_state): + if force_change or record._avaliable_transition(old_state, new_state): + pass + else: raise UserError( _( "Não é possível realizar esta operação,\n" diff --git a/l10n_br_fiscal/views/document_event_view.xml b/l10n_br_fiscal/views/document_event_view.xml index b72801e17cde..27f820b6d131 100644 --- a/l10n_br_fiscal/views/document_event_view.xml +++ b/l10n_br_fiscal/views/document_event_view.xml @@ -50,6 +50,7 @@ + diff --git a/l10n_br_fiscal/views/document_view.xml b/l10n_br_fiscal/views/document_view.xml index 1a2f2a43d9bc..c90641fa15ea 100644 --- a/l10n_br_fiscal/views/document_view.xml +++ b/l10n_br_fiscal/views/document_view.xml @@ -89,7 +89,6 @@ - @@ -110,7 +109,15 @@ string="Enviar" groups="l10n_br_fiscal.group_user" class="btn-primary" - attrs="{'invisible': [('state_edoc', 'not in', ('a_enviar', 'rejeitada'))]}" + attrs="{'invisible': [('state_edoc','!=','a_enviar')]}" + /> + - - - - @@ -451,7 +454,7 @@ - + diff --git a/l10n_br_nfe/models/document.py b/l10n_br_nfe/models/document.py index df2ef8a350fe..2de65a4611a2 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,6 +872,7 @@ 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, @@ -875,6 +880,13 @@ def _processador(self): "ambiente": self.nfe_environment, } + if self.document_type == MODELO_FISCAL_NFE: + params.update( + envio_sincrono=self.env.company.nfe_enable_sync_transmission, + contingencia=self.env.company.nfe_enable_contingency_ws, + ) + return edoc_nfe(**params) + if self.document_type == MODELO_FISCAL_NFCE: params.update( csc_token=self.company_id.nfce_csc_token, @@ -882,8 +894,6 @@ def _processador(self): ) return edoc_nfce(**params) - return edoc_nfe(**params) - def _check_nfe_environment(self): self.ensure_one() company_nfe_environment = self.company_id.nfe_environment @@ -904,7 +914,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 +935,76 @@ 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): + """ + Updates the NFe status based on the webservice response, + handling different scenarios. + """ self.ensure_one() - - if hasattr(processo, "protocolo"): - infProt = processo.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 + force_change_status = False + response = process.resposta + webservice = process.webservice + if hasattr(process, "protocolo"): + inf_prot = process.protocolo.infProt 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) + # The ´nfeRetAutorizacaoLote´ webservice allows + # querying a batch of NFe, therefore in this case the return of protNFe + # is a list, but the localization only sends one NFe per batch. + if webservice == "nfeRetAutorizacaoLote": + inf_prot = response.protNFe[0].infProt else: - protocol_date = fields.Datetime.to_string( - datetime.fromisoformat(infProt.dhRecbto) - ) - - 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( + inf_prot = response.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) + # For ´nfeConsultaNF´ webservice, the status is checked in the main response. + # This is crucial because for canceled NFes, the current status does not + # reflect the authorization protocol status. + if webservice == "nfeConsultaNF": + c_stat = response.cStat + x_motivo = response.xMotivo + force_change_status = True + else: + c_stat = inf_prot.cStat + x_motivo = inf_prot.xMotivo + # 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 +1064,212 @@ def _generate_key(self): ) record.document_key = chave_edoc.chave - def _eletronic_document_send(self): - self._prepare_payments_for_nfce() + 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": # Lote Processado + self._nfe_response_add_proc(receipt_process) + self._nfe_process_authorization(receipt_process) + + def _nfe_response_add_proc(self, ws_response_process): + """ + Inject the final NF-e, tag `nfeProc`, into the response. + """ + xml_soap = ws_response_process.retorno.content + tree_soap = etree.fromstring(xml_soap) + 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: + # it is not always possible to create 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): + """ + Create the `nfeProc` XML by combining the NF-e and the authorization protocol. + + This method decodes the saved `enviNFe` message, extracts the tag, + and combines it with the provided authorization protocol element to create + the `nfeProc` XML, which represents the finalized NF-e document. + + Args: + prot_nfe_element: The XML element containing the authorization protocol. + + Returns: + The assembled `nfeProc` XML, or None if the `send_file_id` data is not + found. + + Note: + Useful for recreating the final NF-e XML, as SEFAZ does not provide the + complete XML upon consultation, only the authorization protocol. + """ + self.ensure_one() - super()._eletronic_document_send() - for record in self.filtered(filter_processador_edoc_nfe): - if record.xml_error_message: - return + 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 - 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(), - } - ) + processor = self._processador() - 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" - ) + # Extract the tag from the `enviNFe` message, which represents the NF-e + 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] - if processo.resposta.cStat in LOTE_PROCESSADO + ["100"]: - record.atualiza_status_nfe(processo) + # Assemble the `nfeProc` using the erpbrasil.edoc library. + proc_nfe_xml = processor.monta_nfe_proc( + nfe=element_nfe, prot_nfe=prot_nfe_element + ) - elif processo.resposta.cStat in DENEGADO: - record._change_state(SITUACAO_EDOC_DENEGADA) - record.write( - { - "status_code": processo.resposta.cStat, - "status_name": processo.resposta.xMotivo, - } - ) + return proc_nfe_xml - elif processo.resposta.cStat in CONTINGENCIA: - record._process_document_in_contingency() + 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(), + } + ) - else: - record._change_state(SITUACAO_EDOC_REJEITADA) - record.write( - { - "status_code": processo.resposta.cStat, - "status_name": processo.resposta.xMotivo, - } - ) + def _eletronic_document_send(self): + super()._eletronic_document_send() + for record in self.filtered(filter_processador_edoc_nfe): + if record.xml_error_message: + return # Skip + if record.state_edoc not in ["enviada", "a_enviar"]: + return # Skip + if record.document_type == MODELO_FISCAL_NFCE: + 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 + and self.document_type == MODELO_FISCAL_NFCE + ): + # Offline contingency is only allowed for NFC-e (65) + self._update_nfce_for_offline_contingency() + return + if service_response.resposta.infRec: + # Only ASYNC: The receipt is only applicable for asynchronous + # transmission. + 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() + + # Check if 'nfe_separate_async_process' is set in the company + # settings. If True, skip the receipt consultation in this + # transaction. The user will need to manually trigger the + # consultation later to obtain the usage protocol. + skip_consult_receipt = self.env.company.nfe_separate_async_process + if skip_consult_receipt: + break + else: + 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" + + def _nfe_process_authorization(self, authorization_process): + """ + Processes the response to the authorization request (batch processing). + This can be called the transmission result or the processing result + of the NF-e batch submission message. + + The responses can be in two formats: + - 'retEnviNFe' for synchronous. + - 'retConsReciNFe' for asynchronous. + """ + self.ensure_one() + if authorization_process.resposta.cStat in LOTE_PROCESSADO: + # Processes the individual result of each NF-e (protNFe). + self._nfe_update_status_and_save_data(authorization_process) + else: + # Batch processing failure. + 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): @@ -1279,21 +1475,20 @@ def _nfe_correction(self, justificative): file_response_xml=processo.retorno.content.decode("utf-8"), ) - def _process_document_in_contingency(self): + def _update_nfce_for_offline_contingency(self): self.write( { - "nfe_transmission": "9", + "nfe_transmission": "9", # 9: contingência off-line (tpEmis) "nfe40_dhCont": fields.Datetime.now().strftime( DEFAULT_SERVER_DATETIME_FORMAT ), - "nfe40_xJust": "Sem comunicacao com o servidor da Sefaz.", + "nfe40_xJust": "Sem comunicação com o servidor da Sefaz.", } ) def get_nfce_qrcode(self): if self.document_type != MODELO_FISCAL_NFCE: return - processador = self._processador() if self.nfe_transmission == "1": return processador.monta_qrcode(self.document_key) diff --git a/l10n_br_nfe/models/res_company.py b/l10n_br_nfe/models/res_company.py index f3c5dd41c969..8c765fd8295e 100644 --- a/l10n_br_nfe/models/res_company.py +++ b/l10n_br_nfe/models/res_company.py @@ -64,6 +64,37 @@ class ResCompany(spec_models.SpecModel): default=NFE_ENVIRONMENT_DEFAULT, ) + nfe_enable_sync_transmission = 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_separate_async_process = fields.Boolean( + string="Separate NF-e Send and Consult", + help=( + "If enabled, the system will send the NF-e and store the receipt without " + "immediately consulting it. The user must manually consult the receipt " + "later. This option is valid only in asynchronous mode." + ), + ) + + nfe_enable_contingency_ws = fields.Boolean( + help=( + "When enabled, all NFe-related services will be accessed using the " + "contingencyweb services. This ensures that operations such as issuing, " + "canceling, and consulting NFe will use the contingency web services " + "instead of the primary web services." + ), + ) + 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..15fab5438473 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_enable_sync_transmission = fields.Boolean( + related="company_id.nfe_enable_sync_transmission", + readonly=False, + ) + nfe_danfe_layout = fields.Selection( string="NFe Layout", related="company_id.nfe_danfe_layout", diff --git a/l10n_br_nfe/tests/mocks/retConsReciNFe/autorizada.xml b/l10n_br_nfe/tests/mocks/retConsReciNFe/autorizada.xml index 1f2a2ba680f4..84d498813dac 100644 --- a/l10n_br_nfe/tests/mocks/retConsReciNFe/autorizada.xml +++ b/l10n_br_nfe/tests/mocks/retConsReciNFe/autorizada.xml @@ -12,7 +12,7 @@ 2 sefaz_mocked 423002202113232 - 100 + 104 Lote processado 42 2023-06-11T01:18:19-03:00 diff --git a/l10n_br_nfe/tests/mocks/retConsReciNFe/uso_denegado.xml b/l10n_br_nfe/tests/mocks/retConsReciNFe/uso_denegado.xml new file mode 100644 index 000000000000..c8506a1528ae --- /dev/null +++ b/l10n_br_nfe/tests/mocks/retConsReciNFe/uso_denegado.xml @@ -0,0 +1,35 @@ + + + + + + 2 + sefaz_mocked + 423002202113232 + 104 + Lote processado + 42 + 2023-06-11T01:18:19-03:00 + + + 2 + SVRSnfce202307311112 + 33230807984267003800650040000000321935136447 + 2023-08-07T11:09:08-03:00 + 333230000396082 + zwJzbq4FXks09tlHU1GEWRI7t/A= + 303 + Uso Denegado: Destinatário não habilitado a operar na UF + + + + + + 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/mocks/retConsSitNFe/uso_denegado.xml b/l10n_br_nfe/tests/mocks/retConsSitNFe/uso_denegado.xml new file mode 100644 index 000000000000..ec7685f1ae2d --- /dev/null +++ b/l10n_br_nfe/tests/mocks/retConsSitNFe/uso_denegado.xml @@ -0,0 +1,34 @@ + + + + + 1 + sefaz_mocked + 110 + Uso Denegado + 26 + 2020-02-13T14:20:52-03:00 + 26200124494200000106550010000010111352744151 + + + 1 + sefaz_mocked + 26200124494200000106550010000010111352744151 + 2020-01-13T14:20:52-03:00 + 135180772366783 + 4A20WByjTePGOz+LoGy1uVk2gaY= + 302 + Uso Denegado: Irregularidade fiscal do destinatário + + + + + + 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/tests/test_nfe_webservices.py b/l10n_br_nfe/tests/test_nfe_webservices.py index 4a31b1f48a0b..39a7417c27fe 100644 --- a/l10n_br_nfe/tests/test_nfe_webservices.py +++ b/l10n_br_nfe/tests/test_nfe_webservices.py @@ -1,20 +1,27 @@ # Copyright (C) 2023 - TODAY Raphaël Valyi - Akretion +# Copyright (C) 2024 - TODAY Antônio S. P. Neto - Engenere # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +import logging import subprocess from unittest import mock from odoo.fields import Datetime from odoo.addons.l10n_br_fiscal.constants.fiscal import ( + SITUACAO_EDOC_A_ENVIAR, SITUACAO_EDOC_AUTORIZADA, SITUACAO_EDOC_CANCELADA, + SITUACAO_EDOC_ENVIADA, ) from odoo.addons.l10n_br_nfe.models.document import NFe from .mock_utils import nfe_mock from .test_nfe_serialize import TestNFeExport +_logger = logging.getLogger(__name__) + def is_libreoffice_command_available(): try: @@ -26,7 +33,7 @@ def is_libreoffice_command_available(): return False -class TestNFeWebservices(TestNFeExport): +class TestNFeWebServices(TestNFeExport): def setUp(self): nfe_list = [ { @@ -91,3 +98,70 @@ def test_inutilizar(self): .create({"document_id": nfe.id, "justification": "Era apenas um teste."}) ) inutilizar_wizard.doit() + + @nfe_mock( + { + "nfeAutorizacaoLote": "retEnviNFe/lote_recebido.xml", + "nfeRetAutorizacaoLote": "retConsReciNFe/autorizada.xml", + } + ) + def test_nfe_consult_receipt(self): + """ + Tests the asynchronous NFe transmission, separating the sending and + the consultation into two distinct steps. + """ + for nfe_data in self.nfe_list: + nfe = nfe_data["nfe"] + self.assertEqual(nfe.state_edoc, SITUACAO_EDOC_A_ENVIAR) + with mock.patch.object(NFe, "make_pdf"): + # enable skip receipt consultation during the send action + self.env.company.nfe_separate_async_process = True + nfe.action_document_send() + # Document has been sent, but the receipt has not been consulted yet, + # meaning the authorization protocol has not been received. + self.assertEqual(nfe.state_edoc, SITUACAO_EDOC_ENVIADA) + # Consult the receipt to receive the usage authorization. + nfe._nfe_consult_receipt() + self.assertEqual(nfe.state_edoc, SITUACAO_EDOC_AUTORIZADA) + + @nfe_mock( + { + "nfeAutorizacaoLote": "retEnviNFe/lote_recebido.xml", + "nfeRetAutorizacaoLote": "retConsReciNFe/autorizada.xml", + } + ) + @mock.patch("odoo.addons.l10n_br_nfe.models.document._logger") + def test_nfe_consult_receipt_without_nfe_saved(self, mock_logger): + """ + Tests the NF-e processing result query after deleting the sent nfe xml. + """ + for nfe_data in self.nfe_list: + nfe = nfe_data["nfe"] + self.assertEqual(nfe.state_edoc, SITUACAO_EDOC_A_ENVIAR) + with mock.patch.object(NFe, "make_pdf"): + # enable skip receipt consultation during the send action + self.env.company.nfe_separate_async_process = True + nfe.action_document_send() + # Document has been sent, but the receipt has not been consulted yet, + # meaning the authorization protocol has not been received. + self.assertEqual(nfe.state_edoc, SITUACAO_EDOC_ENVIADA) + # Consult the receipt to receive the usage authorization. + + # Erase the sending_file + nfe.send_file_id = False + self.assertFalse(nfe.send_file_id) + + nfe._nfe_consult_receipt() + mock_logger.info.assert_called_with( + "NF-e data not found when trying to assemble the " + "xml with the authorization protocol (nfeProc)" + ) + self.assertEqual(nfe.state_edoc, SITUACAO_EDOC_AUTORIZADA) + + @nfe_mock({"nfeConsultaNF": "retConsSitNFe/autorizado.xml"}) + def test_nfe_consult(self): + for nfe_data in self.nfe_list: + nfe = nfe_data["nfe"] + self.assertEqual(nfe.state_edoc, SITUACAO_EDOC_A_ENVIAR) + nfe._document_status() + self.assertEqual(nfe.state_edoc, SITUACAO_EDOC_AUTORIZADA) diff --git a/l10n_br_nfe/views/nfe_document_view.xml b/l10n_br_nfe/views/nfe_document_view.xml index cc31ef4fe28b..257040aaf6af 100644 --- a/l10n_br_nfe/views/nfe_document_view.xml +++ b/l10n_br_nfe/views/nfe_document_view.xml @@ -144,6 +144,7 @@ position="after" > + diff --git a/l10n_br_nfe/views/res_company_view.xml b/l10n_br_nfe/views/res_company_view.xml index 4b18dbb1c01a..6766bd5d1475 100644 --- a/l10n_br_nfe/views/res_company_view.xml +++ b/l10n_br_nfe/views/res_company_view.xml @@ -13,6 +13,18 @@ + + + + + + + + + + + Enables synchronous processing in the transmission of the NFe. + + + DANFE Print Layout