Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[14.0][IMP] product_import: Import products asynchronously #1076

Open
wants to merge 2 commits into
base: 14.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions product_import/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@
"stock",
# OCA/edi
"base_business_document_import",
# OCA/queue
"queue_job",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've thought again about this... We should remove this dependency IMO.
In the simplest case we won't need it, in the more complex case we'll probably use the framework. Then, we can leverage the framework to the split.
Additionally, there's now a new feature in queue_job which is called split and allows to.... split items in batches.
See OCA/queue#658

],
"data": [
"security/ir.model.access.csv",
"wizard/product_import_view.xml",
"data/job_function.xml",
],
}
14 changes: 14 additions & 0 deletions product_import/data/job_function.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<odoo noupdate="1">

<record id="channel_product_import" model="queue.job.channel">
<field name="name">product_import</field>
<field name="parent_id" ref="queue_job.channel_root" />
</record>

<record id="job_create_update_products" model="queue.job.function">
<field name="model_id" ref="model_product_import" />
<field name="method">_create_update_products</field>
<field name="channel_id" ref="channel_product_import" />
</record>

</odoo>
7 changes: 4 additions & 3 deletions product_import/tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ class TestCommon(SavepointCase):
def setUpClass(cls):
super().setUpClass()
cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True))
cls.wiz_model = cls.env["product.import"]
# Execute directly, no job
cls.wiz_model = cls.env["product.import"].with_context(queue_job__no_delay=True)
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):
Expand Down
10 changes: 8 additions & 2 deletions product_import/tests/test_product_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,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"]):

Expand Down
83 changes: 59 additions & 24 deletions product_import/wizard/product_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@
import logging
from base64 import b64decode, b64encode
from datetime import date, timedelta
from itertools import zip_longest

from lxml import etree

from odoo import _, api, fields, models
from odoo.exceptions import UserError

CHUNK_SIZE = 40

logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -197,14 +200,46 @@
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 _create_update_products(self, products, seller_id):
"""Create / Update a product.

This method is called from a queue job.
"""

seller = self.env["res.partner"].browse(seller_id)

log_msgs = []
for parsed_product in products:
product_vals = self._prepare_product(
parsed_product, log_msgs, seller=seller
)
if product_vals:
msg = self._create_update_product(product_vals)
log_msgs.append(msg)

return "\n".join(log_msgs)

@api.model
def _create_update_product(self, product_vals):
"""Create / Update a product.

This method is called from a queue job.
"""
chatter_msg = []

product = product_vals.pop("recordset", None)
if product:
supplierinfo = product_vals.pop("seller_ids", ())
supplierinfo_obj = self.env["product.supplierinfo"]

Check warning on line 233 in product_import/wizard/product_import.py

View check run for this annotation

Codecov / codecov/patch

product_import/wizard/product_import.py#L232-L233

Added lines #L232 - L233 were not covered by tests
for (command, line_id, values) in supplierinfo:
if command == 1:
supplierinfo_obj.browse(line_id).write(values)

Check warning on line 236 in product_import/wizard/product_import.py

View check run for this annotation

Codecov / codecov/patch

product_import/wizard/product_import.py#L236

Added line #L236 was not covered by tests
elif command == 0:
supplierinfo_obj.create({**values, "product_id": product.id})

Check warning on line 238 in product_import/wizard/product_import.py

View check run for this annotation

Codecov / codecov/patch

product_import/wizard/product_import.py#L238

Added line #L238 was not covered by tests
else:
raise RuntimeError(f"Command {command} not supported")

Check warning on line 240 in product_import/wizard/product_import.py

View check run for this annotation

Codecov / codecov/patch

product_import/wizard/product_import.py#L240

Added line #L240 was not covered by tests
product.write(product_vals)
logger.info("Product %d updated", product.id)
logger.debug("Product %s updated", product.default_code)

Check warning on line 242 in product_import/wizard/product_import.py

View check run for this annotation

Codecov / codecov/patch

product_import/wizard/product_import.py#L242

Added line #L242 was not covered by tests
else:
product_active = product_vals.pop("active")
product = self.env["product.product"].create(product_vals)
Expand All @@ -213,33 +248,33 @@
# all characteristics into product.template
product.flush()
product.action_archive()
logger.info("Product %d created", product.id)
return product
logger.debug("Product %s created", product.default_code)

@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"],
seller=seller,
)
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
log_msg = f"Product created/updated {product.id}\n" + "\n".join(chatter_msg)
return log_msg

def import_button(self):
def import_button(self, chunk_size=CHUNK_SIZE):
self.ensure_one()
file_content = b64decode(self.product_file)
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)
# Create products asynchronously
iterators = [iter(catalogue["products"])] * chunk_size
for products in zip_longest(*iterators):
if products[-1] is None:
products = [product for product in products if product]
# One job for x products (chunk of 40)
wiz.with_delay()._create_update_products(products, seller.id)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here I would simply call wiz.create_update_products that will call _create_update_products which can be overridden or you can rely on https://github.com/OCA/queue/blob/16.0/queue_job/models/base.py#L145

# 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"}
8 changes: 7 additions & 1 deletion product_import_ubl/tests/test_ubl_catalogue_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@ class TestUblOrderImport(SavepointCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True))
cls.env = cls.env(
context={
**cls.env.context,
"tracking_disable": True,
"queue_job__no_delay": True,
}
)
cls.supplier = cls.env["res.partner"].create(
{"name": "Medical", "ref": "78456123"}
)
Expand Down
Loading