[IMP] website_sale: allow custom PPR and PPG
Part of https://github.com/odoo/odoo/pull/31147 task-1925327
This commit is contained in:
@@ -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 = []
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<div t-name="website_sale.dialog.choosePPG">
|
||||
<div class="form-group row">
|
||||
<label for="ppg_input" class="col-lg-auto col-form-label">
|
||||
Choose the number of products to display
|
||||
</label>
|
||||
<div class="col">
|
||||
<input type="number" id="ppg_input" class="form-control" t-att-value="widget.ppg"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Products Search Bar settings dialog -->
|
||||
<div t-name="website_sale.dialog.productsSearchBar">
|
||||
<div class="form-group">
|
||||
|
||||
@@ -40,6 +40,24 @@
|
||||
|
||||
<template id="snippet_options" inherit_id="website.snippet_options" name="e-commerce snippet options">
|
||||
<xpath expr="." position="inside">
|
||||
<div data-js="WebsiteSaleGridLayout"
|
||||
data-selector="#products_grid > table"
|
||||
data-no-check="true">
|
||||
<a href="#" class="dropdown-item" role="menuitem" data-no-preview="true" data-choose-ppg="1">
|
||||
<i class="fa fa-cart-plus"/>Choose number of products
|
||||
</a>
|
||||
<div class="dropdown-submenu" role="menuitem" aria-haspopup="true">
|
||||
<a tabindex="-1" href="#" class="dropdown-item" role="menuitem">
|
||||
<i class="fa fa-columns"/>Number of Columns
|
||||
</a>
|
||||
<div class="dropdown-menu" role="menu" data-no-preview="true">
|
||||
<a href="#" class="dropdown-item" role="menuitem" data-set-ppr="2">2</a>
|
||||
<a href="#" class="dropdown-item" role="menuitem" data-set-ppr="3">3</a>
|
||||
<a href="#" class="dropdown-item" role="menuitem" data-set-ppr="4">4</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-js="WebsiteSaleProductsItem"
|
||||
data-selector="#products_grid .oe_product"
|
||||
data-no-check="true">
|
||||
|
||||
@@ -218,10 +218,10 @@
|
||||
<t t-set='editor_msg'>Drag building blocks here to customize the header for "<t t-esc='category.name'/>" category.</t>
|
||||
<div class="mb16" id="category_header" t-att-data-editor-message="editor_msg" t-field="category.website_description"/>
|
||||
</t>
|
||||
<table t-if="bins" class="table table-borderless my-2">
|
||||
<table t-if="bins" class="table table-borderless my-2" t-att-data-ppg="ppg" t-att-data-ppr="ppr">
|
||||
<colgroup t-ignore="true">
|
||||
<!-- Force the number of columns (useful when only one row of (x < ppr) products) -->
|
||||
<col t-foreach="rows" t-as="p"/>
|
||||
<col t-foreach="ppr" t-as="p"/>
|
||||
</colgroup>
|
||||
<tbody>
|
||||
<tr t-foreach="bins" t-as="tr_product">
|
||||
|
||||
Reference in New Issue
Block a user