From 93b239d9003bfaca806f17df850b42ae301f6864 Mon Sep 17 00:00:00 2001 From: nobuQuartile Date: Thu, 26 Feb 2026 07:32:21 +0000 Subject: [PATCH 1/4] [6114][IMP] product_plm_import: support {esc_code} placeholder in lot sequence prefix Allow lot_sequence_prefix in plm.product.mapping to reference the PLM record's ESC ID via '{esc_code}' placeholder (e.g. '{esc_code}'). This enables per-product prefix resolution at creation time. --- product_plm_import/__manifest__.py | 2 +- product_plm_import/models/plm_product_mapping.py | 5 ++++- product_plm_import/models/product_plm.py | 6 +++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/product_plm_import/__manifest__.py b/product_plm_import/__manifest__.py index 16a5949f..6c91cea8 100644 --- a/product_plm_import/__manifest__.py +++ b/product_plm_import/__manifest__.py @@ -2,7 +2,7 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). { "name": "Product PLM Import", - "version": "16.0.1.0.0", + "version": "16.0.1.1.0", "category": "Stock", "license": "AGPL-3", "author": "Quartile Limited", diff --git a/product_plm_import/models/plm_product_mapping.py b/product_plm_import/models/plm_product_mapping.py index 3d0f4457..c864826f 100644 --- a/product_plm_import/models/plm_product_mapping.py +++ b/product_plm_import/models/plm_product_mapping.py @@ -43,7 +43,10 @@ class PlmProductMapping(models.Model): ) auto_create_lot = fields.Boolean() lot_sequence_padding = fields.Integer() - lot_sequence_prefix = fields.Char() + lot_sequence_prefix = fields.Char( + help="Lot sequence prefix. Supports '{esc_code}' placeholder to use the ESC ID " + "of each PLM record (e.g. '{esc_code}')." + ) default_active = fields.Boolean( help="Default value for active field of the created product." ) diff --git a/product_plm_import/models/product_plm.py b/product_plm_import/models/product_plm.py index bdcd8929..01581925 100644 --- a/product_plm_import/models/product_plm.py +++ b/product_plm_import/models/product_plm.py @@ -144,7 +144,11 @@ def create_products(self, batch_size=30): if mapping.lot_sequence_padding: product.lot_sequence_id.padding = mapping.lot_sequence_padding if mapping.lot_sequence_prefix: - product.lot_sequence_id.prefix = mapping.lot_sequence_prefix + prefix = mapping.lot_sequence_prefix.format( + esc_code=plm_rec.esc_code or "", + ) + if prefix: + product.lot_sequence_id.prefix = prefix product.product_tmpl_id.active = mapping.default_active plm_rec.write({"state": "done", "product_id": product.id}) # This step fails with CasheMiss error in case product creation in From 53b26b433e820a2ac03d6637bb80934be76faa5b Mon Sep 17 00:00:00 2001 From: nobuQuartile Date: Thu, 26 Feb 2026 07:47:17 +0000 Subject: [PATCH 2/4] [product_plm_import] fix bugs found in code review - Add constrains on lot_sequence_prefix to raise ValidationError when the format string is invalid (e.g. unknown placeholder) - Fix return -> continue in _send_plm_import_notification() so records without notified partners do not skip remaining records - Clarify _create_product() to explicitly return on both success and failure paths --- product_plm_import/models/plm_import_log.py | 2 +- product_plm_import/models/plm_product_mapping.py | 13 ++++++++++++- product_plm_import/models/product_plm.py | 5 ++--- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/product_plm_import/models/plm_import_log.py b/product_plm_import/models/plm_import_log.py index 5f20780b..c8a2f4ec 100644 --- a/product_plm_import/models/plm_import_log.py +++ b/product_plm_import/models/plm_import_log.py @@ -72,7 +72,7 @@ def _send_plm_import_notification(self): notified_groups = company.plm_notif_group_ids notified_partners = notified_groups.users.partner_id if not notified_partners: - return + continue rec.message_subscribe(partner_ids=notified_partners.ids) state_desc = rec._get_state_description( "plm_product_state", rec.plm_product_state diff --git a/product_plm_import/models/plm_product_mapping.py b/product_plm_import/models/plm_product_mapping.py index c864826f..21e64150 100644 --- a/product_plm_import/models/plm_product_mapping.py +++ b/product_plm_import/models/plm_product_mapping.py @@ -3,7 +3,8 @@ import fnmatch -from odoo import api, fields, models +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError class PlmProductMapping(models.Model): @@ -53,6 +54,16 @@ class PlmProductMapping(models.Model): company_id = fields.Many2one("res.company") active = fields.Boolean(default=True) + @api.constrains("lot_sequence_prefix") + def _check_lot_sequence_prefix(self): + for record in self: + if not record.lot_sequence_prefix: + continue + try: + record.lot_sequence_prefix.format(esc_code="") + except (ValueError, KeyError): + raise ValidationError(_("Lot Sequence Prefix is invalid.")) + @api.onchange("product_type") def onchange_product_type(self): if self.product_type != "product": diff --git a/product_plm_import/models/product_plm.py b/product_plm_import/models/product_plm.py index 01581925..656499c3 100644 --- a/product_plm_import/models/product_plm.py +++ b/product_plm_import/models/product_plm.py @@ -90,7 +90,6 @@ def _get_uom(self): def _create_product(self): self.ensure_one() - product = self.env["product.product"] description_purchase = self._get_description_purchase() uom = self._get_uom() mapping = self.mapping_id @@ -113,12 +112,12 @@ def _create_product(self): "is_draft": True, } try: - product = self.env["product.product"].create(vals) + return self.env["product.product"].create(vals) except Exception as e: _logger.error( "ProductPlm._create_product - failed to create product: %s", str(e) ) - return product + return self.env["product.product"] @api.model def _get_create_products_domain(self): From cba6d4dd762ac2c16637500c412b5b996c9f6dca Mon Sep 17 00:00:00 2001 From: nobuQuartile Date: Thu, 26 Feb 2026 08:28:52 +0000 Subject: [PATCH 3/4] [product_plm_import] fix bugs found in code review and add tests - Add constrains on lot_sequence_prefix to raise ValidationError when the format string is invalid (e.g. unknown placeholder) - Fix return -> continue in _send_plm_import_notification() so records without notified partners do not skip remaining records - Clarify _create_product() to explicitly return on both success and failure paths - Add tests for lot_sequence_prefix validation and create_products() prefix resolution from ESC ID --- product_plm_import/README.rst | 12 ++-- .../models/plm_product_mapping.py | 2 +- .../static/description/index.html | 7 ++- product_plm_import/tests/__init__.py | 4 ++ product_plm_import/tests/test_product_plm.py | 57 +++++++++++++++++++ 5 files changed, 72 insertions(+), 10 deletions(-) create mode 100644 product_plm_import/tests/__init__.py create mode 100644 product_plm_import/tests/test_product_plm.py diff --git a/product_plm_import/README.rst b/product_plm_import/README.rst index d40b7cc9..106b2ca1 100644 --- a/product_plm_import/README.rst +++ b/product_plm_import/README.rst @@ -37,12 +37,12 @@ Configuration Update fields in the company (in the 'PLM I/F' tab): -- PLM Path: the absolute path to the PLM directory to fetch the files - from. -- PLM Notification Body: the text will be included in the notification - email body. -- PLM Notified Groups: assign groups to notify when a new file is - fetched from the PLM. +- PLM Path: the absolute path to the PLM directory to fetch the files + from. +- PLM Notification Body: the text will be included in the notification + email body. +- PLM Notified Groups: assign groups to notify when a new file is + fetched from the PLM. The PLM-Product Mapping menu enables users to define product policies, such as Product Type, Product Category, Routes, etc., which will be diff --git a/product_plm_import/models/plm_product_mapping.py b/product_plm_import/models/plm_product_mapping.py index 21e64150..f29286ba 100644 --- a/product_plm_import/models/plm_product_mapping.py +++ b/product_plm_import/models/plm_product_mapping.py @@ -62,7 +62,7 @@ def _check_lot_sequence_prefix(self): try: record.lot_sequence_prefix.format(esc_code="") except (ValueError, KeyError): - raise ValidationError(_("Lot Sequence Prefix is invalid.")) + raise ValidationError(_("Lot Sequence Prefix is invalid.")) from None @api.onchange("product_type") def onchange_product_type(self): diff --git a/product_plm_import/static/description/index.html b/product_plm_import/static/description/index.html index 27b36582..5c24ac77 100644 --- a/product_plm_import/static/description/index.html +++ b/product_plm_import/static/description/index.html @@ -8,10 +8,11 @@ /* :Author: David Goodger (goodger@python.org) -:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $ +:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $ :Copyright: This stylesheet has been placed in the public domain. Default cascading style sheet for the HTML output of Docutils. +Despite the name, some widely supported CSS2 features are used. See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to customize this style sheet. @@ -274,7 +275,7 @@ margin-left: 2em ; margin-right: 2em } -pre.code .ln { color: grey; } /* line numbers */ +pre.code .ln { color: gray; } /* line numbers */ pre.code, code { background-color: #eeeeee } pre.code .comment, code .comment { color: #5C6576 } pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold } @@ -300,7 +301,7 @@ span.pre { white-space: pre } -span.problematic { +span.problematic, pre.problematic { color: red } span.section-subtitle { diff --git a/product_plm_import/tests/__init__.py b/product_plm_import/tests/__init__.py new file mode 100644 index 00000000..ad80d871 --- /dev/null +++ b/product_plm_import/tests/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2026 Quartile Limited +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import test_product_plm diff --git a/product_plm_import/tests/test_product_plm.py b/product_plm_import/tests/test_product_plm.py new file mode 100644 index 00000000..fab7e86b --- /dev/null +++ b/product_plm_import/tests/test_product_plm.py @@ -0,0 +1,57 @@ +# Copyright 2026 Quartile Limited +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo.exceptions import ValidationError +from odoo.tests.common import TransactionCase + + +class TestProductPlm(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env["ir.config_parameter"].set_param( + "product_lot_sequence.policy", "product" + ) + cls.item_type = cls.env["plm.item.type"].create({"name": "Test PCBA"}) + cls.mapping = cls.env["plm.product.mapping"].create( + { + "item_type_id": cls.item_type.id, + "product_type": "product", + "product_categ_id": cls.env.ref("product.product_category_all").id, + "tracking": "serial", + "auto_create_lot": True, + } + ) + cls.plm_rec = cls.env["product.plm"].create( + { + "part_number": "TEST-001", + "name": "Test Product", + "mapping_id": cls.mapping.id, + "company_id": cls.env.company.id, + "esc_code": "ESC123", + } + ) + + def test_lot_sequence_prefix_valid(self): + self.mapping.write({"lot_sequence_prefix": "{esc_code}"}) + self.mapping.write({"lot_sequence_prefix": "STATIC"}) + self.mapping.write({"lot_sequence_prefix": False}) + with self.assertRaises(ValidationError): + self.mapping.write({"lot_sequence_prefix": "{invalid_key}"}) + with self.assertRaises(ValidationError): + self.mapping.write({"lot_sequence_prefix": "{"}) + + def test_create_products_prefix_from_esc_code(self): + """create_products() sets lot sequence prefix from ESC ID.""" + self.mapping.write({"lot_sequence_prefix": "{esc_code}"}) + self.env["product.plm"].create_products() + self.assertEqual(self.plm_rec.state, "done") + self.assertEqual(self.plm_rec.product_id.lot_sequence_id.prefix, "ESC123") + + def test_create_products_no_prefix_without_esc_code(self): + """create_products() skips prefix when ESC ID is absent.""" + self.mapping.write({"lot_sequence_prefix": "{esc_code}"}) + self.plm_rec.write({"esc_code": False}) + self.env["product.plm"].create_products() + self.assertEqual(self.plm_rec.state, "done") + self.assertFalse(self.plm_rec.product_id.lot_sequence_id.prefix) From d9b04e75963b1064691c7a3e9f203a833704525c Mon Sep 17 00:00:00 2001 From: nobuQuartile Date: Wed, 4 Mar 2026 01:29:32 +0000 Subject: [PATCH 4/4] delete agpl section --- product_plm_import/tests/__init__.py | 3 --- product_plm_import/tests/test_product_plm.py | 3 --- 2 files changed, 6 deletions(-) diff --git a/product_plm_import/tests/__init__.py b/product_plm_import/tests/__init__.py index ad80d871..8893a775 100644 --- a/product_plm_import/tests/__init__.py +++ b/product_plm_import/tests/__init__.py @@ -1,4 +1 @@ -# Copyright 2026 Quartile Limited -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). - from . import test_product_plm diff --git a/product_plm_import/tests/test_product_plm.py b/product_plm_import/tests/test_product_plm.py index fab7e86b..d57de5f7 100644 --- a/product_plm_import/tests/test_product_plm.py +++ b/product_plm_import/tests/test_product_plm.py @@ -1,6 +1,3 @@ -# Copyright 2026 Quartile Limited -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). - from odoo.exceptions import ValidationError from odoo.tests.common import TransactionCase