diff --git a/addons/point_of_sale/models/pos_combo.py b/addons/point_of_sale/models/pos_combo.py index 116ec5b66e8..936ab90832e 100644 --- a/addons/point_of_sale/models/pos_combo.py +++ b/addons/point_of_sale/models/pos_combo.py @@ -23,6 +23,11 @@ class PosCombo(models.Model): combo_line_ids = fields.One2many("pos.combo.line", "combo_id", string="Products in Combo", copy=True) num_of_products = fields.Integer("No of Products", compute="_compute_num_of_products") sequence = fields.Integer(copy=False) + base_price = fields.Float( + compute="_compute_base_price", + string="Product Price", + help="The value from which pro-rating of the component price is based. This is to ensure that whatever product the user chooses for a component, it will always be they same price." + ) @api.depends("combo_line_ids") def _compute_num_of_products(self): @@ -44,3 +49,9 @@ class PosCombo(models.Model): def _check_combo_line_ids_is_not_null(self): if any(not rec.combo_line_ids for rec in self): raise ValidationError(_("Please add products in combo.")) + + @api.depends("combo_line_ids") + def _compute_base_price(self): + for rec in self: + # Use the lowest price of the combo lines as the base price + rec.base_price = min(rec.combo_line_ids.mapped("product_id.lst_price")) diff --git a/addons/point_of_sale/models/pos_session.py b/addons/point_of_sale/models/pos_session.py index 596d8fd599a..b17e6ebccdd 100644 --- a/addons/point_of_sale/models/pos_session.py +++ b/addons/point_of_sale/models/pos_session.py @@ -1990,7 +1990,7 @@ class PosSession(models.Model): def _loader_params_pos_combo(self): products = self._context.get('loaded_data')['product.product'] combo_ids = set().union(*[product.get('combo_ids') for product in products]) - return {'search_params': {'fields': ['id', 'name', 'combo_line_ids']}, 'ids': combo_ids} + return {'search_params': {'fields': ['id', 'name', 'combo_line_ids', 'base_price']}, 'ids': combo_ids} def _get_pos_ui_pos_combo(self, params): return self.env['pos.combo'].browse(params['ids']).read(**params['search_params']) @@ -1998,7 +1998,7 @@ class PosSession(models.Model): def _loader_params_pos_combo_line(self): combo_ids = self._context.get('loaded_data')['pos.combo'] combo_line_ids = set().union(*[combo.get('combo_line_ids') for combo in combo_ids]) - return {'search_params': {'fields': ['id', 'product_id', 'combo_price']}, 'ids': combo_line_ids} + return {'search_params': {'fields': ['id', 'product_id', 'combo_price', 'combo_id']}, 'ids': combo_line_ids} def _get_pos_ui_pos_combo_line(self, params): return self.env['pos.combo.line'].browse(params['ids']).read(**params['search_params']) 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 b58004dd6cd..ca08c4a561c 100644 --- a/addons/point_of_sale/static/src/app/store/models.js +++ b/addons/point_of_sale/static/src/app/store/models.js @@ -2138,21 +2138,19 @@ export class Order extends PosModel { } } async addComboLines(comboParent, options) { - const pricelist = this.pos.getDefaultPricelist(); const originalPrices = {}; - let [originalTotal, targetExtra] = [0, 0]; + let originalTotal = 0; for (const comboLine of options.comboLines) { const product = this.pos.db.product_by_id[comboLine.product_id[0]]; - const originalPrice = product.get_price(pricelist, 1, comboLine.combo_price); + const originalPrice = this.pos.db.combo_by_id[comboLine.combo_id[0]].base_price; originalTotal += product.get_display_price({ price: originalPrice }); - targetExtra += product.get_display_price({ price: comboLine.combo_price }); // Keep track of the original price of each product for the subsequent for loop. originalPrices[product.id] = originalPrice; } - const targetPrice = comboParent.product.lst_price + targetExtra; + const targetPrice = comboParent.product.lst_price; const childPriceFactor = targetPrice / originalTotal; for (const comboLine of options.comboLines) { const product = this.pos.db.product_by_id[comboLine.product_id[0]]; @@ -2162,6 +2160,29 @@ export class Order extends PosModel { comboParent, }); } + + // Adjust the price of the last combo line to make sure the total price of the combo + // is the same as the target price. + const childLines = this.get_orderlines().filter( + (l) => l.comboParent?.uuid === comboParent.uuid + ); + const totalComboPrice = childLines.reduce((acc, l) => acc + l.get_display_price(), 0); + const diff = targetPrice - totalComboPrice; + if (!this.env.utils.floatIsZero(diff)) { + const lastLine = childLines[childLines.length - 1]; + lastLine.set_unit_price(lastLine.get_unit_price() + diff); + } + + // Finally, take into account the combo price for each combo line. + for (const comboLine of options.comboLines) { + if (this.env.utils.floatIsZero(comboLine.combo_price)) { + continue; + } + const presentLine = childLines.find((l) => l.product.id === comboLine.product_id[0]); + if (presentLine) { + presentLine.set_unit_price(presentLine.get_unit_price() + comboLine.combo_price); + } + } } set_orderline_options(orderline, options) { if (options.comboLines?.length) { diff --git a/addons/point_of_sale/static/src/app/store/pos_store.js b/addons/point_of_sale/static/src/app/store/pos_store.js index 73e7909c669..81e69f8d990 100644 --- a/addons/point_of_sale/static/src/app/store/pos_store.js +++ b/addons/point_of_sale/static/src/app/store/pos_store.js @@ -1797,7 +1797,7 @@ export class PosStore extends Reactive { } // FIXME: POSREF, method exist only to be overrided async addProductFromUi(product, options) { - this.get_order().add_product(product, options); + return this.get_order().add_product(product, options); } async addProductToCurrentOrder(product, options = {}) { if (Number.isInteger(product)) { diff --git a/addons/point_of_sale/static/tests/tours/PosComboTour.js b/addons/point_of_sale/static/tests/tours/PosComboTour.js index 1fc028d7141..74bd7f8351b 100644 --- a/addons/point_of_sale/static/tests/tours/PosComboTour.js +++ b/addons/point_of_sale/static/tests/tours/PosComboTour.js @@ -43,21 +43,21 @@ registry.category("web_tour.tours").add("PosComboPriceTaxIncludedTour", { ...ProductScreen.check.selectedOrderlineHas( "Combo Product 3", "1.0", - "14.63", + "12.92", "Office Combo" ), ...ProductScreen.do.clickOrderline("Combo Product 5"), ...ProductScreen.check.selectedOrderlineHas( "Combo Product 5", "1.0", - "16.87", + "17.87", "Office Combo" ), ...ProductScreen.do.clickOrderline("Combo Product 8"), ...ProductScreen.check.selectedOrderlineHas( "Combo Product 8", "1.0", - "28.11", + "28.81", "Office Combo" ), @@ -74,7 +74,7 @@ registry.category("web_tour.tours").add("PosComboPriceTaxIncludedTour", { combo.select("Combo Product 5"), combo.select("Combo Product 8"), combo.confirm(), - ...ProductScreen.check.totalAmountIs("59.61"), + ...ProductScreen.check.totalAmountIs("59.60"), ...ProductScreen.do.clickPayButton(), ...PaymentScreen.do.clickPaymentMethod("Bank"), ...PaymentScreen.do.clickValidate(), @@ -88,7 +88,7 @@ registry.category("web_tour.tours").add("PosComboPriceTaxIncludedTour", { combo.select("Combo Product 6"), combo.confirm(), ...ProductScreen.check.totalAmountIs("50.00"), - ...ProductScreen.check.totalTaxIs("8.91"), + ...ProductScreen.check.totalTaxIs("8.92"), // the split screen is tested in `pos_restaurant` ], diff --git a/addons/point_of_sale/views/pos_combo_view.xml b/addons/point_of_sale/views/pos_combo_view.xml index efe96fa3f1e..4a1dd55e840 100644 --- a/addons/point_of_sale/views/pos_combo_view.xml +++ b/addons/point_of_sale/views/pos_combo_view.xml @@ -28,6 +28,7 @@ + diff --git a/addons/point_of_sale/views/product_view.xml b/addons/point_of_sale/views/product_view.xml index c8b903db57e..b2b492ca5d6 100644 --- a/addons/point_of_sale/views/product_view.xml +++ b/addons/point_of_sale/views/product_view.xml @@ -80,6 +80,7 @@ + diff --git a/addons/pos_restaurant/static/tests/tours/SplitBillScreen.tour.js b/addons/pos_restaurant/static/tests/tours/SplitBillScreen.tour.js index 55c8c807f85..d153f3b835d 100644 --- a/addons/pos_restaurant/static/tests/tours/SplitBillScreen.tour.js +++ b/addons/pos_restaurant/static/tests/tours/SplitBillScreen.tour.js @@ -177,7 +177,7 @@ registry.category("web_tour.tours").add("SplitBillScreenTour4PosCombo", { ...SplitBillScreen.check.orderlineHas("Combo Product 4", "1", "0"), ...SplitBillScreen.check.orderlineHas("Combo Product 7", "1", "0"), - ...SplitBillScreen.check.subtotalIs("52.14"), + ...SplitBillScreen.check.subtotalIs("52.13"), ...SplitBillScreen.do.clickPay(), ...PaymentScreen.do.clickPaymentMethod("Bank"), ...PaymentScreen.do.clickValidate(), @@ -191,21 +191,21 @@ registry.category("web_tour.tours").add("SplitBillScreenTour4PosCombo", { ...ProductScreen.check.selectedOrderlineHas( "Combo Product 2", "1.0", - "6.45", + "6.15", "Office Combo" ), ...ProductScreen.do.clickOrderline("Combo Product 4"), ...ProductScreen.check.selectedOrderlineHas( "Combo Product 4", "1.0", - "12.90", + "13.54", "Office Combo" ), ...ProductScreen.do.clickOrderline("Combo Product 7"), ...ProductScreen.check.selectedOrderlineHas( "Combo Product 7", "1.0", - "20.65", + "20.31", "Office Combo" ), ...ProductScreen.check.totalAmountIs("42.53"),