Skip to content

Commit f1b2cd3

Browse files
committed
[IMP] product_ribbons: enhance ribbon assignment logic and added tests
Enhanced ribbon assignment logic for better dynamic and manual control. Ensured ribbon style and position updates save and reflect in the UI. Added automated tests to ensure correct ribbon assignment behavior.
1 parent 1599278 commit f1b2cd3

File tree

7 files changed

+125
-43
lines changed

7 files changed

+125
-43
lines changed

Diff for: product_ribbons/controllers/shop.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ def shop(self, page=0, category=None, search='', min_price=0.0, max_price=0.0, p
1313
response = super().shop(page, category, search, min_price, max_price, ppg, **post)
1414

1515
if response.qcontext.get('products'):
16+
products_prices = response.qcontext.get('products_prices')
1617
for product in response.qcontext['products']:
17-
product._get_ribbon({})
18+
product._get_ribbon(products_prices)
1819

1920
return response

Diff for: product_ribbons/models/product_ribbon.py

+20-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from odoo import fields, models
1+
from odoo import api, fields, models
2+
from odoo.exceptions import ValidationError
23

34

45
class ProductRibbon(models.Model):
@@ -28,3 +29,21 @@ def _get_position_class(self):
2829
return 'o_ribbon_left' if self.position == 'left' else 'o_ribbon_right'
2930
else: # self.style == 'badge'
3031
return 'o_tag_left' if self.position == 'left' else 'o_tag_right'
32+
33+
@api.model_create_multi
34+
def create(self, vals_list):
35+
for vals in vals_list:
36+
assign_type = vals.get('assign')
37+
if assign_type and assign_type != 'manual':
38+
existing_ribbon = self.search([('assign', '=', assign_type)], limit=1)
39+
if existing_ribbon:
40+
raise ValidationError(f"A ribbon with assign type '{assign_type}' already exists. You cannot create another one.")
41+
return super().create(vals_list)
42+
43+
def write(self, vals):
44+
assign_type = vals.get('assign')
45+
if assign_type != 'manual':
46+
existing_ribbon = self.search([('assign', '=', assign_type), ('id', '!=', self.id)])
47+
if existing_ribbon:
48+
raise ValidationError(f"A ribbon with assign type '{assign_type}' already exists. You cannot assign another one.")
49+
return super().write(vals)

Diff for: product_ribbons/models/product_template.py

+8-7
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
from odoo import models, fields
1+
from odoo import fields, models
22

33

44
class ProductTemplate(models.Model):
55
_inherit = 'product.template'
66

77
website_ribbon_auto = fields.Boolean("Automatic Ribbon", default=False)
88

9-
def _get_ribbon(self, product_prices):
9+
def _get_ribbon(self, products_prices):
1010
self.ensure_one()
1111

1212
product_ribbon_sudo = self.env['product.ribbon'].sudo()
@@ -15,21 +15,22 @@ def _get_ribbon(self, product_prices):
1515
return
1616

1717
# Out of Stock Ribbon
18-
out_of_stock_ribbon = product_ribbon_sudo.search([('assign', '=', 'out_of_stock')], limit=1)
18+
out_of_stock_ribbon = product_ribbon_sudo.search([('assign', '=', 'out_of_stock')])
1919
if out_of_stock_ribbon and self.qty_available <= 0.0 and not self.allow_out_of_stock_order:
2020
self.website_ribbon_id = out_of_stock_ribbon.id
2121
self.website_ribbon_auto = True
2222
return
2323

2424
# Sale Ribbon
25-
sale_ribbon = product_ribbon_sudo.search([('assign', '=', 'sale')], limit=1)
26-
if sale_ribbon and self.list_price < self.standard_price:
25+
pricelist_price = products_prices[self.id].get('price_reduce')
26+
sale_ribbon = product_ribbon_sudo.search([('assign', '=', 'sale')])
27+
if sale_ribbon and (pricelist_price < self.list_price or self.list_price < self.compare_list_price):
2728
self.website_ribbon_id = sale_ribbon.id
2829
self.website_ribbon_auto = True
29-
return
30+
return
3031

3132
# New Product Ribbon
32-
new_ribbon = product_ribbon_sudo.search([('assign', '=', 'new')], limit=1)
33+
new_ribbon = product_ribbon_sudo.search([('assign', '=', 'new')])
3334
if new_ribbon:
3435
days_since_publish = (fields.Date.today() - self.create_date.date()).days
3536
if days_since_publish <= new_ribbon.show_period:

Diff for: product_ribbons/static/src/js/adapter.js

+8-31
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import { patch } from "@web/core/utils/patch";
33

44

55
patch(WysiwygAdapterComponent.prototype, {
6-
6+
/**
7+
* @override
8+
*/
79
async init() {
810
await super.init(...arguments);
911

@@ -15,38 +17,13 @@ patch(WysiwygAdapterComponent.prototype, {
1517
['id', 'name', 'bg_color', 'text_color', 'position', 'style'],
1618
);
1719
}
18-
this.ribbons = Object.fromEntries(ribbons.map(ribbon => [ribbon.id, ribbon]));
20+
this.ribbons = Object.fromEntries(ribbons.map(ribbon => {
21+
return [ribbon.id, ribbon];
22+
}));
1923
this.originalRibbons = Object.assign({}, this.ribbons);
2024
this.productTemplatesRibbons = [];
2125
this.deletedRibbonClasses = '';
22-
this.stylePositionClasses = {
23-
'ribbon': { 'left': 'o_ribbon_left', 'right': 'o_ribbon_right' },
24-
'badge': { 'left': 'o_tag_left', 'right': 'o_tag_right' },
25-
};
26-
},
27-
28-
_onGetRibbonClasses(ev) {
29-
const classes = Object.values(this.ribbons).reduce((classes, ribbon) => {
30-
const style = ribbon.style || 'ribbon';
31-
return classes + ` ${this.stylePositionClasses[style][ribbon.position]}`;
32-
}, '') + this.deletedRibbonClasses;
33-
ev.data.callback(classes);
26+
this.ribbonPositionClasses = {'left': 'o_ribbon_left', 'right': 'o_ribbon_right'};
3427
},
3528

36-
_onDeleteRibbon(ev) {
37-
const ribbon = this.ribbons[ev.data.id];
38-
const style = ribbon.style || 'ribbon';
39-
this.deletedRibbonClasses += ` ${this.stylePositionClasses[style][ribbon.position]}`;
40-
delete this.ribbons[ev.data.id];
41-
},
42-
43-
_onSetRibbon(ev) {
44-
const { ribbon } = ev.data;
45-
const previousRibbon = this.ribbons[ribbon.id];
46-
if (previousRibbon) {
47-
const prevStyle = previousRibbon.style || 'ribbon';
48-
this.deletedRibbonClasses += ` ${this.stylePositionClasses[prevStyle][previousRibbon.position]}`;
49-
}
50-
this.ribbons[ribbon.id] = ribbon;
51-
},
52-
});
29+
});

