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 committed Nov 15, 2019
1 parent 88f17fc commit 87f0388
Show file tree
Hide file tree
Showing 17 changed files with 695 additions and 65 deletions.
2 changes: 1 addition & 1 deletion .isort.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ line_length=88
known_odoo=odoo
known_odoo_addons=odoo.addons
sections=FUTURE,STDLIB,THIRDPARTY,ODOO,ODOO_ADDONS,FIRSTPARTY,LOCALFOLDER
known_third_party=setuptools
known_third_party=psycopg2,setuptools
9 changes: 8 additions & 1 deletion product_template_multi_link/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,19 @@
"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,
"external_dependencies": {"python": ["python-slugify"]},
}
15 changes: 15 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,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 noupdate="1">

<record model="product.template.link.type" id="product_template_link_type_cross_selling">
<field name="name">Cross Selling</field>
</record>

<record model="product.template.link.type" id="product_template_link_type_up_selling">
<field name="name">Up Selling</field>
</record>

</odoo>
14 changes: 4 additions & 10 deletions product_template_multi_link/demo/product_template_link.xml
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>

<record id="link_apple_1" model="product.template.link">
<field name="product_template_id" ref="product.product_product_7_product_template"/>
<field name="linked_product_template_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"/>
<field name="link_type">cross_sell</field>
<record id="link_cross_selling_1" model="product.template.link">
<field name="product_tmpl_id_left" ref="product.product_product_7_product_template"/>
<field name="product_tmpl_id_right" ref="product.product_product_9_product_template"/>
<field name="type_id" ref="product_template_multi_link.product_template_link_type_cross_selling"/>
</record>

</odoo>
20 changes: 20 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,20 @@
<?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_cross_selling">
<field name="name">Cross Selling</field>
</record>

<record model="product.template.link.type" id="product_template_link_type_demo_up_selling">
<field name="name">Up Selling</field>
</record>

<record model="product.template.link.type" id="product_template_link_type_demo_range">
<field name="name">Upper Range</field>
<field name="inverse_name">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
86 changes: 77 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,81 @@ 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 = [
"|",
("product_tmpl_id_left", "in", self.ids),
("product_tmpl_id_right", "in", self.ids),
]
links = link_model.search(domain)
links_by_product_id = defaultdict(link_model.browse)
for link in links:
links_by_product_id[link.product_tmpl_id_left.id] |= link
links_by_product_id[link.product_tmpl_id_right.id] |= link
for record in self:
record.product_template_link_ids = 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 = [
"|",
("product_tmpl_id_left", "in", self.ids),
("product_tmpl_id_right", "in", self.ids),
]

res_1 = link_model.read_group(
domain=domain,
fields=["product_tmpl_id_left"],
groupby=["product_tmpl_id_left"],
)
res_2 = link_model.read_group(
domain=domain,
fields=["product_tmpl_id_right"],
groupby=["product_tmpl_id_right"],
)

link_dict = {}
for dic in res_1:
link_id = dic["product_tmpl_id_left"][0]
link_dict.setdefault(link_id, 0)
link_dict[link_id] += dic["product_tmpl_id_left_count"]
for dic in res_2:
link_id = dic["product_tmpl_id_right"][0]
link_dict.setdefault(link_id, 0)
link_dict[link_id] += dic["product_tmpl_id_right_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": [
"|",
("product_tmpl_id_left", "=", self.id),
("product_tmpl_id_right", "=", self.id),
],
"context": {"default_product_tmpl_id_left": 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 = "product_tmpl_id_left, product_tmpl_id_right"
_description = "Product link"

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

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

linked_product_template_id = fields.Many2one(
product_tmpl_id_right = 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("product_tmpl_id_left", "product_tmpl_id_right", "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.product_tmpl_id_left == rec.product_tmpl_id_right for rec in self):
raise ValidationError(
_("You can only create a link between 2 different products")
)

products = self.mapped("product_tmpl_id_left") + self.mapped(
"product_tmpl_id_right"
)
]
self.env.cr.execute(
"""
SELECT
id,
l2.duplicate or l3.duplicate
FROM (
SELECT
id,
product_tmpl_id_left,
product_tmpl_id_right,
type_id
FROM
%s
WHERE
product_tmpl_id_left in %s
AND product_tmpl_id_right in %s
) as l1
LEFT JOIN LATERAL (
SELECT
TRUE as duplicate
FROM
%s
WHERE
product_tmpl_id_right = l1.product_tmpl_id_left
AND product_tmpl_id_left = l1.product_tmpl_id_right
AND type_id = l1.type_id
) l2 ON TRUE
LEFT JOIN LATERAL (
SELECT
TRUE as duplicate
FROM
%s
WHERE
product_tmpl_id_left = l1.product_tmpl_id_left
AND product_tmpl_id_right = l1.product_tmpl_id_right
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.product_tmpl_id_left.name,
l.link_type_name,
l.link_type_inverse_name,
l.product_tmpl_id_right.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 87f0388

Please sign in to comment.