From ff2644f1d63aa5f0ac275d009200690f8767e53a Mon Sep 17 00:00:00 2001 From: Marcel Savegnago Date: Tue, 2 Jul 2024 13:26:12 -0300 Subject: [PATCH 1/6] [RFC] l10n_br_fiscal: refactor from l10n_br_cte wip --- l10n_br_fiscal/models/__init__.py | 1 + l10n_br_fiscal/models/document.py | 23 +++++++++++++++++++ l10n_br_fiscal/models/document_supplement.py | 13 +++++++++++ l10n_br_fiscal/security/ir.model.access.csv | 2 ++ l10n_br_fiscal/views/document_view.xml | 4 ++++ .../models/document_workflow.py | 10 ++++++++ 6 files changed, 53 insertions(+) create mode 100644 l10n_br_fiscal/models/document_supplement.py diff --git a/l10n_br_fiscal/models/__init__.py b/l10n_br_fiscal/models/__init__.py index ac376ef584d0..e0562968855e 100644 --- a/l10n_br_fiscal/models/__init__.py +++ b/l10n_br_fiscal/models/__init__.py @@ -58,3 +58,4 @@ from . import subsequent_document from . import document_email from . import city_taxation_code +from . import document_supplement diff --git a/l10n_br_fiscal/models/document.py b/l10n_br_fiscal/models/document.py index dfe625f30ca3..dfe3af9c1755 100644 --- a/l10n_br_fiscal/models/document.py +++ b/l10n_br_fiscal/models/document.py @@ -198,6 +198,29 @@ class Document(models.Model): default=False, ) + transport_modal = fields.Selection( + selection=[ + ("01", "Rodoviário"), + ("02", "Aéreo"), + ("03", "Aquaviário"), + ("04", "Ferroviário"), + ("05", "Dutoviário"), + ("06", "Multimodal"), + ], + string="Modal de Transporte", + ) + + service_provider = fields.Selection( + selection=[ + ("0", "Remetente"), + ("1", "Expedidor"), + ("2", "Recebedor"), + ("3", "Destinatário"), + ("4", "Outros"), + ], + string="Tomador do Serviço", + ) + @api.constrains("document_key") def _check_key(self): for record in self: diff --git a/l10n_br_fiscal/models/document_supplement.py b/l10n_br_fiscal/models/document_supplement.py new file mode 100644 index 000000000000..939a5cceff37 --- /dev/null +++ b/l10n_br_fiscal/models/document_supplement.py @@ -0,0 +1,13 @@ +# Copyright 2023 KMEE (Felipe Zago Rodrigues ) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class DocumentSupplement(models.Model): + _name = "l10n_br_fiscal.document.supplement" + _description = "Document Supplement Data" + + qrcode = fields.Char(string="QR Code") + + url_key = fields.Char(string="QR Code URL Key") diff --git a/l10n_br_fiscal/security/ir.model.access.csv b/l10n_br_fiscal/security/ir.model.access.csv index a7f680f0b2b5..b9bab610fd68 100644 --- a/l10n_br_fiscal/security/ir.model.access.csv +++ b/l10n_br_fiscal/security/ir.model.access.csv @@ -99,3 +99,5 @@ "l10n_br_fiscal_city_taxation_code_manager","Fiscal City Taxation Code for Manager","model_l10n_br_fiscal_city_taxation_code","l10n_br_fiscal.group_user",1,1,1,1 "l10n_br_fiscal_base_wizard_mixin_user",l10n_br_fiscal_base_wizard_mixin,model_l10n_br_fiscal_base_wizard_mixin,base.group_user,1,1,1,1 "l10n_br_fiscal_document_status_wizard_user",l10n_br_fiscal_document_status_wizard,model_l10n_br_fiscal_document_status_wizard,base.group_user,1,1,1,1 +"l10n_br_fiscal_document_supplement_user,l10n_br_fiscal_document_supplement_user,model_l10n_br_fiscal_document_supplement,l10n_br_fiscal.group_user,1,1,1,1 +"l10n_br_fiscal_document_supplement_manager,l10n_br_fiscal_document_supplement_manager,model_l10n_br_fiscal_document_supplement,l10n_br_fiscal.group_manager,1,1,1,1 diff --git a/l10n_br_fiscal/views/document_view.xml b/l10n_br_fiscal/views/document_view.xml index 84ed8378d793..e2a0704619a7 100644 --- a/l10n_br_fiscal/views/document_view.xml +++ b/l10n_br_fiscal/views/document_view.xml @@ -285,6 +285,10 @@ + + + + diff --git a/l10n_br_fiscal_edi/models/document_workflow.py b/l10n_br_fiscal_edi/models/document_workflow.py index b703e227c1d1..762e183bcb8d 100644 --- a/l10n_br_fiscal_edi/models/document_workflow.py +++ b/l10n_br_fiscal_edi/models/document_workflow.py @@ -73,6 +73,7 @@ def _exec_before_SITUACAO_EDOC_A_ENVIAR(self, old_state, new_state): self._document_number() self._document_comment() self._document_check() + self._document_qrcode() self._document_export() return True @@ -385,3 +386,12 @@ def _action_document_correction(self): "this fical document you are not the document issuer" ) ) + + def _document_qrcode(self): + pass + + def _processador(self): + pass + + def _valida_xml(self, xml_file): + pass From a123ca6d6182df1a7d7c2489e729314e6aef52f3 Mon Sep 17 00:00:00 2001 From: Marcel Savegnago Date: Sat, 14 Sep 2024 10:46:52 -0300 Subject: [PATCH 2/6] [RFC] l10n_br_nfe: refactor from l10n_br_cte wip --- l10n_br_nfe/models/document.py | 51 ++++++++++++++++++----- l10n_br_nfe/models/document_line.py | 1 + l10n_br_nfe/models/document_related.py | 1 + l10n_br_nfe/models/document_supplement.py | 9 +++- l10n_br_nfe/models/res_company.py | 2 + l10n_br_nfe/models/res_partner.py | 2 + 6 files changed, 53 insertions(+), 13 deletions(-) diff --git a/l10n_br_nfe/models/document.py b/l10n_br_nfe/models/document.py index b0dbd137e8e6..1645caba8960 100644 --- a/l10n_br_nfe/models/document.py +++ b/l10n_br_nfe/models/document.py @@ -86,6 +86,7 @@ class NFe(spec_models.StackedModel): _spec_module = "odoo.addons.l10n_br_nfe_spec.models.v4_0.leiaute_nfe_v4_00" _spec_tab_name = "NFe" _nfe_search_keys = ["nfe40_Id"] + _binding_module = "nfelib.nfe.bindings.v4_0.leiaute_nfe_v4_00" # all m2o at this level will be stacked even if not required: _force_stack_paths = ( @@ -760,16 +761,20 @@ def _build_many2one(self, comodel, vals, new_value, key, value, path): company_cnpj = self.env.company.cnpj_cpf.translate( str.maketrans("", "", string.punctuation) ) - emit_cnpj = new_value.get("nfe40_CNPJ").translate( - str.maketrans("", "", string.punctuation) - ) - if company_cnpj != emit_cnpj: - vals["issuer"] = "partner" - new_value["is_company"] = True - new_value["cnpj_cpf"] = emit_cnpj - super()._build_many2one( - self.env["res.partner"], vals, new_value, "partner_id", value, path - ) + if new_value.get("nfe40_CNPJ"): + emit_cnpj = new_value.get("nfe40_CNPJ").translate( + str.maketrans("", "", string.punctuation) + ) + if company_cnpj != emit_cnpj: + vals["issuer"] = "partner" + new_value["is_company"] = True + new_value["cnpj_cpf"] = emit_cnpj + super()._build_many2one( + self.env["res.partner"], vals, new_value, "partner_id", value, path + ) + else: + new_value["name"] = value.xFant + super()._build_many2one(comodel, vals, new_value, key, value, path) elif key == "nfe40_dest" and self.env.context.get("edoc_type") == "out": enderDest_value = self.env["res.partner"].build_attrs( value.enderDest, path=path @@ -897,6 +902,9 @@ def _serialize(self, edocs): return edocs def _processador(self): + if self.document_type not in (MODELO_FISCAL_NFE, MODELO_FISCAL_NFCE): + return super()._processador() + self._check_nfe_environment() certificado = self.company_id._get_br_ecertificate() session = Session() @@ -1037,6 +1045,10 @@ def _nfe_save_protocol(self, inf_prot, nfe_proc_xml=None): def _valida_xml(self, xml_file): self.ensure_one() + + if self.document_type not in (MODELO_FISCAL_NFE, MODELO_FISCAL_NFCE): + return super()._valida_xml(xml_file) + erros = Nfe.schema_validation(xml_file) erros = "\n".join(erros) self.write({"xml_error_message": erros or False}) @@ -1059,7 +1071,11 @@ def _exec_after_SITUACAO_EDOC_AUTORIZADA(self, old_state, new_state): return super()._exec_after_SITUACAO_EDOC_AUTORIZADA(old_state, new_state) def _generate_key(self): - for record in self.filtered(filter_processador_edoc_nfe): + records = self.filtered(filter_processador_edoc_nfe) + if not records: + return super()._generate_key() + + for record in records: date = fields.Datetime.context_timestamp(record, record.document_date) required_fields_gen_edoc = [] @@ -1103,6 +1119,19 @@ def _nfe_consult_receipt(self): self._nfe_response_add_proc(receipt_process) self._nfe_process_authorization(receipt_process) + def _document_qrcode(self): + super()._document_qrcode() + + for record in self.filtered(filter_processador_edoc_nfe): + record.nfe40_infNFeSupl = self.env[ + "l10n_br_fiscal.document.supplement" + ].create( + { + "qrcode": record.get_nfce_qrcode(), + "url_key": record.get_nfce_qrcode_url(), + } + ) + def _nfe_response_add_proc(self, ws_response_process): """ Inject the final NF-e, tag `nfeProc`, into the response. diff --git a/l10n_br_nfe/models/document_line.py b/l10n_br_nfe/models/document_line.py index 95ef91dc3c05..1416aecba1d0 100644 --- a/l10n_br_nfe/models/document_line.py +++ b/l10n_br_nfe/models/document_line.py @@ -81,6 +81,7 @@ class NFeLine(spec_models.StackedModel): # all m2o below this level will be stacked even if not required: _force_stack_paths = ("det.imposto.",) _stack_skip = ("nfe40_det_infNFe_id",) + _binding_module = "nfelib.nfe.bindings.v4_0.leiaute_nfe_v4_00" # When dynamic stacking is applied, the NFe line has the following structure: DET_TREE = """ diff --git a/l10n_br_nfe/models/document_related.py b/l10n_br_nfe/models/document_related.py index ad5e8b3e1124..99d322f9480c 100644 --- a/l10n_br_nfe/models/document_related.py +++ b/l10n_br_nfe/models/document_related.py @@ -30,6 +30,7 @@ class NFeRelated(spec_models.StackedModel): _stack_skip = ("nfe40_NFref_ide_id",) # all m2o below this level will be stacked even if not required: _rec_name = "nfe40_refNFe" + _binding_module = "nfelib.nfe.bindings.v4_0.leiaute_nfe_v4_00" # When dynamic stacking is applied, this class has the following structure: NFREF_TREE = """ diff --git a/l10n_br_nfe/models/document_supplement.py b/l10n_br_nfe/models/document_supplement.py index 85590853eec7..df1e6b72d0e8 100644 --- a/l10n_br_nfe/models/document_supplement.py +++ b/l10n_br_nfe/models/document_supplement.py @@ -1,14 +1,14 @@ # Copyright 2023 KMEE (Felipe Zago Rodrigues ) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import fields from odoo.addons.spec_driven_model.models import spec_models class NFeSupplement(spec_models.StackedModel): _name = "l10n_br_fiscal.document.supplement" - _description = "NFe Supplement Document" - _inherit = "nfe.40.infnfesupl" + _inherit = ["l10n_br_fiscal.document.supplement", "nfe.40.infnfesupl"] _stacked = "nfe.40.infnfesupl" _field_prefix = "nfe40_" _schema_name = "nfe" @@ -16,3 +16,8 @@ class NFeSupplement(spec_models.StackedModel): _odoo_module = "l10n_br_nfe" _spec_module = "odoo.addons.l10n_br_nfe_spec.models.v4_0.leiaute_nfe_v4_00" _spec_tab_name = "NFe" + _binding_module = "nfelib.nfe.bindings.v4_0.leiaute_nfe_v4_00" + + nfe40_qrCode = fields.Char(related="qrcode") + + nfe40_urlChave = fields.Char(related="url_key") diff --git a/l10n_br_nfe/models/res_company.py b/l10n_br_nfe/models/res_company.py index 6c81c6807abf..0231c805a003 100644 --- a/l10n_br_nfe/models/res_company.py +++ b/l10n_br_nfe/models/res_company.py @@ -31,6 +31,8 @@ class ResCompany(spec_models.SpecModel): _name = "res.company" _inherit = ["res.company", "nfe.40.emit"] _nfe_search_keys = ["nfe40_CNPJ", "nfe40_xNome", "nfe40_xFant"] + _binding_module = "nfelib.nfe.bindings.v4_0.leiaute_nfe_v4_00" + _field_prefix = "nfe40_" nfe40_CNPJ = fields.Char(related="partner_id.nfe40_CNPJ") nfe40_CPF = fields.Char(related="partner_id.nfe40_CPF") diff --git a/l10n_br_nfe/models/res_partner.py b/l10n_br_nfe/models/res_partner.py index 8eaeb55c744f..600d1d12404f 100644 --- a/l10n_br_nfe/models/res_partner.py +++ b/l10n_br_nfe/models/res_partner.py @@ -27,6 +27,8 @@ class ResPartner(spec_models.SpecModel): "nfe.40.autxml", ] _nfe_search_keys = ["nfe40_CNPJ", "nfe40_CPF", "nfe40_xNome"] + _binding_module = "nfelib.nfe.bindings.v4_0.leiaute_nfe_v4_00" + _field_prefix = "nfe40_" @api.model def _prepare_import_dict( From ae7fb786119d54fd21b841ff76c1ebf2f3606a68 Mon Sep 17 00:00:00 2001 From: Marcel Savegnago Date: Tue, 2 Jul 2024 13:27:13 -0300 Subject: [PATCH 3/6] [RFC] l10n_br_cte_spec: refactor from l10n_br_cte wip --- l10n_br_cte_spec/__manifest__.py | 3 + l10n_br_cte_spec/models/spec_models.py | 2 +- .../models/v4_0/cte_modal_aereo_v4_00.py | 1 - .../models/v4_0/cte_tipos_basico_v4_00.py | 248 +++++++++++++----- l10n_br_cte_spec/security/ir.model.access.csv | 4 + .../static/description/index.html | 1 - 6 files changed, 196 insertions(+), 63 deletions(-) create mode 100644 l10n_br_cte_spec/security/ir.model.access.csv diff --git a/l10n_br_cte_spec/__manifest__.py b/l10n_br_cte_spec/__manifest__.py index 63fe69ab1ce8..e7701f6a4616 100644 --- a/l10n_br_cte_spec/__manifest__.py +++ b/l10n_br_cte_spec/__manifest__.py @@ -11,4 +11,7 @@ "development_status": "Alpha", "maintainers": ["rvalyi"], "website": "https://github.com/OCA/l10n-brazil", + "data": [ + "security/ir.model.access.csv", + ], } diff --git a/l10n_br_cte_spec/models/spec_models.py b/l10n_br_cte_spec/models/spec_models.py index 52d5afaccfb3..8389c2a32624 100644 --- a/l10n_br_cte_spec/models/spec_models.py +++ b/l10n_br_cte_spec/models/spec_models.py @@ -12,7 +12,7 @@ class CteSpecMixin(models.AbstractModel): _schema_version = "4.0.0" _odoo_module = "l10n_br_cte" _spec_module = "odoo.addons.l10n_br_cte_spec.models.v4_0.cte_tipos_basico_v4_00" - _binding_module = "nfelib.cte.bindings.v4_0.cte_v4_00" + _binding_module = "nfelib.cte.bindings.v4_0.cte_tipos_basico_v4_00" _spec_tab_name = "cte" brl_currency_id = fields.Many2one( diff --git a/l10n_br_cte_spec/models/v4_0/cte_modal_aereo_v4_00.py b/l10n_br_cte_spec/models/v4_0/cte_modal_aereo_v4_00.py index ca0ea2c6db56..4ded85769bc5 100644 --- a/l10n_br_cte_spec/models/v4_0/cte_modal_aereo_v4_00.py +++ b/l10n_br_cte_spec/models/v4_0/cte_modal_aereo_v4_00.py @@ -126,7 +126,6 @@ class NatCarga(models.AbstractModel): ) - class Tarifa(models.AbstractModel): "Informações de tarifa" _description = textwrap.dedent(" %s" % (__doc__,)) diff --git a/l10n_br_cte_spec/models/v4_0/cte_tipos_basico_v4_00.py b/l10n_br_cte_spec/models/v4_0/cte_tipos_basico_v4_00.py index 235fa73e02f7..86ad46ec637c 100644 --- a/l10n_br_cte_spec/models/v4_0/cte_tipos_basico_v4_00.py +++ b/l10n_br_cte_spec/models/v4_0/cte_tipos_basico_v4_00.py @@ -442,6 +442,7 @@ class TendeEmi(models.AbstractModel): _name = "cte.40.tendeemi" _inherit = "spec.mixin.cte" _binding_type = "TendeEmi" + _generateds_type = "TEndeEmi" cte40_xLgr = fields.Char(string="Logradouro", xsd_required=True) @@ -453,8 +454,8 @@ class TendeEmi(models.AbstractModel): cte40_cMun = fields.Char( string="Código do município", - xsd_required=True, xsd_type="TCodMunIBGE", + xsd_required=True, help="Código do município (utilizar a tabela do IBGE)", ) @@ -469,12 +470,12 @@ class TendeEmi(models.AbstractModel): cte40_fone = fields.Char(string="Telefone", xsd_type="TFone") -class Tendereco(models.AbstractModel): +class Tendernac(models.AbstractModel): "Tipo Dados do Endereço" _description = textwrap.dedent(" %s" % (__doc__,)) - _name = "cte.40.tendereco" + _name = "cte.40.tendernac" _inherit = "spec.mixin.cte" - _binding_type = "Tendereco" + _binding_type = "Tendernac" cte40_xLgr = fields.Char(string="Logradouro", xsd_required=True) @@ -489,20 +490,20 @@ class Tendereco(models.AbstractModel): xsd_required=True, xsd_type="TCodMunIBGE", help=( - "Código do município (utilizar a tabela do IBGE)\nInformar 9999999" + "Código do município (utilizar a tabela do IBGE), informar 9999999" " para operações com o exterior." ), ) cte40_xMun = fields.Char( - string="Nome do município", + string="Nome do município, ", xsd_required=True, - help=("Nome do município\nInformar EXTERIOR para operações com o " "exterior."), + help=( + "Nome do município, , informar EXTERIOR para operações com o " "exterior." + ), ) - cte40_CEP = fields.Char( - string="CEP", help="CEP\nInformar os zeros não significativos" - ) + cte40_CEP = fields.Char(string="CEP") cte40_UF = fields.Selection( TUF, @@ -512,71 +513,158 @@ class Tendereco(models.AbstractModel): help="Sigla da UF\nInformar EX para operações com o exterior.", ) - cte40_cPais = fields.Char( - string="Código do país", help="Código do país\nUtilizar a tabela do BACEN" + +class Timp(models.AbstractModel): + "Tipo Dados do Imposto para CT-e OS" + _description = textwrap.dedent(" %s" % (__doc__,)) + _name = "cte.40.timp" + _inherit = "spec.mixin.cte" + _binding_type = "Timp" + + cte40_ICMSOutraUF = fields.Many2one( + comodel_name="cte.40.icmsoutrauf", + string="ICMS de empresa com UF diferente da emissora", + choice="icms", + xsd_choice_required=True, ) - cte40_xPais = fields.Char(string="Nome do país") +class IcmsOutraUF(models.AbstractModel): + """Partilha do ICMS entre a UF de origem e UF de destino ou a UF definida + na legislação + Operação interestadual para consumidor final com partilha do ICMS devido na + operação entre a UF de origem e a UF do destinatário ou ou a UF definida na + legislação. (Ex. UF da concessionária de entrega do veículos)""" -class Tendernac(models.AbstractModel): - "Tipo Dados do Endereço" _description = textwrap.dedent(" %s" % (__doc__,)) - _name = "cte.40.tendernac" + _name = "cte.40.icmspart" _inherit = "spec.mixin.cte" - _binding_type = "Tendernac" + _binding_type = "Timp.IcmsoutraUf" + _generateds_type = "ICMSOutraUFType" - cte40_xLgr = fields.Char(string="Logradouro", xsd_required=True) + cte40_CST = fields.Selection( + ICMSOUTRAUF_CST, + string="Tributação pelo ICMS", + xsd_required=True, + help=( + "Tributação pelo ICMS \n10 - Tributada e com cobrança do ICMS por " + "substituição tributária;\n90 – Outros." + ), + ) - cte40_nro = fields.Char(string="Número", xsd_required=True) + cte40_vBC = fields.Monetary( + string="Valor da BC do ICMS", + xsd_required=True, + xsd_type="TDec_1302", + currency_field="brl_currency_id", + ) - cte40_xCpl = fields.Char(string="Complemento") + cte40_pRedBC = fields.Float( + string="Percentual de redução da BC", + xsd_type="TDec_0302a04Opc", + digits=( + 3, + 2, + ), + ) - cte40_xBairro = fields.Char(string="Bairro", xsd_required=True) + cte40_pICMS = fields.Float( + string="Alíquota do ICMS", + xsd_required=True, + xsd_type="TDec_0302a04", + digits=( + 3, + 2, + ), + ) - cte40_cMun = fields.Char( - string="Código do município", + cte40_vICMS = fields.Monetary( + string="Valor do ICMS", xsd_required=True, - xsd_type="TCodMunIBGE", - help=( - "Código do município (utilizar a tabela do IBGE), informar 9999999" - " para operações com o exterior." + xsd_type="TDec_1302", + currency_field="brl_currency_id", + ) + + + cte40_pMVAST = fields.Float( + string="Percentual da Margem", + xsd_type="TDec_0302a04Opc", + digits=( + 3, + 2, ), + help="Percentual da Margem de Valor Adicionado ICMS ST", ) - cte40_xMun = fields.Char( - string="Nome do município, ", + cte40_pRedBCST = fields.Float( + string="Percentual de redução da BC ICMS ST", + xsd_type="TDec_0302a04Opc", + digits=( + 3, + 2, + ), + ) + + cte40_vBCST = fields.Monetary( + string="Valor da BC do ICMS ST", xsd_required=True, - help=( - "Nome do município, , informar EXTERIOR para operações com o " "exterior." + xsd_type="TDec_1302", + currency_field="brl_currency_id", + ) + + cte40_pICMSST = fields.Float( + string="Alíquota do ICMS ST", + xsd_required=True, + xsd_type="TDec_0302a04", + digits=( + 3, + 2, ), ) - cte40_CEP = fields.Char(string="CEP") + cte40_vICMSST = fields.Monetary( + string="Valor do ICMS ST", + xsd_required=True, + xsd_type="TDec_1302", + currency_field="brl_currency_id", + ) - cte40_UF = fields.Selection( + cte40_pBCOp = fields.Float( + string="Percentual para determinação do valor", + xsd_required=True, + xsd_type="TDec_0302a04Opc", + digits=( + 3, + 2, + ), + help=( + "Percentual para determinação do valor da Base de Cálculo da " + "operação própria." + ), + ) + + cte40_UFST = fields.Selection( TUF, - string="Sigla da UF", + string="Sigla da UF para qual é devido o ICMS ST", xsd_required=True, xsd_type="TUf", - help="Sigla da UF\nInformar EX para operações com o exterior.", + help="Sigla da UF para qual é devido o ICMS ST da operação.", ) -class Timp(models.AbstractModel): - "Tipo Dados do Imposto CT-e" - _description = textwrap.dedent(" %s" % (__doc__,)) - _name = "cte.40.timp" - _inherit = "spec.mixin.cte" - _binding_type = "Timp" - - class TimpOs(models.AbstractModel): "Tipo Dados do Imposto para CT-e OS" _description = textwrap.dedent(" %s" % (__doc__,)) - _name = "cte.40.timpos" + _name = "cte.40.icmsos" _inherit = "spec.mixin.cte" - _binding_type = "TimpOs" + _binding_type = "IcmsOs" + + cte40_vBC = fields.Monetary( + string="Valor da Base de Cálculo do ICMS", + xsd_required=True, + xsd_type="TDec_1302", + currency_field="brl_currency_id", + ) class Tlocal(models.AbstractModel): @@ -1353,7 +1441,7 @@ class InfPercurso(models.AbstractModel): xsd_required=True, xsd_type="TUf", help=( - "Sigla das Unidades da Federação do percurso do veículo.\nNão é " + "Sigla das Unidades da Federação do percurso do veículo.\nNo é " "necessário repetir as UF de Início e Fim" ), ) @@ -1471,7 +1559,6 @@ class TcteOsEmit(models.AbstractModel): cte40_enderEmit = fields.Many2one( comodel_name="cte.40.tendeemi", string="Endereço do emitente", - xsd_required=True, xsd_type="TEndeEmi", ) @@ -1613,10 +1700,9 @@ class TcteOsImp(models.AbstractModel): _binding_type = "TcteOs.InfCte.Imp" cte40_ICMS = fields.Many2one( - comodel_name="cte.40.timpos", + comodel_name="cte.40.icmsos", string="Informações relativas ao ICMS", - xsd_required=True, - xsd_type="TImp", + xsd_type="Icms", ) cte40_vTotTrib = fields.Monetary( @@ -2472,7 +2558,6 @@ class TomaTerceiro(models.AbstractModel): cte40_enderToma = fields.Many2one( comodel_name="cte.40.tendereco", string="Dados do endereço", - xsd_required=True, xsd_type="TEndereco", ) @@ -2591,7 +2676,6 @@ class TgtveEmit(models.AbstractModel): cte40_enderEmit = fields.Many2one( comodel_name="cte.40.tendeemi", string="Endereço do emitente", - xsd_required=True, xsd_type="TEndeEmi", ) @@ -2648,7 +2732,6 @@ class TgtveRem(models.AbstractModel): cte40_enderReme = fields.Many2one( comodel_name="cte.40.tendereco", string="Dados do endereço", - xsd_required=True, xsd_type="TEndereco", ) @@ -2713,7 +2796,6 @@ class TgtveDest(models.AbstractModel): cte40_enderDest = fields.Many2one( comodel_name="cte.40.tendereco", string="Dados do endereço", - xsd_required=True, xsd_type="TEndereco", ) @@ -4009,6 +4091,7 @@ class TcteEmit(models.AbstractModel): _name = "cte.40.tcte_emit" _inherit = "spec.mixin.cte" _binding_type = "Tcte.InfCte.Emit" + _generateds_type = "emitType" cte40_CNPJ = fields.Char( string="CNPJ do emitente", @@ -4119,7 +4202,6 @@ class TcteRem(models.AbstractModel): cte40_enderReme = fields.Many2one( comodel_name="cte.40.tendereco", string="Dados do endereço", - xsd_required=True, xsd_type="TEndereco", ) @@ -4171,7 +4253,6 @@ class Exped(models.AbstractModel): cte40_enderExped = fields.Many2one( comodel_name="cte.40.tendereco", string="Dados do endereço", - xsd_required=True, xsd_type="TEndereco", ) @@ -4223,7 +4304,6 @@ class Receb(models.AbstractModel): cte40_enderReceb = fields.Many2one( comodel_name="cte.40.tendereco", string="Dados do endereço", - xsd_required=True, xsd_type="TEndereco", ) @@ -4288,7 +4368,6 @@ class TcteDest(models.AbstractModel): cte40_enderDest = fields.Many2one( comodel_name="cte.40.tendereco", string="Dados do endereço", - xsd_required=True, xsd_type="TEndereco", ) @@ -4364,8 +4443,6 @@ class TcteImp(models.AbstractModel): cte40_ICMS = fields.Many2one( comodel_name="cte.40.timp", string="Informações relativas ao ICMS", - xsd_required=True, - xsd_type="TImp", ) cte40_vTotTrib = fields.Monetary( @@ -4965,6 +5042,57 @@ class InfOutros(models.AbstractModel): ) +class TEndereco(models.AbstractModel): + "Tipo Dados do Endereço" + _description = textwrap.dedent(" %s" % (__doc__,)) + _name = "cte.40.tendereco" + _inherit = "spec.mixin.cte" + _binding_type = "Tendereco" + _generateds_type = "TEndereco" + + cte40_xLgr = fields.Char(string="Logradouro", xsd_required=True) + + cte40_nro = fields.Char(string="Número", xsd_required=True) + + cte40_xCpl = fields.Char(string="Complemento") + + cte40_xBairro = fields.Char(string="Bairro", xsd_required=True) + + cte40_cMun = fields.Char( + string="Código do município", + xsd_required=True, + xsd_type="TCodMunIBGE", + help=( + "Código do município (utilizar a tabela do IBGE)\nInformar 9999999" + " para operações com o exterior." + ), + ) + + cte40_xMun = fields.Char( + string="Nome do município", + xsd_required=True, + help=("Nome do município\nInformar EXTERIOR para operações com o " "exterior."), + ) + + cte40_CEP = fields.Char( + string="CEP", help="CEP\nInformar os zeros não significativos" + ) + + cte40_UF = fields.Selection( + TUF, + string="Sigla da UF", + xsd_required=True, + xsd_type="TUf", + help="Sigla da UF\nInformar EX para operações com o exterior.", + ) + + cte40_cPais = fields.Char( + string="Código do país", help="Código do país\nUtilizar a tabela do BACEN" + ) + + cte40_xPais = fields.Char(string="Nome do país") + + class DocAnt(models.AbstractModel): "Documentos de Transporte Anterior" _description = textwrap.dedent(" %s" % (__doc__,)) diff --git a/l10n_br_cte_spec/security/ir.model.access.csv b/l10n_br_cte_spec/security/ir.model.access.csv new file mode 100644 index 000000000000..1821f5fa2720 --- /dev/null +++ b/l10n_br_cte_spec/security/ir.model.access.csv @@ -0,0 +1,4 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_tcte_vprest_comp_user,access_tcte_vprest_comp,model_cte_40_tcte_vprest_comp,base.group_user,1,1,1,1 +access_veicnovos,access_veicnovos,model_cte_40_veicnovos,base.group_user,1,1,1,1 +access_tcte_infctecomp,access_tcte_infctecomp,model_cte_40_tcte_infctecomp,base.group_user,1,1,1,1 diff --git a/l10n_br_cte_spec/static/description/index.html b/l10n_br_cte_spec/static/description/index.html index b3c0f5544677..9f1bed9fc10b 100644 --- a/l10n_br_cte_spec/static/description/index.html +++ b/l10n_br_cte_spec/static/description/index.html @@ -1,4 +1,3 @@ - From 35c08793ec51242f817cff76b1f501e2d10c9c32 Mon Sep 17 00:00:00 2001 From: Marcel Savegnago Date: Tue, 2 Jul 2024 13:27:34 -0300 Subject: [PATCH 4/6] [ADD] l10n_br_cte: add new module --- l10n_br_cte/README.rst | 113 ++ l10n_br_cte/__init__.py | 2 + l10n_br_cte/__manifest__.py | 42 + l10n_br_cte/constants/modal.py | 32 + l10n_br_cte/hooks.py | 21 + l10n_br_cte/modal/modal_aereo.xml | 21 + l10n_br_cte/modal/modal_aquaviario.xml | 18 + l10n_br_cte/modal/modal_ferroviario.xml | 51 + l10n_br_cte/modal/modal_rodoviario.xml | 25 + l10n_br_cte/models/__init__.py | 15 + l10n_br_cte/models/aereo.py | 69 ++ l10n_br_cte/models/aquaviario.py | 52 + l10n_br_cte/models/document.py | 988 ++++++++++++++++++ .../models/document_cargo_quantity_infos.py | 27 + l10n_br_cte/models/document_line.py | 216 ++++ l10n_br_cte/models/document_related.py | 128 +++ l10n_br_cte/models/document_supplement.py | 22 + .../models/document_transported_vehicles.py | 43 + l10n_br_cte/models/dutoviario.py | 25 + l10n_br_cte/models/ferroviario.py | 55 + l10n_br_cte/models/normal_cte_infos.py | 153 +++ l10n_br_cte/models/res_company.py | 105 ++ l10n_br_cte/models/res_config_settings.py | 12 + l10n_br_cte/models/res_partner.py | 156 +++ l10n_br_cte/models/rodoviario.py | 76 ++ l10n_br_cte/readme/CONFIGURE.rst | 10 + l10n_br_cte/readme/CONTRIBUTORS.rst | 1 + l10n_br_cte/readme/DESCRIPTION.rst | 4 + l10n_br_cte/readme/USAGE.rst | 11 + l10n_br_cte/security/ir.model.access.csv | 17 + l10n_br_cte/static/description/icon.png | Bin 0 -> 30809 bytes l10n_br_cte/static/description/index.html | 460 ++++++++ l10n_br_cte/views/cte_document.xml | 138 +++ l10n_br_cte/views/document_line.xml | 13 + l10n_br_cte/views/document_related.xml | 17 + l10n_br_cte/views/res_company.xml | 33 + l10n_br_cte/views/res_partner.xml | 17 + setup/l10n_br_cte/odoo/addons/l10n_br_cte | 1 + setup/l10n_br_cte/setup.py | 6 + 39 files changed, 3195 insertions(+) create mode 100644 l10n_br_cte/README.rst create mode 100644 l10n_br_cte/__init__.py create mode 100644 l10n_br_cte/__manifest__.py create mode 100644 l10n_br_cte/constants/modal.py create mode 100644 l10n_br_cte/hooks.py create mode 100644 l10n_br_cte/modal/modal_aereo.xml create mode 100644 l10n_br_cte/modal/modal_aquaviario.xml create mode 100644 l10n_br_cte/modal/modal_ferroviario.xml create mode 100644 l10n_br_cte/modal/modal_rodoviario.xml create mode 100644 l10n_br_cte/models/__init__.py create mode 100644 l10n_br_cte/models/aereo.py create mode 100644 l10n_br_cte/models/aquaviario.py create mode 100644 l10n_br_cte/models/document.py create mode 100644 l10n_br_cte/models/document_cargo_quantity_infos.py create mode 100644 l10n_br_cte/models/document_line.py create mode 100644 l10n_br_cte/models/document_related.py create mode 100644 l10n_br_cte/models/document_supplement.py create mode 100644 l10n_br_cte/models/document_transported_vehicles.py create mode 100644 l10n_br_cte/models/dutoviario.py create mode 100644 l10n_br_cte/models/ferroviario.py create mode 100644 l10n_br_cte/models/normal_cte_infos.py create mode 100644 l10n_br_cte/models/res_company.py create mode 100644 l10n_br_cte/models/res_config_settings.py create mode 100644 l10n_br_cte/models/res_partner.py create mode 100644 l10n_br_cte/models/rodoviario.py create mode 100644 l10n_br_cte/readme/CONFIGURE.rst create mode 100644 l10n_br_cte/readme/CONTRIBUTORS.rst create mode 100644 l10n_br_cte/readme/DESCRIPTION.rst create mode 100644 l10n_br_cte/readme/USAGE.rst create mode 100644 l10n_br_cte/security/ir.model.access.csv create mode 100644 l10n_br_cte/static/description/icon.png create mode 100644 l10n_br_cte/static/description/index.html create mode 100644 l10n_br_cte/views/cte_document.xml create mode 100644 l10n_br_cte/views/document_line.xml create mode 100644 l10n_br_cte/views/document_related.xml create mode 100644 l10n_br_cte/views/res_company.xml create mode 100644 l10n_br_cte/views/res_partner.xml create mode 120000 setup/l10n_br_cte/odoo/addons/l10n_br_cte create mode 100644 setup/l10n_br_cte/setup.py diff --git a/l10n_br_cte/README.rst b/l10n_br_cte/README.rst new file mode 100644 index 000000000000..caafff26c57b --- /dev/null +++ b/l10n_br_cte/README.rst @@ -0,0 +1,113 @@ +==== +CT-e +==== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:c91616235e33e68d0115aa3807f25142a45f5013a23492f240cf507a41d41340 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png + :target: https://odoo-community.org/page/development-status + :alt: Alpha +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fl10n--brazil-lightgray.png?logo=github + :target: https://github.com/OCA/l10n-brazil/tree/14.0/l10n_br_cte + :alt: OCA/l10n-brazil +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/l10n-brazil-14-0/l10n-brazil-14-0-l10n_br_cte + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/l10n-brazil&target_branch=14.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +[ This file must be max 2-3 paragraphs, and is required. ] + +This module extends the functionality of ... to support ... +and to allow you to ... + +.. IMPORTANT:: + This is an alpha version, the data model and design can change at any time without warning. + Only for development or testing purpose, do not use in production. + `More details on development status `_ + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +[ This file is optional, it should explain how to configure + the module before using it; it is aimed at advanced users. ] + +To configure this module, you need to: + +#. Go to ... + +.. figure:: https://raw.githubusercontent.com/OCA/l10n-brazil/14.0/l10n_br_cte/static/description/image.png + :alt: alternative description + :width: 600 px + +Usage +===== + +[ This file must be present and contains the usage instructions + for end-users. As all other rst files included in the README, + it MUST NOT contain reStructuredText sections + only body text (paragraphs, lists, tables, etc). Should you need + a more elaborate structure to explain the addon, please create a + Sphinx documentation (which may include this file as a "quick start" + section). ] + +To use this module, you need to: + +#. Go to ... + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* KMEE + +Contributors +~~~~~~~~~~~~ + +* Ygor Carvalho + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +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. + +This module is part of the `OCA/l10n-brazil `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/l10n_br_cte/__init__.py b/l10n_br_cte/__init__.py new file mode 100644 index 000000000000..cc6b6354ad8f --- /dev/null +++ b/l10n_br_cte/__init__.py @@ -0,0 +1,2 @@ +from . import models +from .hooks import post_init_hook diff --git a/l10n_br_cte/__manifest__.py b/l10n_br_cte/__manifest__.py new file mode 100644 index 000000000000..c88d09f126d5 --- /dev/null +++ b/l10n_br_cte/__manifest__.py @@ -0,0 +1,42 @@ +# Copyright 2023 KMEE +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "CT-e", + "summary": """Brazilian Electronic Invoice CT-e""", + "version": "14.0.1.0.0", + "category": "Localisation", + "license": "AGPL-3", + "author": "KMEE, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/l10n-brazil", + "development_status": "Alpha", + "depends": [ + "l10n_br_fiscal", + "l10n_br_cte_spec", + "l10n_br_fiscal_certificate", + "spec_driven_model", + ], + "data": [ + "security/ir.model.access.csv", + # "views/document_line.xml", + # 'views/document_related.xml', + # 'views/res_partner.xml', + "modal/modal_rodoviario.xml", + "modal/modal_aquaviario.xml", + "modal/modal_ferroviario.xml", + "modal/modal_aereo.xml", + "views/res_company.xml", + "views/cte_document.xml", + ], + "post_init_hook": "post_init_hook", + "installable": True, + "auto_install": False, + "external_dependencies": { + "python": [ + "nfelib>=2.0.0", + "erpbrasil.assinatura>=1.7.0", + "erpbrasil.transmissao>=1.1.0", + "erpbrasil.edoc>=2.5.2", + ], + }, +} diff --git a/l10n_br_cte/constants/modal.py b/l10n_br_cte/constants/modal.py new file mode 100644 index 000000000000..42dcd1d85a53 --- /dev/null +++ b/l10n_br_cte/constants/modal.py @@ -0,0 +1,32 @@ +CTE_MODAL_VERSION_DEFAULT = "4.00" + +TUF = [ + ("AC", "AC"), + ("AL", "AL"), + ("AM", "AM"), + ("AP", "AP"), + ("BA", "BA"), + ("CE", "CE"), + ("DF", "DF"), + ("ES", "ES"), + ("GO", "GO"), + ("MA", "MA"), + ("MG", "MG"), + ("MS", "MS"), + ("MT", "MT"), + ("PA", "PA"), + ("PB", "PB"), + ("PE", "PE"), + ("PI", "PI"), + ("PR", "PR"), + ("RJ", "RJ"), + ("RN", "RN"), + ("RO", "RO"), + ("RR", "RR"), + ("RS", "RS"), + ("SC", "SC"), + ("SE", "SE"), + ("SP", "SP"), + ("TO", "TO"), + ("EX", "EX"), +] diff --git a/l10n_br_cte/hooks.py b/l10n_br_cte/hooks.py new file mode 100644 index 000000000000..c01e14a97c2b --- /dev/null +++ b/l10n_br_cte/hooks.py @@ -0,0 +1,21 @@ +# Copyright (C) 2019-2020 - Raphael Valyi Akretion +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html +from odoo import SUPERUSER_ID, api + +from odoo.addons.spec_driven_model import hooks + + +def post_init_hook(cr, registry): + env = api.Environment(cr, SUPERUSER_ID, {}) + hooks.register_hook( + env, + "l10n_br_cte", + "odoo.addons.l10n_br_cte_spec.models.v4_0.cte_tipos_basico_v4_00", + ) + + hooks.post_init_hook( + cr, + registry, + "l10n_br_cte", + "odoo.addons.l10n_br_cte_spec.models.v4_0.cte_tipos_basico_v4_00", + ) diff --git a/l10n_br_cte/modal/modal_aereo.xml b/l10n_br_cte/modal/modal_aereo.xml new file mode 100644 index 000000000000..eaa86fccea46 --- /dev/null +++ b/l10n_br_cte/modal/modal_aereo.xml @@ -0,0 +1,21 @@ + + + + + modal.aereo.peri.form.view (in l10n_br_cte) + l10n_br_cte.modal.aereo.peri + +
+ + + + + + + + +
+
+
+
diff --git a/l10n_br_cte/modal/modal_aquaviario.xml b/l10n_br_cte/modal/modal_aquaviario.xml new file mode 100644 index 000000000000..28933d4a09eb --- /dev/null +++ b/l10n_br_cte/modal/modal_aquaviario.xml @@ -0,0 +1,18 @@ + + + + + modal.aquaviario.balsa.form.view (in l10n_br_cte) + l10n_br_cte.modal.aquav.balsa + +
+ + + + + +
+
+
+
diff --git a/l10n_br_cte/modal/modal_ferroviario.xml b/l10n_br_cte/modal/modal_ferroviario.xml new file mode 100644 index 000000000000..2c11907ab735 --- /dev/null +++ b/l10n_br_cte/modal/modal_ferroviario.xml @@ -0,0 +1,51 @@ + + + + + res.partner.ferroenv.form.view (in l10n_br_cte) + res.partner + +
+ + + + + + + + + +
+
+
+ + + res.partner.tenderfer.form.view + res.partner + +
+ + + + + + + + + + + + +
+
+
+
diff --git a/l10n_br_cte/modal/modal_rodoviario.xml b/l10n_br_cte/modal/modal_rodoviario.xml new file mode 100644 index 000000000000..338957199239 --- /dev/null +++ b/l10n_br_cte/modal/modal_rodoviario.xml @@ -0,0 +1,25 @@ + + + + + modal.rodoviario.occ.form.view (in l10n_br_cte) + l10n_br_cte.modal.rodo.occ + +
+ + + + + + + + + + + + +
+
+
+
diff --git a/l10n_br_cte/models/__init__.py b/l10n_br_cte/models/__init__.py new file mode 100644 index 000000000000..3aac89ccbd32 --- /dev/null +++ b/l10n_br_cte/models/__init__.py @@ -0,0 +1,15 @@ +from . import document +from . import res_company +from . import res_partner +from . import document_related +from . import document_line +from . import res_config_settings +from . import ferroviario +from . import rodoviario +from . import aereo +from . import dutoviario +from . import aquaviario +from . import document_cargo_quantity_infos +from . import document_supplement +from . import document_transported_vehicles +from . import normal_cte_infos diff --git a/l10n_br_cte/models/aereo.py b/l10n_br_cte/models/aereo.py new file mode 100644 index 000000000000..f155c5b7230a --- /dev/null +++ b/l10n_br_cte/models/aereo.py @@ -0,0 +1,69 @@ +# Copyright 2023 KMEE INFORMATICA LTDA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + + +from odoo import fields + +from odoo.addons.spec_driven_model.models import spec_models + + +class Aereo(spec_models.StackedModel): + _name = "l10n_br_cte.modal.aereo" + _inherit = "cte.40.aereo" + _stacked = "cte.40.aereo" + _binding_module = "nfelib.cte.bindings.v4_0.cte_modal_aereo_v4_00" + _field_prefix = "cte40_" + _schema_name = "cte" + _schema_version = "4.0.0" + _odoo_module = "l10n_br_cte" + _spec_module = "odoo.addons.l10n_br_cte_spec.models.v4_0.cte_modal_aereo_v4_00" + _spec_tab_name = "CTe" + _description = "Modal Aereo CTe" + + document_id = fields.Many2one(comodel_name="l10n_br_fiscal.document") + + cte40_nMinu = fields.Char(related="document_id.cte40_nMinu") + + cte40_nOCA = fields.Char(related="document_id.cte40_nOCA") + + cte40_dPrevAereo = fields.Date(related="document_id.cte40_dPrevAereo") + + cte40_CL = fields.Char(related="document_id.cte40_CL") + + cte40_cTar = fields.Char(related="document_id.cte40_cTar") + + cte40_vTar = fields.Monetary(related="document_id.cte40_aereo_vTar") + + cte40_xDime = fields.Char(related="document_id.cte40_xDime") + + cte40_peri = fields.One2many(related="document_id.cte40_peri") + + def _prepare_dacte_values(self): + if not self: + return {} + + +class Peri(spec_models.StackedModel): + _name = "l10n_br_cte.modal.aereo.peri" + _inherit = "cte.40.peri" + _stacked = "cte.40.peri" + _binding_module = "nfelib.cte.bindings.v4_0.cte_modal_aereo_v4_00" + _field_prefix = "cte40_" + _schema_name = "cte" + _schema_version = "4.0.0" + _odoo_module = "l10n_br_cte" + _spec_module = "odoo.addons.l10n_br_cte_spec.models.v4_0.cte_modal_aereo_v4_00" + _spec_tab_name = "CTe" + _description = """Preenchido quando for transporte de produtos classificados pela ONU como + perigosos. O preenchimento desses campos não desobriga a empresa aérea de emitir os demais + documentos que constam na legislação vigente.""" + + document_id = fields.Many2one(comodel_name="l10n_br_fiscal.document") + + cte40_nONU = fields.Char(required=True) + + cte40_qTotEmb = fields.Char(required=True) + + cte40_qTotProd = fields.Float(required=True) + + cte40_uniAP = fields.Selection(required=True) diff --git a/l10n_br_cte/models/aquaviario.py b/l10n_br_cte/models/aquaviario.py new file mode 100644 index 000000000000..3e5e7107c957 --- /dev/null +++ b/l10n_br_cte/models/aquaviario.py @@ -0,0 +1,52 @@ +# Copyright 2023 KMEE INFORMATICA LTDA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields + +from odoo.addons.spec_driven_model.models import spec_models + + +class Aquav(spec_models.StackedModel): + _name = "l10n_br_cte.modal.aquav" + _inherit = "cte.40.aquav" + _stacked = "cte.40.aquav" + _binding_module = "nfelib.cte.bindings.v4_0.cte_modal_aquaviario_v4_00" + _field_prefix = "cte40_" + _schema_name = "cte" + _schema_version = "4.0.0" + _odoo_module = "l10n_br_cte" + _spec_module = "odoo.addons.l10n_br_cte_spec.models.v4_0.cte_modal_aquaviario_v4_00" + _spec_tab_name = "CTe" + _description = "Modal Aquaviário CTe" + + document_id = fields.Many2one(comodel_name="l10n_br_fiscal.document") + + cte40_vAFRMM = fields.Monetary(related="document_id.cte40_vAFRMM") + + cte40_vPrest = fields.Monetary(related="document_id.cte40_vPrest") + + cte40_xNavio = fields.Char(related="document_id.cte40_xNavio") + + cte40_nViag = fields.Char(related="document_id.cte40_nViag") + + cte40_direc = fields.Selection(related="document_id.cte40_direc") + + cte40_irin = fields.Char(related="document_id.cte40_irin") + + cte40_tpNav = fields.Selection(related="document_id.cte40_tpNav") + + cte40_balsa = fields.One2many(related="document_id.cte40_balsa") + + def _prepare_dacte_values(self): + if not self: + return {} + + +class Balsa(spec_models.SpecModel): + _name = "l10n_br_cte.modal.aquav.balsa" + _inherit = "cte.40.balsa" + _description = "Grupo de informações das balsas" + + document_id = fields.Many2one(comodel_name="l10n_br_fiscal.document") + + cte40_xBalsa = fields.Char(string="Identificador da Balsa") diff --git a/l10n_br_cte/models/document.py b/l10n_br_cte/models/document.py new file mode 100644 index 000000000000..20b0229e4832 --- /dev/null +++ b/l10n_br_cte/models/document.py @@ -0,0 +1,988 @@ +# Copyright 2023 KMEE INFORMATICA LTDA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import logging +import re +from datetime import datetime + +from erpbrasil.transmissao import TransmissaoSOAP +from nfelib.cte.bindings.v4_0.cte_v4_00 import Cte +from nfelib.nfe.ws.edoc_legacy import CTeAdapter as edoc_cte +from requests import Session + +from odoo import _, api, fields +from odoo.exceptions import UserError + +from odoo.addons.l10n_br_cte_spec.models.v4_0.cte_modal_ferroviario_v4_00 import ( + FERROV_TPTRAF, + TRAFMUT_FERREMI, + TRAFMUT_RESPFAT, +) +from odoo.addons.l10n_br_fiscal.constants.fiscal import ( + AUTORIZADO, + CANCELADO, + CANCELADO_DENTRO_PRAZO, + CANCELADO_FORA_PRAZO, + DENEGADO, + EVENT_ENV_HML, + EVENT_ENV_PROD, + LOTE_PROCESSADO, + SITUACAO_EDOC_AUTORIZADA, + SITUACAO_EDOC_CANCELADA, + SITUACAO_EDOC_DENEGADA, + SITUACAO_EDOC_REJEITADA, + SITUACAO_FISCAL_CANCELADO, + SITUACAO_FISCAL_CANCELADO_EXTEMPORANEO, +) +from odoo.addons.spec_driven_model.models import spec_models + +from ..constants.modal import CTE_MODAL_VERSION_DEFAULT + +_logger = logging.getLogger(__name__) +try: + pass +except ImportError: + _logger.error("Biblioteca erpbrasil.base não in stalada") + + +def filter_processador_edoc_cte(record): + if record.processador_edoc == "oca" and record.document_type_id.code in [ + "57", + "67", + ]: + return True + return False + + +class CTe(spec_models.StackedModel): + + _name = "l10n_br_fiscal.document" + _inherit = ["l10n_br_fiscal.document", "cte.40.tcte_infcte", "cte.40.tcte_fat"] + _stacked = "cte.40.tcte_infcte" + _field_prefix = "cte40_" + _schema_name = "cte" + _schema_version = "4.0.0" + _odoo_module = "l10n_br_cte" + _spec_module = "odoo.addons.l10n_br_cte_spec.models.v4_0.cte_tipos_basico_v4_00" + _binding_module = "nfelib.cte.bindings.v4_0.cte_tipos_basico_v4_00" + _cte_search_keys = ["cte40_Id"] + + INFCTE_TREE = """ + > infCte + > + - res.partner + > res.company + > res.partner + > + > + - + - + > + - + - """ + + ########################## + # CT-e spec related fields + ########################## + + ########################## + # CT-e tag: infCte + ########################## + + cte40_versao = fields.Char(related="document_version") + + cte40_Id = fields.Char( + compute="_compute_cte40_Id", + inverse="_inverse_cte40_Id", + ) + + ########################## + # CT-e tag: Id + # Methods + ########################## + + @api.depends("document_type_id", "document_key") + def _compute_cte40_Id(self): + for record in self.filtered(filter_processador_edoc_cte): + if ( + record.document_type_id + and record.document_type_id.prefix + and record.document_key + ): + record.cte40_Id = "{}{}".format( + record.document_type_id.prefix, record.document_key + ) + else: + record.cte40_Id = False + + def _inverse_cte40_Id(self): + for record in self: + if record.cte40_Id: + record.document_key = re.findall(r"\d+", str(record.cte40_Id))[0] + + ########################## + # CT-e tag: ide + ########################## + + cte40_cUF = fields.Char( + related="company_id.partner_id.state_id.ibge_code", + string="cte40_cUF", + ) + + cte40_cCT = fields.Char(compute="_compute_cct") + + cte40_CFOP = fields.Char(compute="_compute_CFOP", store=True) + + cte40_natOp = fields.Char(related="operation_name") + + cte40_mod = fields.Char(related="document_type_id.code", string="cte40_mod") + + cte40_serie = fields.Char(related="document_serie") + + cte40_nCT = fields.Char(related="document_number") + + cte40_dhEmi = fields.Datetime(related="document_date") + + cte40_cDV = fields.Char(compute="_compute_cDV", store=True) + + cte40_procEmi = fields.Selection(default="0") + + cte40_verProc = fields.Char( + copy=False, + default=lambda s: s.env["ir.config_parameter"] + .sudo() + .get_param("l10n_br_cte.version.name", default="Odoo Brasil OCA v14"), + ) + + cte40_cMunEnv = fields.Char(compute="_compute_cte40_data", store=True) + + cte40_xMunEnv = fields.Char(compute="_compute_cte40_data", store=True) + + cte40_UFEnv = fields.Char( + compute="_compute_cte40_data", string="cte40_UFEnv", store=True + ) + + cte40_indIEToma = fields.Selection( + selection=[ + ("1", "Contribuinte ICMS"), + ("2", "Contribuinte isento de inscrição"), + ("9", "Não Contribuinte"), + ], + default="1", + ) + + cte40_cMunIni = fields.Char(compute="_compute_cte40_data", store=True) + + cte40_xMunIni = fields.Char(compute="_compute_cte40_data", store=True) + + cte40_UFIni = fields.Char(compute="_compute_cte40_data", store=True) + + cte40_cMunFim = fields.Char( + compute="_compute_cte40_data", + related="partner_id.city_id.ibge_code", + store=True, + ) + + cte40_xMunFim = fields.Char( + compute="_compute_cte40_data", related="partner_id.city_id.name", store=True + ) + + cte40_UFFim = fields.Char( + compute="_compute_cte40_data", string="cte40_cUF", store=True + ) + + cte40_retira = fields.Selection(selection=[("0", "Sim"), ("1", "Não")], default="1") + + cte40_tpServ = fields.Selection( + selection=[ + ("0", "Normal"), + ("1", "Subcontratação"), + ("2", "Redespacho"), + ("3", "Redespacho Intermediário"), + ], + default="0", + ) + + cte40_tpCTe = fields.Selection( + selection=[ + ("0", "CTe Normal"), + ("1", "CTe Complementar"), + ("3", "CTe Substituição"), + ], + default="0", + ) + + cte40_tpAmb = fields.Selection( + selection=[("1", "Produção"), ("2", "Homologação")], + string="CTe Environment", + copy=False, + default="2", + ) + + cte40_tpEmis = fields.Selection( + selection=[ + ("1", "Normal"), + ("3", "Regime Especial NFF"), + ("4", "EPEC pela SVC"), + ], + default="1", + ) + + cte40_tpImp = fields.Selection( + selection=[("1", "Retrato"), ("2", "Paisagem")], default="1" + ) + + def _export_fields_cte_40_toma3(self, xsd_fields, class_obj, export_dict): + if self.cte40_choice_toma == "cte40_toma4": + xsd_fields.remove("cte40_toma") + + def _export_fields_cte_40_tcte_toma4(self, xsd_fields, class_obj, export_dict): + if self.cte40_choice_toma == "cte40_toma3": + xsd_fields.remove("cte40_toma") + xsd_fields.remove("cte40_CNPJ") + xsd_fields.remove("cte40_CPF") + xsd_fields.remove("cte40_IE") + xsd_fields.remove("cte40_xNome") + xsd_fields.remove("cte40_xFant") + xsd_fields.remove("cte40_enderToma") + + # toma + cte40_choice_toma = fields.Selection( + selection=[ + ("cte40_toma3", "toma3"), + ("cte40_toma4", "toma4"), + ], + compute="_compute_toma", + store=True, + ) + + cte40_toma = fields.Selection(related="service_provider") + + cte40_CNPJ = fields.Char( + related="partner_id.cte40_CNPJ", + ) + cte40_CPF = fields.Char( + related="partner_id.cte40_CPF", + ) + cte40_IE = fields.Char( + related="partner_id.cte40_IE", + ) + cte40_xNome = fields.Char( + related="partner_id.legal_name", + ) + cte40_xFant = fields.Char( + related="partner_id.name", + ) + + cte40_enderToma = fields.Many2one(comodel_name="res.partner", related="partner_id") + + ########################## + # CT-e tag: ide + # Compute Methods + ########################## + + @api.depends("service_provider") + def _compute_toma(self): + for doc in self: + if doc.service_provider in ["0", "1", "2", "3"]: + doc.cte40_choice_toma = "cte40_toma3" + else: + doc.cte40_choice_toma = "cte40_toma4" + + @api.depends("fiscal_line_ids") + def _compute_CFOP(self): + for rec in self: + if rec.fiscal_line_ids: + rec.cte40_CFOP = rec.fiscal_line_ids[0].cfop_id.code + + @api.depends("document_key") + def _compute_cDV(self): + for rec in self: + if rec.document_key: + rec.cte40_cDV = rec.document_key[-1] + + def _compute_cct(self): + for rec in self: + if rec.document_key: + rec.cte40_cCT = rec.document_key[35:43] + + @api.depends("partner_id", "company_id") + def _compute_cte40_data(self): + for doc in self: + if doc.company_id.partner_id.country_id == doc.partner_id.country_id: + doc.cte40_xMunIni = doc.company_id.partner_id.city_id.name + doc.cte40_cMunIni = doc.company_id.partner_id.city_id.ibge_code + doc.cte40_xMunEnv = doc.company_id.partner_id.city_id.name + doc.cte40_cMunEnv = doc.company_id.partner_id.city_id.ibge_code + doc.cte40_UFEnv = doc.company_id.partner_id.state_id.code + doc.cte40_UFIni = doc.company_id.partner_id.state_id.code + doc.cte40_cMunFim = doc.partner_id.city_id.ibge_code + doc.cte40_xMunFim = doc.partner_id.city_id.name + doc.cte40_UFFim = doc.partner_id.state_id.code + else: + doc.cte40_UFIni = "EX" + doc.cte40_UFEnv = "EX" + doc.cte40_xMunIni = "EXTERIOR" + doc.cte40_cMunIni = "9999999" + doc.cte40_xMunEnv = ( + doc.company_id.partner_id.country_id.name + + "/" + + doc.company_id.partner_id.city_id.name + ) + doc.cte40_cMunEnv = "9999999" + doc.cte40_cMunFim = "9999999" + doc.cte40_xMunFim = "EXTERIOR" + doc.cte40_UFFim = "EX" + + ########################## + # CT-e tag: emit + ########################## + + cte40_emit = fields.Many2one( + comodel_name="res.company", + compute="_compute_emit_data", + readonly=True, + string="Emit", + ) + + cte40_CRT = fields.Selection( + related="company_tax_framework", + string="Código de Regime Tributário (NFe)", + ) + + ########################## + # CT-e tag: emit + # Compute Methods + ########################## + + def _compute_emit_data(self): + for doc in self: # TODO if out + doc.cte40_emit = doc.company_id + + ########################## + # CT-e tag: rem + ########################## + + cte40_rem = fields.Many2one( + comodel_name="res.partner", + compute="_compute_rem_data", + readonly=True, + string="Rem", + ) + + ########################## + # CT-e tag: rem + # Compute Methods + ########################## + + def _compute_rem_data(self): + for doc in self: # TODO if out + doc.cte40_rem = doc.partner_id + + ########################## + # CT-e tag: exped + ########################## + + cte40_exped = fields.Many2one( + comodel_name="res.partner", + compute="_compute_exped_data", + readonly=True, + string="Exped", + ) + + ########################## + # CT-e tag: exped + # Compute Methods + ########################## + + def _compute_exped_data(self): + for doc in self: # TODO if out + doc.cte40_exped = doc.company_id.partner_id + + ########################## + # CT-e tag: dest + ########################## + + cte40_dest = fields.Many2one( + comodel_name="res.partner", + compute="_compute_dest_data", + readonly=True, + string="Dest", + ) + + ########################## + # CT-e tag: dest + # Compute Methods + ########################## + + def _compute_dest_data(self): + for doc in self: # TODO if out + doc.cte40_dest = doc.partner_shipping_id + + ########################## + # CT-e tag: imp TODO + ########################## + + cte40_imp = fields.One2many( + comodel_name="l10n_br_fiscal.document.line", + inverse_name="document_id", + related="fiscal_line_ids", + ) + + ########################## + # CT-e tag: imp + # Compute Methods + ########################## + + def _compute_imp(self): + for doc in self: + doc.cte40_ICMS = doc.fiscal_line_ids + + ##################################### + # CT-e tag: infCTeNorm and infCteComp + ##################################### + + cte40_choice_infcteNorm_infcteComp = fields.Selection( + selection=[ + ("cte40_infCTeComp", "infCTeComp"), + ("cte40_infCTeNorm", "infCTeNorm"), + ], + default="cte40_infCTeNorm", + ) + + cte40_infCTeNorm = fields.One2many( + comodel_name="l10n_br_cte.normal.infos", + inverse_name="document_id", + ) + + # cte40_infCTeComp = fields.One2many( + # comodel_name="l10n_br_fiscal.document.related", + # inverse_name="document_id", + # ) + + ########################## + # CT-e tag: infCarga + ########################## + + cte40_vCarga = fields.Monetary( + string="Valor total da carga", + ) + + cte40_proPred = fields.Char( + string="Produto predominante", + required=True, + ) + + cte40_xOutCat = fields.Char( + string="Outras características da carga", + ) + + cte40_infQ = fields.One2many( + comodel_name="l10n_br_cte.cargo.quantity.infos", + inverse_name="document_id", + ) + + cte40_vCargaAverb = fields.Monetary( + string="Valor da Carga para efeito de averbação", + ) + + ########################## + # CT-e tag: veicNovos + ########################## + + cte40_veicNovos = fields.One2many( + comodel_name="l10n_br_cte.transported.vehicles", + inverse_name="document_id", + ) + + ########################## + # CT-e tag: autXML + # Compute Methods + ########################## + + def _default_cte40_autxml(self): + company = self.env.company + authorized_partners = [] + if company.accountant_id: + authorized_partners.append(company.accountant_id.id) + if company.technical_support_id: + authorized_partners.append(company.technical_support_id.id) + return authorized_partners + + ########################## + # CT-e tag: autXML + ########################## + + cte40_autXML = fields.One2many(default=_default_cte40_autxml) + + ########################## + # NF-e tag: infCTeSupl + ########################## + + cte40_infCTeSupl = fields.Many2one( + comodel_name="l10n_br_fiscal.document.supplement", + ) + + ########################## + # MDF-e tag: infRespTec + ########################## + + cte40_infRespTec = fields.Many2one( + comodel_name="res.partner", + compute="_compute_infresptec", + string="Responsável Técnico CTe", + ) + + ########################## + # MDF-e tag: infRespTec + # Methods + ########################## + + @api.depends("company_id.technical_support_id") + def _compute_infresptec(self): + for record in self.filtered(filter_processador_edoc_cte): + record.cte40_infRespTec = record.company_id.technical_support_id + + ########################## + # CT-e tag: infmodal + ########################## + + cte40_modal = fields.Selection(related="transport_modal") + + cte40_versaoModal = fields.Char(default=CTE_MODAL_VERSION_DEFAULT) + + # Campos do Modal Aereo + modal_aereo_id = fields.Many2one(comodel_name="l10n_br_cte.modal.aereo") + + cte40_nMinu = fields.Char( + string="Número da Minuta", + help=( + "Número da Minuta\nDocumento que precede o CT-e, assinado pelo " + "expedidor, espécie de pedido de serviço" + ), + ) + + cte40_nOCA = fields.Char( + string="Número Operacional do Conhecimento Aéreo", + help=( + "Número Operacional do Conhecimento Aéreo\nRepresenta o número de " + "controle comumente utilizado pelo conhecimento aéreo composto por" + " uma sequência numérica de onze dígitos. Os três primeiros " + "dígitos representam um código que os operadores de transporte " + "aéreo associados à IATA possuem. Em seguida um número de série de" + " sete dígitos determinados pelo operador de transporte aéreo. " + "Para finalizar, um dígito verificador, que é um sistema de módulo" + " sete imponderado o qual divide o número de série do conhecimento" + " aéreo por sete e usa o resto como dígito de verificação." + ), + ) + + cte40_dPrevAereo = fields.Date( + string="Data prevista da entrega", + help="Data prevista da entrega\nFormato AAAA-MM-DD", + ) + + cte40_xDime = fields.Char( + string="Dimensão", + help=( + "Dimensão\nFormato:1234X1234X1234 (cm). Esse campo deve sempre que" + " possível ser preenchido. Entretanto, quando for impossível o " + "preenchimento das dimensões, fica obrigatório o preenchimento da " + "cubagem em metro cúbico do leiaute do CT-e da estrutura genérica " + "(infQ)." + ), + ) + + cte40_CL = fields.Char( + string="Classe", + help=( + "Classe\nPreencher com:\n\t\t\t\t\t\t\t\t\tM - Tarifa " + "Mínima;\n\t\t\t\t\t\t\t\t\tG - Tarifa Geral;\n\t\t\t\t\t\t\t\t\tE" + " - Tarifa Específica" + ), + ) + + cte40_cTar = fields.Char( + string="Código da Tarifa", + help=( + "Código da Tarifa\nDeverão ser incluídos os códigos de três " + "dígitos, correspondentes à tarifa." + ), + ) + # Existem dois vTar no spec, um float e um monetary, por isso a mudança de nome + cte40_aereo_vTar = fields.Monetary( + string="Valor da Tarifa", + currency_field="brl_currency_id", + help="Valor da Tarifa\nValor da tarifa por kg quando for o caso.", + ) + + cte40_peri = fields.One2many( + comodel_name="l10n_br_cte.modal.aereo.peri", + inverse_name="document_id", + string="Dados de carga perigosa", + ) + + # Campos do Modal Aquaviario + modal_aquaviario_id = fields.Many2one(comodel_name="l10n_br_cte.modal.aquav") + + cte40_vPrest = fields.Monetary( + compute="_compute_cte40_vPrest", # FIX + store=True, + string="Valor da Prestação Base de Cálculo", + ) + + cte40_vAFRMM = fields.Monetary( + string="AFRMM", + currency_field="brl_currency_id", + help=("AFRMM (Adicional de Frete para Renovação da Marinha Mercante)"), + ) + + cte40_xNavio = fields.Char(string="Identificação do Navio") + + cte40_nViag = fields.Char(string="Número da Viagem") + + cte40_direc = fields.Selection( + selection=[ + ("N", "Norte, L-Leste, S-Sul, O-Oeste"), + ("S", "Sul, O-Oeste"), + ("L", "Leste, S-Sul, O-Oeste"), + ("O", "Oeste"), + ], + string="Direção", + help="Direção\nPreencher com: N-Norte, L-Leste, S-Sul, O-Oeste", + ) + + cte40_irin = fields.Char( + string="Irin do navio", + help="Irin do navio sempre deverá ser informado", + ) + + cte40_tpNav = fields.Selection( + selection=[ + ("0", "Interior"), + ("1", "Cabotagem"), + ], + string="Tipo de Navegação", + help=( + "Tipo de Navegação\nPreencher com: \n\t\t\t\t\t\t0 - " + "Interior;\n\t\t\t\t\t\t1 - Cabotagem" + ), + ) + + cte40_balsa = fields.One2many( + comodel_name="l10n_br_cte.modal.aquav.balsa", + inverse_name="document_id", + string="Grupo de informações das balsas", + ) + + # Campos do Modal Dutoviario + modal_dutoviario_id = fields.Many2one(comodel_name="l10n_br_cte.modal.duto") + + cte40_dIni = fields.Date(string="Data de Início da prestação do serviço") + + cte40_dFim = fields.Date(string="Data de Fim da prestação do serviço") + + cte40_vTar = fields.Float(string="Valor da tarifa") + + # Campos do Modal Ferroviario + modal_ferroviario_id = fields.Many2one(comodel_name="l10n_br_cte.modal.ferrov") + + cte40_tpTraf = fields.Selection( + selection=FERROV_TPTRAF, + default="0", + string="Tipo de Tráfego", + ) + + cte40_fluxo = fields.Char( + string="Fluxo Ferroviário", + help=( + "Fluxo Ferroviário\nTrata-se de um número identificador do " + "contrato firmado com o cliente" + ), + ) + + cte40_vFrete = fields.Monetary( + related="amount_freight_value", + string="Valor do Frete do Tráfego Mútuo", + currency_field="brl_currency_id", + ) + + cte40_respFat = fields.Selection( + TRAFMUT_RESPFAT, + string="Responsável pelo Faturamento", + ) + + cte40_ferrEmi = fields.Selection( + TRAFMUT_FERREMI, + string="Ferrovia Emitente do CTe", + help=( + "Ferrovia Emitente do CTe\nPreencher com: " + "\n\t\t\t\t\t\t\t\t\t1-Ferrovia de origem; " + "\n\t\t\t\t\t\t\t\t\t2-Ferrovia de destino" + ), + ) + + cte40_chCTeFerroOrigem = fields.Char( + string="Chave de acesso do CT-e emitido", + help="Chave de acesso do CT-e emitido pelo ferrovia de origem", + ) + + cte40_ferroEnv = fields.Many2many( + comodel_name="res.partner", + string="Informações das Ferrovias Envolvidas", + ) + + # Campos do Modal rodoviario + modal_rodoviario_id = fields.Many2one(comodel_name="l10n_br_cte.modal.rodo") + + cte40_RNTRC = fields.Char( + string="RNTRC", + help="Registro Nacional de Transportadores Rodoviários de Carga", + ) + + cte40_occ = fields.One2many( + comodel_name="l10n_br_cte.modal.rodo.occ", + inverse_name="document_id", + string="Ordens de Coleta associados", + ) + + ########################## + # CT-e tag: infmodal + # Compute Methods + ########################## + + def _compute_cte40_vPrest(self): + vPrest = 0 + for record in self.fiscal_line_ids: + vPrest += record.cte40_vTPrest + self.cte40_vPrest = vPrest + + def _export_fields_cte_40_tcte_infmodal(self, xsd_fields, class_obj, export_dict): + self = self.with_context(module="l10n_br_cte") + if self.cte40_modal == "01": + export_dict["any_element"] = self._export_modal_rodoviario() + elif self.cte40_modal == "02": + export_dict["any_element"] = self._export_modal_aereo() + elif self.cte40_modal == "03": + export_dict["any_element"] = self._export_modal_aquaviario() + elif self.cte40_modal == "04": + export_dict["any_element"] = self._export_modal_ferroviario() + elif self.cte40_modal == "05": + export_dict["any_element"] = self._export_modal_dutoviario() + + def _export_modal_aereo(self): + if not self.modal_aereo_id: + self.modal_aereo_id = self.modal_aereo_id.create({"document_id": self.id}) + + return self.modal_aereo_id.export_ds()[0] + + def _export_modal_ferroviario(self): + if not self.modal_ferroviario_id: + self.modal_ferroviario_id = self.modal_ferroviario_id.create( + {"document_id": self.id} + ) + + return self.modal_ferroviario_id.export_ds()[0] + + def _export_modal_aquaviario(self): + if not self.modal_aquaviario_id: + self.modal_aquaviario_id = self.modal_aquaviario_id.create( + {"document_id": self.id} + ) + + return self.modal_aquaviario_id.export_ds()[0] + + def _export_modal_rodoviario(self): + if not self.modal_rodoviario_id: + self.modal_rodoviario_id = self.modal_rodoviario_id.create( + {"document_id": self.id} + ) + + return self.modal_rodoviario_id.export_ds()[0] + + def _export_modal_dutoviario(self): + if not self.modal_dutoviario_id: + self.modal_dutoviario_id = self.modal_dutoviario_id.create( + {"document_id": self.id} + ) + + return self.modal_dutoviario_id.export_ds()[0] + + ################################ + # Business Model Methods + ################################ + + def _serialize(self, edocs): + edocs = super()._serialize(edocs) + for record in self.with_context(lang="pt_BR").filtered( + filter_processador_edoc_cte + ): + inf_cte = record.export_ds()[0] + cte = Cte(infCte=inf_cte, infCTeSupl=None, signature=None) + edocs.append(cte) + return edocs + + def _processador(self): + if not self.company_id.certificate_nfe_id: + raise UserError(_("Certificado não encontrado")) + + certificado = self.env.company._get_br_ecertificate() + session = Session() + session.verify = False + transmissao = TransmissaoSOAP(certificado, session) + return edoc_cte( + transmissao, + self.company_id.state_id.ibge_code, + self.cte40_versao, + self.cte40_tpAmb, + ) + + def _document_export(self, pretty_print=True): + result = super()._document_export() + for record in self.filtered(filter_processador_edoc_cte): + edoc = record.serialize()[0] + processador = record._processador() + xml_file = edoc.to_xml() + event_id = self.event_ids.create_event_save_xml( + company_id=self.company_id, + environment=( + EVENT_ENV_PROD if self.cte40_tpAmb == "1" else EVENT_ENV_HML + ), + event_type="0", + xml_file=xml_file, + document_id=self, + ) + record.authorization_event_id = event_id + xml_assinado = processador.assina_raiz(edoc, edoc.infCte.Id) + self._valida_xml(xml_assinado) + return result + + def _valida_xml(self, xml_file): + self.ensure_one() + erros = Cte.schema_validation(xml_file) + erros = "\n".join(erros) + self.write({"xml_error_message": erros or False}) + + def atualiza_status_cte(self, infProt, xml_file): + self.ensure_one() + if infProt.cStat in AUTORIZADO: + state = SITUACAO_EDOC_AUTORIZADA + elif infProt.cStat in DENEGADO: + state = SITUACAO_EDOC_DENEGADA + else: + state = SITUACAO_EDOC_REJEITADA + if self.authorization_event_id and infProt.nProt: + if type(infProt.dhRecbto) == datetime: + protocol_date = fields.Datetime.to_string(infProt.dhRecbto) + 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=xml_file, + ) + self.write( + { + "status_code": infProt.cStat, + "status_name": infProt.xMotivo, + } + ) + self._change_state(state) + + def _eletronic_document_send(self): + super(CTe, self)._eletronic_document_send() + for record in self.filtered(filter_processador_edoc_cte): + if record.xml_error_message: + return + processador = record._processador() + for edoc in record.serialize(): + processo = None + for p in processador.processar_documento(edoc): + processo = p + if processo.webservice == "cteRecepcaoLote": + record.authorization_event_id._save_event_file( + processo.envio_xml, "xml" + ) + + if processo.resposta.cStat in LOTE_PROCESSADO + ["100"]: + record.atualiza_status_cte(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, + } + ) + + else: + record._change_state(SITUACAO_EDOC_REJEITADA) + record.write( + { + "status_code": processo.resposta.cStat, + "status_name": processo.resposta.xMotivo, + } + ) + + def _document_cancel(self, justificative): + result = super(CTe, self)._document_cancel(justificative) + online_event = self.filtered(filter_processador_edoc_cte) + if online_event: + online_event._cte_cancel() + return result + + def _cte_cancel(self): + self.ensure_one() + processador = self._processador() + + if not self.authorization_protocol: + raise UserError(_("Authorization Protocol Not Found!")) + + evento = processador.cancela_documento( + chave=self.document_key, + protocolo_autorizacao=self.authorization_protocol, + justificativa=self.cancel_reason.replace("\n", "\\n"), + ) + processo = processador.enviar_lote_evento(lista_eventos=[evento]) + + self.cancel_event_id = self.event_ids.create_event_save_xml( + company_id=self.company_id, + environment=( + EVENT_ENV_PROD if self.cte_environment == "1" else EVENT_ENV_HML + ), + event_type="2", + xml_file=processo.envio_xml.decode("utf-8"), + document_id=self, + ) + + for retevento in processo.resposta.retEvento: + if not retevento.infEvento.chCte == self.document_key: + continue + + if retevento.infEvento.cStat not in CANCELADO: + mensagem = "Erro no cancelamento" + mensagem += "\nCódigo: " + retevento.infEvento.cStat + mensagem += "\nMotivo: " + retevento.infEvento.xMotivo + raise UserError(mensagem) + + if retevento.infEvento.cStat == CANCELADO_FORA_PRAZO: + self.state_fiscal = SITUACAO_FISCAL_CANCELADO_EXTEMPORANEO + elif retevento.infEvento.cStat == CANCELADO_DENTRO_PRAZO: + self.state_fiscal = SITUACAO_FISCAL_CANCELADO + + self.state_edoc = SITUACAO_EDOC_CANCELADA + self.cancel_event_id.set_done( + status_code=retevento.infEvento.cStat, + response=retevento.infEvento.xMotivo, + protocol_date=fields.Datetime.to_string( + datetime.fromisoformat(retevento.infEvento.dhRegEvento) + ), + protocol_number=retevento.infEvento.nProt, + file_response_xml=processo.retorno.content.decode("utf-8"), + ) diff --git a/l10n_br_cte/models/document_cargo_quantity_infos.py b/l10n_br_cte/models/document_cargo_quantity_infos.py new file mode 100644 index 000000000000..44ea62fba70b --- /dev/null +++ b/l10n_br_cte/models/document_cargo_quantity_infos.py @@ -0,0 +1,27 @@ +# Copyright 2023 KMEE +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields + +from odoo.addons.spec_driven_model.models import spec_models + + +class CTeCargoQuantityInfos(spec_models.SpecModel): + _name = "l10n_br_cte.cargo.quantity.infos" + _inherit = "cte.40.tcte_infq" + _binding_module = "nfelib.cte.bindings.v4_0.cte_tipos_basico_v4_00" + _description = "Informações de quantidades da Carga do CT-e" + + document_id = fields.Many2one(comodel_name="l10n_br_fiscal.document") + + cte40_cUnid = fields.Selection( + required=True, + ) + + cte40_tpMed = fields.Char( + required=True, + ) + + cte40_qCarga = fields.Float( + required=True, + ) diff --git a/l10n_br_cte/models/document_line.py b/l10n_br_cte/models/document_line.py new file mode 100644 index 000000000000..204dd3bb9491 --- /dev/null +++ b/l10n_br_cte/models/document_line.py @@ -0,0 +1,216 @@ +# Copyright 2023 KMEE +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +import sys + +from odoo import api, fields + +from odoo.addons.l10n_br_fiscal.constants.icms import ICMS_CST, ICMS_SN_CST +from odoo.addons.spec_driven_model.models import spec_models + + +class CTeLine(spec_models.StackedModel): + _name = "l10n_br_fiscal.document.line" + _inherit = ["l10n_br_fiscal.document.line", "cte.40.tcte_imp"] + _stacked = "cte.40.tcte_imp" + _field_prefix = "cte40_" + _schema_name = "cte" + _schema_version = "4.0.0" + _odoo_module = "l10n_br_cte" + _spec_module = "odoo.addons.l10n_br_cte_spec.models.v4_0.cte_tipos_basico_v4_00" + _spec_tab_name = "CTe" + _stacking_points = {} + _force_stack_paths = "tcte_imp.timp" + _binding_module = "nfelib.cte.bindings.v4_0.cte_tipos_basico_v4_00" + + ########################## + # CT-e tag: vPrest + ########################## + + cte40_vTPrest = fields.Monetary(string="vTPrest", related="amount_total") + + cte40_vRec = fields.Monetary( + related="price_gross", + string="vRec", + ) + + ################################################## + # CT-e tag: ICMS + # Grupo N01. Grupo Tributação do ICMS= 00 + # Grupo N02. Grupo Tributação do ICMS= 20 + # Grupo N03. Grupo Tributação do ICMS= 45 (40, 41 e 51) + # Grupo N04. Grupo Tributação do ICMS= 60 + # Grupo N05. Grupo Tributação do ICMS= 90 - ICMS outros + # Grupo N06. Grupo Tributação do ICMS= 90 - ICMS Outra UF + # Grupo N06. Grupo Tributação do ICMS= 01 - ISSN + ################################################# + + cte40_ICMS = fields.Many2one( + comodel_name="l10n_br_fiscal.document.line", compute="_compute_icms", store=True + ) + + def _compute_icms(self): + for doc in self: + doc.cte40_ICMS = doc + + cte40_choice_icms = fields.Selection( + selection=[ + ("cte40_ICMS00", "ICMS00"), + ("cte40_ICMS20", "ICMS20"), + ("cte40_ICMS45", "ICMS45"), + ("cte40_ICMS60", "ICMS60"), + ("cte40_ICMS90", "ICMS90"), + ("cte40_ICMSOutraUF", "ICMSOutraUF"), + ("cte40_ICMSSN", "ICMSSN"), + ], + string="Tipo de ICMS", + compute="_compute_choice_icms", + store=True, + ) + + cte40_CST = fields.Selection( + selection=[ + ("00", "00 - Tributação normal ICMS"), + ("20", "20 - Tributação com BC reduzida do ICMS"), + ("45", "45 - ICMS Isento, não Tributado ou diferido"), + ("60", "60 - ICMS cobrado por substituição tributária"), + ("90", "90 - ICMS outros"), + ("90", "90 - ICMS Outra UF"), + ("01", "01 - Simples Nacional"), + ], + string="Classificação Tributária do Serviço", + compute="_compute_choice_icms", + store=True, + ) + + cte40_vTotTrib = fields.Monetary(related="estimate_tax") + + cte40_pICMS = fields.Float(related="icms_percent", string="pICMS") + + cte40_vICMS = fields.Monetary(related="icms_value") + + # ICMS20 - ICMS90 + cte40_pRedBC = fields.Float( + related="icms_reduction", + ) + + cte40_vBC = fields.Monetary(related="icms_base") + + # ICMS60 + cte40_vBCSTRet = fields.Monetary(related="icmsst_wh_base") + + cte40_vICMSSTRet = fields.Monetary(related="icmsst_wh_value") + + # TODO cte40_pICMSTRet = fields.Monetary(related="") + + # ICMSSN + cte40_indSN = fields.Float(default=1) + + # ICMS NF + cte40_vBCST = fields.Monetary(related="icmsst_base") + + # ICMSOutraUF + # TODO + + ########################## + # CT-e tag: ICMS + # Compute Methods + ########################## + + @api.depends("icms_cst_id") + def _compute_choice_icms(self): + for record in self: + record.cte40_choice_icms = None + record.cte40_CST = None + if record.icms_cst_id.code in ICMS_CST: + if record.icms_cst_id.code in ["40", "41", "50"]: + record.cte40_choice_icms = "cte40_ICMS45" + record.cte40_CST = "45" + elif ( + record.icms_cst_id.code == "90" + and self.partner_id.state_id != self.company_id.state_id + ): + record.cte40_choice_icms = "cte40_ICMSOutraUF" + else: + record.cte40_choice_icms = "{}{}".format( + "cte40_ICMS", record.icms_cst_id.code + ) + record.cte40_CST = record.icms_cst_id.code + elif record.icms_cst_id.code in ICMS_SN_CST: + record.cte40_choice_icms = "cte40_ICMSSN" + record.cte40_CST = "90" + + def _export_fields_icms(self): + icms = { + "CST": self.cte40_CST, + "vBC": str("%.02f" % self.icms_base), + "pRedBC": str("%.04f" % self.icms_reduction), + "pICMS": str("%.02f" % self.icms_percent), + "vICMS": str("%.02f" % self.icms_value), + "vICMSSubstituto": str("%.02f" % self.icms_substitute), + "indSN": int(self.cte40_indSN), + "vBCSTRet": str("%.02f" % self.icmsst_wh_base), + "vICMSSTRet": str("%.02f" % self.icmsst_wh_value), + "pICMSSTRet": str("%.02f" % self.icmsst_wh_percent), + } + return icms + + def _export_fields_cte_40_timp(self, xsd_fields, class_obj, export_dict): + # TODO Not Implemented + if "cte40_ICMSOutraUF" in xsd_fields: + xsd_fields.remove("cte40_ICMSOutraUF") + + xsd_fields = [self.cte40_choice_icms] + icms_tag = ( + self.cte40_choice_icms.replace("cte40_", "") + .replace("ICMSSN", "Icmssn") + .replace("ICMS", "Icms") + ) + binding_module = sys.modules[self._binding_module] + icms = binding_module.Timp + icms_binding = getattr(icms, icms_tag) + icms_dict = self._export_fields_icms() + sliced_icms_dict = { + key: icms_dict.get(key) + for key in icms_binding.__dataclass_fields__.keys() + if icms_dict.get(key) + } + export_dict[icms_tag.upper()] = icms_binding(**sliced_icms_dict) + + ########################## + # CT-e tag: ICMSUFFim + ########################## + + cte40_vBCUFFim = fields.Monetary(related="icms_destination_base") + cte40_pFCPUFFim = fields.Monetary(compute="_compute_cte40_ICMSUFFim", store=True) + cte40_pICMSUFFim = fields.Monetary(compute="_compute_cte40_ICMSUFFim", store=True) + # TODO + # cte40_pICMSInter = fields.Selection( + # selection=[("0", "Teste")], + # compute="_compute_cte40_ICMSUFFim") + + def _compute_cte40_ICMSUFFim(self): + for record in self: + # if record.icms_origin_percent: + # record.cte40_pICMSInter = str("%.02f" % record.icms_origin_percent) + # else: + # record.cte40_pICMSInter = False + + record.cte40_pFCPUFFim = record.icmsfcp_percent + record.cte40_pICMSUFFim = record.icms_destination_percent + + cte40_vFCPUFfim = fields.Monetary(related="icmsfcp_value") + cte40_vICMSUFFim = fields.Monetary(related="icms_destination_value") + cte40_vICMSUFIni = fields.Monetary(related="icms_origin_value") + + ########################## + # CT-e tag: natCarga + ########################## + + cte40_xDime = fields.Char(compute="_compute_dime", store=True) + + def _compute_dime(self): + for record in self: + for package in record.product_id.packaging_ids: + record.cte40_xDime = ( + package.width + "X" + package.packaging_length + "X" + package.width + ) diff --git a/l10n_br_cte/models/document_related.py b/l10n_br_cte/models/document_related.py new file mode 100644 index 000000000000..f41e364f7686 --- /dev/null +++ b/l10n_br_cte/models/document_related.py @@ -0,0 +1,128 @@ +# Copyright 2023 KMEE INFORMATICA LTDA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, fields + +from odoo.addons.spec_driven_model.models import spec_models + + +class CTeRelated(spec_models.StackedModel): + + _name = "l10n_br_fiscal.document.related" + _inherit = [ + "l10n_br_fiscal.document.related", + "cte.40.tcte_infnfe", + "cte.40.tcte_infnf", + "cte.40.tcte_infq", + ] + _stacked = "cte.40.tcte_infnfe" + _field_prefix = "cte40_" + _schema_name = "cte" + _schema_version = "4.0.0" + _odoo_module = "l10n_br_cte" + _spec_module = "odoo.addons.l10n_br_cte_spec.models.v4_0.cte_tipos_basico_v4_00" + _spec_tab_name = "CTe" + _binding_module = "nfelib.cte.bindings.v4_0.cte_tipos_basico_v4_00" + + # infQ TODO computes/relateds + + cte40_tpMed = fields.Char() + + cte40_qCarga = fields.Float() + + cte40_cUnid = fields.Selection( + selection=[ + ("00", "M3"), + ("01", "KG"), + ("02", "TON"), + ("03", "UNIDADE"), + ("04", "LITROS"), + ("05", "MMBTU"), + ], + ) + + # infCarga + cte40_prodPred = fields.Char(string="prodPred") + + cte40_vCarga = fields.Monetary( + currency_field="currency_id", compute="_compute_vCarga", store=True + ) + + currency_id = fields.Many2one( + comodel_name="res.currency", related="company_id.currency_id", readonly=True + ) + + company_id = fields.Many2one( + comodel_name="res.company", + default=lambda self: self.env.company, + ) + + # InfNFe + cte40_chave = fields.Char( + compute="_compute_cte_data", + inverse="_inverse_cte40_chave", + ) + + cte40_tpDoc = fields.Char( + compute="_compute_cte_data", + inverse="_inverse_cte40_tpDoc", + ) + + cte40_infDoc = fields.Selection(related="cte40_choice_infNF_infNFE_infOutros") + + # infCteNorm + cte40_chCTe = fields.Char(compute="_compute_chCte", string="chCte") + + ########################## + # CT-e tag: infCTeComp + # Compute Methods + ########################## + + def _compute_chCTe(self): + records = "" + for rec in self: + if rec.cte40_Id: + records += rec.document_key + self.cte40_chCTe = records + + cte40_choice_infNF_infNFE_infOutros = fields.Selection( + selection=[ + ("cte40_infNF", "infNF"), # TODO + ("cte40_infNFe", "infNFe"), + ("cte40_infOutros", "Outros"), + ], + compute="_compute_cte_data", + inverse="_inverse_cte40_choice_infNF_infNFE_infOutros", + ) + + def _compute_vCarga(self): + for rec in self: + if rec.document_related_id: + rec.cte40_vCarga += rec.document_related_id.amount_price_gross + + @api.depends("document_type_id") + def _compute_cte_data(self): + """Set schema data which are not just related fields""" + for rec in self: + if rec.document_type_id: + if rec.document_type_id.code in ("55",): + rec.cte40_choice_infNF_infNFE_infOutros = "cte40_infNFe" + rec.cte40_chave = rec.document_key + elif rec.document_type_id.code in ("00", "10", "59", "65", "99"): + rec.cte40_choice_infNF_infNFE_infOutros = "cte40_infOutros" + rec.cte40_tpDoc = rec.document_type_id.code + + def _inverse_cte40_chave(self): + for rec in self: + if rec.cte40_chave: + rec.document_key = rec.cte40_chave + + def _inverse_cte40_tpDoc(self): + for rec in self: + if rec.cte40_tpDoc: + rec.document_type_id = rec.cte40_tpDoc + + def _inverse_cte40_choice_infNF_infNFE_infOutros(self): + for rec in self: + if rec.cte40_choice_infNF_infNFE_infOutros == "cte40_infNFe": + rec.document_type_id = self.env.ref("l10n_br_fiscal.document_55") diff --git a/l10n_br_cte/models/document_supplement.py b/l10n_br_cte/models/document_supplement.py new file mode 100644 index 000000000000..d4202faf2e78 --- /dev/null +++ b/l10n_br_cte/models/document_supplement.py @@ -0,0 +1,22 @@ +# Copyright 2023 KMEE (Luiz Felipe do Divino ) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import fields + +from odoo.addons.spec_driven_model.models import spec_models + + +class CTeSupplement(spec_models.StackedModel): + _name = "l10n_br_fiscal.document.supplement" + _inherit = ["l10n_br_fiscal.document.supplement", "cte.40.tcte_infctesupl"] + _stacked = "cte.40.tcte_infctesupl" + _schema_name = "cte" + _schema_version = "4.0.0" + _odoo_module = "l10n_br_cte" + _spec_module = "odoo.addons.l10n_br_cte_spec.models.v4_0.cte_tipos_basico_v4_00" + _binding_module = "nfelib.cte.bindings.v4_0.cte_tipos_basico_v4_00" + _field_prefix = "cte40_" + _spec_tab_name = "CTe" + _description = "Informações Complementares do Documento Fiscal" + + cte40_qrCodCTe = fields.Char(related="qrcode") diff --git a/l10n_br_cte/models/document_transported_vehicles.py b/l10n_br_cte/models/document_transported_vehicles.py new file mode 100644 index 000000000000..bbb6af6bfd07 --- /dev/null +++ b/l10n_br_cte/models/document_transported_vehicles.py @@ -0,0 +1,43 @@ +# Copyright 2023 KMEE +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields + +from odoo.addons.spec_driven_model.models import spec_models + + +class CTeCargoQuantityInfos(spec_models.SpecModel): + _name = "l10n_br_cte.transported.vehicles" + _inherit = "cte.40.veicnovos" + _binding_module = "nfelib.cte.bindings.v4_0.cte_tipos_basico_v4_00" + _description = "Informações dos veículos transportados" + + document_id = fields.Many2one(comodel_name="l10n_br_fiscal.document") + + currency_id = fields.Many2one( + comodel_name="res.currency", + related="document_id.company_id.currency_id", + ) + + cte40_chassi = fields.Char(string="Chassi do veículo", required=True, size=17) + + cte40_cCor = fields.Char(string="Cor do veículo", required=True, size=4) + + cte40_xCor = fields.Char(string="Descrição da cor", required=True) + + cte40_cMod = fields.Char( + string="Código Marca Modelo", + required=True, + ) + + cte40_vUnit = fields.Monetary( + string="Valor Unitário do Veículo", + required=True, + currency_field="currency_id", + ) + + cte40_vFrete = fields.Monetary( + string="Frete Unitário", + required=True, + currency_field="currency_id", + ) diff --git a/l10n_br_cte/models/dutoviario.py b/l10n_br_cte/models/dutoviario.py new file mode 100644 index 000000000000..8d980571c62c --- /dev/null +++ b/l10n_br_cte/models/dutoviario.py @@ -0,0 +1,25 @@ +# Copyright 2023 KMEE INFORMATICA LTDA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields + +from odoo.addons.spec_driven_model.models import spec_models + + +class Duto(spec_models.SpecModel): + _name = "l10n_br_cte.modal.duto" + _inherit = "cte.40.duto" + _binding_module = "nfelib.cte.bindings.v4_0.cte_modal_dutoviario_v4_00" + _description = "Modal Dutoviario CTe" + + document_id = fields.Many2one(comodel_name="l10n_br_fiscal.document") + + cte40_dIni = fields.Date(related="document_id.cte40_dIni") + + cte40_dFim = fields.Date(related="document_id.cte40_dFim") + + cte40_vTar = fields.Float(related="document_id.cte40_vTar") + + def _prepare_dacte_values(self): + if not self: + return {} diff --git a/l10n_br_cte/models/ferroviario.py b/l10n_br_cte/models/ferroviario.py new file mode 100644 index 000000000000..d18932706e1f --- /dev/null +++ b/l10n_br_cte/models/ferroviario.py @@ -0,0 +1,55 @@ +# Copyright 2023 KMEE INFORMATICA LTDA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + + +from odoo import api, fields + +from odoo.addons.spec_driven_model.models import spec_models + + +class Ferrov(spec_models.StackedModel): + _name = "l10n_br_cte.modal.ferrov" + _inherit = "cte.40.ferrov" + _stacked = "cte.40.ferrov" + _binding_module = "nfelib.cte.bindings.v4_0.cte_modal_ferroviario_v4_00" + _field_prefix = "cte40_" + _schema_name = "cte" + _schema_version = "4.0.0" + _odoo_module = "l10n_br_cte" + _spec_module = ( + "odoo.addons.l10n_br_cte_spec.models.v4_0.cte_modal_ferroviario_v4_00" + ) + _spec_tab_name = "CTe" + _description = "Modal Ferroviario CTe" + + document_id = fields.Many2one(comodel_name="l10n_br_fiscal.document") + + cte40_tpTraf = fields.Selection(related="document_id.cte40_tpTraf") + + cte40_fluxo = fields.Char(related="document_id.cte40_fluxo") + + cte40_vFrete = fields.Monetary( + related="document_id.cte40_vFrete", currency_field="currency_id" + ) + + currency_id = fields.Many2one( + comodel_name="res.currency", + default=lambda self: self.env.company.currency_id, + ) + + cte40_chCTeFerroOrigem = fields.Char(related="document_id.cte40_chCTeFerroOrigem") + + cte40_respFat = fields.Selection(related="document_id.cte40_respFat") + + cte40_ferrEmi = fields.Selection(related="document_id.cte40_ferrEmi") + + cte40_ferroEnv = fields.One2many(compute="_compute_railroad") + + @api.depends("document_id.cte40_ferroEnv") + def _compute_railroad(self): + for record in self: + record.cte40_ferroEnv = [(6, 0, record.document_id.cte40_ferroEnv.ids)] + + def _prepare_dacte_values(self): + if not self: + return {} diff --git a/l10n_br_cte/models/normal_cte_infos.py b/l10n_br_cte/models/normal_cte_infos.py new file mode 100644 index 000000000000..59c470e866f8 --- /dev/null +++ b/l10n_br_cte/models/normal_cte_infos.py @@ -0,0 +1,153 @@ +# Copyright 2023 KMEE +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields + +from odoo.addons.spec_driven_model.models import spec_models + + +class CTeNormalInfos(spec_models.StackedModel): + _name = "l10n_br_cte.normal.infos" + _inherit = ["cte.40.tcte_infctenorm"] + _stacked = "cte.40.tcte_infctenorm" + _field_prefix = "cte40_" + _schema_name = "cte" + _schema_version = "4.0.0" + _odoo_module = "l10n_br_cte" + _spec_module = "odoo.addons.l10n_br_cte_spec.models.v4_0.cte_tipos_basico_v4_00" + _binding_module = "nfelib.cte.bindings.v4_0.cte_tipos_basico_v4_00" + _spec_tab_name = "CTe" + _description = "Grupo de informações do CTe Normal e Substituto" + _force_stack_paths = "infctenorm.infdoc" + + document_id = fields.Many2one(comodel_name="l10n_br_fiscal.document") + + currency_id = fields.Many2one( + comodel_name="res.currency", + related="document_id.company_id.currency_id", + ) + + cte40_vCarga = fields.Monetary( + related="document_id.cte40_vCarga", + currency_field="currency_id", + ) + + cte40_proPred = fields.Char( + related="document_id.cte40_proPred", + ) + + cte40_xOutCat = fields.Char( + related="document_id.cte40_xOutCat", + ) + + cte40_infQ = fields.One2many( + comodel_name="l10n_br_cte.cargo.quantity.infos", + related="document_id.cte40_infQ", + ) + + cte40_vCargaAverb = fields.Monetary( + related="document_id.cte40_vCargaAverb", + ) + + cte40_veicNovos = fields.One2many( + comodel_name="l10n_br_cte.transported.vehicles", + related="document_id.cte40_veicNovos", + ) + + cte40_infNFe = fields.One2many( + comodel_name="l10n_br_fiscal.document.related", + related="document_id.document_related_ids", + ) + + cte40_versaoModal = fields.Char(related="document_id.cte40_versaoModal") + + # Campos do Modal Aereo + modal_aereo_id = fields.Many2one( + comodel_name="l10n_br_cte.modal.aereo", related="document_id.modal_aereo_id" + ) + + cte40_nMinu = fields.Char(related="document_id.cte40_nMinu") + + cte40_nOCA = fields.Char(related="document_id.cte40_nOCA") + + cte40_dPrevAereo = fields.Date(related="document_id.cte40_dPrevAereo") + + cte40_xDime = fields.Char(related="document_id.cte40_xDime") + + cte40_CL = fields.Char(related="document_id.cte40_CL") + + cte40_cTar = fields.Char(related="document_id.cte40_cTar") + + # Existem dois vTar no spec, um float e um monetary, por isso a mudança de nome + cte40_aereo_vTar = fields.Monetary(related="document_id.cte40_aereo_vTar") + + cte40_peri = fields.One2many( + comodel_name="l10n_br_cte.modal.aereo.peri", related="document_id.cte40_peri" + ) + + # Campos do Modal Aquaviario + modal_aquaviario_id = fields.Many2one( + comodel_name="l10n_br_cte.modal.aquav", + related="document_id.modal_aquaviario_id", + ) + + cte40_vPrest = fields.Monetary(related="document_id.cte40_vPrest") + + cte40_vAFRMM = fields.Monetary(related="document_id.cte40_vAFRMM") + + cte40_xNavio = fields.Char(related="document_id.cte40_xNavio") + + cte40_nViag = fields.Char(related="document_id.cte40_nViag") + + cte40_direc = fields.Selection(related="document_id.cte40_direc") + + cte40_irin = fields.Char(related="document_id.cte40_irin") + + cte40_tpNav = fields.Selection(related="document_id.cte40_tpNav") + + cte40_balsa = fields.One2many( + comodel_name="l10n_br_cte.modal.aquav.balsa", + related="document_id.cte40_balsa", + ) + + # Campos do Modal Dutoviario + modal_dutoviario_id = fields.Many2one( + comodel_name="l10n_br_cte.modal.duto", + related="document_id.modal_dutoviario_id", + ) + + cte40_dIni = fields.Date(related="document_id.cte40_dIni") + + cte40_dFim = fields.Date(related="document_id.cte40_dFim") + + cte40_vTar = fields.Float(related="document_id.cte40_vTar") + + # Campos do Modal Ferroviario + modal_ferroviario_id = fields.Many2one( + comodel_name="l10n_br_cte.modal.ferrov", + related="document_id.modal_ferroviario_id", + ) + + cte40_tpTraf = fields.Selection(related="document_id.cte40_tpTraf") + + cte40_fluxo = fields.Char(related="document_id.cte40_fluxo") + + cte40_vFrete = fields.Monetary(related="document_id.cte40_vFrete") + + cte40_respFat = fields.Selection(related="document_id.cte40_respFat") + + cte40_ferrEmi = fields.Selection(related="document_id.cte40_ferrEmi") + + cte40_ferroEnv = fields.Many2many(related="document_id.cte40_ferroEnv") + + # Campos do Modal rodoviario + modal_rodoviario_id = fields.Many2one( + comodel_name="l10n_br_cte.modal.rodo", + related="document_id.modal_rodoviario_id", + ) + + cte40_RNTRC = fields.Char(related="document_id.cte40_RNTRC") + + cte40_occ = fields.One2many( + comodel_name="l10n_br_cte.modal.rodo.occ", related="document_id.cte40_occ" + ) diff --git a/l10n_br_cte/models/res_company.py b/l10n_br_cte/models/res_company.py new file mode 100644 index 000000000000..cc465aa6f043 --- /dev/null +++ b/l10n_br_cte/models/res_company.py @@ -0,0 +1,105 @@ +# Copyright 2023 KMEE INFORMATICA LTDA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields + +from odoo.addons.spec_driven_model.models import spec_models + + +class ResCompany(spec_models.SpecModel): + + _name = "res.company" + _inherit = ["res.company", "cte.40.tcte_emit", "cte.40.tendeemi", "cte.40.ferroenv"] + _cte_search_keys = ["cte40_CNPJ", "cte40_xNome", "cte40_xFant"] + _binding_module = "nfelib.cte.bindings.v4_0.cte_tipos_basico_v4_00" + _field_prefix = "cte40_" + + ########################## + # CT-e spec fields + ########################## + + cte40_CNPJ = fields.Char( + related="partner_id.cte40_CNPJ", + ) + cte40_CPF = fields.Char( + related="partner_id.cte40_CPF", + ) + cte40_IE = fields.Char( + related="partner_id.cte40_IE", + ) + cte40_xNome = fields.Char( + related="partner_id.legal_name", + ) + cte40_xFant = fields.Char( + related="partner_id.name", + ) + cte40_CRT = fields.Selection( + related="tax_framework", + ) + + cte40_enderEmit = fields.Many2one( + comodel_name="res.partner", + related="partner_id", + ) + + cte40_enderToma = fields.Many2one(comodel_name="res.partner", related="partner_id") + + ########################## + # CT-e models fields + ########################## + + cte_default_serie_id = fields.Many2one( + comodel_name="l10n_br_fiscal.document.serie", + string="CT-e Default Serie", + ) + + cte_dacte_layout = fields.Selection( + selection=[("1", "Paisagem"), ("2", "Retrato")], + string="CT-e DACTE Layout", + default="1", + ) + + cte_transmission = fields.Selection( + selection=[ + ("1", "Normal"), + ("2", "Regime Especial NFF"), + ("4", "EPEC pela SVC"), + ("5", "Contingência FSDA"), + ("7", "Contingência SVC-RS"), + ("8", "Contingência SVC-SP"), + ], + string="CT-e Transmission Type", + default="1", + ) + + cte_type = fields.Selection( + selection=[ + ("0", "CT-e Normal"), + ("1", "CT-e de Complemento de Valores"), + ("3", "CT-e de Substituição"), + ], + string="CT-e Type", + default="0", + ) + + cte_environment = fields.Selection( + selection=[("1", "Produção"), ("2", "Homologação")], + string="CT-e Environment", + default="2", + ) + + cte_version = fields.Selection( + selection=[("3.00", "3.00"), ("4.00", "4.00")], + string="CT-e Version", + default="4.00", + ) + + processador_edoc = fields.Selection( + selection_add=[("erpbrasil.edoc", "erpbrasil.edoc")], + ) + + cte_authorize_accountant_download_xml = fields.Boolean( + string="Include Accountant Partner data in persons authorized to " + "download CTe XML", + default=False, + ) diff --git a/l10n_br_cte/models/res_config_settings.py b/l10n_br_cte/models/res_config_settings.py new file mode 100644 index 000000000000..278b43cb80ac --- /dev/null +++ b/l10n_br_cte/models/res_config_settings.py @@ -0,0 +1,12 @@ +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + cte_authorize_accountant_download_xml = fields.Boolean( + string="Include Accountant Partner data in persons authorized to " + "download CTe XML", + related="company_id.cte_authorize_accountant_download_xml", + readonly=False, + ) diff --git a/l10n_br_cte/models/res_partner.py b/l10n_br_cte/models/res_partner.py new file mode 100644 index 000000000000..2d636ae6bf29 --- /dev/null +++ b/l10n_br_cte/models/res_partner.py @@ -0,0 +1,156 @@ +# Copyright 2023 KMEE INFORMATICA LTDA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import logging + +from odoo import api, fields + +from odoo.addons.spec_driven_model.models import spec_models + +_logger = logging.getLogger(__name__) + +try: + from erpbrasil.base.misc import format_zipcode, punctuation_rm +except ImportError: + _logger.error("Biblioteca erpbrasil.base não instalada") + + +class ResPartner(spec_models.SpecModel): + + _name = "res.partner" + _inherit = [ + "res.partner", + "cte.40.tendereco", + "cte.40.tlocal", + "cte.40.tendeemi", + "cte.40.tcte_dest", + "cte.40.tresptec", + "cte.40.tcte_autxml", + "cte.40.tenderfer", + ] + _cte_search_keys = ["cte40_CNPJ", "cte40_CPF", "cte40_xNome"] + _binding_module = "nfelib.cte.bindings.v4_0.cte_tipos_basico_v4_00" + _field_prefix = "cte40_" + + cte40_choice_cnpj_cpf = fields.Selection( + selection=[("cte40_CNPJ", "CNPJ"), ("cte40_CPF", "CPF")], + string="CNPJ/CPF do Parceiro", + ) + + cte40_CNPJ = fields.Char( + compute="_compute_cte_data", + store=True, + ) + + cte40_cInt = fields.Char( + string="Código interno da Ferrovia envolvida", + help="Código interno da Ferrovia envolvida\nUso da transportadora", + ) + + cte40_CPF = fields.Char( + compute="_compute_cte_data", + store=True, + ) + + # Same problem with Tendereco that NFE has, it has to use m2o fields + cte40_enderToma = fields.Many2one( + comodel_name="res.partner", compute="_compute_cte40_ender" + ) + + cte40_enderReme = fields.Many2one( + comodel_name="res.partner", compute="_compute_cte40_ender" + ) + + cte40_enderDest = fields.Many2one( + comodel_name="res.partner", compute="_compute_cte40_ender" + ) + + cte40_enderExped = fields.Many2one( + comodel_name="res.partner", compute="_compute_cte40_ender" + ) + + cte40_enderFerro = fields.Many2one( + comodel_name="res.partner", compute="_compute_cte40_ender" + ) + + # enderToma/enderEmit/enderReme + cte40_xLgr = fields.Char(related="street_name", readonly=True) + cte40_nro = fields.Char(related="street_number", readonly=True) + cte40_xCpl = fields.Char(related="street2", readonly=True) + cte40_xBairro = fields.Char(related="district", readonly=True) + cte40_cMun = fields.Char(related="city_id.ibge_code", readonly=True) + cte40_xMun = fields.Char(related="city_id.name", readonly=True) + cte40_UF = fields.Char(related="state_id.code") + cte40_CEP = fields.Char( + compute="_compute_cep", + inverse="_inverse_cte40_CEP", + compute_sudo=True, + store=True, + ) + cte40_cPais = fields.Char( + related="country_id.bc_code", + ) + cte40_xPais = fields.Char( + related="country_id.name", + ) + + cte40_IE = fields.Char(compute="_compute_cte40_IE") + + cte40_xNome = fields.Char(related="legal_name") + + def _compute_cte40_IE(self): + for rec in self: + rec.cte40_IE = str(rec.inscr_est).replace(".", "") + + def _compute_cte40_ender(self): + for rec in self: + rec.cte40_enderToma = rec.id + rec.cte40_enderReme = rec.id + rec.cte40_enderDest = rec.id + rec.cte40_enderExped = rec.id + rec.cte40_enderFerro = rec.id + + @api.depends("company_type", "inscr_est", "cnpj_cpf", "country_id") + def _compute_cte_data(self): + for rec in self: + cnpj_cpf = punctuation_rm(rec.cnpj_cpf) + if cnpj_cpf: + if rec.is_company: + rec.cte40_CNPJ = cnpj_cpf + rec.cte40_CPF = None + else: + rec.cte40_CNPJ = None + rec.cte40_CPF = cnpj_cpf + + def _inverse_cte40_CEP(self): + for rec in self: + if rec.cte40_CEP: + country_code = rec.country_id.code if rec.country_id else "BR" + rec.zip = format_zipcode(rec.cte40_CEP, country_code) + + def _compute_cep(self): + for rec in self: + rec.cte40_CEP = punctuation_rm(rec.zip) + + def _export_field(self, xsd_field, class_obj, member_spec, export_value=None): + if not self.cnpj_cpf and self.parent_id: + cnpj_cpf = punctuation_rm(self.parent_id.cnpj_cpf) + else: + cnpj_cpf = punctuation_rm(self.cnpj_cpf) + + if xsd_field == self.cte40_choice_cnpj_cpf: + return cnpj_cpf + + if self.country_id.code != "BR": + if xsd_field == "cte40_xBairro": + return "EX" + + if xsd_field == "cte40_xMun": + return "EXTERIOR" + + if xsd_field == "cte40_cMun": + return "9999999" + + if xsd_field == "cte40_UF": + return "EX" + return super()._export_field(xsd_field, class_obj, member_spec, export_value) diff --git a/l10n_br_cte/models/rodoviario.py b/l10n_br_cte/models/rodoviario.py new file mode 100644 index 000000000000..7a52647dc089 --- /dev/null +++ b/l10n_br_cte/models/rodoviario.py @@ -0,0 +1,76 @@ +# Copyright 2023 KMEE INFORMATICA LTDA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields + +from odoo.addons.spec_driven_model.models import spec_models + +from ..constants.modal import TUF + + +class Rodo(spec_models.StackedModel): + _name = "l10n_br_cte.modal.rodo" + _inherit = "cte.40.rodo" + _stacked = "cte.40.rodo" + _binding_module = "nfelib.cte.bindings.v4_0.cte_modal_rodoviario_v4_00" + _field_prefix = "cte40_" + _schema_name = "cte" + _schema_version = "4.0.0" + _odoo_module = "l10n_br_cte" + _spec_module = "odoo.addons.l10n_br_cte_spec.models.v4_0.cte_modal_rodoviario_v4_00" + _spec_tab_name = "CTe" + _description = "Modal Rodoviario CTe" + + document_id = fields.Many2one(comodel_name="l10n_br_fiscal.document") + + cte40_RNTRC = fields.Char(related="document_id.cte40_RNTRC") + + cte40_occ = fields.One2many(related="document_id.cte40_occ") + + +class Occ(spec_models.StackedModel): + _name = "l10n_br_cte.modal.rodo.occ" + _inherit = "cte.40.occ" + _stacked = "cte.40.occ" + _binding_module = "nfelib.cte.bindings.v4_0.cte_modal_rodoviario_v4_00" + _field_prefix = "cte40_" + _schema_name = "cte" + _schema_version = "4.0.0" + _odoo_module = "l10n_br_cte" + _spec_module = "odoo.addons.l10n_br_cte_spec.models.v4_0.cte_modal_rodoviario_v4_00" + _spec_tab_name = "CTe" + _description = "Ordens de Coleta associados" + + document_id = fields.Many2one(comodel_name="l10n_br_fiscal.document") + + cte40_serie = fields.Char(string="Série da OCC") + + cte40_nOcc = fields.Char(string="Número da Ordem de coleta") + + cte40_dEmi = fields.Date( + string="Data de emissão da ordem de coleta", + help="Data de emissão da ordem de coleta\nFormato AAAA-MM-DD", + ) + + cte40_CNPJ = fields.Char( + string="Número do CNPJ", + help="Número do CNPJ\nInformar os zeros não significativos.", + ) + + cte40_cInt = fields.Char( + string="Código interno de uso da transportadora", + help=( + "Código interno de uso da transportadora\nUso intermo das " + "transportadoras." + ), + ) + + cte40_IE = fields.Char(string="Inscrição Estadual") + + cte40_UF = fields.Selection( + TUF, + string="Sigla da UF", + help="Sigla da UF\nInformar EX para operações com o exterior.", + ) + + cte40_fone = fields.Char(string="Telefone") diff --git a/l10n_br_cte/readme/CONFIGURE.rst b/l10n_br_cte/readme/CONFIGURE.rst new file mode 100644 index 000000000000..754e51aeff53 --- /dev/null +++ b/l10n_br_cte/readme/CONFIGURE.rst @@ -0,0 +1,10 @@ +[ This file is optional, it should explain how to configure + the module before using it; it is aimed at advanced users. ] + +To configure this module, you need to: + +#. Go to ... + +.. figure:: ../static/description/image.png + :alt: alternative description + :width: 600 px diff --git a/l10n_br_cte/readme/CONTRIBUTORS.rst b/l10n_br_cte/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000000..957041454b69 --- /dev/null +++ b/l10n_br_cte/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Ygor Carvalho diff --git a/l10n_br_cte/readme/DESCRIPTION.rst b/l10n_br_cte/readme/DESCRIPTION.rst new file mode 100644 index 000000000000..43cf54b776e0 --- /dev/null +++ b/l10n_br_cte/readme/DESCRIPTION.rst @@ -0,0 +1,4 @@ +[ This file must be max 2-3 paragraphs, and is required. ] + +This module extends the functionality of ... to support ... +and to allow you to ... diff --git a/l10n_br_cte/readme/USAGE.rst b/l10n_br_cte/readme/USAGE.rst new file mode 100644 index 000000000000..f4629c3d548a --- /dev/null +++ b/l10n_br_cte/readme/USAGE.rst @@ -0,0 +1,11 @@ +[ This file must be present and contains the usage instructions + for end-users. As all other rst files included in the README, + it MUST NOT contain reStructuredText sections + only body text (paragraphs, lists, tables, etc). Should you need + a more elaborate structure to explain the addon, please create a + Sphinx documentation (which may include this file as a "quick start" + section). ] + +To use this module, you need to: + +#. Go to ... diff --git a/l10n_br_cte/security/ir.model.access.csv b/l10n_br_cte/security/ir.model.access.csv new file mode 100644 index 000000000000..a7ed316bbe7a --- /dev/null +++ b/l10n_br_cte/security/ir.model.access.csv @@ -0,0 +1,17 @@ +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" +l10n_br_cte_modal_aereo_user,l10n_br_cte_modal_aereo_user,model_l10n_br_cte_modal_aereo,base.group_user,1,1,1,1 +l10n_br_cte_modal_aereo_peri_user,l10n_br_cte_modal_aereo_peri_user,model_l10n_br_cte_modal_aereo_peri,base.group_user,1,1,1,1 + +l10n_br_cte_modal_rodoviario_user,l10n_br_cte_modal_rodoviario_user,model_l10n_br_cte_modal_rodo,base.group_user,1,1,1,1 +l10n_br_cte_modal_rodoviario_occ_user,l10n_br_cte_modal_rodoviario_occ_user,model_l10n_br_cte_modal_rodo_occ,base.group_user,1,1,1,1 + +l10n_br_cte_modal_ferrov_user,l10n_br_cte_modal_ferrov_user,model_l10n_br_cte_modal_ferrov,base.group_user,1,1,1,1 + +l10n_br_cte_modal_aquaviario_user,l10n_br_cte_modal_aquav_user,model_l10n_br_cte_modal_aquav,base.group_user,1,1,1,1 +l10n_br_cte_modal_aquaviario_balsa_user,l10n_br_cte_modal_aquav_balsa_user,model_l10n_br_cte_modal_aquav_balsa,base.group_user,1,1,1,1 + +l10n_br_cte_modal_duto_user,l10n_br_cte_modal_duto_user,model_l10n_br_cte_modal_duto,base.group_user,1,1,1,1 + +l10n_br_cte_normal_infos_user,l10n_br_cte_normal_infos_user,model_l10n_br_cte_normal_infos,base.group_user,1,1,1,1 +l10n_br_cte_cargo_quantity_infos_user,l10n_br_cte_cargo_quantity_infos_user,model_l10n_br_cte_cargo_quantity_infos,base.group_user,1,1,1,1 +l10n_br_cte_transported_vehicles_user,l10n_br_cte_transported_vehicles_user,model_l10n_br_cte_transported_vehicles,base.group_user,1,1,1,1 diff --git a/l10n_br_cte/static/description/icon.png b/l10n_br_cte/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..4515bffd318de5f2fc48e095121da295ddffce72 GIT binary patch literal 30809 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 literal 0 HcmV?d00001 diff --git a/l10n_br_cte/static/description/index.html b/l10n_br_cte/static/description/index.html new file mode 100644 index 000000000000..4f44cdb9306d --- /dev/null +++ b/l10n_br_cte/static/description/index.html @@ -0,0 +1,460 @@ + + + + + +CT-e + + + +
+

