feat: Implement and configure loyalty discounts to apply before tax in POS.
This commit is contained in:
parent
8d756a0e6f
commit
a7c6781af5
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "POS Loyalty Discount Before Tax",
|
"name": "POS Loyalty Discount Before Tax",
|
||||||
"version": "1.6",
|
"version": "19.0.1.0.0",
|
||||||
"category": "Point of Sale",
|
"category": "Point of Sale",
|
||||||
"summary": "Modify loyalty reward discount calculation to apply before tax in POS",
|
"summary": "Modify loyalty reward discount calculation to apply before tax in POS",
|
||||||
"author": "Suherdy Yacob",
|
"author": "Suherdy Yacob",
|
||||||
|
|||||||
1
pos_loyalty_discount_before_tax
Submodule
1
pos_loyalty_discount_before_tax
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit d975966b70f550bb2de8dbf35abb0e5106a56371
|
||||||
@ -1,71 +1,59 @@
|
|||||||
/** @odoo-module **/
|
/** @odoo-module **/
|
||||||
|
|
||||||
import { Order } from "@point_of_sale/app/store/models";
|
import { PosOrder } from "@point_of_sale/app/models/pos_order";
|
||||||
|
import { _t } from "@web/core/l10n/translation";
|
||||||
|
import { formatCurrency } from "@web/core/currency";
|
||||||
import { patch } from "@web/core/utils/patch";
|
import { patch } from "@web/core/utils/patch";
|
||||||
import { roundPrecision } from "@web/core/utils/numbers";
|
|
||||||
|
|
||||||
// Patch Order methods to handle all loyalty discounts with discount_before_tax
|
// Patch PosOrder methods to handle all loyalty discounts with discount_before_tax
|
||||||
patch(Order.prototype, {
|
patch(PosOrder.prototype, {
|
||||||
/**
|
/**
|
||||||
* Override to calculate discountable amount without tax for all reward types
|
* Override to calculate discountable amount without tax for all reward types
|
||||||
*/
|
*/
|
||||||
_getDiscountableOnOrder(reward) {
|
_getDiscountableOnOrder(reward) {
|
||||||
// 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();
|
|
||||||
|
|
||||||
for (const line of orderLines) {
|
for (const line of this.getOrderlines()) {
|
||||||
if (!line.get_quantity()) {
|
if (!line.getQuantity()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip reward lines to avoid circular discounts (unless specifically allowed)
|
// Skip reward lines to avoid circular discounts
|
||||||
if (line.reward_id || line.is_reward_line) {
|
if (line.reward_id || line.is_reward_line) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use price without tax for discount calculation
|
|
||||||
const line_total_without_tax = line.get_price_without_tax();
|
|
||||||
|
|
||||||
const taxKey = ['ewallet', 'gift_card'].includes(reward.program_id.program_type)
|
const taxKey = ['ewallet', 'gift_card'].includes(reward.program_id.program_type)
|
||||||
? line.get_taxes().map((t) => t.id)
|
? line.tax_ids.map((t) => t.id)
|
||||||
: line.get_taxes().filter((t) => t.amount_type !== 'fixed').map((t) => t.id);
|
: line.tax_ids.filter((t) => t.amount_type !== 'fixed').map((t) => t.id);
|
||||||
|
|
||||||
|
// Use priceExcl instead of total_included (price_without_tax)
|
||||||
|
discountable += line.priceExcl;
|
||||||
|
|
||||||
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;
|
// basePrice is effectively quantity * unit_price * (1 - discount), before tax is handled
|
||||||
discountableWithTaxPerTax[taxKey] += line.get_price_with_tax();
|
discountablePerTax[taxKey] += line.basePrice;
|
||||||
formattedLines.push(line);
|
|
||||||
}
|
}
|
||||||
return { discountable, discountablePerTax, discountableWithTaxPerTax, formattedLines };
|
return { discountable, discountablePerTax };
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Override to calculate cheapest line discountable without tax for all reward types
|
* Override to calculate cheapest line discountable without tax for all reward types
|
||||||
*/
|
*/
|
||||||
_getDiscountableOnCheapest(reward) {
|
_getDiscountableOnCheapest(reward) {
|
||||||
// For all rewards, we calculate discounts without tax
|
const cheapestLine = this._getCheapestLine(reward);
|
||||||
const cheapestLine = this._getCheapestLine();
|
|
||||||
if (!cheapestLine) {
|
if (!cheapestLine) {
|
||||||
return { discountable: 0, discountablePerTax: {} };
|
return { discountable: 0, discountablePerTax: {} };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use price without tax for discount calculation
|
const taxKey = cheapestLine.tax_ids.map((t) => t.id);
|
||||||
const discountableWithoutTax = cheapestLine.get_price_without_tax();
|
|
||||||
const discountableWithTax = cheapestLine.get_price_with_tax();
|
|
||||||
const taxKey = cheapestLine.get_taxes().map((t) => t.id);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
discountable: discountableWithoutTax,
|
discountable: cheapestLine.comboTotalPriceWithoutTax,
|
||||||
discountablePerTax: Object.fromEntries([[taxKey, discountableWithoutTax]]),
|
discountablePerTax: Object.fromEntries([[taxKey, cheapestLine.comboTotalPriceWithoutTax]]),
|
||||||
discountableWithTaxPerTax: Object.fromEntries([[taxKey, discountableWithTax]]),
|
|
||||||
formattedLines: [cheapestLine],
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -73,45 +61,42 @@ patch(Order.prototype, {
|
|||||||
* Override to calculate specific product discountable without tax for all reward types
|
* Override to calculate specific product discountable without tax for all reward types
|
||||||
*/
|
*/
|
||||||
_getDiscountableOnSpecific(reward) {
|
_getDiscountableOnSpecific(reward) {
|
||||||
// For all rewards, we calculate discounts without tax
|
const applicableProductIds = new Set(reward.all_discount_product_ids.map((p) => p.id));
|
||||||
const applicableProducts = reward.all_discount_product_ids;
|
|
||||||
const linesToDiscount = [];
|
const linesToDiscount = [];
|
||||||
const discountLinesPerReward = {};
|
const discountLinesPerReward = {};
|
||||||
const orderLines = this.get_orderlines();
|
const orderLines = this.getOrderlines();
|
||||||
|
const orderProducts = orderLines.map((line) => line.product_id.id);
|
||||||
const remainingAmountPerLine = {};
|
const remainingAmountPerLine = {};
|
||||||
const remainingAmountWithTaxPerLine = {};
|
|
||||||
|
|
||||||
for (const line of orderLines) {
|
for (const line of orderLines) {
|
||||||
if (!line.get_quantity() || !line.price) {
|
if (!line.getQuantity() || !line.price_unit) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const product_id = line.get_product().id;
|
remainingAmountPerLine[line.uuid] = line.priceExcl;
|
||||||
remainingAmountPerLine[line.cid] = line.get_price_without_tax();
|
const product_id = line.combo_parent_id?.product_id.id || line.getProduct().id;
|
||||||
remainingAmountWithTaxPerLine[line.cid] = line.get_price_with_tax();
|
|
||||||
|
|
||||||
let included = false;
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
applicableProducts.has(product_id) ||
|
applicableProductIds.has(product_id) ||
|
||||||
(line.reward_product_id && applicableProducts.has(line.reward_product_id))
|
(line._reward_product_id && applicableProductIds.has(line._reward_product_id.id))
|
||||||
) {
|
) {
|
||||||
linesToDiscount.push(line);
|
linesToDiscount.push(line);
|
||||||
included = true;
|
|
||||||
} else if (line.reward_id) {
|
} else if (line.reward_id) {
|
||||||
const lineReward = this.pos.reward_by_id[line.reward_id];
|
const lineReward = line.reward_id;
|
||||||
if (lineReward.id === reward.id ||
|
const lineRewardApplicableProductsIds = new Set(
|
||||||
(
|
lineReward.all_discount_product_ids.map((p) => p.id)
|
||||||
orderLines.some(product =>
|
);
|
||||||
lineReward.all_discount_product_ids.has(product.get_product().id) &&
|
if (
|
||||||
applicableProducts.has(product.get_product().id)
|
lineReward.id === reward.id ||
|
||||||
) &&
|
(orderProducts.some(
|
||||||
lineReward.reward_type === 'discount' &&
|
(product) =>
|
||||||
lineReward.discount_mode != 'percent'
|
lineRewardApplicableProductsIds.has(product) &&
|
||||||
)
|
applicableProductIds.has(product)
|
||||||
|
) &&
|
||||||
|
lineReward.reward_type === "discount" &&
|
||||||
|
lineReward.discount_mode != "percent")
|
||||||
) {
|
) {
|
||||||
linesToDiscount.push(line);
|
linesToDiscount.push(line);
|
||||||
included = true;
|
|
||||||
}
|
}
|
||||||
if (!discountLinesPerReward[line.reward_identifier_code]) {
|
if (!discountLinesPerReward[line.reward_identifier_code]) {
|
||||||
discountLinesPerReward[line.reward_identifier_code] = [];
|
discountLinesPerReward[line.reward_identifier_code] = [];
|
||||||
@ -120,22 +105,50 @@ patch(Order.prototype, {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let cheapestLine = false;
|
||||||
|
for (const lines of Object.values(discountLinesPerReward)) {
|
||||||
|
const lineReward = lines[0].reward_id;
|
||||||
|
if (lineReward.reward_type !== "discount") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let discountedLines = orderLines;
|
||||||
|
if (lineReward.discount_applicability === "cheapest") {
|
||||||
|
cheapestLine = cheapestLine || this._getCheapestLine(lineReward);
|
||||||
|
discountedLines = [cheapestLine];
|
||||||
|
} else if (lineReward.discount_applicability === "specific") {
|
||||||
|
discountedLines = this._getSpecificDiscountableLines(lineReward);
|
||||||
|
}
|
||||||
|
if (!discountedLines.length) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (lineReward.discount_mode === "percent") {
|
||||||
|
const discount = lineReward.discount / 100;
|
||||||
|
for (const line of discountedLines) {
|
||||||
|
if (line.reward_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (lineReward.discount_applicability === "cheapest") {
|
||||||
|
remainingAmountPerLine[line.uuid] *= 1 - discount / line.getQuantity();
|
||||||
|
} else {
|
||||||
|
remainingAmountPerLine[line.uuid] *= 1 - discount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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.uuid];
|
||||||
const taxKey = line.get_taxes().map((t) => t.id);
|
const taxKey = line.tax_ids.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];
|
line.basePrice * (remainingAmountPerLine[line.uuid] / line.priceExcl);
|
||||||
discountableWithTaxPerTax[taxKey] +=
|
|
||||||
remainingAmountWithTaxPerLine[line.cid];
|
|
||||||
}
|
}
|
||||||
return { discountable, discountablePerTax, discountableWithTaxPerTax, formattedLines: linesToDiscount };
|
return { discountable, discountablePerTax };
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -144,10 +157,7 @@ patch(Order.prototype, {
|
|||||||
_getRewardLineValuesDiscount(args) {
|
_getRewardLineValuesDiscount(args) {
|
||||||
const reward = args["reward"];
|
const reward = args["reward"];
|
||||||
const coupon_id = args["coupon_id"];
|
const coupon_id = args["coupon_id"];
|
||||||
|
|
||||||
// For all rewards, we calculate discounts without tax
|
|
||||||
const rewardAppliesTo = reward.discount_applicability;
|
const rewardAppliesTo = reward.discount_applicability;
|
||||||
|
|
||||||
let getDiscountable;
|
let getDiscountable;
|
||||||
if (rewardAppliesTo === "order") {
|
if (rewardAppliesTo === "order") {
|
||||||
getDiscountable = this._getDiscountableOnOrder.bind(this);
|
getDiscountable = this._getDiscountableOnOrder.bind(this);
|
||||||
@ -157,122 +167,104 @@ patch(Order.prototype, {
|
|||||||
getDiscountable = this._getDiscountableOnSpecific.bind(this);
|
getDiscountable = this._getDiscountableOnSpecific.bind(this);
|
||||||
}
|
}
|
||||||
if (!getDiscountable) {
|
if (!getDiscountable) {
|
||||||
return "Unknown discount type";
|
return _t("Unknown discount type");
|
||||||
}
|
}
|
||||||
|
|
||||||
let { discountable, discountablePerTax, discountableWithTaxPerTax, formattedLines } = getDiscountable(reward);
|
let { discountable, discountablePerTax } = getDiscountable(reward);
|
||||||
// For all rewards, we should use total without tax for comparison
|
|
||||||
const totalForComparison = this.get_total_without_tax();
|
// Use priceExcl instead of priceIncl for maximum limit comparison
|
||||||
discountable = Math.min(totalForComparison, discountable);
|
discountable = Math.min(this.priceExcl, discountable);
|
||||||
if (!discountable) {
|
|
||||||
|
if (Math.abs(discountable) < 0.0001) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
let maxDiscount = reward.discount_max_amount || Infinity;
|
let maxDiscount = reward.discount_max_amount || Infinity;
|
||||||
if (reward.discount_mode === "per_point") {
|
if (reward.discount_mode === "per_point") {
|
||||||
const points = (["ewallet", "gift_card"].includes(reward.program_id.program_type)) ?
|
// Rewards cannot be partially offered to customers
|
||||||
this._getRealCouponPoints(coupon_id) :
|
const points = ["ewallet", "gift_card"].includes(reward.program_id.program_type)
|
||||||
Math.floor(this._getRealCouponPoints(coupon_id) / reward.required_points) * reward.required_points;
|
? this._getRealCouponPoints(coupon_id)
|
||||||
maxDiscount = Math.min(
|
: Math.floor(this._getRealCouponPoints(coupon_id) / reward.required_points) *
|
||||||
maxDiscount,
|
reward.required_points;
|
||||||
roundPrecision(reward.discount * points, this.pos.currency.rounding)
|
maxDiscount = Math.min(maxDiscount, reward.discount * points);
|
||||||
);
|
|
||||||
} else if (reward.discount_mode === "per_order") {
|
} else if (reward.discount_mode === "per_order") {
|
||||||
maxDiscount = Math.min(maxDiscount, reward.discount);
|
maxDiscount = Math.min(maxDiscount, reward.discount);
|
||||||
} else if (reward.discount_mode === "percent") {
|
} else if (reward.discount_mode === "percent") {
|
||||||
maxDiscount = Math.min(maxDiscount, roundPrecision(discountable * (reward.discount / 100), this.pos.currency.rounding));
|
maxDiscount = Math.min(maxDiscount, discountable * (reward.discount / 100));
|
||||||
}
|
}
|
||||||
|
|
||||||
const rewardCode = Math.random().toString(36).substring(3);
|
const rewardCode = (Math.random() + 1).toString(36).substring(7);
|
||||||
let pointCost = reward.clear_wallet
|
let pointCost = reward.clear_wallet
|
||||||
? this._getRealCouponPoints(coupon_id)
|
? this._getRealCouponPoints(coupon_id)
|
||||||
: reward.required_points;
|
: reward.required_points;
|
||||||
|
|
||||||
if (reward.discount_mode === "per_point" && !reward.clear_wallet) {
|
if (reward.discount_mode === "per_point" && !reward.clear_wallet) {
|
||||||
pointCost = roundPrecision(Math.min(maxDiscount, discountable) / reward.discount, this.pos.currency.rounding);
|
pointCost = Math.min(maxDiscount, discountable) / reward.discount;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply rounding to pointCost if it's calculated from division
|
// These are considered payments and do not require to be either taxed or split by tax
|
||||||
if (pointCost && typeof pointCost === 'number') {
|
const discountProduct = reward.discount_line_product_id;
|
||||||
pointCost = roundPrecision(pointCost, this.pos.currency.rounding);
|
if (["ewallet", "gift_card"].includes(reward.program_id.program_type)) {
|
||||||
}
|
const price = discountProduct.getTaxDetails({
|
||||||
|
overridedValues: {
|
||||||
|
tax_ids: discountProduct.taxes_id,
|
||||||
|
price_unit: -Math.min(maxDiscount, discountable),
|
||||||
|
special_mode: "total_included",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// For all rewards, we calculate discount on price without tax
|
return [
|
||||||
// Calculate the total discountable amount without tax
|
{
|
||||||
let totalDiscountableWithoutTax = 0;
|
product_id: discountProduct,
|
||||||
for (const [, amount] of Object.entries(discountablePerTax)) {
|
price_unit: price.total_excluded,
|
||||||
totalDiscountableWithoutTax += amount;
|
qty: 1,
|
||||||
}
|
reward_id: reward,
|
||||||
|
|
||||||
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 (0.01 issues)
|
|
||||||
// Tags added for UI-level grouping
|
|
||||||
return formattedLines.map((line, index) => {
|
|
||||||
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,
|
is_reward_line: true,
|
||||||
coupon_id: coupon_id,
|
coupon_id: coupon_id,
|
||||||
points_cost: 0,
|
points_cost: pointCost,
|
||||||
reward_identifier_code: rewardCode,
|
reward_identifier_code: rewardCode,
|
||||||
tax_ids: taxKey, // Use IDs, not tax objects
|
tax_ids: discountProduct.taxes_id,
|
||||||
merge: false,
|
},
|
||||||
is_reward_group_member: true,
|
];
|
||||||
reward_group_id: rewardCode,
|
|
||||||
is_reward_group_head: index === 0,
|
|
||||||
reward_group_count: formattedLines.length,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const discountFactor = discountable ? Math.min(1, maxDiscount / discountable) : 1;
|
||||||
const result = Object.entries(discountablePerTax).reduce((lst, entry) => {
|
const result = Object.entries(discountablePerTax).reduce((lst, entry) => {
|
||||||
|
// Ignore 0 price lines
|
||||||
if (!entry[1]) {
|
if (!entry[1]) {
|
||||||
return lst;
|
return lst;
|
||||||
}
|
}
|
||||||
const taxIds = entry[0] === "" ? [] : entry[0].split(",").map((str) => parseInt(str));
|
let taxIds = entry[0] === "" ? [] : entry[0].split(",").map((str) => parseInt(str));
|
||||||
// Calculate tax-exclusive discount value for display
|
if (this.models) {
|
||||||
let preliminaryAmount = entry[1] * discountFactor;
|
taxIds = this.models["account.tax"].filter((tax) => taxIds.includes(tax.id));
|
||||||
|
} else if (this.pos) {
|
||||||
|
// Fallback if this is somehow running differently
|
||||||
|
taxIds = this.pos.taxes.filter((tax) => taxIds.includes(tax.id));
|
||||||
|
}
|
||||||
|
|
||||||
// Back-calculate logic removed as line-by-line strategy handles 100% case.
|
var discount_amount = -(Math.min(this.priceExcl, entry[1]) * discountFactor);
|
||||||
// Keeping partial discount logic standard for now.
|
// OVERRIDE: Inject JSON variables (will be automatically assigned in Odoo 19 extraFields, we must make sure these properties are forwarded)
|
||||||
|
|
||||||
const discountAmount = roundPrecision(preliminaryAmount, this.pos.currency.rounding);
|
|
||||||
lst.push({
|
lst.push({
|
||||||
product: reward.discount_line_product_id,
|
product_id: discountProduct,
|
||||||
price: -discountAmount,
|
price_unit: discount_amount,
|
||||||
quantity: 1,
|
qty: 1,
|
||||||
reward_id: reward.id,
|
reward_id: reward,
|
||||||
is_reward_line: true,
|
is_reward_line: true,
|
||||||
coupon_id: coupon_id,
|
coupon_id: coupon_id,
|
||||||
points_cost: 0,
|
points_cost: 0,
|
||||||
reward_identifier_code: rewardCode,
|
reward_identifier_code: rewardCode,
|
||||||
tax_ids: taxIds,
|
tax_ids: taxIds,
|
||||||
merge: false,
|
|
||||||
|
is_reward_group_member : true,
|
||||||
|
is_reward_group_head : false,
|
||||||
|
reward_group_id : reward.id,
|
||||||
});
|
});
|
||||||
return lst;
|
return lst;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (result.length) {
|
if (result.length) {
|
||||||
result[0]["points_cost"] = pointCost;
|
result[0]["points_cost"] = pointCost;
|
||||||
|
result[0]["is_reward_group_head"] = true;
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
set_orderline_options(line, options) {
|
|
||||||
super.set_orderline_options(...arguments);
|
|
||||||
if (options && options.is_reward_group_member) {
|
|
||||||
line.is_reward_group_member = options.is_reward_group_member;
|
|
||||||
line.reward_group_id = options.reward_group_id;
|
|
||||||
line.is_reward_group_head = options.is_reward_group_head;
|
|
||||||
line.reward_group_count = options.reward_group_count;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,76 +1,90 @@
|
|||||||
/** @odoo-module **/
|
/** @odoo-module **/
|
||||||
|
|
||||||
import { Order, Orderline } from "@point_of_sale/app/store/models";
|
import { PosOrderline } from "@point_of_sale/app/models/pos_order_line";
|
||||||
|
import { formatCurrency } from "@web/core/currency";
|
||||||
import { patch } from "@web/core/utils/patch";
|
import { patch } from "@web/core/utils/patch";
|
||||||
|
|
||||||
patch(Order.prototype, {
|
patch(PosOrderline, {
|
||||||
export_for_printing() {
|
extraFields: {
|
||||||
const result = super.export_for_printing(...arguments);
|
...(PosOrderline.extraFields || {}),
|
||||||
if (result.orderlines) {
|
is_reward_group_member: {
|
||||||
result.orderlines = result.orderlines.filter((lineData) => {
|
model: "pos.order.line",
|
||||||
const line = this.orderlines.find((l) => l.cid === lineData.cid);
|
name: "is_reward_group_member",
|
||||||
if (line && line.is_reward_group_member && !line.is_reward_group_head) {
|
type: "boolean",
|
||||||
return false;
|
local: true,
|
||||||
}
|
},
|
||||||
return true;
|
reward_group_id: {
|
||||||
});
|
model: "pos.order.line",
|
||||||
}
|
name: "reward_group_id",
|
||||||
return result;
|
type: "char",
|
||||||
|
local: true,
|
||||||
|
},
|
||||||
|
is_reward_group_head: {
|
||||||
|
model: "pos.order.line",
|
||||||
|
name: "is_reward_group_head",
|
||||||
|
type: "boolean",
|
||||||
|
local: true,
|
||||||
|
},
|
||||||
|
reward_group_count: {
|
||||||
|
model: "pos.order.line",
|
||||||
|
name: "reward_group_count",
|
||||||
|
type: "integer",
|
||||||
|
local: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
patch(Orderline.prototype, {
|
patch(PosOrderline.prototype, {
|
||||||
getDisplayClasses() {
|
setOptions(options) {
|
||||||
// Need to ensure we call the original function, might not exist in some versions or might be different
|
if (options.is_reward_group_member) {
|
||||||
let classes = {};
|
this.is_reward_group_member = options.is_reward_group_member;
|
||||||
if (typeof super.getDisplayClasses === 'function') {
|
this.reward_group_id = options.reward_group_id;
|
||||||
classes = super.getDisplayClasses();
|
this.is_reward_group_head = options.is_reward_group_head;
|
||||||
|
this.reward_group_count = options.reward_group_count;
|
||||||
}
|
}
|
||||||
|
return super.setOptions(...arguments);
|
||||||
|
},
|
||||||
|
|
||||||
|
getDisplayClasses() {
|
||||||
|
// Hide secondary lines of the reward group
|
||||||
|
const classes = super.getDisplayClasses ? super.getDisplayClasses() : {};
|
||||||
if (this.is_reward_group_member && !this.is_reward_group_head) {
|
if (this.is_reward_group_member && !this.is_reward_group_head) {
|
||||||
classes['d-none'] = true;
|
classes["d-none"] = true;
|
||||||
}
|
}
|
||||||
return classes;
|
return classes;
|
||||||
},
|
},
|
||||||
getDisplayData() {
|
|
||||||
const data = super.getDisplayData();
|
get currencyDisplayPrice() {
|
||||||
data.cid = this.cid;
|
|
||||||
if (this.is_reward_group_head) {
|
if (this.is_reward_group_head) {
|
||||||
// Group and sum all lines in this reward group for display
|
const groupLines = this.order_id.lines.filter(
|
||||||
const groupLines = this.order.get_orderlines().filter(
|
|
||||||
line => line.reward_group_id === this.reward_group_id
|
line => line.reward_group_id === this.reward_group_id
|
||||||
);
|
);
|
||||||
|
|
||||||
let totalDisplayPrice = 0;
|
let totalDisplayPrice = 0;
|
||||||
let totalQty = 0;
|
|
||||||
for (const line of groupLines) {
|
for (const line of groupLines) {
|
||||||
totalDisplayPrice += line.get_display_price();
|
totalDisplayPrice += line.displayPrice;
|
||||||
totalQty += line.get_quantity();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// this.env might not exist in Odoo 19 this way, we'll try to use this.pos.env if this.env is absent.
|
return formatCurrency(totalDisplayPrice, this.currency.id);
|
||||||
const env = this.env || this.pos.env;
|
|
||||||
data.price = env.utils.formatCurrency(totalDisplayPrice, this.pos.currency);
|
|
||||||
data.qty = totalQty.toString();
|
|
||||||
}
|
}
|
||||||
return data;
|
return super.currencyDisplayPrice;
|
||||||
},
|
},
|
||||||
export_as_JSON() {
|
|
||||||
const result = super.export_as_JSON(...arguments);
|
getQuantityStr() {
|
||||||
if (this.is_reward_group_member) {
|
const res = super.getQuantityStr();
|
||||||
result.is_reward_group_member = this.is_reward_group_member;
|
if (this.is_reward_group_head) {
|
||||||
result.reward_group_id = this.reward_group_id;
|
const groupLines = this.order_id.lines.filter(
|
||||||
result.is_reward_group_head = this.is_reward_group_head;
|
line => line.reward_group_id === this.reward_group_id
|
||||||
result.reward_group_count = this.reward_group_count;
|
);
|
||||||
|
|
||||||
|
let totalQty = 0;
|
||||||
|
for (const line of groupLines) {
|
||||||
|
totalQty += line.getQuantity();
|
||||||
|
}
|
||||||
|
res.unitPart = "" + totalQty;
|
||||||
|
res.decimalPart = "";
|
||||||
|
res.qtyStr = "" + totalQty;
|
||||||
}
|
}
|
||||||
return result;
|
return res;
|
||||||
},
|
|
||||||
init_from_JSON(json) {
|
|
||||||
if (json.is_reward_group_member) {
|
|
||||||
this.is_reward_group_member = json.is_reward_group_member;
|
|
||||||
this.reward_group_id = json.reward_group_id;
|
|
||||||
this.is_reward_group_head = json.is_reward_group_head;
|
|
||||||
this.reward_group_count = json.reward_group_count;
|
|
||||||
}
|
|
||||||
super.init_from_JSON(...arguments);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user