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
lmignon authored and rousseldenis committed Oct 22, 2020

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent 266ae01 commit ae27763
Showing 16 changed files with 892 additions and 136 deletions.
17 changes: 10 additions & 7 deletions product_template_multi_link/__manifest__.py
Original file line number Diff line number Diff line change
@@ -14,15 +14,18 @@
'sale',
],
'data': [
'security/ir.model.access.csv',
'views/action.xml',
'views/product_template_view.xml',
'views/product_template_link_view.xml',
'views/menu.xml',
'wizards/product_template_linker.xml',
"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',
"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>
44 changes: 13 additions & 31 deletions product_template_multi_link/demo/product_template_link.xml
Original file line number Diff line number Diff line change
@@ -1,37 +1,19 @@
<?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="left_product_tmpl_id"
ref="product.product_product_7_product_template"
/>
<field
name="right_product_tmpl_id"
ref="product.product_product_9_product_template"
/>
<field
name="type_id"
ref="product_template_multi_link.product_template_link_type_cross_selling"
/>
</record>

<record id="link_membership_1" model="product.template.link">
<field name="product_template_id" ref="product.membership_2_product_template"/>
<field name="linked_product_template_id" ref="product.membership_1_product_template"/>
<field name="link_type">up_sell</field>
</record>

<record id="link_membership_2" model="product.template.link">
<field name="product_template_id" ref="product.membership_2_product_template"/>
<field name="linked_product_template_id" ref="product.membership_0_product_template"/>
<field name="link_type">up_sell</field>
</record>

<record id="link_membership_3" model="product.template.link">
<field name="product_template_id" ref="product.membership_1_product_template"/>
<field name="linked_product_template_id" ref="product.membership_0_product_template"/>
<field name="link_type">up_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>
17 changes: 0 additions & 17 deletions product_template_multi_link/demo/res_groups.xml

This file was deleted.

1 change: 1 addition & 0 deletions product_template_multi_link/models/__init__.py
Original file line number Diff line number Diff line change
@@ -2,3 +2,4 @@

from . import product_template
from . import product_template_link
from . import product_template_link_type
102 changes: 89 additions & 13 deletions product_template_multi_link/models/product_template.py
Original file line number Diff line number Diff line change
@@ -3,22 +3,98 @@
# @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):
_inherit = 'product.template'

product_template_link_ids = fields.One2many(
string='Product Links', comodel_name='product.template.link',
inverse_name='product_template_id')

product_template_link_qty = fields.Integer(
string='Product Links Quantity', compute='_product_template_link_qty',
store=True)

@api.depends('product_template_link_ids')
def _product_template_link_qty(self):
for template in self:
template.product_template_link_qty =\
len(template.product_template_link_ids)
string="Product Links",
comodel_name="product.template.link",
compute="_compute_product_link_ids",
)

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_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,
},
}
169 changes: 134 additions & 35 deletions product_template_multi_link/models/product_template_link.py
Original file line number Diff line number Diff line change
@@ -2,44 +2,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 api, 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'
_name = "product.template.link"
_order = "left_product_tmpl_id, right_product_tmpl_id"
_description = "Product link"

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

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

type_id = fields.Many2one(
string="Link type",
comodel_name="product.template.link.type",
required=True,
ondelete="restrict",
)

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.invalidate_cache() # 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 _selection_link_type(self):
return [
('cross_sell', 'Cross-Sell'),
('up_sell', 'Up-Sell'),
]

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

product_template_image_small = fields.Binary(
related='product_template_id.image_small')

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

linked_product_template_image_small = fields.Binary(
related='linked_product_template_id.image_small')

link_type = fields.Selection(
string='Link Type', selection="_selection_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.")

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')
]
def create(self, vals):
with self._invalidate_links_on_product_template():
return super(ProductTemplateLink, self).create(vals)

def write(self, vals):
with self._invalidate_links_on_product_template():
return super(ProductTemplateLink, self).write(vals)
104 changes: 104 additions & 0 deletions product_template_multi_link/models/product_template_link_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# -*- coding: utf-8 -*-
# Copyright 2019 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from odoo import api, fields, models


class ProductTemplateLinkType(models.Model):

