diff --git a/product_harmonized_system/models/hs_code.py b/product_harmonized_system/models/hs_code.py index 3864d47a9..c4cab1816 100644 --- a/product_harmonized_system/models/hs_code.py +++ b/product_harmonized_system/models/hs_code.py @@ -4,7 +4,8 @@ # @author Luc de Meyer # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import api, fields, models +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError class HSCode(models.Model): @@ -74,32 +75,41 @@ def _compute_product_tmpl_count(self): for code in self: code.product_tmpl_count = len(code.product_tmpl_ids) - @api.depends("local_code", "description") + def _get_name(self): + self.ensure_one() + name = self.local_code + if self.description: + name += " " + self.description + return len(name) > 55 and name[:55] + "..." or name + def name_get(self): res = [] for this in self: - name = this.local_code - if this.description: - name += " " + this.description - name = len(name) > 55 and name[:55] + "..." or name + name = this._get_name() res.append((this.id, name)) return res - _sql_constraints = [ - ( - "local_code_company_uniq", - "unique(local_code, company_id)", - "This code already exists for this company !", - ) - ] + @api.constrains("local_code", "company_id") + def _check_duplicate_hs_code(self): + for rec in self: + domain = [("local_code", "=", rec.local_code), ("id", "!=", rec.id)] + if rec.company_id: + domain += [("company_id", "=", rec.company_id.id)] + dup = self.search_count(domain) + if dup: + msg = ( + "There is already an existing H.S. Code with the specified Code: %s." + % rec.local_code + ) + raise ValidationError(_(msg)) @api.model def create(self, vals): if vals.get("local_code"): - vals["local_code"] = vals["local_code"].replace(" ", "") + vals["local_code"] = vals["local_code"].strip() return super().create(vals) def write(self, vals): if vals.get("local_code"): - vals["local_code"] = vals["local_code"].replace(" ", "") + vals["local_code"] = vals["local_code"].strip() return super().write(vals) diff --git a/product_harmonized_system_heading/__init__.py b/product_harmonized_system_heading/__init__.py new file mode 100644 index 000000000..cc6b6354a --- /dev/null +++ b/product_harmonized_system_heading/__init__.py @@ -0,0 +1,2 @@ +from . import models +from .hooks import post_init_hook diff --git a/product_harmonized_system_heading/__manifest__.py b/product_harmonized_system_heading/__manifest__.py new file mode 100644 index 000000000..b20f6705d --- /dev/null +++ b/product_harmonized_system_heading/__manifest__.py @@ -0,0 +1,24 @@ +# Copyright 2023 ForgeFlow S.L. (https://www.forgeflow.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +{ + "name": "Product Harmonized System Code Heading", + "summary": """ + Adds a heading model for the Product Harmonized System Codes (H.S. Codes). + """, + "version": "13.0.1.0.0", + "category": "Reporting", + "license": "AGPL-3", + "website": "https://github.com/OCA/intrastat-extrastat", + "author": "ForgeFlow, Odoo Community Association (OCA)", + "depends": ["product_harmonized_system"], + "data": [ + "security/security.xml", + "security/ir.model.access.csv", + "views/hs_code_heading_views.xml", + "views/hs_code_views.xml", + ], + "maintainers": ["GuillemCForgeFlow"], + "installable": True, + "application": False, + "post_init_hook": "post_init_hook", +} diff --git a/product_harmonized_system_heading/hooks.py b/product_harmonized_system_heading/hooks.py new file mode 100644 index 000000000..896a9c89a --- /dev/null +++ b/product_harmonized_system_heading/hooks.py @@ -0,0 +1,33 @@ +# Copyright 2023 ForgeFlow S.L. (https://www.forgeflow.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +import logging + +from odoo import SUPERUSER_ID, api + +_logger = logging.getLogger(__name__) + + +def post_init_hook(cr, registry): + _logger.info("Creating default H.S. Code Headings for H.S. Codes in the System.") + env = api.Environment(cr, SUPERUSER_ID, {}) + HSCode = env["hs.code"].with_context(active_test=False) + HSCodeHeading = env["hs.code.heading"].with_context(active_test=False) + existing_hs_code_heading_codes = HSCodeHeading.search([]).mapped("code") + to_create_dict = dict() + hs_code_ids = HSCode.search([]) + for hs_code in hs_code_ids: + vals = False + heading_code = hs_code.local_code[:4] + if heading_code in existing_hs_code_heading_codes: + continue + if heading_code not in to_create_dict.keys(): + vals = {"code": heading_code} + to_create_dict[heading_code] = [hs_code.id] + else: + to_create_dict[heading_code].append(hs_code.id) + vals_list = list() + for code, rel_hs_code_ids in to_create_dict.items(): + vals = {"code": code, "hs_code_ids": [(6, 0, rel_hs_code_ids)]} + vals_list.append(vals) + _logger.info("Creating %s H.S. Code Headings" % len(vals_list)) + HSCodeHeading.create(vals_list) diff --git a/product_harmonized_system_heading/models/__init__.py b/product_harmonized_system_heading/models/__init__.py new file mode 100644 index 000000000..4f6ba15e7 --- /dev/null +++ b/product_harmonized_system_heading/models/__init__.py @@ -0,0 +1,2 @@ +from . import hs_code_heading +from . import hs_code diff --git a/product_harmonized_system_heading/models/hs_code.py b/product_harmonized_system_heading/models/hs_code.py new file mode 100644 index 000000000..7e789d427 --- /dev/null +++ b/product_harmonized_system_heading/models/hs_code.py @@ -0,0 +1,63 @@ +# Copyright 2023 ForgeFlow S.L. (https://www.forgeflow.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class HSCode(models.Model): + _inherit = "hs.code" + + hs_code_heading_id = fields.Many2one( + comodel_name="hs.code.heading", string="H.S. Code Heading", readonly=True + ) + + @api.constrains("local_code") + def _check_local_code(self): + for hs_code in self: + if len(hs_code.local_code) < 4: + msg = ( + "The H.S. Local Code needs to have at least 4 characters as this " + "indicate the H.S. Heading." + ) + raise ValidationError(_(msg)) + + @api.model + def _get_hs_code_heading_id(self, vals): + """ + Method used in the `create` and `write` methods of the `hs.code`. + Used to get the corresponding H.S. Code Heading based on the `local_code` value + of the H.S. Code. Get the existing one, if there is, otherwise create a new one. + :return: a modified vals dict with the `hs_code_heading_id` value + """ + HSCodeHeading = self.env["hs.code.heading"] + local_code = vals.get("local_code", "") + heading_code = local_code[:4] + heading = HSCodeHeading.search([("code", "=", heading_code)], limit=1) + if not heading: + heading = HSCodeHeading.create({"code": heading_code}) + vals["hs_code_heading_id"] = heading.id + return vals + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + vals = self._get_hs_code_heading_id(vals) + return super().create(vals_list) + + def write(self, vals): + if "local_code" in vals: + vals = self._get_hs_code_heading_id(vals) + return super().write(vals) + + def _get_name(self): + """ + Overwrite method from the `product_harmonized_system` module in order to set the + Heading Description in the H.S. Code name + """ + self.ensure_one() + name = self.local_code + if self.hs_code_heading_id.description: + name += " " + self.hs_code_heading_id.description + if self.description: + name += ": " + self.description + return len(name) > 55 and name[:55] + "..." or name diff --git a/product_harmonized_system_heading/models/hs_code_heading.py b/product_harmonized_system_heading/models/hs_code_heading.py new file mode 100644 index 000000000..7ddb3bbfe --- /dev/null +++ b/product_harmonized_system_heading/models/hs_code_heading.py @@ -0,0 +1,48 @@ +# Copyright 2023 ForgeFlow S.L. (https://www.forgeflow.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo import fields, models + + +class HSCodeHeading(models.Model): + _name = "hs.code.heading" + _description = "H.S. Code Heading" + + # General fields + active = fields.Boolean(default=True) + code = fields.Char( + required=True, + size=4, + help="The H.S. Code Heading can only have 4 digits, as this define the Heading.", + ) + description = fields.Text() + + # Relational fields + hs_code_ids = fields.One2many( + comodel_name="hs.code", + inverse_name="hs_code_heading_id", + string="H.S. Codes", + readonly=True, + help="The related H.S. Codes using the Heading.", + ) + + _sql_constraints = [ + ( + "code_uniq", + "UNIQUE(code)", + "There is already an existing H.S. Code Heading with this Code.", + ) + ] + + def _get_name(self): + self.ensure_one() + name = self.code + if self.description: + name += " " + self.description + return len(name) > 55 and name[:55] + "..." or name + + def name_get(self): + res = [] + for rec in self: + name = rec._get_name() + res.append((rec.id, name)) + return res diff --git a/product_harmonized_system_heading/readme/CONTRIBUTORS.rst b/product_harmonized_system_heading/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..163379ac6 --- /dev/null +++ b/product_harmonized_system_heading/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Guillem Casassas diff --git a/product_harmonized_system_heading/readme/DESCRIPTION.rst b/product_harmonized_system_heading/readme/DESCRIPTION.rst new file mode 100644 index 000000000..f642cb821 --- /dev/null +++ b/product_harmonized_system_heading/readme/DESCRIPTION.rst @@ -0,0 +1,4 @@ +This module adds the possibility to assign Headings to the H.S. Codes in order to have +a better structure. + +The H.S. Code Heading has a unique Code and a possible Description. diff --git a/product_harmonized_system_heading/readme/USAGE.rst b/product_harmonized_system_heading/readme/USAGE.rst new file mode 100644 index 000000000..2008a142e --- /dev/null +++ b/product_harmonized_system_heading/readme/USAGE.rst @@ -0,0 +1,6 @@ +In order to assign the H.S. Code Heading to a new H.S. Code, we first need to have the +Heading created, if there is none when creating the Code, the user will see an error +demanding to create the Heading first. + +The relation is not editable from the User prespective as we just to need to ensure that +the Headings are created before creating the H.S. Codes. diff --git a/product_harmonized_system_heading/security/ir.model.access.csv b/product_harmonized_system_heading/security/ir.model.access.csv new file mode 100644 index 000000000..ce78aad18 --- /dev/null +++ b/product_harmonized_system_heading/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_hs_code_heading_user,access_hs_code_heading_user,model_hs_code_heading,,1,0,0,0 +access_hs_code_heading_manager,access_hs_code_heading_manager,model_hs_code_heading,product_harmonized_system_heading.group_product_harmonized_system_heading_manager,1,1,1,1 diff --git a/product_harmonized_system_heading/security/security.xml b/product_harmonized_system_heading/security/security.xml new file mode 100644 index 000000000..849884074 --- /dev/null +++ b/product_harmonized_system_heading/security/security.xml @@ -0,0 +1,22 @@ + + + + H.S. Code + 20 + + + H.S. Code Heading Manager + The user will have manager access to H.S. Code Heading. + + + + + diff --git a/product_harmonized_system_heading/tests/__init__.py b/product_harmonized_system_heading/tests/__init__.py new file mode 100644 index 000000000..3d640cda3 --- /dev/null +++ b/product_harmonized_system_heading/tests/__init__.py @@ -0,0 +1 @@ +from . import test_product_harmonized_system_heading diff --git a/product_harmonized_system_heading/tests/test_product_harmonized_system_heading.py b/product_harmonized_system_heading/tests/test_product_harmonized_system_heading.py new file mode 100644 index 000000000..c4f6d7c39 --- /dev/null +++ b/product_harmonized_system_heading/tests/test_product_harmonized_system_heading.py @@ -0,0 +1,79 @@ +# Copyright 2023 ForgeFlow S.L. (https://www.forgeflow.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from odoo.exceptions import ValidationError +from odoo.tests.common import SavepointCase + + +class TestProductHarmonizedSystemHeading(SavepointCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + # Models + cls.hs_code_model = cls.env["hs.code"] + cls.hs_code_heading_model = cls.env["hs.code.heading"] + + @classmethod + def _create_hs_code_heading(cls, code="1234", description=False): + return cls.hs_code_heading_model.create( + {"code": code, "description": description} + ) + + @classmethod + def _create_hs_code(cls, local_code="123456789", heading=False, description=False): + return cls.hs_code_model.create( + { + "local_code": local_code, + "hs_code_heading_id": heading.id if heading else False, + "description": description, + } + ) + + def test_01_check_create_hs_code_with_no_heading(self): + """ + Check that a Heading is automatically created if there is none for the H.S. + Code. + """ + local_code = "123456789" + heading_code = local_code[:4] + self._create_hs_code(local_code=local_code) + heading = self.hs_code_heading_model.search( + [("code", "=", heading_code)], limit=1 + ) + self.assertTrue(heading) + + def test_02_check_hs_code_creation(self): + """ + Check that the H.S. Code is correctly created if there is a related Heading. + """ + heading = self._create_hs_code_heading(code="5678") + code = self._create_hs_code(local_code="56789123", heading=heading) + self.assertTrue(code) + + def test_03_check_hs_code_creation(self): + """ + Check that a ValidationError is raised if the H.S. Local Code has less than 4 + digits. + """ + with self.assertRaises(ValidationError): + self._create_hs_code(local_code="123") + + def test_04_check_correct_hs_code_name_get_value(self): + """ + Check that the H.S. Code display name is correctly set based on the changes. + """ + heading_description = "Heading Description" + code_description = "Code Description" + heading = self._create_hs_code_heading(description=heading_description) + code = self._create_hs_code(heading=heading, description=code_description) + self.assertEqual( + code.name_get()[0][1], + "123456789 %s: %s" % (heading_description, code_description), + ) + + def test_05_check_correct_hs_code_heading_name_get_value(self): + """ + Check that the H.S. Code display name is correctly set based on the changes. + """ + heading_description = "Heading Description 2" + heading = self._create_hs_code_heading(description=heading_description) + self.assertEqual(heading.name_get()[0][1], "1234 %s" % heading_description) diff --git a/product_harmonized_system_heading/views/hs_code_heading_views.xml b/product_harmonized_system_heading/views/hs_code_heading_views.xml new file mode 100644 index 000000000..d390c0d94 --- /dev/null +++ b/product_harmonized_system_heading/views/hs_code_heading_views.xml @@ -0,0 +1,57 @@ + + + + + hs.code.heading.form + hs.code.heading + +
+ + + + + + + + + + + + + + + +
+
+
+ + hs.code.heading.tree + hs.code.heading + + + + + + + + + + hs.code.heading.search.view + hs.code.heading + + + + + + + + + H.S. Code Heading + ir.actions.act_window + hs.code.heading + tree,form + +
diff --git a/product_harmonized_system_heading/views/hs_code_views.xml b/product_harmonized_system_heading/views/hs_code_views.xml new file mode 100644 index 000000000..7fda1d83f --- /dev/null +++ b/product_harmonized_system_heading/views/hs_code_views.xml @@ -0,0 +1,49 @@ + + + + + hs.code.search - product_harmonized_system_heading + hs.code + + + + + + + + + + + + + hs.code.tree - product_harmonized_system_heading + hs.code + + + + + + + + + hs.code.form - product_harmonized_system_heading + hs.code + + + + + + + + diff --git a/product_harmonized_system_heading_stock/__init__.py b/product_harmonized_system_heading_stock/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/product_harmonized_system_heading_stock/__manifest__.py b/product_harmonized_system_heading_stock/__manifest__.py new file mode 100644 index 000000000..ee443dc25 --- /dev/null +++ b/product_harmonized_system_heading_stock/__manifest__.py @@ -0,0 +1,16 @@ +# Copyright 2023 ForgeFlow S.L. (https://www.forgeflow.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +{ + "name": "Product Harmonized System Header (menu entry)", + "version": "13.0.1.0.0", + "category": "Reporting", + "license": "AGPL-3", + "summary": "Adds a Menu Entry for H.S. Code Heading", + "website": "https://github.com/OCA/intrastat-extrastat", + "author": "ForgeFlow, Odoo Community Association (OCA)", + "depends": ["product_harmonized_system_heading", "stock"], + "data": ["views/hs_code_header_menu.xml"], + "installable": True, + "application": False, + "auto_install": True, +} diff --git a/product_harmonized_system_heading_stock/readme/CONTRIBUTORS.rst b/product_harmonized_system_heading_stock/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..163379ac6 --- /dev/null +++ b/product_harmonized_system_heading_stock/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Guillem Casassas diff --git a/product_harmonized_system_heading_stock/readme/DESCRIPTION.rst b/product_harmonized_system_heading_stock/readme/DESCRIPTION.rst new file mode 100644 index 000000000..a9e70ff53 --- /dev/null +++ b/product_harmonized_system_heading_stock/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +This module adds a menu entry for H.S. Code Heading. This menu entry is available under *Inventory > Configuration > Products*. diff --git a/product_harmonized_system_heading_stock/views/hs_code_header_menu.xml b/product_harmonized_system_heading_stock/views/hs_code_header_menu.xml new file mode 100644 index 000000000..14459eeca --- /dev/null +++ b/product_harmonized_system_heading_stock/views/hs_code_header_menu.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/setup/product_harmonized_system_heading/odoo/addons/product_harmonized_system_heading b/setup/product_harmonized_system_heading/odoo/addons/product_harmonized_system_heading new file mode 120000 index 000000000..64470d0ba --- /dev/null +++ b/setup/product_harmonized_system_heading/odoo/addons/product_harmonized_system_heading @@ -0,0 +1 @@ +../../../../product_harmonized_system_heading \ No newline at end of file diff --git a/setup/product_harmonized_system_heading/setup.py b/setup/product_harmonized_system_heading/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/product_harmonized_system_heading/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/product_harmonized_system_heading_stock/odoo/addons/product_harmonized_system_heading_stock b/setup/product_harmonized_system_heading_stock/odoo/addons/product_harmonized_system_heading_stock new file mode 120000 index 000000000..cf9131410 --- /dev/null +++ b/setup/product_harmonized_system_heading_stock/odoo/addons/product_harmonized_system_heading_stock @@ -0,0 +1 @@ +../../../../product_harmonized_system_heading_stock \ No newline at end of file diff --git a/setup/product_harmonized_system_heading_stock/setup.py b/setup/product_harmonized_system_heading_stock/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/product_harmonized_system_heading_stock/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)