fix if split_pendapatan_payment module also installed

This commit is contained in:
admin.suherdy 2025-11-07 09:48:01 +07:00
parent fdb4eac9a1
commit 9ae5b96956
5 changed files with 279 additions and 279 deletions

View File

@ -1,49 +1,49 @@
# POS Loyalty Discount Before Tax
This module modifies the loyalty reward discount calculation in POS to apply discounts before tax calculation and ensures that discount values are displayed as tax-exclusive in both the POS receipt and accounting entries.
## Features
1. **Discount Calculation Before Tax**: All loyalty rewards are calculated on the tax-exclusive amount rather than the tax-inclusive amount.
2. **Tax-Exclusive Display**: Discount values are displayed as tax-exclusive in the POS receipt.
3. **Proper Accounting Entries**: Accounting entries are created with tax only on the credit side, ensuring compliance with accounting standards.
## Technical Changes
### JavaScript Changes
- Modified `static/src/overrides/models/loyalty.js` to calculate discounts on tax-exclusive amounts
- Ensured discount line values are tax-exclusive for display purposes
### Backend Changes
- Created `models/pos_order.py` to handle accounting entries for loyalty discounts
- Created `models/loyalty_reward.py` to ensure discount products are properly configured
- Created `models/account_move.py` to ensure discount lines in account moves are tax-exclusive
- Created `models/pos_session.py` to handle session-level accounting for loyalty discounts
## Installation
1. Copy the module to your Odoo addons directory
2. Update the apps list in Odoo
3. Install the "POS Loyalty Discount Before Tax" module
## Configuration
No additional configuration is required. The module automatically applies the changes to all loyalty programs in POS.
## Usage
The module works automatically with all existing loyalty programs. Discounts will be calculated on tax-exclusive amounts and displayed as such in the POS interface and accounting entries.
## Compatibility
This module is compatible with Odoo 17 and requires the following modules:
- point_of_sale
- pos_loyalty
## License
LGPL-3
# POS Loyalty Discount Before Tax
This module modifies the loyalty reward discount calculation in POS to apply discounts before tax calculation and ensures that discount values are displayed as tax-exclusive in both the POS receipt and accounting entries.
## Features
1. **Discount Calculation Before Tax**: All loyalty rewards are calculated on the tax-exclusive amount rather than the tax-inclusive amount.
2. **Tax-Exclusive Display**: Discount values are displayed as tax-exclusive in the POS receipt.
3. **Proper Accounting Entries**: Accounting entries are created with tax only on the credit side, ensuring compliance with accounting standards.
## Technical Changes
### JavaScript Changes
- Modified `static/src/overrides/models/loyalty.js` to calculate discounts on tax-exclusive amounts
- Ensured discount line values are tax-exclusive for display purposes
### Backend Changes
- Created `models/pos_order.py` to handle accounting entries for loyalty discounts
- Created `models/loyalty_reward.py` to ensure discount products are properly configured
- Created `models/account_move.py` to ensure discount lines in account moves are tax-exclusive
- Created `models/pos_session.py` to handle session-level accounting for loyalty discounts
## Installation
1. Copy the module to your Odoo addons directory
2. Update the apps list in Odoo
3. Install the "POS Loyalty Discount Before Tax" module
## Configuration
No additional configuration is required. The module automatically applies the changes to all loyalty programs in POS.
## Usage
The module works automatically with all existing loyalty programs. Discounts will be calculated on tax-exclusive amounts and displayed as such in the POS interface and accounting entries.
## Compatibility
This module is compatible with Odoo 17 and requires the following modules:
- point_of_sale
- pos_loyalty
## License
LGPL-3

View File

@ -1,22 +1,22 @@
{
"name": "POS Loyalty Discount Before Tax",
"version": "1.0",
"category": "Point of Sale",
"summary": "Modify loyalty reward discount calculation to apply before tax in POS",
"author": "Suherdy Yacob",
"description": """
This module modifies the loyalty reward discount calculation in POS to apply discounts before tax calculation.
""",
"depends": ["point_of_sale", "pos_loyalty"],
"data": [
],
"assets": {
"point_of_sale._assets_pos": [
'pos_loyalty_discount_before_tax/static/src/overrides/models/loyalty.js',
]
},
"installable": True,
"auto_install": False,
"license": "LGPL-3"
}
{
"name": "POS Loyalty Discount Before Tax",
"version": "1.0",
"category": "Point of Sale",
"summary": "Modify loyalty reward discount calculation to apply before tax in POS",
"author": "Suherdy Yacob",
"description": """
This module modifies the loyalty reward discount calculation in POS to apply discounts before tax calculation.
""",
"depends": ["point_of_sale", "pos_loyalty"],
"data": [
],
"assets": {
"point_of_sale._assets_pos": [
'pos_loyalty_discount_before_tax/static/src/overrides/models/loyalty.js',
]
},
"installable": True,
"auto_install": False,
"license": "LGPL-3"
}

