[FIX] sale_loyalty,pos_loyalty: fix discountable on specific products
Prior to this commit the discountable amount on specific products was not computed correctly when using fixed price discounts as it did not take the taxes into account. They will now be taken into account and result in the correct discount. TaskId-2834863 closes odoo/odoo#91202 X-original-commit: 669f4cca94af0c022318f6cb508c1c197aae0846 Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com> Signed-off-by: William Braeckman (wbr) <wbr@odoo.com>
This commit is contained in:
@@ -976,14 +976,18 @@ const PosLoyaltyOrder = (Order) => class PosLoyaltyOrder extends Order {
|
||||
}
|
||||
} else {
|
||||
const nonCommonLines = discountedLines.filter((line) => !linesToDiscount.includes(line));
|
||||
let discountedAmount = Math.abs(lines.reduce((sum, line) => sum + line.get_price_with_tax(), 0));
|
||||
const discountedAmounts = lines.reduce((map, line) => {
|
||||
map[line.get_taxes().map((t) => t.id)];
|
||||
return map;
|
||||
}, {});
|
||||
const process = (line) => {
|
||||
if (!discountedAmount || line.reward_id) {
|
||||
const key = line.get_taxes().map((t) => t.id);
|
||||
if (!discountedAmounts[key] || line.reward_id) {
|
||||
return;
|
||||
}
|
||||
const remaining = remainingAmountPerLine[line.cid];
|
||||
const consumed = Math.min(remaining, discountedAmount);
|
||||
discountedAmount -= consumed;
|
||||
const consumed = Math.min(remaining, discountedAmounts[key]);
|
||||
discountedAmounts[key] -= consumed;
|
||||
remainingAmountPerLine[line.cid] -= consumed;
|
||||
}
|
||||
nonCommonLines.forEach(process);
|
||||
@@ -1071,6 +1075,10 @@ const PosLoyaltyOrder = (Order) => class PosLoyaltyOrder extends Order {
|
||||
}
|
||||
const discountFactor = discountable ? Math.min(1, (maxDiscount / discountable)) : 1;
|
||||
const result = Object.entries(discountablePerTax).reduce((lst, entry) => {
|
||||
// Ignore 0 price lines
|
||||
if (!entry[1]) {
|
||||
return;
|
||||
}
|
||||
const taxIds = entry[0] === '' ? [] : entry[0].split(',').map((str) => parseInt(str));
|
||||
lst.push({
|
||||
product: discountProduct,
|
||||
|
||||
@@ -241,16 +241,21 @@ class SaleOrder(models.Model):
|
||||
common_lines = discounted_lines & lines_to_discount
|
||||
if line_reward.discount_mode == 'percent':
|
||||
for line in discounted_lines:
|
||||
remaining_amount_per_line[line] *= (1 - line_reward.discount/100)
|
||||
if line_reward.discount_applicability == 'cheapest':
|
||||
remaining_amount_per_line[line] *= (1 - line_reward.discount / 100 / line.product_uom_qty)
|
||||
else:
|
||||
remaining_amount_per_line[line] *= (1 - line_reward.discount / 100)
|
||||
else:
|
||||
non_common_lines = discounted_lines - lines_to_discount
|
||||
discounted_amount = abs(sum(lines.mapped('price_total')))
|
||||
# Fixed prices are per tax
|
||||
discounted_amounts = {line.tax_id: abs(line.price_total) for line in lines}
|
||||
for line in itertools.chain(non_common_lines, common_lines):
|
||||
discounted_amount = discounted_amounts[line.tax_id]
|
||||
if discounted_amount == 0:
|
||||
break
|
||||
continue
|
||||
remaining = remaining_amount_per_line[line]
|
||||
consumed = min(remaining, discounted_amount)
|
||||
discounted_amount -= consumed
|
||||
discounted_amounts[line.tax_id] -= consumed
|
||||
remaining_amount_per_line[line] -= consumed
|
||||
|
||||
discountable = 0
|
||||
@@ -331,7 +336,7 @@ class SaleOrder(models.Model):
|
||||
'points_cost': 0,
|
||||
'reward_identifier_code': reward_code,
|
||||
'tax_id': [(Command.CLEAR, 0, 0)] + [(Command.LINK, tax.id, False) for tax in mapped_taxes[tax]]
|
||||
} for tax, price in discountable_per_tax.items()}
|
||||
} for tax, price in discountable_per_tax.items() if price}
|
||||
# We only assign the point cost to one line to avoid counting the cost multiple times
|
||||
if reward_dict:
|
||||
reward_dict[next(iter(reward_dict))]['points_cost'] = point_cost
|
||||
|
||||
@@ -1461,3 +1461,103 @@ class TestSaleCouponProgramNumbers(TestSaleCouponCommon):
|
||||
self.assertEqual(order.amount_total, 18, "The total should be 9$.")
|
||||
self._auto_rewards(order, programs)
|
||||
self.assertEqual(order.amount_total, 9, "The total should be 9$.")
|
||||
|
||||
def test_specific_discount_multiple_taxes(self):
|
||||
# Check the following setup
|
||||
# Product A 10$ 10% tva excl
|
||||
# Product B 10$ 20% tva excl
|
||||
# Program A -100% on product A
|
||||
# Program B -5$ fixed on both products
|
||||
# Applying both programs in a different order should result in a different
|
||||
# outcome since discountable amounts are computed per tax
|
||||
# Applying program A before B should yield a better final price
|
||||
tax_10pc_excl, tax_20pc_excl = self.env['account.tax'].create([
|
||||
{
|
||||
'name': "10% Tax excl",
|
||||
'amount_type': 'percent',
|
||||
'amount': 10,
|
||||
'type_tax_use': 'sale',
|
||||
},
|
||||
{
|
||||
'name': "20% Tax excl",
|
||||
'amount_type': 'percent',
|
||||
'amount': 20,
|
||||
'type_tax_use': 'sale',
|
||||
},
|
||||
])
|
||||
product_a, product_b = self.env['product.product'].create([
|
||||
{
|
||||
'name': 'Product A',
|
||||
'list_price': 10,
|
||||
'sale_ok': True,
|
||||
'taxes_id': [(6, 0, [tax_10pc_excl.id])],
|
||||
},
|
||||
{
|
||||
'name': 'Product B',
|
||||
'list_price': 10,
|
||||
'sale_ok': True,
|
||||
'taxes_id': [(6, 0, [tax_20pc_excl.id])],
|
||||
},
|
||||
])
|
||||
program_a, program_b = self.env['loyalty.program'].create([
|
||||
{
|
||||
'name': '-100% on A',
|
||||
'trigger': 'auto',
|
||||
'program_type': 'promotion',
|
||||
'applies_on': 'current',
|
||||
'rule_ids': [(0, 0, {
|
||||
})],
|
||||
'reward_ids': [(0, 0, {
|
||||
'reward_type': 'discount',
|
||||
'discount': 100,
|
||||
'discount_mode': 'percent',
|
||||
'discount_applicability': 'specific',
|
||||
'discount_product_ids': product_a,
|
||||
'required_points': 1,
|
||||
})],
|
||||
},
|
||||
{
|
||||
'name': '-5 USD on [A, B]',
|
||||
'trigger': 'auto',
|
||||
'program_type': 'promotion',
|
||||
'applies_on': 'current',
|
||||
'rule_ids': [(0, 0, {
|
||||
})],
|
||||
'reward_ids': [(0, 0, {
|
||||
'reward_type': 'discount',
|
||||
'discount': 5,
|
||||
'discount_mode': 'per_point',
|
||||
'discount_applicability': 'specific',
|
||||
'discount_product_ids': product_a | product_b,
|
||||
'required_points': 1,
|
||||
})],
|
||||
},
|
||||
])
|
||||
|
||||
order = self.empty_order
|
||||
self.env['sale.order.line'].create([
|
||||
{
|
||||
'product_id': product_a.id,
|
||||
'name': 'Product A',
|
||||
'product_uom_qty': 1,
|
||||
'order_id': order.id,
|
||||
},
|
||||
{
|
||||
'product_id': product_b.id,
|
||||
'name': 'Product B',
|
||||
'product_uom_qty': 1,
|
||||
'order_id': order.id,
|
||||
},
|
||||
])
|
||||
self._auto_rewards(order, program_a)
|
||||
self.assertEqual(order.amount_total, 12, 'Total should be 12$')
|
||||
self._auto_rewards(order, program_b)
|
||||
self.assertAlmostEqual(order.amount_total, 7, 0, 'Total should be 7$')
|
||||
# Now the order way around
|
||||
order.order_line.filtered('reward_id').unlink()
|
||||
self._auto_rewards(order, program_b)
|
||||
self.assertAlmostEqual(order.amount_total, 18, 0, 'Total should be 18$')
|
||||
self._auto_rewards(order, program_a)
|
||||
# We essentially create a discount of -100% off of an already discounted product
|
||||
# (11 - 2.4) = 8.6$ discount ~
|
||||
self.assertAlmostEqual(order.amount_total, 9.4, 1, 'Total should be 9.4$')
|
||||
|
||||
Reference in New Issue
Block a user