Skip to content

Commit

Permalink
shopinvader: refactor and improve binding wiz
Browse files Browse the repository at this point in the history
* delegate to backend
* collect records in a job
* create one job per template

This way, we get:

* no frozen instance if tons of records to be created
* if a product fails it doesn't make other fail
  • Loading branch information
simahawk committed Jun 4, 2021
1 parent e452b34 commit 1bb5c42
Show file tree
Hide file tree
Showing 9 changed files with 148 additions and 69 deletions.
4 changes: 4 additions & 0 deletions shopinvader/data/queue_job_channel_data.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,8 @@
<field name="name">notification</field>
<field name="parent_id" ref="channel_shopinvader" />
</record>
<record model="queue.job.channel" id="channel_shopinvader_bind_products">
<field name="name">bind_products</field>
<field name="parent_id" ref="channel_shopinvader" />
</record>
</odoo>
16 changes: 16 additions & 0 deletions shopinvader/data/queue_job_function_data.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,20 @@
<field name="method">send</field>
<field name="channel_id" ref="channel_shopinvader_notification" />
</record>
<record
id="job_function_shopinvader_bind_selected_products"
model="queue.job.function"
>
<field name="model_id" ref="model_shopinvader_backend" />
<field name="method">bind_selected_products</field>
<field name="channel_id" ref="channel_shopinvader_bind_products" />
</record>
<record
id="job_function_shopinvader_bind_single_product"
model="queue.job.function"
>
<field name="model_id" ref="model_shopinvader_backend" />
<field name="method">bind_single_product</field>
<field name="channel_id" ref="channel_shopinvader_bind_products" />
</record>
</odoo>
90 changes: 90 additions & 0 deletions shopinvader/models/shopinvader_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
# @author Simone Orsi <[email protected]>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from collections import defaultdict
from contextlib import contextmanager

from odoo import _, api, fields, models, tools
Expand Down Expand Up @@ -408,6 +409,95 @@ def bind_all_category(self, domain=None):
# TODO: we should exclude levels from `category_binding_level` as well
self._bind_all_content("product.category", "shopinvader.category", domain)

def bind_selected_products(self, products, langs=None, run_immediately=False):
"""Bind given product variants.
:param products: product.product recordset
:param langs: res.lang recordset. If none, all langs from backend
:param run_immediately: do not use jobs
"""
for backend in self:
langs = langs or backend.lang_ids
grouped_by_template = defaultdict(self.env["product.product"].browse)
for rec in products:
grouped_by_template[rec.product_tmpl_id] |= rec
method = backend.with_delay().bind_single_product
if run_immediately:
method = backend.bind_single_product
for tmpl, variants in grouped_by_template.items():
method(langs, tmpl, variants)

def bind_single_product(self, langs, product_tmpl, variants):
"""Bind given product variants for given template and languages.
:param langs: res.lang recordset
:param product_tmpl: product.template browse record
:param variants: product.product recordset
:param run_immediately: do not use jobs
"""
self.ensure_one()
shopinvader_products = self._get_or_create_shopinvader_products(
langs, product_tmpl
)
for shopinvader_product in shopinvader_products:
self._get_or_create_shopinvader_variants(shopinvader_product, variants)
self.auto_bind_categories()

def _get_or_create_shopinvader_products(self, langs, product_tmpl):
"""Get template bindings for given languages or create if missing.
:param langs: res.lang recordset
:param product_tmpl: product.template browse record
"""
binding_model = self.env["shopinvader.product"].with_context(active_test=False)
bound_templates = binding_model.search(
[
("record_id", "=", product_tmpl.id),
("backend_id", "=", self.id),
("lang_id", "in", langs.ids),
]
)
for lang in langs:
shopinvader_product = bound_templates.filtered(lambda x: x.lang_id == lang)
if not shopinvader_product:
# fmt: off
data = {
"record_id": product_tmpl.id,
"backend_id": self.id,
"lang_id": lang.id,
}
# fmt: on
bound_templates |= binding_model.create(data)
elif not shopinvader_product.active:
shopinvader_product.write({"active": True})
return bound_templates

def _get_or_create_shopinvader_variants(self, shopinvader_product, variants):
"""Get variant bindings, create if missing.
:param langs: res.lang recordset
:param product_tmpl: product.template browse record
"""
binding_model = self.env["shopinvader.variant"]
bound_variants = shopinvader_product.shopinvader_variant_ids
for variant in variants:
shopinvader_variant = bound_variants.filtered(
lambda p: p.record_id == variant
)
if not shopinvader_variant:
# fmt: off
data = {
"record_id": variant.id,
"backend_id": self.id,
"shopinvader_product_id":
shopinvader_product.id,
}
# fmt: on
bound_variants |= binding_model.create(data)
elif not shopinvader_variant.active:
shopinvader_variant.write({"active": True})
return bound_variants

def _send_notification(self, notification, record):
self.ensure_one()
record.ensure_one()
Expand Down
6 changes: 5 additions & 1 deletion shopinvader/tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@ def _bind_products(self, products, backend=None):
backend = backend or self.backend
bind_wizard_model = self.env["shopinvader.variant.binding.wizard"]
bind_wizard = bind_wizard_model.create(
{"backend_id": backend.id, "product_ids": [(6, 0, products.ids)]}
{
"backend_id": backend.id,
"product_ids": [(6, 0, products.ids)],
"run_immediately": True,
}
)
bind_wizard.bind_products()