_name = "product.template.link.type"
_description = "Product Template Link Type"

name = fields.Char(required=True, translate=True)
inverse_name = fields.Char(
compute="_compute_inverse_name",
inverse="_inverse_inverse_name",
readonly=False,
store=True,
translate=True,
)
manual_inverse_name = fields.Char()
is_symmetric = fields.Boolean(
help="The relation meaning is the same from each side of the relation",
default=True,
)
code = fields.Char(
"Technical code",
help="This code allows to provide a technical code to external"
"systems identifying this link type",
)
inverse_code = fields.Char(
"Technical code (inverse)",
compute="_compute_inverse_code",
inverse="_inverse_inverse_code",
readonly=False,
store=True,
help="This code allows to provide a technical code to external"
"systems identifying this link type",
)
manual_inverse_code = fields.Char()
_sql_constraints = [
("name_uniq", "unique (name)", "Link type name already exists !"),
(
"inverse_name_uniq",
"unique (inverse_name)",
"Link type inverse name already exists !",
),
(
"code_uniq",
"EXCLUDE (code WITH =) WHERE (code is not null)",
"Link code already exists !",
),
(
"inverse_code_uniq",
"EXCLUDE (inverse_code WITH =) WHERE (inverse_code is not null)",
"Link inverse code already exists !",
),
]

display_name = fields.Char(compute="_compute_display_name")

@api.multi
def _inverse_inverse_name(self):
for record in self:
record.manual_inverse_name = record.inverse_name

@api.multi
def _inverse_inverse_code(self):
for record in self:
record.manual_inverse_code = record.inverse_code

@api.depends("name", "inverse_name")
def _compute_display_name(self):
for record in self:
display_name = record.name
if not record.is_symmetric:
display_name = "{} / {}".format(record.inverse_name, record.name)
record.display_name = display_name

@api.depends("name", "is_symmetric")
def _compute_inverse_name(self):
for record in self:
if record.is_symmetric:
record.inverse_name = record.name
else:
record.inverse_name = record.manual_inverse_name

@api.depends("code", "is_symmetric")
def _compute_inverse_code(self):
for record in self:
if record.is_symmetric:
record.inverse_code = record.code
else:
record.inverse_code = record.manual_inverse_code

def write(self, vals):
for record in self:
is_symmetric = vals.get("is_symmetric", record.is_symmetric)
v = vals.copy()
if is_symmetric:
v.pop("inverse_code", None)
v.pop("inverse_name", None)
r = super(ProductTemplateLinkType, record).write(v)
return r
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?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="ir.model.access" id="product_template_link_type_access_read">
<field name="name">product.template.link.type access read</field>
<field name="model_id" ref="model_product_template_link_type" />
<field name="group_id" ref="base.group_user" />
<field name="perm_read" eval="1" />
<field name="perm_create" eval="0" />
<field name="perm_write" eval="0" />
<field name="perm_unlink" eval="0" />
</record>
<record model="ir.model.access" id="product_template_link_type_access_manage">
<field name="name">product.template.link.type access manager</field>
<field name="model_id" ref="model_product_template_link_type" />
<field name="group_id" ref="sales_team.group_sale_manager" />
<field name="perm_read" eval="1" />
<field name="perm_create" eval="1" />
<field name="perm_write" eval="1" />
<field name="perm_unlink" eval="1" />
</record>
</odoo>
6 changes: 2 additions & 4 deletions product_template_multi_link/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
# -*- coding: utf-8 -*-
# Copyright 2020 ACSONE SA/NV (<http://acsone.eu>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from . import test_product_template_linker
from . import test_product_template_link_type
from . import test_product_template_link
118 changes: 118 additions & 0 deletions product_template_multi_link/tests/test_product_template_link.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# -*- coding: utf-8 -*-
# Copyright 2019 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from odoo.exceptions import ValidationError
from odoo.tests.common import SavepointCase


class TestProductTemplateLink(SavepointCase):
@classmethod
def setUpClass(cls):
super(TestProductTemplateLink, cls).setUpClass()
cls.ProductTemplateLink = cls.env["product.template.link"]
cls.product_product_1 = cls.env.ref("product.product_product_1")
cls.product_product_2 = cls.env.ref("product.product_product_2")
cls.link_type = cls.env.ref(
"product_template_multi_link.product_template_link_type_cross_selling"
)

