Skip to content

Commit

Permalink
[10.0][ADD] product_tempalte_multi_link: add possibility to link prod…
Browse files Browse the repository at this point in the history
…ucts in one shot by selecting them into Tree view and picking the link type (by a wizard). You can also remove every link of a product. + Update Readme + add unit tests
  • Loading branch information
acsonefho authored and David-Luis-Mora committed May 2, 2024
1 parent b09c094 commit 0c5b399
Show file tree
Hide file tree
Showing 8 changed files with 324 additions and 1 deletion.
2 changes: 1 addition & 1 deletion product_template_multi_link/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Product Multi Links (Template)
:target: https://runbot.odoo-community.org/runbot/113/14.0
:alt: Try me on Runbot

|badge1| |badge2| |badge3| |badge4| |badge5|
|badge1| |badge2| |badge3| |badge4| |badge5|

This module extends the functionality of sale module to support links between
products templates.
Expand Down
1 change: 1 addition & 0 deletions product_template_multi_link/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

from . import models
from . import wizards
1 change: 1 addition & 0 deletions product_template_multi_link/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"views/product_template_view.xml",
"views/product_template_link_view.xml",
"views/menu.xml",
"wizards/product_template_linker.xml",
],
"demo": ["demo/product_template_link_type.xml", "demo/product_template_link.xml"],
"installable": True,
Expand Down
1 change: 1 addition & 0 deletions product_template_multi_link/security/ir.model.access.csv
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_product_template_link_employee,product template link employee,model_product_template_link,base.group_user,1,0,0,0
access_product_template_link_sale_manager,product template link sale manager,model_product_template_link,sales_team.group_sale_manager,1,1,1,1
access_product_template_linker,access_product_template_linker,model_product_template_linker,base.group_user,1,1,1,1
149 changes: 149 additions & 0 deletions product_template_multi_link/tests/test_product_template_linker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# Copyright 2020 ACSONE SA/NV (<http://acsone.eu>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo.tests.common import SavepointCase


class TestProductTemplateLinker(SavepointCase):
"""
Tests for product.template.linker
"""

@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True))
cls.wizard_obj = cls.env["product.template.linker"]
cls.product_link_obj = cls.env["product.template.link"]
cls.type_cross = cls.env.ref(
"product_template_multi_link.product_template_link_type_cross_selling"
)
cls.type_up = cls.env.ref(
"product_template_multi_link.product_template_link_type_up_selling"
)
cls.product1 = cls.env.ref("product.product_product_25").product_tmpl_id
cls.product2 = cls.env.ref("product.product_product_5").product_tmpl_id
cls.product3 = cls.env.ref("product.product_product_27").product_tmpl_id
cls.products = cls.product1 | cls.product2 | cls.product3
cls.products.mapped("product_template_link_ids").unlink()

def _launch_wizard(self, products, operation_type, link_type=False):
"""
:param products: product.template recordset
:return: product.template.linker recordset
"""
link_type = link_type or self.env["product.template.link.type"].browse()
values = {
"operation_type": operation_type,
"type_id": link_type.id,
"product_ids": [(6, False, products.ids)],
}
return self.wizard_obj.create(values)

def test_wizard_link_cross_sell(self):
link_type = self.type_cross
wizard = self._launch_wizard(
self.products, operation_type="link", link_type=link_type
)
links = wizard.action_apply_link()
for link in links:
source_product = link.product_template_id
linked_products = source_product.product_template_link_ids
expected_products_linked = self.products - source_product
self.assertEquals(
set(expected_products_linked.ids),
set(linked_products.mapped("linked_product_template_id").ids),
)
self.assertEquals(link_type, link.link_type)

def test_wizard_link_up_sell(self):
link_type = self.type_up
wizard = self._launch_wizard(
self.products, operation_type="link", link_type=link_type
)
links = wizard.action_apply_link()
for link in links:
source_product = link.product_template_id
linked_products = source_product.product_template_link_ids
expected_products_linked = self.products - source_product
self.assertEquals(
set(expected_products_linked.ids),
set(linked_products.mapped("linked_product_template_id").ids),
)
self.assertEquals(link_type, link.link_type)

def test_wizard_link_duplicate1(self):
link_type = self.type_up
wizard = self._launch_wizard(
self.products, operation_type="link", link_type=link_type
)
self.product_link_obj.create(
{
"product_template_id": self.product1.id,
"linked_product_template_id": self.product2.id,
"type_id": link_type.id,
}
)
links = wizard.action_apply_link()
for link in links:
source_product = link.product_template_id
linked_products = source_product.product_template_link_ids
expected_products_linked = self.products - source_product
self.assertEquals(
set(expected_products_linked.ids),
set(linked_products.mapped("linked_product_template_id").ids),
)
self.assertEquals(link_type, link.link_type)
# Ensure no duplicates
link = self.product1.product_template_link_ids.filtered(
lambda l: l.linked_product_template_id == self.product2
)
self.assertEquals(1, len(link))

def test_wizard_link_duplicate2(self):
link_type = self.type_cross
wizard = self._launch_wizard(
self.products, operation_type="link", link_type=link_type
)
self.product_link_obj.create(
{
"product_template_id": self.product1.id,
"linked_product_template_id": self.product2.id,
"type_id": self.type_up.id,
}
)
links = wizard.action_apply_link()
for link in links:
source_product = link.product_template_id
linked_products = source_product.product_template_link_ids
expected_products_linked = self.products - source_product
self.assertEquals(
set(expected_products_linked.ids),
set(linked_products.mapped("linked_product_template_id").ids),
)
self.assertEquals(link_type, link.link_type)
# Ensure no duplicates
link = self.product1.product_template_link_ids.filtered(
lambda l: l.linked_product_template_id == self.product2
)
# 2 because we have up_sell and cross_sell
self.assertEquals(2, len(link))

