From 0254d7d86f9f26dcb7e714e464b748f4d7d1a851 Mon Sep 17 00:00:00 2001 From: Florent Xicluna Date: Mon, 27 Jan 2025 23:44:59 +0100 Subject: [PATCH 1/3] [IMP] product_import: Reorganize for inheritance to import by batch --- product_import/tests/common.py | 4 +- product_import/tests/test_product_import.py | 13 ++++- product_import/wizard/product_import.py | 65 ++++++++++++++------- 3 files changed, 57 insertions(+), 25 deletions(-) diff --git a/product_import/tests/common.py b/product_import/tests/common.py index dac4cb1b48..404df803b8 100644 --- a/product_import/tests/common.py +++ b/product_import/tests/common.py @@ -14,8 +14,8 @@ def setUpClass(cls): cls.wiz_model = cls.env["product.import"] cls.supplier = cls.env["res.partner"].create({"name": "Catalogue Vendor"}) - def _mock(self, method_name): - return mock.patch.object(type(self.wiz_model), method_name) + def _mock(self, method_name, **kw): + return mock.patch.object(type(self.wiz_model), method_name, **kw) @property def wiz_form(self): diff --git a/product_import/tests/test_product_import.py b/product_import/tests/test_product_import.py index 953850796b..5cc227aa8f 100644 --- a/product_import/tests/test_product_import.py +++ b/product_import/tests/test_product_import.py @@ -50,6 +50,7 @@ }, ], "ref": "1387", + "company": {"name": "Customer ABC"}, "seller": { "contact": False, "email": False, @@ -88,9 +89,15 @@ def test_get_company_id(self): def test_product_import(self): # product.product - products = self.wiz_model._create_products( - self.parsed_catalog, seller=self.supplier + product_obj = self.env["product.product"].with_context(active_test=False) + existing = product_obj.search([], order="id") + + wiz = self.wiz_model.create( + {"product_file": b"====", "product_filename": "test_import.xml"} ) + with self._mock("parse_product_catalogue", return_value=self.parsed_catalog): + wiz.import_button() + products = product_obj.search([], order="id") - existing self.assertEqual(len(products), 3) for product, parsed in zip(products, PARSED_CATALOG["products"]): @@ -140,7 +147,7 @@ def test_product_import(self): self.assertEqual(product.seller_ids, product_tmpl.seller_ids) self.assertEqual( product.seller_ids.mapped("delay")[0], parsed.get("sale_delay", 0) - ), + ) def test_import_button(self): form = self.wiz_form diff --git a/product_import/wizard/product_import.py b/product_import/wizard/product_import.py index cbf1c13002..7102a185c2 100644 --- a/product_import/wizard/product_import.py +++ b/product_import/wizard/product_import.py @@ -150,7 +150,7 @@ def _prepare_supplierinfo(self, seller_info, product): return result @api.model - def _prepare_product(self, parsed_product, chatter_msg, seller=None): + def _prepare_product(self, parsed_product, seller, chatter_msg): # Important: barcode is unique key of product.template model # So records product.product are created with company_id=False. # Only the pricelist (product.supplierinfo) is company-specific. @@ -197,14 +197,12 @@ def _prepare_product(self, parsed_product, chatter_msg, seller=None): return product_vals @api.model - def create_product(self, parsed_product, chatter_msg, seller=None): - product_vals = self._prepare_product(parsed_product, chatter_msg, seller=seller) - if not product_vals: - return False + def _save_product(self, product_vals, chatter_msg): + """Create / Update a product.""" product = product_vals.pop("recordset", None) if product: product.write(product_vals) - logger.info("Product %d updated", product.id) + logger.debug("Product %s updated", product.default_code) else: product_active = product_vals.pop("active") product = self.env["product.product"].create(product_vals) @@ -213,33 +211,60 @@ def create_product(self, parsed_product, chatter_msg, seller=None): # all characteristics into product.template product.flush() product.action_archive() - logger.info("Product %d created", product.id) + logger.debug("Product %s created", product.default_code) + return product @api.model - def _create_products(self, catalogue, seller, filename=None): - products = self.env["product.product"].browse() - for product in catalogue.get("products"): - record = self.create_product( - product, - catalogue["chatter_msg"], + def _create_update_products(self, products, seller_id, chatter_msg): + """Create / Update all products.""" + seller = self.env["res.partner"].browse(seller_id) + + for parsed_product in products: + product_vals = self._prepare_product( + parsed_product, seller=seller, + chatter_msg=chatter_msg, ) - if record: - products |= record - self._bdimport.post_create_or_update(catalogue, seller, doc_filename=filename) - logger.info("Products updated for vendor %d", seller.id) - return products + if product_vals: + product = self._save_product(product_vals, chatter_msg=chatter_msg) + chatter_msg.append( + f"Product created/updated {product.default_code} ({product.id})" + ) + return True + + @api.model + def create_update_products(self, products, seller_id, chatter_msg): + """Create / Update a product. + + This method can be overriden, for example to import asynchronously with queue_job. + """ + return self._create_update_products( + products, seller_id, chatter_msg=chatter_msg + ) def import_button(self): self.ensure_one() file_content = b64decode(self.product_file) + # 1st step: Parse the (UBL) document --> get a "catalogue" dictionary catalogue = self.parse_product_catalogue(file_content, self.product_filename) if not catalogue.get("products"): raise UserError(_("This catalogue doesn't have any product!")) company_id = self._get_company_id(catalogue) seller = self._get_seller(catalogue) - self.with_context(product_company_id=company_id)._create_products( - catalogue, seller, filename=self.product_filename + wiz = self.with_context(product_company_id=company_id) + # 2nd step: Prepare values and create the "product.product" records in Odoo + wiz.create_update_products( + catalogue["products"], + seller.id, + chatter_msg=catalogue["chatter_msg"], + ) + # Save imported file as attachment + self._bdimport.post_create_or_update( + catalogue, seller, doc_filename=self.product_filename ) + logger.info( + "Update for vendor %s: %d products", seller.name, len(catalogue["products"]) + ) + return {"type": "ir.actions.act_window_close"} From eab0720b2087102ab557daf996d75cc2b74c4735 Mon Sep 17 00:00:00 2001 From: Florent Xicluna Date: Tue, 28 Jan 2025 00:05:04 +0100 Subject: [PATCH 2/3] [REF] product_import: do not use the env.context to configure import --- product_import/wizard/product_import.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/product_import/wizard/product_import.py b/product_import/wizard/product_import.py index 7102a185c2..82fae5661a 100644 --- a/product_import/wizard/product_import.py +++ b/product_import/wizard/product_import.py @@ -150,11 +150,10 @@ def _prepare_supplierinfo(self, seller_info, product): return result @api.model - def _prepare_product(self, parsed_product, seller, chatter_msg): + def _prepare_product(self, parsed_product, seller, company_id, chatter_msg): # Important: barcode is unique key of product.template model # So records product.product are created with company_id=False. # Only the pricelist (product.supplierinfo) is company-specific. - product_company_id = self.env.context.get("product_company_id", False) if not parsed_product["barcode"]: chatter_msg.append( _("Cannot import product without barcode: %s") % (parsed_product,) @@ -187,7 +186,7 @@ def _prepare_product(self, parsed_product, seller, chatter_msg): "price": parsed_product["price"], "currency_id": currency.id, "min_qty": parsed_product["min_qty"], - "company_id": product_company_id, + "company_id": company_id, "delay": parsed_product.get("sale_delay", 0), } product_vals["seller_ids"] = self._prepare_supplierinfo(seller_info, product) @@ -216,7 +215,7 @@ def _save_product(self, product_vals, chatter_msg): return product @api.model - def _create_update_products(self, products, seller_id, chatter_msg): + def _create_update_products(self, products, seller_id, company_id, chatter_msg): """Create / Update all products.""" seller = self.env["res.partner"].browse(seller_id) @@ -224,6 +223,7 @@ def _create_update_products(self, products, seller_id, chatter_msg): product_vals = self._prepare_product( parsed_product, seller=seller, + company_id=company_id, chatter_msg=chatter_msg, ) if product_vals: @@ -234,13 +234,13 @@ def _create_update_products(self, products, seller_id, chatter_msg): return True @api.model - def create_update_products(self, products, seller_id, chatter_msg): + def create_update_products(self, products, seller_id, company_id, chatter_msg): """Create / Update a product. This method can be overriden, for example to import asynchronously with queue_job. """ return self._create_update_products( - products, seller_id, chatter_msg=chatter_msg + products, seller_id, company_id, chatter_msg=chatter_msg ) def import_button(self): @@ -252,11 +252,11 @@ def import_button(self): raise UserError(_("This catalogue doesn't have any product!")) company_id = self._get_company_id(catalogue) seller = self._get_seller(catalogue) - wiz = self.with_context(product_company_id=company_id) # 2nd step: Prepare values and create the "product.product" records in Odoo - wiz.create_update_products( + self.create_update_products( catalogue["products"], seller.id, + company_id, chatter_msg=catalogue["chatter_msg"], ) # Save imported file as attachment From f23337c1acccd79a7218d43ccce4ae1c4179ebe1 Mon Sep 17 00:00:00 2001 From: Florent Xicluna Date: Mon, 24 Feb 2025 12:57:17 +0100 Subject: [PATCH 3/3] [FIX] product_import: use update() instead of write() It is needed in order to have a safe recomputation of fields of other models. There was an issue in a third-party add-on which has a field recomputed when a new 'product.supplierinfo' is added to 'seller_ids' of the product. --- product_import/wizard/product_import.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/product_import/wizard/product_import.py b/product_import/wizard/product_import.py index 82fae5661a..9cf6eae1b1 100644 --- a/product_import/wizard/product_import.py +++ b/product_import/wizard/product_import.py @@ -200,7 +200,7 @@ def _save_product(self, product_vals, chatter_msg): """Create / Update a product.""" product = product_vals.pop("recordset", None) if product: - product.write(product_vals) + product.update(product_vals) logger.debug("Product %s updated", product.default_code) else: product_active = product_vals.pop("active")