Expand Down
2 changes: 2 additions & 0 deletions shopinvader/tests/test_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ class BackendCase(CommonCase):
def setUpClass(cls):
super(BackendCase, cls).setUpClass()
cls.lang_fr = cls._install_lang(cls, "base.lang_fr")
cls.env = cls.env(context=dict(cls.env.context, test_queue_job_no_delay=True))
cls.backend = cls.backend.with_context(test_queue_job_no_delay=True)

def _all_products_count(self):
return self.env["product.template"].search_count([("sale_ok", "=", True)])
Expand Down
6 changes: 6 additions & 0 deletions shopinvader/tests/test_product.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@


class ProductCase(ProductCommonCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env = cls.env(context=dict(cls.env.context, test_queue_job_no_delay=True))
cls.backend = cls.backend.with_context(test_queue_job_no_delay=True)

def test_create_shopinvader_variant(self):
self.assertEqual(
len(self.template.product_variant_ids),
Expand Down
8 changes: 7 additions & 1 deletion shopinvader/tests/test_shopinvader_variant_binding_wizard.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@ class TestShopinvaderVariantBindingWizard(SavepointComponentCase):
@classmethod
def setUpClass(cls):
super(TestShopinvaderVariantBindingWizard, cls).setUpClass()
cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True))
cls.env = cls.env(
context=dict(
cls.env.context,
tracking_disable=True,
test_queue_job_no_delay=True,
)
)
cls.backend = cls.env.ref("shopinvader.backend_1")
cls.template = cls.env.ref("product.product_product_4_product_template")
cls.variant = cls.env.ref("product.product_product_4b")
Expand Down
84 changes: 17 additions & 67 deletions shopinvader/wizards/shopinvader_variant_binding_wizard.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Copyright 2017 ACSONE SA/NV
# Copyright 2021 Camptocamp (http://www.camptocamp.com).
# @author Simone Orsi <[email protected]>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from collections import defaultdict

from odoo import api, fields, models


Expand All @@ -27,6 +27,7 @@ class ShopinvaderVariantBindingWizard(models.TransientModel):
help="List of langs for which a binding must exists. If not "
"specified, the list of langs defined on the backend is used.",
)
run_immediately = fields.Boolean(help="Do not schedule jobs.")

@api.model
def default_get(self, fields_list):
Expand All @@ -38,77 +39,26 @@ def default_get(self, fields_list):
res["lang_ids"] = [(6, None, backend.lang_ids.ids)]
return res

def _get_langs_to_bind(self):
self.ensure_one()
return self.lang_ids or self.backend_id.lang_ids

def _get_bound_templates(self):
"""
return a dict of bound shopinvader.product by product template id
:return:
"""
self.ensure_one()
binding = self.env["shopinvader.product"]
product_template_ids = self.mapped("product_ids.product_tmpl_id")
bound_templates = binding.with_context(active_test=False).search(
[
("record_id", "in", product_template_ids.ids),
("backend_id", "=", self.backend_id.id),
("lang_id", "in", self._get_langs_to_bind().ids),
]
)
ret = defaultdict(dict)
for bt in bound_templates:
ret[bt.record_id][bt.lang_id] = bt
for product in self.mapped("product_ids.product_tmpl_id"):
product_tmpl_id = product
bind_records = ret.get(product_tmpl_id)
for lang_id in self._get_langs_to_bind():
bind_record = bind_records and bind_records.get(lang_id)
if not bind_record:
data = {
"record_id": product.id,
"backend_id": self.backend_id.id,
"lang_id": lang_id.id,
}
ret[product_tmpl_id][lang_id] = binding.create(data)
elif not bind_record.active:
bind_record.write({"active": True})
return ret

def bind_products(self):
for wizard in self:
bound_templates = wizard._get_bound_templates()
binding = self.env["shopinvader.variant"]
for product in wizard.product_ids:
bound_products = bound_templates[product.product_tmpl_id]
for lang_id in wizard._get_langs_to_bind():
for shopinvader_product in bound_products[lang_id]:
bound_variants = shopinvader_product.shopinvader_variant_ids
bind_record = bound_variants.filtered(
lambda p: p.record_id == product
)
if not bind_record:
# fmt: off
data = {
"record_id": product.id,
"backend_id": wizard.backend_id.id,
"shopinvader_product_id":
shopinvader_product.id,
}
# fmt: on
binding.create(data)
elif not bind_record.active:
bind_record.write({"active": True})
wizard.backend_id.auto_bind_categories()
backend = wizard.backend_id
method = backend.with_delay().bind_selected_products
if wizard.run_immediately:
method = backend.bind_selected_products
method(
wizard.product_ids,
langs=wizard.lang_ids,
run_immediately=wizard.run_immediately,
)

@api.model
def bind_langs(self, backend, lang_ids):
"""
Ensure that a shopinvader.variant exists for each lang_id. If not,
create a new binding for the missing lang. This method is usefull
"""Ensure that a shopinvader.variant exists for each lang_id.
If not, create a new binding for the missing lang. This method is useful
to ensure that when a lang is added to a backend, all the binding
for this lang are created for the existing bound products
for this lang are created for the existing bound products.
:param backend: backend record
:param lang_ids: list of lang ids we must ensure that a binding exists
:return:
Expand Down
1 change: 1 addition & 0 deletions shopinvader/wizards/shopinvader_variant_binding_wizard.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<form string="Shopinvader Variant Binding Wizard">
<field name='backend_id' />
<field name='product_ids' />
<field name='run_immediately' />
<footer>
<button
string="Bind Products"
Expand Down

0 comments on commit 1bb5c42

Please sign in to comment.