Skip to content

Commit 8fb7fd7

Browse files
FlorianGilbertpbch-odoo
authored andcommitted
[ADD] dynamic_ribbons: add new ribbon functionality
1 parent 4f1d255 commit 8fb7fd7

12 files changed

+312
-11
lines changed

README.md

-11
This file was deleted.

dynamic_ribbons/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from . import models
2+
from . import controllers

dynamic_ribbons/__manifest__.py

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
'name': 'Ribbon',
3+
'version': '1.0',
4+
'summary': '',
5+
'author': 'PBCH',
6+
'category': 'Website',
7+
'depends': [ 'website_sale', 'stock' ],
8+
'data': [
9+
'views/product_ribbon_view.xml',
10+
'views/snippet.xml',
11+
],
12+
'assets': {
13+
'website.assets_wysiwyg':[
14+
'dynamic_ribbons/static/src/js/website_sale_editor.js',
15+
],
16+
'website.backend_assets_all_wysiwyg': [
17+
'dynamic_ribbons/static/src/js/adapter.js',
18+
],
19+
},
20+
'license': 'LGPL-3',
21+
}
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import shop_product
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from odoo.addons.website_sale.controllers.main import WebsiteSale
2+
from odoo import http
3+
4+
class WebsiteSaleExtended(WebsiteSale):
5+
6+
@http.route([
7+
'/shop',
8+
'/shop/page/<int:page>',
9+
'/shop/category/<model("product.public.category"):category>',
10+
'/shop/category/<model("product.public.category"):category>/page/<int:page>',
11+
], type='http', auth="public", website=True)
12+
def shop(self, page=0, category=None, search='', min_price=0.0, max_price=0.0, ppg=False, **post):
13+
response = super().shop(page, category, search, min_price, max_price, ppg, **post)
14+
15+
if response.qcontext.get('products'):
16+
products_prices = response.qcontext.get('products_prices')
17+
for product in response.qcontext['products']:
18+
product._get_ribbon(products_prices)
19+
20+
return response
21+

