diff --git a/l10n_br_nfe/__manifest__.py b/l10n_br_nfe/__manifest__.py index 9251920b6b72..1530b1e28a54 100644 --- a/l10n_br_nfe/__manifest__.py +++ b/l10n_br_nfe/__manifest__.py @@ -29,8 +29,8 @@ "views/res_company_view.xml", "views/nfe_document_view.xml", "views/res_config_settings_view.xml", - "views/mde/mde_views.xml", - "views/dfe/dfe_views.xml", + "views/nfe_recipient_manifestation_event/nfe_recipient_manifestation_event_view.xml", + "views/dfe/dfe_access_key_views.xml", "views/supplier_info_view.xml", # Report "report/reports.xml", @@ -38,6 +38,7 @@ "report/danfe_report.xml", # Wizards "wizards/import_document.xml", + "wizards/nfe_recipient_manifestation_event_wizard.xml", # Actions, "views/nfe_action.xml", # Menus diff --git a/l10n_br_nfe/constants/mde.py b/l10n_br_nfe/constants/mde.py index c2cc9af1051c..e5dac49b85c5 100644 --- a/l10n_br_nfe/constants/mde.py +++ b/l10n_br_nfe/constants/mde.py @@ -1,34 +1,14 @@ # Copyright (C) 2023 KMEE Informática LTDA # License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html -SIT_MANIF_PENDENTE = ("pendente", "Pendente") SIT_MANIF_CIENTE = ("ciente", "Ciente da Operação") SIT_MANIF_CONFIRMADO = ("confirmado", "Confirmada operação") SIT_MANIF_DESCONHECIDO = ("desconhecido", "Desconhecimento") SIT_MANIF_NAO_REALIZADO = ("nao_realizado", "Não realizado") -SCHEMA_RESNFE = ("resNFe", "NFe Resumida") -SCHEMA_RESEVENTO = ("resEvento", "Evento de NFe Resumido") -SCHEMA_PROCNFE = ("procNFe", "NFe Completa") -SCHEMA_PROCEVENTONFE = ("procEventoNFe", "Evento de NFe Completo") - -SCHEMAS = [SCHEMA_RESNFE, SCHEMA_RESEVENTO, SCHEMA_PROCNFE, SCHEMA_PROCEVENTONFE] - SITUACAO_MANIFESTACAO = [ - SIT_MANIF_PENDENTE, SIT_MANIF_CIENTE, SIT_MANIF_CONFIRMADO, SIT_MANIF_DESCONHECIDO, SIT_MANIF_NAO_REALIZADO, ] - -SIT_NFE_AUTORIZADA = ("1", "Autorizada") -SIT_NFE_CANCELADA = ("2", "Cancelada") -SIT_NFE_DENEGADA = ("3", "Denegada") - -SITUACAO_NFE = [SIT_NFE_AUTORIZADA, SIT_NFE_CANCELADA, SIT_NFE_DENEGADA] - -OP_TYPE_ENTRADA = ("0", "Entrada") -OP_TYPE_SAIDA = ("1", "Saída") - -OPERATION_TYPE = [OP_TYPE_ENTRADA, OP_TYPE_SAIDA] diff --git a/l10n_br_nfe/models/__init__.py b/l10n_br_nfe/models/__init__.py index d57ab0b103e9..f2b1e4b79e4a 100644 --- a/l10n_br_nfe/models/__init__.py +++ b/l10n_br_nfe/models/__init__.py @@ -18,8 +18,8 @@ from . import res_config_settings from . import cfop from . import invalidate_number +from . import nfe_recipient_manifestation_event from . import dfe -from . import mde spec_schema = "nfe" spec_version = "40" diff --git a/l10n_br_nfe/models/dfe.py b/l10n_br_nfe/models/dfe.py index 1a9d85aecfd2..da060edb7c4b 100644 --- a/l10n_br_nfe/models/dfe.py +++ b/l10n_br_nfe/models/dfe.py @@ -1,131 +1,24 @@ -# Copyright (C) 2023 KMEE Informatica LTDA -# License AGPL-3 or later (http://www.gnu.org/licenses/agpl) - -from datetime import datetime - -from lxml import objectify -from nfelib.nfe.bindings.v4_0.leiaute_nfe_v4_00 import TnfeProc - -from odoo import api, fields, models - -from odoo.addons.l10n_br_fiscal_dfe.tools import utils +# Copyright (C) 2025-Today - Engenere (). +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import _, models class DFe(models.Model): _inherit = "l10n_br_fiscal.dfe" - mde_ids = fields.One2many( - comodel_name="l10n_br_nfe.mde", - inverse_name="dfe_id", - string="Manifestações do Destinatário Importadas", - ) - - def _process_distribution(self, result): - for doc in result.resposta.loteDistDFeInt.docZip: - xml = utils.parse_gzip_xml(doc.valueOf_).read() - root = objectify.fromstring(xml) - - mde_id = self.env["l10n_br_nfe.mde"].search( - [ - ("nsu", "=", utils.format_nsu(doc.NSU)), - ("company_id", "=", self.company_id.id), - ], - limit=1, + def import_document(self): + self.ensure_one() + try: + document = self.dfe_monitor_id._download_document(self.key) + document_id = self.dfe_monitor_id._parse_xml_document(document) + except Exception as e: + self.message_post( + body=_("Error importing document: \n\n %(error)s", error=e) ) - if not mde_id: - mde_id = self._create_mde_from_schema(doc.schema, root) - if mde_id: - mde_id.nsu = doc.NSU - mde_id.create_xml_attachment(xml) - - @api.model - def _create_mde_from_schema(self, schema, root): - schema_type = schema.split("_")[0] - method = "_create_mde_from_%s" % schema_type - if not hasattr(self, method): return + if document_id: + self.document_id = document_id - return getattr(self, method)(root) - - @api.model - def _create_mde_from_procNFe(self, root): - nfe_key = root.protNFe.infProt.chNFe - mde_id = self.find_mde_by_key(nfe_key) - if mde_id: - return mde_id - - supplier_cnpj = utils.mask_cnpj("%014d" % root.NFe.infNFe.emit.CNPJ) - partner = self.env["res.partner"].search([("cnpj_cpf", "=", supplier_cnpj)]) - - return self.env["l10n_br_nfe.mde"].create( - { - "number": root.NFe.infNFe.ide.nNF, - "key": nfe_key, - "operation_type": str(root.NFe.infNFe.ide.tpNF), - "document_value": root.NFe.infNFe.total.ICMSTot.vNF, - "state": "pendente", - "inclusion_datetime": datetime.now(), - "cnpj_cpf": supplier_cnpj, - "ie": root.NFe.infNFe.emit.IE, - "partner_id": partner.id, - "emission_datetime": datetime.strptime( - str(root.NFe.infNFe.ide.dhEmi)[:19], - "%Y-%m-%dT%H:%M:%S", - ), - "company_id": self.company_id.id, - "dfe_id": self.id, - "inclusion_mode": "Verificação agendada", - "schema": "procNFe", - } - ) - - @api.model - def _create_mde_from_resNFe(self, root): - nfe_key = root.chNFe - mde_id = self.find_mde_by_key(nfe_key) - if mde_id: - return mde_id - - supplier_cnpj = utils.mask_cnpj("%014d" % root.CNPJ) - partner_id = self.env["res.partner"].search([("cnpj_cpf", "=", supplier_cnpj)]) - - return self.env["l10n_br_nfe.mde"].create( - { - "key": nfe_key, - "emitter": root.xNome, - "operation_type": str(root.tpNF), - "document_value": root.vNF, - "document_state": str(root.cSitNFe), - "state": "pendente", - "inclusion_datetime": datetime.now(), - "cnpj_cpf": supplier_cnpj, - "ie": root.IE, - "partner_id": partner_id.id, - "emission_datetime": datetime.strptime( - str(root.dhEmi)[:19], "%Y-%m-%dT%H:%M:%S" - ), - "company_id": self.company_id.id, - "dfe_id": self.id, - "inclusion_mode": "Verificação agendada - manifestada por outro app", - "schema": "resNFe", - } - ) - - @api.model - def find_mde_by_key(self, key): - mde_id = self.env["l10n_br_nfe.mde"].search([("key", "=", key)]) - if not mde_id: - return False - - if mde_id not in self.mde_ids: - mde_id.dfe_id = self.id - return mde_id - - def import_documents(self): - for record in self: - record.mde_ids.import_document_multi() - - @api.model - def parse_procNFe(self, xml): - binding = TnfeProc.from_xml(xml.read().decode()) - return self.env["l10n_br_fiscal.document"].import_binding_nfe(binding) + def import_document_multi(self): + for rec in self: + rec.import_document() diff --git a/l10n_br_nfe/models/dfe_access_key.py b/l10n_br_nfe/models/dfe_access_key.py new file mode 100644 index 000000000000..9bb1fc7d2b03 --- /dev/null +++ b/l10n_br_nfe/models/dfe_access_key.py @@ -0,0 +1,13 @@ +# Copyright (C) 2025-Today - Engenere (). +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import fields, models + + +class DFeAccessKey(models.Model): + _inherit = "l10n_br_fiscal.dfe_access_key" + + nfe_recipient_manifestation_event_ids = fields.One2many( + comodel_name="l10n_br_nfe.recipient_manifestation_event", + inverse_name="dfe_access_key_id", + string="Manifestações do Destinatário Importadas", + ) diff --git a/l10n_br_nfe/models/mde.py b/l10n_br_nfe/models/mde.py deleted file mode 100644 index ccf637ac73d7..000000000000 --- a/l10n_br_nfe/models/mde.py +++ /dev/null @@ -1,250 +0,0 @@ -# Copyright (C) 2023 KMEE Informatica LTDA -# License AGPL-3 or later (http://www.gnu.org/licenses/agpl) - -import base64 -import logging -import re - -from erpbrasil.transmissao import TransmissaoSOAP -from nfelib.nfe.ws.edoc_legacy import MDeAdapter as edoc_mde -from requests import Session - -from odoo import _, api, fields, models -from odoo.exceptions import ValidationError - -from ..constants.mde import ( - OPERATION_TYPE, - SCHEMAS, - SIT_MANIF_CIENTE, - SIT_MANIF_CONFIRMADO, - SIT_MANIF_DESCONHECIDO, - SIT_MANIF_NAO_REALIZADO, - SIT_MANIF_PENDENTE, - SITUACAO_MANIFESTACAO, - SITUACAO_NFE, -) - -_logger = logging.getLogger(__name__) - - -class MDe(models.Model): - _name = "l10n_br_nfe.mde" - _description = "Recipient Manifestation" - - company_id = fields.Many2one(comodel_name="res.company", string="Company") - - key = fields.Char(string="Access Key", size=44) - - serie = fields.Char(size=3, index=True) - - number = fields.Float(string="Document Number", index=True, digits=(18, 0)) - - document_id = fields.Many2one( - comodel_name="l10n_br_fiscal.document", - string="Fiscal Document", - ) - - emitter = fields.Char(size=60) - - cnpj_cpf = fields.Char(string="CNPJ/CPF", size=18) - - nsu = fields.Char(string="NSU", size=25, index=True) - - operation_type = fields.Selection( - selection=OPERATION_TYPE, - ) - - document_value = fields.Float( - string="Document Total Value", - readonly=True, - digits=(18, 2), - ) - - ie = fields.Char(string="Inscrição estadual", size=18) - - partner_id = fields.Many2one( - comodel_name="res.partner", - string="Supplier (partner)", - ) - - emission_datetime = fields.Datetime( - string="Emission Date", - index=True, - default=fields.Datetime.now, - ) - - inclusion_datetime = fields.Datetime( - string="Inclusion Date", - index=True, - default=fields.Datetime.now, - ) - - authorization_datetime = fields.Datetime(string="Authorization Date", index=True) - - cancellation_datetime = fields.Datetime(string="Cancellation Date", index=True) - - digest_value = fields.Char(size=28) - - inclusion_mode = fields.Char(size=255) - - authorization_protocol = fields.Char(size=60) - - cancellation_protocol = fields.Char(size=60) - - document_state = fields.Selection( - selection=SITUACAO_NFE, - index=True, - ) - - state = fields.Selection( - string="Manifestation State", - selection=SITUACAO_MANIFESTACAO, - index=True, - ) - - dfe_id = fields.Many2one(string="DF-e", comodel_name="l10n_br_fiscal.dfe") - - schema = fields.Selection(selection=SCHEMAS) - - attachment_id = fields.Many2one(comodel_name="ir.attachment") - - def name_get(self): - return [ - ( - rec.id, - f"NFº: {rec.number} ({rec.cnpj_cpf}): {rec.company_id.legal_name}", - ) - for rec in self - ] - - def _get_processor(self): - certificado = self.env.company._get_br_ecertificate() - session = Session() - session.verify = False - - return edoc_mde( - TransmissaoSOAP(certificado, session), - self.company_id.state_id.ibge_code, - ambiente=self.dfe_id.environment, - ) - - @api.model - def validate_event_response(self, result, valid_codes): - valid = False - if result.retorno.status_code != 200: - code = result.retorno.status_code - message = "Invalid Status Code" - else: - inf_evento = result.resposta.retEvento[0].infEvento - if inf_evento.cStat not in valid_codes: - code = inf_evento.cStat - message = inf_evento.xMotivo - else: - valid = True - - if not valid: - raise ValidationError( - _( - "Error on validating event: %(code)s - %(msg)s", - code=code, - msg=message, - ) - ) - - def import_document(self): - self.ensure_one() - - if self.state == "pendente": - self.action_ciencia_emissao() - - try: - document = self.dfe_id._download_document(self.key) - document_id = self.dfe_id._parse_xml_document(document) - except Exception as e: - self.dfe_id.message_post( - body=_("Error importing document: \n\n %(error)s", error=e) - ) - return - - if document_id: - document_id.dfe_id = self.dfe_id.id - self.document_id = document_id - - def import_document_multi(self): - for rec in self.filtered( - lambda m: m.state in (SIT_MANIF_PENDENTE[0], SIT_MANIF_CIENTE[0]) - ): - rec.import_document() - - def _send_event(self, method, valid_codes): - processor = self._get_processor() - cnpj_partner = re.sub("[^0-9]", "", self.company_id.cnpj_cpf) - - if hasattr(processor, method): - result = getattr(processor, method)(self.key, cnpj_partner) - self.validate_event_response(result, valid_codes) - - def action_send_event(self, operation, valid_codes, new_state): - for record in self: - try: - record._send_event(operation, valid_codes) - record.state = new_state - except Exception as e: - raise e - - def action_ciencia_emissao(self): - return self.action_send_event( - "ciencia_da_operacao", ["135"], SIT_MANIF_CIENTE[0] - ) - - def action_confirmar_operacacao(self): - return self.action_send_event( - "confirmacao_da_operacao", ["135"], SIT_MANIF_CONFIRMADO[0] - ) - - def action_operacao_desconhecida(self): - return self.action_send_event( - "desconhecimento_da_operacao", ["135"], SIT_MANIF_DESCONHECIDO[0] - ) - - def action_negar_operacao(self): - return self.action_send_event( - "operacao_nao_realizada", ["135"], SIT_MANIF_NAO_REALIZADO[0] - ) - - def create_xml_attachment(self, xml): - file_name = "NFe%s.xml" % self.dfe_id.last_nsu - self.attachment_id = self.env["ir.attachment"].create( - { - "name": file_name, - "datas": base64.b64encode(xml), - "store_fname": file_name, - "description": "NFe via Manifesto", - "res_model": self._name, - "res_id": self.id, - } - ) - - def action_download_xml(self): - for record in self.filtered(lambda m: m.state == SIT_MANIF_PENDENTE[0]): - record.action_ciencia_emissao() - - if len(self) == 1: - return self.download_attachment(self.attachment_id) - - compressed_attachment_id = ( - self.env["l10n_br_fiscal.attachment"] - .create([]) - .build_compressed_attachment(self.mapped("attachment_id")) - ) - return self.download_attachment(compressed_attachment_id) - - def download_attachment(self, attachment_id): - return { - "type": "ir.actions.act_url", - "url": ( - f"/web/content/{attachment_id.id}" - f"/{attachment_id.name}?download=true" - ), - "target": "self", - } diff --git a/l10n_br_nfe/models/nfe_recipient_manifestation_event.py b/l10n_br_nfe/models/nfe_recipient_manifestation_event.py new file mode 100644 index 000000000000..fe58bd5868c2 --- /dev/null +++ b/l10n_br_nfe/models/nfe_recipient_manifestation_event.py @@ -0,0 +1,155 @@ +# Copyright (C) 2023 KMEE Informatica LTDA +# License AGPL-3 or later (http://www.gnu.org/licenses/agpl) + +import re + +from erpbrasil.transmissao import TransmissaoSOAP +from nfelib.nfe.ws.edoc_legacy import MDeAdapter as edoc_mde +from requests import Session + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + +from ..constants.mde import ( + SIT_MANIF_CIENTE, + SIT_MANIF_CONFIRMADO, + SIT_MANIF_DESCONHECIDO, + SIT_MANIF_NAO_REALIZADO, + SITUACAO_MANIFESTACAO, +) + + +class NfeRecipientManifestationEvent(models.Model): + _name = "l10n_br_nfe.recipient_manifestation_event" + _description = "NFe Recipient Manifestation Event" + + company_id = fields.Many2one(comodel_name="res.company", string="Company") + + document_number = fields.Float( + string="Document Number", + index=True, + digits=(18, 0), + related="dfe_access_key_id.document_number", + ) + + key = fields.Char(string="Access Key", size=44, related="dfe_access_key_id.key") + + serie = fields.Char(size=3, index=True, related="dfe_access_key_id.serie") + + event_type = fields.Selection( + string="Manifestation State", + selection=SITUACAO_MANIFESTACAO, + index=True, + ) + + event_type_selection = fields.Selection( + selection=SITUACAO_MANIFESTACAO, + default="ciente", + required=True, + ) + + status = fields.Selection( + selection=[ + ("rascunho", "Rascunho"), + ("transmitido", "Transmitido"), + ], + ) + + environment = fields.Selection(related="company_id.dfe_environment") + + dfe_access_key_id = fields.Many2one( + string="DF-e", comodel_name="l10n_br_fiscal.dfe_access_key" + ) + + mde_document_type = fields.Selection( + selection=[ + ("mde_nfe", "NFe"), + ("mde_nfce", "NFCe"), + ], + string="MDe Document Type", + ) + + def name_get(self): + return [(rec.id, f"{rec.key}") for rec in self] + + def _get_processor(self): + certificado = self.env.company._get_br_ecertificate() + session = Session() + session.verify = False + + return edoc_mde( + TransmissaoSOAP(certificado, session), + self.company_id.state_id.ibge_code, + ambiente=self.environment, + ) + + @api.model + def validate_event_response(self, result, valid_codes): + valid = False + if result.retorno.status_code != 200: + code = result.retorno.status_code + message = "Invalid Status Code" + else: + inf_evento = result.resposta.retEvento[0].infEvento + if inf_evento.cStat not in valid_codes: + code = inf_evento.cStat + message = inf_evento.xMotivo + else: + valid = True + + if not valid: + raise ValidationError( + _( + "Error on validating event: %(code)s - %(msg)s", + code=code, + msg=message, + ) + ) + + def _send_event(self, method, valid_codes): + processor = self._get_processor() + cnpj_partner = re.sub("[^0-9]", "", self.company_id.cnpj_cpf) + + if hasattr(processor, method): + result = getattr(processor, method)(self.key, cnpj_partner) + self.validate_event_response(result, valid_codes) + + def action_send_event(self, operation, valid_codes, new_state): + for record in self: + try: + record._send_event(operation, valid_codes) + record.event_type = new_state + except Exception as e: + raise e + + def action_confirm_selection(self): + event_mapping = { + SIT_MANIF_CIENTE[0]: ("ciencia_da_operacao", ["135"], SIT_MANIF_CIENTE[0]), + SIT_MANIF_CONFIRMADO[0]: ( + "confirmacao_da_operacao", + ["135"], + SIT_MANIF_CONFIRMADO[0], + ), + SIT_MANIF_DESCONHECIDO[0]: ( + "desconhecimento_da_operacao", + ["135"], + SIT_MANIF_DESCONHECIDO[0], + ), + SIT_MANIF_NAO_REALIZADO[0]: ( + "operacao_nao_realizada", + ["135"], + SIT_MANIF_NAO_REALIZADO[0], + ), + } + + for record in self: + if record.event_type_selection in event_mapping: + operation, valid_codes, new_state = event_mapping[ + record.event_type_selection + ] + record.action_send_event(operation, valid_codes, new_state) + record.event_type_selection = False + else: + raise ValidationError( + _("Select a manifestation type before confirming") + ) diff --git a/l10n_br_nfe/security/ir.model.access.csv b/l10n_br_nfe/security/ir.model.access.csv index 672e48737f9e..a5d88a92ddc3 100644 --- a/l10n_br_nfe/security/ir.model.access.csv +++ b/l10n_br_nfe/security/ir.model.access.csv @@ -11,5 +11,7 @@ access_l10n_br_account_product_nfe_export_user,access_l10n_br_account_product_nf access_l10n_br_account_product_nfe_export_manager,access_l10n_br_account_product_nfe_export_manager,model_l10n_br_account_product_nfe_export,l10n_br_nfe.group_manager,1,1,1,1 access_l10n_br_account_product_nfe_export_result_user,access_l10n_br_account_product_nfe_export_result_user,model_l10n_br_account_product_nfe_export_result,l10n_br_nfe.group_user,1,0,0,0 access_l10n_br_account_product_nfe_export_result_manager,access_l10n_br_account_product_nfe_export_result_manager,model_l10n_br_account_product_nfe_export_result,l10n_br_nfe.group_manager,1,1,1,1 -access_l10n_br_nfe_mde_user,access_l10n_br_nfe_mde_user,model_l10n_br_nfe_mde,l10n_br_nfe.group_user,1,0,0,0 -access_l10n_br_nfe_mde_manager,access_l10n_br_nfe_mde_manager,model_l10n_br_nfe_mde,l10n_br_nfe.group_manager,1,1,1,1 +access_l10n_br_nfe_recipient_manifestation_event_user,access_l10n_br_nfe_recipient_manifestation_event_user,model_l10n_br_nfe_recipient_manifestation_event,l10n_br_nfe.group_user,1,0,0,0 +access_l10n_br_nfe_nfe_recipient_manifestation_event_manager,access_nfe_recipient_manifestation_event_manager,model_l10n_br_nfe_recipient_manifestation_event,l10n_br_nfe.group_manager,1,1,1,1 +access_nfe_recipient_manifestation_event_wizard_user,nfe_recipient_manifestation_event_wizard_user,model_nfe_recipient_manifestation_event_wizard,l10n_br_nfe.group_user,1,0,0,0 +access_nfe_recipient_manifestation_event_wizard_manager,nfe_recipient_manifestation_event_wizard_manager,model_nfe_recipient_manifestation_event_wizard,l10n_br_nfe.group_manager,1,1,1,1 diff --git a/l10n_br_nfe/static/description/index.html b/l10n_br_nfe/static/description/index.html index 55ed8d006a2a..01dc8d1306a6 100644 --- a/l10n_br_nfe/static/description/index.html +++ b/l10n_br_nfe/static/description/index.html @@ -8,11 +8,10 @@ /* :Author: David Goodger (goodger@python.org) -:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $ +:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $ :Copyright: This stylesheet has been placed in the public domain. Default cascading style sheet for the HTML output of Docutils. -Despite the name, some widely supported CSS2 features are used. See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to customize this style sheet. @@ -275,7 +274,7 @@ margin-left: 2em ; margin-right: 2em } -pre.code .ln { color: gray; } /* line numbers */ +pre.code .ln { color: grey; } /* line numbers */ pre.code, code { background-color: #eeeeee } pre.code .comment, code .comment { color: #5C6576 } pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold } @@ -301,7 +300,7 @@ span.pre { white-space: pre } -span.problematic, pre.problematic { +span.problematic { color: red } span.section-subtitle { @@ -472,9 +471,7 @@

