From 42087c6fa1851174b045aefaf679114b314efcf9 Mon Sep 17 00:00:00 2001 From: vlst Date: Tue, 30 May 2023 13:47:34 +0000 Subject: [PATCH] [IMP] point_of_sale: Allow products to be part of multiple pos categories Currently, a product can only be part of one `pos_category`. This PR changes the relationship between the `product` and the `pos_category` from a `One2many` to a `Many2many`. closes odoo/odoo#122907 Task: 3138825 Related: odoo/enterprise#41791 Related: odoo/upgrade#4739 Signed-off-by: Joseph Caburnay (jcb) --- .../point_of_sale/data/point_of_sale_demo.xml | 128 +++++++++--------- .../data/point_of_sale_onboarding.xml | 34 ++--- addons/point_of_sale/models/pos_category.py | 18 +-- addons/point_of_sale/models/pos_config.py | 5 +- addons/point_of_sale/models/pos_session.py | 40 +----- addons/point_of_sale/models/product.py | 2 +- .../models/report_sale_details.py | 20 +-- .../point_of_sale/report/pos_order_report.py | 6 +- .../point_of_sale/static/src/app/store/db.js | 114 +++++++++------- .../static/src/app/store/models.js | 27 +--- addons/point_of_sale/tests/test_frontend.py | 6 +- addons/point_of_sale/views/product_view.xml | 8 +- .../data/pos_restaurant_demo.xml | 41 +++--- .../Screens/ProductScreen/ActionpadWidget.js | 8 +- addons/pos_restaurant/tests/test_frontend.py | 13 +- addons/pos_self_order/models/pos_config.py | 3 +- .../pos_self_order/models/product_product.py | 2 +- .../static/src/models/product.js | 4 +- .../static/src/self_order_service.js | 10 +- 19 files changed, 226 insertions(+), 263 deletions(-) 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) {