[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) <jcb@odoo.com>
This commit is contained in:
Loukas Wets (lowe)
2023-11-24 16:22:03 +00:00
parent 362ef0042a
commit 79f6e17a48
4 changed files with 115 additions and 43 deletions
@@ -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;
}
}
}
}
@@ -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;
}
@@ -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 }"
/>
<label t-attf-for="combo-{{combo_id}}-combo_line-{{combo_line_id}}" class="combo-line h-100 w-100 rounded cursor-pointer transition-base">
<ProductCard
@@ -26,7 +27,7 @@
productId="product.id"
price="formattedComboPrice(combo_line)"
imageUrl="product.getImageUrl()"
onClick="() => {}"
onClick="(ev) => this.onClickProduct({ product, combo_line }, ev)"
/>
</label>
</div>
@@ -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) {