def test_01(self):
"""
Data:
- 2 publication templates
Test Case:
- Try to create 2 links of same type
Expected result:
- ValidationError is raised
"""
link1 = self.ProductTemplateLink.create(
{
"left_product_tmpl_id": self.product_product_1.id,
"right_product_tmpl_id": self.product_product_2.id,
"type_id": self.link_type.id,
}
)
with self.assertRaises(ValidationError), self.env.cr.savepoint():
link1.copy()

# create the same link but inverse ids
with self.assertRaises(ValidationError), self.env.cr.savepoint():
self.ProductTemplateLink.create(
{
"left_product_tmpl_id": self.product_product_2.id,
"right_product_tmpl_id": self.product_product_1.id,
"type_id": self.link_type.id,
}
)

def test_02(self):
"""
Data:
- 1 publication templates
Test Case:
- Try to create 1 link between the same product
Expected result:
- ValidationError is raised
"""
with self.assertRaises(ValidationError), self.env.cr.savepoint():
self.ProductTemplateLink.create(
{
"left_product_tmpl_id": self.product_product_1.id,
"right_product_tmpl_id": self.product_product_1.id,
"type_id": self.link_type.id,
}
)

def test_03(self):
"""
Data:
- 2 publication templates
Test Case:
- Create 1 link between the 2 products
Expected result:
- The link is visible from the 2 products
"""
link1 = self.ProductTemplateLink.create(
{
"left_product_tmpl_id": self.product_product_1.id,
"right_product_tmpl_id": self.product_product_2.id,
"type_id": self.link_type.id,
}
)
self.assertEqual(link1, self.product_product_1.product_template_link_ids)

self.assertEqual(link1, self.product_product_2.product_template_link_ids)

def test_04(self):
"""
Data:
- 2 publication templates
Test Case:
1 Create 1 link between the 2 products
2 Unlik the link
Expected result:
1 The link is visible from the 2 products
2 No link remains between the 2 products
This test check the cache invalidation of the computed fields on the
product.template
"""
link1 = self.ProductTemplateLink.create(
{
"left_product_tmpl_id": self.product_product_1.id,
"right_product_tmpl_id": self.product_product_2.id,
"type_id": self.link_type.id,
}
)
self.assertEqual(link1, self.product_product_1.product_template_link_ids)

self.assertEqual(link1, self.product_product_2.product_template_link_ids)

self.assertEqual(1, self.product_product_1.product_template_link_count)
self.assertEqual(1, self.product_product_2.product_template_link_count)

link1.unlink()
self.assertFalse(self.product_product_1.product_template_link_ids)
self.assertFalse(self.product_product_2.product_template_link_ids)
self.assertEqual(0, self.product_product_1.product_template_link_count)
self.assertEqual(0, self.product_product_2.product_template_link_count)
262 changes: 262 additions & 0 deletions product_template_multi_link/tests/test_product_template_link_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
# -*- coding: utf-8 -*-
# Copyright 2019 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from psycopg2 import IntegrityError

from odoo.tests import SavepointCase
from odoo.tools import mute_logger


class TestProductTemplateLinkType(SavepointCase):
@classmethod
def setUpClass(cls):
super(TestProductTemplateLinkType, cls).setUpClass()
cls.LinkType = cls.env["product.template.link.type"]
cls.link_type_cross_selling = cls.env.ref(
"product_template_multi_link.product_template_link_type_cross_selling"
)
cls.link_type_range = cls.env.ref(
"product_template_multi_link.product_template_link_type_demo_range"
)
cls.symmetric_link_without_code = cls.LinkType.create(
{"name": "symmetric_link_without_code"}
)
cls.symmetric_link_with_code = cls.LinkType.create(
{
"name": "symmetric_link_with_code-name",
"code": "symmetric_link_with_code-code",
}
)
cls.asymmetric_link_without_inverse_code = cls.LinkType.create(
{
"is_symmetric": False,
"name": "asymmetric_link_without_code-name",
"inverse_name": "asymmetric_link_without_code-inverse_name",
}
)

