From 766d0165e6f39ce88e49c06493ae2de9f51ae777 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Fri, 4 Nov 2022 18:04:09 +0100 Subject: [PATCH] [IMP] Big simplification, update and cleanup This commit aims at removing the over-complexity of intrastat modules while increasing simplicity/usability for users. - Move default intrastat transaction from res.company to account.fiscal.position to add su pport for B2C (and not just B2B) - improve usability: auto-generate declaration lines and XML export when going from draft to done. Auto-delete declaration lines and XML export when going from done to draft (and add confirmation pop-up). - declaration lines are now readonly. Only computation lines can be created/edited manuall y - add field region_code on computation lines and declaration lines. Remove region_id on declaration lines. This change allows big simplification in some localization modules such as l10n_fr_intrastat_product. - simplify Brexit implementation. Northern Ireland is important, but we can't afford to have so many lines of code and add a field on product.template (origin_state_id) for a territory of 1.9 million inhabitants! This is too costly to maintain and too complex for users. - improve default visibility of fields when reporting_level = 'standard' - add support for weight calculation from uom categories other than units and weight, supposing that the 'weight' field on product.template is the weight per uom of the product - add EU companies from several different countries in demo data with valid VAT numbers --- intrastat_base/demo/intrastat_demo.xml | 56 ++- intrastat_base/models/__init__.py | 1 - intrastat_base/models/res_partner.py | 67 --- intrastat_product/__manifest__.py | 1 + intrastat_product/models/__init__.py | 1 + .../models/account_fiscal_position.py | 28 ++ intrastat_product/models/account_move.py | 23 +- .../models/intrastat_product_declaration.py | 421 +++++++----------- .../models/intrastat_transaction.py | 1 + intrastat_product/models/res_company.py | 16 - .../models/res_config_settings.py | 12 - .../report/intrastat_product_report_xls.py | 21 +- intrastat_product/tests/common_purchase.py | 4 +- intrastat_product/tests/common_sale.py | 2 +- intrastat_product/tests/test_brexit.py | 34 +- .../views/account_fiscal_position.xml | 45 ++ intrastat_product/views/account_move.xml | 2 +- .../views/intrastat_product_declaration.xml | 286 ++++++++---- .../views/res_config_settings.xml | 31 -- .../models/product_template.py | 10 - .../views/product_template.xml | 4 - 21 files changed, 506 insertions(+), 560 deletions(-) delete mode 100644 intrastat_base/models/res_partner.py create mode 100644 intrastat_product/models/account_fiscal_position.py create mode 100644 intrastat_product/views/account_fiscal_position.xml diff --git a/intrastat_base/demo/intrastat_demo.xml b/intrastat_base/demo/intrastat_demo.xml index 6e6cd904c..f19b28539 100644 --- a/intrastat_base/demo/intrastat_demo.xml +++ b/intrastat_base/demo/intrastat_demo.xml @@ -1,6 +1,6 @@ @@ -8,9 +8,57 @@ FR58441019213 - - - BE0884025633 + + + Noviat + 1 + https://www.noviat.com + Avenue de Rusatiralaan 1 + Ganshoren + 1083 + + BE0820512013 + + + Acsone + 1 + https://www.acsone.eu + Drève Richelle, 167 + Waterloo + 1410 + + BE0835207216 + + + Tecnativa + 1 + https://www.tecnativa.com + Calle Tormos 1-A, 25 + Alicante + 03008 + + ESB87530432 + + + ForgeFlow + 1 + https://www.forgeflow.com + Rosselló 319, 6-1 + Barcelona + 08037 + + ESB66676008 + + + Akretion France + 1 + https://www.akretion.com + 27 rue Henri Rolland + Villeurbanne + 69100 + + FR86792377731 Shipping costs diff --git a/intrastat_base/models/__init__.py b/intrastat_base/models/__init__.py index cc10e08e5..cea19b9f8 100644 --- a/intrastat_base/models/__init__.py +++ b/intrastat_base/models/__init__.py @@ -3,4 +3,3 @@ from . import account_fiscal_position from . import account_fiscal_position_template from . import account_move -from . import res_partner diff --git a/intrastat_base/models/res_partner.py b/intrastat_base/models/res_partner.py deleted file mode 100644 index ed692b8ce..000000000 --- a/intrastat_base/models/res_partner.py +++ /dev/null @@ -1,67 +0,0 @@ -# Copyright 2022 Noviat. -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - -from odoo import _, api, models -from odoo.exceptions import UserError - -XI_COUNTY_NAMES = [ - "antrim", - "armagh", - "down", - "fermanagh", - "londonderry", - "tyrone", - "northern ireland", -] - -XI_COUNTIES = [ - "base.state_uk18", # County Antrim - "base.state_uk19", # County Armagh - "base.state_uk20", # County Down - "base.state_uk22", # County Fermanagh - "base.state_uk23", # County Londonderry - "base.state_uk24", # County Tyrone - "base.state_ie_27", # Antrim - "base.state_ie_28", # Armagh - "base.state_ie_29", # Down - "base.state_ie_30", # Fermanagh - "base.state_ie_31", # Londonderry - "base.state_ie_32", # Tyrone -] - - -class ResPartner(models.Model): - _inherit = "res.partner" - - @api.model - def _get_xi_counties(self): - return [self.env.ref(x) for x in XI_COUNTIES] - - @api.model - def _get_xu_counties(self): - uk_counties = self.env.ref("base.uk").state_ids - xu_counties = uk_counties.filtered(lambda r: r not in self._get_xi_counties()) - return xu_counties - - def _get_intrastat_country_code(self, country=None, state=None): - if self: - self.ensure_one() - country = self.country_id - state = self.state_id - else: - state = state or self.env["res.country.state"] - country = country or state.country_id - if not country: - raise UserError( - _("Programming Error when calling '_get_intrastat_country_code()") - ) - cc = country.code - if cc == "GB": - cc = "XU" - if state and cc in ["XU", "IE"]: - if ( - state in self._get_xi_counties() - or state.name.lower().strip() in XI_COUNTY_NAMES - ): - cc = "XI" - return cc diff --git a/intrastat_product/__manifest__.py b/intrastat_product/__manifest__.py index 5e0c5dda2..e2f8442e2 100644 --- a/intrastat_product/__manifest__.py +++ b/intrastat_product/__manifest__.py @@ -33,6 +33,7 @@ "views/intrastat_product_declaration.xml", "views/res_config_settings.xml", "views/res_partner_view.xml", + "views/account_fiscal_position.xml", "views/account_move.xml", "views/sale_order.xml", "views/stock_warehouse.xml", diff --git a/intrastat_product/models/__init__.py b/intrastat_product/models/__init__.py index b7c21ba58..f919d7688 100644 --- a/intrastat_product/models/__init__.py +++ b/intrastat_product/models/__init__.py @@ -10,3 +10,4 @@ from . import intrastat_unit from . import sale_order from . import stock_warehouse +from . import account_fiscal_position diff --git a/intrastat_product/models/account_fiscal_position.py b/intrastat_product/models/account_fiscal_position.py new file mode 100644 index 000000000..ad86b3f43 --- /dev/null +++ b/intrastat_product/models/account_fiscal_position.py @@ -0,0 +1,28 @@ +# Copyright 2022 Akretion France (http://www.akretion.com/) +# @author: +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class AccountFiscalPosition(models.Model): + _inherit = "account.fiscal.position" + + intrastat_out_invoice_transaction_id = fields.Many2one( + comodel_name="intrastat.transaction", + string="Default Intrastat Transaction For Customer Invoice", + ) + intrastat_out_refund_transaction_id = fields.Many2one( + comodel_name="intrastat.transaction", + string="Default Intrastat Transaction for Customer Refunds", + ) + intrastat_in_invoice_transaction_id = fields.Many2one( + comodel_name="intrastat.transaction", + string="Default Intrastat Transaction For Supplier Invoices", + ) + intrastat_in_refund_transaction_id = fields.Many2one( + comodel_name="intrastat.transaction", + string="Default Intrastat Transaction For Supplier Refunds", + ) + # field used to show/hide fields in country-specific modules + company_country_code = fields.Char(related="company_id.country_id.code") diff --git a/intrastat_product/models/account_move.py b/intrastat_product/models/account_move.py index 8dda931be..2dbad1321 100644 --- a/intrastat_product/models/account_move.py +++ b/intrastat_product/models/account_move.py @@ -93,14 +93,6 @@ def _get_intrastat_line_vals(self, line): if not hs_code: return vals weight, qty = decl_model._get_weight_and_supplunits(line, hs_code, notedict) - product_country = line.product_id.origin_country_id - product_state = line.product_id.origin_state_id - country = product_country or product_state.country_id - product_origin_country_code = "QU" - if country: - product_origin_country_code = self.env[ - "res.partner" - ]._get_intrastat_country_code(product_country, product_state) vals.update( { "invoice_line_id": line.id, @@ -108,7 +100,6 @@ def _get_intrastat_line_vals(self, line): "transaction_weight": weight, "transaction_suppl_unit_qty": qty, "product_origin_country_id": line.product_id.origin_country_id.id, - "product_origin_country_code": product_origin_country_code, } ) return vals @@ -208,22 +199,10 @@ class AccountMoveIntrastatLine(models.Model): transaction_weight = fields.Integer( help="Transaction weight in Kg: Quantity x Product Weight" ) - # product_origin_country_id is replaced by product_origin_country_code - # this field should be dropped once the localisation modules have been - # adapted accordingly product_origin_country_id = fields.Many2one( comodel_name="res.country", - string="Country of Origin", - help="Country of origin of the product i.e. product " "'made in ____'.", - ) - product_origin_country_code = fields.Char( string="Country of Origin of the Product", - size=2, - required=True, - default="QU", - help="2 digit code of country of origin of the product except for the UK.\n" - "Specify 'XI' for UK Northern Ireland and 'XU' for rest of the UK.\n" - "Specify 'QU' when the country is unknown.\n", + help="Country of origin of the product i.e. product " "'made in ____'.", ) @api.onchange("invoice_line_id") diff --git a/intrastat_product/models/intrastat_product_declaration.py b/intrastat_product/models/intrastat_product_declaration.py index 130c27430..9bcbeca61 100644 --- a/intrastat_product/models/intrastat_product_declaration.py +++ b/intrastat_product/models/intrastat_product_declaration.py @@ -4,7 +4,6 @@ # @author Luc de Meyer import logging -import warnings from datetime import date from dateutil.relativedelta import relativedelta @@ -50,9 +49,8 @@ def default_get(self, fields_list): default=lambda self: self.env.company, ) company_country_code = fields.Char( - compute="_compute_company_country_code", + related="company_id.country_id.code", string="Company Country Code", - readonly=True, store=True, ) state = fields.Selection( @@ -129,7 +127,7 @@ def default_get(self, fields_list): comodel_name="intrastat.product.declaration.line", inverse_name="parent_id", string="Intrastat Product Declaration Lines", - states={"done": [("readonly", True)]}, + readonly=True, ) num_decl_lines = fields.Integer( compute="_compute_numbers", @@ -148,10 +146,11 @@ def default_get(self, fields_list): ) reporting_level = fields.Selection( selection="_get_reporting_level", + compute="_compute_reporting_level", + readonly=False, string="Reporting Level", states={"done": [("readonly", True)]}, ) - valid = fields.Boolean(compute="_compute_check_validity", string="Valid") xml_attachment_id = fields.Many2one("ir.attachment", string="XML Attachment") xml_attachment_datas = fields.Binary( related="xml_attachment_id.datas", string="XML Export" @@ -184,25 +183,20 @@ def _get_action(self): ("nihil", _("Nihil")), ] - @api.depends("company_id") - def _compute_company_country_code(self): - for this in self: - if this.company_id: - if not this.company_id.country_id: - raise ValidationError(_("You must set company's country !")) - this.company_country_code = this.company_id.country_id.code.lower() - @api.depends("year", "month") def _compute_year_month(self): for this in self: if this.year and this.month: this.year_month = "-".join([this.year, this.month]) - @api.depends("month") - def _compute_check_validity(self): - """TO DO: logic based upon computation lines""" + @api.constrains("company_id") + def _check_company_country(self): for this in self: - this.valid = True + if not this.company_id.country_id: + raise ValidationError( + _("You must set the country on company '%s'.") + % this.company_id.display_name + ) @api.constrains("year") def _check_year(self): @@ -210,20 +204,23 @@ def _check_year(self): if len(this.year) != 4 or this.year[0] != "2": raise ValidationError(_("Invalid Year!")) - @api.onchange("declaration_type") - def _onchange_declaration_type(self): - if self.declaration_type == "arrivals": - self.reporting_level = ( - self.company_id.intrastat_arrivals == "extended" - and "extended" - or "standard" - ) - if self.declaration_type == "dispatches": - self.reporting_level = ( - self.company_id.intrastat_dispatches == "extended" - and "extended" - or "standard" - ) + @api.depends("declaration_type", "company_id") + def _compute_reporting_level(self): + for this in self: + reporting_level = False + if this.declaration_type == "arrivals": + reporting_level = ( + this.company_id.intrastat_arrivals == "extended" + and "extended" + or "standard" + ) + elif this.declaration_type == "dispatches": + reporting_level = ( + this.company_id.intrastat_dispatches == "extended" + and "extended" + or "standard" + ) + this.reporting_level = reporting_level def copy(self, default=None): self.ensure_one() @@ -252,12 +249,6 @@ def _attach_xml_file(self, xml_bytes, declaration_name): ) return attach.id - def _unlink_attachments(self): - atts = self.env["ir.attachment"].search( - [("res_model", "=", self._name), ("res_id", "=", self.id)] - ) - atts.unlink() - def unlink(self): for this in self: if this.state == "done": @@ -269,7 +260,7 @@ def unlink(self): def _get_partner_country(self, inv_line, notedict, eu_countries): inv = inv_line.move_id - country = inv.src_dest_country_id or inv.partner_id.country_id + country = inv.src_dest_country_id if not country: line_notes = [ _( @@ -294,70 +285,42 @@ def _get_partner_country(self, inv_line, notedict, eu_countries): % (inv.name, country.name) ] self._format_line_note(inv_line, notedict, line_notes) - if country and country.code == "GB" and self.year >= "2021": - vat = inv.commercial_partner_id.vat - if not vat: - line_notes = [ - _( - "On invoice '%s', the source/destination country " - "is United-Kingdom and the fiscal position is '%s'. " - "Make sure that the fiscal position is right. If " - "the origin/destination is Northern Ireland, please " - "set the VAT number of the partner '%s' in Odoo with " - "its new VAT number starting with 'XI' following Brexit." - ) - % ( - inv.name, - inv.fiscal_position_id.display_name, - inv.commercial_partner_id.display_name, - ) - ] - self._format_line_note(inv_line, notedict, line_notes) - elif not vat.startswith("XI"): - line_notes = [ - _( - "On invoice '%s', the source/destination country " - "is United-Kingdom, the fiscal position is '%s' and " - "the partner's VAT number is '%s'. " - "Make sure that the fiscal position is right. If " - "the origin/destination is Northern Ireland, please " - "update the VAT number of the partner '%s' in Odoo with " - "its new VAT number starting with 'XI' following Brexit." - ) - % ( - inv.name, - inv.fiscal_position_id.display_name, - vat, - inv.commercial_partner_id.display_name, - ) - ] - self._format_line_note(inv_line, notedict, line_notes) return country def _get_intrastat_transaction(self, inv_line, notedict): invoice = inv_line.move_id - if invoice.intrastat_transaction_id: - return invoice.intrastat_transaction_id - else: - company = invoice.company_id + transaction = invoice.intrastat_transaction_id + if not transaction: + # as we have searched with intrastat_fiscal_position = True + # we should always have a fiscal position on the invoice + fp = invoice.fiscal_position_id if invoice.move_type == "out_invoice": - return company.intrastat_transaction_out_invoice + transaction = fp.intrastat_out_invoice_transaction_id elif invoice.move_type == "out_refund": - return company.intrastat_transaction_out_refund + transaction = fp.intrastat_out_refund_transaction_id elif invoice.move_type == "in_invoice": - return company.intrastat_transaction_in_invoice + transaction = fp.intrastat_in_invoice_transaction_id elif invoice.move_type == "in_refund": - return company.intrastat_transaction_in_refund + transaction = fp.intrastat_in_refund_transaction_id + if not transaction: + line_notes = [ + _( + "No Intrastat Transaction Type on invoice '%s', " + "nor on the fiscal position of the invoice (%s)." + ) + % (invoice.name, invoice.fiscal_position_id.display_name) + ] + self._format_line_note(inv_line, notedict, line_notes) + return transaction def _get_weight_and_supplunits(self, inv_line, hs_code, notedict): line_qty = inv_line.quantity product = inv_line.product_id intrastat_unit_id = hs_code.intrastat_unit_id source_uom = inv_line.product_uom_id - weight_uom_categ = self._get_uom_refs("weight_uom_categ") - kg_uom = self._get_uom_refs("kg_uom") - pce_uom_categ = self._get_uom_refs("pce_uom_categ") - pce_uom = self._get_uom_refs("pce_uom") + weight_uom_categ = self.env.ref("uom.product_uom_categ_kgm") + kg_uom = self.env.ref("uom.product_uom_kgm") + self.env["decimal.precision"].precision_get("Stock Weight") weight = suppl_unit_qty = 0.0 if not source_uom: @@ -391,40 +354,35 @@ def _get_weight_and_supplunits(self, inv_line, hs_code, notedict): self._format_line_note(inv_line, notedict, line_notes) return weight, suppl_unit_qty - if weight: - return weight, suppl_unit_qty - if source_uom == kg_uom: weight = line_qty elif source_uom.category_id == weight_uom_categ: weight = source_uom._compute_quantity(line_qty, kg_uom) - elif source_uom.category_id == pce_uom_categ: - if not product.weight: # re-create weight_net ? - line_notes = [_("Missing weight on product %s.") % product.display_name] - self._format_line_note(inv_line, notedict, line_notes) - return weight, suppl_unit_qty - if source_uom == pce_uom: - weight = product.weight * line_qty # product.weight_net - else: - # Here, I suppose that, on the product, the - # weight is per PCE and not per uom_id - # product.weight_net - weight = product.weight * source_uom._compute_quantity( - line_qty, pce_uom - ) + elif source_uom.category_id == product.uom_id.category_id: + # We suppose that, on product.template, + # the 'weight' field is per uom_id + weight = product.weight * source_uom._compute_quantity( + line_qty, product.uom_id + ) else: line_notes = [ _( "Conversion from unit of measure '%s' to 'Kg' " - "is not implemented yet. It is needed for product '%s'." + "cannot be done automatically. It is needed for product " + "'%s' whose unit of measure is %s." ) - % (source_uom.name, product.display_name) + % (source_uom.name, product.display_name, product.uom_id.display_name) ] self._format_line_note(inv_line, notedict, line_notes) - return weight, suppl_unit_qty - return weight, suppl_unit_qty + def _get_region_code(self, inv_line, notedict): + """May be inherited by localisation modules + If set, Odoo will use the region code returned by this method + and will not call _get_region() and leave region_id empty + """ + return False + def _get_region(self, inv_line, notedict): """ For supplier invoices/refunds: if the invoice line is linked @@ -491,35 +449,22 @@ def _get_incoterm(self, inv_line, notedict): return incoterm def _get_product_origin_country(self, inv_line, notedict): - warnings.warn( - "Method '_get_product_origin_country' is deprecated, " - "please use '_get_product_origin_country_code'.", - DeprecationWarning, - ) - return inv_line.product_id.origin_country_id - - def _get_product_origin_country_code( - self, inv_line, product_origin_country, notedict - ): - cc = "QU" - if product_origin_country.code: - cc = product_origin_country.code - year = self.year or str(inv_line.move_id.date.year) - if year >= "2021": - product_origin_state = getattr( - inv_line.product_id, - "origin_state_id", - self.env["res.country.state"], - ) - cc = self.env["res.partner"]._get_intrastat_country_code( - product_origin_country, product_origin_state - ) - return cc + origin_country = inv_line.product_id.origin_country_id + if not origin_country: + line_notes = [ + _("The country of origin is missing on product '%s'.") + % inv_line.product_id.display_name + ] + self._format_line_note(inv_line, notedict, line_notes) + return origin_country def _get_vat(self, inv_line, notedict): vat = False inv = inv_line.move_id - if self.declaration_type == "dispatches": + if ( + self.declaration_type == "dispatches" + and inv_line.move_id.fiscal_position_id.vat_required + ): vat = inv.commercial_partner_id.vat if vat: if vat.startswith("GB"): @@ -608,12 +553,11 @@ def _prepare_invoice_domain(self): ("state", "=", "posted"), ("intrastat_fiscal_position", "=", True), ("company_id", "=", self.company_id.id), - ( - "move_type", - "in", - ("out_invoice", "out_refund", "in_invoice", "in_refund"), - ), ] + if self.declaration_type == "arrivals": + domain.append(("move_type", "in", ("in_invoice", "in_refund"))) + elif self.declaration_type == "dispatches": + domain.append(("move_type", "in", ("out_invoice", "out_refund"))) return domain def _is_product(self, invoice_line): @@ -638,7 +582,6 @@ def _format_line_note(self, line, notedict, line_notes): notedict["note"] += note def _gather_invoices(self, notedict): - lines = [] qty_prec = self.env["decimal.precision"].precision_get( "Product Unit of Measure" @@ -693,10 +636,13 @@ def _gather_invoices(self, notedict): ) # When the country is the same as the company's country must be skipped. if partner_country == self.company_id.country_id: + _logger.info( + "Skipping invoice line %s qty %s " + "of invoice %s. Reason: partner_country = " + "company country" + % (inv_line.name, inv_line.quantity, invoice.name) + ) continue - partner_country_code = ( - invoice.commercial_partner_id._get_intrastat_country_code() - ) if inv_intrastat_line: hs_code = inv_intrastat_line.hs_code_id @@ -738,16 +684,15 @@ def _gather_invoices(self, notedict): product_origin_country = ( inv_intrastat_line.product_origin_country_id ) - product_origin_country_code = ( - inv_intrastat_line.product_origin_country_code - ) else: - product_origin_country = inv_line.product_id.origin_country_id - product_origin_country_code = self._get_product_origin_country_code( - inv_line, product_origin_country, notedict + product_origin_country = self._get_product_origin_country( + inv_line, notedict ) - region = self._get_region(inv_line, notedict) + region_code = self._get_region_code(inv_line, notedict) + region = False + if not region_code: + region = self._get_region(inv_line, notedict) vat = self._get_vat(inv_line, notedict) @@ -755,7 +700,6 @@ def _gather_invoices(self, notedict): "parent_id": self.id, "invoice_line_id": inv_line.id, "src_dest_country_id": partner_country.id, - "src_dest_country_code": partner_country_code, "product_id": inv_line.product_id.id, "hs_code_id": hs_code.id, "weight": weight, @@ -764,7 +708,7 @@ def _gather_invoices(self, notedict): "amount_accessory_cost_company_currency": 0.0, "transaction_id": intrastat_transaction.id, "product_origin_country_id": product_origin_country.id or False, - "product_origin_country_code": product_origin_country_code, + "region_code": region_code, "region_id": region and region.id or False, "vat": vat, } @@ -798,23 +742,16 @@ def _gather_invoices(self, notedict): _logger.info( "Skipping invoice line %s qty %s " "of invoice %s. Reason: price_subtotal = 0 " - "and accessory costs = 0" - % (inv_line.name, inv_line.quantity, inv_line.move_id.name) + "and accessory costs = 0", + inv_line.name, + inv_line.quantity, + inv_line.move_id.name, ) continue lines.append(line_vals) return lines - def _get_uom_refs(self, ref): - uom_refs = { - "weight_uom_categ": self.env.ref("uom.product_uom_categ_kgm"), - "kg_uom": self.env.ref("uom.product_uom_kgm"), - "pce_uom_categ": self.env.ref("uom.product_uom_categ_unit"), - "pce_uom": self.env.ref("uom.product_uom_unit"), - } - return uom_refs[ref] - def action_gather(self): self.ensure_one() self.message_post(body=_("Generate Lines from Invoices")) @@ -857,8 +794,6 @@ def action_gather(self): "type": "ir.actions.act_window", } - return True - @api.model def _group_line_hashcode_fields(self, computation_line): return { @@ -867,7 +802,7 @@ def _group_line_hashcode_fields(self, computation_line): "intrastat_unit": computation_line.intrastat_unit_id.id or False, "transaction": computation_line.transaction_id.id or False, "transport": computation_line.transport_id.id or False, - "region": computation_line.region_id.id or False, + "region": computation_line.region_code or False, "product_origin_country": computation_line.product_origin_country_code, "vat": computation_line.vat or False, } @@ -880,15 +815,13 @@ def group_line_hashcode(self, computation_line): @api.model def _prepare_grouped_fields(self, computation_line, fields_to_sum): vals = { - "src_dest_country_id": computation_line.src_dest_country_id.id, "src_dest_country_code": computation_line.src_dest_country_code, "intrastat_unit_id": computation_line.intrastat_unit_id.id, "hs_code_id": computation_line.hs_code_id.id, "transaction_id": computation_line.transaction_id.id, "transport_id": computation_line.transport_id.id, - "region_id": computation_line.region_id.id, + "region_code": computation_line.region_code, "parent_id": computation_line.parent_id.id, - "product_origin_country_id": computation_line.product_origin_country_id.id, "product_origin_country_code": computation_line.product_origin_country_code, "amount_company_currency": 0.0, "vat": computation_line.vat, @@ -921,27 +854,21 @@ def _prepare_declaration_line(self, computation_lines): return vals def generate_declaration(self): - """generate declaration lines""" + """generate declaration lines from computation lines""" self.ensure_one() - assert self.valid, "Computation lines are not valid" - self.message_post(body=_("Generate Declaration Lines")) - # Delete existing declaration lines - self.declaration_line_ids.unlink() - # Regenerate declaration lines from computation lines + assert not self.declaration_line_ids dl_group = {} for cl in self.computation_line_ids: hashcode = self.group_line_hashcode(cl) - if hashcode in dl_group: - dl_group[hashcode].append(cl) + if hashcode not in dl_group: + dl_group[hashcode] = cl else: - dl_group[hashcode] = [cl] + dl_group[hashcode] |= cl ipdl = self.declaration_line_ids - for cl_lines in list(dl_group.values()): + for cl_lines in dl_group.values(): vals = self._prepare_declaration_line(cl_lines) declaration_line = ipdl.create(vals) - for cl in cl_lines: - cl.write({"declaration_line_id": declaration_line.id}) - return True + cl_lines.write({"declaration_line_id": declaration_line.id}) def _check_generate_xml(self): self.ensure_one() @@ -964,7 +891,6 @@ def generate_xml(self): ) self.message_post(body=_("Generate XML Declaration File")) self._check_generate_xml() - self._unlink_attachments() xml_bytes = self._generate_xml() if xml_bytes: attach_id = self._attach_xml_file( @@ -999,9 +925,9 @@ def _xls_computation_line_fields(self): """ return [ "product", - "product_origin_country", "hs_code", "src_dest_country", + "src_dest_country_code", "amount_company_currency", "accessory_cost", "transaction", @@ -1009,6 +935,10 @@ def _xls_computation_line_fields(self): "suppl_unit_qty", "suppl_unit", "transport", + "region", + "region_code", + "product_origin_country", + "product_origin_country_code", "vat", "partner_id", "invoice", @@ -1018,13 +948,11 @@ def _xls_computation_line_fields(self): def _xls_declaration_line_fields(self): """ Update list in custom module to add/drop columns or change order + Use same order as tree view by default """ return [ "hs_code", - "product_origin_country_code", - "product_origin_country", "src_dest_country_code", - "src_dest_country", "amount_company_currency", "transaction_code", "transaction", @@ -1033,6 +961,8 @@ def _xls_declaration_line_fields(self): "suppl_unit", "transport_code", "transport", + "region_code", + "product_origin_country_code", "vat", ] @@ -1045,14 +975,15 @@ def _xls_template(self): return {} def done(self): + for decl in self: + decl.generate_declaration() + decl.generate_xml() self.write({"state": "done"}) def back2draft(self): for decl in self: - if decl.xml_attachment_id: - raise UserError( - _("Before going back to draft, you must delete the XML export.") - ) + decl.delete_xml() + decl.declaration_line_ids.unlink() self.write({"state": "draft"}) @@ -1070,9 +1001,11 @@ class IntrastatProductComputationLine(models.Model): company_currency_id = fields.Many2one( related="company_id.currency_id", string="Company currency" ) + company_country_code = fields.Char( + related="parent_id.company_id.country_id.code", string="Company Country Code" + ) declaration_type = fields.Selection(related="parent_id.declaration_type") reporting_level = fields.Selection(related="parent_id.reporting_level") - valid = fields.Boolean(compute="_compute_check_validity", string="Valid") invoice_line_id = fields.Many2one( "account.move.line", string="Invoice Line", readonly=True ) @@ -1091,6 +1024,7 @@ class IntrastatProductComputationLine(models.Model): help="Country of Origin/Destination", ) src_dest_country_code = fields.Char( + compute="_compute_src_dest_country_code", string="Country Code", required=True, readonly=False, @@ -1131,25 +1065,26 @@ class IntrastatProductComputationLine(models.Model): "at the pro-rata of the amount of each invoice line.", ) transaction_id = fields.Many2one( - "intrastat.transaction", string="Intrastat Transaction" + "intrastat.transaction", + string="Intrastat Transaction", ) region_id = fields.Many2one("intrastat.region", string="Intrastat Region") - # product_origin_country_id is replaced by product_origin_country_code - # this field should be dropped once the localisation modules have been - # adapted accordingly + # Note that, in l10n_fr_intrastat_product and maybe in other localization modules + # region_id is left empty and Odoo writes directly in region_code + region_code = fields.Char() product_origin_country_id = fields.Many2one( "res.country", string="Country of Origin of the Product", help="Country of origin of the product i.e. product 'made in ____'", ) product_origin_country_code = fields.Char( - string="Country of Origin of the Product", + compute="_compute_product_origin_country_code", + string="Country Code of Origin of the Product", size=2, required=True, - default="QU", - help="2 digit code of country of origin of the product except for the UK.\n" - "Specify 'XI' for UK Northern Ireland and 'XU' for rest of the UK.\n" - "Specify 'QU' when the country is unknown.\n", + readonly=False, + help="2 digit ISO code of the country of origin of the product.\n" + "Specify 'QU' when the country of origin is unknown.\n", ) vat = fields.Char(string="VAT Number") @@ -1157,19 +1092,32 @@ class IntrastatProductComputationLine(models.Model): incoterm_id = fields.Many2one("account.incoterms", string="Incoterm") transport_id = fields.Many2one("intrastat.transport_mode", string="Transport Mode") - @api.onchange("src_dest_country_id") - def _onchange_src_dest_country_id(self): - self.src_dest_country_code = self.src_dest_country_id.code - if self.parent_id.year >= "2021": - self.src_dest_country_code = self.env[ - "res.partner" - ]._get_intrastat_country_code(country=self.src_dest_country_id) - - @api.depends("transport_id") - def _compute_check_validity(self): - """TO DO: logic based upon fields""" + @api.onchange("region_id") + def _region_id_change(self): + if self.region_id: + self.region_code = self.region_id.code + + @api.depends("src_dest_country_id") + def _compute_src_dest_country_code(self): for this in self: - this.valid = True + code = this.src_dest_country_id and this.src_dest_country_id.code or False + if code == "GB": + code = "XI" # Northern Ireland + this.src_dest_country_code = code + + @api.depends("product_origin_country_id") + def _compute_product_origin_country_code(self): + for this in self: + code = ( + this.product_origin_country_id + and this.product_origin_country_id.code + or False + ) + if code == "GB": + code = "XU" + # XU can be used when you don't know if the product + # originate from Great-Britain or from Northern Ireland + this.product_origin_country_code = code @api.constrains("vat") def _check_vat(self): @@ -1177,21 +1125,6 @@ def _check_vat(self): if this.vat and not is_valid(this.vat): raise ValidationError(_("The VAT number '%s' is invalid.") % this.vat) - # TODO: product_id is a readonly related field 'invoice_line_id.product_id' - # so the onchange is non-sense. Either we convert product_id to a regular - # field or we keep it a related field and we remove this onchange - @api.onchange("product_id") - def _onchange_product(self): - self.weight = 0.0 - self.suppl_unit_qty = 0.0 - self.intrastat_code_id = False - self.intrastat_unit_id = False - if self.product_id: - self.intrastat_code_id = self.product_id.intrastat_id - self.intrastat_unit_id = self.product_id.intrastat_id.intrastat_unit_id - if not self.intrastat_unit_id: - self.weight = self.product_id.weight - class IntrastatProductDeclarationLine(models.Model): _name = "intrastat.product.declaration.line" @@ -1207,6 +1140,9 @@ class IntrastatProductDeclarationLine(models.Model): company_currency_id = fields.Many2one( related="company_id.currency_id", string="Company currency" ) + company_country_code = fields.Char( + related="parent_id.company_id.country_id.code", string="Company Country Code" + ) declaration_type = fields.Selection(related="parent_id.declaration_type") reporting_level = fields.Selection(related="parent_id.reporting_level") computation_line_ids = fields.One2many( @@ -1215,11 +1151,6 @@ class IntrastatProductDeclarationLine(models.Model): string="Computation Lines", readonly=True, ) - src_dest_country_id = fields.Many2one( - "res.country", - string="Country", - help="Country of Origin/Destination", - ) src_dest_country_code = fields.Char( string="Country Code", required=True, @@ -1246,15 +1177,7 @@ class IntrastatProductDeclarationLine(models.Model): transaction_id = fields.Many2one( "intrastat.transaction", string="Intrastat Transaction" ) - region_id = fields.Many2one("intrastat.region", string="Intrastat Region") - # product_origin_country_id is replaced by product_origin_country_code - # this field should be dropped once the localisation modules have been - # adapted accordingly - product_origin_country_id = fields.Many2one( - "res.country", - string="Country of Origin of the Product", - help="Country of origin of the product i.e. product 'made in ____'", - ) + region_code = fields.Char() product_origin_country_code = fields.Char( string="Country of Origin of the Product", size=2, @@ -1268,17 +1191,3 @@ class IntrastatProductDeclarationLine(models.Model): # extended declaration incoterm_id = fields.Many2one("account.incoterms", string="Incoterm") transport_id = fields.Many2one("intrastat.transport_mode", string="Transport Mode") - - @api.onchange("src_dest_country_id") - def _onchange_src_dest_country_id(self): - self.src_dest_country_code = self.src_dest_country_id.code - if self.parent_id.year >= "2021": - self.src_dest_country_code = self.env[ - "res.partner" - ]._get_intrastat_country_code(country=self.src_dest_country_id) - - @api.constrains("vat") - def _check_vat(self): - for this in self: - if this.vat and not is_valid(this.vat): - raise ValidationError(_("The VAT number '%s' is invalid.") % this.vat) diff --git a/intrastat_product/models/intrastat_transaction.py b/intrastat_product/models/intrastat_transaction.py index b542e8732..6b75f9617 100644 --- a/intrastat_product/models/intrastat_transaction.py +++ b/intrastat_product/models/intrastat_transaction.py @@ -9,6 +9,7 @@ class IntrastatTransaction(models.Model): _name = "intrastat.transaction" _description = "Intrastat Transaction" + _rec_name = "code" _order = "code" _sql_constraints = [ ( diff --git a/intrastat_product/models/res_company.py b/intrastat_product/models/res_company.py index 14e3fa7e2..78e7f8a42 100644 --- a/intrastat_product/models/res_company.py +++ b/intrastat_product/models/res_company.py @@ -35,22 +35,6 @@ class ResCompany(models.Model): intrastat_region_id = fields.Many2one( comodel_name="intrastat.region", string="Default Intrastat Region" ) - intrastat_transaction_out_invoice = fields.Many2one( - comodel_name="intrastat.transaction", - string="Default Intrastat Transaction For Customer Invoice", - ) - intrastat_transaction_out_refund = fields.Many2one( - comodel_name="intrastat.transaction", - string="Default Intrastat Transaction for Customer Refunds", - ) - intrastat_transaction_in_invoice = fields.Many2one( - comodel_name="intrastat.transaction", - string="Default Intrastat Transaction For Supplier Invoices", - ) - intrastat_transaction_in_refund = fields.Many2one( - comodel_name="intrastat.transaction", - string="Default Intrastat Transaction For Supplier Refunds", - ) intrastat_accessory_costs = fields.Boolean( string="Include Accessory Costs in Fiscal Value of Product" ) diff --git a/intrastat_product/models/res_config_settings.py b/intrastat_product/models/res_config_settings.py index 6b162293e..f48d6a7aa 100644 --- a/intrastat_product/models/res_config_settings.py +++ b/intrastat_product/models/res_config_settings.py @@ -21,18 +21,6 @@ class ResConfigSettings(models.TransientModel): intrastat_region_id = fields.Many2one( related="company_id.intrastat_region_id", readonly=False ) - intrastat_transaction_out_invoice = fields.Many2one( - related="company_id.intrastat_transaction_out_invoice", readonly=False - ) - intrastat_transaction_out_refund = fields.Many2one( - related="company_id.intrastat_transaction_out_refund", readonly=False - ) - intrastat_transaction_in_invoice = fields.Many2one( - related="company_id.intrastat_transaction_in_invoice", readonly=False - ) - intrastat_transaction_in_refund = fields.Many2one( - related="company_id.intrastat_transaction_in_refund", readonly=False - ) intrastat_accessory_costs = fields.Boolean( related="company_id.intrastat_accessory_costs", readonly=False ) diff --git a/intrastat_product/report/intrastat_product_report_xls.py b/intrastat_product/report/intrastat_product_report_xls.py index a666d25e8..2e6199bfb 100644 --- a/intrastat_product/report/intrastat_product_report_xls.py +++ b/intrastat_product/report/intrastat_product_report_xls.py @@ -45,9 +45,9 @@ def _get_template(self, declaration): "header": {"type": "string", "value": self._("Product C/O Code")}, "line": { "type": "string", - "value": self._render("line.product_origin_country_id.code or ''"), + "value": self._render("line.product_origin_country_code or ''"), }, - "width": 28, + "width": 10, }, "product_origin_country": { "header": {"type": "string", "value": self._("Product C/O")}, @@ -68,13 +68,13 @@ def _get_template(self, declaration): "src_dest_country_code": { "header": { "type": "string", - "value": self._("Country of Origin/Destination"), + "value": self._("Country Code of Origin/Destination"), }, "line": { "type": "string", - "value": self._render("line.src_dest_country_id.code"), + "value": self._render("line.src_dest_country_code"), }, - "width": 28, + "width": 10, }, "src_dest_country": { "header": { @@ -83,7 +83,7 @@ def _get_template(self, declaration): }, "line": { "type": "string", - "value": self._render("line.src_dest_country_code"), + "value": self._render("line.src_dest_country_id.name"), }, "width": 28, }, @@ -121,7 +121,7 @@ def _get_template(self, declaration): "value": self._("Intrastat Transaction code"), }, "line": {"value": self._render("line.transaction_id.code")}, - "width": 36, + "width": 10, }, "transaction": { "header": {"type": "string", "value": self._("Intrastat Transaction")}, @@ -169,7 +169,7 @@ def _get_template(self, declaration): "transport_code": { "header": {"type": "string", "value": self._("Transport Mode Code")}, "line": {"value": self._render("line.transport_id.code or ''")}, - "width": 14, + "width": 10, }, "transport": { "header": {"type": "string", "value": self._("Transport Mode")}, @@ -181,6 +181,11 @@ def _get_template(self, declaration): "line": {"value": self._render("line.region_id.name or ''")}, "width": 28, }, + "region_code": { + "header": {"type": "string", "value": self._("Intrastat Region Code")}, + "line": {"value": self._render("line.region_code or ''")}, + "width": 10, + }, "vat": { "header": {"type": "string", "value": self._("VAT")}, "line": {"value": self._render("line.vat or ''")}, diff --git a/intrastat_product/tests/common_purchase.py b/intrastat_product/tests/common_purchase.py index 757445744..2133e949b 100644 --- a/intrastat_product/tests/common_purchase.py +++ b/intrastat_product/tests/common_purchase.py @@ -16,9 +16,9 @@ def _get_expected_vals(self, line): "declaration_type": "arrivals", "suppl_unit_qty": line.qty_received, "hs_code_id": line.product_id.hs_code_id, - "product_origin_country_id": line.product_id.origin_country_id, + "product_origin_country_code": line.product_id.origin_country_id.code, "amount_company_currency": line.price_subtotal, - "src_dest_country_id": line.partner_id.country_id, + "src_dest_country_code": line.partner_id.country_id.code, } def _check_line_values(self, final=False, declaration=None, purchase=None): diff --git a/intrastat_product/tests/common_sale.py b/intrastat_product/tests/common_sale.py index 4a3a4575e..99d9c7b79 100644 --- a/intrastat_product/tests/common_sale.py +++ b/intrastat_product/tests/common_sale.py @@ -16,7 +16,7 @@ def _get_expected_vals(self, line): "declaration_type": "dispatches", "suppl_unit_qty": line.qty_delivered, "hs_code_id": line.product_id.hs_code_id, - "product_origin_country_id": line.product_id.origin_country_id, + "product_origin_country_code": line.product_id.origin_country_id.code, } def _check_line_values(self, final=False, declaration=None, sale=None): diff --git a/intrastat_product/tests/test_brexit.py b/intrastat_product/tests/test_brexit.py index 5001c5ace..c4c8f9e3b 100644 --- a/intrastat_product/tests/test_brexit.py +++ b/intrastat_product/tests/test_brexit.py @@ -17,25 +17,13 @@ def setUpClass(cls): "local_code": "22083000", } ) - cls.product_xi = cls.env["product.product"].create( + cls.product_uk = cls.env["product.product"].create( { "name": "Bushmills Original", "weight": 1.4, "list_price": 30.0, "standard_price": 15.0, "origin_country_id": cls.env.ref("base.uk").id, - "origin_state_id": cls.env.ref("base.state_uk18").id, - "hs_code_id": cls.hs_code_whiskey.id, - } - ) - cls.product_xu = cls.env["product.product"].create( - { - "name": "Glenfiddich", - "weight": 1.4, - "list_price": 50.0, - "standard_price": 25.0, - "origin_country_id": cls.env.ref("base.uk").id, - "origin_state_id": cls.env.ref("base.state_uk6").id, "hs_code_id": cls.hs_code_whiskey.id, } ) @@ -84,9 +72,7 @@ def test_brexit_purchase(self): ) with Form(inv_in_xi) as inv_form: with inv_form.invoice_line_ids.new() as ail: - ail.product_id = self.product_xi - with inv_form.invoice_line_ids.new() as ail: - ail.product_id = self.product_xu + ail.product_id = self.product_uk inv_in_xi.invoice_date = inv_in_xi.date inv_in_xi.action_post() @@ -100,15 +86,11 @@ def test_brexit_purchase(self): self.declaration.action_gather() self.declaration.generate_declaration() clines = self.declaration.computation_line_ids - cl_xi = clines.filtered(lambda r: r.product_id == self.product_xi) - cl_xu = clines.filtered(lambda r: r.product_id == self.product_xu) + cl_uk = clines.filtered(lambda r: r.product_id == self.product_uk) dlines = self.declaration.declaration_line_ids - dl_xi = dlines.filtered(lambda r: r.computation_line_ids == cl_xi) - dl_xu = dlines.filtered(lambda r: r.computation_line_ids == cl_xu) - self.assertEqual(cl_xi.product_origin_country_code, "XI") - self.assertEqual(cl_xu.product_origin_country_code, "XU") - self.assertEqual(dl_xi.product_origin_country_code, "XI") - self.assertEqual(dl_xu.product_origin_country_code, "XU") + dl_uk = dlines.filtered(lambda r: r.computation_line_ids == cl_uk) + self.assertEqual(cl_uk.product_origin_country_code, "XU") + self.assertEqual(dl_uk.product_origin_country_code, "XU") def test_brexit_invoice_intrastat_details(self): inv_in_xi = self.inv_obj.with_context(default_move_type="in_invoice").create( @@ -119,8 +101,8 @@ def test_brexit_invoice_intrastat_details(self): ) with Form(inv_in_xi) as inv_form: with inv_form.invoice_line_ids.new() as ail: - ail.product_id = self.product_xi + ail.product_id = self.product_uk inv_in_xi.invoice_date = inv_in_xi.date inv_in_xi.compute_intrastat_lines() ilines = inv_in_xi.intrastat_line_ids - self.assertEqual(ilines.product_origin_country_code, "XI") + self.assertEqual(ilines.product_origin_country_id, self.env.ref("base.uk")) diff --git a/intrastat_product/views/account_fiscal_position.xml b/intrastat_product/views/account_fiscal_position.xml new file mode 100644 index 000000000..c9360d762 --- /dev/null +++ b/intrastat_product/views/account_fiscal_position.xml @@ -0,0 +1,45 @@ + + + + + intrastat_product.account.fiscal.position.form + account.fiscal.position + + + + + + + + + + + + + + + + diff --git a/intrastat_product/views/account_move.xml b/intrastat_product/views/account_move.xml index 654faf892..9ab1d4e7d 100644 --- a/intrastat_product/views/account_move.xml +++ b/intrastat_product/views/account_move.xml @@ -47,7 +47,7 @@ - + diff --git a/intrastat_product/views/intrastat_product_declaration.xml b/intrastat_product/views/intrastat_product_declaration.xml index aee1eeed9..0d5e3093d 100644 --- a/intrastat_product/views/intrastat_product_declaration.xml +++ b/intrastat_product/views/intrastat_product_declaration.xml @@ -4,7 +4,7 @@ intrastat.product.declaration.form intrastat.product.declaration -
+
- +

Intrastat Product Declaration @@ -59,6 +42,7 @@ + - @@ -86,7 +69,7 @@ - +