Contributors

Maintainers

This module is maintained by the OCA.

- -Odoo Community Association - +Odoo Community Association

OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use.

diff --git a/l10n_br_nfe/tests/__init__.py b/l10n_br_nfe/tests/__init__.py index 03b2175a7b40..ab4d87eced2a 100644 --- a/l10n_br_nfe/tests/__init__.py +++ b/l10n_br_nfe/tests/__init__.py @@ -9,5 +9,5 @@ from . import test_nfe_xml_validation from . import test_res_partner from . import test_nfe_dfe -from . import test_nfe_mde +from . import test_nfe_recipient_manifestation_event from . import test_nfe_danfe diff --git a/l10n_br_nfe/tests/test_nfe_dfe.py b/l10n_br_nfe/tests/test_nfe_dfe.py index 7026ca4e8fac..b2bd962b9c4c 100644 --- a/l10n_br_nfe/tests/test_nfe_dfe.py +++ b/l10n_br_nfe/tests/test_nfe_dfe.py @@ -1,44 +1,38 @@ # Copyright (C) 2023 - TODAY Felipe Zago - KMEE # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -# pylint: disable=line-too-long from unittest import mock from nfelib.nfe.ws.edoc_legacy import DocumentoElectronicoAdapter +from odoo.exceptions import UserError from odoo.tests.common import TransactionCase from odoo.addons.l10n_br_fiscal_dfe.tests.test_dfe import ( - mocked_post_error_status_code, mocked_post_success_multiple, mocked_post_success_single, ) -from ..models.mde import MDe - class TestNFeDFe(TransactionCase): @classmethod def setUpClass(cls): super().setUpClass() - cls.dfe_id = cls.env["l10n_br_fiscal.dfe"].create( + cls.dfe_monitor = cls.env["l10n_br_fiscal.dfe_monitor"].create( {"company_id": cls.env.ref("l10n_br_base.empresa_lucro_presumido").id} ) @mock.patch.object( - DocumentoElectronicoAdapter, - "_post", - side_effect=mocked_post_success_single, + DocumentoElectronicoAdapter, "_post", side_effect=mocked_post_success_single ) - @mock.patch.object(MDe, "action_ciencia_emissao", return_value=None) - def test_download_document_proc_nfe(self, _mock_post, _mock_ciencia): - self.dfe_id.search_documents() + def test_download_document_proc_nfe(self, _mock_post): + self.dfe_monitor.search_documents() + self.dfe_monitor.dfe_ids.import_document() - self.dfe_id.import_documents() - self.assertEqual(len(self.dfe_id.imported_document_ids), 1) + self.assertEqual(len(self.dfe_monitor.dfe_ids.document_id), 1) self.assertEqual( - self.dfe_id.imported_document_ids[0].document_key, + self.dfe_monitor.dfe_ids.document_id[0].document_key, "35200159594315000157550010000000012062777161", ) @@ -46,66 +40,91 @@ def test_download_document_proc_nfe(self, _mock_post, _mock_ciencia): DocumentoElectronicoAdapter, "_post", side_effect=mocked_post_success_multiple ) def test_search_dfe_success(self, _mock_post): - self.dfe_id.search_documents() - self.assertEqual(self.dfe_id.mde_ids[-1].nsu, self.dfe_id.last_nsu) + self.dfe_monitor.search_documents() + self.assertEqual(self.dfe_monitor.dfe_ids[0].nsu, self.dfe_monitor.last_nsu) + + dfe2, dfe1 = self.dfe_monitor.dfe_ids + self.assertEqual(dfe1.company_id, self.dfe_monitor.company_id) + self.assertEqual(dfe1.key, "31201010588201000105550010038421171838422178") + self.assertEqual(dfe1.emitter, "ZAP GRAFICA E EDITORA EIRELI") + self.assertEqual(dfe1.cnpj_cpf, "10.588.201/0001-05") + self.assertEqual(dfe1.dfe_nfe_document_type, "dfe_nfe_summary") + self.assertEqual(dfe1.nsu, "000000000000200") + self.assertEqual( + dfe1.display_name, + "31201010588201000105550010038421171838422178 - Resumo da NF-e", + ) + self.assertEqual(dfe1.dfe_access_key_id.color_status, "blue") + self.assertEqual( + dfe1.dfe_access_key_id.display_name, + "31201010588201000105550010038421171838422178", + ) - mde1, mde2 = self.dfe_id.mde_ids - self.assertEqual(mde1.company_id, self.dfe_id.company_id) - self.assertEqual(mde1.key, "31201010588201000105550010038421171838422178") - self.assertEqual(mde1.emitter, "ZAP GRAFICA E EDITORA EIRELI") - self.assertEqual(mde1.cnpj_cpf, "10.588.201/0001-05") - self.assertEqual(mde1.state, "pendente") + self.assertEqual( + dfe1.dfe_access_key_id.key, "31201010588201000105550010038421171838422178" + ) - attachment_1 = self.env["ir.attachment"].search([("res_id", "=", mde1.id)]) - self.assertTrue(attachment_1) + self.assertEqual(dfe2.company_id, self.dfe_monitor.company_id) + self.assertEqual(dfe2.key, "35200159594315000157550010000000012062777161") + self.assertEqual( + dfe2.partner_id, self.env.ref("l10n_br_base.simples_nacional_partner") + ) + self.assertEqual(dfe2.cnpj_cpf, "59.594.315/0001-57") + self.assertEqual(dfe2.dfe_nfe_document_type, "dfe_nfe_complete") + self.assertEqual(dfe2.emitter, "TESTE - Simples Nacional") + self.assertEqual(dfe2.document_amount, 14.0) - self.assertEqual(mde2.company_id, self.dfe_id.company_id) - self.assertEqual(mde2.key, "35200159594315000157550010000000012062777161") self.assertEqual( - mde2.partner_id, self.env.ref("l10n_br_base.simples_nacional_partner") + dfe2.dfe_access_key_id.key, "35200159594315000157550010000000012062777161" + ) + self.assertEqual( + dfe2.display_name, + "35200159594315000157550010000000012062777161 - NF-e Completa", + ) + self.assertEqual(dfe2.dfe_access_key_id.color_status, "green") + self.assertEqual( + dfe2.dfe_access_key_id.display_name, + "35200159594315000157550010000000012062777161", ) - self.assertEqual(mde2.cnpj_cpf, "59.594.315/0001-57") - self.assertEqual(mde2.state, "pendente") - attachment_2 = self.env["ir.attachment"].search([("res_id", "=", mde2.id)]) - self.assertTrue(attachment_2) + @mock.patch.object( + DocumentoElectronicoAdapter, "_post", side_effect=mocked_post_success_single + ) + def test_generate_danfe(self, _mock_post): + self.dfe_monitor.search_documents() + dfe = self.dfe_monitor.dfe_ids + + result = dfe.dfe_access_key_id.make_pdf() + + self.assertEqual(result["type"], "ir.actions.act_url") + self.assertTrue(result["url"].startswith("/web/content/")) + self.assertIn("download=true", result["url"]) @mock.patch.object( - DocumentoElectronicoAdapter, - "_post", - side_effect=mocked_post_success_single, + DocumentoElectronicoAdapter, "_post", side_effect=mocked_post_success_multiple ) - @mock.patch.object(MDe, "action_ciencia_emissao", return_value=None) - def test_import_documents(self, _mock_post, _mock_ciencia): - self.dfe_id.search_documents() - self.dfe_id.import_documents() - - document_id = self.dfe_id.mde_ids[0].document_id - self.assertTrue(document_id) - self.assertEqual(document_id.dfe_id, self.dfe_id) - - with mock.patch.object( - DocumentoElectronicoAdapter, - "_post", - side_effect=mocked_post_error_status_code, - ): - xml = self.dfe_id._download_document("dummy") - self.assertIsNone(xml) - - def test_create_mde(self): - mde = self.dfe_id._create_mde_from_schema("dummy_v1.0", False) - self.assertIsNone(mde) - - mde_id = self.env["l10n_br_nfe.mde"].create({"key": "123456789"}) - - mock_resNFe = mock.MagicMock(spec=["chNFe"]) - mock_resNFe.chNFe = "123456789" - resnfe_mde_id = self.dfe_id._create_mde_from_schema("resNFe_v1.0", mock_resNFe) - self.assertEqual(resnfe_mde_id, mde_id) - - mock_procNFe = mock.MagicMock(spec=["protNFe"]) - mock_procNFe.protNFe.infProt.chNFe = "123456789" - procnfe_mde_id = self.dfe_id._create_mde_from_schema( - "procNFe_v1.0", mock_procNFe + def test_download_documents(self, _mock_post): + self.dfe_monitor.search_documents() + dfe2, dfe1 = self.dfe_monitor.dfe_ids + + attachment_2 = self.env["ir.attachment"].search([("res_id", "=", dfe2.id)]) + self.assertTrue(attachment_2) + + result_dfe2 = dfe1.action_download_xml() + attachment_single_dfe2 = self.get_attachment_from_result(result_dfe2) + self.assertTrue(attachment_single_dfe2) + self.assertEqual(attachment_single_dfe2, dfe1.attachment_id) + + result_dfe_2_access_key = dfe2.dfe_access_key_id.action_download_xml() + attachment_single_dfe2_access_key = self.get_attachment_from_result( + result_dfe_2_access_key ) - self.assertEqual(procnfe_mde_id, mde_id) + + self.assertTrue(attachment_single_dfe2_access_key) + + with self.assertRaises(UserError): + dfe1.dfe_access_key_id.action_download_xml() + + def get_attachment_from_result(self, result): + _, _, _, att_id, _ = result["url"].split("/") + return self.env["ir.attachment"].browse(int(att_id)) diff --git a/l10n_br_nfe/tests/test_nfe_mde.py b/l10n_br_nfe/tests/test_nfe_recipient_manifestation_event.py similarity index 73% rename from l10n_br_nfe/tests/test_nfe_mde.py rename to l10n_br_nfe/tests/test_nfe_recipient_manifestation_event.py index 89374382a008..6620bf03b005 100644 --- a/l10n_br_nfe/tests/test_nfe_mde.py +++ b/l10n_br_nfe/tests/test_nfe_recipient_manifestation_event.py @@ -13,8 +13,6 @@ from odoo.addons.l10n_br_fiscal_dfe.tests.test_dfe import mocked_post_success_multiple -from ..models.mde import MDe - response_confirmacao_operacao = """2SVRS2023052515551352SVRS202305251555135Teste Confirmação da Operação.31201010588201000105550010038421171838422178210200Confirmacao de Operacao registrada1815830540001292023-07-10T10:00:00-03:00""" # noqa: E501 response_confirmacao_operacao_rejeicao = """2SVRS2023052515554942SVRS202305251555494Rejeição: Chave de Acesso inexistente31201010588201000105550010038421171838422178210200Confirmacao de Operacao registrada1815830540001292023-07-10T10:00:00-03:00""" # noqa: E501 @@ -101,51 +99,74 @@ class TestMDe(TransactionCase): def setUpClass(cls): super().setUpClass() - cls.dfe_id = cls.env["l10n_br_fiscal.dfe"].create( - {"company_id": cls.env.ref("l10n_br_base.empresa_lucro_presumido").id} + dfe_monitor = cls.env["l10n_br_fiscal.dfe_monitor"].create( + { + "last_nsu": "000000000000001", + "company_id": cls.env.ref("l10n_br_base.empresa_simples_nacional").id, + } ) - with mock.patch.object( DocumentoElectronicoAdapter, "_post", side_effect=mocked_post_success_multiple, ): - cls.dfe_id.search_documents() - - cls.mde_id = cls.dfe_id.mde_ids[0] + dfe_monitor.search_documents() + + cls.dfe = dfe_monitor.dfe_ids[0] + cls.mde_create = cls.env[ + "l10n_br_nfe.recipient_manifestation_event" + ].create( + { + "company_id": cls.dfe.company_id.id, + "key": cls.dfe.key, + "document_number": cls.dfe.document_number, + "event_type": "ciente", + "status": "rascunho", + "dfe_access_key_id": cls.dfe.dfe_access_key_id.id, + "mde_document_type": "mde_nfe", + } + ) + cls.mde_id = cls.mde_create def test_events_success(self): with mock.patch.object( DocumentoElectronicoAdapter, "_post", - side_effect=mocked_post_confirmacao, + side_effect=mocked_post_ciencia, ): - self.mde_id.action_confirmar_operacacao() - self.assertEqual(self.mde_id.state, "confirmado") + self.mde_id.event_type_selection = "ciente" + self.mde_id.action_confirm_selection() + self.assertEqual(self.mde_id.event_type, "ciente") + self.assertEqual( + self.mde_id.display_name, "31201010588201000105550010038421171838422178" + ) with mock.patch.object( DocumentoElectronicoAdapter, "_post", - side_effect=mocked_post_ciencia, + side_effect=mocked_post_confirmacao, ): - self.mde_id.action_ciencia_emissao() - self.assertEqual(self.mde_id.state, "ciente") + self.mde_id.event_type_selection = "confirmado" + self.mde_id.action_confirm_selection() + self.assertEqual(self.mde_id.event_type, "confirmado") with mock.patch.object( DocumentoElectronicoAdapter, "_post", side_effect=mocked_post_desconhecimento, ): - self.mde_id.action_operacao_desconhecida() - self.assertEqual(self.mde_id.state, "desconhecido") + self.mde_id.event_type_selection = "desconhecido" + self.mde_id.action_confirm_selection() + self.assertEqual(self.mde_id.event_type, "desconhecido") with mock.patch.object( DocumentoElectronicoAdapter, "_post", side_effect=mocked_post_nao_realizada, ): - self.mde_id.action_negar_operacao() - self.assertEqual(self.mde_id.state, "nao_realizado") + self.mde_id.event_type_selection = "nao_realizado" + self.mde_id.action_confirm_selection() + self.assertEqual(self.mde_id.event_type, "nao_realizado") def test_event_error(self): with mock.patch.object( @@ -153,36 +174,40 @@ def test_event_error(self): "_post", side_effect=mocked_post_confirmacao_status_code_error, ), self.assertRaises(ValidationError): - self.mde_id.action_confirmar_operacacao() + self.mde_id.event_type_selection = "confirmado" + self.mde_id.action_confirm_selection() with mock.patch.object( DocumentoElectronicoAdapter, "_post", side_effect=mocked_post_confirmacao_invalid_status_error, ), self.assertRaises(ValidationError): - self.mde_id.action_confirmar_operacacao() - - @mock.patch.object( - DocumentoElectronicoAdapter, - "_post", - side_effect=mocked_post_success_multiple, - ) - @mock.patch.object(MDe, "action_ciencia_emissao", return_value=None) - def test_download_documents(self, _mock_post, _mock_ciencia): - mde_ids = self.mde_id + self.mde_id.copy() - - result_single = self.mde_id.action_download_xml() - result_multiple = mde_ids.action_download_xml() - - attachment_single = self.get_attachment_from_result(result_single) - attachment_multiple = self.get_attachment_from_result(result_multiple) - - self.assertTrue(attachment_single) - self.assertEqual(attachment_single, self.mde_id.attachment_id) + self.mde_id.event_type_selection = "confirmado" + self.mde_id.action_confirm_selection() + + def test_wizard_create_nfe_recipient_manifestation_event(self): + mde_wizard = ( + self.env["nfe_recipient_manifestation_event.wizard"] + .with_context(default_dfe_access_key_id=self.dfe.dfe_access_key_id.id) + .create({"event_type_selection": "confirmado"}) + ) + with mock.patch.object( + DocumentoElectronicoAdapter, + "_post", + side_effect=mocked_post_confirmacao, + ): + mde = mde_wizard.action_create_nfe_recipient_manifestation_event() - self.assertTrue(attachment_multiple) - self.assertEqual(attachment_multiple.name, "attachments.tar.gz") + mde = self.env["l10n_br_nfe.recipient_manifestation_event"].search( + [("dfe_access_key_id", "=", self.dfe.dfe_access_key_id.id)] + ) - def get_attachment_from_result(self, result): - _, _, _, att_id, _ = result["url"].split("/") - return self.env["ir.attachment"].browse(int(att_id)) + self.assertEqual(mde[1].event_type, "confirmado") + with mock.patch.object( + DocumentoElectronicoAdapter, + "_post", + side_effect=mocked_post_confirmacao, + ): + mde[1].event_type_selection = "confirmado" + mde[1].action_confirm_selection() + self.assertEqual(mde[1].event_type, "confirmado") diff --git a/l10n_br_nfe/views/dfe/dfe_access_key_views.xml b/l10n_br_nfe/views/dfe/dfe_access_key_views.xml new file mode 100644 index 000000000000..38fa43cca2db --- /dev/null +++ b/l10n_br_nfe/views/dfe/dfe_access_key_views.xml @@ -0,0 +1,38 @@ + + + + + + l10n_br_nfe.dfe_access_key.tree + l10n_br_fiscal.dfe_access_key + + + +