diff --git a/addons/point_of_sale/models/pos_session.py b/addons/point_of_sale/models/pos_session.py index e6e760cc440..0e28431a1a0 100644 --- a/addons/point_of_sale/models/pos_session.py +++ b/addons/point_of_sale/models/pos_session.py @@ -1638,7 +1638,10 @@ class PosSession(models.Model): for key, group in groupby(sorted(product_template_attribute_values, key=key1), key=key2): attribute_line_id, attribute = key values = [{**ptav.product_attribute_value_id.read(['name', 'is_custom', 'html_color', 'image'])[0], - 'price_extra': ptav.price_extra} for ptav in list(group)] + 'price_extra': ptav.price_extra, + # id of a value should be from the "product.template.attribute.value" record + 'id': ptav.id, + } for ptav in list(group)] res[attribute_line_id] = { 'id': attribute_line_id, 'name': attribute.name, diff --git a/addons/point_of_sale/static/src/app/store/combo_configurator_popup/combo_configurator_popup.js b/addons/point_of_sale/static/src/app/store/combo_configurator_popup/combo_configurator_popup.js index 47f6d9de5eb..88ae9ee194f 100644 --- a/addons/point_of_sale/static/src/app/store/combo_configurator_popup/combo_configurator_popup.js +++ b/addons/point_of_sale/static/src/app/store/combo_configurator_popup/combo_configurator_popup.js @@ -17,15 +17,6 @@ export class ComboConfiguratorPopup extends AbstractAwaitablePopup { }); } - getTotalPrice() { - const selectedComponents = this.getPayload(); - const extraPrice = selectedComponents.reduce((acc, x) => { - const product = this.pos.db.product_by_id[x.product_id[0]]; - return acc + product.get_display_price({ price: x.combo_price }); - }, 0); - return this.props.product.lst_price + extraPrice; - } - areAllCombosSelected() { return Object.values(this.state.combo).every((x) => Boolean(x)); } diff --git a/addons/point_of_sale/static/src/app/store/combo_configurator_popup/combo_configurator_popup.xml b/addons/point_of_sale/static/src/app/store/combo_configurator_popup/combo_configurator_popup.xml index 14e949998f9..ecf2bd62f5e 100644 --- a/addons/point_of_sale/static/src/app/store/combo_configurator_popup/combo_configurator_popup.xml +++ b/addons/point_of_sale/static/src/app/store/combo_configurator_popup/combo_configurator_popup.xml @@ -34,16 +34,20 @@ 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 2a87673ee7f..818bb73170a 100644 --- a/addons/point_of_sale/static/src/app/store/models.js +++ b/addons/point_of_sale/static/src/app/store/models.js @@ -2027,49 +2027,44 @@ export class Order extends PosModel { } } async addComboLines(comboParent, options) { - const originalPrices = {}; - - let originalTotal = 0; - for (const comboLine of options.comboLines) { - const product = this.pos.db.product_by_id[comboLine.product_id[0]]; + const parentLstPrice = comboParent.product.lst_price; + const originalTotal = options.comboLines.reduce((acc, comboLine) => { const originalPrice = this.pos.db.combo_by_id[comboLine.combo_id[0]].base_price; - originalTotal += product.get_display_price({ price: originalPrice }); + return acc + originalPrice; + }, 0); - // Keep track of the original price of each product for the subsequent for loop. - originalPrices[product.id] = originalPrice; - } + let remainingTotal = parentLstPrice; - const targetPrice = comboParent.product.lst_price; - const childPriceFactor = targetPrice / originalTotal; - for (const comboLine of options.comboLines) { + for (let i = 0; i < options.comboLines.length; i++) { + const comboLine = options.comboLines[i]; const product = this.pos.db.product_by_id[comboLine.product_id[0]]; - const childUnitPrice = originalPrices[product.id] * childPriceFactor; + const combo = this.pos.db.combo_by_id[comboLine.combo_id[0]]; + let priceUnit = round_di( + (combo.base_price * parentLstPrice) / originalTotal, + this.pos.dp["Product Price"] + ); + remainingTotal -= priceUnit; + if (i == options.comboLines.length - 1) { + priceUnit += remainingTotal; + } await this.pos.addProductToCurrentOrder(product, { - price: childUnitPrice, + price: priceUnit, 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. + // Take into account the extra prices. 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); + const attributesPriceExtra = (presentLine.attribute_value_ids ?? []) + .map((id) => this.pos.db.attribute_value_by_id[id]?.price_extra || 0) + .reduce((acc, price) => acc + price, 0); + const totalPriceExtra = attributesPriceExtra + comboLine.combo_price; + presentLine.set_unit_price(presentLine.get_unit_price() + totalPriceExtra); } } } diff --git a/addons/point_of_sale/static/tests/tours/PosComboTour.js b/addons/point_of_sale/static/tests/tours/PosComboTour.js index 5004ccbd14b..3b1e7c309d3 100644 --- a/addons/point_of_sale/static/tests/tours/PosComboTour.js +++ b/addons/point_of_sale/static/tests/tours/PosComboTour.js @@ -34,19 +34,14 @@ registry.category("web_tour.tours").add("PosComboPriceTaxIncludedTour", { combo.select("Combo Product 8"), combo.isSelected("Combo Product 8"), combo.isNotSelected("Combo Product 7"), - { - content: "check that the total amount is properly displayed", - trigger: `.combo-configurator-popup .total-amount:contains("59.60")`, - isCheck: true, - }, combo.confirm(), ...ProductScreen.selectedOrderlineHas("Office Combo"), ...ProductScreen.clickOrderline("Combo Product 3"), - ...ProductScreen.selectedOrderlineHas("Combo Product 3", "1.0", "12.92"), + ...ProductScreen.selectedOrderlineHas("Combo Product 3", "1.0", "13.43"), ...ProductScreen.clickOrderline("Combo Product 5"), - ...ProductScreen.selectedOrderlineHas("Combo Product 5", "1.0", "17.87"), + ...ProductScreen.selectedOrderlineHas("Combo Product 5", "1.0", "18.67"), ...ProductScreen.clickOrderline("Combo Product 8"), - ...ProductScreen.selectedOrderlineHas("Combo Product 8", "1.0", "28.81"), + ...ProductScreen.selectedOrderlineHas("Combo Product 8", "1.0", "30.00"), // check that you cannot change the quantity of a combo product ...ProductScreen.pressNumpad("2"), @@ -61,7 +56,7 @@ registry.category("web_tour.tours").add("PosComboPriceTaxIncludedTour", { combo.select("Combo Product 5"), combo.select("Combo Product 8"), combo.confirm(), - ...ProductScreen.totalAmountIs("59.60"), + ...ProductScreen.totalAmountIs("62.10"), ...ProductScreen.clickPayButton(), ...PaymentScreen.clickPaymentMethod("Bank"), ...PaymentScreen.clickValidate(), @@ -74,8 +69,8 @@ registry.category("web_tour.tours").add("PosComboPriceTaxIncludedTour", { combo.select("Combo Product 4"), combo.select("Combo Product 6"), combo.confirm(), - ...ProductScreen.totalAmountIs("50.00"), - ...inLeftSide(Order.hasTax("8.92")), + ...ProductScreen.totalAmountIs("59.17"), + ...inLeftSide(Order.hasTax("10.56")), // the split screen is tested in `pos_restaurant` ], }); diff --git a/addons/pos_restaurant/static/tests/tours/SplitBillScreen.tour.js b/addons/pos_restaurant/static/tests/tours/SplitBillScreen.tour.js index 9b80d95fcd1..5822d93b436 100644 --- a/addons/pos_restaurant/static/tests/tours/SplitBillScreen.tour.js +++ b/addons/pos_restaurant/static/tests/tours/SplitBillScreen.tour.js @@ -170,7 +170,7 @@ registry.category("web_tour.tours").add("SplitBillScreenTour4PosCombo", { ...SplitBillScreen.orderlineHas("Combo Product 4", "1", "0"), ...SplitBillScreen.orderlineHas("Combo Product 7", "1", "0"), - ...SplitBillScreen.subtotalIs("52.13"), + ...SplitBillScreen.subtotalIs("54.13"), ...SplitBillScreen.clickPay(), ...PaymentScreen.clickPaymentMethod("Bank"), ...PaymentScreen.clickValidate(), @@ -181,11 +181,11 @@ registry.category("web_tour.tours").add("SplitBillScreenTour4PosCombo", { ...ProductScreen.selectedOrderlineHas("Minute Maid", "1.0"), ...ProductScreen.clickOrderline("Office Combo"), ...ProductScreen.clickOrderline("Combo Product 2"), - ...ProductScreen.selectedOrderlineHas("Combo Product 2", "1.0", "6.15", "Office Combo"), + ...ProductScreen.selectedOrderlineHas("Combo Product 2", "1.0", "6.67", "Office Combo"), ...ProductScreen.clickOrderline("Combo Product 4"), - ...ProductScreen.selectedOrderlineHas("Combo Product 4", "1.0", "13.54", "Office Combo"), + ...ProductScreen.selectedOrderlineHas("Combo Product 4", "1.0", "14.66", "Office Combo"), ...ProductScreen.clickOrderline("Combo Product 7"), - ...ProductScreen.selectedOrderlineHas("Combo Product 7", "1.0", "20.31", "Office Combo"), - ...ProductScreen.totalAmountIs("42.53"), + ...ProductScreen.selectedOrderlineHas("Combo Product 7", "1.0", "22.00", "Office Combo"), + ...ProductScreen.totalAmountIs("45.86"), ], }); diff --git a/addons/pos_self_order/controllers/orders.py b/addons/pos_self_order/controllers/orders.py index 0e0eb1ab321..36f05b868ba 100644 --- a/addons/pos_self_order/controllers/orders.py +++ b/addons/pos_self_order/controllers/orders.py @@ -4,6 +4,7 @@ import uuid from datetime import timedelta from odoo import http, fields from odoo.http import request +from odoo.tools import float_round from werkzeug.exceptions import NotFound, BadRequest, Unauthorized class PosSelfOrderController(http.Controller): @@ -202,57 +203,57 @@ class PosSelfOrderController(http.Controller): appended_uuid = [] newLines = [] pricelist = pos_config.pricelist_id - if take_away and pos_config.self_ordering_mode == 'kiosk': - config_fiscal_pos = pos_config.self_ordering_alternative_fp_id - else: - config_fiscal_pos = pos_config.default_fiscal_position_id + sale_price_digits = pos_config.env['decimal.precision'].precision_get('Product Price') + + combo_line_ids = [line['combo_line_id'] for line in lines if line.get('combo_line_id')] + combo_lines = pos_config.env['pos.combo.line'].search([('id', 'in', combo_line_ids)]) + attribute_value_ids = sum([line.get('attribute_value_ids', []) for line in lines], []) + fetched_attributes = pos_config.env['product.template.attribute.value'].search([('id', 'in', attribute_value_ids)]) + + fiscal_pos = pos_config.default_fiscal_position_id + + if take_away and pos_config.self_ordering_alternative_fp_id: + fiscal_pos = pos_config.self_ordering_alternative_fp_id for line in lines: if line.get('uuid') in appended_uuid or not line.get('product_id'): continue + line_qty = line.get('qty') product = pos_config.env['product.product'].browse(int(line.get('product_id'))) - # todo take into account the price extra - price_unit = pricelist._get_product_price(product, quantity=line.get('qty')) if pricelist else product.lst_price - selected_account_tax = config_fiscal_pos.map_tax(product.taxes_id) if config_fiscal_pos else product.taxes_id + lst_price = pricelist._get_product_price(product, quantity=line_qty) if pricelist else product.lst_price - # parent_product_taxe_ids = None children = [l for l in lines if l.get('combo_parent_uuid') == line.get('uuid')] + pos_combo_lines = combo_lines.browse([child.get('combo_line_id') for child in children]) + if len(children) > 0: - total_price = 0 - unit_price_by_id = {} - for child in children: - product = pos_config.env['product.product'].browse(int(child.get('product_id'))) - child_selected_account_tax = config_fiscal_pos.map_tax(product.taxes_id) if config_fiscal_pos else product.taxes_id - tax_results = child_selected_account_tax.compute_all( - pricelist._get_product_price(product, quantity=line.get('qty')) if pricelist else product.lst_price, - pos_config.currency_id, - child.get('qty'), - product, - ) - unit_price_by_id[child['uuid']] = pricelist._get_product_price(product, quantity=line.get('qty')) if pricelist else product.lst_price - total_price += tax_results.get('total_included') if pos_config.iface_tax_included == 'total' else tax_results.get('total_excluded') - ratio = (price_unit * line.get('qty') / total_price) - for child in children: - child_line_combo = pos_config.env['pos.combo'].browse(int(child.get('combo_id'))) - child_line_combo_line = child_line_combo.combo_line_ids.filtered(lambda l: l.product_id.id == child.get('product_id'))[0] - child_product = pos_config.env["product.product"].browse(int(child.get('product_id'))) - child_price_unit = ratio * unit_price_by_id[child['uuid']] + child_line_combo_line.combo_price - child_selected_account_tax = config_fiscal_pos.map_tax(child_product.taxes_id) if config_fiscal_pos else child_product.taxes_id - child_tax_results = child_selected_account_tax.compute_all( - child_price_unit, - pos_config.currency_id, - child.get('qty'), - child_product, - ) + original_total = sum(pos_combo_lines.mapped("combo_id.base_price")) + remaining_total = lst_price + factor = lst_price / original_total + + for i, child in enumerate(children): + child_product = pos_config.env['product.product'].browse(int(child.get('product_id'))) + pos_combo_line = pos_combo_lines.browse(child.get('combo_line_id')) + price_unit = float_round(pos_combo_line.combo_id.base_price * factor, precision_digits=sale_price_digits) + remaining_total -= price_unit + if i == len(children) - 1: + price_unit += remaining_total + + selected_attributes = fetched_attributes.browse(child.get('attribute_value_ids', [])) + price_unit += pos_combo_line.combo_price + sum([attr.price_extra for attr in selected_attributes]) + + price_unit_fp = child_product._get_price_unit_after_fp(price_unit, pos_config.currency_id, fiscal_pos) + taxes = fiscal_pos.map_tax(child_product.taxes_id) if fiscal_pos else child_product.taxes_id + pdetails = taxes.compute_all(price_unit_fp, pos_config.currency_id, line_qty, child_product) + newLines.append({ - 'price_unit': child_price_unit, - 'price_subtotal': child_tax_results.get('total_excluded'), - 'price_subtotal_incl': child_tax_results.get('total_included'), + 'price_unit': price_unit_fp, + 'price_subtotal': pdetails.get('total_excluded'), + 'price_subtotal_incl': pdetails.get('total_included'), 'custom_attribute_value_ids': [[0, 0, cAttr] for cAttr in child.get('custom_attribute_value_ids')] if child.get('custom_attribute_value_ids') else [], 'id': child.get('id'), 'order_id': pos_order_id, - 'tax_ids': product.taxes_id, + 'tax_ids': child_product.taxes_id, 'uuid': child.get('uuid'), 'product_id': child.get('product_id'), 'qty': child.get('qty'), @@ -263,25 +264,23 @@ class PosSelfOrderController(http.Controller): 'combo_id': child.get('combo_id'), }) appended_uuid.append(child.get('uuid')) - price_unit = 0 - tax_results = selected_account_tax.compute_all( - price_unit, - pos_config.currency_id, - line.get('qty'), - product, - ) + lst_price = 0 + + price_unit_fp = product._get_price_unit_after_fp(lst_price, pos_config.currency_id, fiscal_pos) + taxes_after_fp = fiscal_pos.map_tax(product.taxes_id) if fiscal_pos else product.taxes_id + pdetails = taxes_after_fp.compute_all(price_unit_fp, pos_config.currency_id, line_qty, product) newLines.append({ - 'price_unit': price_unit, - 'price_subtotal': tax_results.get('total_excluded'), - 'price_subtotal_incl': tax_results.get('total_included'), + 'price_unit': price_unit_fp, + 'price_subtotal': pdetails.get('total_excluded'), + 'price_subtotal_incl': pdetails.get('total_included'), 'id': line.get('id'), 'order_id': pos_order_id, 'tax_ids': product.taxes_id, 'uuid': line.get('uuid'), 'product_id': line.get('product_id'), - 'qty': line.get('qty'), + 'qty': line_qty, 'customer_note': line.get('customer_note'), 'attribute_value_ids': line.get('attribute_value_ids') or [], 'custom_attribute_value_ids': [[0, 0, cAttr] for cAttr in line.get('custom_attribute_value_ids')] if line.get('custom_attribute_value_ids') else [], diff --git a/addons/pos_self_order/models/pos_config.py b/addons/pos_self_order/models/pos_config.py index 206c9d3d831..fca890f5ed9 100644 --- a/addons/pos_self_order/models/pos_config.py +++ b/addons/pos_self_order/models/pos_config.py @@ -348,6 +348,7 @@ class PosConfig(models.Model): "custom_links": self._get_self_order_custom_links(), "currency_id": self.currency_id.id, "pos_payment_methods": payment_methods if self.self_ordering_mode == "kiosk" else [], + "currency_decimals": self.currency_id.decimal_places, "pos_category": self._get_available_categories().read(["name", "sequence", "has_image"]), "products": self._get_available_products()._get_self_order_data(self), "combos": self._get_combos_data(), diff --git a/addons/pos_self_order/static/src/app/components/attribute_selection/attribute_selection.js b/addons/pos_self_order/static/src/app/components/attribute_selection/attribute_selection.js index 41871d96c9f..dd562a4b466 100644 --- a/addons/pos_self_order/static/src/app/components/attribute_selection/attribute_selection.js +++ b/addons/pos_self_order/static/src/app/components/attribute_selection/attribute_selection.js @@ -4,6 +4,7 @@ import { Component, useState } from "@odoo/owl"; import { ProductCustomAttribute } from "@point_of_sale/app/store/models/product_custom_attribute"; import { useSelfOrder } from "@pos_self_order/app/self_order_service"; import { attributeFlatter, attributeFormatter } from "@pos_self_order/app/utils"; +import { floatIsZero } from "@web/core/utils/numbers"; export class AttributeSelection extends Component { static template = "pos_self_order.AttributeSelection"; @@ -90,4 +91,22 @@ export class AttributeSelection extends Component { ? this.selectedValues[attribute.id][value.id] : parseInt(this.selectedValues[attribute.id]) === value.id; } + + _getPriceExtra(value) { + const isTakeAway = this.selfOrder.take_away; + const priceExtra = isTakeAway + ? value.price_extra.display_price_default + : value.price_extra.display_price_alternative; + return priceExtra; + } + + shouldShowPriceExtra(value) { + const priceExtra = this._getPriceExtra(value); + return !floatIsZero(priceExtra, this.selfOrder.config.currency_decimals); + } + + getfPriceExtra(value) { + const priceExtra = this._getPriceExtra(value); + return this.selfOrder.formatMonetary(priceExtra); + } } diff --git a/addons/pos_self_order/static/src/app/components/attribute_selection/attribute_selection.xml b/addons/pos_self_order/static/src/app/components/attribute_selection/attribute_selection.xml index 5b2eb352110..b88903f6db4 100644 --- a/addons/pos_self_order/static/src/app/components/attribute_selection/attribute_selection.xml +++ b/addons/pos_self_order/static/src/app/components/attribute_selection/attribute_selection.xml @@ -14,8 +14,8 @@ d-flex align-items-center justify-content-center h-100 rounded border ratio ratio-16x9">
- - + + + +
diff --git a/addons/pos_self_order/static/src/app/components/combo_selection/combo_selection.js b/addons/pos_self_order/static/src/app/components/combo_selection/combo_selection.js index 35a06ac7b37..42c2d73e517 100644 --- a/addons/pos_self_order/static/src/app/components/combo_selection/combo_selection.js +++ b/addons/pos_self_order/static/src/app/components/combo_selection/combo_selection.js @@ -14,6 +14,11 @@ export class ComboSelection extends Component { } productClicked(lineId) { + // Keep track of the current combo line id. + // It servers as additional info for each line so that when calculating prices, + // no need to look for the specific combo line the product belongs to. + this.env.currentComboLineId.value = lineId; + const comboLine = this.props.combo.combo_line_ids.find((line) => line.id == lineId); const productSelected = this.selfOrder.productByIds[comboLine.product_id[0]]; if (!productSelected.self_order_available) { diff --git a/addons/pos_self_order/static/src/app/models/line.js b/addons/pos_self_order/static/src/app/models/line.js index be1f3d0dd74..80135d4d600 100644 --- a/addons/pos_self_order/static/src/app/models/line.js +++ b/addons/pos_self_order/static/src/app/models/line.js @@ -18,6 +18,7 @@ export class Line extends Reactive { custom_attribute_value_ids, combo_parent_uuid, combo_id, + combo_line_id, child_lines, }) { super(); @@ -39,6 +40,7 @@ export class Line extends Reactive { this.custom_attribute_value_ids = line.custom_attribute_value_ids || []; this.combo_parent_uuid = line.combo_parent_uuid || null; this.combo_id = line.combo_id || null; + this.combo_line_id = line.combo_line_id || null; this.child_lines = line.child_lines || []; this.initCustomAttribute(); diff --git a/addons/pos_self_order/static/src/app/pages/combo_page/combo_page.js b/addons/pos_self_order/static/src/app/pages/combo_page/combo_page.js index 98b74043b55..319a3b84e7c 100644 --- a/addons/pos_self_order/static/src/app/pages/combo_page/combo_page.js +++ b/addons/pos_self_order/static/src/app/pages/combo_page/combo_page.js @@ -17,7 +17,14 @@ export class ComboPage extends Component { this.selfOrder = useSelfOrder(); this.selfOrder.lastEditedProductId = this.props.product.id; this.router = useService("router"); - useSubEnv({ selectedValues: {}, customValues: {}, editable: this.editableProductLine }); + useSubEnv({ + selectedValues: {}, + customValues: {}, + editable: this.editableProductLine, + currentComboLineId: { + value: null, + }, + }); if (!this.props.product) { this.router.navigate("productList"); @@ -79,28 +86,21 @@ export class ComboPage extends Component { next() { const combo = this.currentCombo; const index = this.state.selectedCombos.findIndex((c) => c.id === combo.id); + const selectedCombo = { + id: combo.id, + name: combo.name, + combo_line_id: this.env.currentComboLineId.value, + product: { + id: this.state.selectedProduct.id, + name: this.state.selectedProduct.name, + variants: { ...this.env.selectedValues }, + customValues: { ...this.env.customValues }, + }, + }; if (index !== -1) { - this.state.selectedCombos[index] = { - id: combo.id, - name: combo.name, - product: { - id: this.state.selectedProduct.id, - name: this.state.selectedProduct.name, - variants: { ...this.env.selectedValues }, - customValues: { ...this.env.customValues }, - }, - }; + this.state.selectedCombos[index] = selectedCombo; } else { - this.state.selectedCombos.push({ - id: combo.id, - name: combo.name, - product: { - id: this.state.selectedProduct.id, - name: this.state.selectedProduct.name, - variants: { ...this.env.selectedValues }, - customValues: { ...this.env.customValues }, - }, - }); + this.state.selectedCombos.push(selectedCombo); } this.resetState(); if (this.state.editMode) { @@ -156,6 +156,7 @@ export class ComboPage extends Component { custom_attribute_value_ids: Object.values(combo.product.customValues), combo_parent_uuid: parent_line.uuid, combo_id: combo.id, + combo_line_id: combo.combo_line_id, }); child_line.full_product_name = constructFullProductName( child_line,