CT-e

+ + +

Alpha License: AGPL-3 OCA/l10n-brazil Translate me on Weblate Try me on Runboat

+

[ This file must be max 2-3 paragraphs, and is required. ]

+

This module extends the functionality of … to support … +and to allow you to …

+
+

Important

+

This is an alpha version, the data model and design can change at any time without warning. +Only for development or testing purpose, do not use in production. +More details on development status

+
+

Table of contents

+ +
+

Configuration

+
+
[ This file is optional, it should explain how to configure
+
the module before using it; it is aimed at advanced users. ]
+
+

To configure this module, you need to:

+
    +
  1. Go to …
  2. +
+
+alternative description +
+
+
+

Usage

+
+
[ This file must be present and contains the usage instructions
+
for end-users. As all other rst files included in the README, +it MUST NOT contain reStructuredText sections +only body text (paragraphs, lists, tables, etc). Should you need +a more elaborate structure to explain the addon, please create a +Sphinx documentation (which may include this file as a “quick start” +section). ]
+
+

To use this module, you need to:

+
    +
  1. Go to …
  2. +
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • KMEE
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+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.

+

This module is part of the OCA/l10n-brazil project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/l10n_br_cte/views/cte_document.xml b/l10n_br_cte/views/cte_document.xml new file mode 100644 index 000000000000..ef90107f0474 --- /dev/null +++ b/l10n_br_cte/views/cte_document.xml @@ -0,0 +1,138 @@ + + + + + + l10n_br_cte.document.form.inherit + l10n_br_fiscal.document + 10 + + + + [('document_type_id.code', 'in', ['57', '08', '09', '10', '11', '26', '67', '8B'])] + + + [('document_type_id.code', 'in', ['57', '08', '09', '10', '11', '26', '67', '8B'])] + + + + + + + + + + + + + + + + + +
+ + + + + +
+
+
+ + + + + + + + + + + +
+ + + + + + + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
diff --git a/l10n_br_cte/views/document_line.xml b/l10n_br_cte/views/document_line.xml new file mode 100644 index 000000000000..e2da706e8b22 --- /dev/null +++ b/l10n_br_cte/views/document_line.xml @@ -0,0 +1,13 @@ + + + + + + document.line + + + + + + diff --git a/l10n_br_cte/views/document_related.xml b/l10n_br_cte/views/document_related.xml new file mode 100644 index 000000000000..1a66fbe13e15 --- /dev/null +++ b/l10n_br_cte/views/document_related.xml @@ -0,0 +1,17 @@ + + + + + + document.related.form (in l10n_br_cte) + document.related + + + + + + + + + diff --git a/l10n_br_cte/views/res_company.xml b/l10n_br_cte/views/res_company.xml new file mode 100644 index 000000000000..8907285e0e3a --- /dev/null +++ b/l10n_br_cte/views/res_company.xml @@ -0,0 +1,33 @@ + + + + + + cte.res.company.form + res.company + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/l10n_br_cte/views/res_partner.xml b/l10n_br_cte/views/res_partner.xml new file mode 100644 index 000000000000..9d738eaa174c --- /dev/null +++ b/l10n_br_cte/views/res_partner.xml @@ -0,0 +1,17 @@ + + + + + + res.partner.form (in l10n_br_cte) + res.partner + + + + + + + + + diff --git a/setup/l10n_br_cte/odoo/addons/l10n_br_cte b/setup/l10n_br_cte/odoo/addons/l10n_br_cte new file mode 120000 index 000000000000..a76696a2ca0b --- /dev/null +++ b/setup/l10n_br_cte/odoo/addons/l10n_br_cte @@ -0,0 +1 @@ +../../../../l10n_br_cte \ No newline at end of file diff --git a/setup/l10n_br_cte/setup.py b/setup/l10n_br_cte/setup.py new file mode 100644 index 000000000000..28c57bb64031 --- /dev/null +++ b/setup/l10n_br_cte/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From bdb2d2d2b1fe47e5996888fb5dd711b2cc5ad80a Mon Sep 17 00:00:00 2001 From: Marcel Savegnago Date: Thu, 4 Jul 2024 14:34:46 -0300 Subject: [PATCH 5/6] [RFC] l10n_br_cte: full refactor --- l10n_br_cte/__manifest__.py | 5 +- l10n_br_cte/hooks.py | 20 +- l10n_br_cte/models/__init__.py | 1 + l10n_br_cte/models/aquaviario.py | 4 +- l10n_br_cte/models/comment.py | 45 + l10n_br_cte/models/document.py | 829 +++++++++++++++--- l10n_br_cte/models/document_line.py | 201 +---- l10n_br_cte/models/document_related.py | 69 +- l10n_br_cte/models/document_supplement.py | 5 +- l10n_br_cte/models/normal_cte_infos.py | 11 +- l10n_br_cte/models/res_company.py | 41 +- l10n_br_cte/models/res_partner.py | 28 +- l10n_br_cte/static/description/index.html | 11 +- l10n_br_cte/views/cte_document.xml | 13 +- l10n_br_cte/views/res_company.xml | 12 +- .../wizards/document_correction_wizard.xml | 30 + .../models/v4_0/cte_tipos_basico_v4_00.py | 1 - .../static/description/index.html | 11 +- 18 files changed, 892 insertions(+), 445 deletions(-) create mode 100644 l10n_br_cte/models/comment.py create mode 100644 l10n_br_cte/wizards/document_correction_wizard.xml diff --git a/l10n_br_cte/__manifest__.py b/l10n_br_cte/__manifest__.py index c88d09f126d5..3e7d3b73fc09 100644 --- a/l10n_br_cte/__manifest__.py +++ b/l10n_br_cte/__manifest__.py @@ -11,7 +11,7 @@ "website": "https://github.com/OCA/l10n-brazil", "development_status": "Alpha", "depends": [ - "l10n_br_fiscal", + "l10n_br_fiscal_edi", "l10n_br_cte_spec", "l10n_br_fiscal_certificate", "spec_driven_model", @@ -27,13 +27,14 @@ "modal/modal_aereo.xml", "views/res_company.xml", "views/cte_document.xml", + "wizards/document_correction_wizard.xml", ], "post_init_hook": "post_init_hook", "installable": True, "auto_install": False, "external_dependencies": { "python": [ - "nfelib>=2.0.0", + "nfelib<=2.0.7", "erpbrasil.assinatura>=1.7.0", "erpbrasil.transmissao>=1.1.0", "erpbrasil.edoc>=2.5.2", diff --git a/l10n_br_cte/hooks.py b/l10n_br_cte/hooks.py index c01e14a97c2b..572921aaf117 100644 --- a/l10n_br_cte/hooks.py +++ b/l10n_br_cte/hooks.py @@ -2,20 +2,12 @@ # License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html from odoo import SUPERUSER_ID, api -from odoo.addons.spec_driven_model import hooks - def post_init_hook(cr, registry): env = api.Environment(cr, SUPERUSER_ID, {}) - hooks.register_hook( - env, - "l10n_br_cte", - "odoo.addons.l10n_br_cte_spec.models.v4_0.cte_tipos_basico_v4_00", - ) - - hooks.post_init_hook( - cr, - registry, - "l10n_br_cte", - "odoo.addons.l10n_br_cte_spec.models.v4_0.cte_tipos_basico_v4_00", - ) + env["cte.40.tcte_infcte"]._register_hook() + # hooks.register_hook( + # env, + # "l10n_br_cte", + # "odoo.addons.l10n_br_cte_spec.models.v4_0.cte_tipos_basico_v4_00", + # ) diff --git a/l10n_br_cte/models/__init__.py b/l10n_br_cte/models/__init__.py index 3aac89ccbd32..40014e44edb5 100644 --- a/l10n_br_cte/models/__init__.py +++ b/l10n_br_cte/models/__init__.py @@ -13,3 +13,4 @@ from . import document_supplement from . import document_transported_vehicles from . import normal_cte_infos +from . import comment diff --git a/l10n_br_cte/models/aquaviario.py b/l10n_br_cte/models/aquaviario.py index 3e5e7107c957..54709b03fba0 100644 --- a/l10n_br_cte/models/aquaviario.py +++ b/l10n_br_cte/models/aquaviario.py @@ -23,7 +23,9 @@ class Aquav(spec_models.StackedModel): cte40_vAFRMM = fields.Monetary(related="document_id.cte40_vAFRMM") - cte40_vPrest = fields.Monetary(related="document_id.cte40_vPrest") + cte40_vPrest = fields.Monetary( + related="document_id.cte40_vTPrest" + ) # TODO: avaliar melhor cte40_xNavio = fields.Char(related="document_id.cte40_xNavio") diff --git a/l10n_br_cte/models/comment.py b/l10n_br_cte/models/comment.py new file mode 100644 index 000000000000..b84b7df44441 --- /dev/null +++ b/l10n_br_cte/models/comment.py @@ -0,0 +1,45 @@ +# Copyright 2024 - TODAY, Marcel Savegnago +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields + +from odoo.addons.spec_driven_model.models import spec_models + + +class CTeComment(spec_models.StackedModel): + + _name = "l10n_br_fiscal.comment" + _inherit = ["l10n_br_fiscal.comment", "cte.40.tcte_obscont", "cte.40.tcte_obsfisco"] + _stacked = "cte.40.tcte_obscont" + _field_prefix = "cte40_" + _schema_name = "cte" + _schema_version = "4.0.0" + _odoo_module = "l10n_br_cte" + _spec_module = "odoo.addons.l10n_br_cte_spec.models.v4_0.cte_tipos_basico_v4_00" + _spec_tab_name = "CTe" + _stacking_points = {} + _stack_skip = ("cte40_ObsCont_compl_id", "cte40_ObsFisco_compl_id") + _binding_module = "nfelib.cte.bindings.v4_0.cte_tipos_basico_v4_00" + + cte40_xCampo = fields.Char() + + cte40_xTexto = fields.Text() + + def _export_field(self, xsd_field, class_obj, member_spec, export_value=None): + if xsd_field == "cte40_xCampo": + return self.name[:20].strip() + if xsd_field == "cte40_xTexto": + # Aparentemente isso terá que ser feito de outra forma + if ( + "active_id" in self.env.context + and self.env.context.get("active_model") + == "l10n_br_fiscal.document.line" + ): + active_id = self.env.context["active_id"] + doc = self.env["l10n_br_fiscal.document"].browse(active_id) + vals = {"user": self.env.user, "ctx": self._context, "doc": doc} + message = self.compute_message(vals).strip() + if self.comment_type == "fiscal": + return message[:60] + return message[:160] + return super()._export_field(xsd_field, class_obj, member_spec, export_value) diff --git a/l10n_br_cte/models/document.py b/l10n_br_cte/models/document.py index 20b0229e4832..c7bcb8f4d1a1 100644 --- a/l10n_br_cte/models/document.py +++ b/l10n_br_cte/models/document.py @@ -1,14 +1,19 @@ # Copyright 2023 KMEE INFORMATICA LTDA # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +import base64 import logging import re +import sys from datetime import datetime -from erpbrasil.transmissao import TransmissaoSOAP +from erpbrasil.edoc.cte import TransmissaoCTE +from lxml import etree from nfelib.cte.bindings.v4_0.cte_v4_00 import Cte +from nfelib.cte.bindings.v4_0.proc_cte_v4_00 import CteProc from nfelib.nfe.ws.edoc_legacy import CTeAdapter as edoc_cte from requests import Session +from xsdata.formats.dataclass.parsers import XmlParser from odoo import _, api, fields from odoo.exceptions import UserError @@ -18,26 +23,42 @@ TRAFMUT_FERREMI, TRAFMUT_RESPFAT, ) +from odoo.addons.l10n_br_cte_spec.models.v4_0.cte_tipos_basico_v4_00 import ( + COMDATA_TPPER, + SEMHORA_TPHOR, +) from odoo.addons.l10n_br_fiscal.constants.fiscal import ( AUTORIZADO, CANCELADO, CANCELADO_DENTRO_PRAZO, CANCELADO_FORA_PRAZO, DENEGADO, + DOCUMENT_ISSUER_COMPANY, EVENT_ENV_HML, EVENT_ENV_PROD, + EVENTO_RECEBIDO, LOTE_PROCESSADO, + PROCESSADOR_OCA, + SITUACAO_EDOC_A_ENVIAR, SITUACAO_EDOC_AUTORIZADA, SITUACAO_EDOC_CANCELADA, SITUACAO_EDOC_DENEGADA, + SITUACAO_EDOC_EM_DIGITACAO, SITUACAO_EDOC_REJEITADA, SITUACAO_FISCAL_CANCELADO, SITUACAO_FISCAL_CANCELADO_EXTEMPORANEO, ) +from odoo.addons.l10n_br_fiscal.constants.icms import ICMS_CST, ICMS_SN_CST from odoo.addons.spec_driven_model.models import spec_models from ..constants.modal import CTE_MODAL_VERSION_DEFAULT +CTE_XML_NAMESPACE = {"cte": "http://www.portalfiscal.inf.br/cte"} + +# TODO: https://github.com/Engenere/BrazilFiscalReport/pull/23 +# from brazilfiscalreport.dacte import Dacte + + _logger = logging.getLogger(__name__) try: pass @@ -57,29 +78,66 @@ def filter_processador_edoc_cte(record): class CTe(spec_models.StackedModel): _name = "l10n_br_fiscal.document" - _inherit = ["l10n_br_fiscal.document", "cte.40.tcte_infcte", "cte.40.tcte_fat"] + _inherit = [ + "l10n_br_fiscal.document", + "cte.40.tcte_infcte", + "cte.40.tcte_imp", + "cte.40.tcte_fat", + ] _stacked = "cte.40.tcte_infcte" _field_prefix = "cte40_" _schema_name = "cte" _schema_version = "4.0.0" _odoo_module = "l10n_br_cte" _spec_module = "odoo.addons.l10n_br_cte_spec.models.v4_0.cte_tipos_basico_v4_00" + _spec_tab_name = "CTe" _binding_module = "nfelib.cte.bindings.v4_0.cte_tipos_basico_v4_00" + _stack_skip = ( + "cte40_fluxo", + "cte40_semData", + "cte40_noInter", + "cte40_comHora", + "cte40_noPeriodo", + ) _cte_search_keys = ["cte40_Id"] + # all m2o at this level will be stacked even if not required: + _force_stack_paths = ( + "infcte.compl", + "infcte.compl.entrega" "infcte.vprest", + "infcte.imp", + ) + INFCTE_TREE = """ > infCte > - - res.partner + > res.partner + > + > + - + ≡ + ≡ + > res.company - > res.partner + > res.company + > res.partner + > res.partner > + ≡ > - - - - + > + > > - - - - """ + > + ≡ + > + ≡ + ≡ + ≡ + - > + > + > + > """ ########################## # CT-e spec related fields @@ -158,9 +216,7 @@ def _inverse_cte40_Id(self): cte40_xMunEnv = fields.Char(compute="_compute_cte40_data", store=True) - cte40_UFEnv = fields.Char( - compute="_compute_cte40_data", string="cte40_UFEnv", store=True - ) + cte40_UFEnv = fields.Char(compute="_compute_cte40_data", store=True) cte40_indIEToma = fields.Selection( selection=[ @@ -171,25 +227,17 @@ def _inverse_cte40_Id(self): default="1", ) - cte40_cMunIni = fields.Char(compute="_compute_cte40_data", store=True) + cte40_cMunIni = fields.Char(compute="_compute_cte40_data") - cte40_xMunIni = fields.Char(compute="_compute_cte40_data", store=True) + cte40_xMunIni = fields.Char(compute="_compute_cte40_data") - cte40_UFIni = fields.Char(compute="_compute_cte40_data", store=True) + cte40_UFIni = fields.Char(compute="_compute_cte40_data") - cte40_cMunFim = fields.Char( - compute="_compute_cte40_data", - related="partner_id.city_id.ibge_code", - store=True, - ) + cte40_cMunFim = fields.Char(compute="_compute_cte40_data") - cte40_xMunFim = fields.Char( - compute="_compute_cte40_data", related="partner_id.city_id.name", store=True - ) + cte40_xMunFim = fields.Char(compute="_compute_cte40_data") - cte40_UFFim = fields.Char( - compute="_compute_cte40_data", string="cte40_cUF", store=True - ) + cte40_UFFim = fields.Char(compute="_compute_cte40_data") cte40_retira = fields.Selection(selection=[("0", "Sim"), ("1", "Não")], default="1") @@ -258,22 +306,6 @@ def _export_fields_cte_40_tcte_toma4(self, xsd_fields, class_obj, export_dict): cte40_toma = fields.Selection(related="service_provider") - cte40_CNPJ = fields.Char( - related="partner_id.cte40_CNPJ", - ) - cte40_CPF = fields.Char( - related="partner_id.cte40_CPF", - ) - cte40_IE = fields.Char( - related="partner_id.cte40_IE", - ) - cte40_xNome = fields.Char( - related="partner_id.legal_name", - ) - cte40_xFant = fields.Char( - related="partner_id.name", - ) - cte40_enderToma = fields.Many2one(comodel_name="res.partner", related="partner_id") ########################## @@ -306,19 +338,41 @@ def _compute_cct(self): if rec.document_key: rec.cte40_cCT = rec.document_key[35:43] - @api.depends("partner_id", "company_id") + @api.depends( + "partner_id", + "company_id", + "cte40_rem", + "cte40_dest", + "cte40_exped", + "cte40_receb", + ) def _compute_cte40_data(self): for doc in self: if doc.company_id.partner_id.country_id == doc.partner_id.country_id: - doc.cte40_xMunIni = doc.company_id.partner_id.city_id.name - doc.cte40_cMunIni = doc.company_id.partner_id.city_id.ibge_code - doc.cte40_xMunEnv = doc.company_id.partner_id.city_id.name + doc.cte40_xMunEnv = ( + doc.company_id.partner_id.city_id.name + ) # TODO: provavelmente vai depender de quem é o emissor doc.cte40_cMunEnv = doc.company_id.partner_id.city_id.ibge_code doc.cte40_UFEnv = doc.company_id.partner_id.state_id.code - doc.cte40_UFIni = doc.company_id.partner_id.state_id.code - doc.cte40_cMunFim = doc.partner_id.city_id.ibge_code - doc.cte40_xMunFim = doc.partner_id.city_id.name - doc.cte40_UFFim = doc.partner_id.state_id.code + doc.cte40_xMunIni = ( + doc.cte40_exped.city_id.name or doc.cte40_rem.city_id.name + ) + doc.cte40_cMunIni = ( + doc.cte40_exped.city_id.ibge_code or doc.cte40_rem.city_id.ibge_code + ) + doc.cte40_UFIni = ( + doc.cte40_exped.state_id.code or doc.cte40_rem.state_id.code + ) + doc.cte40_xMunFim = ( + doc.cte40_receb.city_id.name or doc.cte40_dest.city_id.name + ) + doc.cte40_cMunFim = ( + doc.cte40_receb.city_id.ibge_code + or doc.cte40_dest.city_id.ibge_code + ) + doc.cte40_UFFim = ( + doc.cte40_receb.state_id.code or doc.cte40_dest.state_id.code + ) else: doc.cte40_UFIni = "EX" doc.cte40_UFEnv = "EX" @@ -334,20 +388,82 @@ def _compute_cte40_data(self): doc.cte40_xMunFim = "EXTERIOR" doc.cte40_UFFim = "EX" + ########################## + # CT-e tag: compl + ########################## + + cte40_xObs = fields.Text(compute="_compute_cte40_compl") + cte40_obsCont = fields.One2many( + "l10n_br_fiscal.comment", compute="_compute_cte40_obsCont" + ) + + cte40_obsFisco = fields.One2many( + "l10n_br_fiscal.comment", compute="_compute_cte40_obsCont" + ) + + ########################## + # CT-e tag: compl + # Methods + ########################## + + @api.depends("comment_ids") + def _compute_cte40_obsCont(self): + for doc in self: + doc.cte40_obsCont = doc.comment_ids.filtered( + lambda c: c.comment_type == "commercial" + ) + doc.cte40_obsFisco = doc.comment_ids.filtered( + lambda c: c.comment_type == "fiscal" + ) + + def _compute_cte40_compl(self): + for doc in self: + fiscal_data = ( + doc.fiscal_additional_data if doc.fiscal_additional_data else "" + ) + customer_data = ( + doc.customer_additional_data if doc.customer_additional_data else "" + ) + doc.cte40_xObs = (fiscal_data + " " + customer_data)[:256].strip() + + ########################## + # CT-e tag: entrega + ########################## + + # TODO: pensar em algo genericoom base nisso decidir quais tags + # puxar (comData,semData,noPeriodo...) + cte40_tpPer = fields.Selection( + selection=COMDATA_TPPER, string="Tipo de data/período programado", default="2" + ) + cte40_dProg = fields.Date("Data Programada", default=fields.Date.today) + + cte40_tpHor = fields.Selection(SEMHORA_TPHOR, string="Tipo de hora", default="0") + ########################## # CT-e tag: emit ########################## - cte40_emit = fields.Many2one( - comodel_name="res.company", + cte40_CNPJ = fields.Char( + compute="_compute_emit_data", + ) + cte40_CPF = fields.Char( compute="_compute_emit_data", - readonly=True, - string="Emit", + ) + cte40_IE = fields.Char( + compute="_compute_emit_data", + ) + cte40_xNome = fields.Char( + compute="_compute_emit_data", + ) + cte40_xFant = fields.Char( + compute="_compute_emit_data", + ) + cte40_enderEmit = fields.Many2one( + comodel_name="res.partner", compute="_compute_emit_data" ) cte40_CRT = fields.Selection( - related="company_tax_framework", - string="Código de Regime Tributário (NFe)", + compute="_compute_emit_data", ) ########################## @@ -355,9 +471,25 @@ def _compute_cte40_data(self): # Compute Methods ########################## + @api.depends("company_id", "partner_id", "issuer") def _compute_emit_data(self): - for doc in self: # TODO if out - doc.cte40_emit = doc.company_id + for doc in self: + if doc.issuer == DOCUMENT_ISSUER_COMPANY: + doc.cte40_CNPJ = doc.company_id.partner_id.cte40_CNPJ + doc.cte40_CPF = doc.company_id.partner_id.cte40_CPF + doc.cte40_IE = doc.company_id.partner_id.cte40_IE + doc.cte40_xNome = doc.company_id.partner_id.legal_name + doc.cte40_xFant = doc.company_id.partner_id.name + doc.cte40_enderEmit = doc.company_id.partner_id + doc.cte40_CRT = doc.company_tax_framework + else: + doc.cte40_CNPJ = doc.partner_id.cte40_CNPJ + doc.cte40_CPF = doc.partner_id.cte40_CPF + doc.cte40_IE = doc.partner_id.cte40_IE + doc.cte40_xNome = doc.partner_id.legal_name + doc.cte40_xFant = doc.partner_id.name + doc.cte40_enderEmit = doc.partner_id + doc.cte40_CRT = doc.partner_tax_framework ########################## # CT-e tag: rem @@ -365,78 +497,240 @@ def _compute_emit_data(self): cte40_rem = fields.Many2one( comodel_name="res.partner", - compute="_compute_rem_data", - readonly=True, - string="Rem", + string="Remetente", ) - ########################## - # CT-e tag: rem - # Compute Methods - ########################## - - def _compute_rem_data(self): - for doc in self: # TODO if out - doc.cte40_rem = doc.partner_id - ########################## # CT-e tag: exped ########################## cte40_exped = fields.Many2one( comodel_name="res.partner", - compute="_compute_exped_data", - readonly=True, - string="Exped", + string="Expedidor", ) ########################## - # CT-e tag: exped - # Compute Methods + # CT-e tag: dest ########################## - def _compute_exped_data(self): - for doc in self: # TODO if out - doc.cte40_exped = doc.company_id.partner_id + cte40_dest = fields.Many2one( + comodel_name="res.partner", string="Destinatário", related="partner_shipping_id" + ) ########################## - # CT-e tag: dest + # CT-e tag: receb ########################## - cte40_dest = fields.Many2one( + cte40_receb = fields.Many2one( comodel_name="res.partner", - compute="_compute_dest_data", - readonly=True, - string="Dest", + string="Recebedor", ) ########################## - # CT-e tag: dest - # Compute Methods + # CT-e tag: vPrest + # Methods ########################## - def _compute_dest_data(self): - for doc in self: # TODO if out - doc.cte40_dest = doc.partner_shipping_id + cte40_vTPrest = fields.Monetary( + compute="_compute_cte40_vPrest", + string="Valor da Total Prestação Base de Cálculo", + ) - ########################## - # CT-e tag: imp TODO - ########################## + cte40_vRec = fields.Monetary( + compute="_compute_cte40_vPrest", + string="Valor Recebido", + ) - cte40_imp = fields.One2many( + cte40_comp = fields.One2many( comodel_name="l10n_br_fiscal.document.line", inverse_name="document_id", related="fiscal_line_ids", ) + def _compute_cte40_vPrest(self): + vTPrest = 0 + vRec = 0 + for doc in self: + for line in self.fiscal_line_ids: + vTPrest += line.amount_total + vRec += line.price_gross + doc.cte40_vTPrest = vTPrest + doc.cte40_vRec = vRec + + ################################################## + # CT-e tag: ICMS + # Grupo N01. Grupo Tributação do ICMS= 00 + # Grupo N02. Grupo Tributação do ICMS= 20 + # Grupo N03. Grupo Tributação do ICMS= 45 (40, 41 e 51) + # Grupo N04. Grupo Tributação do ICMS= 60 + # Grupo N05. Grupo Tributação do ICMS= 90 - ICMS outros + # Grupo N06. Grupo Tributação do ICMS= 90 - ICMS Outra UF + # Grupo N06. Grupo Tributação do ICMS= 01 - ISSN + ################################################# + + cte40_vTotTrib = fields.Monetary(related="amount_estimate_tax") + + # cte40_infAdFisco = fields.Text(related="additional_data") + + ################################################## + # CT-e tag: ICMS + # Methods + ################################################## + + cte40_choice_icms = fields.Selection( + selection=[ + ("cte40_ICMS00", "ICMS00"), + ("cte40_ICMS20", "ICMS20"), + ("cte40_ICMS45", "ICMS45"), + ("cte40_ICMS60", "ICMS60"), + ("cte40_ICMS90", "ICMS90"), + ("cte40_ICMSOutraUF", "ICMSOutraUF"), + ("cte40_ICMSSN", "ICMSSN"), + ], + string="Tipo de ICMS", + compute="_compute_choice_icms", + store=True, + ) + + cte40_CST = fields.Selection( + selection=[ + ("00", "00 - Tributação normal ICMS"), + ("20", "20 - Tributação com BC reduzida do ICMS"), + ("45", "45 - ICMS Isento, não Tributado ou diferido"), + ("60", "60 - ICMS cobrado por substituição tributária"), + ("90", "90 - ICMS outros"), + ("90", "90 - ICMS Outra UF"), + ("01", "01 - Simples Nacional"), + ], + string="Classificação Tributária do Serviço", + compute="_compute_choice_icms", + store=True, + ) + + # ICMSSN + cte40_indSN = fields.Float(default=1) + + # # ICMSOutraUF + # # TODO + ########################## - # CT-e tag: imp + # CT-e tag: ICMS # Compute Methods ########################## - def _compute_imp(self): - for doc in self: - doc.cte40_ICMS = doc.fiscal_line_ids + @api.depends("fiscal_line_ids") + def _compute_choice_icms(self): + for record in self: + record.cte40_choice_icms = None + record.cte40_CST = None + if not record.fiscal_line_ids: + continue + if record.fiscal_line_ids[0].icms_cst_id.code in ICMS_CST: + if record.fiscal_line_ids[0].icms_cst_id.code in ["40", "41", "50"]: + record.cte40_choice_icms = "cte40_ICMS45" + record.cte40_CST = "45" + elif ( + record.fiscal_line_ids[0].icms_cst_id.code == "90" + and record.partner_id.state_id != record.company_id.state_id + ): + record.cte40_choice_icms = "cte40_ICMSOutraUF" + else: + record.cte40_choice_icms = "{}{}".format( + "cte40_ICMS", record.fiscal_line_ids[0].icms_cst_id.code + ) + record.cte40_CST = record.fiscal_line_ids[0].icms_cst_id.code + elif record.fiscal_line_ids[0].icms_cst_id.code in ICMS_SN_CST: + record.cte40_choice_icms = "cte40_ICMSSN" + record.cte40_CST = "90" + + def _export_fields_icms(self): + # Verifica se fiscal_line_ids está vazio para evitar erros + if not self.fiscal_line_ids: + return {} + + # TODO:aprimorar. talvez criar os campos relacionados com os campos e totais + # do documento fiscal e buscar apenas os percentuais da primeira linha + first_line = self.fiscal_line_ids[0] + + icms = { + "CST": self.cte40_CST, + "vBC": 0.0, + "pRedBC": first_line.icms_reduction, + "pICMS": first_line.icms_percent, + "vICMS": 0.0, + "vICMSSubstituto": 0.0, + "indSN": int(self.cte40_indSN), + "vBCSTRet": 0.0, + "vICMSSTRet": 0.0, + "pICMSSTRet": first_line.icmsst_wh_percent, + } + + for line in self.fiscal_line_ids: + icms["vBC"] += line.icms_base + icms["vICMS"] += line.icms_value + icms["vICMSSubstituto"] += line.icms_substitute + icms["vBCSTRet"] += line.icmsst_wh_base + icms["vICMSSTRet"] += line.icmsst_wh_value + + # Formatar os valores acumulados + icms["vBC"] = str("%.02f" % icms["vBC"]) + icms["vICMS"] = str("%.02f" % icms["vICMS"]) + icms["vICMSSubstituto"] = str("%.02f" % icms["vICMSSubstituto"]) + icms["vBCSTRet"] = str("%.02f" % icms["vBCSTRet"]) + icms["vICMSSTRet"] = str("%.02f" % icms["vICMSSTRet"]) + icms["pRedBC"] = str("%.04f" % icms["pRedBC"]) + icms["pICMS"] = str("%.02f" % icms["pICMS"]) + icms["pICMSSTRet"] = str("%.02f" % icms["pICMSSTRet"]) + + return icms + + def _export_fields_cte_40_timp(self, xsd_fields, class_obj, export_dict): + # TODO Not Implemented + if "cte40_ICMSOutraUF" in xsd_fields: + xsd_fields.remove("cte40_ICMSOutraUF") + + xsd_fields = [self.cte40_choice_icms] + icms_tag = ( + self.cte40_choice_icms.replace("cte40_", "") + .replace("ICMSSN", "Icmssn") + .replace("ICMS", "Icms") + ) + binding_module = sys.modules[self._binding_module] + icms = binding_module.Timp + icms_binding = getattr(icms, icms_tag) + icms_dict = self._export_fields_icms() + sliced_icms_dict = { + key: icms_dict.get(key) + for key in icms_binding.__dataclass_fields__.keys() + if icms_dict.get(key) + } + export_dict[icms_tag.upper()] = icms_binding(**sliced_icms_dict) + + # ########################## + # # CT-e tag: ICMSUFFim + # ########################## + + # cte40_vBCUFFim = fields.Monetary(related="icms_destination_base") + # cte40_pFCPUFFim = fields.Monetary(compute="_compute_cte40_ICMSUFFim", store=True) + # cte40_pICMSUFFim = fields.Monetary(compute="_compute_cte40_ICMSUFFim", store=True) + # # TODO + # # cte40_pICMSInter = fields.Selection( + # # selection=[("0", "Teste")], + # # compute="_compute_cte40_ICMSUFFim") + + # def _compute_cte40_ICMSUFFim(self): + # for record in self: + # # if record.icms_origin_percent: + # # record.cte40_pICMSInter = str("%.02f" % record.icms_origin_percent) + # # else: + # # record.cte40_pICMSInter = False + + # record.cte40_pFCPUFFim = record.icmsfcp_percent + # record.cte40_pICMSUFFim = record.icms_destination_percent + + # cte40_vFCPUFfim = fields.Monetary(related="icmsfcp_value") + # cte40_vICMSUFFim = fields.Monetary(related="icms_destination_value") + # cte40_vICMSUFIni = fields.Monetary(related="icms_origin_value") ##################################### # CT-e tag: infCTeNorm and infCteComp @@ -470,7 +764,6 @@ def _compute_imp(self): cte40_proPred = fields.Char( string="Produto predominante", - required=True, ) cte40_xOutCat = fields.Char( @@ -480,12 +773,55 @@ def _compute_imp(self): cte40_infQ = fields.One2many( comodel_name="l10n_br_cte.cargo.quantity.infos", inverse_name="document_id", + compute="_compute_cte40_infQ", + readonly=False, + store=True, ) cte40_vCargaAverb = fields.Monetary( string="Valor da Carga para efeito de averbação", ) + ########################## + # CT-e tag: infDoc + ########################## + + cte40_infDoc = fields.Many2one( + comodel_name="l10n_br_fiscal.document", + compute="_compute_cte40_infDoc", + string="Informações dos documentos transportados", + ) + + def _compute_cte40_infDoc(self): + for doc in self: + doc.cte40_infDoc = doc + + def _compute_cte40_infNFe(self): + for record in self: + record.cte40_infNFe = record.document_related_ids.filtered( + lambda r: r.cte40_infDoc == "cte40_infNFe" + ) + + def _compute_cte40_infOutros(self): + for record in self: + record.cte40_infOutros = record.document_related_ids.filtered( + lambda r: r.cte40_infDoc == "cte40_infOutros" + ) + + cte40_infNFe = fields.One2many( + comodel_name="l10n_br_fiscal.document.related", + inverse_name="document_id", + string="Informações das NF-e DOCS (Cte)", + compute="_compute_cte40_infNFe", + ) + + cte40_infOutros = fields.One2many( + comodel_name="l10n_br_fiscal.document.related", + inverse_name="document_id", + string="Informações dos Outros DOCS (Cte)", + compute="_compute_cte40_infOutros", + ) + ########################## # CT-e tag: veicNovos ########################## @@ -516,7 +852,7 @@ def _default_cte40_autxml(self): cte40_autXML = fields.One2many(default=_default_cte40_autxml) ########################## - # NF-e tag: infCTeSupl + # CT-e tag: infCTeSupl ########################## cte40_infCTeSupl = fields.Many2one( @@ -524,7 +860,7 @@ def _default_cte40_autxml(self): ) ########################## - # MDF-e tag: infRespTec + # CT-e tag: infRespTec ########################## cte40_infRespTec = fields.Many2one( @@ -593,6 +929,14 @@ def _compute_infresptec(self): ), ) + # TODO: avaliar + # def _compute_dime(self): + # for record in self: + # for package in record.product_id.packaging_ids: + # record.cte40_xDime = ( + # package.width + "X" + package.packaging_length + "X" + package.width + # ) + cte40_CL = fields.Char( string="Classe", help=( @@ -625,11 +969,12 @@ def _compute_infresptec(self): # Campos do Modal Aquaviario modal_aquaviario_id = fields.Many2one(comodel_name="l10n_br_cte.modal.aquav") - cte40_vPrest = fields.Monetary( - compute="_compute_cte40_vPrest", # FIX - store=True, - string="Valor da Prestação Base de Cálculo", - ) + # TODO: fix + # cte40_vPrest = fields.Monetary( + # compute="_compute_cte40_vPrest", # FIX + # store=True, + # string="Valor da Prestação Base de Cálculo", + # ) cte40_vAFRMM = fields.Monetary( string="AFRMM", @@ -738,12 +1083,21 @@ def _compute_infresptec(self): cte40_RNTRC = fields.Char( string="RNTRC", help="Registro Nacional de Transportadores Rodoviários de Carga", + compute="_compute_cte40_RNTRC", ) + def _compute_cte40_RNTRC(self): + for record in self: + if record.issuer == DOCUMENT_ISSUER_COMPANY: + record.cte40_RNTRC = record.company_id.partner_id.rntrc_code + else: + record.cte40_RNTRC = record.partner_id.rntrc_code + cte40_occ = fields.One2many( comodel_name="l10n_br_cte.modal.rodo.occ", inverse_name="document_id", string="Ordens de Coleta associados", + copy=False, ) ########################## @@ -751,12 +1105,6 @@ def _compute_infresptec(self): # Compute Methods ########################## - def _compute_cte40_vPrest(self): - vPrest = 0 - for record in self.fiscal_line_ids: - vPrest += record.cte40_vTPrest - self.cte40_vPrest = vPrest - def _export_fields_cte_40_tcte_infmodal(self, xsd_fields, class_obj, export_dict): self = self.with_context(module="l10n_br_cte") if self.cte40_modal == "01": @@ -808,6 +1156,21 @@ def _export_modal_dutoviario(self): return self.modal_dutoviario_id.export_ds()[0] + ################################ + # Framework Spec model's methods + ################################ + + def _export_field(self, xsd_field, class_obj, member_spec, export_value=None): + if xsd_field == "cte40_tpAmb": + self.env.context = dict(self.env.context) + self.env.context.update({"tpAmb": self[xsd_field]}) + + # TODO: Força a remoção da tag infGlobalizado já que o + # campo xObs está no l10n_br_fiscal.document + if xsd_field == "cte40_infGlobalizado": + return False + return super()._export_field(xsd_field, class_obj, member_spec, export_value) + ################################ # Business Model Methods ################################ @@ -818,7 +1181,10 @@ def _serialize(self, edocs): filter_processador_edoc_cte ): inf_cte = record.export_ds()[0] - cte = Cte(infCte=inf_cte, infCTeSupl=None, signature=None) + inf_cte_supl = None + if record.cte40_infCTeSupl: + inf_cte_supl = record.cte40_infCTeSupl.export_ds()[0] + cte = Cte(infCte=inf_cte, infCTeSupl=inf_cte_supl, signature=None) edocs.append(cte) return edocs @@ -829,7 +1195,7 @@ def _processador(self): certificado = self.env.company._get_br_ecertificate() session = Session() session.verify = False - transmissao = TransmissaoSOAP(certificado, session) + transmissao = TransmissaoCTE(certificado, session) return edoc_cte( transmissao, self.company_id.state_id.ibge_code, @@ -863,10 +1229,17 @@ def _valida_xml(self, xml_file): erros = "\n".join(erros) self.write({"xml_error_message": erros or False}) - def atualiza_status_cte(self, infProt, xml_file): + def atualiza_status_cte(self, processo): self.ensure_one() + + if hasattr(processo, "protocolo"): + infProt = processo.protocolo.infProt + else: + infProt = processo.resposta.protCTe.infProt + if infProt.cStat in AUTORIZADO: state = SITUACAO_EDOC_AUTORIZADA + self._cte_response_add_proc(processo) elif infProt.cStat in DENEGADO: state = SITUACAO_EDOC_DENEGADA else: @@ -884,7 +1257,7 @@ def atualiza_status_cte(self, infProt, xml_file): response=infProt.xMotivo, protocol_date=protocol_date, protocol_number=infProt.nProt, - file_response_xml=xml_file, + file_response_xml=processo.processo_xml.decode("utf-8"), ) self.write( { @@ -953,36 +1326,230 @@ def _cte_cancel(self): self.cancel_event_id = self.event_ids.create_event_save_xml( company_id=self.company_id, - environment=( - EVENT_ENV_PROD if self.cte_environment == "1" else EVENT_ENV_HML - ), + environment=(EVENT_ENV_PROD if self.cte40_tpAmb == "1" else EVENT_ENV_HML), event_type="2", - xml_file=processo.envio_xml.decode("utf-8"), + xml_file=processo.envio_xml, document_id=self, ) - for retevento in processo.resposta.retEvento: - if not retevento.infEvento.chCte == self.document_key: - continue + resposta = processo.resposta.infEvento - if retevento.infEvento.cStat not in CANCELADO: - mensagem = "Erro no cancelamento" - mensagem += "\nCódigo: " + retevento.infEvento.cStat - mensagem += "\nMotivo: " + retevento.infEvento.xMotivo - raise UserError(mensagem) + if resposta.cStat not in CANCELADO: + mensagem = "Erro no cancelamento" + mensagem += "\nCódigo: " + resposta.cStat + mensagem += "\nMotivo: " + resposta.xMotivo + raise UserError(mensagem) - if retevento.infEvento.cStat == CANCELADO_FORA_PRAZO: + if resposta.chCTe == self.document_key: + if resposta.cStat in CANCELADO_FORA_PRAZO: self.state_fiscal = SITUACAO_FISCAL_CANCELADO_EXTEMPORANEO - elif retevento.infEvento.cStat == CANCELADO_DENTRO_PRAZO: + elif resposta.cStat in CANCELADO_DENTRO_PRAZO: self.state_fiscal = SITUACAO_FISCAL_CANCELADO self.state_edoc = SITUACAO_EDOC_CANCELADA self.cancel_event_id.set_done( - status_code=retevento.infEvento.cStat, - response=retevento.infEvento.xMotivo, + status_code=resposta.cStat, + response=resposta.xMotivo, protocol_date=fields.Datetime.to_string( - datetime.fromisoformat(retevento.infEvento.dhRegEvento) + datetime.fromisoformat(resposta.dhRegEvento) ), - protocol_number=retevento.infEvento.nProt, + protocol_number=resposta.nProt, file_response_xml=processo.retorno.content.decode("utf-8"), ) + + def _document_correction(self, justificative): + result = super(CTe, self)._document_correction(justificative) + online_event = self.filtered(filter_processador_edoc_cte) + if online_event: + online_event._cte_correction(justificative) + return result + + def _cte_correction(self, justificative): + self.ensure_one() + processador = self._processador() + + numeros = self.event_ids.filtered( + lambda e: e.type == "14" and e.state == "done" + ).mapped("sequence") + + sequence = str(int(max(numeros)) + 1) if numeros else "1" + + evento = processador.carta_correcao( + chave=self.document_key, + protocolo_autorizacao=self.authorization_protocol, + justificativa=justificative.replace("\n", "\\n"), + sequencia=sequence, + ) + processo = processador.enviar_lote_evento(lista_eventos=[evento]) + # Gravamos o arquivo no disco e no filestore ASAP. + event_id = self.event_ids.create_event_save_xml( + company_id=self.company_id, + environment=(EVENT_ENV_PROD if self.cte40_tpAmb == "1" else EVENT_ENV_HML), + event_type="14", + xml_file=processo.envio_xml, + document_id=self, + sequence=sequence, + justification=justificative, + ) + + resposta = processo.resposta.infEvento + + if resposta.cStat not in EVENTO_RECEBIDO and not ( + resposta.chCTe == self.document_key + ): + mensagem = "Erro na carta de correção" + mensagem += "\nCódigo: " + resposta.cStat + mensagem += "\nMotivo: " + resposta.xMotivo + raise UserError(mensagem) + + event_id.set_done( + status_code=resposta.cStat, + response=resposta.xMotivo, + protocol_date=fields.Datetime.to_string( + datetime.fromisoformat(resposta.dhRegEvento) + ), + protocol_number=resposta.nProt, + file_response_xml=processo.retorno.content.decode("utf-8"), + ) + + def _document_qrcode(self): + super()._document_qrcode() + + for record in self: + record.cte40_infCTeSupl = self.env[ + "l10n_br_fiscal.document.supplement" + ].create( + { + "qrcode": record.get_cte_qrcode(), + } + ) + + def get_cte_qrcode(self): + # if self.document_type != MODELO_FISCAL_CTE: + # return + + processador = self._processador() + # if self.nfe_transmission == "1": + # return processador.monta_qrcode(self.document_key) + return processador.monta_qrcode(self.document_key) + + # serialized_doc = self.serialize()[0] + # xml = processador.assina_raiz(serialized_doc, serialized_doc.infNFe.Id) + # return processador._generate_qrcode_contingency(serialized_doc, xml) + + # TODO: nao esta rodando direto.. corrigir + def _compute_cte40_infQ(self): + for record in self: + cargo_info_vals = [ + {"cte40_cUnid": "01", "cte40_tpMed": "Peso Bruto", "cte40_qCarga": 0}, + { + "cte40_cUnid": "01", + "cte40_tpMed": "Peso Base Calculado", + "cte40_qCarga": 0, + }, + {"cte40_cUnid": "01", "cte40_tpMed": "Peso Aferido", "cte40_qCarga": 0}, + {"cte40_cUnid": "00", "cte40_tpMed": "Cubagem", "cte40_qCarga": 0}, + {"cte40_cUnid": "03", "cte40_tpMed": "Unidade", "cte40_qCarga": 0}, + ] + + record.cte40_infQ = self.env["l10n_br_cte.cargo.quantity.infos"].create( + cargo_info_vals + ) + + def _need_compute_cte_tags(self): + if ( + self.state_edoc in [SITUACAO_EDOC_EM_DIGITACAO, SITUACAO_EDOC_A_ENVIAR] + and self.processador_edoc == PROCESSADOR_OCA + and self.document_type_id.code in ["57"] + and self.issuer == DOCUMENT_ISSUER_COMPANY + ): + return True + else: + return False + + # cte40_infAdFisco = fields.Text(related="additional_data") + + # def make_pdf(self): + # if not self.filtered(filter_processador_edoc_cte): + # return super().make_pdf() + + # file_pdf = self.file_report_id + # self.file_report_id = False + # file_pdf.unlink() + + # if self.authorization_file_id: + # arquivo = self.authorization_file_id + # xml_string = base64.b64decode(arquivo.datas).decode() + # else: + # arquivo = self.send_file_id + # xml_string = base64.b64decode(arquivo.datas).decode() + # # TODO: implementar temp_xml_autorizacao igual nfe ? + # # xml_string = self.temp_xml_autorizacao(xml_string) + + # pdf = Dacte(xml=xml_string).output() + + # self.file_report_id = self.env["ir.attachment"].create( + # { + # "name": self.document_key + ".pdf", + # "res_model": self._name, + # "res_id": self.id, + # "datas": base64.b64encode(pdf), + # "mimetype": "application/pdf", + # "type": "binary", + # } + # ) + + def _cte_response_add_proc(self, ws_response_process): + """ + Inject the final NF-e, tag `cteProc`, into the response. + """ + xml_soap = ws_response_process.retorno.content + tree_soap = etree.fromstring(xml_soap) + prot_element = tree_soap.xpath("//cte:protCTe", namespaces=CTE_XML_NAMESPACE)[0] + proc_xml = self._cte_create_proc(prot_element) + if proc_xml: + # it is not always possible to create cteProc. + parser = XmlParser() + proc = parser.from_string(proc_xml.decode(), CteProc) + ws_response_process.processo = proc + ws_response_process.processo_xml = proc_xml + + def _cte_create_proc(self, prot_element): + """ + Create the `cteProc` XML by combining the CT-e and the authorization protocol. + + This method decodes the saved `enviCTe` message, extracts the CTe> tag, + and combines it with the provided authorization protocol element to create + the `cteProc` XML, which represents the finalized CT-e document. + + Args: + prot_element: The XML element containing the authorization protocol. + + Returns: + The assembled `cteProc` XML, or None if the `send_file_id` data is not + found. + + Note: + Useful for recreating the final CT-e XML, as SEFAZ does not provide the + complete XML upon consultation, only the authorization protocol. + """ + self.ensure_one() + + if not self.send_file_id.datas: + _logger.info( + "CT-e data not found when trying to assemble the " + "xml with the authorization protocol (cteProc)" + ) + return None + + processor = self._processador() + + # Extract the tag from the `enviCTe` message, which represents the CT-e + xml_send = base64.b64decode(self.send_file_id.datas) + tree_send = etree.fromstring(xml_send) + doc_element = tree_send.xpath("//cte:CTe", namespaces=CTE_XML_NAMESPACE)[0] + + # Assemble the `cteProc` using the erpbrasil.edoc library. + proc_xml = processor.monta_cte_proc(doc=doc_element, prot=prot_element) + + return proc_xml diff --git a/l10n_br_cte/models/document_line.py b/l10n_br_cte/models/document_line.py index 204dd3bb9491..df7ef721f09b 100644 --- a/l10n_br_cte/models/document_line.py +++ b/l10n_br_cte/models/document_line.py @@ -1,17 +1,15 @@ # Copyright 2023 KMEE # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -import sys -from odoo import api, fields +from odoo import fields -from odoo.addons.l10n_br_fiscal.constants.icms import ICMS_CST, ICMS_SN_CST from odoo.addons.spec_driven_model.models import spec_models class CTeLine(spec_models.StackedModel): _name = "l10n_br_fiscal.document.line" - _inherit = ["l10n_br_fiscal.document.line", "cte.40.tcte_imp"] - _stacked = "cte.40.tcte_imp" + _inherit = ["l10n_br_fiscal.document.line", "cte.40.tcte_vprest_comp"] + _stacked = "cte.40.tcte_vprest_comp" _field_prefix = "cte40_" _schema_name = "cte" _schema_version = "4.0.0" @@ -19,198 +17,13 @@ class CTeLine(spec_models.StackedModel): _spec_module = "odoo.addons.l10n_br_cte_spec.models.v4_0.cte_tipos_basico_v4_00" _spec_tab_name = "CTe" _stacking_points = {} - _force_stack_paths = "tcte_imp.timp" + _stack_skip = ("cte40_Comp_vPrest_id",) _binding_module = "nfelib.cte.bindings.v4_0.cte_tipos_basico_v4_00" ########################## - # CT-e tag: vPrest + # CT-e tag: comp ########################## - cte40_vTPrest = fields.Monetary(string="vTPrest", related="amount_total") + cte40_xNome = fields.Text(related="name") - cte40_vRec = fields.Monetary( - related="price_gross", - string="vRec", - ) - - ################################################## - # CT-e tag: ICMS - # Grupo N01. Grupo Tributação do ICMS= 00 - # Grupo N02. Grupo Tributação do ICMS= 20 - # Grupo N03. Grupo Tributação do ICMS= 45 (40, 41 e 51) - # Grupo N04. Grupo Tributação do ICMS= 60 - # Grupo N05. Grupo Tributação do ICMS= 90 - ICMS outros - # Grupo N06. Grupo Tributação do ICMS= 90 - ICMS Outra UF - # Grupo N06. Grupo Tributação do ICMS= 01 - ISSN - ################################################# - - cte40_ICMS = fields.Many2one( - comodel_name="l10n_br_fiscal.document.line", compute="_compute_icms", store=True - ) - - def _compute_icms(self): - for doc in self: - doc.cte40_ICMS = doc - - cte40_choice_icms = fields.Selection( - selection=[ - ("cte40_ICMS00", "ICMS00"), - ("cte40_ICMS20", "ICMS20"), - ("cte40_ICMS45", "ICMS45"), - ("cte40_ICMS60", "ICMS60"), - ("cte40_ICMS90", "ICMS90"), - ("cte40_ICMSOutraUF", "ICMSOutraUF"), - ("cte40_ICMSSN", "ICMSSN"), - ], - string="Tipo de ICMS", - compute="_compute_choice_icms", - store=True, - ) - - cte40_CST = fields.Selection( - selection=[ - ("00", "00 - Tributação normal ICMS"), - ("20", "20 - Tributação com BC reduzida do ICMS"), - ("45", "45 - ICMS Isento, não Tributado ou diferido"), - ("60", "60 - ICMS cobrado por substituição tributária"), - ("90", "90 - ICMS outros"), - ("90", "90 - ICMS Outra UF"), - ("01", "01 - Simples Nacional"), - ], - string="Classificação Tributária do Serviço", - compute="_compute_choice_icms", - store=True, - ) - - cte40_vTotTrib = fields.Monetary(related="estimate_tax") - - cte40_pICMS = fields.Float(related="icms_percent", string="pICMS") - - cte40_vICMS = fields.Monetary(related="icms_value") - - # ICMS20 - ICMS90 - cte40_pRedBC = fields.Float( - related="icms_reduction", - ) - - cte40_vBC = fields.Monetary(related="icms_base") - - # ICMS60 - cte40_vBCSTRet = fields.Monetary(related="icmsst_wh_base") - - cte40_vICMSSTRet = fields.Monetary(related="icmsst_wh_value") - - # TODO cte40_pICMSTRet = fields.Monetary(related="") - - # ICMSSN - cte40_indSN = fields.Float(default=1) - - # ICMS NF - cte40_vBCST = fields.Monetary(related="icmsst_base") - - # ICMSOutraUF - # TODO - - ########################## - # CT-e tag: ICMS - # Compute Methods - ########################## - - @api.depends("icms_cst_id") - def _compute_choice_icms(self): - for record in self: - record.cte40_choice_icms = None - record.cte40_CST = None - if record.icms_cst_id.code in ICMS_CST: - if record.icms_cst_id.code in ["40", "41", "50"]: - record.cte40_choice_icms = "cte40_ICMS45" - record.cte40_CST = "45" - elif ( - record.icms_cst_id.code == "90" - and self.partner_id.state_id != self.company_id.state_id - ): - record.cte40_choice_icms = "cte40_ICMSOutraUF" - else: - record.cte40_choice_icms = "{}{}".format( - "cte40_ICMS", record.icms_cst_id.code - ) - record.cte40_CST = record.icms_cst_id.code - elif record.icms_cst_id.code in ICMS_SN_CST: - record.cte40_choice_icms = "cte40_ICMSSN" - record.cte40_CST = "90" - - def _export_fields_icms(self): - icms = { - "CST": self.cte40_CST, - "vBC": str("%.02f" % self.icms_base), - "pRedBC": str("%.04f" % self.icms_reduction), - "pICMS": str("%.02f" % self.icms_percent), - "vICMS": str("%.02f" % self.icms_value), - "vICMSSubstituto": str("%.02f" % self.icms_substitute), - "indSN": int(self.cte40_indSN), - "vBCSTRet": str("%.02f" % self.icmsst_wh_base), - "vICMSSTRet": str("%.02f" % self.icmsst_wh_value), - "pICMSSTRet": str("%.02f" % self.icmsst_wh_percent), - } - return icms - - def _export_fields_cte_40_timp(self, xsd_fields, class_obj, export_dict): - # TODO Not Implemented - if "cte40_ICMSOutraUF" in xsd_fields: - xsd_fields.remove("cte40_ICMSOutraUF") - - xsd_fields = [self.cte40_choice_icms] - icms_tag = ( - self.cte40_choice_icms.replace("cte40_", "") - .replace("ICMSSN", "Icmssn") - .replace("ICMS", "Icms") - ) - binding_module = sys.modules[self._binding_module] - icms = binding_module.Timp - icms_binding = getattr(icms, icms_tag) - icms_dict = self._export_fields_icms() - sliced_icms_dict = { - key: icms_dict.get(key) - for key in icms_binding.__dataclass_fields__.keys() - if icms_dict.get(key) - } - export_dict[icms_tag.upper()] = icms_binding(**sliced_icms_dict) - - ########################## - # CT-e tag: ICMSUFFim - ########################## - - cte40_vBCUFFim = fields.Monetary(related="icms_destination_base") - cte40_pFCPUFFim = fields.Monetary(compute="_compute_cte40_ICMSUFFim", store=True) - cte40_pICMSUFFim = fields.Monetary(compute="_compute_cte40_ICMSUFFim", store=True) - # TODO - # cte40_pICMSInter = fields.Selection( - # selection=[("0", "Teste")], - # compute="_compute_cte40_ICMSUFFim") - - def _compute_cte40_ICMSUFFim(self): - for record in self: - # if record.icms_origin_percent: - # record.cte40_pICMSInter = str("%.02f" % record.icms_origin_percent) - # else: - # record.cte40_pICMSInter = False - - record.cte40_pFCPUFFim = record.icmsfcp_percent - record.cte40_pICMSUFFim = record.icms_destination_percent - - cte40_vFCPUFfim = fields.Monetary(related="icmsfcp_value") - cte40_vICMSUFFim = fields.Monetary(related="icms_destination_value") - cte40_vICMSUFIni = fields.Monetary(related="icms_origin_value") - - ########################## - # CT-e tag: natCarga - ########################## - - cte40_xDime = fields.Char(compute="_compute_dime", store=True) - - def _compute_dime(self): - for record in self: - for package in record.product_id.packaging_ids: - record.cte40_xDime = ( - package.width + "X" + package.packaging_length + "X" + package.width - ) + cte40_vComp = fields.Monetary(related="amount_total") diff --git a/l10n_br_cte/models/document_related.py b/l10n_br_cte/models/document_related.py index f41e364f7686..9c84c820905e 100644 --- a/l10n_br_cte/models/document_related.py +++ b/l10n_br_cte/models/document_related.py @@ -11,11 +11,9 @@ class CTeRelated(spec_models.StackedModel): _name = "l10n_br_fiscal.document.related" _inherit = [ "l10n_br_fiscal.document.related", - "cte.40.tcte_infnfe", - "cte.40.tcte_infnf", - "cte.40.tcte_infq", + "cte.40.tcte_infdoc", ] - _stacked = "cte.40.tcte_infnfe" + _stacked = "cte.40.tcte_infdoc" _field_prefix = "cte40_" _schema_name = "cte" _schema_version = "4.0.0" @@ -24,51 +22,42 @@ class CTeRelated(spec_models.StackedModel): _spec_tab_name = "CTe" _binding_module = "nfelib.cte.bindings.v4_0.cte_tipos_basico_v4_00" - # infQ TODO computes/relateds - - cte40_tpMed = fields.Char() - - cte40_qCarga = fields.Float() + # InfNFe + cte40_chave = fields.Char( + compute="_compute_cte_data", + inverse="_inverse_cte40_chave", + ) - cte40_cUnid = fields.Selection( - selection=[ - ("00", "M3"), - ("01", "KG"), - ("02", "TON"), - ("03", "UNIDADE"), - ("04", "LITROS"), - ("05", "MMBTU"), - ], + cte40_tpDoc = fields.Char( + compute="_compute_cte_data", + inverse="_inverse_cte40_tpDoc", ) - # infCarga - cte40_prodPred = fields.Char(string="prodPred") + # infOutros - cte40_vCarga = fields.Monetary( - currency_field="currency_id", compute="_compute_vCarga", store=True - ) + cte40_descOutros = fields.Char(string="Descrição do documento") - currency_id = fields.Many2one( - comodel_name="res.currency", related="company_id.currency_id", readonly=True - ) + cte40_nDoc = fields.Char(string="Número", default="123123") - company_id = fields.Many2one( - comodel_name="res.company", - default=lambda self: self.env.company, + cte40_dEmi = fields.Date( + string="Data de Emissão", + help="Data de Emissão\nFormato AAAA-MM-DD", ) - # InfNFe - cte40_chave = fields.Char( - compute="_compute_cte_data", - inverse="_inverse_cte40_chave", + cte40_vDocFisc = fields.Monetary( + string="Valor do documento", + default=1000.0, + currency_field="brl_currency_id", ) - cte40_tpDoc = fields.Char( - compute="_compute_cte_data", - inverse="_inverse_cte40_tpDoc", + cte40_dPrev = fields.Date( + string="Data prevista de entrega", + help="Data prevista de entrega\nFormato AAAA-MM-DD", ) - cte40_infDoc = fields.Selection(related="cte40_choice_infNF_infNFE_infOutros") + cte40_infDoc = fields.Selection( + related="cte40_choice_infNF_infNFE_infOutros", string="infDoc" + ) # infCteNorm cte40_chCTe = fields.Char(compute="_compute_chCte", string="chCte") @@ -93,13 +82,9 @@ def _compute_chCTe(self): ], compute="_compute_cte_data", inverse="_inverse_cte40_choice_infNF_infNFE_infOutros", + string="CHOICE", ) - def _compute_vCarga(self): - for rec in self: - if rec.document_related_id: - rec.cte40_vCarga += rec.document_related_id.amount_price_gross - @api.depends("document_type_id") def _compute_cte_data(self): """Set schema data which are not just related fields""" diff --git a/l10n_br_cte/models/document_supplement.py b/l10n_br_cte/models/document_supplement.py index d4202faf2e78..10fd555c1514 100644 --- a/l10n_br_cte/models/document_supplement.py +++ b/l10n_br_cte/models/document_supplement.py @@ -10,13 +10,12 @@ class CTeSupplement(spec_models.StackedModel): _name = "l10n_br_fiscal.document.supplement" _inherit = ["l10n_br_fiscal.document.supplement", "cte.40.tcte_infctesupl"] _stacked = "cte.40.tcte_infctesupl" + _field_prefix = "cte40_" _schema_name = "cte" _schema_version = "4.0.0" _odoo_module = "l10n_br_cte" _spec_module = "odoo.addons.l10n_br_cte_spec.models.v4_0.cte_tipos_basico_v4_00" - _binding_module = "nfelib.cte.bindings.v4_0.cte_tipos_basico_v4_00" - _field_prefix = "cte40_" _spec_tab_name = "CTe" - _description = "Informações Complementares do Documento Fiscal" + _binding_module = "nfelib.cte.bindings.v4_0.cte_tipos_basico_v4_00" cte40_qrCodCTe = fields.Char(related="qrcode") diff --git a/l10n_br_cte/models/normal_cte_infos.py b/l10n_br_cte/models/normal_cte_infos.py index 59c470e866f8..71165a69ad6b 100644 --- a/l10n_br_cte/models/normal_cte_infos.py +++ b/l10n_br_cte/models/normal_cte_infos.py @@ -56,7 +56,12 @@ class CTeNormalInfos(spec_models.StackedModel): cte40_infNFe = fields.One2many( comodel_name="l10n_br_fiscal.document.related", - related="document_id.document_related_ids", + related="document_id.cte40_infNFe", + ) + + cte40_infOutros = fields.One2many( + comodel_name="l10n_br_fiscal.document.related", + related="document_id.cte40_infOutros", ) cte40_versaoModal = fields.Char(related="document_id.cte40_versaoModal") @@ -91,7 +96,9 @@ class CTeNormalInfos(spec_models.StackedModel): related="document_id.modal_aquaviario_id", ) - cte40_vPrest = fields.Monetary(related="document_id.cte40_vPrest") + cte40_vPrest = fields.Monetary( + related="document_id.cte40_vTPrest" + ) # TODO: avaliar melhor cte40_vAFRMM = fields.Monetary(related="document_id.cte40_vAFRMM") diff --git a/l10n_br_cte/models/res_company.py b/l10n_br_cte/models/res_company.py index cc465aa6f043..49c9336fba65 100644 --- a/l10n_br_cte/models/res_company.py +++ b/l10n_br_cte/models/res_company.py @@ -1,48 +1,13 @@ # Copyright 2023 KMEE INFORMATICA LTDA # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -from odoo import fields +from odoo import fields, models -from odoo.addons.spec_driven_model.models import spec_models - -class ResCompany(spec_models.SpecModel): +class ResCompany(models.Model): _name = "res.company" - _inherit = ["res.company", "cte.40.tcte_emit", "cte.40.tendeemi", "cte.40.ferroenv"] - _cte_search_keys = ["cte40_CNPJ", "cte40_xNome", "cte40_xFant"] - _binding_module = "nfelib.cte.bindings.v4_0.cte_tipos_basico_v4_00" - _field_prefix = "cte40_" - - ########################## - # CT-e spec fields - ########################## - - cte40_CNPJ = fields.Char( - related="partner_id.cte40_CNPJ", - ) - cte40_CPF = fields.Char( - related="partner_id.cte40_CPF", - ) - cte40_IE = fields.Char( - related="partner_id.cte40_IE", - ) - cte40_xNome = fields.Char( - related="partner_id.legal_name", - ) - cte40_xFant = fields.Char( - related="partner_id.name", - ) - cte40_CRT = fields.Selection( - related="tax_framework", - ) - - cte40_enderEmit = fields.Many2one( - comodel_name="res.partner", - related="partner_id", - ) - - cte40_enderToma = fields.Many2one(comodel_name="res.partner", related="partner_id") + _inherit = ["res.company"] ########################## # CT-e models fields diff --git a/l10n_br_cte/models/res_partner.py b/l10n_br_cte/models/res_partner.py index 2d636ae6bf29..a4546c11aad9 100644 --- a/l10n_br_cte/models/res_partner.py +++ b/l10n_br_cte/models/res_partner.py @@ -69,6 +69,10 @@ class ResPartner(spec_models.SpecModel): comodel_name="res.partner", compute="_compute_cte40_ender" ) + cte40_enderReceb = fields.Many2one( + comodel_name="res.partner", compute="_compute_cte40_ender" + ) + cte40_enderFerro = fields.Many2one( comodel_name="res.partner", compute="_compute_cte40_ender" ) @@ -85,7 +89,6 @@ class ResPartner(spec_models.SpecModel): compute="_compute_cep", inverse="_inverse_cte40_CEP", compute_sudo=True, - store=True, ) cte40_cPais = fields.Char( related="country_id.bc_code", @@ -98,6 +101,14 @@ class ResPartner(spec_models.SpecModel): cte40_xNome = fields.Char(related="legal_name") + cte40_xContato = fields.Char(related="legal_name") + + cte40_email = fields.Char(related="email") + + cte40_fone = fields.Char( + compute="_compute_cte_data", inverse="_inverse_cte40_fone", compute_sudo=True + ) + def _compute_cte40_IE(self): for rec in self: rec.cte40_IE = str(rec.inscr_est).replace(".", "") @@ -108,6 +119,7 @@ def _compute_cte40_ender(self): rec.cte40_enderReme = rec.id rec.cte40_enderDest = rec.id rec.cte40_enderExped = rec.id + rec.cte40_enderReceb = rec.id rec.cte40_enderFerro = rec.id @api.depends("company_type", "inscr_est", "cnpj_cpf", "country_id") @@ -121,6 +133,7 @@ def _compute_cte_data(self): else: rec.cte40_CNPJ = None rec.cte40_CPF = cnpj_cpf + rec.cte40_fone = punctuation_rm(rec.phone or "").replace(" ", "") def _inverse_cte40_CEP(self): for rec in self: @@ -128,11 +141,24 @@ def _inverse_cte40_CEP(self): country_code = rec.country_id.code if rec.country_id else "BR" rec.zip = format_zipcode(rec.cte40_CEP, country_code) + def _inverse_cte40_fone(self): + for rec in self: + if rec.cte40_fone: + rec.phone = rec.nfe40_fone + def _compute_cep(self): for rec in self: rec.cte40_CEP = punctuation_rm(rec.zip) def _export_field(self, xsd_field, class_obj, member_spec, export_value=None): + if ( + xsd_field == "cte40_xNome" + and class_obj._name + in ["cte.40.tcte_rem", "cte.40.tcte_dest", "cte.40.exped", "cte.40.receb"] + and self.env.context.get("tpAmb") == "2" + ): + return "CTE EMITIDO EM AMBIENTE DE HOMOLOGACAO - SEM VALOR FISCAL" + if not self.cnpj_cpf and self.parent_id: cnpj_cpf = punctuation_rm(self.parent_id.cnpj_cpf) else: diff --git a/l10n_br_cte/static/description/index.html b/l10n_br_cte/static/description/index.html index 4f44cdb9306d..e1d718ef2fd5 100644 --- a/l10n_br_cte/static/description/index.html +++ b/l10n_br_cte/static/description/index.html @@ -8,10 +8,11 @@ /* :Author: David Goodger (goodger@python.org) -:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $ +:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z 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. @@ -274,7 +275,7 @@ margin-left: 2em ; margin-right: 2em } -pre.code .ln { color: grey; } /* line numbers */ +pre.code .ln { color: gray; } /* 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 } @@ -300,7 +301,7 @@ span.pre { white-space: pre } -span.problematic { +span.problematic, pre.problematic { color: red } span.section-subtitle { @@ -447,7 +448,9 @@

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_cte/views/cte_document.xml b/l10n_br_cte/views/cte_document.xml index ef90107f0474..1f092ef14c5e 100644 --- a/l10n_br_cte/views/cte_document.xml +++ b/l10n_br_cte/views/cte_document.xml @@ -21,6 +21,17 @@ + + + + + + + + + + + @@ -97,7 +108,7 @@ - + diff --git a/l10n_br_cte/views/res_company.xml b/l10n_br_cte/views/res_company.xml index 8907285e0e3a..b4faac397deb 100644 --- a/l10n_br_cte/views/res_company.xml +++ b/l10n_br_cte/views/res_company.xml @@ -12,19 +12,17 @@ - - + + - - - - - + + + diff --git a/l10n_br_cte/wizards/document_correction_wizard.xml b/l10n_br_cte/wizards/document_correction_wizard.xml new file mode 100644 index 000000000000..7b93ccffab13 --- /dev/null +++ b/l10n_br_cte/wizards/document_correction_wizard.xml @@ -0,0 +1,30 @@ + + + + + + l10n_br_fiscal.document.correction.wizard + + + + + + + +
+ Por favor, para carta de correção de CT-e, no campo Justificativa informe por linha (Grupo Alterado;Campo Alterado;Valor Alterado).
+ Exemplo:
+ compl;xObs;Nova Observação
+ ide;cfop;6353 +
+
+
+
+
+ +
diff --git a/l10n_br_cte_spec/models/v4_0/cte_tipos_basico_v4_00.py b/l10n_br_cte_spec/models/v4_0/cte_tipos_basico_v4_00.py index 86ad46ec637c..0cfffed535bf 100644 --- a/l10n_br_cte_spec/models/v4_0/cte_tipos_basico_v4_00.py +++ b/l10n_br_cte_spec/models/v4_0/cte_tipos_basico_v4_00.py @@ -4132,7 +4132,6 @@ class TcteEmit(models.AbstractModel): cte40_enderEmit = fields.Many2one( comodel_name="cte.40.tendeemi", string="Endereço do emitente", - xsd_required=True, xsd_type="TEndeEmi", ) diff --git a/l10n_br_cte_spec/static/description/index.html b/l10n_br_cte_spec/static/description/index.html index 9f1bed9fc10b..7120355cce7a 100644 --- a/l10n_br_cte_spec/static/description/index.html +++ b/l10n_br_cte_spec/static/description/index.html @@ -8,10 +8,11 @@ /* :Author: David Goodger (goodger@python.org) -:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $ +:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z 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. @@ -274,7 +275,7 @@ margin-left: 2em ; margin-right: 2em } -pre.code .ln { color: grey; } /* line numbers */ +pre.code .ln { color: gray; } /* 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 } @@ -300,7 +301,7 @@ span.pre { white-space: pre } -span.problematic { +span.problematic, pre.problematic { color: red } span.section-subtitle { @@ -429,7 +430,9 @@

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.

From a9eda9403847164be9988c9055d4e6a1116a2f0b Mon Sep 17 00:00:00 2001 From: Marcel Savegnago Date: Mon, 16 Sep 2024 12:56:15 -0300 Subject: [PATCH 6/6] [FIX] l10n_br_cte: fix cte comments compute --- l10n_br_cte/models/comment.py | 11 +++-------- l10n_br_cte/models/document.py | 1 + 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/l10n_br_cte/models/comment.py b/l10n_br_cte/models/comment.py index b84b7df44441..53fec7efafae 100644 --- a/l10n_br_cte/models/comment.py +++ b/l10n_br_cte/models/comment.py @@ -29,14 +29,9 @@ def _export_field(self, xsd_field, class_obj, member_spec, export_value=None): if xsd_field == "cte40_xCampo": return self.name[:20].strip() if xsd_field == "cte40_xTexto": - # Aparentemente isso terá que ser feito de outra forma - if ( - "active_id" in self.env.context - and self.env.context.get("active_model") - == "l10n_br_fiscal.document.line" - ): - active_id = self.env.context["active_id"] - doc = self.env["l10n_br_fiscal.document"].browse(active_id) + if "doc" in self.env.context: + doc_id = self.env.context["doc"] + doc = self.env["l10n_br_fiscal.document"].browse(doc_id) vals = {"user": self.env.user, "ctx": self._context, "doc": doc} message = self.compute_message(vals).strip() if self.comment_type == "fiscal": diff --git a/l10n_br_cte/models/document.py b/l10n_br_cte/models/document.py index c7bcb8f4d1a1..50467d4df875 100644 --- a/l10n_br_cte/models/document.py +++ b/l10n_br_cte/models/document.py @@ -1164,6 +1164,7 @@ def _export_field(self, xsd_field, class_obj, member_spec, export_value=None): if xsd_field == "cte40_tpAmb": self.env.context = dict(self.env.context) self.env.context.update({"tpAmb": self[xsd_field]}) + self.env.context.update({"doc": self.id}) # TODO: Força a remoção da tag infGlobalizado já que o # campo xObs está no l10n_br_fiscal.document