cls.asymmetric_link_with_inverse_code = cls.LinkType.create(
{
"is_symmetric": False,
"name": "asymmetric_link_with_code-name",
"inverse_name": "asymmetric_link_with_code-inverse_name",
"inverse_code": "asymmetric_link_with_code-inverse_code",
}
)

def test_0(self):
"""
Data:
None
Test case:
Create a link type by providing only the name
Expected Result:
by default link is symmetric
inverse_name is computed
inverse_name is equal to name
code is false (not provided)
inverse_code is false
"""
link_type = self.LinkType.create({"name": "my type"})
self.assertTrue(link_type.is_symmetric)
self.assertTrue(link_type.name)
self.assertEqual(link_type.name, link_type.inverse_name)
self.assertFalse(link_type.code)
self.assertFalse(link_type.inverse_code)

def test_1(self):
"""
Data:
None
Test case:
Create a link type by providing only the name and the code
Expected Result:
by default link is symmetric
inverse_name is computed
inverse_name is equal to name
inverse_code is computed
inverse_code is equal to code
"""
link_type = self.LinkType.create({"name": "my type", "code": "my-code"})
self.assertTrue(link_type.is_symmetric)
self.assertEqual(link_type.name, "my type")
self.assertEqual(link_type.code, "my-code")
self.assertEqual(link_type.name, link_type.inverse_name)
self.assertEqual(link_type.code, link_type.inverse_code)

@mute_logger("odoo.sql_db")
def test_2(self):
"""
Data:
None
Test case:
Create a link type without providing the name
Expected Result:
Exception is raised
"""
with self.assertRaises(IntegrityError), self.env.cr.savepoint():
self.LinkType.create(
{
"inverse_name": "my type",
"is_symmetric": False,
"code": "my_code",
"inverse_code": "my_inverse_code",
}
)

def test_3(self):
"""
Data:
An existing symmetric link type
Test case:
Update the name
Expected Result:
inverse_name is still equal to name
"""
self.assertEqual(
self.link_type_cross_selling.name, self.link_type_cross_selling.inverse_name
)
self.link_type_cross_selling.write({"name": "new name"})
self.assertEqual(self.link_type_cross_selling.name, "new name")
self.assertEqual(
self.link_type_cross_selling.name, self.link_type_cross_selling.inverse_name
)

def test_4(self):
"""
Data:
An existing symmetric link type
Test case:
Update the code
Expected Result:
inverse_code is still equal to code
"""
self.assertEqual(
self.link_type_cross_selling.code, self.link_type_cross_selling.inverse_code
)
self.link_type_cross_selling.write({"code": "new-code"})
self.assertEqual(self.link_type_cross_selling.code, "new-code")
self.assertEqual(
self.link_type_cross_selling.code, self.link_type_cross_selling.inverse_code
)

def test_5(self):
"""
Data:
An existing symmetric link type
Test case:
Update the inverse_name
Update the inverse_code
Expected Result:
inverse_name and inverse_code are not updated
"""
self.assertEqual(
self.link_type_cross_selling.name, self.link_type_cross_selling.inverse_name
)
self.assertEqual(
self.link_type_cross_selling.code, self.link_type_cross_selling.inverse_code
)
inverse_name = self.link_type_cross_selling.inverse_name
inverse_code = self.link_type_cross_selling.inverse_code
self.link_type_cross_selling.write(
{
"inverse_name": "new " + inverse_name,
"inverse_code": "new " + inverse_code,
}
)
self.assertEqual(self.link_type_cross_selling.inverse_name, inverse_name)
self.assertEqual(self.link_type_cross_selling.inverse_code, inverse_code)

def test_6(self):
"""
Data:
An existing symmetric link type
Test case:
Update the inverse_name with name != inverse_name,
code != inverse_code and make it asymmetric
Expected Result:
inverse_name is no more equal to name and
the code is not more equald to inverse_code
"""
self.assertEqual(
self.link_type_cross_selling.name, self.link_type_cross_selling.inverse_name
)
self.assertEqual(
self.link_type_cross_selling.code, self.link_type_cross_selling.inverse_code
)
self.link_type_cross_selling.write(
{
"is_symmetric": False,
"inverse_name": "new inverse name",
"inverse_code": "new inverse code",
}
)
self.assertEqual(self.link_type_cross_selling.inverse_name, "new inverse name")
self.assertEqual(self.link_type_cross_selling.inverse_code, "new inverse code")