Diff for: product_ribbons/static/src/js/website_sale_editor_extend.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,9 @@ options.registry.WebsiteSaleProductsItem = options.registry.WebsiteSaleProductsI
8282
? (currentStyle === 'badge' ? 'o_tag_left' : 'o_ribbon_left')
8383
: (currentStyle === 'badge' ? 'o_tag_right' : 'o_ribbon_right');
8484

85-
this.$ribbon[0].className = this.$ribbon[0].className.replace(
86-
/o_(ribbon|tag)_(left|right)/, positionClass
87-
);
85+
this.$ribbon.removeClass('o_ribbon_left o_ribbon_right o_tag_left o_tag_right')
86+
.addClass(positionClass)
87+
.attr('data-style', currentStyle);
8888

8989
await this._saveRibbon();
9090
},

Diff for: product_ribbons/tests/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import test_product_ribbons

Diff for: product_ribbons/tests/test_product_ribbons.py

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
from odoo.tests.common import TransactionCase, tagged
2+
from odoo.exceptions import ValidationError
3+
4+
5+
@tagged("post_install", "-at_install")
6+
class TestSaleRibbon(TransactionCase):
7+
@classmethod
8+
def setUpClass(cls):
9+
super().setUpClass()
10+
cls.website = cls.env["website"].search([], limit=1)
11+
cls.product = cls.env["product.template"].search([], limit=1)
12+
cls.christmas_pricelist = cls.env["product.pricelist"].search(
13+
[("name", "=", "Christmas")], limit=1
14+
)
15+
cls.christmas_pricelist.write({"selectable": True})
16+
cls.prices = cls.product._get_sales_prices(cls.website)
17+
18+
def test_pricelist_discount_applies_sale_ribbon(self):
19+
sale_ribbon = self.env["product.ribbon"].create({
20+
"name": "Sale",
21+
"assign": "sale",
22+
})
23+
self.product._get_ribbon(self.prices)
24+
self.assertEqual(
25+
self.product.website_ribbon_id,
26+
sale_ribbon,
27+
"Pricelist discount applied, but Sale Ribbon was not correctly assigned!",
28+
)
29+
30+
def test_ribbon_priority_out_of_stock(self):
31+
self.product.write({"qty_available": 0, "allow_out_of_stock_order": False})
32+
out_of_stock_ribbon = self.env["product.ribbon"].create({
33+
"name": "Out of Stock",
34+
"assign": "out_of_stock",
35+
})
36+
self.env["product.ribbon"].create({"name": "Sale", "assign": "sale"})
37+
self.product._get_ribbon(self.prices)
38+
self.assertEqual(
39+
self.product.website_ribbon_id,
40+
out_of_stock_ribbon,
41+
" Out of Stock Ribbon not prioritized!",
42+
)
43+
44+
def test_duplicate_ribbon_validation(self):
45+
self.env["product.ribbon"].create({"name": "Sale Discount", "assign": "sale"})
46+
with self.assertRaises(ValidationError):
47+
self.env["product.ribbon"].create({"name": "Sale Offer", "assign": "sale"})
48+
49+
def test_manual_ribbon_priority_over_any(self):
50+
manual_ribbon = self.env["product.ribbon"].create({
51+
"name": "Sold Out",
52+
"assign": "manual",
53+
})
54+
self.product.write({"website_ribbon_id": manual_ribbon.id})
55+
self.env["product.ribbon"].create({"name": "New", "assign": "new"})
56+
self.product._get_ribbon(self.prices)
57+
self.assertEqual(
58+
self.product.website_ribbon_id,
59+
manual_ribbon,
60+
" Manual ribbon was replaced by an automatic ribbon!",
61+
)
62+
63+
def test_out_of_stock_ribbon_respects_allow_out_of_stock(self):
64+
out_of_stock_ribbon = self.env["product.ribbon"].create({
65+
"name": "Out of Stock",
66+
"assign": "out_of_stock",
67+
})
68+
69+
self.product.write({"allow_out_of_stock_order": False, "qty_available": 0})
70+
self.product._get_ribbon(self.prices)
71+
self.assertEqual(
72+
self.product.website_ribbon_id,
73+
out_of_stock_ribbon,
74+
" Out of Stock ribbon did NOT apply when allow_out_of_stock_order is False!",
75+
)
76+
77+
self.product.write({"allow_out_of_stock_order": True, "qty_available": 0})
78+
self.product._get_ribbon(self.prices)
79+
self.assertNotEqual(
80+
self.product.website_ribbon_id,
81+
out_of_stock_ribbon,
82+
" Out of Stock ribbon incorrectly applied when allow_out_of_stock_order is True!",
83+
)

0 commit comments

Comments
 (0)