From a41f9bd0ce2d5edcbf227e9999934907ce8a1e6a Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Mon, 8 May 2023 10:41:09 +0200 Subject: [PATCH 01/14] connector_importer_product: fix supplierinfo default conf --- .../data/import_type.xml | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/connector_importer_product/data/import_type.xml b/connector_importer_product/data/import_type.xml index fa1648d80..d15ad8b26 100644 --- a/connector_importer_product/data/import_type.xml +++ b/connector_importer_product/data/import_type.xml @@ -66,10 +66,22 @@ Import Product Supplier Info product_supplierinfo +- model: res.partner + options: + importer: + odoo_unique_key: name + override_existing: false + mapper: + name: importer.mapper.dynamic + default_keys: + supplier_rank: 1 + - model: product.supplierinfo - importer: - name: - product.supplierinfo.importer + options: + importer: + odoo_unique_key: name + mapper: + name: product.supplierinfo.mapper From f9dcb2ac682c9e408c9fa5f5fc762934fd2b69a9 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Mon, 8 May 2023 10:46:04 +0200 Subject: [PATCH 02/14] connector_importer_product: add ROADMAP --- connector_importer_product/readme/ROADMAP.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 connector_importer_product/readme/ROADMAP.rst diff --git a/connector_importer_product/readme/ROADMAP.rst b/connector_importer_product/readme/ROADMAP.rst new file mode 100644 index 000000000..4af03b91b --- /dev/null +++ b/connector_importer_product/readme/ROADMAP.rst @@ -0,0 +1,2 @@ +* get rid of supplier info specific importer +* find a flexible way to define matching products From 9ab1e1c4874a326cca780858d88b9b60d80fefdb Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Wed, 10 May 2023 11:06:58 +0200 Subject: [PATCH 03/14] connector_importer_product: exclude blacklisted columns --- .../components/product_product/record_handler.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/connector_importer_product/components/product_product/record_handler.py b/connector_importer_product/components/product_product/record_handler.py index 8b78c4453..3397323e2 100644 --- a/connector_importer_product/components/product_product/record_handler.py +++ b/connector_importer_product/components/product_product/record_handler.py @@ -64,8 +64,10 @@ def _update_template_attributes(self, odoo_record, values, orig_values): TplAttrLine = self.env["product.template.attribute.line"] TplAttrValue = self.env["product.template.attribute.value"] template = odoo_record.product_tmpl_id + blacklist = self.work.options.mapper.get("source_key_blacklist", []) attr_columns = filter( - lambda col: col.startswith("product_attr"), orig_values.keys() + lambda col: col.startswith("product_attr") and col not in blacklist, + orig_values.keys(), ) tpl_attr_values = self.env["product.template.attribute.value"] # Detect and gather attributes and attribute values to import From b142892e400fa0faaf654f58ddbb5851b523729e Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Wed, 10 May 2023 11:11:32 +0200 Subject: [PATCH 04/14] connector_importer_product: create xid for tmpl when needed If xid is used as a unique identifier we must create a xid for the template too. This way you can reference it w/ 'xid::product_tmpl_id' --- .../product_product/record_handler.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/connector_importer_product/components/product_product/record_handler.py b/connector_importer_product/components/product_product/record_handler.py index 3397323e2..eece6d5fe 100644 --- a/connector_importer_product/components/product_product/record_handler.py +++ b/connector_importer_product/components/product_product/record_handler.py @@ -33,6 +33,25 @@ def odoo_post_create(self, odoo_record, values, orig_values): def odoo_post_write(self, odoo_record, values, orig_values): self._update_template_attributes(odoo_record, values, orig_values) + def odoo_create(self, values, orig_values): + odoo_record = super().odoo_create(values, orig_values) + # Set the external ID for the template if necessary + # TODO: add tests + if self.must_generate_xmlid and orig_values.get("xid::product_tmpl_id"): + tmpl_xid = sanitize_external_id(orig_values.get("xid::product_tmpl_id")) + if not self.env.ref(tmpl_xid, raise_if_not_found=False): + module, id_ = tmpl_xid.split(".", 1) + self.env["ir.model.data"].create( + { + "name": id_, + "module": module, + "model": odoo_record.product_tmpl_id._name, + "res_id": odoo_record.product_tmpl_id.id, + "noupdate": False, + } + ) + return odoo_record + def _update_template_attributes(self, odoo_record, values, orig_values): """Update the 'attribute_line_ids' field of the related template. From eb26a7e17139b81abfb12a9d62ff4a16dd5a442a Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Wed, 10 May 2023 11:28:44 +0200 Subject: [PATCH 05/14] c_importer_product: supplierinfo fix tmpl lookup via xid --- .../components/product_supplierinfo.py | 33 ++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/connector_importer_product/components/product_supplierinfo.py b/connector_importer_product/components/product_supplierinfo.py index 373e6804c..bb3f22655 100644 --- a/connector_importer_product/components/product_supplierinfo.py +++ b/connector_importer_product/components/product_supplierinfo.py @@ -2,7 +2,9 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) from odoo.addons.component.core import Component +from odoo.addons.connector.components.mapper import mapping from odoo.addons.connector_importer.utils.mapper_utils import backend_to_rel +from odoo.addons.connector_importer.utils.misc import sanitize_external_id class ProductSupplierinfoMapper(Component): @@ -12,22 +14,31 @@ class ProductSupplierinfoMapper(Component): _apply_on = "product.supplierinfo" _usage = "importer.mapper" - direct = [ - ( - backend_to_rel( - "tmpl_default_code", - # See motiviation in product_product.mapper. - search_field="product_variant_ids.default_code", - ), - "product_tmpl_id", - ), - ] required = { "__name": "name", "__tmpl": "product_tmpl_id", } - # TODO: set partner supplier rank + @mapping + def product_tmpl_id(self, record): + """Ensure a template is found.""" + # TODO: add test + value = None + if record.get("tmpl_default_code"): + handler = backend_to_rel( + "tmpl_default_code", + # See motiviation in product_product.mapper. + search_field="product_variant_ids.default_code", + ) + value = handler(record, "product_tmpl_id") + elif record.get("xid::product_tmpl_id"): + # Special case for when products are univocally identified via xid. + # TODO: try to get rid of this + # by allowing to specify backend_to_rel options via conf + tmpl_xid = sanitize_external_id(record.get("xid::product_tmpl_id")) + rec = self.env.ref(tmpl_xid, raise_if_not_found=False) + value = rec.id if rec else None + return {"product_tmpl_id": value} class ProductSupplierinfoRecordHandler(Component): From 6eecc892423851bb49426f3803520e4ec292b08b Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Mon, 15 May 2023 09:41:32 +0200 Subject: [PATCH 06/14] connector_importer_product: add comment for pkg import --- connector_importer_product/data/import_type.xml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/connector_importer_product/data/import_type.xml b/connector_importer_product/data/import_type.xml index d15ad8b26..f4efb3db8 100644 --- a/connector_importer_product/data/import_type.xml +++ b/connector_importer_product/data/import_type.xml @@ -129,6 +129,16 @@ name: product.supplierinfo.mapper source_key_prefix: supplier. + + From b45d9720d777027d0c0c0e9f136ebe4eda2a3bbf Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Mon, 5 Jun 2023 10:25:58 +0200 Subject: [PATCH 07/14] c_importer_product: fix docstring --- .../product_product/record_handler.py | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/connector_importer_product/components/product_product/record_handler.py b/connector_importer_product/components/product_product/record_handler.py index eece6d5fe..d1c4651ae 100644 --- a/connector_importer_product/components/product_product/record_handler.py +++ b/connector_importer_product/components/product_product/record_handler.py @@ -188,21 +188,29 @@ def _update_template_attributes(self, odoo_record, values, orig_values): def _find_attr_value(self, orig_values, attr_column): """Find matching attribute value. - FIXME - By computing their XID w/ the column name - + _value_ + the value itself. + The column name is used to compute the product.attribute xid. Eg: - For instance, a column `product_attr_Size` could have the values - "S" , "M", "L" and they will be converted - to find their matching attributes, like this: + * product_attr_Size -> __setup__.product_attr_Size + * product_attr_Color -> __setup__.product_attr_Color - * S -> product_attr_Size_value_S - * M -> product_attr_Size_value_M - * L -> product_attr_Size_value_L + The value will be used to determine the product.attribute.value. + The lookup happens in this order: - If no attribute value matching this convention is found, - the value will be skipped. + 1. search by name + 2. search by xid, assuming the value itself is already an xid. + 3. search by composed xid, assuming the value is the last part of an xid. + The first part is computed as: `__setup__.$product_attr_xid_value_$col_value`. + For instance, a column `product_attr_Size` could have the values + "S" , "M", "L" and they will be converted + to find their matching attributes, like this: + + * S -> product_attr_Size_value_S + * M -> product_attr_Size_value_M + * L -> product_attr_Size_value_L + + If no attribute value matching this convention is found, + the value will be skipped. """ attr_xid = sanitize_external_id(attr_column) attr = self.env.ref(attr_xid) From d710942b31b7fa5e527291d1021b7d37fcf9d184 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Mon, 5 Jun 2023 12:00:41 +0200 Subject: [PATCH 08/14] c_importer_product: split find_attr --- .../components/product_product/record_handler.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/connector_importer_product/components/product_product/record_handler.py b/connector_importer_product/components/product_product/record_handler.py index d1c4651ae..35f3f843b 100644 --- a/connector_importer_product/components/product_product/record_handler.py +++ b/connector_importer_product/components/product_product/record_handler.py @@ -94,7 +94,8 @@ def _update_template_attributes(self, odoo_record, values, orig_values): for attr_column in attr_columns: if not orig_values[attr_column]: continue - attr_value = self._find_attr_value(orig_values, attr_column) + attr = self._find_attr(attr_column, orig_values) + attr_value = self._find_attr_value(attr, attr_column, orig_values) if attr_value: attr_values_to_import_ids.append(attr_value.id) # Detect if the set of attributes among this template is wrong @@ -184,8 +185,13 @@ def _update_template_attributes(self, odoo_record, values, orig_values): else: odoo_record.product_template_attribute_value_ids = valid_tpl_attr_values + def _find_attr(self, attr_column, orig_values): + """Find matching attribute.""" + attr_xid = sanitize_external_id(attr_column) + return self.env.ref(attr_xid) + # TODO: add unit test - def _find_attr_value(self, orig_values, attr_column): + def _find_attr_value(self, attr, attr_column, orig_values): """Find matching attribute value. @@ -212,8 +218,6 @@ def _find_attr_value(self, orig_values, attr_column): If no attribute value matching this convention is found, the value will be skipped. """ - attr_xid = sanitize_external_id(attr_column) - attr = self.env.ref(attr_xid) # 1st search by name orig_val = orig_values[attr_column] model = self.env["product.attribute.value"] From 8f86ab8a8d3b75a820dd2eb0012b6e98c0691c6f Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Mon, 5 Jun 2023 12:01:02 +0200 Subject: [PATCH 09/14] c_importer_product: add unit tests for record_handler --- connector_importer_product/tests/common.py | 4 +- .../tests/test_record_handler.py | 131 ++++++++++++++++++ 2 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 connector_importer_product/tests/test_record_handler.py diff --git a/connector_importer_product/tests/common.py b/connector_importer_product/tests/common.py index 23d03f8c4..ae99434b6 100644 --- a/connector_importer_product/tests/common.py +++ b/connector_importer_product/tests/common.py @@ -20,8 +20,8 @@ class TestImportProductBase(TransactionComponentCase): def setUpClass(cls): super().setUpClass() cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) - backend = cls.env.ref("connector_importer_product.demo_import_backend") - backend.debug_mode = True # synchronous jobs + cls.backend = cls.env.ref("connector_importer_product.demo_import_backend") + cls.backend.debug_mode = True # synchronous jobs @classmethod def importer_load_file(cls, src_external_id, csv_filename): diff --git a/connector_importer_product/tests/test_record_handler.py b/connector_importer_product/tests/test_record_handler.py new file mode 100644 index 000000000..698d6783d --- /dev/null +++ b/connector_importer_product/tests/test_record_handler.py @@ -0,0 +1,131 @@ +# Copyright 2022 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo.tools import DotDict + +from .common import TestImportProductBase + + +class TestProduct(TestImportProductBase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.prod = cls.env["product.product"].create({"name": "Test prod"}) + cls.env["ir.model.data"].create( + { + "name": "variant_test_1", + "module": "__setup__", + "model": cls.prod._name, + "res_id": cls.prod.id, + "noupdate": False, + } + ) + cls.env["ir.model.data"].create( + { + "name": "tmpl_test_1", + "module": "__setup__", + "model": cls.prod.product_tmpl_id._name, + "res_id": cls.prod.product_tmpl_id.id, + "noupdate": False, + } + ) + cls.prod_attr = cls.env["product.attribute"].create({"name": "Size"}) + cls.prod_attr_imd = cls.env["ir.model.data"].create( + { + "name": "product_attr_Size", + "module": "__setup__", + "model": cls.prod_attr._name, + "res_id": cls.prod_attr.id, + "noupdate": False, + } + ) + cls.prod_attr_value_L = cls.env["product.attribute.value"].create( + {"attribute_id": cls.prod_attr.id, "name": "L"} + ) + cls.prod_attr_value_M = cls.env["product.attribute.value"].create( + { + "attribute_id": cls.prod_attr.id, + "name": "M", + } + ) + # conventional xid + cls.env["ir.model.data"].create( + { + "name": "product_attr_Size_value_M", + "module": "__setup__", + "model": cls.prod_attr_value_M._name, + "res_id": cls.prod_attr_value_M.id, + "noupdate": False, + } + ) + # custom xid + cls.env["ir.model.data"].create( + { + "name": "product_attr_SizeM", + "module": "__setup__", + "model": cls.prod_attr_value_M._name, + "res_id": cls.prod_attr_value_M.id, + "noupdate": False, + } + ) + + def _get_handler(self, options=None): + options = options or DotDict( + {"importer": {}, "mapper": {}, "record_handler": {}} + ) + with self.backend.work_on( + "import.record", + options=options, + ) as work: + return work.component_by_name( + "product.product.handler", model_name="product.product" + ) + + def test_find_attr_not_found(self): + self.prod_attr_imd.unlink() + handler = self._get_handler() + attr_column = "product_attr_Size" + orig_values = {attr_column: "L"} + with self.assertRaisesRegex( + ValueError, + "External ID not found in the system: __setup__.product_attr_Size", + ): + self.assertEqual( + handler._find_attr(attr_column, orig_values).name, "TEST_1" + ) + + def test_find_attr_found(self): + handler = self._get_handler() + attr_column = "product_attr_Size" + orig_values = {attr_column: "L"} + self.assertEqual(handler._find_attr(attr_column, orig_values), self.prod_attr) + + def test_find_attr_value_by_name(self): + handler = self._get_handler() + attr_column = "product_attr_Size" + orig_values = {attr_column: "L"} + self.assertEqual( + handler._find_attr_value(self.prod_attr, attr_column, orig_values), + self.prod_attr_value_L, + ) + + def test_find_attr_value_by_xid_conventional(self): + handler = self._get_handler() + attr_column = "product_attr_Size" + # Value does not match name anymore, but it matched the conventional auto-computed xid + orig_values = {attr_column: "M"} + self.prod_attr_value_M.name = "Medium" + self.assertEqual( + handler._find_attr_value(self.prod_attr, attr_column, orig_values), + self.prod_attr_value_M, + ) + + def test_find_attr_value_by_xid_custom(self): + handler = self._get_handler() + attr_column = "product_attr_Size" + # pass a specific value for xid + orig_values = {attr_column: "product_attr_SizeM"} + self.assertEqual( + handler._find_attr_value(self.prod_attr, attr_column, orig_values), + self.prod_attr_value_M, + ) From 8f0fd7b620e4f89f24bc0e5c2bb9640a85c93c1e Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Mon, 5 Jun 2023 12:32:14 +0200 Subject: [PATCH 10/14] c_importer_product: split handling of tmpl xid --- .../components/product_product/record_handler.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/connector_importer_product/components/product_product/record_handler.py b/connector_importer_product/components/product_product/record_handler.py index 35f3f843b..5ac219f53 100644 --- a/connector_importer_product/components/product_product/record_handler.py +++ b/connector_importer_product/components/product_product/record_handler.py @@ -37,6 +37,16 @@ def odoo_create(self, values, orig_values): odoo_record = super().odoo_create(values, orig_values) # Set the external ID for the template if necessary # TODO: add tests + self._handle_template_xid(odoo_record, values, orig_values) + return odoo_record + + def _handle_template_xid(self, odoo_record, values, orig_values): + """Create the xid for the template if needed. + + The xid for the variant has been already created by `odoo_create`. + If the template is identified via xid using the column `xid::product_tmpl_id` + we must create this reference or other variant lines won't use the same template. + """ if self.must_generate_xmlid and orig_values.get("xid::product_tmpl_id"): tmpl_xid = sanitize_external_id(orig_values.get("xid::product_tmpl_id")) if not self.env.ref(tmpl_xid, raise_if_not_found=False): @@ -50,7 +60,6 @@ def odoo_create(self, values, orig_values): "noupdate": False, } ) - return odoo_record def _update_template_attributes(self, odoo_record, values, orig_values): """Update the 'attribute_line_ids' field of the related template. From aabd12f9776ddfc9c27f0b058799086606716fc5 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Mon, 5 Jun 2023 12:31:41 +0200 Subject: [PATCH 11/14] c_importer_product: create attr value if missing --- .../product_product/record_handler.py | 61 ++++++++++++++++--- connector_importer_product/tests/__init__.py | 1 + .../tests/test_record_handler.py | 47 +++++++++++--- 3 files changed, 90 insertions(+), 19 deletions(-) diff --git a/connector_importer_product/components/product_product/record_handler.py b/connector_importer_product/components/product_product/record_handler.py index 5ac219f53..b902d45b5 100644 --- a/connector_importer_product/components/product_product/record_handler.py +++ b/connector_importer_product/components/product_product/record_handler.py @@ -87,7 +87,7 @@ def _update_template_attributes(self, odoo_record, values, orig_values): * product attribute column values will be used to find the values which were already imported first by name then by XID. - See `_find_attr_value` docs. + See `_find_or_create_attr_value` docs. """ TplAttrLine = self.env["product.template.attribute.line"] TplAttrValue = self.env["product.template.attribute.value"] @@ -104,7 +104,7 @@ def _update_template_attributes(self, odoo_record, values, orig_values): if not orig_values[attr_column]: continue attr = self._find_attr(attr_column, orig_values) - attr_value = self._find_attr_value(attr, attr_column, orig_values) + attr_value = self._find_or_create_attr_value(attr, attr_column, orig_values) if attr_value: attr_values_to_import_ids.append(attr_value.id) # Detect if the set of attributes among this template is wrong @@ -200,10 +200,9 @@ def _find_attr(self, attr_column, orig_values): return self.env.ref(attr_xid) # TODO: add unit test - def _find_attr_value(self, attr, attr_column, orig_values): + def _find_or_create_attr_value(self, attr, attr_column, orig_values): """Find matching attribute value. - The column name is used to compute the product.attribute xid. Eg: * product_attr_Size -> __setup__.product_attr_Size @@ -225,7 +224,17 @@ def _find_attr_value(self, attr, attr_column, orig_values): * L -> product_attr_Size_value_L If no attribute value matching this convention is found, - the value will be skipped. + the value will be skipped unless `create_attribute_value_if_missing` + flag is passed to `record_handler` options. Eg: + + - model: product.product + options: + importer: + odoo_unique_key: barcode + mapper: + name: product.product.mapper + record_handler: + create_attribute_value_if_missing: true """ # 1st search by name orig_val = orig_values[attr_column] @@ -238,10 +247,44 @@ def _find_attr_value(self, attr, attr_column, orig_values): attr_value = self.env.ref(sanitize_external_id(orig_val), False) if not attr_value and "_value_" not in orig_val: # 3rd try w/ auto generated xid - value = slugify_one(orig_val).replace("-", "_") - xid = f"{attr_column}_value_{value}" - attr_value_external_id = sanitize_external_id(xid) - attr_value = self.env.ref(attr_value_external_id, False) + attr_value_xid = self._make_attribute_value_xid(attr_column, orig_val) + attr_value = self.env.ref(attr_value_xid, False) + if not attr_value and self.create_attribute_value_if_missing: + attr_value = self._create_missing_attribute_value( + attr, attr_column, orig_val + ) if not attr_value: logger.error("Cannot determine product attr value: %s", orig_val) return attr_value + + def _make_attribute_value_xid(self, attr_column, orig_val): + value = slugify_one(orig_val).replace("-", "_") + xid = f"{attr_column}_value_{value}" + return sanitize_external_id(xid) + + def _create_missing_attribute_value(self, attr, attr_column, orig_val): + rec = self.env["product.attribute.value"].create( + self._create_missing_attribute_value_values(attr, orig_val) + ) + xid = self._make_attribute_value_xid(attr_column, orig_val) + module, id_ = xid.split(".", 1) + self.env["ir.model.data"].create( + { + "name": id_, + "module": module, + "model": rec._name, + "res_id": rec.id, + "noupdate": False, + } + ) + logger.info("Created product.attribute.value: %s", xid) + return rec + + def _create_missing_attribute_value_values(self, attr, orig_val): + return {"attribute_id": attr.id, "name": orig_val} + + @property + def create_attribute_value_if_missing(self): + return self.work.options.record_handler.get( + "create_attribute_value_if_missing", False + ) diff --git a/connector_importer_product/tests/__init__.py b/connector_importer_product/tests/__init__.py index 5234e94a3..1ec62d987 100644 --- a/connector_importer_product/tests/__init__.py +++ b/connector_importer_product/tests/__init__.py @@ -2,3 +2,4 @@ from . import test_product_category from . import test_product_attribute from . import test_product_packaging +from . import test_record_handler diff --git a/connector_importer_product/tests/test_record_handler.py b/connector_importer_product/tests/test_record_handler.py index 698d6783d..2eff958e1 100644 --- a/connector_importer_product/tests/test_record_handler.py +++ b/connector_importer_product/tests/test_record_handler.py @@ -70,12 +70,10 @@ def setUpClass(cls): ) def _get_handler(self, options=None): - options = options or DotDict( - {"importer": {}, "mapper": {}, "record_handler": {}} - ) + options = options or {"importer": {}, "mapper": {}, "record_handler": {}} with self.backend.work_on( "import.record", - options=options, + options=DotDict(options), ) as work: return work.component_by_name( "product.product.handler", model_name="product.product" @@ -100,32 +98,61 @@ def test_find_attr_found(self): orig_values = {attr_column: "L"} self.assertEqual(handler._find_attr(attr_column, orig_values), self.prod_attr) - def test_find_attr_value_by_name(self): + def test_find_or_create_attr_value_by_name(self): handler = self._get_handler() attr_column = "product_attr_Size" orig_values = {attr_column: "L"} self.assertEqual( - handler._find_attr_value(self.prod_attr, attr_column, orig_values), + handler._find_or_create_attr_value( + self.prod_attr, attr_column, orig_values + ), self.prod_attr_value_L, ) - def test_find_attr_value_by_xid_conventional(self): + def test_find_or_create_attr_value_by_xid_conventional(self): handler = self._get_handler() attr_column = "product_attr_Size" # Value does not match name anymore, but it matched the conventional auto-computed xid orig_values = {attr_column: "M"} self.prod_attr_value_M.name = "Medium" self.assertEqual( - handler._find_attr_value(self.prod_attr, attr_column, orig_values), + handler._find_or_create_attr_value( + self.prod_attr, attr_column, orig_values + ), self.prod_attr_value_M, ) - def test_find_attr_value_by_xid_custom(self): + def test_find_or_create_attr_value_by_xid_custom(self): handler = self._get_handler() attr_column = "product_attr_Size" # pass a specific value for xid orig_values = {attr_column: "product_attr_SizeM"} self.assertEqual( - handler._find_attr_value(self.prod_attr, attr_column, orig_values), + handler._find_or_create_attr_value( + self.prod_attr, attr_column, orig_values + ), self.prod_attr_value_M, ) + + def test_find_or_create_attr_value_create_missing(self): + handler = self._get_handler( + options=dict(record_handler=dict(create_attribute_value_if_missing=True)) + ) + attr_column = "product_attr_Size" + orig_values = {attr_column: "XL"} + self.assertFalse( + self.env.ref( + "__setup__.product_attr_Size_value_XL", raise_if_not_found=False + ) + ) + self.assertEqual( + handler._find_or_create_attr_value( + self.prod_attr, attr_column, orig_values + ).name, + "XL", + ) + self.assertTrue( + self.env.ref( + "__setup__.product_attr_Size_value_XL", raise_if_not_found=False + ) + ) From b81e5aa7096f83b943bac36589c2ef11024ed117 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Mon, 6 Mar 2023 14:31:20 +0100 Subject: [PATCH 12/14] connector_importer_product: migrate to v16 --- .pre-commit-config.yaml | 1 - connector_importer_product/__manifest__.py | 3 +-- .../components/product_supplierinfo.py | 2 +- connector_importer_product/data/import_type.xml | 9 +++++++-- .../odoo/addons/connector_importer_product | 1 + setup/connector_importer_product/setup.py | 6 ++++++ 6 files changed, 16 insertions(+), 6 deletions(-) create mode 120000 setup/connector_importer_product/odoo/addons/connector_importer_product create mode 100644 setup/connector_importer_product/setup.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2495ff05b..9fc27a2ee 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,6 @@ exclude: | (?x) # NOT INSTALLABLE ADDONS - ^connector_importer_product/| # END NOT INSTALLABLE ADDONS # Files and folders generated by bots, to avoid loops ^setup/|/static/description/index\.html$| diff --git a/connector_importer_product/__manifest__.py b/connector_importer_product/__manifest__.py index 289491bef..688b4f61a 100644 --- a/connector_importer_product/__manifest__.py +++ b/connector_importer_product/__manifest__.py @@ -3,7 +3,7 @@ { "name": "Connector Importer Product", "summary": "Ease definition of product imports using `connector_importer`.", - "version": "15.0.1.1.0", + "version": "16.0.1.0.0", "category": "Tools", "website": "https://github.com/OCA/connector-interfaces", "author": "Camptocamp, Odoo Community Association (OCA)", @@ -22,5 +22,4 @@ "demo/import_source.xml", "demo/import_recordset.xml", ], - "installable": False, } diff --git a/connector_importer_product/components/product_supplierinfo.py b/connector_importer_product/components/product_supplierinfo.py index bb3f22655..b094acd46 100644 --- a/connector_importer_product/components/product_supplierinfo.py +++ b/connector_importer_product/components/product_supplierinfo.py @@ -15,7 +15,7 @@ class ProductSupplierinfoMapper(Component): _usage = "importer.mapper" required = { - "__name": "name", + "__name": "partner_id", "__tmpl": "product_tmpl_id", } diff --git a/connector_importer_product/data/import_type.xml b/connector_importer_product/data/import_type.xml index f4efb3db8..bad5b02bb 100644 --- a/connector_importer_product/data/import_type.xml +++ b/connector_importer_product/data/import_type.xml @@ -73,13 +73,16 @@ override_existing: false mapper: name: importer.mapper.dynamic + source_key_whitelist: supplier.name + source_key_rename: + partner_id: name default_keys: supplier_rank: 1 - model: product.supplierinfo options: importer: - odoo_unique_key: name + odoo_unique_key: partner_id mapper: name: product.supplierinfo.mapper @@ -118,13 +121,15 @@ name: importer.mapper.dynamic source_key_prefix: supplier. source_key_whitelist: supplier.name + source_key_rename: + partner_id: name default_keys: supplier_rank: 1 - model: product.supplierinfo options: importer: - odoo_unique_key: name + odoo_unique_key: partner_id mapper: name: product.supplierinfo.mapper source_key_prefix: supplier. diff --git a/setup/connector_importer_product/odoo/addons/connector_importer_product b/setup/connector_importer_product/odoo/addons/connector_importer_product new file mode 120000 index 000000000..bfea37de6 --- /dev/null +++ b/setup/connector_importer_product/odoo/addons/connector_importer_product @@ -0,0 +1 @@ +../../../../connector_importer_product \ No newline at end of file diff --git a/setup/connector_importer_product/setup.py b/setup/connector_importer_product/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/connector_importer_product/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From ec2f9448a99fa7586fbb976bd4f4a0a1238e2d92 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Mon, 16 Oct 2023 11:49:55 +0200 Subject: [PATCH 13/14] connector_importer_product: add oca-port blacklist --- .oca/oca-port/blacklist/connector_importer_product.json | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .oca/oca-port/blacklist/connector_importer_product.json diff --git a/.oca/oca-port/blacklist/connector_importer_product.json b/.oca/oca-port/blacklist/connector_importer_product.json new file mode 100644 index 000000000..aa6eeea3d --- /dev/null +++ b/.oca/oca-port/blacklist/connector_importer_product.json @@ -0,0 +1,8 @@ +{ + "pull_requests": { + "OCA/connector-interfaces#110": "nothing to port", + "OCA/connector-interfaces#116": "nothing to port", + "OCA/connector-interfaces#118": "nothing to port", + "OCA/connector-interfaces#119": "nothing to port" + } +} From 582b0773f937856acddadfd85b90983bb544e84f Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Wed, 4 Sep 2024 12:19:26 +0200 Subject: [PATCH 14/14] connector_importer_product: fix transl string params --- .../product_product/record_handler.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/connector_importer_product/components/product_product/record_handler.py b/connector_importer_product/components/product_product/record_handler.py index b902d45b5..fd706ffe8 100644 --- a/connector_importer_product/components/product_product/record_handler.py +++ b/connector_importer_product/components/product_product/record_handler.py @@ -129,9 +129,12 @@ def _update_template_attributes(self, odoo_record, values, orig_values): if existing_variant and attrs_to_import != existing_attrs: raise ValueError( _( - "Product '{}' has not the same attributes than '{}'. " - "Unable to import it." - ).format(odoo_record.default_code, existing_variant.default_code) + "Product '%(code)s' has not the same attributes " + "than '%(existing_code)s'. " + "Unable to import it.", + code=odoo_record.default_code, + existing_code=existing_variant.default_code, + ) ) # Prepare attributes and attribute values for attr_value in attr_values_to_import: @@ -185,9 +188,12 @@ def _update_template_attributes(self, odoo_record, values, orig_values): if combination_indices and existing_product: raise ValueError( _( - "Product '{}' seems to be a duplicate of '{}' (same attributes). " - "Unable to import it." - ).format(odoo_record.default_code, existing_product.default_code) + "Product '%(code)s' " + "seems to be a duplicate of '%(existing_code)s' (same attributes). " + "Unable to import it.", + code=odoo_record.default_code, + existing_code=existing_variant.default_code, + ) ) # It is required to set the whole template attribute values at the end # (and not in the loop) to not trigger internal mechanisms done by Odoo