def test_7(self):
"""
Data:
An existing asymmetric link with inverse_code != code
and inverse_name != name
Test case:
1 Make it symmetric
Expected Result:
invsere_code=code and inverse_name=name
"""
self.assertFalse(self.link_type_range.is_symmetric)
self.assertNotEqual(
self.link_type_range.name, self.link_type_range.inverse_name
)
self.assertNotEqual(
self.link_type_range.code, self.link_type_range.inverse_code
)
self.link_type_range.write({"is_symmetric": True})
self.assertEqual(self.link_type_range.name, self.link_type_range.inverse_name)
self.assertEqual(self.link_type_range.code, self.link_type_range.inverse_code)

@mute_logger("odoo.sql_db")
def test_8(self):
"""
Data:
symmetric link type without code
symmetric link with code
asymmetric link type without inverse_code
asymmetric link type with inverse_code
Test case:
1 create a new link type with the same name without code
1 create a new link type with the same name
1 create a new link type with the same code
1 create a new link type with the same inverse_name
Expected Result:
Intergrity Error
"""
with self.assertRaises(IntegrityError), self.env.cr.savepoint():
self.LinkType.create({"name": self.symmetric_link_without_code.name})

with self.assertRaises(IntegrityError), self.env.cr.savepoint():
self.LinkType.create(
{
"name": self.symmetric_link_with_code.name + "test_8",
"code": self.symmetric_link_with_code.code,
}
)
with self.assertRaises(IntegrityError), self.env.cr.savepoint():
inverse_name = self.asymmetric_link_without_inverse_code.inverse_name
self.LinkType.create(
{
"name": self.asymmetric_link_without_inverse_code.name + "test_8",
"inverse_name": inverse_name,
}
)
with self.assertRaises(IntegrityError), self.env.cr.savepoint():
self.LinkType.create(
{
"name": self.asymmetric_link_with_inverse_code.name + "test_8",
"inverse_name": self.asymmetric_link_with_inverse_code.inverse_name
+ "test_8",
"inverse_code": self.asymmetric_link_with_inverse_code.inverse_code,
}
)
40 changes: 40 additions & 0 deletions product_template_multi_link/views/product_template_link_type.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?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="ir.ui.view" id="product_template_link_type_tree_view">
<field
name="name"
>product.template.link.type.tree (in product_template_multi_link)</field>
<field name="model">product.template.link.type</field>
<field name="arch" type="xml">
<tree editable="top">
<field name="is_symmetric" />
<field name="name" />
<field
name="inverse_name"
required="1"
attrs="{'readonly': [('is_symmetric', '=', True)]}"
/>
<field name="code" />
<field
name="inverse_code"
attrs="{'readonly': [('is_symmetric', '=', True)]}"
/>
</tree>
</field>
</record>
<record model="ir.actions.act_window" id="product_template_link_type_act_window">
<field name="name">Product Template Link Type</field>
<field name="res_model">product.template.link.type</field>
<field name="view_mode">tree,form</field>
<field name="domain">[]</field>
<field name="context">{}</field>
</record>
<record model="ir.ui.menu" id="product_template_link_type_menu">
<field name="name">Product Template Link Type</field>
<field name="parent_id" ref="sale.prod_config_main" />
<field name="action" ref="product_template_link_type_act_window" />
<field name="sequence" eval="16" />
</record>
</odoo>
76 changes: 50 additions & 26 deletions product_template_multi_link/views/product_template_link_view.xml
Original file line number Diff line number Diff line change
@@ -1,20 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>

<!-- Technical Note: domain=[('id', '!=', 0)] is set on both template fields
to enable research by name for product templates. Otherwise, the resarch will
be done on product.product table, that is not the attempted behaviour
See odoo/addons/product/models/product_template.py // def name_search()
-->

