Skip to content

Commit

Permalink
[IMP] product_template_multi_link: Makes links bi-directional
Browse files Browse the repository at this point in the history
fixes OCA#307
  • Loading branch information
lmignon authored and Pablocce committed Apr 22, 2024
1 parent 62a3b9b commit ccc760e
Show file tree
Hide file tree
Showing 15 changed files with 813 additions and 78 deletions.
8 changes: 7 additions & 1 deletion product_template_multi_link/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,18 @@
"license": "AGPL-3",
"depends": ["sale"],
"data": [
"security/product_template_link_type.xml",
"views/product_template_link_type.xml",
"security/ir.model.access.csv",
"views/action.xml",
"views/product_template_view.xml",
"views/product_template_link_view.xml",
"views/menu.xml",
],
"demo": ["demo/product_template_link.xml"],
"demo": [
"data/product_template_link_type.xml",
"demo/product_template_link_type.xml",
"demo/product_template_link.xml",
],
"installable": True,
}
19 changes: 19 additions & 0 deletions product_template_multi_link/data/product_template_link_type.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2019 ACSONE SA/NV
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo noupdate="1">
<record
model="product.template.link.type"
id="product_template_link_type_cross_selling"
>
<field name="name">Cross Selling</field>
<field name="code">cross-selling</field>
</record>
<record
model="product.template.link.type"
id="product_template_link_type_up_selling"
>
<field name="name">Up Selling</field>
<field name="code">up-selling</field>
</record>
</odoo>
18 changes: 5 additions & 13 deletions product_template_multi_link/demo/product_template_link.xml
Original file line number Diff line number Diff line change
@@ -1,25 +1,17 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="link_apple_1" model="product.template.link">
<record id="link_cross_selling_1" model="product.template.link">
<field
name="product_template_id"
name="left_product_tmpl_id"
ref="product.product_product_7_product_template"
/>
<field
name="linked_product_template_id"
name="right_product_tmpl_id"
ref="product.product_product_9_product_template"
/>
<field name="link_type">cross_sell</field>
</record>
<record id="link_apple_2" model="product.template.link">
<field
name="product_template_id"
ref="product.product_product_9_product_template"
/>
<field
name="linked_product_template_id"
ref="product.product_product_7_product_template"
name="type_id"
ref="product_template_multi_link.product_template_link_type_cross_selling"
/>
<field name="link_type">cross_sell</field>
</record>
</odoo>
15 changes: 15 additions & 0 deletions product_template_multi_link/demo/product_template_link_type.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2019 ACSONE SA/NV
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<record
model="product.template.link.type"
id="product_template_link_type_demo_range"
>
<field name="is_symmetric" eval="0" />
<field name="name">Upper Range</field>
<field name="inverse_name">Lower Range</field>
<field name="code">upper-range</field>
<field name="inverse_code">lower-range</field>
</record>
</odoo>
1 change: 1 addition & 0 deletions product_template_multi_link/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
from . import product_template
from . import product_template_link
from . import product_template_link_type
91 changes: 82 additions & 9 deletions product_template_multi_link/models/product_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
# @author Sylvain LE GAL <https://twitter.com/legalsylvain>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from odoo import api, fields, models
from collections import defaultdict

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


class ProductTemplate(models.Model):
Expand All @@ -11,16 +14,86 @@ class ProductTemplate(models.Model):
product_template_link_ids = fields.One2many(
string="Product Links",
comodel_name="product.template.link",
inverse_name="product_template_id",
compute="_compute_product_link_ids",
)

