From 79f6e17a489e8de51576afbc1faa6dc6b01ce638 Mon Sep 17 00:00:00 2001 From: "Loukas Wets (lowe)" Date: Tue, 21 Nov 2023 14:48:32 +0100 Subject: [PATCH] [FIX] point_of_sale: application of pricelist on combo Steps to reproduce: - create an order with a combo product - change the customer to a customer that has a custom pricelist Bug: Each combo orderline will be recalculated independently Fix: Extract the combolines price calculation from addComboLines. Modify the setPricelist method to use the new price calculation method for the combolines. closes odoo/odoo#143168 Signed-off-by: Joseph Caburnay (jcb) --- .../combo_configurator_popup.js | 22 ++- .../combo_configurator_popup.scss | 2 +- .../combo_configurator_popup.xml | 3 +- .../static/src/app/store/models.js | 131 ++++++++++++------ 4 files changed, 115 insertions(+), 43 deletions(-) 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 88ae9ee194f..405e9ac0af3 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 @@ -14,6 +14,8 @@ export class ComboConfiguratorPopup extends AbstractAwaitablePopup { this.pos = usePos(); this.state = useState({ combo: Object.fromEntries(this.props.product.combo_ids.map((elem) => [elem, 0])), + // configuration: id of combo_line -> ProductConfiguratorPopup payload + configuration: {}, }); } @@ -37,6 +39,24 @@ export class ComboConfiguratorPopup extends AbstractAwaitablePopup { getPayload() { return Object.values(this.state.combo) .filter((x) => x) // we only keep the non-zero values - .map((x) => this.pos.db.combo_line_by_id[x]); + .map((x) => { + const combo_line = this.pos.db.combo_line_by_id[x]; + return { + ...combo_line, + configuration: this.state.configuration[combo_line.id], + }; + }); + } + + async onClickProduct({ product, combo_line }, ev) { + if (product.isConfigurable()) { + const { confirmed, payload } = await product.openConfigurator({ initQuantity: 1 }); + if (confirmed) { + this.state.configuration[combo_line.id] = payload; + } else { + // Do not select the product if configuration popup is cancelled. + this.state.combo[combo_line.id] = 0; + } + } } } diff --git a/addons/point_of_sale/static/src/app/store/combo_configurator_popup/combo_configurator_popup.scss b/addons/point_of_sale/static/src/app/store/combo_configurator_popup/combo_configurator_popup.scss index 788b1181d13..5a75eb00e42 100644 --- a/addons/point_of_sale/static/src/app/store/combo_configurator_popup/combo_configurator_popup.scss +++ b/addons/point_of_sale/static/src/app/store/combo_configurator_popup/combo_configurator_popup.scss @@ -4,6 +4,6 @@ appearance:none; } -.combo-configurator-popup input:checked + label { +.combo-configurator-popup input.selected + label { box-shadow: 0 0 0 ($border-width * 2) $primary; } 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 e1abe074b8b..0ee52e9eb6b 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 @@ -18,6 +18,7 @@ t-attf-id="combo-{{combo_id}}-combo_line-{{combo_line_id}}" t-attf-value="{{combo_line_id}}" t-model="state.combo[combo_id]" + t-att-class="{ 'selected': state.combo[combo_id] == combo_line_id }" /> 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 5028d81fc0c..3ec381655f6 100644 --- a/addons/point_of_sale/static/src/app/store/models.js +++ b/addons/point_of_sale/static/src/app/store/models.js @@ -143,6 +143,22 @@ export class Product extends PosModel { return formattedUnitPrice; } } + + async openConfigurator({ initQuantity = 1 }) { + const attributes = this.attribute_line_ids + .map((id) => this.pos.attributes_by_ptal_id[id]) + .filter((attr) => attr !== undefined); + return await this.env.services.popup.add(ProductConfiguratorPopup, { + product: this, + attributes: attributes, + quantity: initQuantity, + }); + } + + isConfigurable() { + return this.attribute_line_ids.some((id) => id in this.pos.attributes_by_ptal_id); + } + async getAddProductOptions(code) { let price_extra = 0.0; let draftPackLotLines, packLotLinesToEdit, attribute_value_ids; @@ -154,19 +170,8 @@ export class Product extends PosModel { quantity = this.pos.db.product_packaging_by_barcode[code.code].qty; } - if (this.attribute_line_ids.some((id) => id in this.pos.attributes_by_ptal_id)) { - const attributes = this.attribute_line_ids - .map((id) => this.pos.attributes_by_ptal_id[id]) - .filter((attr) => attr !== undefined); - const { confirmed, payload } = await this.env.services.popup.add( - ProductConfiguratorPopup, - { - product: this, - attributes: attributes, - quantity: quantity, - } - ); - + if (this.isConfigurable()) { + const { confirmed, payload } = await this.openConfigurator({ initQuantity: quantity }); if (confirmed) { attribute_value_ids = payload.attribute_value_ids; attribute_custom_values = payload.attribute_custom_values; @@ -179,7 +184,7 @@ export class Product extends PosModel { if (this.combo_ids.length) { const { confirmed, payload } = await this.env.services.popup.add( ComboConfiguratorPopup, - { product: this } + { product: this, keepBehind: true } ); if (!confirmed) { return; @@ -404,7 +409,7 @@ export class Orderline extends PosModel { this.product = this.pos.db.get_product_by_id(json.product_id); this.set_product_lot(this.product); this.price = json.price_unit; - this.price_type = json.price_type; + this.price_type = json.price_type || "original"; this.set_discount(json.discount); this.set_quantity(json.qty, "do not recompute unit price"); this.attribute_value_ids = json.attribute_value_ids || []; @@ -436,6 +441,7 @@ export class Orderline extends PosModel { this.skipChange = json.skip_change; this.combo_line_ids = json.combo_line_ids; this.combo_parent_id = json.combo_parent_id; + this.comboLine = this.pos.db.combo_line_by_id[json.combo_line_id]; } clone() { var orderline = new Orderline( @@ -791,6 +797,7 @@ export class Orderline extends PosModel { price_type: this.price_type, combo_line_ids: this.comboLines?.map((line) => line.id || line.cid), combo_parent_id: this.comboParent?.id || this.comboParent?.cid, + combo_line_id: this.comboLine?.id, }; } @@ -1925,8 +1932,11 @@ export class Order extends PosModel { var self = this; this.pricelist = pricelist; - var lines_to_recompute = this.get_orderlines().filter( - (line) => line.price_type === "original" + const orderlines = this.get_orderlines(); + + const lines_to_recompute = orderlines.filter( + (line) => + line.price_type === "original" && !(line.comboLines?.length || line.comboParent) ); lines_to_recompute.forEach((line) => { line.set_unit_price( @@ -1934,6 +1944,36 @@ export class Order extends PosModel { ); self.fix_tax_included_price(line); }); + const combo_parent_lines = orderlines.filter( + (line) => line.price_type === "original" && line.comboLines?.length + ); + const attributes_prices = {}; + combo_parent_lines.forEach((parentLine) => { + attributes_prices[parentLine.id] = this.compute_child_lines( + parentLine.product, + parentLine.comboLines.map((childLine) => { + const comboLineCopy = { ...childLine.comboLine }; + if (childLine.attribute_value_ids) { + comboLineCopy.configuration = { + attribute_value_ids: childLine.attribute_value_ids, + }; + } + return comboLineCopy; + }), + pricelist + ); + }); + const combo_children_lines = orderlines.filter( + (line) => line.price_type === "original" && line.comboParent + ); + combo_children_lines.forEach((line) => { + line.set_unit_price( + attributes_prices[line.comboParent.id].find( + (item) => item.comboLine.id === line.comboLine.id + ).price + ); + self.fix_tax_included_price(line); + }); } /** @@ -2032,46 +2072,56 @@ export class Order extends PosModel { this.select_orderline(line); } } - async addComboLines(comboParent, options) { - const parentLstPrice = comboParent.product.lst_price; - const originalTotal = options.comboLines.reduce((acc, comboLine) => { + + compute_child_lines(comboParentProduct, comboLines, pricelist) { + const combolines = []; + const parentLstPrice = comboParentProduct.get_price(pricelist, 1); + const originalTotal = comboLines.reduce((acc, comboLine) => { const originalPrice = this.pos.db.combo_by_id[comboLine.combo_id[0]].base_price; return acc + originalPrice; }, 0); let remainingTotal = parentLstPrice; - 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]]; + for (const comboLine of comboLines) { 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) { + if (comboLine == comboLines[comboLines.length - 1]) { priceUnit += remainingTotal; } - await this.pos.addProductToCurrentOrder(product, { - price: priceUnit, - comboParent, - }); + const attribute_value_ids = comboLine.configuration?.attribute_value_ids; + const attributesPriceExtra = (attribute_value_ids ?? []) + .map((id) => this.pos.db.attribute_value_by_id[id]?.price_extra || 0) + .reduce((acc, price) => acc + price, 0); + const totalPriceExtra = priceUnit + attributesPriceExtra + comboLine.combo_price; + combolines.push({ comboLine: comboLine, price: totalPriceExtra, attribute_value_ids }); } + return combolines; + } - // Take into account the extra prices. - const childLines = this.get_orderlines().filter( - (l) => l.comboParent?.uuid === comboParent.uuid + async addComboLines(comboParent, options) { + const comboLinesPrices = this.compute_child_lines( + comboParent.product, + options.comboLines, + this.pricelist ); - for (const comboLine of options.comboLines) { - const presentLine = childLines.find((l) => l.product.id === comboLine.product_id[0]); - if (presentLine) { - 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); - } + for (const line of comboLinesPrices) { + // Important to call addProductFromUi instead of addProductToCurrentOrder + // to avoid showing the ProductConfiguratorPopup. + // Product configuration is already done during the setup of the combo. + await this.pos.addProductFromUi( + this.pos.db.product_by_id[line.comboLine.product_id[0]], + { + price: line.price, + comboParent, + comboLine: line.comboLine, + attribute_value_ids: line.attribute_value_ids, + } + ); } } set_orderline_options(orderline, options) { @@ -2081,6 +2131,7 @@ export class Order extends PosModel { } if (options.comboParent) { orderline.comboParent = options.comboParent; + orderline.comboLine = options.comboLine; orderline.comboParent.comboLines.push(orderline); } if (options.quantity !== undefined) {