def test_wizard_unlink(self):
wizard = self._launch_wizard(self.products, operation_type="unlink")
self.product_link_obj.create(
{
"product_template_id": self.product1.id,
"linked_product_template_id": self.product2.id,
"type_id": self.type_up.id,
}
)
self.product_link_obj.create(
{
"product_template_id": self.product1.id,
"linked_product_template_id": self.product3.id,
"type_id": self.type_cross.id,
}
)
wizard.action_apply_unlink()
self.assertFalse(self.product1.product_template_link_ids)
3 changes: 3 additions & 0 deletions product_template_multi_link/wizards/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Copyright 2020 ACSONE SA/NV (<http://acsone.eu>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from . import product_template_linker
103 changes: 103 additions & 0 deletions product_template_multi_link/wizards/product_template_linker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Copyright 2020 ACSONE SA/NV (<http://acsone.eu>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo import api, fields, models


class ProductTemplateLinker(models.TransientModel):
"""
Wizard used to link product template together in one shot
"""

_name = "product.template.linker"
_description = "Product template linker wizard"

operation_type = fields.Selection(
selection=[
("unlink", "Remove existing links"),
("link", "Link these products"),
],
string="Operation",
required=True,
help="Remove existing links: will remove every existing link "
"on each selected products;\n"
"Link these products: will link all selected "
"products together.",
)
product_ids = fields.Many2many(
comodel_name="product.template",
string="Products",
)
type_id = fields.Many2one(
string="Link type",
comodel_name="product.template.link.type",
ondelete="restrict",
)

@api.model
def default_get(self, fields_list):
"""Inherit default_get to auto-fill product_ids with current context
:param fields_list: list of str
:return: dict
"""
result = super().default_get(fields_list)
ctx = self.env.context
active_ids = ctx.get("active_ids", ctx.get("active_id", []))
products = []
if ctx.get("active_model") == self.product_ids._name and active_ids:
products = [(6, False, list(active_ids))]
result.update(
{
"product_ids": products,
}
)
return result

def action_apply(self):
if self.operation_type == "link":
self.action_apply_link()
elif self.operation_type == "unlink":
self.action_apply_unlink()
return {}

def action_apply_unlink(self):
"""Remove links from products.
:return: product.template.link recordset
"""
self.product_ids.mapped("product_template_link_ids").unlink()
return self.env["product.template.link"].browse()

def action_apply_link(self):
"""Add link to products.
:return: product.template.link recordset
"""
links = self.env["product.template.link"].browse()
for product in self.product_ids:
existing_links = product.product_template_link_ids.filtered(
lambda l: l.type_id == self.type_id
)
linked_products = existing_links.mapped("linked_product_template_id")
products_to_link = self.product_ids - linked_products - product
links |= self._create_link(product, products_to_link)
return links

def _create_link(self, product_source, target_products):
"""Create the link between given product source and target products.
:param product_source: product.template recordset
:param target_products: product.template recordset
:return: product.template.link recordset
"""
self.ensure_one()
prod_link_obj = self.env["product.template.link"]
product_links = prod_link_obj.browse()
for target_product in target_products:
values = {
"product_template_id": product_source.id,
"linked_product_template_id": target_product.id,
"type_id": self.type_id.id,
}
product_links |= prod_link_obj.create(values)
return product_links
65 changes: 65 additions & 0 deletions product_template_multi_link/wizards/product_template_linker.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2020 ACSONE SA/NV
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<record model="ir.ui.view" id="product_template_linker_form_view">
<field
name="name"
>product.template.linker.form (in product_template_multi_link)</field>
<field name="model">product.template.linker</field>
<field name="arch" type="xml">
<form>
<group name="main_group">
<group name="main_data_group">
<field name="operation_type" />
<field
name="type_id"
attrs="{'invisible': [('operation_type', '!=', 'link')], 'required': [('operation_type', '=', 'link')]}"
/>
</group>
</group>
<group colspan="2" name="product_group">
<field
name="product_ids"
nolabel="1"
colspan="2"
options="{'no_open':True, 'no_create':True}"
>
<tree>
<field name="name" />
<field name="default_code" />
</tree>
</field>
</group>
<footer>
<button
name="action_apply"
string="Create links"
type="object"
class="oe_highlight"
help="Create links?"
attrs="{'invisible': [('operation_type', '!=', 'link')]}"
/>
<button
name="action_apply"
string="Remove links"
type="object"
class="oe_highlight"
help="Remove links?"
attrs="{'invisible': [('operation_type', '!=', 'unlink')]}"
/>
<button string="Cancel" class="oe_link" special="cancel" />
</footer>
</form>
</field>
</record>

<record model="ir.actions.act_window" id="product_template_linker_action">
<field name="name">Manage Product Links</field>
<field name="res_model">product.template.linker</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="view_id" ref="product_template_linker_form_view" />
<field name="binding_model_id" ref="product.model_product_template" />
</record>
</odoo>

0 comments on commit 0c5b399

Please sign in to comment.