dynamic_ribbons/models/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from . import product_ribbon
2+
from . import product_template
+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from odoo import fields, models, api
2+
from odoo.exceptions import ValidationError
3+
4+
class ProductRibbon(models.Model):
5+
_inherit = "product.ribbon"
6+
7+
style = fields.Selection(
8+
[("tag", "Tag"), ("ribbon", "Ribbon")],
9+
string="Style",
10+
default="ribbon",
11+
required=True,
12+
)
13+
assign = fields.Selection(
14+
[
15+
("manual", "Manual"),
16+
("sale", "Sale"),
17+
("out_of_stock", "Out of Stock"),
18+
("new", "New"),
19+
],
20+
string="Assign",
21+
default="manual",
22+
required=True,
23+
)
24+
show_period = fields.Integer(default=30)
25+
26+
def _get_position_class(self):
27+
if self.style == 'ribbon':
28+
return 'o_ribbon_left' if self.position == 'left' else 'o_ribbon_right'
29+
else: # self.style == 'tag'
30+
return 'o_tag_left' if self.position == 'left' else 'o_tag_right'
31+
32+
@api.model_create_multi
33+
def create(self, vals_list):
34+
for vals in vals_list:
35+
assign_type = vals.get('assign')
36+
if assign_type and assign_type != 'manual':
37+
existing_ribbon = self.search([('assign', '=', assign_type)], limit=1)
38+
if existing_ribbon:
39+
raise ValidationError(f"A ribbon with assign type '{assign_type}' already exists. You cannot create another one.")
40+
return super().create(vals_list)
41+
42+
def write(self, vals):
43+
assign_type = vals.get('assign')
44+
if assign_type != 'manual':
45+
existing_ribbon = self.search([('assign', '=', assign_type), ('id', '!=', self.id)])
46+
if existing_ribbon:
47+
raise ValidationError(f"A ribbon with assign type '{assign_type}' already exists. You cannot assign another one.")
48+
return super().write(vals)
49+
+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
from odoo import models, fields
2+
3+
class ProductTemplate(models.Model):
4+
_inherit = 'product.template'
5+
6+
website_ribbon_auto = fields.Boolean("Automatic Ribbon", default=False)
7+
8+
def _get_ribbon(self, products_prices):
9+
self.ensure_one()
10+
11+
product_ribbon_sudo = self.env['product.ribbon'].sudo()
12+
13+
if self.website_ribbon_id and not self.website_ribbon_auto:
14+
return
15+
16+
# if (True):
17+
# continue
18+
19+
# Out of Stock Ribbon
20+
out_of_stock_ribbon = product_ribbon_sudo.search([('assign', '=', 'out_of_stock')])
21+
if out_of_stock_ribbon and self.qty_available <= 0.0 and not self.allow_out_of_stock_order:
22+
self.website_ribbon_id = out_of_stock_ribbon.id
23+
self.website_ribbon_auto = True
24+
return
25+
26+
# Sale Ribbon
27+
pricelist_price = products_prices[self.id].get('price_reduce')
28+
sale_ribbon = product_ribbon_sudo.search([('assign', '=', 'sale')])
29+
if sale_ribbon and (pricelist_price < self.list_price or self.list_price < self.compare_list_price):
30+
self.website_ribbon_id = sale_ribbon.id
31+
self.website_ribbon_auto = True
32+
return
33+
34+
# New Product Ribbon
35+
new_ribbon = product_ribbon_sudo.search([('assign', '=', 'new')])
36+
if new_ribbon:
37+
days_since_publish = (fields.Date.today() - self.create_date.date()).days
38+
if days_since_publish <= new_ribbon.show_period:
39+
self.website_ribbon_id = new_ribbon.id
40+
self.website_ribbon_auto = True
41+
return
42+
43+
# If no ribbon was assigned automatically then clearing any existing automatic ribbon
44+
self.website_ribbon_id = None
45+
46+
47+
def write(self, vals):
48+
if 'website_ribbon_id' in vals and vals['website_ribbon_id'] != self.website_ribbon_id.id:
49+
vals['website_ribbon_auto'] = False
50+
return super().write(vals)
+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { WysiwygAdapterComponent } from '@website/components/wysiwyg_adapter/wysiwyg_adapter';
2+
import { patch } from "@web/core/utils/patch";
3+
4+
patch(WysiwygAdapterComponent.prototype, {
5+
/**
6+
* @override
7+
*/
8+
async init() {
9+
await super.init(...arguments);
10+
11+
let ribbons = [];
12+
if (this._isProductListPage()) {
13+
ribbons = await this.orm.searchRead(
14+
'product.ribbon',
15+
[],
16+
['id', 'name', 'bg_color', 'text_color', 'position', 'style'],
17+
);
18+
}
19+
this.ribbons = Object.fromEntries(ribbons.map(ribbon => {
20+
return [ribbon.id, ribbon];
21+
}));
22+
this.originalRibbons = Object.assign({}, this.ribbons);
23+
this.productTemplatesRibbons = [];
24+
this.deletedRibbonClasses = '';
25+
this.ribbonPositionClasses = {
26+
left: 'o_ribbon_left o_tag_left',
27+
right: 'o_ribbon_right o_tag_right'
28+
};
29+
},
30+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import options from "@web_editor/js/editor/snippets.options";
2+
3+
options.registry.WebsiteSaleProductsItem = options.registry.WebsiteSaleProductsItem.extend({
4+
willStart: async function () {
5+
const _super = this._super.bind(this);
6+
this.ppr = this.$target.closest('[data-ppr]').data('ppr');
7+
this.defaultSort = this.$target[0].closest('[data-default-sort]').dataset.defaultSort
8+
this.productTemplateID = parseInt(this.$target.find('[data-oe-model="product.template"]').data('oe-id'));
9+
this.ribbonPositionClasses = {'left': 'o_ribbon_left o_tag_left', 'right': 'o_ribbon_right o_tag_right'};
10+
this.ribbons = await new Promise(resolve => this.trigger_up('get_ribbons', {callback: resolve}));
11+
this.$ribbon = this.$target.find('.o_ribbon');
12+
return _super(...arguments);
13+
},
14+
15+
async setRibbonPosition(previewMode, widgetValue, params) {
16+
const ribbon = this.$ribbon[0];
17+
// Get the style (tag or ribbon) from the dataset or params
18+
const ribbonData = this.ribbons[this.$target[0].dataset.ribbonId] || {};
19+
const style = params?.style || ribbonData?.style || "ribbon"; // Safe access // Remove both tag and ribbon position classes
20+
ribbon.classList.remove('o_ribbon_right', 'o_ribbon_left', 'o_tag_right', 'o_tag_left');
21+
22+
// Add only the appropriate classes based on the selected style
23+
ribbon.classList.add(style === "ribbon" ? `o_ribbon_${widgetValue}` : `o_tag_${widgetValue}`);
24+
25+
await this._saveRibbon();
26+
},
27+
28+
async _computeWidgetState(methodName, params) {
29+
const classList = this.$ribbon[0].classList;
30+
switch (methodName) {
31+
case 'setRibbon':
32+
return this.$target.attr('data-ribbon-id') || '';
33+
case 'setRibbonName':
34+
return this.$ribbon.text();
35+
case 'setRibbonPosition': {
36+
if (classList.contains('o_ribbon_left') || classList.contains('o_tag_left')) {
37+
return 'left';
38+
}
39+
return 'right';
40+
}
41+
case 'setRibbonStyle': {
42+
return this.$ribbon.attr('data-style') ||
43+
(this.$ribbon.hasClass('o_tag_left') || this.$ribbon.hasClass('o_tag_right') ? 'tag' : 'ribbon');
44+
}
45+
}
46+
return this._super(methodName, params);
47+
},
48+
49+
async _saveRibbon(isNewRibbon = false) {
50+
const text = this.$ribbon.text().trim();
51+
52+
const ribbon = {
53+
'name': text,
54+
'bg_color': this.$ribbon[0].style.backgroundColor,
55+
'text_color': this.$ribbon[0].style.color,
56+
'position': (this.$ribbon.attr('class').includes('o_ribbon_left') || this.$ribbon.attr('class').includes('o_tag_left')) ? 'left' : 'right',
57+
'style': this.$ribbon.attr('data-style') || 'ribbon',
58+
};
59+
ribbon.id = isNewRibbon ? Date.now() : parseInt(this.$target.closest('.oe_product')[0].dataset.ribbonId);
60+
this.trigger_up('set_ribbon', {ribbon: ribbon});
61+
this.ribbons = await new Promise(resolve => this.trigger_up('get_ribbons', {callback: resolve}));
62+
this.rerender = true;
63+
await this._setRibbon(ribbon.id);
64+
},
65+
66+
async _setRibbon(ribbonId) {
67+
this.$target[0].dataset.ribbonId = ribbonId;
68+
this.trigger_up('set_product_ribbon', {
69+
templateId: this.productTemplateID,
70+
ribbonId: ribbonId || false,
71+
});
72+
const ribbon = (
73+
this.ribbons[ribbonId] ||
74+
{name: '', bg_color: '', text_color: '', position: 'left', style: 'ribbon'}
75+
);
76+
// This option also manages other products' ribbon, therefore we need a
77+
// way to access all of them at once. With the content being in an iframe,
78+
// this is the simplest way.
79+
const $editableDocument = $(this.$target[0].ownerDocument.body);
80+
const $ribbons = $editableDocument.find(`[data-ribbon-id="${ribbonId}"] .o_ribbon`);
81+
$ribbons.empty().append(ribbon.name);
82+
$ribbons.removeClass('o_ribbon_left o_ribbon_right o_tag_left o_tag_right');
83+
84+
const ribbonPositionClasses = ribbon.style === 'tag'
85+
? (ribbon.position === 'left' ? 'o_tag_left' : 'o_tag_right')
86+
: (ribbon.position === 'left' ? 'o_ribbon_left' : 'o_ribbon_right');
87+
88+
$ribbons.addClass(ribbonPositionClasses);
89+
$ribbons.attr('data-style', ribbon.style);
90+
$ribbons.css('background-color', ribbon.bg_color || '');
91+
$ribbons.css('color', ribbon.text_color || '');
92+
93+
if (!this.ribbons[ribbonId]) {
94+
$editableDocument.find(`[data-ribbon-id="${ribbonId}"]`).each((index, product) => delete product.dataset.ribbonId);
95+
}
96+
97+
// The ribbon does not have a savable parent, so we need to trigger the
98+
// saving process manually by flagging the ribbon as dirty.
99+
this.$ribbon.addClass('o_dirty');
100+
},
101+
102+
async setRibbonStyle(previewMode, widgetValue, params) {
103+
this.$ribbon.attr('data-style', widgetValue);
104+
105+
await this._saveRibbon();
106+
},
107+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<odoo>
2+
<record id="inherit_product_ribbon_form_view" model="ir.ui.view">
3+
<field name="name">product.ribbon.form.view.inherit</field>
4+
<field name="model">product.ribbon</field>
5+
<field name="inherit_id" ref="website_sale.product_ribbon_form_view"/>
6+
<field name="arch" type="xml">
7+
<xpath expr="//field[@name='position']" position="after">
8+
<field name="style"/>
9+
<field name="assign"/>
10+
<div invisible="assign != 'new'" class="o_row">
11+
<span>For</span>
12+
<field name="show_period" style="max-width: 2rem;" nolabel="1"/>
13+
<span>days after publication</span>
14+
</div>
15+
</xpath>
16+
</field>
17+
</record>
18+
</odoo>

dynamic_ribbons/views/snippet.xml

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<odoo>
2+
<template id="inherit_snippet_options"
3+
inherit_id="website.snippet_options">
4+
<xpath expr="//div[@data-name='ribbon_customize_opt']/we-colorpicker[1]" position="before">
5+
<we-select string="Style" class="o_we_sublevel_1">
6+
<we-button data-set-ribbon-style="tag">Tag</we-button>
7+
<we-button data-set-ribbon-style="ribbon">Ribbon</we-button>
8+
</we-select>
9+
</xpath>
10+
</template>
11+
</odoo>

0 commit comments

Comments
 (0)