diff --git a/addons/point_of_sale/data/point_of_sale_demo.xml b/addons/point_of_sale/data/point_of_sale_demo.xml
index ca4625d7826..9cd2b62d421 100644
--- a/addons/point_of_sale/data/point_of_sale_demo.xml
+++ b/addons/point_of_sale/data/point_of_sale_demo.xml
@@ -40,14 +40,14 @@
Preparation Printer
localhost
-
+
-
+
-
+
True
1.98
Wall Shelf Unit
@@ -56,11 +56,11 @@
0.01
True
2100002000003
-
+
-
-
-
+
+
+
@@ -70,12 +70,12 @@
FURN_0008
product
0.01
-
+
-
+
True
-
-
+
+
@@ -88,9 +88,9 @@
0.01
True
-
-
-
+
+
+
@@ -103,11 +103,11 @@
product
0.01
-
-
-
+
+
+
-
+
@@ -183,9 +183,9 @@
2301000000006
True
-
-
-
+
+
+
@@ -197,9 +197,9 @@
0.01
True
-
-
-
+
+
+
@@ -211,9 +211,9 @@
0.01
True
-
-
-
+
+
+
@@ -225,8 +225,8 @@
product
0.01
-
-
+
+
@@ -239,9 +239,9 @@
0.01
True
-
-
-
+
+
+
@@ -255,9 +255,9 @@
True
2100001000004
-
-
-
+
+
+
@@ -269,114 +269,114 @@
CONS_0001
True
-
-
-
+
+
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
diff --git a/addons/point_of_sale/data/point_of_sale_onboarding.xml b/addons/point_of_sale/data/point_of_sale_onboarding.xml
index f5b12af823f..386757f2044 100644
--- a/addons/point_of_sale/data/point_of_sale_onboarding.xml
+++ b/addons/point_of_sale/data/point_of_sale_onboarding.xml
@@ -17,11 +17,11 @@
0.01
True
2100002000003
-
+
-
-
-
+
+
+
@@ -32,12 +32,12 @@
FURN_0008
product
0.01
-
+
-
+
True
-
-
+
+
@@ -50,9 +50,9 @@
0.01
True
-
-
-
+
+
+
@@ -66,9 +66,9 @@
product
0.01
-
-
-
+
+
+
@@ -80,9 +80,9 @@
CONS_0001
True
-
-
-
+
+
+
diff --git a/addons/point_of_sale/models/pos_category.py b/addons/point_of_sale/models/pos_category.py
index 7e477dc5248..31bf309ed4d 100644
--- a/addons/point_of_sale/models/pos_category.py
+++ b/addons/point_of_sale/models/pos_category.py
@@ -1,5 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from typing import List, Tuple
+
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError, UserError
@@ -24,14 +27,13 @@ class PosCategory(models.Model):
# field to determine whether a pos.category has an image or not.
has_image = fields.Boolean(compute='_compute_has_image')
- def name_get(self):
- def get_names(cat):
- res = []
- while cat:
- res.append(cat.name)
- cat = cat.parent_id
- return res
- return [(cat.id, " / ".join(reversed(get_names(cat)))) for cat in self if cat.name]
+ def _get_hierarchy(self) -> List[str]:
+ """ Returns a list representing the hierarchy of the categories. """
+ self.ensure_one()
+ return (self.parent_id._get_hierarchy() if self.parent_id else []) + [self.name]
+
+ def name_get(self) -> List[Tuple[int, str]]:
+ return [(category.id, " / ".join(category._get_hierarchy())) for category in self if category.name]
@api.ondelete(at_uninstall=False)
def _unlink_except_session_open(self):
diff --git a/addons/point_of_sale/models/pos_config.py b/addons/point_of_sale/models/pos_config.py
index 958d2bdbc82..7001fed9017 100644
--- a/addons/point_of_sale/models/pos_config.py
+++ b/addons/point_of_sale/models/pos_config.py
@@ -654,7 +654,10 @@ class PosConfig(models.Model):
t.available_in_pos
AND t.sale_ok
AND (t.company_id=%(company_id)s OR t.company_id IS NULL)
- AND %(available_categ_ids)s IS NULL OR t.pos_categ_id=ANY(%(available_categ_ids)s)
+ AND (%(available_categ_ids)s IS NULL OR EXISTS (
+ SELECT 1 FROM pos_category_product_template_rel
+ WHERE product_template_id = t.id
+ AND pos_category_id = ANY(%(available_categ_ids)s)))
) OR p.id=%(tip_product_id)s
ORDER BY t.priority DESC,
t.detailed_type DESC,
diff --git a/addons/point_of_sale/models/pos_session.py b/addons/point_of_sale/models/pos_session.py
index d462ba4a972..f4119017bcc 100644
--- a/addons/point_of_sale/models/pos_session.py
+++ b/addons/point_of_sale/models/pos_session.py
@@ -1913,7 +1913,7 @@ class PosSession(models.Model):
('company_id', '=', self.config_id.company_id.id), ('company_id', '=', False)
]
if self.config_id.limit_categories and self.config_id.iface_available_categ_ids:
- domain = AND([domain, [('pos_categ_id', 'in', self.config_id.iface_available_categ_ids.ids)]])
+ domain = AND([domain, [('pos_categ_ids', 'in', self.config_id.iface_available_categ_ids.ids)]])
if self.config_id.iface_tipproduct:
domain = OR([domain, [('id', '=', self.config_id.tip_product_id.id)]])
@@ -1921,7 +1921,7 @@ class PosSession(models.Model):
'search_params': {
'domain': domain,
'fields': [
- 'display_name', 'lst_price', 'standard_price', 'categ_id', 'pos_categ_id', 'taxes_id', 'barcode',
+ 'display_name', 'lst_price', 'standard_price', 'categ_id', 'pos_categ_ids', 'taxes_id', 'barcode',
'default_code', 'to_weight', 'uom_id', 'description_sale', 'description', 'product_tmpl_id', 'tracking',
'write_date', 'available_in_pos', 'attribute_line_ids', 'active', 'image_128'
],
@@ -2049,42 +2049,6 @@ class PosSession(models.Model):
return amount
- def get_total_sold_refund_per_category(self, group_by_user_id=None):
- total_sold_per_user_per_category = {}
- total_refund_per_user_per_category = {}
-
- for order in self.order_ids:
- if group_by_user_id:
- user_id = order.user_id.id
- else:
- # use a user_id of 0 to keep the logic between with user group and without user group the same
- user_id = 0
-
- if user_id not in total_sold_per_user_per_category:
- total_sold_per_user_per_category[user_id] = {}
- total_refund_per_user_per_category[user_id] = {}
-
- total_sold_per_category = total_sold_per_user_per_category[user_id]
- total_refund_per_category = total_refund_per_user_per_category[user_id]
-
- for line in order.lines:
- key = line.product_id.pos_categ_id.name or "None"
- if line.qty >= 0:
- if key in total_sold_per_category:
- total_sold_per_category[key] += line.price_subtotal_incl
- else:
- total_sold_per_category[key] = line.price_subtotal_incl
- else:
- if key in total_refund_per_category:
- total_refund_per_category[key] += line.price_subtotal_incl
- else:
- total_refund_per_category[key] = line.price_subtotal_incl
-
- if group_by_user_id or not total_sold_per_user_per_category:
- return list(total_sold_per_user_per_category.items()), list(total_refund_per_user_per_category.items())
- else:
- return list(total_sold_per_user_per_category[0].items()), list(total_refund_per_user_per_category[0].items())
-
def get_pos_ui_product_pricelists_by_ids(self, pricelist_ids):
params = self._loader_params_product_pricelist()
params['search_params']['domain'] = [('id', 'in', pricelist_ids)]
diff --git a/addons/point_of_sale/models/product.py b/addons/point_of_sale/models/product.py
index 32db9580623..99f3b0e3869 100644
--- a/addons/point_of_sale/models/product.py
+++ b/addons/point_of_sale/models/product.py
@@ -12,7 +12,7 @@ class ProductTemplate(models.Model):
available_in_pos = fields.Boolean(string='Available in POS', help='Check if you want this product to appear in the Point of Sale.', default=False)
to_weight = fields.Boolean(string='To Weigh With Scale', help="Check if the product should be weighted using the hardware scale integration.")
- pos_categ_id = fields.Many2one(
+ pos_categ_ids = fields.Many2many(
'pos.category', string='Point of Sale Category',
help="Category used in the Point of Sale.")
diff --git a/addons/point_of_sale/models/report_sale_details.py b/addons/point_of_sale/models/report_sale_details.py
index 050b31d9ca6..faa0de67da3 100644
--- a/addons/point_of_sale/models/report_sale_details.py
+++ b/addons/point_of_sale/models/report_sale_details.py
@@ -272,11 +272,12 @@ class ReportSaleDetails(models.AbstractModel):
}
def _get_products_and_taxes_dict(self, line, products, taxes, currency):
- key1 = line.product_id.product_tmpl_id.pos_categ_id.name
key2 = (line.product_id, line.price_unit, line.discount)
- products.setdefault(key1, {})
- products[key1].setdefault(key2, 0.0)
- products[key1][key2] += line.qty
+ keys1 = line.product_id.product_tmpl_id.pos_categ_ids.mapped("name")
+ for key1 in keys1:
+ products.setdefault(key1, {})
+ products[key1].setdefault(key2, 0.0)
+ products[key1][key2] += line.qty
if line.tax_ids_after_fiscal_position:
line_taxes = line.tax_ids_after_fiscal_position.sudo().compute_all(line.price_unit * (1-(line.discount or 0.0)/100.0), currency, line.qty, product=line.product_id, partner=line.order_id.partner_id or False)
@@ -293,17 +294,20 @@ class ReportSaleDetails(models.AbstractModel):
def _get_total_and_qty_per_category(self, categories):
all_qty = 0
all_total = 0
+ total = lambda product: (product['quantity'] * product['price_unit']) * (100 - product['discount']) / 100
for category_dict in categories:
qty_cat = 0
total_cat = 0
for product in category_dict['products']:
qty_cat += product['quantity']
- total_cat += (product['quantity'] * product['price_unit']) * (100 - product['discount']) / 100
- product['total_paid'] = (product['quantity'] * product['price_unit']) * (100 - product['discount']) / 100
+ product['total_paid'] = total(product)
+ total_cat += product['total_paid']
category_dict['total'] = total_cat
category_dict['qty'] = qty_cat
- all_qty += qty_cat
- all_total += total_cat
+ # IMPROVEMENT: It would be better if the `products` are grouped by pos.order.line.id.
+ unique_products = list({tuple(sorted(product.items())): product for category in categories for product in category['products']}.values())
+ all_qty = sum([product['quantity'] for product in unique_products])
+ all_total = sum([total(product) for product in unique_products])
return categories, {'total': all_total, 'qty': all_qty}
diff --git a/addons/point_of_sale/report/pos_order_report.py b/addons/point_of_sale/report/pos_order_report.py
index e76bfc93c96..91d19aea55c 100644
--- a/addons/point_of_sale/report/pos_order_report.py
+++ b/addons/point_of_sale/report/pos_order_report.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from odoo import api, fields, models, tools
+from odoo import fields, models, tools
class PosOrderReport(models.Model):
@@ -32,7 +32,6 @@ class PosOrderReport(models.Model):
product_categ_id = fields.Many2one('product.category', string='Product Category', readonly=True)
invoiced = fields.Boolean(readonly=True)
config_id = fields.Many2one('pos.config', string='Point of Sale', readonly=True)
- pos_categ_id = fields.Many2one('pos.category', string='PoS Category', readonly=True)
pricelist_id = fields.Many2one('product.pricelist', string='Pricelist', readonly=True)
session_id = fields.Many2one('pos.session', string='Session', readonly=True)
margin = fields.Float(string='Margin', readonly=True)
@@ -62,7 +61,6 @@ class PosOrderReport(models.Model):
pt.categ_id AS product_categ_id,
p.product_tmpl_id,
ps.config_id,
- pt.pos_categ_id,
s.pricelist_id,
s.session_id,
s.account_move IS NOT NULL AS invoiced,
@@ -88,7 +86,7 @@ class PosOrderReport(models.Model):
s.user_id, s.company_id, s.sale_journal,
s.pricelist_id, s.account_move, s.create_date, s.session_id,
l.product_id,
- pt.categ_id, pt.pos_categ_id,
+ pt.categ_id,
p.product_tmpl_id,
ps.config_id
"""
diff --git a/addons/point_of_sale/static/src/app/store/db.js b/addons/point_of_sale/static/src/app/store/db.js
index c642e20cf64..50f8a1f9a1d 100644
--- a/addons/point_of_sale/static/src/app/store/db.js
+++ b/addons/point_of_sale/static/src/app/store/db.js
@@ -133,17 +133,7 @@ export class PosDB {
}
make_ancestors(this.root_category_id, []);
}
- category_contains(categ_id, product_id) {
- var product = this.product_by_id[product_id];
- if (product) {
- var cid = product.pos_categ_id[0];
- while (cid && cid !== categ_id) {
- cid = this.category_parent[cid];
- }
- return !!cid;
- }
- return false;
- }
+
/* loads a record store from the database. returns default if nothing is found */
load(store, deft) {
if (CACHE[store] !== undefined) {
@@ -193,33 +183,35 @@ export class PosDB {
}
if (product.available_in_pos) {
var search_string = unaccent(this._product_search_string(product));
- var categ_id = product.pos_categ_id
- ? product.pos_categ_id[0]
- : this.root_category_id;
+ const all_categ_ids = product.pos_categ_ids.length
+ ? product.pos_categ_ids
+ : [this.root_category_id];
product.product_tmpl_id = product.product_tmpl_id[0];
- if (!stored_categories[categ_id]) {
- stored_categories[categ_id] = [];
- }
- stored_categories[categ_id].push(product.id);
-
- if (this.category_search_string[categ_id] === undefined) {
- this.category_search_string[categ_id] = "";
- }
- this.category_search_string[categ_id] += search_string;
-
- var ancestors = this.get_category_ancestors_ids(categ_id) || [];
-
- for (var j = 0, jlen = ancestors.length; j < jlen; j++) {
- var ancestor = ancestors[j];
- if (!stored_categories[ancestor]) {
- stored_categories[ancestor] = [];
+ for (const categ_id of all_categ_ids) {
+ if (!stored_categories[categ_id]) {
+ stored_categories[categ_id] = [];
}
- stored_categories[ancestor].push(product.id);
+ stored_categories[categ_id].push(product.id);
- if (this.category_search_string[ancestor] === undefined) {
- this.category_search_string[ancestor] = "";
+ if (this.category_search_string[categ_id] === undefined) {
+ this.category_search_string[categ_id] = "";
+ }
+ this.category_search_string[categ_id] += search_string;
+
+ var ancestors = this.get_category_ancestors_ids(categ_id) || [];
+
+ for (var j = 0, jlen = ancestors.length; j < jlen; j++) {
+ var ancestor = ancestors[j];
+ if (!stored_categories[ancestor]) {
+ stored_categories[ancestor] = [];
+ }
+ stored_categories[ancestor].push(product.id);
+
+ if (this.category_search_string[ancestor] === undefined) {
+ this.category_search_string[ancestor] = "";
+ }
+ this.category_search_string[ancestor] += search_string;
}
- this.category_search_string[ancestor] += search_string;
}
}
this.product_by_id[product.id] = product;
@@ -408,13 +400,18 @@ export class PosDB {
}
return undefined;
}
+
+ shouldAddProduct(product, list) {
+ return product.active && product.available_in_pos && !list.includes(product);
+ }
+
get_product_by_category(category_id) {
var product_ids = this.product_by_category_id[category_id];
var list = [];
if (product_ids) {
for (var i = 0, len = Math.min(product_ids.length, this.limit); i < len; i++) {
const product = this.product_by_id[product_ids[i]];
- if (!(product.active && product.available_in_pos)) {
+ if (!this.shouldAddProduct(product, list)) {
continue;
}
list.push(product);
@@ -441,7 +438,7 @@ export class PosDB {
if (r) {
var id = Number(r[1]);
const product = this.get_product_by_id(id);
- if (!(product.active && product.available_in_pos)) {
+ if (!this.shouldAddProduct(product, results)) {
continue;
}
results.push(product);
@@ -451,22 +448,41 @@ export class PosDB {
}
return results;
}
- /* from a product id, and a list of category ids, returns
- * true if the product belongs to one of the provided category
+ /**
+ * returns true if the product belongs to one of the provided categories
* or one of its child categories.
+ * @param {number[]} category_ids
+ * @param {number} product_id
+ * @returns {boolean}
*/
is_product_in_category(category_ids, product_id) {
- let cat = this.get_product_by_id(product_id).pos_categ_id[0];
- while (cat) {
- for (const cat_id of category_ids) {
- if (cat == cat_id) {
- // The == is important, ids may be strings
- return true;
- }
- }
- cat = this.get_category_parent_id(cat);
- }
- return false;
+ const product_categ_ids = this.get_product_by_id(product_id).pos_categ_ids;
+ return this.any_of_is_subcategory(product_categ_ids, category_ids);
+ }
+
+ /**
+ * Recursively check if any of `subcategory_ids` belongs to any of the `category_ids`
+ * @param {number[]} subcategory_ids
+ * @param {number[]} category_ids
+ * @returns {boolean}
+ */
+ any_of_is_subcategory(subcategory_ids, category_ids) {
+ return subcategory_ids.some((subcategory_id) =>
+ this.is_subcategory(subcategory_id, category_ids)
+ );
+ }
+ /**
+ * Recursively check if a `subcategory_id` a child of any of the provided `category_ids`.
+ * @param {number} subcategory_id
+ * @param {number[]} category_ids
+ * @returns {boolean}
+ */
+ is_subcategory(subcategory_id, category_ids) {
+ const check = (categ_id) => categ_id && this.is_subcategory(categ_id, category_ids);
+ return (
+ category_ids.includes(Number(subcategory_id)) ||
+ check(this.get_category_parent_id(subcategory_id))
+ );
}
/* paid orders */
diff --git a/addons/point_of_sale/static/src/app/store/models.js b/addons/point_of_sale/static/src/app/store/models.js
index 74fe8466963..87d0f9355fc 100644
--- a/addons/point_of_sale/static/src/app/store/models.js
+++ b/addons/point_of_sale/static/src/app/store/models.js
@@ -1638,8 +1638,10 @@ export class Order extends PosModel {
const note = orderline.getNote();
const productKey = `${product.id} - ${orderline.get_full_product_name()} - ${note}`;
const lineKey = `${orderline.uuid} - ${note}`;
-
- if (prepaCategoryIds.has(product.pos_categ_id[0]) || prepaCategoryIds.size === 0) {
+ if (
+ prepaCategoryIds.size === 0 ||
+ this.pos.db.any_of_is_subcategory(product.pos_categ_ids, [...prepaCategoryIds])
+ ) {
const quantity = orderline.get_quantity();
const quantityDiff = oldChanges[lineKey]
? quantity - oldChanges[lineKey].quantity
@@ -1664,7 +1666,6 @@ export class Order extends PosModel {
orderline.setHasChange(false);
}
}
-
// Checks whether an orderline has been deleted from the order since it
// was last sent to the preparation tools. If so we add this to the changes.
for (const [lineKey, lineResume] of Object.entries(this.lastOrderPrepaChange)) {
@@ -2266,26 +2267,6 @@ export class Order extends PosModel {
return fulldetails;
}
- // Returns a total only for the orderlines with products belonging to the category
- get_total_for_category_with_tax(categ_id) {
- var total = 0;
- var self = this;
-
- if (categ_id instanceof Array) {
- for (var i = 0; i < categ_id.length; i++) {
- total += this.get_total_for_category_with_tax(categ_id[i]);
- }
- return total;
- }
-
- this.orderlines.forEach(function (line) {
- if (self.pos.db.category_contains(categ_id, line.product.id)) {
- total += line.get_price_with_tax();
- }
- });
-
- return total;
- }
get_total_for_taxes(tax_id) {
var total = 0;
diff --git a/addons/point_of_sale/tests/test_frontend.py b/addons/point_of_sale/tests/test_frontend.py
index 690c82523e1..db73f69061c 100644
--- a/addons/point_of_sale/tests/test_frontend.py
+++ b/addons/point_of_sale/tests/test_frontend.py
@@ -106,7 +106,7 @@ class TestPointOfSaleHttpCommon(AccountTestInvoicingHttpCommon):
'taxes_id': False,
'weight': 0.01,
'to_weight': True,
- 'pos_categ_id': pos_categ_misc.id,
+ 'pos_categ_ids': [(4, pos_categ_misc.id)],
})
cls.wall_shelf = env['product.product'].create({
'name': 'Wall Shelf Unit',
@@ -137,14 +137,14 @@ class TestPointOfSaleHttpCommon(AccountTestInvoicingHttpCommon):
'available_in_pos': True,
'list_price': 1.98,
'taxes_id': False,
- 'pos_categ_id': pos_categ_desks.id,
+ 'pos_categ_ids': [(4, pos_categ_desks.id)],
})
cls.letter_tray = env['product.product'].create({
'name': 'Letter Tray',
'available_in_pos': True,
'list_price': 4.80,
'taxes_id': False,
- 'pos_categ_id': pos_categ_chairs.id,
+ 'pos_categ_ids': [(4, pos_categ_chairs.id)],
})
cls.desk_organizer = env['product.product'].create({
'name': 'Desk Organizer',
diff --git a/addons/point_of_sale/views/product_view.xml b/addons/point_of_sale/views/product_view.xml
index 49785ca1872..66003b96b5a 100644
--- a/addons/point_of_sale/views/product_view.xml
+++ b/addons/point_of_sale/views/product_view.xml
@@ -9,7 +9,7 @@
-
+
@@ -68,7 +68,7 @@
-
+
@@ -122,7 +122,7 @@
-
+
@@ -133,7 +133,7 @@
-
+
diff --git a/addons/pos_restaurant/data/pos_restaurant_demo.xml b/addons/pos_restaurant/data/pos_restaurant_demo.xml
index e25964359eb..cbd25c20908 100644
--- a/addons/pos_restaurant/data/pos_restaurant_demo.xml
+++ b/addons/pos_restaurant/data/pos_restaurant_demo.xml
@@ -1,4 +1,3 @@
-
@@ -22,7 +21,7 @@
7.0
Margherita
0.01
-
+
@@ -31,7 +30,7 @@
7.0
Funghi
0.01
-
+
@@ -40,7 +39,7 @@
7.0
Vegetarian
0.01
-
+
@@ -49,7 +48,7 @@
4.5
Pasta Bolognese
0.01
-
+
@@ -58,7 +57,7 @@
5.5
Pasta 4 formaggi
0.01
-
+
@@ -67,7 +66,7 @@
7.5
Bacon Burger
0.01
-
+
@@ -76,7 +75,7 @@
7.0
Cheese Burger
0.01
-
+
@@ -85,7 +84,7 @@
3.0
Chicken Curry Sandwich
0.01
-
+
@@ -94,7 +93,7 @@
3.0
Spicy Tuna Sandwich
0.01
-
+
@@ -103,7 +102,7 @@
3.9
Mozzarella Sandwich
0.01
-
+
@@ -112,7 +111,7 @@
3.4
Club Sandwich
0.01
-
+
@@ -121,7 +120,7 @@
12.0
Lunch Maki 18pc
0.01
-
+
@@ -130,7 +129,7 @@
13.80
Lunch Salmon 20pc
0.01
-
+
@@ -139,7 +138,7 @@
14.0
Lunch Temaki mix 3pc
0.01
-
+
@@ -148,7 +147,7 @@
9.25
Salmon and Avocado
0.01
-
+
@@ -159,7 +158,7 @@
2.20
Coca-Cola
0.01
-
+
@@ -169,7 +168,7 @@
2.20
Water
0.01
-
+
@@ -179,7 +178,7 @@
2.20
Minute Maid
0.01
-
+
@@ -504,5 +503,5 @@
315
-
-
+
+
\ No newline at end of file
diff --git a/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ActionpadWidget.js b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ActionpadWidget.js
index e2ffb3ba8b3..ad21ae7d190 100644
--- a/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ActionpadWidget.js
+++ b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ActionpadWidget.js
@@ -8,9 +8,7 @@ import { nbsp } from "@web/core/utils/strings";
patch(ActionpadWidget.prototype, "point_of_sale.ActionpadWidget", {
get swapButton() {
- return (
- this.props.actionType === "payment" && this.pos.config.module_pos_restaurant
- );
+ return this.props.actionType === "payment" && this.pos.config.module_pos_restaurant;
},
get currentOrder() {
return this.pos.get_order();
@@ -57,8 +55,8 @@ patch(ActionpadWidget.prototype, "point_of_sale.ActionpadWidget", {
const orderChange = this.currentOrder.getOrderChanges().orderlines;
for (const idx in orderChange) {
const orderline = orderChange[idx];
- const category = this.pos.db.get_product_by_id(orderline.product_id)
- .pos_categ_id[1];
+ const categoryId = this.pos.db.get_product_by_id(orderline.product_id).pos_categ_ids[0];
+ const category = this.pos.db.category_by_id[categoryId].name;
const numProd = orderline.quantity;
categories[category] = categories[category] ? categories[category] + numProd : numProd;
}
diff --git a/addons/pos_restaurant/tests/test_frontend.py b/addons/pos_restaurant/tests/test_frontend.py
index 8b963d8355b..426805aec8c 100644
--- a/addons/pos_restaurant/tests/test_frontend.py
+++ b/addons/pos_restaurant/tests/test_frontend.py
@@ -39,13 +39,6 @@ class TestFrontend(odoo.tests.HttpCase):
'journal_id': second_cash_journal.id,
})
- printer = self.env['pos.printer'].create({
- 'name': 'Preparation Printer',
- 'epson_printer_ip': '127.0.0.1',
- 'printer_type': 'epson_epos',
- 'product_categories_ids': [drinks_category.id]
- })
-
pos_config = self.env['pos.config'].create({
'name': 'Bar',
'module_pos_restaurant': True,
@@ -144,7 +137,7 @@ class TestFrontend(odoo.tests.HttpCase):
'list_price': 2.20,
'name': 'Coca-Cola',
'weight': 0.01,
- 'pos_categ_id': drinks_category.id,
+ 'pos_categ_ids': [(4, drinks_category.id)],
'categ_id': self.env.ref('point_of_sale.product_category_pos').id,
'taxes_id': [(6, 0, [])],
})
@@ -154,7 +147,7 @@ class TestFrontend(odoo.tests.HttpCase):
'list_price': 2.20,
'name': 'Water',
'weight': 0.01,
- 'pos_categ_id': drinks_category.id,
+ 'pos_categ_ids': [(4, drinks_category.id)],
'categ_id': self.env.ref('point_of_sale.product_category_pos').id,
'taxes_id': [(6, 0, [])],
})
@@ -164,7 +157,7 @@ class TestFrontend(odoo.tests.HttpCase):
'list_price': 2.20,
'name': 'Minute Maid',
'weight': 0.01,
- 'pos_categ_id': drinks_category.id,
+ 'pos_categ_ids': [(4, drinks_category.id)],
'categ_id': self.env.ref('point_of_sale.product_category_pos').id,
'taxes_id': [(6, 0, [])],
})
diff --git a/addons/pos_self_order/models/pos_config.py b/addons/pos_self_order/models/pos_config.py
index 12ab9faca1a..a25ae4db605 100644
--- a/addons/pos_self_order/models/pos_config.py
+++ b/addons/pos_self_order/models/pos_config.py
@@ -153,11 +153,10 @@ class PosConfig(models.Model):
*(
self.limit_categories
and self.iface_available_categ_ids
- and [("pos_categ_id", "in", self.iface_available_categ_ids.ids)]
+ and [("pos_categ_ids", "in", self.iface_available_categ_ids.ids)]
or []
),
],
- order="pos_categ_id.sequence asc nulls last",
)
)
diff --git a/addons/pos_self_order/models/product_product.py b/addons/pos_self_order/models/product_product.py
index 781374cca29..4aa933531ee 100644
--- a/addons/pos_self_order/models/product_product.py
+++ b/addons/pos_self_order/models/product_product.py
@@ -101,7 +101,7 @@ class ProductProduct(models.Model):
"name": product._get_name(),
"product_id": product.id,
"description_sale": product.description_sale,
- "tag": product.pos_categ_id.name if product.pos_categ_id else "Other",
+ "tags": product.pos_categ_ids.mapped("name") or ["Other"],
"is_pos_groupable": product.uom_id.is_pos_groupable,
}
for product in self
diff --git a/addons/pos_self_order/static/src/models/product.js b/addons/pos_self_order/static/src/models/product.js
index 98e9e0f990f..0c68bc76404 100644
--- a/addons/pos_self_order/static/src/models/product.js
+++ b/addons/pos_self_order/static/src/models/product.js
@@ -10,7 +10,7 @@ export class Product extends Reactive {
name,
product_id,
description_sale,
- tag,
+ tags,
is_pos_groupable,
},
selfOrder
@@ -27,7 +27,7 @@ export class Product extends Reactive {
this.attributes = product.attributes;
this.name = product.name;
this.description_sale = product.description_sale;
- this.tag = product.tag;
+ this.tags = product.tags;
this.is_pos_groupable = product.is_pos_groupable;
// data
diff --git a/addons/pos_self_order/static/src/self_order_service.js b/addons/pos_self_order/static/src/self_order_service.js
index 5d05485592e..53872644332 100644
--- a/addons/pos_self_order/static/src/self_order_service.js
+++ b/addons/pos_self_order/static/src/self_order_service.js
@@ -50,7 +50,7 @@ export class SelfOrder {
this.products = this.products.map((p) => {
const product = new Product(p, this);
- this.tagList.add(product.tag);
+ this.tagList.add(...product.tags);
this.productByIds[product.id] = product;
return product;
@@ -67,7 +67,13 @@ export class SelfOrder {
);
}
- this.productsGroupedByTag = groupBy(this.products, "tag");
+ this.productsGroupedByTag = this._getProductsGroupedByTag(this.products);
+ }
+ _getProductsGroupedByTag(products) {
+ const productsDuplicatedSuchThatEachHasOneTag = products.flatMap((product) =>
+ product.tags.map((tag) => ({ ...product, tag }))
+ );
+ return groupBy(productsDuplicatedSuchThatEachHasOneTag, "tag");
}
saveOrderToLocalStorage(orders) {