feat: Enhance loyalty discount application to prevent rounding errors for 100% discounts by generating individual reward lines.
This commit is contained in:
parent
4c6b4cec58
commit
3fed893293
0
__init__.py
Normal file → Executable file
0
__init__.py
Normal file → Executable file
0
__manifest__.py
Normal file → Executable file
0
__manifest__.py
Normal file → Executable file
BIN
__pycache__/__init__.cpython-312.pyc
Normal file
BIN
__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
49
static/src/overrides/models/loyalty.js
Normal file → Executable file
49
static/src/overrides/models/loyalty.js
Normal file → Executable file
@ -13,6 +13,8 @@ patch(Order.prototype, {
|
|||||||
// For all rewards, we calculate discounts without tax
|
// For all rewards, we calculate discounts without tax
|
||||||
let discountable = 0;
|
let discountable = 0;
|
||||||
const discountablePerTax = {};
|
const discountablePerTax = {};
|
||||||
|
const discountableWithTaxPerTax = {};
|
||||||
|
const formattedLines = [];
|
||||||
const orderLines = this.get_orderlines();
|
const orderLines = this.get_orderlines();
|
||||||
|
|
||||||
for (const line of orderLines) {
|
for (const line of orderLines) {
|
||||||
@ -35,10 +37,13 @@ patch(Order.prototype, {
|
|||||||
discountable += line_total_without_tax;
|
discountable += line_total_without_tax;
|
||||||
if (!discountablePerTax[taxKey]) {
|
if (!discountablePerTax[taxKey]) {
|
||||||
discountablePerTax[taxKey] = 0;
|
discountablePerTax[taxKey] = 0;
|
||||||
|
discountableWithTaxPerTax[taxKey] = 0;
|
||||||
}
|
}
|
||||||
discountablePerTax[taxKey] += line_total_without_tax;
|
discountablePerTax[taxKey] += line_total_without_tax;
|
||||||
|
discountableWithTaxPerTax[taxKey] += line.get_price_with_tax();
|
||||||
|
formattedLines.push(line);
|
||||||
}
|
}
|
||||||
return { discountable, discountablePerTax };
|
return { discountable, discountablePerTax, discountableWithTaxPerTax, formattedLines };
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -53,11 +58,14 @@ patch(Order.prototype, {
|
|||||||
|
|
||||||
// Use price without tax for discount calculation
|
// Use price without tax for discount calculation
|
||||||
const discountableWithoutTax = cheapestLine.get_price_without_tax();
|
const discountableWithoutTax = cheapestLine.get_price_without_tax();
|
||||||
|
const discountableWithTax = cheapestLine.get_price_with_tax();
|
||||||
const taxKey = cheapestLine.get_taxes().map((t) => t.id);
|
const taxKey = cheapestLine.get_taxes().map((t) => t.id);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
discountable: discountableWithoutTax,
|
discountable: discountableWithoutTax,
|
||||||
discountablePerTax: Object.fromEntries([[taxKey, discountableWithoutTax]]),
|
discountablePerTax: Object.fromEntries([[taxKey, discountableWithoutTax]]),
|
||||||
|
discountableWithTaxPerTax: Object.fromEntries([[taxKey, discountableWithTax]]),
|
||||||
|
formattedLines: [cheapestLine],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -71,13 +79,16 @@ patch(Order.prototype, {
|
|||||||
const discountLinesPerReward = {};
|
const discountLinesPerReward = {};
|
||||||
const orderLines = this.get_orderlines();
|
const orderLines = this.get_orderlines();
|
||||||
const remainingAmountPerLine = {};
|
const remainingAmountPerLine = {};
|
||||||
|
const remainingAmountWithTaxPerLine = {};
|
||||||
|
|
||||||
for (const line of orderLines) {
|
for (const line of orderLines) {
|
||||||
if (!line.get_quantity() || !line.price) {
|
if (!line.get_quantity() || !line.price) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const product_id = line.get_product().id;
|
const product_id = line.get_product().id;
|
||||||
remainingAmountPerLine[line.cid] = line.get_price_without_tax();
|
remainingAmountPerLine[line.cid] = line.get_price_without_tax();
|
||||||
|
remainingAmountWithTaxPerLine[line.cid] = line.get_price_with_tax();
|
||||||
|
|
||||||
let included = false;
|
let included = false;
|
||||||
|
|
||||||
@ -111,16 +122,20 @@ patch(Order.prototype, {
|
|||||||
|
|
||||||
let discountable = 0;
|
let discountable = 0;
|
||||||
const discountablePerTax = {};
|
const discountablePerTax = {};
|
||||||
|
const discountableWithTaxPerTax = {};
|
||||||
for (const line of linesToDiscount) {
|
for (const line of linesToDiscount) {
|
||||||
discountable += remainingAmountPerLine[line.cid];
|
discountable += remainingAmountPerLine[line.cid];
|
||||||
const taxKey = line.get_taxes().map((t) => t.id);
|
const taxKey = line.get_taxes().map((t) => t.id);
|
||||||
if (!discountablePerTax[taxKey]) {
|
if (!discountablePerTax[taxKey]) {
|
||||||
discountablePerTax[taxKey] = 0;
|
discountablePerTax[taxKey] = 0;
|
||||||
|
discountableWithTaxPerTax[taxKey] = 0;
|
||||||
}
|
}
|
||||||
discountablePerTax[taxKey] +=
|
discountablePerTax[taxKey] +=
|
||||||
remainingAmountPerLine[line.cid];
|
remainingAmountPerLine[line.cid];
|
||||||
|
discountableWithTaxPerTax[taxKey] +=
|
||||||
|
remainingAmountWithTaxPerLine[line.cid];
|
||||||
}
|
}
|
||||||
return { discountable, discountablePerTax };
|
return { discountable, discountablePerTax, discountableWithTaxPerTax, formattedLines: linesToDiscount };
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -145,7 +160,7 @@ patch(Order.prototype, {
|
|||||||
return "Unknown discount type";
|
return "Unknown discount type";
|
||||||
}
|
}
|
||||||
|
|
||||||
let { discountable, discountablePerTax } = getDiscountable(reward);
|
let { discountable, discountablePerTax, discountableWithTaxPerTax, formattedLines } = getDiscountable(reward);
|
||||||
// For all rewards, we should use total without tax for comparison
|
// For all rewards, we should use total without tax for comparison
|
||||||
const totalForComparison = this.get_total_without_tax();
|
const totalForComparison = this.get_total_without_tax();
|
||||||
discountable = Math.min(totalForComparison, discountable);
|
discountable = Math.min(totalForComparison, discountable);
|
||||||
@ -191,13 +206,39 @@ patch(Order.prototype, {
|
|||||||
|
|
||||||
const discountFactor = totalDiscountableWithoutTax ? Math.min(1, maxDiscount / totalDiscountableWithoutTax) : 1;
|
const discountFactor = totalDiscountableWithoutTax ? Math.min(1, maxDiscount / totalDiscountableWithoutTax) : 1;
|
||||||
|
|
||||||
|
if ((1 - discountFactor) < 0.00001) {
|
||||||
|
// 100% Discount: Generate one reward line per original line to prevent rounding aggregation errors
|
||||||
|
return formattedLines.map(line => {
|
||||||
|
const taxKey = ['ewallet', 'gift_card'].includes(reward.program_id.program_type)
|
||||||
|
? line.get_taxes().map((t) => t.id)
|
||||||
|
: line.get_taxes().filter((t) => t.amount_type !== 'fixed').map((t) => t.id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
product: reward.discount_line_product_id,
|
||||||
|
price: -line.get_price_without_tax(),
|
||||||
|
quantity: 1,
|
||||||
|
reward_id: reward.id,
|
||||||
|
is_reward_line: true,
|
||||||
|
coupon_id: coupon_id,
|
||||||
|
points_cost: 0,
|
||||||
|
reward_identifier_code: rewardCode,
|
||||||
|
tax_ids: taxKey, // Use IDs, not tax objects
|
||||||
|
merge: false,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const result = Object.entries(discountablePerTax).reduce((lst, entry) => {
|
const result = Object.entries(discountablePerTax).reduce((lst, entry) => {
|
||||||
if (!entry[1]) {
|
if (!entry[1]) {
|
||||||
return lst;
|
return lst;
|
||||||
}
|
}
|
||||||
const taxIds = entry[0] === "" ? [] : entry[0].split(",").map((str) => parseInt(str));
|
const taxIds = entry[0] === "" ? [] : entry[0].split(",").map((str) => parseInt(str));
|
||||||
// Calculate tax-exclusive discount value for display
|
// Calculate tax-exclusive discount value for display
|
||||||
const preliminaryAmount = entry[1] * discountFactor;
|
let preliminaryAmount = entry[1] * discountFactor;
|
||||||
|
|
||||||
|
// Back-calculate logic removed as line-by-line strategy handles 100% case.
|
||||||
|
// Keeping partial discount logic standard for now.
|
||||||
|
|
||||||
const discountAmount = roundPrecision(preliminaryAmount, this.pos.currency.rounding);
|
const discountAmount = roundPrecision(preliminaryAmount, this.pos.currency.rounding);
|
||||||
lst.push({
|
lst.push({
|
||||||
product: reward.discount_line_product_id,
|
product: reward.discount_line_product_id,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user