summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--addons/website/static/src/scss/website.scss37
-rw-r--r--addons/website_event_track/static/src/css/website_event_track.css32
-rw-r--r--addons/website_event_track/views/event_track_templates.xml12
-rw-r--r--addons/website_sale/controllers/main.py24
-rw-r--r--addons/website_sale/data/data.xml16
-rw-r--r--addons/website_sale/models/product.py14
-rw-r--r--addons/website_sale/security/ir.model.access.csv3
-rw-r--r--addons/website_sale/static/src/js/website_sale.editor.js408
-rw-r--r--addons/website_sale/static/src/scss/website_sale.editor.scss6
-rw-r--r--addons/website_sale/static/src/scss/website_sale.scss27
-rw-r--r--addons/website_sale/static/src/xml/website_sale_utils.xml9
-rw-r--r--addons/website_sale/views/product_views.xml2
-rw-r--r--addons/website_sale/views/snippets.xml26
-rw-r--r--addons/website_sale/views/templates.xml11
14 files changed, 501 insertions, 126 deletions
diff --git a/addons/website/static/src/scss/website.scss b/addons/website/static/src/scss/website.scss
index 520102c1f7c..321f7792112 100644
--- a/addons/website/static/src/scss/website.scss
+++ b/addons/website/static/src/scss/website.scss
@@ -959,3 +959,40 @@ header {
background-color: rgba(theme-color('primary'), 0.8);
}
}
+
+$ribbon-padding: 100px;
+.o_ribbon {
+ margin: 0;
+ font-size: 1rem;
+ font-weight: bold;
+ white-space: nowrap;
+ text-align: center;
+ pointer-events: none;
+}
+
+.o_ribbon_right {
+ @include o-position-absolute($top: 0, $right: 0);
+ padding: 0.5rem $ribbon-padding;
+ // 0.708 is 1 - cos(45deg)
+ // Transforms are applied right-to-left
+ // Cannot use matrix because of the use of % values.
+ transform: translateX(calc(-0.708 * (100% - #{2 * $ribbon-padding}))) rotate(45deg) translateX(calc(100% - #{$ribbon-padding}));
+ transform-origin: top right;
+}
+
+.o_ribbon_left {
+ @include o-position-absolute($top: 0, $left: 0);
+ padding: 0.5rem $ribbon-padding;
+ transform: translateX(calc(0.708 * (100% - #{2 * $ribbon-padding}) - 100%)) rotate(-45deg) translateX($ribbon-padding);
+ transform-origin: top right;
+}
+
+.o_tag_right {
+ @include o-position-absolute($top: 0, $right: 0);
+ padding: 0.25rem 1rem;
+}
+
+.o_tag_left {
+ @include o-position-absolute($top: 0, $left: 0);
+ padding: 0.25rem 1rem;
+}
diff --git a/addons/website_event_track/static/src/css/website_event_track.css b/addons/website_event_track/static/src/css/website_event_track.css
index 865dc812980..0940ceea082 100644
--- a/addons/website_event_track/static/src/css/website_event_track.css
+++ b/addons/website_event_track/static/src/css/website_event_track.css
@@ -1,32 +1,20 @@
-.o_wevent_event .ribbon-wrapper {
- width: 60px;
- height: 60px;
- z-index: 5;
+.o_wevent_event .o_wevent_sponsor {
+ position: relative;
+ display: inline-block;
overflow: hidden;
- position: absolute;
- top: 0;
- right: 0;
}
-.o_wevent_event .ribbon {
- font: bold 13px Sans-Serif;
+.o_wevent_event .o_ribbon {
+ font: 12px Sans-Serif;
color: #404040;
- text-align: center;
- -webkit-transform: rotate(45deg);
- -moz-transform: rotate(45deg);
- -ms-transform: rotate(45deg);
- -o-transform: rotate(45deg);
- position: relative;
- padding: 5px 0;
- left: -4px;
- top: 8px;
- width: 90px;
+ padding-top: 2px;
+ padding-bottom: 2px;
-webkit-box-shadow: 0px 0px 3px rgba(0, 0, 0, 0.3);
-moz-box-shadow: 0px 0px 3px rgba(0, 0, 0, 0.3);
box-shadow: 0px 0px 3px rgba(0, 0, 0, 0.3);
}
-.o_wevent_event .ribbon.ribbon_Gold {
+.o_wevent_event .ribbon_Gold {
background-color: #FDE21B;
background-image: -webkit-gradient(linear, left top, left bottom, from(#E9CE0C), to(#FDE21B));
background-image: -webkit-linear-gradient(top, #E9CE0C, #FDE21B);
@@ -35,7 +23,7 @@
background-image: -o-linear-gradient(top, #E9CE0C, #FDE21B);
}
-.o_wevent_event .ribbon.ribbon_Silver {
+.o_wevent_event .ribbon_Silver {
background-color: #CCCCCC;
background-image: -webkit-gradient(linear, left top, left bottom, from(#BBBBBB), to(#CCCCCC));
background-image: -webkit-linear-gradient(top, #BBBBBB, #CCCCCC);
@@ -44,7 +32,7 @@
background-image: -o-linear-gradient(top, #BBBBBB, #CCCCCC);
}
-.o_wevent_event .ribbon.ribbon_Bronze {
+.o_wevent_event .ribbon_Bronze {
background-color: #DB9141;
background-image: -webkit-gradient(linear, left top, left bottom, from(#C2792A), to(#DB9141));
background-image: -webkit-linear-gradient(top, #C2792A, #DB9141);
diff --git a/addons/website_event_track/views/event_track_templates.xml b/addons/website_event_track/views/event_track_templates.xml
index c9003a9e3bf..020b25c1923 100644
--- a/addons/website_event_track/views/event_track_templates.xml
+++ b/addons/website_event_track/views/event_track_templates.xml
@@ -18,21 +18,17 @@
<div class="row">
<div t-attf-class="col-sm-6 col-lg-#{(len(event.sponsor_ids) > 6) and 2 or (12 // len(event.sponsor_ids))} text-center mb16 oe_sponsor" t-foreach="event.sponsor_ids" t-as="sponsor">
<t t-if="sponsor.url">
- <a t-att-href="sponsor.url" style="position: relative; display: inline-block;" class="bg-white">
+ <a t-att-href="sponsor.url" class="bg-white o_wevent_sponsor">
<span t-field="sponsor.image_128"
t-options='{"widget": "image", "class": "shadow"}'/>
- <div class="ribbon-wrapper">
- <div t-field="sponsor.sponsor_type_id" t-attf-class="ribbon ribbon_#{sponsor.sponsor_type_id.name}"/>
- </div>
+ <span t-field="sponsor.sponsor_type_id" t-attf-class="o_ribbon o_ribbon_right ribbon_#{sponsor.sponsor_type_id.name}"/>
</a>
</t>
<t t-if="not sponsor.url">
- <span style="position: relative; display: inline-block;" class="bg-white">
+ <span class="bg-white o_wevent_sponsor">
<span t-field="sponsor.image_128"
t-options='{"widget": "image", "class": "shadow"}'/>
- <div class="ribbon-wrapper">
- <div t-field="sponsor.sponsor_type_id" t-attf-class="ribbon ribbon_#{sponsor.sponsor_type_id.name}"/>
- </div>
+ <span t-field="sponsor.sponsor_type_id" t-attf-class="o_ribbon o_ribbon_right ribbon_#{sponsor.sponsor_type_id.name}"/>
</span>
</t>
</div>
diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py
index f3901529aa7..6aa56b87e99 100644
--- a/addons/website_sale/controllers/main.py
+++ b/addons/website_sale/controllers/main.py
@@ -71,7 +71,7 @@ class TableCompute(object):
self.table[(pos // ppr) + y2][(pos % ppr) + x2] = False
self.table[pos // ppr][pos % ppr] = {
'product': p, 'x': x, 'y': y,
- 'class': " ".join(x.html_class for x in p.website_style_ids if x.html_class)
+ 'ribbon': p.website_ribbon_id,
}
if index <= ppg:
maxy = max(maxy, y + (pos // ppr))
@@ -1054,28 +1054,6 @@ class WebsiteSale(http.Controller):
})
return "%s?enable_editor=1" % product.product_tmpl_id.website_url
- @http.route(['/shop/change_styles'], type='json', auth='user')
- def change_styles(self, id, style_id):
- product = request.env['product.template'].browse(id)
-
- remove = []
- active = False
- style_id = int(style_id)
- for style in product.website_style_ids:
- if style.id == style_id:
- remove.append(style.id)
- active = True
- break
-
- style = request.env['product.style'].browse(style_id)
-
- if remove:
- product.write({'website_style_ids': [(3, rid) for rid in remove]})
- if not active:
- product.write({'website_style_ids': [(4, style.id)]})
-
- return not active
-
@http.route(['/shop/change_sequence'], type='json', auth='user')
def change_sequence(self, id, sequence):
product_tmpl = request.env['product.template'].browse(id)
diff --git a/addons/website_sale/data/data.xml b/addons/website_sale/data/data.xml
index e110dd493e6..c2fddedad54 100644
--- a/addons/website_sale/data/data.xml
+++ b/addons/website_sale/data/data.xml
@@ -18,9 +18,19 @@
<field name="state">open</field>
</record>
- <record id="website_sale.image_promo" model="product.style">
- <field name="name">Sale Ribbon</field>
- <field name="html_class">oe_ribbon_promo</field>
+ <record id="website_sale.sale_ribbon" model="product.ribbon">
+ <field name="html">Sale</field>
+ <field name="html_class">bg-success o_ribbon_left</field>
+ </record>
+
+ <record id="website_sale.sold_out_ribbon" model="product.ribbon">
+ <field name="html">Sold out</field>
+ <field name="html_class">bg-danger o_ribbon_left</field>
+ </record>
+
+ <record id="website_sale.new_ribbon" model="product.ribbon">
+ <field name="html">New!</field>
+ <field name="html_class">bg-primary o_ribbon_left</field>
</record>
<record id="sales_team.salesteam_website_sales" model="crm.team">
diff --git a/addons/website_sale/models/product.py b/addons/website_sale/models/product.py
index effe4749833..57398dc3130 100644
--- a/addons/website_sale/models/product.py
+++ b/addons/website_sale/models/product.py
@@ -9,12 +9,14 @@ from odoo.tools.translate import html_translate
from odoo.osv import expression
-class ProductStyle(models.Model):
- _name = "product.style"
- _description = 'Product Style'
+class ProductRibbon(models.Model):
+ _name = "product.ribbon"
+ _description = 'Product ribbon'
- name = fields.Char(string='Style Name', required=True)
- html_class = fields.Char(string='HTML Classes')
+ html = fields.Char(string='Ribbon html', required=True)
+ bg_color = fields.Char(string='Ribbon background color', required=False)
+ text_color = fields.Char(string='Ribbon text color', required=False)
+ html_class = fields.Char(string='Ribbon class', required=True, default='')
class ProductPricelist(models.Model):
@@ -186,7 +188,7 @@ class ProductTemplate(models.Model):
help='Accessories show up when the customer reviews the cart before payment (cross-sell strategy).')
website_size_x = fields.Integer('Size X', default=1)
website_size_y = fields.Integer('Size Y', default=1)
- website_style_ids = fields.Many2many('product.style', string='Styles')
+ website_ribbon_id = fields.Many2one('product.ribbon', string='Ribbon')
website_sequence = fields.Integer('Website Sequence', help="Determine the display order in the Website E-commerce",
default=lambda self: self._default_website_sequence(), copy=False)
public_categ_ids = fields.Many2many(
diff --git a/addons/website_sale/security/ir.model.access.csv b/addons/website_sale/security/ir.model.access.csv
index 0e3f19844c8..479a8838622 100644
--- a/addons/website_sale/security/ir.model.access.csv
+++ b/addons/website_sale/security/ir.model.access.csv
@@ -6,7 +6,8 @@ access_product_category_pos_manager,product.public.category manager,model_produc
access_product_public_category_public,product.category.public,model_product_public_category,,1,0,0,0
access_product_pricelist_public,product.pricelist.public,product.model_product_pricelist,,1,0,0,0
access_product_pricelist_item_public,product.pricelist.item.public,product.model_product_pricelist_item,,1,0,0,0
-access_product_style,product.style.public,website_sale.model_product_style,,1,0,0,0
+access_product_ribbon_public,product.ribbon.public,website_sale.model_product_ribbon,,1,0,0,0
+access_product_ribbon_sale_manager,product.ribbon.sale_manager,website_sale.model_product_ribbon,sales_team.group_sale_manager,1,1,1,1
access_product_attribute_public,product.attribute public,product.model_product_attribute,,1,0,0,0
access_product_attribute_value_public,product.attribute value public,product.model_product_attribute_value,,1,0,0,0
access_product_product_attribute,product.template.attribute value public,product.model_product_template_attribute_value,,1,0,0,0
diff --git a/addons/website_sale/static/src/js/website_sale.editor.js b/addons/website_sale/static/src/js/website_sale.editor.js
index e9188bbad7d..4b1a7946ec9 100644
--- a/addons/website_sale/static/src/js/website_sale.editor.js
+++ b/addons/website_sale/static/src/js/website_sale.editor.js
@@ -54,6 +54,197 @@ odoo.define('website_sale.editor', function (require) {
var options = require('web_editor.snippets.options');
var publicWidget = require('web.public.widget');
+const {Class: EditorMenuBar} = require('web_editor.editor');
+const {qweb} = require('web.core');
+
+EditorMenuBar.include({
+ custom_events: Object.assign(EditorMenuBar.prototype.custom_events, {
+ get_ribbons: '_onGetRibbons',
+ get_ribbon_classes: '_onGetRibbonClasses',
+ delete_ribbon: '_onDeleteRibbon',
+ set_ribbon: '_onSetRibbon',
+ set_product_ribbon: '_onSetProductRibbon',
+ }),
+
+ /**
+ * @override
+ */
+ async willStart() {
+ const _super = this._super.bind(this);
+ let ribbons = [];
+ if (this._isProductListPage()) {
+ ribbons = await this._rpc({
+ model: 'product.ribbon',
+ method: 'search_read',
+ fields: ['id', 'html', 'bg_color', 'text_color', 'html_class'],
+ });
+ }
+ this.ribbons = Object.fromEntries(ribbons.map(ribbon => [ribbon.id, ribbon]));
+ this.originalRibbons = Object.assign({}, this.ribbons);
+ this.productTemplatesRibbons = [];
+ this.deletedRibbonClasses = '';
+ return _super(...arguments);
+ },
+ /**
+ * @override
+ */
+ async save() {
+ const _super = this._super.bind(this);
+ await this._saveRibbons();
+ return _super(...arguments);
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Saves the ribbons in the database.
+ *
+ * @private
+ */
+ async _saveRibbons() {
+ if (!this._isProductListPage()) {
+ return;
+ }
+ const originalIds = Object.keys(this.originalRibbons).map(id => parseInt(id));
+ const currentIds = Object.keys(this.ribbons).map(id => parseInt(id));
+
+ const ribbons = Object.values(this.ribbons);
+ const created = ribbons.filter(ribbon => !originalIds.includes(ribbon.id));
+ const deletedIds = originalIds.filter(id => !currentIds.includes(id));
+ const modified = ribbons.filter(ribbon => {
+ if (created.includes(ribbon)) {
+ return false;
+ }
+ const original = this.originalRibbons[ribbon.id];
+ return Object.entries(ribbon).some(([key, value]) => value !== original[key]);
+ });
+
+ const proms = [];
+ let createdRibbonIds;
+ if (created.length > 0) {
+ proms.push(this._rpc({
+ method: 'create',
+ model: 'product.ribbon',
+ args: [created.map(ribbon => {
+ ribbon = Object.assign({}, ribbon);
+ delete ribbon.id;
+ return ribbon;
+ })],
+ }).then(ids => createdRibbonIds = ids));
+ }
+
+ modified.forEach(ribbon => proms.push(this._rpc({
+ method: 'write',
+ model: 'product.ribbon',
+ args: [[ribbon.id], ribbon],
+ })));
+
+ if (deletedIds.length > 0) {
+ proms.push(this._rpc({
+ method: 'unlink',
+ model: 'product.ribbon',
+ args: [deletedIds],
+ }));
+ }
+
+ await Promise.all(proms);
+ const localToServer = Object.assign(
+ this.ribbons,
+ Object.fromEntries(created.map((ribbon, index) => [ribbon.id, {id: createdRibbonIds[index]}])),
+ {'false': {id: false}},
+ );
+
+ // Building the final template to ribbon-id map
+ const finalTemplateRibbons = this.productTemplatesRibbons.reduce((acc, {templateId, ribbonId}) => {
+ acc[templateId] = ribbonId;
+ return acc;
+ }, {});
+ // Inverting the relationship so that we have all templates that have the same ribbon to reduce RPCs
+ const ribbonTemplates = Object.entries(finalTemplateRibbons).reduce((acc, [templateId, ribbonId]) => {
+ if (!acc[ribbonId]) {
+ acc[ribbonId] = [];
+ }
+ acc[ribbonId].push(parseInt(templateId));
+ return acc;
+ }, {});
+ const setProductTemplateRibbons = Object.entries(ribbonTemplates)
+ // If the ribbonId that the template had no longer exists, remove the ribbon (id = false)
+ .map(([ribbonId, templateIds]) => {
+ const id = currentIds.includes(parseInt(ribbonId)) ? ribbonId : false;
+ return [id, templateIds];
+ }).map(([ribbonId, templateIds]) => this._rpc({
+ method: 'write',
+ model: 'product.template',
+ args: [templateIds, {'website_ribbon_id': localToServer[ribbonId].id}],
+ }));
+ return Promise.all(setProductTemplateRibbons);
+ },
+ /**
+ * Checks whether the current page is the product list.
+ *
+ * @private
+ */
+ _isProductListPage() {
+ return $('#products_grid').length !== 0;
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Returns a copy of this.ribbons through a callback.
+ *
+ * @private
+ */
+ _onGetRibbons(ev) {
+ ev.data.callback(Object.assign({}, this.ribbons));
+ },
+ /**
+ * Returns all ribbon classes, current and deleted, so they can be removed.
+ *
+ * @private
+ */
+ _onGetRibbonClasses(ev) {
+ const classes = Object.values(this.ribbons).reduce((classes, ribbon) => {
+ return classes + ` ${ribbon.html_class}`;
+ }, '') + this.deletedRibbonClasses;
+ ev.data.callback(classes);
+ },
+ /**
+ * Deletes a ribbon.
+ *
+ * @private
+ */
+ _onDeleteRibbon(ev) {
+ this.deletedRibbonClasses += ` ${this.ribbons[ev.data.id].html_class}`;
+ delete this.ribbons[ev.data.id];
+ },
+ /**
+ * Sets a ribbon;
+ *
+ * @private
+ */
+ _onSetRibbon(ev) {
+ const {ribbon} = ev.data;
+ const previousRibbon = this.ribbons[ribbon.id];
+ if (previousRibbon) {
+ this.deletedRibbonClasses += ` ${previousRibbon.html_class}`;
+ }
+ this.ribbons[ribbon.id] = ribbon;
+ },
+ /**
+ * Sets which ribbon is used by a product template.
+ *
+ * @private
+ */
+ _onSetProductRibbon(ev) {
+ const {templateId, ribbonId} = ev.data;
+ this.productTemplatesRibbons.push({templateId, ribbonId});
+ },
+});
publicWidget.registry.websiteSaleCurrency = publicWidget.Widget.extend({
selector: '.oe_website_sale',
@@ -154,6 +345,7 @@ options.registry.WebsiteSaleGridLayout = options.Class.extend({
});
options.registry.WebsiteSaleProductsItem = options.Class.extend({
+ xmlDependencies: (options.Class.prototype.xmlDependencies || []).concat(['/website_sale/static/src/xml/website_sale_utils.xml']),
events: _.extend({}, options.Class.prototype.events || {}, {
'mouseenter .o_wsale_soptions_menu_sizes table': '_onTableMouseEnter',
'mouseleave .o_wsale_soptions_menu_sizes table': '_onTableMouseLeave',
@@ -164,10 +356,18 @@ options.registry.WebsiteSaleProductsItem = options.Class.extend({
/**
* @override
*/
- willStart: function () {
+ willStart: async function () {
+ const _super = this._super.bind(this);
this.ppr = this.$target.closest('[data-ppr]').data('ppr');
this.productTemplateID = parseInt(this.$target.find('[data-oe-model="product.template"]').data('oe-id'));
-
+ this.ribbons = await new Promise(resolve => this.trigger_up('get_ribbons', {callback: resolve}));
+ return _super(...arguments);
+ },
+ /**
+ * @override
+ */
+ start: function () {
+ this._resetRibbonDummy();
return this._super(...arguments);
},
/**
@@ -177,6 +377,8 @@ options.registry.WebsiteSaleProductsItem = options.Class.extend({
var listLayoutEnabled = this.$target.closest('#products_grid').hasClass('o_wsale_layout_list');
this.$el.find('.o_wsale_soptions_menu_sizes')
.toggleClass('d-none', listLayoutEnabled);
+ // Ribbons may have been edited or deleted in another products' option, need to make sure they're up to date
+ this.rerender = true;
},
//--------------------------------------------------------------------------
@@ -184,16 +386,113 @@ options.registry.WebsiteSaleProductsItem = options.Class.extend({
//--------------------------------------------------------------------------
/**
+ * @override
+ */
+ selectStyle(previewMode, widgetValue, params) {
+ const proms = [this._super(...arguments)];
+ if (params.cssProperty === 'background-color' && params.colorNames.includes(widgetValue)) {
+ // Reset text-color when choosing a background-color class, so it uses the automatic text-color of the class.
+ proms.push(this.selectStyle(previewMode, '', {applyTo: '.o_wsale_ribbon_dummy', cssProperty: 'color'}));
+ }
+ return Promise.all(proms);
+ },
+ /**
* @see this.selectClass for params
*/
- style: function (previewMode, widgetValue, params) {
- this._rpc({
- route: '/shop/change_styles',
- params: {
- 'id': this.productTemplateID,
- 'style_id': params.possibleValues[params.possibleValues.length - 1],
- },
+ async setRibbon(previewMode, widgetValue, params) {
+ this.$target[0].dataset.ribbonId = widgetValue;
+ this.trigger_up('set_product_ribbon', {
+ templateId: this.productTemplateID,
+ ribbonId: widgetValue || false,
});
+ const ribbon = this.ribbons[widgetValue] || {html: '', bg_color: '', text_color: '', html_class: ''};
+ const $ribbons = $(`[data-ribbon-id="${widgetValue}"] .o_ribbon:not(.o_wsale_ribbon_dummy)`);
+ $ribbons.html(ribbon.html);
+ let htmlClasses;
+ this.trigger_up('get_ribbon_classes', {callback: classes => htmlClasses = classes});
+ $ribbons.removeClass(htmlClasses);
+
+ $ribbons.addClass(ribbon.html_class || '');
+ $ribbons.css('color', ribbon.text_color);
+ $ribbons.css('background-color', ribbon.bg_color || '');
+
+ if (!this.ribbons[widgetValue]) {
+ $(`[data-ribbon-id="${widgetValue}"]`).each((index, product) => delete product.dataset.ribbonId);
+ }
+ this._resetRibbonDummy();
+ },
+ /**
+ * @see this.selectClass for params
+ */
+ editRibbon(previewMode, widgetValue, params) {
+ this.saveMethod = 'modify';
+ this._toggleEditingUI(true);
+ },
+ /**
+ * @see this.selectClass for params
+ */
+ createRibbon(previewMode, widgetValue, params) {
+ this.saveMethod = 'create';
+ this.$ribbon.html('Ribbon text');
+ this.$ribbon.addClass('bg-primary o_ribbon_left');
+ this._toggleEditingUI(true);
+ this.isCreating = true;
+ },
+ /**
+ * @see this.selectClass for params
+ */
+ async deleteRibbon(previewMode, widgetValue, params) {
+ if (this.isCreating) {
+ // Ribbon doesn't exist yet, simply discard.
+ this.isCreating = false;
+ this._resetRibbonDummy();
+ return this._toggleEditingUI(false);
+ }
+ const {ribbonId} = this.$target[0].dataset;
+ this.trigger_up('delete_ribbon', {id: ribbonId});
+ this.ribbons = await new Promise(resolve => this.trigger_up('get_ribbons', {callback: resolve}));
+ this.rerender = true;
+ await this.setRibbon(false, ribbonId);
+ },
+ /**
+ * @see this.selectClass for params
+ */
+ async saveRibbon(previewMode, widgetValue, params) {
+ const text = this.$ribbon.html().trim();
+ if (!text) {
+ return;
+ }
+ const ribbon = {
+ 'html': text,
+ 'bg_color': this.$ribbon[0].style.backgroundColor,
+ 'text_color': this.$ribbon[0].style.color,
+ 'html_class': this.$ribbon.attr('class').split(' ')
+ .filter(c => !['d-none', 'o_wsale_ribbon_dummy', 'o_ribbon'].includes(c))
+ .join(' '),
+ };
+ ribbon.id = this.saveMethod === 'modify' ? parseInt(this.$target[0].dataset.ribbonId) : Date.now();
+ this.trigger_up('set_ribbon', {ribbon: ribbon});
+ this.ribbons = await new Promise(resolve => this.trigger_up('get_ribbons', {callback: resolve}));
+ this.rerender = true;
+ await this.setRibbon(false, ribbon.id);
+ },
+ /**
+ * @see this.selectClass for params
+ */
+ setRibbonHtml(previewMode, widgetValue, params) {
+ this.$ribbon.html(widgetValue);
+ },
+ /**
+ * @see this.selectClass for params
+ */
+ setRibbonMode(previewMode, widgetValue, params) {
+ this.$ribbon[0].className = this.$ribbon[0].className.replace(/o_(ribbon|tag)_(left|right)/, `o_${widgetValue}_$2`);
+ },
+ /**
+ * @see this.selectClass for params
+ */
+ setRibbonPosition(previewMode, widgetValue, params) {
+ this.$ribbon[0].className = this.$ribbon[0].className.replace(/o_(ribbon|tag)_(left|right)/, `o_$1_${widgetValue}`);
},
/**
* @see this.selectClass for params
@@ -227,6 +526,10 @@ options.registry.WebsiteSaleProductsItem = options.Class.extend({
// Adapt size array preview to fit ppr
$size.find('tr td:nth-child(n + ' + parseInt(this.ppr + 1) + ')').hide();
+ if (this.rerender) {
+ this.rerender = false;
+ return this._rerenderXML();
+ }
},
//--------------------------------------------------------------------------
@@ -236,28 +539,75 @@ options.registry.WebsiteSaleProductsItem = options.Class.extend({
/**
* @override
*/
- _renderCustomWidgets: async function (uiFragment) {
- const checkboxes = [];
- return this._rpc({
- model: 'product.style',
- method: 'search_read',
- }).then(async data => {
- for (var k in data) {
- const checkboxWidget = this._registerUserValueWidget('we-checkbox', this, data[k]['name'], {
- dataAttributes: {
- 'style': data[k]['id'],
- 'selectClass': data[k]['html_class'],
- },
- });
- checkboxes.push(checkboxWidget);
- await checkboxWidget.appendTo(document.createDocumentFragment());
+ async _renderCustomXML(uiFragment) {
+ const $select = $(uiFragment.querySelector('.o_wsale_ribbon_select'));
+ this.ribbons = await new Promise(resolve => this.trigger_up('get_ribbons', {callback: resolve}));
+ if (!this.$ribbon) {
+ this._resetRibbonDummy();
+ }
+ const classes = this.$ribbon[0].className;
+ this.$ribbon[0].className = '';
+ const defaultTextColor = window.getComputedStyle(this.$ribbon[0]).color;
+ this.$ribbon[0].className = classes;
+ Object.values(this.ribbons).forEach(ribbon => {
+ const colorClasses = ribbon.html_class
+ .split(' ')
+ .filter(className => !/^o_(ribbon|tag)_(left|right)$/.test(className))
+ .join(' ');
+ $select.append(qweb.render('website_sale.ribbonSelectItem', {
+ ribbon,
+ colorClasses,
+ isTag: /o_tag_(left|right)/.test(ribbon.html_class),
+ isLeft: /o_(tag|ribbon)_left/.test(ribbon.html_class),
+ textColor: ribbon.text_color || colorClasses ? 'currentColor' : defaultTextColor,
+ }));
+ });
+ },
+ /**
+ * @override
+ */
+ async _computeWidgetState(methodName, params) {
+ const classList = this.$ribbon[0].classList;
+ switch (methodName) {
+ case 'setRibbon':
+ return this.$target.attr('data-ribbon-id') || '';
+ case 'setRibbonHtml':
+ return this.$ribbon.html();
+ case 'setRibbonMode': {
+ if (classList.contains('o_ribbon_left') || classList.contains('o_ribbon_right')) {
+ return 'ribbon';
+ }
+ return 'tag';
}
- }).then(() => {
- const menuEl = uiFragment.querySelector('[name="style"]');
- for (const checkbox of checkboxes) {
- menuEl.appendChild(checkbox.el);
+ case 'setRibbonPosition': {
+ if (classList.contains('o_tag_left') || classList.contains('o_ribbon_left')) {
+ return 'left';
+ }
+ return 'right';
}
- });
+ }
+ return this._super(methodName, params);
+ },
+ /**
+ * Toggles the UI mode between select and create/edit mode.
+ *
+ * @private
+ * @param {Boolean} state true to activate editing UI, false to deactivate.
+ */
+ _toggleEditingUI(state) {
+ this.$el.find('[data-name="ribbon_options"]').toggleClass('d-none', state);
+ this.$el.find('[data-name="ribbon_customize_opt"]').toggleClass('d-none', !state);
+ },
+ /**
+ * Creates a copy of current ribbon to manipulate for edition/creation.
+ *
+ * @private
+ */
+ _resetRibbonDummy() {
+ if (this.$ribbon) {
+ this.$ribbon.remove();
+ }
+ this.$ribbon = this.$('.o_ribbon').clone().addClass('d-none o_wsale_ribbon_dummy').appendTo(this.$target);
},
//--------------------------------------------------------------------------
diff --git a/addons/website_sale/static/src/scss/website_sale.editor.scss b/addons/website_sale/static/src/scss/website_sale.editor.scss
index 729bde03af2..658ed4f5e42 100644
--- a/addons/website_sale/static/src/scss/website_sale.editor.scss
+++ b/addons/website_sale/static/src/scss/website_sale.editor.scss
@@ -26,6 +26,12 @@
}
}
+.o_wsale_color_preview {
+ width: 1em;
+ height: 1em;
+ border: 1px solid white;
+}
+
.oe_drop_zone + .s_wsale_products_recently_viewed {
display: block !important;
}
diff --git a/addons/website_sale/static/src/scss/website_sale.scss b/addons/website_sale/static/src/scss/website_sale.scss
index cb9b44b8ed4..42b2006a1e0 100644
--- a/addons/website_sale/static/src/scss/website_sale.scss
+++ b/addons/website_sale/static/src/scss/website_sale.scss
@@ -132,28 +132,6 @@ $o-wsale-products-layout-grid-gutter-width: min($grid-gutter-width / 2, $o-wsale
transform: scale(1);
}
}
-
- // Ribbon for promotions on products
- .ribbon-wrapper {
- display: none;
- width: 85px;
- height: 88px;
- z-index: 5;
- overflow: hidden;
- @include o-position-absolute($left: 0, $top: 0);
- }
- .ribbon {
- font: bold 15px Sans-Serif;
- color: white;
- text-align: center;
- transform: rotate(-45deg);
- position: relative;
- padding: 7px 0;
- left: -31px;
- top: 14px;
- width: 120px;
- cursor: default;
- }
}
// Options relative to where the product card is put
@@ -167,11 +145,6 @@ $o-wsale-products-layout-grid-gutter-width: min($grid-gutter-width / 2, $o-wsale
@include o-position-absolute(auto, 0, 0, 0); // The wrapper is always relatively positioned
}
}
-
- // Enable product promo ribbon
- &.oe_ribbon_promo .ribbon-wrapper {
- display: block;
- }
}
#products_grid {
diff --git a/addons/website_sale/static/src/xml/website_sale_utils.xml b/addons/website_sale/static/src/xml/website_sale_utils.xml
index f1cf6a40c6a..8a470613325 100644
--- a/addons/website_sale/static/src/xml/website_sale_utils.xml
+++ b/addons/website_sale/static/src/xml/website_sale_utils.xml
@@ -37,4 +37,13 @@
</t>
</div>
+<!-- Products Search Bar autocomplete item -->
+<we-button t-name="website_sale.ribbonSelectItem" t-att-data-set-ribbon="ribbon.id">
+ <t t-raw="ribbon.html"/>
+ <span t-attf-class="fa fa-#{isTag ? 'tag' : 'bookmark'} ml-auto"></span>
+ <span t-attf-class="fa fa-arrow-#{isLeft ? 'left' : 'right'} ml-1"></span>
+ <span t-attf-class="o_wsale_color_preview #{colorClasses} ml-1" t-attf-style="background-color: #{ribbon.bg_color}"></span>
+ <span t-attf-class="o_wsale_color_preview #{colorClasses} ml-1" t-attf-style="background-color: #{textColor} !important;"></span>
+</we-button>
+
</templates>
diff --git a/addons/website_sale/views/product_views.xml b/addons/website_sale/views/product_views.xml
index 2a7e1c81d94..f8387d9710b 100644
--- a/addons/website_sale/views/product_views.xml
+++ b/addons/website_sale/views/product_views.xml
@@ -94,7 +94,7 @@
<field name="public_categ_ids" widget="many2many_tags" string="Categories"/>
<field name="alternative_product_ids" widget="many2many_tags" domain="[('id', '!=', active_id), '|', ('company_id', '=', company_id), ('company_id', '=', False)]"/>
<field name="accessory_product_ids" widget="many2many_tags"/>
- <field name="website_style_ids" widget="many2many_tags" groups="base.group_no_one"/>
+ <field name="website_ribbon_id" groups="base.group_no_one"/>
</group>
</group>
<group name="product_template_images" string="Extra Product Media">
diff --git a/addons/website_sale/views/snippets.xml b/addons/website_sale/views/snippets.xml
index 41810ec0ff2..3a3dec8dd97 100644
--- a/addons/website_sale/views/snippets.xml
+++ b/addons/website_sale/views/snippets.xml
@@ -62,8 +62,30 @@
</table>
</div>
- <div name="style" data-no-preview="true">
- <!-- Placeholder to be filled in JS -->
+ <we-row data-name="ribbon_options">
+ <we-select string="Ribbon" data-no-preview="true" class="o_wsale_ribbon_select">
+ <we-button data-set-ribbon="" data-name="no_ribbon_opt">None</we-button>
+ <!-- Ribbons are filled in JS -->
+ </we-select>
+ <we-button data-edit-ribbon="" class="fa fa-edit" data-no-preview="true" data-dependencies="!no_ribbon_opt"/>
+ <we-button data-create-ribbon="" class="fa fa-plus text-success" data-no-preview="true"/>
+ </we-row>
+ <div class="d-none" data-name="ribbon_customize_opt">
+ <we-row string="Ribbon">
+ <we-input data-set-ribbon-html="" data-apply-to=".o_wsale_ribbon_dummy" data-no-preview="true"/>
+ <we-button class="fa fa-check" data-save-ribbon="" title="Validate" data-no-preview="true"/>
+ <we-button class="fa fa-trash" data-delete-ribbon="" title="Delete" data-no-preview="true"/>
+ </we-row>
+ <we-colorpicker string="Background Color" title="" data-select-style="" data-apply-to=".o_wsale_ribbon_dummy" data-css-property="background-color" data-color-prefix="bg-" data-no-preview="true"/>
+ <we-colorpicker string="Text Color" title="" data-select-style="" data-apply-to=".o_wsale_ribbon_dummy" data-css-property="color" data-no-preview="true"/>
+ <we-select string="Mode">
+ <we-button data-set-ribbon-mode="ribbon">Slanted</we-button>
+ <we-button data-set-ribbon-mode="tag">Tag</we-button>
+ </we-select>
+ <we-select string="Position">
+ <we-button data-set-ribbon-position="left">Left</we-button>
+ <we-button data-set-ribbon-position="right">Right</we-button>
+ </we-select>
</div>
<div name="reordering" data-no-preview="true">
diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml
index 9a0cba9ea4f..e814f8570b8 100644
--- a/addons/website_sale/views/templates.xml
+++ b/addons/website_sale/views/templates.xml
@@ -132,9 +132,6 @@
itemscope="itemscope" itemtype="http://schema.org/Product">
<div class="card-body p-1 oe_product_image">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()" />
- <div class="ribbon-wrapper">
- <a href="#" role="button" class="ribbon btn btn-danger">Sale</a>
- </div>
<a t-att-href="product_href" class="d-block h-100" itemprop="url">
<span t-field="product.image_1920"
t-options="{'widget': 'image', 'preview_image': 'image_1024' if product_image_big else 'image_256'}"
@@ -156,6 +153,10 @@
</div>
<div class="o_wsale_product_btn"/>
</div>
+ <t t-set="bg_color" t-value="td_product['ribbon']['bg_color'] or ''"/>
+ <t t-set="text_color" t-value="td_product['ribbon']['text_color']"/>
+ <t t-set="bg_class" t-value="td_product['ribbon']['html_class']"/>
+ <span t-attf-class="o_ribbon #{bg_class}" t-attf-style="#{text_color and ('color: %s; ' % text_color)}#{bg_color and 'background-color:' + bg_color}" t-raw="td_product['ribbon']['html'] or ''"/>
</form>
</template>
@@ -232,9 +233,11 @@
<t t-foreach="tr_product" t-as="td_product">
<t t-if="td_product">
<t t-set="product" t-value="td_product['product']" />
+ <!-- We use t-attf-class here to allow easier customization -->
<td t-att-colspan="td_product['x'] != 1 and td_product['x']"
t-att-rowspan="td_product['y'] != 1 and td_product['y']"
- t-attf-class="oe_product #{td_product['class']}">
+ t-attf-class="oe_product"
+ t-att-data-ribbon-id="td_product['ribbon'].id">
<div t-attf-class="o_wsale_product_grid_wrapper o_wsale_product_grid_wrapper_#{td_product['x']}_#{td_product['y']}">
<t t-call="website_sale.products_item">
<t t-set="product_image_big" t-value="td_product['x'] + td_product['y'] &gt; 2"/>