Binary file not shown.

Binary file not shown.

View File

@ -1,208 +1,208 @@
/** @odoo-module **/
import { Order } from "@point_of_sale/app/store/models";
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(Order.prototype, {
/**
* Override to calculate discountable amount without tax for all reward types
*/
_getDiscountableOnOrder(reward) {
// For all rewards, we calculate discounts without tax
let discountable = 0;
const discountablePerTax = {};
for (const line of this.get_orderlines()) {
if (!line.get_quantity()) {
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)
? line.get_taxes().map((t) => t.id)
: line.get_taxes().filter((t) => t.amount_type !== 'fixed').map((t) => t.id);
discountable += line_total_without_tax;
if (!discountablePerTax[taxKey]) {
discountablePerTax[taxKey] = 0;
}
discountablePerTax[taxKey] += line_total_without_tax;
}
return { discountable, discountablePerTax };
},
/**
* Override to calculate cheapest line discountable without tax for all reward types
*/
_getDiscountableOnCheapest(reward) {
// For all rewards, we calculate discounts without tax
const cheapestLine = this._getCheapestLine();
if (!cheapestLine) {
return { discountable: 0, discountablePerTax: {} };
}
// Use price without tax for discount calculation
const discountableWithoutTax = cheapestLine.get_price_without_tax();
const taxKey = cheapestLine.get_taxes().map((t) => t.id);
return {
discountable: discountableWithoutTax,
discountablePerTax: Object.fromEntries([[taxKey, discountableWithoutTax]]),
};
},
/**
* Override to calculate specific product discountable without tax for all reward types
*/
_getDiscountableOnSpecific(reward) {
// For all rewards, we calculate discounts without tax
const applicableProducts = reward.all_discount_product_ids;
const linesToDiscount = [];
const discountLinesPerReward = {};
const orderLines = this.get_orderlines();
const remainingAmountPerLine = {};
for (const line of orderLines) {
if (!line.get_quantity() || !line.price) {
continue;
}
const product_id = line.get_product().id;
remainingAmountPerLine[line.cid] = line.get_price_without_tax();
if (
applicableProducts.has(product_id) ||
(line.reward_product_id && applicableProducts.has(line.reward_product_id))
) {
linesToDiscount.push(line);
} else if (line.reward_id) {
const lineReward = this.pos.reward_by_id[line.reward_id];
if (lineReward.id === reward.id ||
(
orderLines.some(product =>
lineReward.all_discount_product_ids.has(product.get_product().id) &&
applicableProducts.has(product.get_product().id)
) &&
lineReward.reward_type === 'discount' &&
lineReward.discount_mode != 'percent'
)
) {
linesToDiscount.push(line);
}
if (!discountLinesPerReward[line.reward_identifier_code]) {
discountLinesPerReward[line.reward_identifier_code] = [];
}
discountLinesPerReward[line.reward_identifier_code].push(line);
}
}
let discountable = 0;
const discountablePerTax = {};
for (const line of linesToDiscount) {
discountable += remainingAmountPerLine[line.cid];
const taxKey = line.get_taxes().map((t) => t.id);
if (!discountablePerTax[taxKey]) {
discountablePerTax[taxKey] = 0;
}
discountablePerTax[taxKey] +=
remainingAmountPerLine[line.cid];
}
return { discountable, discountablePerTax };
},
/**
* Override reward line values creation to handle discount calculation without tax for all rewards
*/
_getRewardLineValuesDiscount(args) {
const reward = args["reward"];
const coupon_id = args["coupon_id"];
// For all rewards, we calculate discounts without tax
const rewardAppliesTo = reward.discount_applicability;
let getDiscountable;
if (rewardAppliesTo === "order") {
getDiscountable = this._getDiscountableOnOrder.bind(this);
} else if (rewardAppliesTo === "cheapest") {
getDiscountable = this._getDiscountableOnCheapest.bind(this);
} else if (rewardAppliesTo === "specific") {
getDiscountable = this._getDiscountableOnSpecific.bind(this);
}
if (!getDiscountable) {
return "Unknown discount type";
}
let { discountable, discountablePerTax } = getDiscountable(reward);
// For all rewards, we should use total without tax for comparison
const totalForComparison = this.get_total_without_tax();
discountable = Math.min(totalForComparison, discountable);
if (!discountable) {
return [];
}
let maxDiscount = reward.discount_max_amount || Infinity;
if (reward.discount_mode === "per_point") {
const points = (["ewallet", "gift_card"].includes(reward.program_id.program_type)) ?
this._getRealCouponPoints(coupon_id) :
Math.floor(this._getRealCouponPoints(coupon_id) / reward.required_points) * reward.required_points;
maxDiscount = Math.min(
maxDiscount,
roundPrecision(reward.discount * points, this.pos.currency.rounding)
);
} else if (reward.discount_mode === "per_order") {
maxDiscount = Math.min(maxDiscount, reward.discount);
} else if (reward.discount_mode === "percent") {
maxDiscount = Math.min(maxDiscount, roundPrecision(discountable * (reward.discount / 100), this.pos.currency.rounding));
}
const rewardCode = Math.random().toString(36).substring(3);
let pointCost = reward.clear_wallet
? this._getRealCouponPoints(coupon_id)
: reward.required_points;
if (reward.discount_mode === "per_point" && !reward.clear_wallet) {
pointCost = roundPrecision(Math.min(maxDiscount, discountable) / reward.discount, this.pos.currency.rounding);
}
// Apply rounding to pointCost if it's calculated from division
if (pointCost && typeof pointCost === 'number') {
pointCost = roundPrecision(pointCost, this.pos.currency.rounding);
}
// For all rewards, we calculate discount on price without tax
// Calculate the total discountable amount without tax
let totalDiscountableWithoutTax = 0;
for (const [, amount] of Object.entries(discountablePerTax)) {
totalDiscountableWithoutTax += amount;
}
// Calculate discount factor based on the total discountable amount without tax
const discountFactor = totalDiscountableWithoutTax ? roundPrecision(Math.min(1, maxDiscount / totalDiscountableWithoutTax), this.pos.currency.rounding) : 1;
const result = Object.entries(discountablePerTax).reduce((lst, entry) => {
if (!entry[1]) {
return lst;
}
const taxIds = entry[0] === "" ? [] : entry[0].split(",").map((str) => parseInt(str));
// Calculate tax-exclusive discount value for display
const discountAmount = roundPrecision(entry[1] * discountFactor, this.pos.currency.rounding);
lst.push({
product: reward.discount_line_product_id,
price: -discountAmount,
quantity: 1,
reward_id: reward.id,
is_reward_line: true,
coupon_id: coupon_id,
points_cost: 0,
reward_identifier_code: rewardCode,
tax_ids: taxIds,
merge: false,
});
return lst;
}, []);
if (result.length) {
result[0]["points_cost"] = pointCost;
}
return result;
}
});
/** @odoo-module **/
import { Order } from "@point_of_sale/app/store/models";
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(Order.prototype, {
/**
* Override to calculate discountable amount without tax for all reward types
*/
_getDiscountableOnOrder(reward) {
// For all rewards, we calculate discounts without tax
let discountable = 0;
const discountablePerTax = {};
for (const line of this.get_orderlines()) {
if (!line.get_quantity()) {
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)
? line.get_taxes().map((t) => t.id)
: line.get_taxes().filter((t) => t.amount_type !== 'fixed').map((t) => t.id);
discountable += line_total_without_tax;
if (!discountablePerTax[taxKey]) {
discountablePerTax[taxKey] = 0;
}
discountablePerTax[taxKey] += line_total_without_tax;
}
return { discountable, discountablePerTax };
},
/**
* Override to calculate cheapest line discountable without tax for all reward types
*/
_getDiscountableOnCheapest(reward) {
// For all rewards, we calculate discounts without tax
const cheapestLine = this._getCheapestLine();
if (!cheapestLine) {
return { discountable: 0, discountablePerTax: {} };
}
// Use price without tax for discount calculation
const discountableWithoutTax = cheapestLine.get_price_without_tax();
const taxKey = cheapestLine.get_taxes().map((t) => t.id);
return {
discountable: discountableWithoutTax,
discountablePerTax: Object.fromEntries([[taxKey, discountableWithoutTax]]),
};
},
/**
* Override to calculate specific product discountable without tax for all reward types
*/
_getDiscountableOnSpecific(reward) {
// For all rewards, we calculate discounts without tax
const applicableProducts = reward.all_discount_product_ids;
const linesToDiscount = [];
const discountLinesPerReward = {};
const orderLines = this.get_orderlines();
const remainingAmountPerLine = {};
for (const line of orderLines) {
if (!line.get_quantity() || !line.price) {
continue;
}
const product_id = line.get_product().id;
remainingAmountPerLine[line.cid] = line.get_price_without_tax();
if (
applicableProducts.has(product_id) ||
(line.reward_product_id && applicableProducts.has(line.reward_product_id))
) {
linesToDiscount.push(line);
} else if (line.reward_id) {
const lineReward = this.pos.reward_by_id[line.reward_id];
if (lineReward.id === reward.id ||
(
orderLines.some(product =>
lineReward.all_discount_product_ids.has(product.get_product().id) &&
applicableProducts.has(product.get_product().id)
) &&
lineReward.reward_type === 'discount' &&
lineReward.discount_mode != 'percent'
)
) {
linesToDiscount.push(line);
}
if (!discountLinesPerReward[line.reward_identifier_code]) {
discountLinesPerReward[line.reward_identifier_code] = [];
}
discountLinesPerReward[line.reward_identifier_code].push(line);
}
}
let discountable = 0;
const discountablePerTax = {};
for (const line of linesToDiscount) {
discountable += remainingAmountPerLine[line.cid];
const taxKey = line.get_taxes().map((t) => t.id);
if (!discountablePerTax[taxKey]) {
discountablePerTax[taxKey] = 0;
}
discountablePerTax[taxKey] +=
remainingAmountPerLine[line.cid];
}
return { discountable, discountablePerTax };
},
/**
* Override reward line values creation to handle discount calculation without tax for all rewards
*/
_getRewardLineValuesDiscount(args) {
const reward = args["reward"];
const coupon_id = args["coupon_id"];
// For all rewards, we calculate discounts without tax
const rewardAppliesTo = reward.discount_applicability;
let getDiscountable;
if (rewardAppliesTo === "order") {
getDiscountable = this._getDiscountableOnOrder.bind(this);
} else if (rewardAppliesTo === "cheapest") {
getDiscountable = this._getDiscountableOnCheapest.bind(this);
} else if (rewardAppliesTo === "specific") {
getDiscountable = this._getDiscountableOnSpecific.bind(this);
}
if (!getDiscountable) {
return "Unknown discount type";
}
let { discountable, discountablePerTax } = getDiscountable(reward);
// For all rewards, we should use total without tax for comparison
const totalForComparison = this.get_total_without_tax();
discountable = Math.min(totalForComparison, discountable);
if (!discountable) {
return [];
}
let maxDiscount = reward.discount_max_amount || Infinity;
if (reward.discount_mode === "per_point") {
const points = (["ewallet", "gift_card"].includes(reward.program_id.program_type)) ?
this._getRealCouponPoints(coupon_id) :
Math.floor(this._getRealCouponPoints(coupon_id) / reward.required_points) * reward.required_points;
maxDiscount = Math.min(
maxDiscount,
roundPrecision(reward.discount * points, this.pos.currency.rounding)
);
} else if (reward.discount_mode === "per_order") {
maxDiscount = Math.min(maxDiscount, reward.discount);
} else if (reward.discount_mode === "percent") {
maxDiscount = Math.min(maxDiscount, roundPrecision(discountable * (reward.discount / 100), this.pos.currency.rounding));
}
const rewardCode = Math.random().toString(36).substring(3);
let pointCost = reward.clear_wallet
? this._getRealCouponPoints(coupon_id)
: reward.required_points;
if (reward.discount_mode === "per_point" && !reward.clear_wallet) {
pointCost = roundPrecision(Math.min(maxDiscount, discountable) / reward.discount, this.pos.currency.rounding);
}
// Apply rounding to pointCost if it's calculated from division
if (pointCost && typeof pointCost === 'number') {
pointCost = roundPrecision(pointCost, this.pos.currency.rounding);
}
// For all rewards, we calculate discount on price without tax
// Calculate the total discountable amount without tax
let totalDiscountableWithoutTax = 0;
for (const [, amount] of Object.entries(discountablePerTax)) {
totalDiscountableWithoutTax += amount;
}
// Calculate discount factor based on the total discountable amount without tax
const discountFactor = totalDiscountableWithoutTax ? roundPrecision(Math.min(1, maxDiscount / totalDiscountableWithoutTax), this.pos.currency.rounding) : 1;
const result = Object.entries(discountablePerTax).reduce((lst, entry) => {
if (!entry[1]) {
return lst;
}
const taxIds = entry[0] === "" ? [] : entry[0].split(",").map((str) => parseInt(str));
// Calculate tax-exclusive discount value for display
const discountAmount = roundPrecision(entry[1] * discountFactor, this.pos.currency.rounding);
lst.push({
product: reward.discount_line_product_id,
price: -discountAmount,
quantity: 1,
reward_id: reward.id,
is_reward_line: true,
coupon_id: coupon_id,
points_cost: 0,
reward_identifier_code: rewardCode,
tax_ids: taxIds,
merge: false,
});
return lst;
}, []);
if (result.length) {
result[0]["points_cost"] = pointCost;
}
return result;
}
});