diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py index 32ad2f8c499..de1bfcc6915 100644 --- a/addons/website_sale/controllers/main.py +++ b/addons/website_sale/controllers/main.py @@ -17,65 +17,62 @@ from odoo.osv import expression _logger = logging.getLogger(__name__) -PPG = 20 # Products Per Page -PPR = 4 # Products Per Row - class TableCompute(object): def __init__(self): self.table = {} - def _check_place(self, posx, posy, sizex, sizey): + def _check_place(self, posx, posy, sizex, sizey, ppr): res = True for y in range(sizey): for x in range(sizex): - if posx + x >= PPR: + if posx + x >= ppr: res = False break row = self.table.setdefault(posy + y, {}) if row.setdefault(posx + x) is not None: res = False break - for x in range(PPR): + for x in range(ppr): self.table[posy + y].setdefault(x, None) return res - def process(self, products, ppg=PPG): + def process(self, products, ppg=20, ppr=4): # Compute products positions on the grid minpos = 0 index = 0 maxy = 0 x = 0 for p in products: - x = min(max(p.website_size_x, 1), PPR) - y = min(max(p.website_size_y, 1), PPR) + x = min(max(p.website_size_x, 1), ppr) + y = min(max(p.website_size_y, 1), ppr) if index >= ppg: x = y = 1 pos = minpos - while not self._check_place(pos % PPR, pos // PPR, x, y): + while not self._check_place(pos % ppr, pos // ppr, x, y, ppr): pos += 1 - # if 21st products (index 20) and the last line is full (PPR products in it), break - # (pos + 1.0) / PPR is the line where the product would be inserted + # if 21st products (index 20) and the last line is full (ppr products in it), break + # (pos + 1.0) / ppr is the line where the product would be inserted # maxy is the number of existing lines # + 1.0 is because pos begins at 0, thus pos 20 is actually the 21st block # and to force python to not round the division operation - if index >= ppg and ((pos + 1.0) // PPR) > maxy: + if index >= ppg and ((pos + 1.0) // ppr) > maxy: break if x == 1 and y == 1: # simple heuristic for CPU optimization - minpos = pos // PPR + minpos = pos // ppr for y2 in range(y): for x2 in range(x): - self.table[(pos // PPR) + y2][(pos % PPR) + x2] = False - self.table[pos // PPR][pos % PPR] = { + 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) } if index <= ppg: - maxy = max(maxy, y + (pos // PPR)) + maxy = max(maxy, y + (pos // ppr)) index += 1 # Format table according to HTML needs @@ -210,11 +207,13 @@ class WebsiteSale(http.Controller): if ppg: try: ppg = int(ppg) + post['ppg'] = ppg except ValueError: - ppg = PPG - post["ppg"] = ppg - else: - ppg = PPG + ppg = False + if not ppg: + ppg = request.env['website'].get_current_website().shop_ppg or 20 + + ppr = request.env['website'].get_current_website().shop_ppr or 4 attrib_list = request.httprequest.args.getlist('attrib') attrib_values = [[int(x) for x in v.split("-")] for v in attrib_list if v] @@ -279,8 +278,9 @@ class WebsiteSale(http.Controller): 'add_qty': add_qty, 'products': products, 'search_count': product_count, # common for all searchbox - 'bins': TableCompute().process(products, ppg), - 'rows': PPR, + 'bins': TableCompute().process(products, ppg, ppr), + 'ppg': ppg, + 'ppr': ppr, 'categories': categs, 'attributes': attributes, 'compute_currency': compute_currency, @@ -1075,6 +1075,14 @@ class WebsiteSale(http.Controller): product = request.env['product.template'].browse(id) return product.write({'website_size_x': x, 'website_size_y': y}) + @http.route(['/shop/change_ppg'], type='json', auth='user') + def change_ppg(self, ppg): + request.env['website'].get_current_website().shop_ppg = ppg + + @http.route(['/shop/change_ppr'], type='json', auth='user') + def change_ppr(self, ppr): + request.env['website'].get_current_website().shop_ppr = ppr + def order_lines_2_google_api(self, order_lines): """ Transforms a list of order lines into a dict for google analytics """ ret = [] diff --git a/addons/website_sale/models/website.py b/addons/website_sale/models/website.py index 5d76fdf2f7c..bc9c702760f 100644 --- a/addons/website_sale/models/website.py +++ b/addons/website_sale/models/website.py @@ -25,8 +25,8 @@ class Website(models.Model): except ValueError: return None - salesteam_id = fields.Many2one('crm.team', - string='Sales Team', + salesteam_id = fields.Many2one('crm.team', + string='Sales Team', default=_get_default_website_team) pricelist_ids = fields.One2many('product.pricelist', compute="_compute_pricelist_ids", string='Price list available for this Ecommerce/Website') @@ -40,6 +40,9 @@ class Website(models.Model): cart_recovery_mail_template_id = fields.Many2one('mail.template', string='Cart Recovery Email', default=_default_recovery_mail_template, domain="[('model', '=', 'sale.order')]") cart_abandoned_delay = fields.Float("Abandoned Delay", default=1.0) + shop_ppg = fields.Integer(default=20, string="Number of products in the grid on the shop") + shop_ppr = fields.Integer(default=4, string="Number of grid columns on the shop") + @api.one def _compute_pricelist_ids(self): """ Return the pricelists that can be used directly or indirectly on 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 c24ae506e01..53e3e4496d1 100644 --- a/addons/website_sale/static/src/js/website_sale.editor.js +++ b/addons/website_sale/static/src/js/website_sale.editor.js @@ -87,6 +87,94 @@ function reload() { } } +options.registry.WebsiteSaleGridLayout = options.Class.extend({ + xmlDependencies: ['/website_sale/static/src/xml/website_sale.editor.xml'], + + /** + * @override + */ + start: function () { + this.ppg = this.$target.closest('[data-ppg]').data('ppg'); + this.ppr = this.$target.closest('[data-ppr]').data('ppr'); + return this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * @see this.selectClass for params + */ + choosePpg: function (previewMode, value, $opt) { + var self = this; + new Dialog(this, { + title: _t("Choose number of products"), + $content: $(qweb.render('website_sale.dialog.choosePPG', {widget: this})), + buttons: [ + {text: _t("Save"), classes: 'btn-primary', click: function () { + var $input = this.$('input'); + var def = self._setPPG($input.val()); + if (!def) { + $input.addClass('is-invalid'); + return; + } + return def.then(this.close.bind(this)); + }}, + {text: _t("Discard"), close: true}, + ], + }).open(); + }, + /** + * @see this.selectClass for params + */ + setPpr: function (previewMode, value, $opt) { + this._rpc({ + route: '/shop/change_ppr', + params: { + 'ppr': value, + }, + }).then(reload); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _setActive: function () { + var self = this; + this._super.apply(this, arguments); + this.$el.find('[data-set-ppr]') + .addBack('[data-set-ppr]') + .removeClass('active') + .filter(function () { + var nbColumns = $(this).data('setPpr'); + return nbColumns === self.ppr; + }) + .addClass('active'); + }, + /** + * @private + * @param {integer} ppg + * @returns {Promise|false} + */ + _setPPG: function (ppg) { + ppg = parseInt(ppg); + if (!ppg || ppg < 1) { + return false; + } + return this._rpc({ + route: '/shop/change_ppg', + params: { + 'ppg': ppg, + }, + }).then(reload); + }, +}); + options.registry.WebsiteSaleProductsItem = options.Class.extend({ events: _.extend({}, options.Class.prototype.events || {}, { 'mouseenter .o_wsale_soptions_menu_sizes table': '_onTableMouseEnter', @@ -101,6 +189,7 @@ options.registry.WebsiteSaleProductsItem = options.Class.extend({ start: function () { var self = this; + this.ppr = this.$target.closest('[data-ppr]').data('ppr'); this.productTemplateID = parseInt(this.$target.find('[data-oe-model="product.template"]').data('oe-id')); var defs = [this._super.apply(this, arguments)]; @@ -166,6 +255,9 @@ options.registry.WebsiteSaleProductsItem = options.Class.extend({ $size.find('tr:nth-child(-n + ' + sizeY + ') td:nth-child(-n + ' + sizeX + ')') .addClass('selected'); + // Adapt size array preview to fit ppr + $size.find('tr td:nth-child(n + ' + parseInt(this.ppr + 1) + ')').hide(); + return this._super.apply(this, arguments); }, diff --git a/addons/website_sale/static/src/xml/website_sale.editor.xml b/addons/website_sale/static/src/xml/website_sale.editor.xml index 4cc12eafb1a..363d817585f 100644 --- a/addons/website_sale/static/src/xml/website_sale.editor.xml +++ b/addons/website_sale/static/src/xml/website_sale.editor.xml @@ -1,6 +1,17 @@ +
+
+ +
+ +
+
+
+
diff --git a/addons/website_sale/views/snippets.xml b/addons/website_sale/views/snippets.xml index e895c50d018..6d513f0c39b 100644 --- a/addons/website_sale/views/snippets.xml +++ b/addons/website_sale/views/snippets.xml @@ -40,6 +40,24 @@