diff --git a/l10n_fr_intrastat_product/migrations/14.0.1.0.0/pre-migration.py b/l10n_fr_intrastat_product/migrations/14.0.1.0.0/pre-migration.py deleted file mode 100644 index cc47d4ffe..000000000 --- a/l10n_fr_intrastat_product/migrations/14.0.1.0.0/pre-migration.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright 2022 Akretion France (http://www.akretion.com/) -# @author: Alexis de Lattre -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - - -def migrate(cr, version): - if not version: - return - - cr.execute( - 'ALTER TABLE "l10n_fr_intrastat_product_declaration" RENAME "type" ' - 'TO "declaration_type"' - ) diff --git a/l10n_fr_intrastat_product/models/intrastat_product_declaration.py b/l10n_fr_intrastat_product/models/intrastat_product_declaration.py index 7fbdee86f..dc06b765d 100644 --- a/l10n_fr_intrastat_product/models/intrastat_product_declaration.py +++ b/l10n_fr_intrastat_product/models/intrastat_product_declaration.py @@ -6,7 +6,7 @@ from datetime import datetime from dateutil.relativedelta import relativedelta -from lxml import etree +from lxml import etree, objectify from odoo import _, api, fields, models from odoo.exceptions import UserError, ValidationError @@ -127,7 +127,13 @@ def _update_computation_line_vals(self, inv_line, line_vals, notedict): if invoice.intrastat_fiscal_position == "b2b": regime_code = 21 elif invoice.intrastat_fiscal_position == "b2c": - regime_code = 29 + # 29 is only for EMEBI (extended), + # not for the fiscal declaration (standard) + if self.reporting_level == "standard": + line_vals.clear() + return + else: + regime_code = 29 if regime_code: regime = self.env.ref( "l10n_fr_intrastat_product.fr_regime_%d" % regime_code @@ -157,71 +163,58 @@ def _generate_xml(self): my_company_currency = self.company_id.currency_id.name eu_countries = self.env.ref("base.europe").country_ids - root = etree.Element("INSTAT") - envelope = etree.SubElement(root, "Envelope") - envelope_id = etree.SubElement(envelope, "envelopeId") + root = objectify.Element("INSTAT") + envelope = objectify.SubElement(root, "Envelope") if not self.company_id.fr_intrastat_accreditation: - self.message_post( - body=_( - "No XML file generated because the Customs Accreditation " - "Identifier is not set on the accounting configuration " - "page of the company '%s'." + msg = ( + _( + "The Customs Accreditation " + "Identifier is not set for the company '%s'." ) % self.company_id.display_name ) - return - envelope_id.text = self.company_id.fr_intrastat_accreditation - create_date_time = etree.SubElement(envelope, "DateTime") - create_date = etree.SubElement(create_date_time, "date") + self._account_config_warning(msg) + envelope.envelopeId = self.company_id.fr_intrastat_accreditation + create_date_time = objectify.SubElement(envelope, "DateTime") now_user_tz = fields.Datetime.context_timestamp(self, datetime.now()) - create_date.text = datetime.strftime(now_user_tz, "%Y-%m-%d") - create_time = etree.SubElement(create_date_time, "time") - create_time.text = datetime.strftime(now_user_tz, "%H:%M:%S") - party = etree.SubElement(envelope, "Party", partyType="PSI", partyRole="PSI") - party_id = etree.SubElement(party, "partyId") - party_id.text = my_company_identifier - party_name = etree.SubElement(party, "partyName") - party_name.text = self.company_id.name - software_used = etree.SubElement(envelope, "softwareUsed") - software_used.text = "Odoo" - declaration = etree.SubElement(envelope, "Declaration") - declaration_id = etree.SubElement(declaration, "declarationId") - declaration_id.text = self.year_month.replace("-", "") - reference_period = etree.SubElement(declaration, "referencePeriod") - reference_period.text = self.year_month - psi_id = etree.SubElement(declaration, "PSIId") - psi_id.text = my_company_identifier - function = etree.SubElement(declaration, "Function") - function_code = etree.SubElement(function, "functionCode") - function_code.text = "O" - declaration_type_code = etree.SubElement(declaration, "declarationTypeCode") + create_date_time.date = datetime.strftime(now_user_tz, "%Y-%m-%d") + create_date_time.time = datetime.strftime(now_user_tz, "%H:%M:%S") + party = objectify.SubElement( + envelope, "Party", partyType="PSI", partyRole="PSI" + ) + party.partyId = my_company_identifier + party.partyName = self.company_id.name + envelope.softwareUsed = "Odoo" + declaration = objectify.SubElement(envelope, "Declaration") + declaration.declarationId = self.year_month.replace("-", "") + declaration.referencePeriod = self.year_month + declaration.PSIId = my_company_identifier + function = objectify.SubElement(declaration, "Function") + function.functionCode = "O" # O = Déclaration originelle level2letter = { "standard": "4", "extended": "5", # EMEBI 2022: stat + fisc, 2 in 1 combo } assert self.reporting_level in level2letter - declaration_type_code.text = level2letter[self.reporting_level] - flow_code = etree.SubElement(declaration, "flowCode") + declaration.declarationTypeCode = level2letter[self.reporting_level] type2letter = { "arrivals": "A", "dispatches": "D", } assert self.declaration_type in type2letter - flow_code.text = type2letter[self.declaration_type] - currency_code = etree.SubElement(declaration, "currencyCode") + declaration.flowCode = type2letter[self.declaration_type] assert my_company_currency == "EUR", "Company currency must be 'EUR'" - currency_code.text = my_company_currency + declaration.currencyCode = my_company_currency # THEN, the fields which vary from a line to the next if not self.declaration_line_ids: raise UserError( _("No declaration lines. You probably forgot to generate " "them !") ) - line = 0 for pline in self.declaration_line_ids: - line += 1 # increment line number - pline._generate_xml_line(declaration, eu_countries, line) + pline._generate_xml_line(declaration, eu_countries) + objectify.deannotate(root, xsi_nil=True, cleanup_namespaces=True) xml_bytes = etree.tostring( root, pretty_print=True, encoding="UTF-8", xml_declaration=True ) @@ -410,105 +403,111 @@ class IntrastatProductDeclarationLine(models.Model): ) # flake8: noqa: C901 - # TODO update error message to avoid quoting declaration line number - def _generate_xml_line(self, parent_node, eu_countries, line_number): + def _generate_xml_line(self, parent_node, eu_countries): self.ensure_one() decl = self.parent_id assert self.fr_regime_id, "Missing Intrastat Type" transaction = self.transaction_id regime = self.fr_regime_id - item = etree.SubElement(parent_node, "Item") - item_number = etree.SubElement(item, "itemNumber") - item_number.text = str(line_number) + item = objectify.SubElement(parent_node, "Item") + item.itemNumber = str(self.line_number) # START of elements which are only required in "detailed" level if decl.reporting_level == "extended" and not regime.is_fiscal_only: - cn8 = etree.SubElement(item, "CN8") - cn8_code = etree.SubElement(cn8, "CN8Code") + cn8 = objectify.SubElement(item, "CN8") if not self.hs_code_id: - raise UserError(_("Missing H.S. code on line %d.") % line_number) + raise UserError( + _("Missing H.S. code on declaration line %d.") % self.line_number + ) # local_code is required=True, so no need to check it - cn8_code.text = self.hs_code_id.local_code + cn8.CN8Code = self.hs_code_id.local_code # We fill SUCode only if the H.S. code requires it iunit_id = self.intrastat_unit_id if iunit_id: - su_code = etree.SubElement(cn8, "SUCode") - su_code.text = iunit_id.fr_xml_label or iunit_id.name + cn8.SUCode = iunit_id.fr_xml_label or iunit_id.name - src_dest_country = etree.SubElement(item, "MSConsDestCode") if not self.src_dest_country_code: raise UserError( - _("Missing Country Code of Origin/Destination on line %d.") - % line_number + _( + "Missing country code of origin/destination on declaration line %d." + ) + % self.line_number ) - src_dest_country.text = self.src_dest_country_code + item.MSConsDestCode = self.src_dest_country_code # EMEBI 2022 : origin country is now for arrival AND dispatches - country_origin = etree.SubElement(item, "countryOfOriginCode") if not self.product_origin_country_code: raise UserError( - _("Missing product country of origin code on line %d.") - % line_number + _("Missing product country of origin code on declaration line %d.") + % self.line_number ) - country_origin.text = self.product_origin_country_code + item.countryOfOriginCode = self.product_origin_country_code - weight = etree.SubElement(item, "netMass") + # no need for float_is_zero() because weight is an integer on decl lines if not self.weight: - raise UserError(_("Missing weight on line %d.") % line_number) - weight.text = str(self.weight) + raise UserError( + _("Missing weight on declaration line %d.") % self.line_number + ) + item.netMass = str(self.weight) if iunit_id: - quantity_in_SU = etree.SubElement(item, "quantityInSU") + # no need for float_is_zero() because suppl_unit_qty is an integer + # on declaration lines if not self.suppl_unit_qty: - raise UserError(_("Missing quantity on line %d.") % line_number) - quantity_in_SU.text = str(self.suppl_unit_qty) + raise UserError( + _("Missing quantity on declaration line %d.") % self.line_number + ) + item.quantityInSU = str(self.suppl_unit_qty) # START of elements that are part of all EMEBIs - invoiced_amount = etree.SubElement(item, "invoicedAmount") if not self.amount_company_currency: - raise UserError(_("Missing fiscal value on line %d.") % line_number) - invoiced_amount.text = str(self.amount_company_currency) + raise UserError( + _("Missing fiscal value on declaration line %d.") % self.line_number + ) + item.invoicedAmount = str(self.amount_company_currency) # EMEBI 2022 : Partner VAT now required for all dispatches with # some exceptions for regime 29 in case of B2C if decl.declaration_type == "dispatches": - partner_vat = etree.SubElement(item, "partnerId") if not self.vat and regime.code != "29": - raise UserError(_("Missing VAT number on line %d.") % line_number) + raise UserError( + _("Missing VAT number on declaration line %d.") % self.line_number + ) if self.vat and self.vat.startswith("GB") and decl.year >= "2021": raise UserError( _( - "Bad VAT number '%(vat)s' on line %(line_number)d. " + "Bad VAT number '%(vat)s' on declaration line %(line_number)d. " "Brexit took place on January 1st 2021 and companies " - "in Northern Ireland have a new VAT number starting with 'XI'." + "in Northern Ireland have a new VAT number starting with 'XI'.", + vat=self.vat, + line_number=self.line_number, ) - % {"vat": self.vat, "line_number": line_number} ) - partner_vat.text = self.vat and self.vat.replace(" ", "") or "" + item.partnerId = self.vat or "" # Code régime is on all EMEBIs - statistical_procedure_code = etree.SubElement(item, "statisticalProcedureCode") - statistical_procedure_code.text = regime.code + item.statisticalProcedureCode = regime.code # START of elements which are only required in "detailed" level if decl.reporting_level == "extended" and not regime.is_fiscal_only: - transaction_nature = etree.SubElement(item, "NatureOfTransaction") - transaction_nature_a = etree.SubElement( - transaction_nature, "natureOfTransactionACode" - ) - transaction_nature_a.text = transaction.code[0] - transaction_nature_b = etree.SubElement( - transaction_nature, "natureOfTransactionBCode" - ) - if len(transaction.code) != 2: + if not transaction: + raise UserError( + _("Missing intrastat transaction on declaration line %d.") + % self.line_number + ) + if len(transaction.code) != 2 or not transaction.code.isdigit(): raise UserError( - _("Transaction code on line %d should have 2 digits.") % line_number + _("Transaction code on declaration line %d should have 2 digits.") + % self.line_number ) - transaction_nature_b.text = transaction.code[1] - mode_of_transport_code = etree.SubElement(item, "modeOfTransportCode") + transaction_nature = objectify.SubElement(item, "NatureOfTransaction") + transaction_nature.natureOfTransactionACode = transaction.code[0] + transaction_nature.natureOfTransactionBCode = transaction.code[1] if not self.transport_id: raise UserError( - _("Mode of transport is not set on line %d.") % line_number + _("Missing mode of transport on declaration line %d.") + % self.line_number ) - mode_of_transport_code.text = str(self.transport_id.code) - region_code = etree.SubElement(item, "regionCode") + item.modeOfTransportCode = str(self.transport_id.code) if not self.region_code: - raise UserError(_("Region Code is not set on line %d.") % line_number) - region_code.text = self.region_code + raise UserError( + _("Missing region code on declaration line %d.") % self.line_number + ) + item.regionCode = self.region_code