From 2874b76ab8fcb1516cfc84fafee3592a07827103 Mon Sep 17 00:00:00 2001 From: Luc De Meyer Date: Mon, 2 May 2022 15:35:56 +0200 Subject: [PATCH] [14.0]intrastat - improved brexit support --- intrastat_base/models/__init__.py | 1 + intrastat_base/models/res_partner.py | 67 +++++++++++ .../models/intrastat_product_declaration.py | 63 +++++++--- .../report/intrastat_product_report_xls.py | 4 +- intrastat_product/tests/__init__.py | 1 + intrastat_product/tests/common.py | 4 +- intrastat_product/tests/test_brexit.py | 111 ++++++++++++++++++ .../views/intrastat_product_declaration.xml | 6 +- .../models/product_template.py | 4 +- 9 files changed, 240 insertions(+), 21 deletions(-) create mode 100644 intrastat_base/models/res_partner.py create mode 100644 intrastat_product/tests/test_brexit.py diff --git a/intrastat_base/models/__init__.py b/intrastat_base/models/__init__.py index cea19b9..cc10e08 100644 --- a/intrastat_base/models/__init__.py +++ b/intrastat_base/models/__init__.py @@ -3,3 +3,4 @@ from . import res_company 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 new file mode 100644 index 0000000..ed692b8 --- /dev/null +++ b/intrastat_base/models/res_partner.py @@ -0,0 +1,67 @@ +# 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/models/intrastat_product_declaration.py b/intrastat_product/models/intrastat_product_declaration.py index c3b93f6..23d5506 100644 --- a/intrastat_product/models/intrastat_product_declaration.py +++ b/intrastat_product/models/intrastat_product_declaration.py @@ -1,5 +1,5 @@ # Copyright 2011-2020 Akretion France (http://www.akretion.com) -# Copyright 2009-2021 Noviat (http://www.noviat.com) +# Copyright 2009-2022 Noviat (http://www.noviat.com) # @author Alexis de Lattre # @author Luc de Meyer @@ -492,21 +492,20 @@ class IntrastatProductDeclaration(models.Model): def _get_product_origin_country_code( self, inv_line, product_origin_country, notedict ): - product_origin_country_code = "QU" + cc = "QU" if product_origin_country.code: - product_origin_country_code = product_origin_country.code + cc = product_origin_country.code year = self.year or str(inv_line.move_id.date.year) if year >= "2021": - if ( - hasattr(inv_line.product_id, "origin_state_id") - and inv_line.product_id.origin_state_id - and inv_line.product_id.origin_state_id.name.lower() - == "northern ireland" - ): - product_origin_country_code = "XI" - elif inv_line.product_id.origin_country_id.code == "GB": - product_origin_country_code = "XU" - return product_origin_country_code + 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 def _get_vat(self, inv_line, notedict): vat = False @@ -683,6 +682,9 @@ class IntrastatProductDeclaration(models.Model): partner_country = self._get_partner_country( inv_line, notedict, eu_countries ) + partner_country_code = ( + invoice.commercial_partner_id._get_intrastat_country_code() + ) if inv_intrastat_line: hs_code = inv_intrastat_line.hs_code_id @@ -741,6 +743,7 @@ class IntrastatProductDeclaration(models.Model): "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, @@ -847,7 +850,7 @@ class IntrastatProductDeclaration(models.Model): @api.model def _group_line_hashcode_fields(self, computation_line): return { - "country": computation_line.src_dest_country_id.id or False, + "country": computation_line.src_dest_country_code, "hs_code_id": computation_line.hs_code_id.id or False, "intrastat_unit": computation_line.intrastat_unit_id.id or False, "transaction": computation_line.transaction_id.id or False, @@ -866,6 +869,7 @@ class IntrastatProductDeclaration(models.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, @@ -1070,6 +1074,15 @@ class IntrastatProductComputationLine(models.Model): string="Country", help="Country of Origin/Destination", ) + src_dest_country_code = fields.Char( + string="Country Code", + compute="_compute_src_dest_country_code", + store=True, + required=True, + readonly=False, + help="2 digit code of country of origin/destination.\n" + "Specify 'XI' for UK Northern Ireland and 'XU' for rest of the UK.", + ) product_id = fields.Many2one( "product.product", related="invoice_line_id.product_id" ) @@ -1129,6 +1142,14 @@ 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""" @@ -1184,6 +1205,12 @@ class IntrastatProductDeclarationLine(models.Model): string="Country", help="Country of Origin/Destination", ) + src_dest_country_code = fields.Char( + string="Country Code", + required=True, + help="2 digit code of country of origin/destination.\n" + "Specify 'XI' for UK Northern Ireland and 'XU' for rest of the UK.", + ) hs_code_id = fields.Many2one("hs.code", string="Intrastat Code") intrastat_unit_id = fields.Many2one( "intrastat.unit", @@ -1226,6 +1253,14 @@ class IntrastatProductDeclarationLine(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.constrains("vat") def _check_vat(self): for this in self: diff --git a/intrastat_product/report/intrastat_product_report_xls.py b/intrastat_product/report/intrastat_product_report_xls.py index 48ccb26..54968fb 100644 --- a/intrastat_product/report/intrastat_product_report_xls.py +++ b/intrastat_product/report/intrastat_product_report_xls.py @@ -1,4 +1,4 @@ -# Copyright 2009-2020 Noviat +# Copyright 2009-2022 Noviat # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import logging @@ -60,7 +60,7 @@ class IntrastatProductDeclarationXlsx(models.AbstractModel): }, "line": { "type": "string", - "value": self._render("line.src_dest_country_id.name"), + "value": self._render("line.src_dest_country_code"), }, "width": 28, }, diff --git a/intrastat_product/tests/__init__.py b/intrastat_product/tests/__init__.py index 804b5f7..a7ccefc 100644 --- a/intrastat_product/tests/__init__.py +++ b/intrastat_product/tests/__init__.py @@ -2,6 +2,7 @@ from . import common from . import common_purchase from . import common_sale from . import test_intrastat_product +from . import test_brexit from . import test_company from . import test_purchase_order from . import test_sale_order diff --git a/intrastat_product/tests/common.py b/intrastat_product/tests/common.py index 6efb68b..ae84988 100644 --- a/intrastat_product/tests/common.py +++ b/intrastat_product/tests/common.py @@ -39,7 +39,7 @@ class IntrastatProductCommon(IntrastatCommon): @classmethod def _init_regions(cls): - # Create Belgium Region + # Create Region cls._create_region() vals = { @@ -75,7 +75,7 @@ class IntrastatProductCommon(IntrastatCommon): @classmethod def _create_region(cls, vals=None): values = { - "code": "BE_w", + "code": "2", "country_id": cls.env.ref("base.be").id, "company_id": cls.env.company.id, "description": "Belgium Walloon Region", diff --git a/intrastat_product/tests/test_brexit.py b/intrastat_product/tests/test_brexit.py new file mode 100644 index 0000000..1917a98 --- /dev/null +++ b/intrastat_product/tests/test_brexit.py @@ -0,0 +1,111 @@ +# Copyright 2022 Noviat. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.tests import Form, SavepointCase + +from .common import IntrastatProductCommon + + +class TestIntrastatBrexit(IntrastatProductCommon, SavepointCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.inv_obj = cls.env["account.move"] + cls.hs_code_whiskey = cls.env["hs.code"].create( + { + "description": "Whiskey", + "local_code": "22083000", + } + ) + cls.product_xi = 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, + } + ) + cls.partner_xi = cls.env["res.partner"].create( + { + "name": "Bushmills Distillery", + "country_id": cls.env.ref("base.uk").id, + "state_id": cls.env.ref("base.state_uk18").id, + "vat": "XI123456782", + "property_account_position_id": cls.position.id, + } + ) + + def test_brexit_sale(self): + inv_out_xi = self.inv_obj.with_context(default_move_type="out_invoice").create( + { + "partner_id": self.partner_xi.id, + "fiscal_position_id": self.position.id, + } + ) + with Form(inv_out_xi) as inv_form: + with inv_form.invoice_line_ids.new() as ail: + ail.product_id = self.product_c3po.product_variant_ids[0] + inv_out_xi.action_post() + + self._create_declaration( + { + "declaration_type": "dispatches", + "year": str(inv_out_xi.date.year), + "month": str(inv_out_xi.date.month).zfill(2), + } + ) + self.declaration.action_gather() + self.declaration.generate_declaration() + cline = self.declaration.computation_line_ids + dline = self.declaration.declaration_line_ids + self.assertEqual(cline.src_dest_country_code, "XI") + self.assertEqual(dline.src_dest_country_code, "XI") + + def test_brexit_purchase(self): + inv_in_xi = self.inv_obj.with_context(default_move_type="in_invoice").create( + { + "partner_id": self.partner_xi.id, + "fiscal_position_id": self.position.id, + } + ) + 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 + inv_in_xi.invoice_date = inv_in_xi.date + inv_in_xi.action_post() + + self._create_declaration( + { + "declaration_type": "arrivals", + "year": str(inv_in_xi.date.year), + "month": str(inv_in_xi.date.month).zfill(2), + } + ) + 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) + 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") diff --git a/intrastat_product/views/intrastat_product_declaration.xml b/intrastat_product/views/intrastat_product_declaration.xml index 21f1bb4..aee1eee 100644 --- a/intrastat_product/views/intrastat_product_declaration.xml +++ b/intrastat_product/views/intrastat_product_declaration.xml @@ -224,6 +224,7 @@ + + @@ -322,6 +324,7 @@ /> + - + + diff --git a/product_harmonized_system/models/product_template.py b/product_harmonized_system/models/product_template.py index d524206..902deea 100644 --- a/product_harmonized_system/models/product_template.py +++ b/product_harmonized_system/models/product_template.py @@ -33,9 +33,9 @@ class ProductTemplate(models.Model): domain="[('country_id', '=?', origin_country_id)]", help="Country State of origin of the product.\n" "This field is used for the Intrastat declaration, " - "selecting 'Northern Ireland' will set the code 'XI' " + "selecting one of the Northern Ireland counties will set the code 'XI' " "for products from the United Kingdom whereas code 'XU' " - "will be used for the other UK states.", + "will be used for the other UK counties.", )