product_template_link_qty = fields.Integer(
string="Product Links Quantity",
compute="_compute_product_template_link_qty",
store=True,
product_template_link_count = fields.Integer(
string="Product Links Count", compute="_compute_product_template_link_count"
)

def _compute_product_link_ids(self):
link_model = self.env["product.template.link"]
domain = [
"|",
("left_product_tmpl_id", "in", self.ids),
("right_product_tmpl_id", "in", self.ids),
]
links = link_model.search(domain)
links_by_product_id = defaultdict(set)
for link in links:
links_by_product_id[link.left_product_tmpl_id.id].add(link.id)
links_by_product_id[link.right_product_tmpl_id.id].add(link.id)
for record in self:
record.product_template_link_ids = self.env["product.template.link"].browse(
list(links_by_product_id[record.id])
)

@api.depends("product_template_link_ids")
def _compute_product_template_link_qty(self):
for template in self:
template.product_template_link_qty = len(template.product_template_link_ids)
def _compute_product_template_link_count(self):
link_model = self.env["product.template.link"]
# Set product_template_link_qty to 0 if user has no access on the model
try:
link_model.check_access_rights("read")
except AccessError:
for rec in self:
rec.product_template_link_count = 0
return

domain = [
"|",
("left_product_tmpl_id", "in", self.ids),
("right_product_tmpl_id", "in", self.ids),
]

res_1 = link_model.read_group(
domain=domain,
fields=["left_product_tmpl_id"],
groupby=["left_product_tmpl_id"],
)
res_2 = link_model.read_group(
domain=domain,
fields=["right_product_tmpl_id"],
groupby=["right_product_tmpl_id"],
)

link_dict = {}
for dic in res_1:
link_id = dic["left_product_tmpl_id"][0]
link_dict.setdefault(link_id, 0)
link_dict[link_id] += dic["left_product_tmpl_id_count"]
for dic in res_2:
link_id = dic["right_product_tmpl_id"][0]
link_dict.setdefault(link_id, 0)
link_dict[link_id] += dic["right_product_tmpl_id_count"]

for rec in self:
rec.product_template_link_count = link_dict.get(rec.id, 0)

def show_product_template_links(self):
self.ensure_one()

return {
"name": _("Product links"),
"type": "ir.actions.act_window",
"view_mode": "tree,form",
"res_model": "product.template.link",
"domain": [
"|",
("left_product_tmpl_id", "=", self.id),
("right_product_tmpl_id", "=", self.id),
],
"context": {
"search_default_groupby_type": True,
"default_left_product_tmpl_id": self.id,
},
}
137 changes: 117 additions & 20 deletions product_template_multi_link/models/product_template_link.py
Original file line number Diff line number Diff line change
@@ -1,46 +1,143 @@
# Copyright 2017-Today GRAP (http://www.grap.coop).
# @author Sylvain LE GAL <https://twitter.com/legalsylvain>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from contextlib import contextmanager

from odoo import fields, models
from psycopg2.extensions import AsIs

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


class ProductTemplateLink(models.Model):
_name = "product.template.link"
_order = "product_template_id, linked_product_template_id"
_order = "left_product_tmpl_id, right_product_tmpl_id"
_description = "Product link"

_LINK_TYPE_SELECTION = [("cross_sell", "Cross-Sell"), ("up_sell", "Up-Sell")]

product_template_id = fields.Many2one(
left_product_tmpl_id = fields.Many2one(
string="Source Product",
comodel_name="product.template",
required=True,
ondelete="cascade",
)

linked_product_template_id = fields.Many2one(
right_product_tmpl_id = fields.Many2one(
string="Linked Product",
comodel_name="product.template",
required=True,
ondelete="cascade",
)

link_type = fields.Selection(
string="Link Type",
selection=_LINK_TYPE_SELECTION,
type_id = fields.Many2one(
string="Link type",
comodel_name="product.template.link.type",
required=True,
default="cross_sell",
help="* Cross-Sell : suggest your customer to"
" purchase an additional product\n"
"* Up-Sell : suggest your customer to purchase a higher-end product,"
" an upgrade, etc.",
ondelete="restrict",
)

sql_constraints = [
(
"template_link_uniq",
"unique (product_template_id, linked_product_template_id, link_type)",
"The products and the link type combination must be unique",
link_type_name = fields.Char(related="type_id.name") # left to right
link_type_inverse_name = fields.Char(
related="type_id.inverse_name"
) # right to left

@api.constrains("left_product_tmpl_id", "right_product_tmpl_id", "type_id")
def _check_products(self):
"""
This method checks whether:
- the two products are different
- there is only one link between the same two templates for the same type
:raise: ValidationError if not ok
"""
self.flush() # flush required since the method uses plain sql
if any(rec.left_product_tmpl_id == rec.right_product_tmpl_id for rec in self):
raise ValidationError(
_("You can only create a link between 2 different products")
)

products = self.mapped("left_product_tmpl_id") + self.mapped(
"right_product_tmpl_id"
)
]
self.env.cr.execute(
"""
SELECT
id,
l2.duplicate or l3.duplicate
FROM (
SELECT
id,
left_product_tmpl_id,
right_product_tmpl_id,
type_id
FROM
%s
WHERE
left_product_tmpl_id in %s
AND right_product_tmpl_id in %s
) as l1
LEFT JOIN LATERAL (
SELECT
TRUE as duplicate
FROM
%s
WHERE
right_product_tmpl_id = l1.left_product_tmpl_id
AND left_product_tmpl_id = l1.right_product_tmpl_id
AND type_id = l1.type_id
) l2 ON TRUE
LEFT JOIN LATERAL (
SELECT
TRUE as duplicate
FROM
%s
WHERE
left_product_tmpl_id = l1.left_product_tmpl_id
AND right_product_tmpl_id = l1.right_product_tmpl_id
AND type_id = l1.type_id
AND id != l1.id
) l3 ON TRUE
""",
(
AsIs(self._table),
tuple(products.ids),
tuple(products.ids),
AsIs(self._table),
AsIs(self._table),
),
)
res = self.env.cr.fetchall()
is_duplicate_by_link_id = dict(res)
if True in is_duplicate_by_link_id.values():
ids = [k for k, v in is_duplicate_by_link_id.items() if v]
links = self.browse(ids)
descrs = []
for l in links:
descrs.append(
u"{} <-> {} / {} <-> {}".format(
l.left_product_tmpl_id.name,
l.link_type_name,
l.link_type_inverse_name,
l.right_product_tmpl_id.name,
)
)
links = "\n ".join(descrs)
raise ValidationError(
_(
"Only one link with the same type is allowed between 2 "
"products. \n %s"
)
% links
)

@contextmanager
def _invalidate_links_on_product_template(self):
yield
self.env["product.template"].invalidate_cache(["product_template_link_ids"])

@api.model
def create(self, vals_list):
with self._invalidate_links_on_product_template():
return super().create(vals_list)

def write(self, vals):
with self._invalidate_links_on_product_template():
return super().write(vals)
Loading

0 comments on commit ccc760e

Please sign in to comment.