<record id='product_template_link__search' model='ir.ui.view'>
<field name="name">Product.template.link.search</field>
<field name="model">product.template.link</field>
<field name="arch" type="xml">
<search string="Product template links">
<field
name="left_product_tmpl_id"
string="Product"
filter_domain="['|',('right_product_tmpl_id','ilike',self),('left_product_tmpl_id','ilike',self)]"
/>
<field name="type_id" />
<group expand="1" string="Group By">
<filter
name="groupby_type"
context="{'group_by' : 'type_id'}"
string="Type"
/>
</group>
</search>
</field>
</record>
<!-- Technical Note: domain=[('id', '!=', 0)] is set on both template fields
to enable research by name for product templates. Otherwise, the search will
be done on product.product table, that is not the attempted behaviour
See odoo/addons/product/models/product_template.py // def name_search()
-->
<record id="product_template_link_tree" model="ir.ui.view">
<field name="model">product.template.link</field>
<field name="arch" type="xml">
<tree editable="bottom">
<field name="product_template_id" domain="[('id', '!=', 0)]"
invisible="context.get('default_product_template_id', False)"/>
<field name="linked_product_template_id" domain="[('id', '!=', 0)]" />
<field name="link_type" />
<tree editable="top">
<field name="left_product_tmpl_id" domain="[('id', '!=', 0)]" />
<field name="type_id" />
<field name="right_product_tmpl_id" domain="[('id', '!=', 0)]" />
</tree>
</field>
</record>
@@ -23,34 +41,40 @@ See odoo/addons/product/models/product_template.py // def name_search()
<field name="model">product.template.link</field>
<field name="arch" type="xml">
<kanban>
<field name="product_template_id"/>
<field name="linked_product_template_id"/>
<field name="product_template_image_small"/>
<field name="linked_product_template_image_small"/>
<field name="link_type"/>
<field name="left_product_tmpl_id" />
<field name="right_product_tmpl_id" />
<field name="type_id" />
<templates>
<t t-name="kanban-box">
<div>
<div style="height: 64px; width: 45%; float:left;">
<div class="o_kanban_image">
<img t-att-src="kanban_image('product.template.link', 'product_template_image_small', record.id.value)"/>
<img
t-att-src="kanban_image('product.template', 'image_128', record.left_product_tmpl_id.raw_value)"
alt="Product"
class="o_image_64_contain"
/>
</div>
<div class="oe_kanban_details" style="font-size: 10px;">
<field name="product_template_id"/>
<field name="left_product_tmpl_id" />
</div>
</div>
<div name="product_template_link_type" style="height: 64px; width: 10%; float:left;">
<img src="/product_template_multi_link/static/src/img/type_cross_sell.png"
t-if="record.link_type.raw_value == 'cross_sell'" style="width:100%"/>
<img src="/product_template_multi_link/static/src/img/type_up_sell.png"
t-if="record.link_type.raw_value == 'up_sell'" style="width:100%"/>
<div
name="product_template_link_type"
style="height: 64px; width: 10%; float:left;"
>
<field name="type_id" />
</div>
<div style="height: 64px; width: 45%; float:left;">
<div class="o_kanban_image">
<img t-att-src="kanban_image('product.template.link', 'linked_product_template_image_small', record.id.value)"/>
<img
t-att-src="kanban_image('product.template', 'image_128', record.right_product_tmpl_id.raw_value)"
alt="Product"
class="o_image_64_contain"
/>
</div>
<div class="oe_kanban_details" style="font-size: 10px;">
<field name="linked_product_template_id"/>
<field name="right_product_tmpl_id" />
</div>
</div>
</div>
15 changes: 12 additions & 3 deletions product_template_multi_link/views/product_template_view.xml
Original file line number Diff line number Diff line change
@@ -6,9 +6,18 @@
<field name="inherit_id" ref="product.product_template_only_form_view" />
<field name="arch" type="xml">
<div name="button_box" position="inside">
<button name="%(action_product_template_link_from_product_template)d" type="action"
icon="fa-hand-o-right" class="oe_stat_button">
<field string="Product Links" name="product_template_link_qty" widget="statinfo"/>
<button
name="show_product_template_links"
help="Product links"
class="oe_stat_button"
icon="fa-sitemap"
type="object"
>
<field
string="Product Links"
name="product_template_link_count"
widget="statinfo"
/>
</button>
</div>
</field>

0 comments on commit ae27763

Please sign in to comment.