19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:32:12 +01:00
parent 79f83631d5
commit 73afc09215
6267 changed files with 1534193 additions and 1130106 deletions

View file

@ -1,13 +1,14 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_buy_gift_card
from . import test_loyalty
from . import test_loyalty_history
from . import test_pay_with_gift_card
from . import test_program_multi_company
from . import test_program_numbers
from . import test_program_rules
from . import test_program_with_code_operations
from . import test_program_without_code_operations
from . import test_sale_auto_invoice
from . import test_sale_invoicing
from . import test_unlink_reward

View file

@ -1,86 +1,75 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import defaultdict
from odoo import Command
from odoo.exceptions import ValidationError
from odoo.fields import Command
from odoo.addons.sale.tests.test_sale_product_attribute_value_config import TestSaleProductAttributeValueCommon
from odoo.addons.sale.tests.common import SaleCommon
class TestSaleCouponCommon(TestSaleProductAttributeValueCommon):
class TestSaleCouponCommon(SaleCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
def setUpClass(cls):
super().setUpClass()
# set currency to not rely on demo data and avoid possible race condition
cls.currency_ratio = 1.0
pricelist = cls.env.ref('product.list0')
pricelist.currency_id = cls._setup_currency(cls.currency_ratio)
# Disable noisy pricelist (aka demo data Benelux)
cls.env.user.partner_id.write({
'property_product_pricelist': pricelist.id,
})
(cls.env['product.pricelist'].search([]) - pricelist).write({'active': False})
# Set all the existing programs to active=False to avoid interference
cls.env['loyalty.program'].search([]).sudo().write({'active': False})
# create partner for sale order.
cls.steve = cls.env['res.partner'].create({
'name': 'Steve Bucknor',
'email': 'steve.bucknor@example.com',
})
cls.empty_order = cls.env['sale.order'].create({
'partner_id': cls.steve.id
})
cls.uom_unit = cls.env.ref('uom.product_uom_unit')
# Taxes
tax_group_group = cls.env['account.tax.group'].create({
'name': 'Test Account Tax Group'
})
cls.tax_15pc_excl = cls.env['account.tax'].create({
'name': "Tax 15%",
'amount_type': 'percent',
'amount': 15,
'type_tax_use': 'sale',
'tax_group_id': tax_group_group.id,
})
cls.tax_10pc_incl = cls.env['account.tax'].create({
'name': "10% Tax incl",
'amount_type': 'percent',
'amount': 10,
'price_include': True,
'price_include_override': 'tax_included',
'tax_group_id': tax_group_group.id,
})
cls.tax_10pc_base_incl = cls.env['account.tax'].create({
'name': "10% Tax incl base amount",
'amount_type': 'percent',
'amount': 10,
'price_include': True,
'price_include_override': 'tax_included',
'include_base_amount': True,
'tax_group_id': tax_group_group.id,
})
cls.tax_10pc_excl = cls.env['account.tax'].create({
'name': "10% Tax excl",
'amount_type': 'percent',
'amount': 10,
'price_include': False,
'price_include_override': 'tax_excluded',
'tax_group_id': tax_group_group.id,
})
cls.tax_20pc_excl = cls.env['account.tax'].create({
'name': "20% Tax excl",
'amount_type': 'percent',
'amount': 20,
'price_include': False,
'price_include_override': 'tax_excluded',
'tax_group_id': tax_group_group.id,
})
cls.tax_group = cls.env['account.tax'].create({
'name': "tax_group",
'amount_type': 'group',
'children_tax_ids': [Command.set((cls.tax_10pc_incl + cls.tax_10pc_base_incl).ids)],
'tax_group_id': tax_group_group.id,
})
#products
@ -114,7 +103,7 @@ class TestSaleCouponCommon(TestSaleProductAttributeValueCommon):
cls.product_gift_card = cls.env['product.product'].create({
'name': 'Gift Card 50',
'detailed_type': 'service',
'type': 'service',
'list_price': 50,
'sale_ok': True,
'taxes_id': False,
@ -223,6 +212,8 @@ class TestSaleCouponCommon(TestSaleProductAttributeValueCommon):
status = order._apply_program_reward(rewards, coupons)
if 'error' in status:
raise ValidationError(status['error'])
elif len(coupons) == 1 and len(rewards) > 1:
return rewards
def _claim_reward(self, order, program, coupon=False):
if len(program.reward_ids) != 1:
@ -243,6 +234,12 @@ class TestSaleCouponCommon(TestSaleProductAttributeValueCommon):
continue
self._claim_reward(order, program, coupons_per_program[program])
def _generate_coupons(self, loyality_program, coupon_qty=1):
self.env['loyalty.generate.wizard'].with_context(active_id=loyality_program.id).create({
'coupon_qty': coupon_qty,
}).generate_coupons()
return loyality_program.coupon_ids
class TestSaleCouponNumbersCommon(TestSaleCouponCommon):
@classmethod
def setUpClass(cls):
@ -274,14 +271,6 @@ class TestSaleCouponNumbersCommon(TestSaleCouponCommon):
'taxes_id': False,
})
cls.steve = cls.env['res.partner'].create({
'name': 'Steve Bucknor',
'email': 'steve.bucknor@example.com',
})
cls.empty_order = cls.env['sale.order'].create({
'partner_id': cls.steve.id
})
cls.p1 = cls.env['loyalty.program'].create({
'name': 'Code for 10% on orders',
'trigger': 'with_code',

View file

@ -1,10 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import Command
from odoo.tests.common import tagged
from odoo.addons.sale_loyalty.tests.common import TestSaleCouponCommon
from odoo.tests.common import tagged
@tagged('-at_install', 'post_install')
class TestBuyGiftCard(TestSaleCouponCommon):
@ -16,13 +16,11 @@ class TestBuyGiftCard(TestSaleCouponCommon):
(0, False, {
'product_id': self.product_A.id,
'name': 'Ordinary Product A',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
}),
(0, False, {
'product_id': self.product_gift_card.id,
'name': 'Gift Card Product',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})

View file

@ -1,11 +1,15 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import Command
from odoo.tests import tagged, new_test_user
from freezegun import freeze_time
from odoo.exceptions import ValidationError
from odoo.fields import Command
from odoo.tests import new_test_user, tagged
from odoo.tools.float_utils import float_compare
from odoo.addons.sale_loyalty.tests.common import TestSaleCouponCommon
@tagged('post_install', '-at_install')
class TestLoyalty(TestSaleCouponCommon):
@ -14,14 +18,18 @@ class TestLoyalty(TestSaleCouponCommon):
super().setUpClass()
cls.env['loyalty.program'].search([]).write({'active': False})
cls.partner_a = cls.env['res.partner'].create({'name': 'Jean Jacques'})
cls.product_a = cls.env['product.product'].create({
'name': 'Product C',
'list_price': 100,
'sale_ok': True,
'taxes_id': [(6, 0, [])],
})
cls.product_a, cls.product_b = cls.env['product.product'].create([
{
'name': 'Product C',
'list_price': 100,
'sale_ok': True,
'taxes_id': [Command.set([])],
},
{
'name': "Product B",
'sale_ok': True,
}
])
cls.ewallet_program = cls.env['loyalty.program'].create({
'name': 'eWallet Program',
@ -43,7 +51,7 @@ class TestLoyalty(TestSaleCouponCommon):
cls.ewallet = cls.env['loyalty.card'].create({
'program_id': cls.ewallet_program.id,
'partner_id': cls.partner_a.id,
'partner_id': cls.partner.id,
'points': 10,
})
cls.ewallet_program.coupon_ids = [Command.set([cls.ewallet.id])]
@ -88,16 +96,14 @@ class TestLoyalty(TestSaleCouponCommon):
})],
})
order = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
})
order = self.empty_order
order._update_programs_and_rewards()
claimable_rewards = order._get_claimable_rewards()
# Should be empty since we do not have any coupon created yet
self.assertFalse(claimable_rewards, "No program should be applicable")
loyalty_card = self.env['loyalty.card'].create({
'program_id': loyalty_program.id,
'partner_id': self.partner_a.id,
'partner_id': self.partner.id,
'points': 10,
})
self.ewallet.points = 0
@ -142,7 +148,7 @@ class TestLoyalty(TestSaleCouponCommon):
})
order = self.env['sale.order'].with_user(self.user_salemanager).create({
'partner_id': self.partner_a.id,
'partner_id': self.partner.id,
'order_line': [
(0, 0, {
'product_id': self.product_a.id,
@ -216,20 +222,20 @@ class TestLoyalty(TestSaleCouponCommon):
coupon_partner, _ = self.env['loyalty.card'].create([
{
'program_id': coupon_program.id,
'partner_id': self.partner_a.id,
'partner_id': self.partner.id,
'points': 1,
'code': '5555',
},
{
'program_id': ewallet_program.id,
'partner_id': self.partner_a.id,
'partner_id': self.partner.id,
'points': 115,
},
])
# Create the order
order = self.env['sale.order'].with_user(self.user_salemanager).create({
'partner_id': self.partner_a.id,
'partner_id': self.partner.id,
'order_line': [
Command.create({
'product_id': product_a.id,
@ -282,7 +288,7 @@ class TestLoyalty(TestSaleCouponCommon):
})
order = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
'partner_id': self.partner.id,
'order_line': [Command.create({'product_id': product_a.id})],
})
self.assertEqual(order.reward_amount, 0)
@ -338,16 +344,16 @@ class TestLoyalty(TestSaleCouponCommon):
loyalty_card = self.env['loyalty.card'].create({
'program_id': loyalty_program.id,
'partner_id': self.partner_a.id,
'partner_id': self.partner.id,
'points': 0,
})
order = self.env['sale.order'].with_user(self.user_salemanager).create({
'partner_id': self.partner_a.id,
'partner_id': self.partner.id,
'order_line': [
Command.create({
'product_id': self.product_A.id,
'tax_id': False,
'tax_ids': False,
}),
]
})
@ -376,6 +382,61 @@ class TestLoyalty(TestSaleCouponCommon):
order.action_confirm()
self.assertEqual(loyalty_card.points, 90)
def test_multiple_rewards_after_confirm(self):
"""
Check that multiple rewards from a loyalty promotion program are correctly applied to a SO
after its confirmation by asserting that:
- Both rewards are applied to the order lines.
- The total points cost matches the rule's requirement.
- The coupon's points are fully consumed after applying the rewards.
"""
promo_program = self.env['loyalty.program'].create({
'name': 'Multiple Rewards Promotion',
'program_type': 'promotion',
'applies_on': 'current',
'company_id': self.env.company.id,
'trigger': 'auto',
'rule_ids': [
Command.create({
'product_ids': self.product_A,
'reward_point_amount': 1,
'reward_point_mode': 'order',
'minimum_qty': 1,
}),
],
'reward_ids': [
Command.create({
'discount': 10,
'discount_applicability': 'specific',
'discount_product_ids': [self.product_A.id],
'required_points': 0.5,
}),
Command.create({
'discount': 15,
'discount_applicability': 'specific',
'discount_product_ids': [self.product_B.id],
'required_points': 0.5,
}),
],
})
order = self.empty_order
order.order_line = [
Command.create({'product_id': self.product_A.id, 'product_uom_qty': 1}),
Command.create({'product_id': self.product_B.id, 'product_uom_qty': 1}),
]
order.action_confirm()
order._update_programs_and_rewards()
coupon = order.coupon_point_ids.coupon_id.filtered(lambda c: c.program_id == promo_program)
reward1, reward2 = rewards = promo_program.reward_ids
order._apply_program_reward(reward1, coupon)
order._apply_program_reward(reward2, coupon)
self.assertEqual(order.order_line.reward_id, rewards, "All rewards should be applied")
self.assertEqual(sum(order.order_line.mapped('points_cost')), 1)
self.assertEqual(coupon.points, 0)
def test_points_awarded_discount_code_no_domain_program(self):
"""
Check the calculation for points awarded when there is a discount coupon applied and the
@ -385,16 +446,16 @@ class TestLoyalty(TestSaleCouponCommon):
loyalty_program = LoyaltyProgram.create(LoyaltyProgram._get_template_values()['loyalty'])
loyalty_card = self.env['loyalty.card'].create({
'program_id': loyalty_program.id,
'partner_id': self.partner_a.id,
'partner_id': self.partner.id,
'points': 0,
})
order = self.env['sale.order'].with_user(self.user_salemanager).create({
'partner_id': self.partner_a.id,
'partner_id': self.partner.id,
'order_line': [
Command.create({
'product_id': self.product_A.id,
'tax_id': False,
'tax_ids': False,
}),
]
})
@ -412,10 +473,8 @@ class TestLoyalty(TestSaleCouponCommon):
related to that discount is not in the domain of the loyalty program.
Expected behavior: The discount is not included in the computation of points
"""
product_category_base = self.env.ref('product.product_category_1')
product_category_food = self.env['product.category'].create({
'name': "Food",
'parent_id': product_category_base.id
})
self.product_A.categ_id = product_category_food
@ -426,20 +485,20 @@ class TestLoyalty(TestSaleCouponCommon):
loyalty_program.rule_ids.product_category_id = product_category_food.id
loyalty_card = self.env['loyalty.card'].create({
'program_id': loyalty_program.id,
'partner_id': self.partner_a.id,
'partner_id': self.partner.id,
'points': 0,
})
order = self.env['sale.order'].with_user(self.user_salemanager).create({
'partner_id': self.partner_a.id,
'partner_id': self.partner.id,
'order_line': [
Command.create({
'product_id': self.product_A.id,
'tax_id': False,
'tax_ids': False,
}),
Command.create({
'product_id': self.product_B.id,
'tax_id': False,
'tax_ids': False,
}),
]
})
@ -458,10 +517,8 @@ class TestLoyalty(TestSaleCouponCommon):
domain of the loyalty program.
Expected behavior: The discount is included in the computation of points
"""
product_category_base = self.env.ref('product.product_category_1')
product_category_food = self.env['product.category'].create({
'name': "Food",
'parent_id': product_category_base.id
})
self.product_A.categ_id = product_category_food
@ -472,7 +529,7 @@ class TestLoyalty(TestSaleCouponCommon):
loyalty_program.rule_ids.product_category_id = product_category_food.id
loyalty_card = self.env['loyalty.card'].create({
'program_id': loyalty_program.id,
'partner_id': self.partner_a.id,
'partner_id': self.partner.id,
'points': 0,
})
@ -484,15 +541,15 @@ class TestLoyalty(TestSaleCouponCommon):
discount_product.categ_id = product_category_food.id
order = self.env['sale.order'].with_user(self.user_salemanager).create({
'partner_id': self.partner_a.id,
'partner_id': self.partner.id,
'order_line': [
Command.create({
'product_id': self.product_A.id,
'tax_id': False,
'tax_ids': False,
}),
Command.create({
'product_id': self.product_B.id,
'tax_id': False,
'tax_ids': False,
}),
]
})
@ -511,15 +568,15 @@ class TestLoyalty(TestSaleCouponCommon):
loyalty_program = LoyaltyProgram.create(LoyaltyProgram._get_template_values()['loyalty'])
loyalty_card = self.env['loyalty.card'].create({
'program_id': loyalty_program.id,
'partner_id': self.partner_a.id,
'partner_id': self.partner.id,
'points': 0,
})
order = self.env['sale.order'].with_user(self.user_salemanager).create({
'partner_id': self.partner_a.id,
'partner_id': self.partner.id,
'order_line': [
Command.create({
'product_id': self.product_A.id,
'tax_id': False,
'tax_ids': False,
}),
]
})
@ -539,7 +596,7 @@ class TestLoyalty(TestSaleCouponCommon):
loyalty_program = LoyaltyProgram.create(LoyaltyProgram._get_template_values()['loyalty'])
loyalty_card = self.env['loyalty.card'].create({
'program_id': loyalty_program.id,
'partner_id': self.partner_a.id,
'partner_id': self.partner.id,
'points': 0,
})
@ -563,11 +620,11 @@ class TestLoyalty(TestSaleCouponCommon):
gift_card = program_gift_card.coupon_ids[0]
order = self.env['sale.order'].with_user(self.user_salemanager).create({
'partner_id': self.partner_a.id,
'partner_id': self.partner.id,
'order_line': [
Command.create({
'product_id': self.product_A.id,
'tax_id': False,
'tax_ids': False,
}),
]
})
@ -608,7 +665,7 @@ class TestLoyalty(TestSaleCouponCommon):
}])
order = self.env['sale.order'].with_user(self.user_salemanager).create({
'partner_id': self.partner_a.id,
'partner_id': self.partner.id,
'order_line': [Command.create({
'product_id': product_A.id,
'product_uom_qty': 3,
@ -629,6 +686,133 @@ class TestLoyalty(TestSaleCouponCommon):
self._claim_reward(order, coupon_program)
self.assertEqual(float_compare(order.amount_total, 218.7, precision_rounding=3), 0, "300 * 0.9 * 0.9 * 0.9 = 218.7")
def test_promotion_program_restricted_to_pricelists(self):
self.env['product.pricelist'].search([]).action_archive()
company_currency = self.env.company.currency_id
pricelist_1, pricelist_2 = self.env['product.pricelist'].create([
{'name': 'Basic company_currency pricelist', 'currency_id': company_currency.id},
{'name': 'Other company_currency pricelist', 'currency_id': company_currency.id},
])
self.immediate_promotion_program.active = True
order = self.empty_order.copy()
order.write({'order_line': [
(0, False, {
'product_id': self.product_A.id,
'name': '1 Product A',
'product_uom_qty': 1.0,
}),
(0, False, {
'product_id': self.product_B.id,
'name': '2 Product B',
'product_uom_qty': 1.0,
}),
]})
applied_message = "The promo offer should have been applied."
not_applied_message = "The promo offer should not have been applied because the order's " \
"pricelist is not eligible to this promotion."
order.pricelist_id = self.env['product.pricelist']
order._update_programs_and_rewards()
self._claim_reward(order, self.immediate_promotion_program)
self.assertEqual(len(order.order_line.ids), 3, applied_message)
order.pricelist_id = pricelist_1
order._update_programs_and_rewards()
self._claim_reward(order, self.immediate_promotion_program)
self.assertEqual(len(order.order_line.ids), 3, applied_message)
self.immediate_promotion_program.pricelist_ids = [pricelist_1.id]
order.pricelist_id = self.env['product.pricelist']
order._update_programs_and_rewards()
self._claim_reward(order, self.immediate_promotion_program)
self.assertEqual(len(order.order_line.ids), 2, not_applied_message)
order.pricelist_id = pricelist_1
order._update_programs_and_rewards()
self._claim_reward(order, self.immediate_promotion_program)
self.assertEqual(len(order.order_line.ids), 3, applied_message)
order.pricelist_id = pricelist_2
order._update_programs_and_rewards()
self._claim_reward(order, self.immediate_promotion_program)
self.assertEqual(len(order.order_line.ids), 2, not_applied_message)
self.immediate_promotion_program.pricelist_ids = [pricelist_1.id, pricelist_2.id]
order.pricelist_id = self.env['product.pricelist']
order._update_programs_and_rewards()
self._claim_reward(order, self.immediate_promotion_program)
self.assertEqual(len(order.order_line.ids), 2, not_applied_message)
order.pricelist_id = pricelist_1
order._update_programs_and_rewards()
self._claim_reward(order, self.immediate_promotion_program)
self.assertEqual(len(order.order_line.ids), 3, applied_message)
def test_coupon_program_restricted_to_pricelists(self):
self.env['product.pricelist'].search([]).action_archive()
company_currency = self.env.company.currency_id
pricelist_1, pricelist_2 = self.env['product.pricelist'].create([
{'name': 'Basic company_currency pricelist', 'currency_id': company_currency.id},
{'name': 'Other company_currency pricelist', 'currency_id': company_currency.id},
])
self.code_promotion_program.active = True
self.env['loyalty.generate.wizard'].with_context(
active_id=self.code_promotion_program.id
).create({'coupon_qty': 7, 'points_granted': 1}).generate_coupons()
coupons = self.code_promotion_program.coupon_ids
order_no_pricelist = self.empty_order.copy()
order_no_pricelist.write({'pricelist_id': None, 'order_line': [
(0, False, {
'product_id': self.product_A.id,
'name': '1 Product A',
'product_uom_qty': 1.0,
}),
]})
order_pricelist_1 = order_no_pricelist.copy()
order_pricelist_1.pricelist_id = pricelist_1
order_pricelist_2 = order_no_pricelist.copy()
order_pricelist_2.pricelist_id = pricelist_2
applied_message = "The coupon code should have been applied."
not_applied_message = "The coupon code should not have been applied because the order's " \
"pricelist is not eligible to this promotion."
order_0 = order_no_pricelist.copy()
self._apply_promo_code(order_0, coupons[0].code)
self.assertEqual(len(order_0.order_line.ids), 2, applied_message)
order_1 = order_pricelist_1.copy()
self._apply_promo_code(order_1, coupons[1].code)
self.assertEqual(len(order_1.order_line.ids), 2, applied_message)
self.code_promotion_program.pricelist_ids = [pricelist_1.id]
order_2 = order_no_pricelist.copy()
with self.assertRaises(ValidationError):
self._apply_promo_code(order_2, coupons[2].code)
self.assertEqual(len(order_2.order_line.ids), 1, not_applied_message)
order_3 = order_pricelist_1.copy()
self._apply_promo_code(order_3, coupons[3].code)
self.assertEqual(len(order_3.order_line.ids), 2, applied_message)
order_4 = order_pricelist_2.copy()
with self.assertRaises(ValidationError):
self._apply_promo_code(order_4, coupons[4].code)
self.assertEqual(len(order_4.order_line.ids), 1, not_applied_message)
self.code_promotion_program.pricelist_ids = [pricelist_1.id, pricelist_2.id]
order_5 = order_no_pricelist.copy()
with self.assertRaises(ValidationError):
self._apply_promo_code(order_5, coupons[5].code)
self.assertEqual(len(order_5.order_line.ids), 1, not_applied_message)
order_6 = order_pricelist_1.copy()
self._apply_promo_code(order_6, coupons[6].code)
self.assertEqual(len(order_6.order_line.ids), 2, applied_message)
def test_specific_promotion_on_free_product(self):
product_A = self.env['product.product'].create({
@ -657,7 +841,7 @@ class TestLoyalty(TestSaleCouponCommon):
}])
order = self.env['sale.order'].with_user(self.user_salemanager).create({
'partner_id': self.partner_a.id,
'partner_id': self.partner.id,
'order_line': [
Command.create({
'product_id': product_A.id,
@ -693,7 +877,7 @@ class TestLoyalty(TestSaleCouponCommon):
}])
order = self.env['sale.order'].with_user(self.user_salemanager).create({
'partner_id': self.partner_a.id,
'partner_id': self.partner.id,
'order_line': [
Command.create({
'product_id': product_A.id,
@ -706,6 +890,24 @@ class TestLoyalty(TestSaleCouponCommon):
self.assertEqual(giftcard_program.coupon_count, 0)
def test_ewallet_code_use_restriction(self):
self.env['loyalty.generate.wizard'].with_context(active_id=self.ewallet_program.id).create({
'coupon_qty': 1,
'points_granted': 100,
}).generate_coupons()
order = self.env['sale.order'].with_user(self.user_salemanager).create({
'partner_id': self.partner.id,
'order_line': [
Command.create({
'product_id': self.product_a.id,
}),
],
})
with self.assertRaises(ValidationError):
self._apply_promo_code(order, self.ewallet_program.coupon_ids[0].code)
def test_100_percent_discount(self):
"""
Check whether a program offering 100% discount on an order reduces the order's total amount
@ -734,10 +936,10 @@ class TestLoyalty(TestSaleCouponCommon):
})],
}])
self.env['loyalty.card'].create({
'program_id': loyalty_program.id, 'partner_id': self.partner_a.id, 'points': 2
'program_id': loyalty_program.id, 'partner_id': self.partner.id, 'points': 2
})
order = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
'partner_id': self.partner.id,
'order_line': [(0, 0, {
'product_id': self.product_A.id, 'product_uom_qty': 1, 'price_unit': price
}) for price in (5.60, 8.92, 44.91, 217.26, 2400.00)],
@ -771,9 +973,9 @@ class TestLoyalty(TestSaleCouponCommon):
'required_points': 1,
})],
}])
self.env['loyalty.card'].create({'program_id': loyalty_program.id, 'partner_id': self.partner_a.id, 'points': 2})
self.env['loyalty.card'].create({'program_id': loyalty_program.id, 'partner_id': self.partner.id, 'points': 2})
order = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
'partner_id': self.partner.id,
'order_line': [(0, 0, {'product_id': self.product_D.id, 'product_uom_qty': 1})],
})
@ -787,7 +989,7 @@ class TestLoyalty(TestSaleCouponCommon):
self.ewallet.points = 1000
order = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
'partner_id': self.partner.id,
'order_line': [Command.create({
'product_id': self.product_a.id,
'points_cost': 100,
@ -804,7 +1006,7 @@ class TestLoyalty(TestSaleCouponCommon):
self.ewallet.points = 10
order = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
'partner_id': self.partner.id,
'order_line': [Command.create({
'product_id': self.product_a.id,
'points_cost': 100,
@ -820,6 +1022,47 @@ class TestLoyalty(TestSaleCouponCommon):
self.assertEqual(self.ewallet.points, 50)
def test_discount_reward_claimable_only_once(self):
"""
Check that discount rewards already applied won't be shown in the claimable rewards anymore.
"""
program = self.env['loyalty.program'].create({
'name': "10% Discount & Gift",
'applies_on': 'current',
'trigger': 'with_code',
'program_type': 'promotion',
'rule_ids': [Command.create({'mode': 'with_code', 'code': "10PERCENT&GIFT"})],
'reward_ids': [
Command.create({
'reward_type': 'product',
'reward_product_id': self.product_B.id,
'reward_product_qty': 1,
}),
Command.create({
'reward_type': 'discount',
'discount': 10,
'discount_mode': 'percent',
'discount_applicability': 'specific',
}),
],
})
coupon = self.env['loyalty.card'].create({
'program_id': program.id, 'points': 20, 'code': 'GIFT_CARD'
})
order = self.env['sale.order'].create({
'partner_id': self.partner.id,
'order_line': [Command.create({'product_id': self.product_a.id})]
})
product_reward = program.reward_ids.filtered(lambda reward: reward.reward_type == 'product')
discount_reward = program.reward_ids - product_reward
order._apply_program_reward(discount_reward, coupon)
rewards = order._get_claimable_rewards()[coupon]
msg = "Only the free product should be applicable, as the discount was already applied."
self.assertEqual(rewards, product_reward, msg)
def test_archived_reward_products(self):
"""
Check that we do not use loyalty rewards that have no active reward product.
@ -855,7 +1098,7 @@ class TestLoyalty(TestSaleCouponCommon):
product_c.active = False
order = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
'partner_id': self.partner.id,
'order_line': [
Command.create({
'product_id': self.product_a.id,
@ -873,43 +1116,206 @@ class TestLoyalty(TestSaleCouponCommon):
rewards = [value.ids for value in order._get_claimable_rewards().values()]
self.assertTrue(any(loyalty_program_tag.reward_ids[0].id in r for r in rewards))
def test_discount_reward_claimable_only_once(self):
def test_domain_on_cheapest_reward(self):
product_tag = self.env['product.tag'].create({'name': "Discountable"})
self.env['loyalty.program'].create({
'name': "10% Discount",
'program_type': 'promo_code',
'rule_ids': [Command.create({'code': "10discount"})],
'reward_ids': [
Command.create({
'reward_type': 'discount',
'discount': 10,
'discount_mode': 'percent',
'discount_applicability': 'cheapest',
'discount_product_tag_id': product_tag.id,
}),
],
})
self.product_A.product_tag_ids = product_tag
order = self.empty_order
order.write({
'order_line':[
# product_A: lst_price: 100, Tax included price: 115
Command.create({'product_id': self.product_A.id}),
# Product_B: lst_price: 5, Tax included price: 5.75
Command.create({'product_id': self.product_B.id}),
]
})
self._apply_promo_code(order, '10discount')
msg = "Discount should only be applied to the line with a correctly tagged product."
self.assertEqual(order.order_line[2].price_total, -11.5, msg)
self.product_C.write({
'list_price': 50,
'product_tag_ids': product_tag,
})
order.order_line[2:].unlink()
order.write({
'order_line':[
# product_C: lst_price = Tax included price: 50
Command.create({'product_id': self.product_C.id}),
]
})
self._apply_promo_code(order, '10discount')
msg = "Discount should be applied to the line with the cheapest valid product."
self.assertEqual(order.order_line[3].price_total, -5.0, msg)
def test_sol_free_product_description_equals_reward_description(self):
"""
Check that discount rewards already applied won't be shown in the claimable rewards anymore.
Ensure that if a "Free Product" reward is added to a sale order,
its line description matches the reward description.
"""
program = self.env['loyalty.program'].create({
'name': '10% Discount & Gift',
'applies_on': 'current',
'trigger': 'with_code',
loyalty_program = self.env['loyalty.program'].create(
self.env['loyalty.program']._get_template_values()['buy_x_get_y']
)
reward = loyalty_program.reward_ids[0]
updated_description = f"{reward.description} Adding manual description"
reward.description = updated_description
order = self.empty_order
order.write({
'order_line': [
Command.create({
'product_id': reward.reward_product_id.id,
'name': '1 Product',
'product_uom_qty': 4.0,
}),
]
})
order._update_programs_and_rewards()
self._claim_reward(order, loyalty_program)
self.assertEqual(len(order.order_line.ids), 2)
self.assertEqual(order.order_line[1].name, updated_description)
def test_archiving_loyalty_card_unlinks_draft_points_from_sale_order(self):
"""
When a loyalty card has points accrued from a draft sale order, archiving the
card should unlink those draft points so they are no longer claimable on that order
"""
loyalty_program = self.env['loyalty.program'].create({
'name': 'Loyalty Program',
'program_type': 'loyalty',
'trigger': 'auto',
'applies_on': 'both',
'rule_ids': [
Command.create({
'reward_point_mode': 'unit',
'reward_point_amount': 100,
'product_ids': [self.product_a.id],
}),
],
'reward_ids': [
Command.create({
'reward_type': 'discount',
'discount': 50,
'discount_mode': 'percent',
'discount_applicability': 'order',
'required_points': 10,
}),
],
})
loyalty_card = self.env['loyalty.card'].create({
'program_id': loyalty_program.id,
'partner_id': self.partner.id,
'points': 0,
})
sale_order = self.empty_order
sale_order.write({
'order_line': [
Command.create({
'product_id': self.product_a.id,
}),
]
})
sale_order._update_programs_and_rewards()
claimable_rewards = sale_order._get_claimable_rewards()
self.assertTrue(claimable_rewards[loyalty_card])
loyalty_card.action_archive()
claimable_rewards = sale_order._get_claimable_rewards()
self.assertFalse(claimable_rewards.get(loyalty_card))
def test_free_product_sol_is_zero_price(self):
self.env['res.config.settings'].create({
'group_discount_per_so_line': True,
}).execute()
loyalty_program = self.env['loyalty.program'].create({
'name': 'Loyalty Program',
'program_type': 'promotion',
'rule_ids': [Command.create({'mode': 'with_code', 'code': '10PERCENT&GIFT'})],
'trigger': 'auto',
'applies_on': 'both',
'rule_ids': [
Command.create({
'reward_point_mode': 'unit',
'reward_point_amount': 1,
'product_ids': [self.product_a.id],
}),
],
'reward_ids': [
Command.create({
'reward_type': 'product',
'reward_product_id': self.product_B.id,
'reward_product_qty': 1,
}),
Command.create({
'reward_type': 'discount',
'discount': 10,
'discount_mode': 'percent',
'discount_applicability': 'specific',
'required_points': 1,
}),
],
})
coupon = self.env['loyalty.card'].create({
'program_id': program.id, 'points': 20, 'code': 'GIFT_CARD'
sale_order = self.empty_order
sale_order.write({
'order_line': [
Command.create({
'product_id': self.product_a.id,
'product_uom_qty': 1,
}),
],
})
sale_order._update_programs_and_rewards()
self._claim_reward(sale_order, loyalty_program)
# In real use case, so.plan_id is set to False in _verify_cart_after_update in
# sale_subscription module. Since discount depends on so.plan_id, this triggers
# a recomputation of the discount.
# Here we manually call the compute method to simulate the behavior
sale_order.order_line._compute_discount()
reward_line = sale_order.order_line.filtered('reward_id')
self.assertEqual(reward_line.discount, 100)
self.assertEqual(reward_line.price_total, 0)
order = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
'order_line': [Command.create({'product_id': self.product_a.id})]
def test_reapplying_reward_keeps_reward_price_unit(self):
"""
Ensure that re-applying a reward doesn't reset the existing reward line unit price to zero
"""
self.immediate_promotion_program.active = True
sale_order = self.empty_order
sale_order.write({
'order_line': [
Command.create({
'product_id': self.product_A.id,
'product_uom_qty': 1,
}),
],
})
sale_order._update_programs_and_rewards()
self._claim_reward(sale_order, self.immediate_promotion_program)
reward_line = sale_order.order_line.filtered('reward_id')
reward_line_price_unit = reward_line.price_unit
sale_order._update_programs_and_rewards()
self._claim_reward(sale_order, self.immediate_promotion_program)
self.assertEqual(reward_line.price_unit, reward_line_price_unit)
product_reward = program.reward_ids.filtered(lambda reward: reward.reward_type == 'product')
discount_reward = program.reward_ids - product_reward
order._apply_program_reward(discount_reward, coupon)
rewards = order._get_claimable_rewards()[coupon]
msg = "Only the free product should be applicable, as the discount was already applied."
self.assertEqual(rewards, product_reward, msg)
@freeze_time("2026-01-10")
def test_expired_ewallet_is_not_claimable(self):
self.ewallet.expiration_date = '2026-01-01'
sale_order = self.empty_order
sale_order.write({
'partner_id': self.partner.id,
'order_line': [
Command.create({
'product_id': self.product_a.id,
}),
],
})
sale_order.action_open_reward_wizard()
sale_order._update_programs_and_rewards()
claimable_rewards = sale_order._get_claimable_rewards()
self.assertFalse(claimable_rewards.get(self.ewallet))

View file

@ -0,0 +1,129 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.fields import Command
from odoo.tests import tagged
from odoo.addons.sale_loyalty.tests.common import TestSaleCouponCommon
@tagged('post_install', '-at_install')
class TestLoyaltyhistory(TestSaleCouponCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.partner_a = cls.env['res.partner'].create({'name': 'Jean Jacques'})
cls.loyalty_program = cls.env['loyalty.program'].create({
'name': 'Full Discount',
'program_type': 'loyalty',
'trigger': 'auto',
'applies_on': 'both',
'rule_ids': [Command.create({
'reward_point_mode': 'unit',
'reward_point_amount': 1,
'product_ids': [cls.product_A.id],
})],
'reward_ids': [
Command.create({
'reward_type': 'discount',
'discount': 10,
'discount_mode': 'percent',
'discount_applicability': 'order',
'required_points': 1,
}),
Command.create({
'active': False,
'reward_type': 'product',
'reward_product_id': cls.product_B.id,
'required_points': 2,
}),
],
})
cls.loyalty_card = cls.env['loyalty.card'].create({
'program_id': cls.loyalty_program.id, 'partner_id': cls.partner_a.id, 'points': 2
})
def test_add_loyalty_history_line_with_reward(self):
order = self.empty_order
order.write({
'order_line': [
Command.create({
'product_id': self.product_A.id,
'name': 'Ordinary Product A',
'product_uom_qty': 1.0,
}),
],
})
order._update_programs_and_rewards()
self._auto_rewards(order, self.immediate_promotion_program)
order.action_confirm()
coupon_applied = self.immediate_promotion_program.coupon_ids.filtered(lambda x: x.order_id == order)
history_records = len(coupon_applied.history_ids.filtered(lambda history: history.order_id == order.id))
self.assertEqual(history_records, 1, "A history line should be created on confirmation of order")
def test_add_loyalty_history_line_without_reward(self):
order = self.empty_order
order.write({
'partner_id': self.partner_a.id,
'order_line': [
Command.create({
'product_id': self.product_A.id,
'tax_ids': False,
}),
]
})
order.action_confirm()
order._update_programs_and_rewards()
self._claim_reward(order, self.loyalty_program)
history_records = self.loyalty_card.history_ids.filtered(lambda history: history.order_id == order.id)
self.assertEqual(history_records.used, 1.0,
"The history line should be updated on change of order lines in a confirmed order")
def test_delete_loyalty_history_line_on_cancel(self):
order = self.empty_order
order.write({
'partner_id': self.partner_a.id,
'order_line': [
Command.create({
'product_id': self.product_A.id,
'tax_ids': False,
}),
]
})
order._update_programs_and_rewards()
self._claim_reward(order, self.loyalty_program)
order.action_confirm()
lines_before_cancel = len(self.loyalty_card.history_ids)
order._action_cancel()
self.assertEqual(lines_before_cancel - 1, len(self.loyalty_card.history_ids),
"History line should be deleted after order cancel")
def test_loyalty_history_multi_reward(self):
"""Verify that applying multiple rewards sums up the total points cost."""
self.loyalty_card.points = initial_points = 4
self.loyalty_program.with_context(active_test=False).reward_ids.active = True
order = self.empty_order
order.write({
'partner_id': self.partner_a.id,
'order_line': [
Command.create({
'product_id': self.product_A.id,
'tax_ids': False,
}),
],
})
for reward in self.loyalty_program.reward_ids:
order._apply_program_reward(reward, self.loyalty_card)
self.assertEqual(len(order.order_line.filtered('reward_id')), 2)
self.assertEqual(order.order_line.mapped('points_cost'), [0, 1, 2])
order.action_confirm()
loyalty_history = self.loyalty_card.history_ids
self.assertEqual(loyalty_history.issued, 1, "1 point should be rewarded")
self.assertEqual(loyalty_history.used, 3, "A total of 3 points should be used")
self.assertEqual(
self.loyalty_card.points,
initial_points + loyalty_history.issued - loyalty_history.used,
"Loyalty points should equal initial points + points issued - points used",
)

View file

@ -20,7 +20,6 @@ class TestPayWithGiftCard(TestSaleCouponCommon):
Command.create({
'product_id': self.product_A.id,
'name': 'Ordinary Product A',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
@ -41,7 +40,6 @@ class TestPayWithGiftCard(TestSaleCouponCommon):
Command.create({
'product_id': self.product_B.id,
'name': 'Ordinary Product b',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
@ -62,7 +60,6 @@ class TestPayWithGiftCard(TestSaleCouponCommon):
Command.create({
'product_id': self.product_A.id,
'name': 'Ordinary Product A',
'product_uom': self.uom_unit.id,
'product_uom_qty': 20.0,
})
]})
@ -83,7 +80,6 @@ class TestPayWithGiftCard(TestSaleCouponCommon):
Command.create({
'product_id': self.product_C.id,
'name': 'Ordinary Product C',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
@ -124,7 +120,6 @@ class TestPayWithGiftCard(TestSaleCouponCommon):
Command.create({
'product_id': self.product_C.id,
'name': 'Ordinary Product C',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
@ -180,7 +175,6 @@ class TestPayWithGiftCard(TestSaleCouponCommon):
Command.create({
'product_id': self.product_B.id,
'name': 'Ordinary Product b',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
'price_unit': 200.0,
})
@ -205,8 +199,8 @@ class TestPayWithGiftCard(TestSaleCouponCommon):
gift_card_line = order.order_line - sol
self.assertAlmostEqual(gift_card_line.price_total, -100.0)
self.assertAlmostEqual(order.amount_total, before_gift_card_payment - 100.0)
self.assertTrue(all(line.tax_id for line in order.order_line))
self.assertEqual(order.order_line.tax_id, self.tax_15pc_excl)
self.assertTrue(all(line.tax_ids for line in order.order_line))
self.assertEqual(order.order_line.tax_ids, self.tax_15pc_excl)
# TAX INCL
gift_card_line.unlink() # Remove gift card
@ -217,8 +211,8 @@ class TestPayWithGiftCard(TestSaleCouponCommon):
gift_card_line = order.order_line - sol
self.assertAlmostEqual(gift_card_line.price_total, -100.0)
self.assertAlmostEqual(order.amount_total, before_gift_card_payment - 100.0)
self.assertTrue(all(line.tax_id for line in order.order_line))
self.assertEqual(gift_card_line.tax_id, self.tax_10pc_incl)
self.assertTrue(all(line.tax_ids for line in order.order_line))
self.assertEqual(gift_card_line.tax_ids, self.tax_10pc_incl)
# TAX INCL + TAX EXCL
gift_card_line.unlink() # Remove gift card
@ -229,8 +223,8 @@ class TestPayWithGiftCard(TestSaleCouponCommon):
gift_card_line = order.order_line - sol
self.assertAlmostEqual(gift_card_line.price_total, -100.0)
self.assertAlmostEqual(order.amount_total, before_gift_card_payment - 100.0)
self.assertTrue(all(line.tax_id for line in order.order_line))
self.assertEqual(gift_card_line.tax_id, self.tax_10pc_incl + self.tax_15pc_excl)
self.assertTrue(all(line.tax_ids for line in order.order_line))
self.assertEqual(gift_card_line.tax_ids, self.tax_10pc_incl + self.tax_15pc_excl)
def test_paying_with_gift_card_fixed_tax(self):
""" Test payment of sale order with fixed tax using gift card """
@ -253,7 +247,6 @@ class TestPayWithGiftCard(TestSaleCouponCommon):
Command.create({
'product_id': self.product_A.id,
'name': "Ordinary Product A",
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})

View file

@ -1,10 +1,9 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.sale_loyalty.tests.common import TestSaleCouponCommon
from odoo.exceptions import UserError
from odoo.fields import Command
from odoo.tests import tagged
from odoo import Command
from odoo.addons.sale_loyalty.tests.common import TestSaleCouponCommon
@tagged('post_install', '-at_install')
@ -45,13 +44,11 @@ class TestSaleCouponMultiCompany(TestSaleCouponCommon):
(0, False, {
'product_id': self.product_A.id,
'name': '1 Product A',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
}),
(0, False, {
'product_id': self.product_B.id,
'name': '2 Product B',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
@ -68,13 +65,11 @@ class TestSaleCouponMultiCompany(TestSaleCouponCommon):
(0, False, {
'product_id': self.product_A.id,
'name': '1 Product A',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
}),
(0, False, {
'product_id': self.product_B.id,
'name': '2 Product B',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
@ -95,20 +90,48 @@ class TestSaleCouponMultiCompany(TestSaleCouponCommon):
Command.create({
'product_id': self.product_A.id,
'name': '1 Product A',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
}),
Command.create({
'product_id': self.product_B.id,
'name': '2 Product B',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
],
'company_id': branch_a.id,
'partner_id': self.steve.id
'partner_id': self.partner.id
}
)
order._update_programs_and_rewards()
self.assertIn(self.immediate_promotion_program, order._get_applied_programs())
def test_applicable_programs_confirm_on_branch(self):
# create a branch
self.env['loyalty.program'].search([]).write({'active': False})
branch_a = self.env['res.company'].create(
{'name': 'Branch A', 'parent_id': self.company_a.id}
)
LoyaltyProgram = self.env['loyalty.program']
LoyaltyProgram.create(LoyaltyProgram._get_template_values()['loyalty'])
self.sale_user.write({'company_ids': [Command.set((branch_a + self.company_a).ids)]})
# create an order
order = self.empty_order
order.update(
{
'order_line': [
Command.create({
'product_id': self.product_A.id,
}),
],
'company_id': branch_a.id,
'partner_id': self.partner.id,
'user_id': self.sale_user.id
}
)
order.with_user(self.sale_user).with_company(branch_a.id).sudo(False).action_confirm()
self.assertEqual(order.state, 'sale')

View file

@ -1,12 +1,12 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.sale_loyalty.tests.common import TestSaleCouponNumbersCommon
from odoo.exceptions import ValidationError
from odoo.fields import Command
from odoo.tests import tagged
from odoo.tools.float_utils import float_compare
from odoo.addons.sale_loyalty.tests.common import TestSaleCouponNumbersCommon
@tagged('post_install', '-at_install')
class TestSaleCouponProgramNumbers(TestSaleCouponNumbersCommon):
@ -93,7 +93,7 @@ class TestSaleCouponProgramNumbers(TestSaleCouponNumbersCommon):
'name': "15% Tax",
'amount_type': 'percent',
'amount': 15,
'price_include': True,
'price_include_override': 'tax_included',
})
p_specific_product = self.env['loyalty.program'].create({
'name': '20% reduction on Large Cabinet in cart',
@ -126,10 +126,9 @@ class TestSaleCouponProgramNumbers(TestSaleCouponNumbersCommon):
self._auto_rewards(order, self.all_programs)
self.assertEqual(len(order.order_line.ids), 1, "We should not get the reduction line since we dont have 320$ tax excluded (cabinet is 320$ tax included)")
sol1.tax_id.price_include = False
sol1._compute_tax_id()
sol1.tax_ids.price_include_override = 'tax_excluded'
sol1._compute_tax_ids()
self.env.flush_all()
self.env['account.tax'].invalidate_model(['price_include'])
self._auto_rewards(order, self.all_programs)
self.assertEqual(len(order.order_line.ids), 2, "We should now get the reduction line since we have 320$ tax included (cabinet is 320$ tax included)")
# Name | Qty | price_unit | Tax | HTVA | TVAC | TVA |
@ -217,7 +216,7 @@ class TestSaleCouponProgramNumbers(TestSaleCouponNumbersCommon):
'name': "35% Tax incl",
'amount_type': 'percent',
'amount': 35,
'price_include': True,
'price_include_override': 'tax_included',
})
# Set tax and prices on products as neeed for the test
@ -330,8 +329,10 @@ class TestSaleCouponProgramNumbers(TestSaleCouponNumbersCommon):
# --------------------------------------------------------------------------------
# TOTAL | 2576.77 | 2946.11 | 369.34
self.assertAlmostEqual(order.amount_total, 1901.11, 2, "The order total with programs should be 1901.11")
self.assertEqual(order.amount_untaxed, 1594.95, "The order untaxed total without any programs should be 2576.77")
self.assertRecordValues(order, [{
'amount_total': 1901.11,
'amount_untaxed': 1594.95,
}])
self.assertEqual(len(order.order_line.ids), 5, "The order without any programs should have 5 lines")
# Apply all the programs
@ -345,8 +346,10 @@ class TestSaleCouponProgramNumbers(TestSaleCouponNumbersCommon):
# --------------------------------------------------------------------------------
# TOTAL AFTER APPLYING FREE PRODUCT PROGRAMS | 1594.95 | 1901.11 | 306.16
self.assertAlmostEqual(order.amount_total, 1901.11, 2, "The order total with programs should be 1901.11")
self.assertEqual(order.amount_untaxed, 1594.95, "The order untaxed total with programs should be 1594.95")
self.assertRecordValues(order, [{
'amount_total': 1901.11,
'amount_untaxed': 1594.95,
}])
self.assertEqual(len(order.order_line.ids), 8, "Order should contains 5 regular product lines and 3 free product lines")
# Apply 10% on top of everything
@ -362,18 +365,24 @@ class TestSaleCouponProgramNumbers(TestSaleCouponNumbersCommon):
# --------------------------------------------------------------------------------
# TOTAL AFTER APPLYING 10% GLOBAL PROGRAM | 1435.46 | 1711.00 | 275.54
self.assertEqual(order.amount_total, 1711, "The order total with programs should be 1711")
self.assertEqual(order.amount_untaxed, 1435.46, "The order untaxed total with programs should be 1435.46")
self.assertRecordValues(order, [{
'amount_total': 1711.0,
'amount_untaxed': 1435.45,
}])
self.assertEqual(len(order.order_line.ids), 12, "Order should contains 5 regular product lines, 3 free product lines and 4 discount lines (one for every tax)")
# -- This is a test inside the test
order.order_line._compute_tax_id()
self.assertEqual(order.amount_total, 1711, "Recomputing tax on sale order lines should not change total amount")
self.assertEqual(order.amount_untaxed, 1435.46, "Recomputing tax on sale order lines should not change untaxed amount")
order.order_line._compute_tax_ids()
self.assertRecordValues(order, [{
'amount_total': 1711.0,
'amount_untaxed': 1435.45,
}])
self.assertEqual(len(order.order_line.ids), 12, "Recomputing tax on sale order lines should not change number of order line")
self._auto_rewards(order, self.all_programs)
self.assertEqual(order.amount_total, 1711, "Recomputing tax on sale order lines should not change total amount")
self.assertEqual(order.amount_untaxed, 1435.46, "Recomputing tax on sale order lines should not change untaxed amount")
self.assertRecordValues(order, [{
'amount_total': 1711.0,
'amount_untaxed': 1435.45,
}])
self.assertEqual(len(order.order_line.ids), 12, "Recomputing tax on sale order lines should not change number of order line")
# -- End test inside the test
@ -402,8 +411,10 @@ class TestSaleCouponProgramNumbers(TestSaleCouponNumbersCommon):
# --------------------------------------------------------------------------------
# TOTAL AFTER APPLYING 20% ON LARGE CABINET | 1363.46 | 1628.2 | 264.74
self.assertEqual(order.amount_total, 1628.2, "The order total with programs should be 1628.2")
self.assertEqual(order.amount_untaxed, 1363.46, "The order untaxed total with programs should be 1363.45")
self.assertRecordValues(order, [{
'amount_total': 1628.2,
'amount_untaxed': 1363.45,
}])
self.assertEqual(len(order.order_line.ids), 13, "Order should have a new discount line for 20% on Large Cabinet")
# Check that if you delete one of the discount tax line, the others tax lines from the same promotion got deleted as well.
@ -425,7 +436,7 @@ class TestSaleCouponProgramNumbers(TestSaleCouponNumbersCommon):
# Name | Qty | price_unit | Tax | HTVA | TVAC | TVA |
# --------------------------------------------------------------------------------
# Large Cabinet | 4 | 100.00 | 15% excl | 400.00 | 460.00 | 60.00
# Conference Chair | 4 | 100.00 | 10% incl | 363.64 | 400.00 | 36.36
# Conference Chair | 4 | 100.00 | 10% incl | 363.63 | 400.00 | 36.36
# Pedal Bins | 5 | 100.00 | / | 500.00 | 500.00 | /
# Drawer Black | 2 | 100.00 | 15% excl | 200.00 | 230.00 | 30.00
# Product A | 3 | 100.00 | 35% incl | 222.22 | 411.11 | 188.89
@ -440,9 +451,9 @@ class TestSaleCouponProgramNumbers(TestSaleCouponNumbersCommon):
# 10% on tax 35+50% | 1 | -30.00 | 35% incl | -22.22 | -41.11 | -18.89
# 50% excl
# --------------------------------------------------------------------------------
# TOTAL | 1445.28 | 1718.20 | 272.92
# TOTAL | 1445.27 | 1718.20 | 272.92
self.assertEqual(order.amount_untaxed, 1445.28, "The order should have one more paid Conference Chair with 10% incl tax and discounted by 10%")
self.assertEqual(order.amount_untaxed, 1445.27, "The order should have one more paid Conference Chair with 10% incl tax and discounted by 10%")
# Check that if you remove a product, his reward lines got removed, especially the discount per tax one
sol2.unlink()
@ -464,8 +475,10 @@ class TestSaleCouponProgramNumbers(TestSaleCouponNumbersCommon):
# --------------------------------------------------------------------------------
# TOTAL | 1118.00 | 1349.00 | 240.20
self.assertAlmostEqual(order.amount_total, 1358.2, 2, "The order total with programs should be 1358.20")
self.assertEqual(order.amount_untaxed, 1118, "The order untaxed total with programs should be 1118.00")
self.assertRecordValues(order, [{
'amount_total': 1358.2,
'amount_untaxed': 1118.0,
}])
self.assertEqual(len(order.order_line.ids), 10, "Order should contains 10 lines: 4 products lines, 2 free products lines and 4 discount lines")
def test_program_numbers_extras(self):
@ -519,7 +532,7 @@ class TestSaleCouponProgramNumbers(TestSaleCouponNumbersCommon):
'name': 'Drawer Black',
'product_uom_qty': 1.0,
'order_id': order.id,
'tax_id': [(4, self.tax_0pc_excl.id)]
'tax_ids': [(4, self.tax_0pc_excl.id)]
})
self._auto_rewards(order, self.all_programs)
self.assertEqual(order.amount_total, 0, "Total should be null. The fixed amount discount is higher than the SO total, it should be reduced to the SO total")
@ -577,6 +590,17 @@ class TestSaleCouponProgramNumbers(TestSaleCouponNumbersCommon):
generated_coupon = order._get_reward_coupons()
self.assertEqual(len(generated_coupon), 1, "We should still have only 1 coupon as we now benefit again from the program but no need to create a new one (see next assert)")
self.assertEqual(generated_coupon.points, 0, "The coupon should not have it's points already.")
self.assertFalse(order._get_claimable_rewards(), "No rewards should be claimable")
order.action_confirm()
self.assertEqual(
generated_coupon.points, 1,
"The coupon should have 1 point after confirmation",
)
self.assertFalse(
order._get_claimable_rewards(),
"Next-order coupon rewards shouldn't be claimable on current order",
)
def test_coupon_rule_minimum_amount(self):
""" Ensure coupon with minimum amount rule are correctly
@ -684,7 +708,7 @@ class TestSaleCouponProgramNumbers(TestSaleCouponNumbersCommon):
'product_uom_qty': 1.0,
'price_unit': 100.0,
'order_id': order.id,
'tax_id': [(6, 0, (self.tax_15pc_excl.id,))],
'tax_ids': [(6, 0, (self.tax_15pc_excl.id,))],
},
{
'product_id': self.pedalBin.id,
@ -692,7 +716,7 @@ class TestSaleCouponProgramNumbers(TestSaleCouponNumbersCommon):
'product_uom_qty': 1.0,
'price_unit': 100.0,
'order_id': order.id,
'tax_id': [(6, 0, [])],
'tax_ids': [(6, 0, [])],
},
{
'product_id': self.product_A.id,
@ -700,7 +724,7 @@ class TestSaleCouponProgramNumbers(TestSaleCouponNumbersCommon):
'product_uom_qty': 1.0,
'price_unit': 100.0,
'order_id': order.id,
'tax_id': [(6, 0, [])],
'tax_ids': [(6, 0, [])],
},
])
@ -729,13 +753,13 @@ class TestSaleCouponProgramNumbers(TestSaleCouponNumbersCommon):
self._auto_rewards(order, self.all_programs)
self._apply_promo_code(order, 'test_10pc')
self._auto_rewards(order, self.all_programs)
self.assertAlmostEqual(order.amount_tax, 1.13, 2)
self.assertEqual(order.amount_untaxed, 22.72)
self.assertAlmostEqual(order.amount_tax, 1.14, 2)
self.assertEqual(order.amount_untaxed, 22.71)
self.assertEqual(order.amount_total, 23.85, "The promotion program should not make the order total go below 0be altered after recomputation")
# It should stay the same after a recompute, order matters
self._auto_rewards(order, self.all_programs)
self.assertAlmostEqual(order.amount_tax, 1.13, 2)
self.assertEqual(order.amount_untaxed, 22.72)
self.assertAlmostEqual(order.amount_tax, 1.14, 2)
self.assertEqual(order.amount_untaxed, 22.71)
self.assertEqual(order.amount_total, 23.85, "The promotion program should not make the order total go below 0be altered after recomputation")
def test_coupon_and_coupon_discount_fixed_amount_tax_incl(self):
@ -771,7 +795,7 @@ class TestSaleCouponProgramNumbers(TestSaleCouponNumbersCommon):
'product_uom_qty': 1.0,
'price_unit': 100.0,
'order_id': order.id,
'tax_id': [(6, 0, (self.tax_10pc_incl.id,))],
'tax_ids': [(6, 0, (self.tax_10pc_incl.id,))],
},
{
'product_id': self.pedalBin.id,
@ -779,7 +803,7 @@ class TestSaleCouponProgramNumbers(TestSaleCouponNumbersCommon):
'product_uom_qty': 1.0,
'price_unit': 100.0,
'order_id': order.id,
'tax_id': [(6, 0, [])],
'tax_ids': [(6, 0, [])],
},
{
'product_id': self.product_A.id,
@ -787,7 +811,7 @@ class TestSaleCouponProgramNumbers(TestSaleCouponNumbersCommon):
'product_uom_qty': 1.0,
'price_unit': 100.0,
'order_id': order.id,
'tax_id': [(6, 0, [])],
'tax_ids': [(6, 0, [])],
},
])
@ -800,10 +824,10 @@ class TestSaleCouponProgramNumbers(TestSaleCouponNumbersCommon):
}).generate_coupons()
coupon = coupon_program.coupon_ids
self._apply_promo_code(order, coupon.code)
self.assertEqual(order.amount_total, 0.0, "The promotion program should not make the order total go below 0")
self.assertEqual(order.amount_total, 0, "The promotion program should not make the order total go below 0")
self.assertEqual(order.amount_tax, 0)
self._auto_rewards(order, self.all_programs)
self.assertEqual(order.amount_total, 0.0, "The promotion program should not be altered after recomputation")
self.assertEqual(order.amount_total, 0, "The promotion program should not be altered after recomputation")
self.assertEqual(order.amount_tax, 0)
order.order_line[3:].unlink() #remove all coupon
@ -895,9 +919,9 @@ class TestSaleCouponProgramNumbers(TestSaleCouponNumbersCommon):
'name': "30% Tax",
'amount_type': 'percent',
'amount': 30,
'price_include': True,
'price_include_override': 'tax_included',
})
sol2.tax_id = percent_tax
sol2.tax_ids = percent_tax
self._auto_rewards(order, self.all_programs)
self.assertEqual(len(order.order_line.ids), 4, "Conference Chair + Drawer Black + 20% on no TVA product (Conference Chair) + 20% on 15% tva product (Drawer Black)")
@ -908,9 +932,9 @@ class TestSaleCouponProgramNumbers(TestSaleCouponNumbersCommon):
# 25% discount | 1 | -16.50 | / | -16.50 | -16.50 | 0.00
# 25% discount | 1 | -12.50 | 30% incl | -9.62 | -12.50 | -2.88
# --------------------------------------------------------------------------------
# TOTAL | 78.34 | 87.00 | 8.66
# TOTAL | 78.35 | 87.00 | 8.66
self.assertEqual(order.amount_total, 87.00, "Total untaxed should be as per above comment")
self.assertEqual(order.amount_untaxed, 78.34, "Total with taxes should be as per above comment")
self.assertEqual(order.amount_untaxed, 78.35, "Total with taxes should be as per above comment")
def test_program_numbers_free_prod_with_min_amount_and_qty_on_same_prod(self):
# This test focus on giving a free product based on both
@ -1052,7 +1076,7 @@ class TestSaleCouponProgramNumbers(TestSaleCouponNumbersCommon):
'product_uom_qty': 14.0,
'price_unit': 118.0,
'order_id': order.id,
'tax_id': False,
'tax_ids': False,
})
self._auto_rewards(order, self.all_programs)
self.assertEqual(order.amount_total, 1486.80, "10% discount should be applied")
@ -1264,6 +1288,42 @@ class TestSaleCouponProgramNumbers(TestSaleCouponNumbersCommon):
self.assertEqual(order.amount_total, 190.0, 'The price must be 190.0 since there is now 2x 5$ discount and 2x Product F')
self.assertEqual(order.order_line.filtered(lambda x: x.is_reward_line).price_unit, -5, 'The discount unit price should still be -5 after the quantity was manually changed')
def test_program_multi_product_max_discount(self):
order = self.empty_order
coupon_program = self.env['loyalty.program'].create({
'name': "50% off for cheapest product(max $30)",
'trigger': 'with_code',
'program_type': 'coupons',
'reward_ids': [(0, 0, {
'reward_type': 'discount',
'discount': 50,
'discount_mode': 'percent',
'discount_applicability': 'cheapest',
'discount_max_amount': 30,
})],
})
# create SOL
self.env['sale.order.line'].create({
'product_id': self.largeCabinet.id,
'product_uom_qty': 2.0,
'order_id': order.id,
})
# generate and apply coupon
self.env['loyalty.generate.wizard'].with_context(active_id=coupon_program.id).create({
'coupon_qty': 1,
'points_granted': 1,
}).generate_coupons()
coupon = coupon_program.coupon_ids
self._apply_promo_code(order, coupon.code)
self.assertEqual(len(order.order_line), 2, "The order must contain 2 order lines")
self.assertEqual(
order.amount_total, 610.0, "The price must be 610.0 since the max discount is 30"
)
def test_specific_discount_product_group(self):
# Tests the following:
# 1 program: -5$ on [A, B]
@ -1475,26 +1535,26 @@ class TestSaleCouponProgramNumbers(TestSaleCouponNumbersCommon):
self.assertEqual(order.amount_total, 5, 'Price should be 10$ - 5$(discount) = 5$')
self.assertEqual(order.amount_tax, 0, 'No taxes are applied yet')
sol.tax_id = self.tax_10pc_base_incl
sol.tax_ids = self.tax_10pc_base_incl
self._auto_rewards(order, program)
self.assertEqual(order.amount_total, 5, 'Price should be 10$ - 5$(discount) = 5$')
self.assertEqual(float_compare(order.amount_tax, 5 / 11, precision_rounding=3), 0, '10% Tax included in 5$')
sol.tax_id = self.tax_10pc_excl
sol.tax_ids = self.tax_10pc_excl
self._auto_rewards(order, program)
# Value is 5.99 instead of 6 because you cannot have 6 with 10% tax excluded and a precision rounding of 2
self.assertAlmostEqual(order.amount_total, 6, 1, msg='Price should be 11$ - 5$(discount) = 6$')
self.assertEqual(float_compare(order.amount_tax, 6 / 11, precision_rounding=3), 0, '10% Tax included in 6$')
sol.tax_id = self.tax_20pc_excl
sol.tax_ids = self.tax_20pc_excl
self._auto_rewards(order, program)
self.assertEqual(order.amount_total, 7, 'Price should be 12$ - 5$(discount) = 7$')
self.assertEqual(float_compare(order.amount_tax, 7 / 12, precision_rounding=3), 0, '20% Tax included on 7$')
sol.tax_id = self.tax_10pc_base_incl + self.tax_10pc_excl
sol.tax_ids = self.tax_10pc_base_incl + self.tax_10pc_excl
self._auto_rewards(order, program)
self.assertAlmostEqual(order.amount_total, 6, 1, msg='Price should be 11$ - 5$(discount) = 6$')
@ -1537,21 +1597,21 @@ class TestSaleCouponProgramNumbers(TestSaleCouponNumbersCommon):
self.assertAlmostEqual(order.amount_total, 15, 1, msg='Price should be 20$ - 5$(discount) = 15$')
self.assertEqual(order.amount_tax, 0, 'No taxes are applied yet')
sol1.tax_id = self.tax_10pc_base_incl
sol1.tax_ids = self.tax_10pc_base_incl
self._auto_rewards(order, program)
self.assertAlmostEqual(order.amount_total, 15, 1, msg='Price should be 20$ - 5$(discount) = 15$')
self.assertEqual(float_compare(order.amount_tax, 5 / 11 + 0, precision_rounding=3), 0,
'10% Tax included in 5$ in sol1 (highest cost) and 0 in sol2')
sol2.tax_id = self.tax_10pc_excl
sol2.tax_ids = self.tax_10pc_excl
self._auto_rewards(order, program)
self.assertAlmostEqual(order.amount_total, 16, 1, msg='Price should be 21$ - 5$(discount) = 16$')
# Tax amount = 10% in 10$ + 10% in 11$ - 10% in 5$ (apply on excluded)
self.assertEqual(float_compare(order.amount_tax, 5 / 11, precision_rounding=3), 0)
sol2.tax_id = self.tax_10pc_base_incl + self.tax_10pc_excl
sol2.tax_ids = self.tax_10pc_base_incl + self.tax_10pc_excl
self._auto_rewards(order, program)
self.assertAlmostEqual(order.amount_total, 16, 1, msg='Price should be 21$ - 5$(discount) = 16$')
@ -1567,7 +1627,7 @@ class TestSaleCouponProgramNumbers(TestSaleCouponNumbersCommon):
'product_uom_qty': 1.0,
'order_id': order.id,
})
sol3.tax_id = self.tax_10pc_excl
sol3.tax_ids = self.tax_10pc_excl
self._auto_rewards(order, program)
self.assertAlmostEqual(order.amount_total, 27, 1, msg='Price should be 32$ - 5$(discount) = 27$')
@ -1801,6 +1861,100 @@ class TestSaleCouponProgramNumbers(TestSaleCouponNumbersCommon):
self._auto_rewards(order, loyalty_program)
self.assertEqual(len(order.order_line), 2, 'Promotion should add 1 line')
self.assertEqual(order.order_line[0].tax_id, tax_15pc_excl)
self.assertEqual(order.order_line[1].tax_id, tax_15pc_excl)
self.assertEqual(order.order_line[0].tax_ids, tax_15pc_excl)
self.assertEqual(order.order_line[1].tax_ids, tax_15pc_excl)
self.assertEqual(order.amount_total, 156.0, '140$ + 15% - 5$ = 156$')
def test_rounded_used_loyalty_points(self):
"""Check that the loyalty points used in a reward are rounded according to the currency."""
loyalty_program = self.env['loyalty.program'].create({
'name': 'Test loyalty card',
'program_type': 'loyalty',
'trigger': 'auto',
'applies_on': 'both',
'rule_ids': [Command.set([])],
'reward_ids': [Command.create({
'reward_type': 'discount',
'discount_mode': 'per_point',
'discount': 0.03,
'discount_applicability': 'order',
'required_points': 1,
})],
})
order = self.empty_order
self.env['loyalty.card'].create([{
'program_id': loyalty_program.id,
'partner_id': order.partner_id.id,
'points': 3030,
}])
product_a = self._create_product(
name='product_a',
lst_price=3000.0,
taxes_id=[Command.set([])],
)
order.order_line = [Command.create({'product_id': product_a.id})]
coupon = loyalty_program.coupon_ids[0]
order._apply_program_reward(loyalty_program.reward_ids[0], coupon)
order.action_confirm()
self.assertEqual(len(order.order_line), 2, 'Promotion should add 1 line')
used_points = coupon.history_ids[0].used
self.assertEqual(used_points, coupon.currency_id.round(used_points))
def test_apply_order_and_specific_discounts(self):
"""Ensure you can apply a full-order discount, and then a product-specific discount."""
order_program, specific_program = self.env['loyalty.program'].create([
{
'name': "$50 discount",
'program_type': 'promotion',
'trigger': 'auto',
'applies_on': 'current',
'rule_ids': [Command.create({})],
'reward_ids': [Command.create({
'reward_type': 'discount',
'discount_mode': 'per_order',
'discount': 50,
'discount_applicability': 'order',
'required_points': 1,
})],
},
{
'name': "$10 discount on Pedal Bin",
'program_type': 'promotion',
'trigger': 'auto',
'applies_on': 'current',
'rule_ids': [Command.create({})],
'reward_ids': [Command.create({
'reward_type': 'discount',
'discount_mode': 'per_order',
'discount': 10,
'discount_applicability': 'specific',
'discount_product_ids': self.pedalBin.ids,
'required_points': 1,
})],
},
])
order = self.empty_order
order.order_line = [Command.create({
'product_id': self.pedalBin.id,
'tax_ids': self.tax_20pc_excl.ids,
})]
self.assertAlmostEqual(
order.amount_total,
self.pedalBin.list_price * (1 + self.tax_20pc_excl.amount / 100), # $56.4
msg="Order total should equal product list price plus taxes",
)
self._auto_rewards(order, order_program)
self.assertAlmostEqual(
order.amount_total,
self.pedalBin.list_price * (1 + self.tax_20pc_excl.amount / 100) - 50, # $6.4
msg="The order total should be $50 less than initially after the discount is applied.",
)
self._auto_rewards(order, specific_program)
self.assertFalse(
order.amount_total,
"Order total should be 0, as a specific discount should have been applied.",
)

View file

@ -1,13 +1,18 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import date, timedelta
from odoo.addons.sale_loyalty.tests.common import TestSaleCouponCommon
from odoo.exceptions import ValidationError
from odoo import Command
from freezegun import freeze_time
from pytz import timezone
class TestProgramRules(TestSaleCouponCommon):
from odoo.exceptions import ValidationError
from odoo.fields import Command, Datetime
from odoo.addons.payment.tests.common import PaymentCommon
from odoo.addons.sale_loyalty.tests.common import TestSaleCouponCommon
class TestProgramRules(TestSaleCouponCommon, PaymentCommon):
# Test all the validity rules to allow a customer to have a reward.
# The check based on the products is already done in the basic operations test
@ -25,13 +30,11 @@ class TestProgramRules(TestSaleCouponCommon):
(0, False, {
'product_id': self.product_A.id,
'name': '1 Product A',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
}),
(0, False, {
'product_id': self.product_B.id,
'name': '2 Product B',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
@ -39,18 +42,16 @@ class TestProgramRules(TestSaleCouponCommon):
self._claim_reward(order, self.immediate_promotion_program)
self.assertEqual(len(order.order_line.ids), 2, "The promo offer shouldn't have been applied as the purchased amount is not enough")
order = self.env['sale.order'].create({'partner_id': self.steve.id})
order = self.env['sale.order'].create({'partner_id': self.partner.id})
order.write({'order_line': [
(0, False, {
'product_id': self.product_A.id,
'name': '10 Product A',
'product_uom': self.uom_unit.id,
'product_uom_qty': 10.0,
}),
(0, False, {
'product_id': self.product_B.id,
'name': '2 Product B',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
@ -282,8 +283,8 @@ class TestProgramRules(TestSaleCouponCommon):
discounts = set(order.order_line.mapped('name')) - {'Product A'}
self.assertEqual(len(discounts), 1, "The order should contains the Product A line and a discount")
# The name of the discount is dynamically changed to smth looking like:
# "Discount: Get 5% discount if buy at least 2 Product - On product with following tax: Tax 15.00%"
self.assertTrue('Discount: 5% on your order' in discounts.pop(), "The discount should be a 5% discount")
# "Discount Get 5% discount if buy at least 2 Product - On product with following tax: Tax 15.00%"
self.assertTrue('Discount 5% on your order' in discounts.pop(), "The discount should be a 5% discount")
sol.product_uom_qty = 5
order._update_programs_and_rewards()
@ -291,71 +292,215 @@ class TestProgramRules(TestSaleCouponCommon):
self._claim_reward(order, p2)
discounts = set(order.order_line.mapped('name')) - {'Product A'}
self.assertEqual(len(discounts), 1, "The order should contains the Product A line and a discount")
self.assertTrue('Discount: 10% on your order' in discounts.pop(), "The discount should be a 10% discount")
self.assertTrue('Discount 10% on your order' in discounts.pop(), "The discount should be a 10% discount")
def test_program_rules_validity_dates_and_uses(self):
# Test case: Based on the validity dates and the number of allowed uses
@freeze_time('2011-11-02 09:00:21')
def test_program_rules_validity_dates(self):
# Test date_to (no date_from)
today = date.today()
past_day = today - timedelta(days=2)
future_day = today + timedelta(days=2)
self.immediate_promotion_program.write({'date_to': past_day})
order = self.empty_order
order.write({'order_line': [
Command.create({
'product_id': self.product_A.id,
'name': '1 Product A',
'product_uom_qty': 1.0,
}),
Command.create({
'product_id': self.product_B.id,
'name': '2 Product B',
'product_uom_qty': 1.0,
})
]})
self._auto_rewards(order, self.immediate_promotion_program)
msg = "The promo shouldn't have been applied as it is expired."
self.assertEqual(len(order.order_line.ids), 2, msg)
self.immediate_promotion_program.write({'date_to': future_day})
self._auto_rewards(order, self.immediate_promotion_program)
msg = "The promo should have been applied we're between the validity dates."
self.assertEqual(len(order.order_line.ids), 3, msg)
# Test date_from (no date_to)
self.immediate_promotion_program.write({
'date_from': future_day, 'date_to': False,
})
self._auto_rewards(order, self.immediate_promotion_program)
msg = "The promo shouldn't have been applied as it is not active yet."
self.assertEqual(len(order.order_line.ids), 2, msg)
self.immediate_promotion_program.write({'date_from': past_day})
self._auto_rewards(order, self.immediate_promotion_program)
msg = "The promo should have been applied we're between the validity dates."
self.assertEqual(len(order.order_line.ids), 3, msg)
# Test date_from and date_to
self.immediate_promotion_program.write({'date_from': past_day, 'date_to': future_day})
self._auto_rewards(order, self.immediate_promotion_program)
msg = "The promo should have been applied as we're between the validity dates"
self.assertEqual(len(order.order_line.ids), 3, msg)
self.immediate_promotion_program.write({
'date_to': date.today() - timedelta(days=2),
'date_from': today + timedelta(days=1),
'date_to': future_day,
})
self._auto_rewards(order, self.immediate_promotion_program)
msg = "The promo offer shouldn't have been applied as it is not active yet."
self.assertEqual(len(order.order_line.ids), 2, msg)
self.immediate_promotion_program.write({
'date_from': past_day,
'date_to': today - timedelta(days=1),
})
self._auto_rewards(order, self.immediate_promotion_program)
msg = "The promo offer shouldn't have been applied as it is expired."
self.assertEqual(len(order.order_line.ids), 2, msg)
self.immediate_promotion_program.write({'date_from': today, 'date_to': today})
self._auto_rewards(order, self.immediate_promotion_program)
msg = "The promo should have been applied as today is a valid starting and ending date."
self.assertEqual(len(order.order_line.ids), 3, msg)
def test_program_rules_number_of_uses(self):
# Test case: Based on the number of allowed uses
self.immediate_promotion_program.write({
'limit_usage': True,
'max_usage': 1,
})
order = self.empty_order
order.write({'order_line': [
(0, False, {
Command.create({
'product_id': self.product_A.id,
'name': '1 Product A',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
}),
(0, False, {
'product_id': self.product_B.id,
'name': '2 Product B',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
self._auto_rewards(order, self.immediate_promotion_program)
self.assertEqual(len(order.order_line.ids), 2, "The promo offer shouldn't have been applied we're not between the validity dates")
self.assertEqual(len(order.order_line.ids), 2, "The promo offer should have been applied")
self.immediate_promotion_program.write({
'date_to': date.today() + timedelta(days=2),
order = self.env['sale.order'].create({
'partner_id': self.env['res.partner'].create({'name': 'My Partner'}).id
})
order = self.env['sale.order'].create({'partner_id': self.steve.id})
order.write({'order_line': [
(0, False, {
'product_id': self.product_A.id,
'name': '1 Product A',
'product_uom': self.uom_unit.id,
'product_uom_qty': 10.0,
}),
(0, False, {
Command.create({
'product_id': self.product_B.id,
'name': '2 Product B',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
self._auto_rewards(order, self.immediate_promotion_program)
self.assertEqual(len(order.order_line.ids), 3, "The promo offer should have been applied as we're between the validity dates")
order = self.env['sale.order'].create({'partner_id': self.env['res.partner'].create({'name': 'My Partner'}).id})
order.write({'order_line': [
(0, False, {
'product_id': self.product_A.id,
'name': '1 Product A',
'product_uom': self.uom_unit.id,
'product_uom_qty': 10.0,
}),
(0, False, {
'product_id': self.product_B.id,
'name': '2 Product B',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
# Invalidate total_order_count
self.immediate_promotion_program.invalidate_recordset(['order_count', 'total_order_count'])
self._auto_rewards(order, self.immediate_promotion_program)
self.assertEqual(len(order.order_line.ids), 2, "The promo offer shouldn't have been applied as the number of uses is exceeded")
msg = "The promo offer shouldn't have been applied as the number of uses is exceeded"
self.assertEqual(len(order.order_line.ids), 1, msg)
def test_program_rules_validity_date_timezones(self):
"""Test that the validity dates are checked according to the company's time zone"""
self.env.company.partner_id.tz = 'Europe/London'
self.partner.tz = 'America/Los_Angeles'
midnight = Datetime.today()
yesterday = (midnight - timedelta(days=1)).date()
self.immediate_promotion_program.update({
'date_to': yesterday,
'limit_usage': True,
'max_usage': 1,
})
order = self.empty_order.with_context(tz=self.partner.tz)
order.order_line = [
Command.create({'product_id': self.product_A.id}),
Command.create({'product_id': self.product_B.id}),
]
with freeze_time(midnight):
# Try apply reward at UTC midnight with LA time zone in context (expired)
self._auto_rewards(order, self.immediate_promotion_program)
self.assertFalse(
order.order_line.filtered('is_reward_line'),
"Promo should not be applied if only valid in the customer's time zone",
)
with freeze_time(timezone(self.env.company.partner_id.tz).localize(midnight)):
# Try apply reward at London midnight (expired)
self._auto_rewards(order, self.immediate_promotion_program)
self.assertFalse(
order.order_line.filtered('is_reward_line'),
"Promo should not be applied if only valid in the customer's time zone",
)
self.partner.tz = 'Europe/Brussels'
with freeze_time(timezone(self.partner.tz).localize(midnight)):
# Apply reward at Brussels midnight (still valid in company's time zone)
self._auto_rewards(order, self.immediate_promotion_program)
self.assertTrue(
order.order_line.filtered('is_reward_line'),
"Promo should be applied if valid in the company's time zone",
)
def test_program_rules_validity_date_transactions(self):
"""Test that the validity dates are checked according to the time of transaction."""
today = Datetime.today()
tomorrow = today + timedelta(days=1)
self.immediate_promotion_program.update({
'date_to': today,
'limit_usage': True,
'max_usage': 1,
'reward_ids': [Command.set(self.program_gift_card.reward_ids.ids)],
})
order = self.empty_order
order.order_line = [
Command.create({'product_id': self.product_A.id}),
Command.create({'product_id': self.product_B.id}),
]
intial_amount = order.amount_total
self._auto_rewards(order, self.immediate_promotion_program)
self.assertLess(order.amount_total, intial_amount, "A discount should be applied")
tx = order.transaction_ids = self._create_transaction(
flow='redirect',
sale_order_ids=[order.id],
state='pending',
reference=order.name,
amount=order.amount_total,
)
# Our slow provider only gets around to confirming the transaction the next day
with freeze_time(tomorrow):
tx._set_done()
tx._post_process()
self.assertAlmostEqual(
order.amount_total, tx.amount,
msg="Discount should still apply if transaction gets confirmed post-expiration",
)
def test_buy_x_get_y_free_applies_correctly_with_non_unit_uom(self):
buy_x_get_y = self.env['loyalty.program'].create({
'name': 'Buy 12 Take 6',
'program_type': 'buy_x_get_y',
'trigger': 'auto',
'applies_on': 'current',
'rule_ids': [Command.create({
'reward_point_mode': 'unit',
'product_ids': self.product_A.ids,
})],
'reward_ids': [Command.create({
'reward_type': 'product',
'reward_product_id': self.product_A.id,
'required_points': 12,
'reward_product_qty': 6,
})],
})
order = self.empty_order
order.order_line = [
Command.create({
'product_id': self.product_A.id,
'product_uom_id': self.ref('uom.product_uom_dozen'),
'product_uom_qty': 1,
}),
]
self._auto_rewards(order, buy_x_get_y)
reward_line = order.order_line.filtered('is_reward_line')
self.assertTrue(reward_line)
self.assertEqual(reward_line.product_uom_qty, 6)

View file

@ -1,14 +1,35 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.sale_loyalty.tests.common import TestSaleCouponCommon
from odoo.exceptions import ValidationError
from odoo.fields import Command
from odoo.addons.sale_loyalty.tests.common import TestSaleCouponCommon
class TestProgramWithCodeOperations(TestSaleCouponCommon):
# Test the basic operation (apply_coupon) on an coupon program on which we should
# apply the reward when the code is correct or remove the reward automatically when the reward is
# not valid anymore.
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.discount_with_multi_rewards = cls.env['loyalty.program'].create({
'name': 'Loyalty program with multiple discount rewards',
'program_type': 'coupons',
'reward_ids': [
Command.create({
'reward_type': 'discount',
'discount_mode': 'percent',
'discount': 20,
}),
Command.create({
'reward_type': 'discount',
'discount_mode': 'percent',
'discount': 5,
}),
],
})
def test_program_usability(self):
# After clicking "Generate coupons", there is no domain so it shows "Match all records".
@ -32,7 +53,7 @@ class TestProgramWithCodeOperations(TestSaleCouponCommon):
self.env['loyalty.generate.wizard'].with_context(active_id=self.code_promotion_program.id).create({
'mode': 'selected',
'customer_ids': self.steve,
'customer_ids': self.partner,
'points_granted': 1,
}).generate_coupons()
coupon = self.code_promotion_program.coupon_ids
@ -50,7 +71,6 @@ class TestProgramWithCodeOperations(TestSaleCouponCommon):
(0, False, {
'product_id': self.product_A.id,
'name': '1 Product A',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
@ -87,7 +107,6 @@ class TestProgramWithCodeOperations(TestSaleCouponCommon):
(0, False, {
'product_id': self.product_A.id,
'name': '1 Product A',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
@ -100,7 +119,6 @@ class TestProgramWithCodeOperations(TestSaleCouponCommon):
(0, False, {
'product_id': self.product_A.id,
'name': '1 Product A',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
@ -126,7 +144,6 @@ class TestProgramWithCodeOperations(TestSaleCouponCommon):
first_pricelist = self.env['product.pricelist'].create({
'name': 'First pricelist',
'discount_policy': 'with_discount',
'item_ids': [(0, 0, {
'compute_price': 'percentage',
'base': 'list_price',
@ -142,7 +159,6 @@ class TestProgramWithCodeOperations(TestSaleCouponCommon):
(0, False, {
'product_id': self.product_C.id,
'name': '1 Product C',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
@ -199,7 +215,6 @@ class TestProgramWithCodeOperations(TestSaleCouponCommon):
(0, False, {
'product_id': self.third_product.id,
'name': '1 Third Product',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
@ -213,7 +228,6 @@ class TestProgramWithCodeOperations(TestSaleCouponCommon):
(0, False, {
'product_id': self.product_A.id,
'name': '1 Product A',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
@ -229,7 +243,6 @@ class TestProgramWithCodeOperations(TestSaleCouponCommon):
(0, False, {
'product_id': self.product_B.id,
'name': '1 Product B',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
@ -258,7 +271,6 @@ class TestProgramWithCodeOperations(TestSaleCouponCommon):
(0, False, {
'product_id': self.product_A.id,
'name': '1 Product A',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
@ -269,7 +281,6 @@ class TestProgramWithCodeOperations(TestSaleCouponCommon):
(0, False, {
'product_id': self.product_B.id,
'name': '1 Product B',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
@ -283,6 +294,128 @@ class TestProgramWithCodeOperations(TestSaleCouponCommon):
order_bis._update_programs_and_rewards()
self.assertEqual(len(order_bis.order_line), 2, "Free product from a coupon generated from a promotion program on next order should not dissapear")
def test_partner_assigned_to_next_order_coupon(self):
""" Test the assignment of a partner on coupons with program type `next_order_coupons`.
1. Create a loyalty program of type `next_order_coupons`.
2. Create a sale order and add a product to it.
3. Apply the loyalty program to the sale order.
4. Verify that the generated coupon is assigned to the order's partner.
"""
loyalty_program = self.env['loyalty.program'].create({
'name': '10% Discount on Next Order',
'program_type': 'next_order_coupons',
'applies_on': 'future',
'trigger': 'auto',
'rule_ids': [Command.create({})],
'reward_ids': [Command.create({
'reward_type': 'discount',
'discount': 10,
'discount_mode': 'percent',
'discount_applicability': 'order',
})],
})
order = self.empty_order
order.write({'order_line': [
Command.create({
'product_id': self.product_A.id,
'name': '1 Product A',
'product_uom_qty': 1.0,
})
]})
generated_coupons = order._try_apply_program(loyalty_program).get('coupon')
self.assertTrue(generated_coupons, "A coupon should have been generated")
self.assertEqual(generated_coupons.partner_id, order.partner_id,
"The partner should be set on the coupon with program type 'next_order_coupons'"
)
def test_public_partner_updated_in_next_order_coupon(self):
""" Test the update of a partner on coupons with program type `next_order_coupons`.
1. Create a loyalty program of type `next_order_coupons`.
2. Create a sale order for a public user and add a product to it.
3. Apply the loyalty program to the sale order.
4. Verify that the generated coupon is assigned to the public user.
5. Change the partner.
6. Verify that the generated coupon was updated to this new user.
"""
loyalty_program = self.env['loyalty.program'].create({
'name': "10% Discount on Next Order",
'program_type': 'next_order_coupons',
'applies_on': 'future',
'trigger': 'auto',
'rule_ids': [Command.create({})],
'reward_ids': [Command.create({
'reward_type': 'discount',
'discount': 10,
'discount_mode': 'percent',
'discount_applicability': 'order',
})],
})
order = self.empty_order
order.write({
'partner_id': self.env.ref('base.public_partner').id,
'order_line': [Command.create({'product_id': self.product_A.id})],
})
generated_coupons = order._try_apply_program(loyalty_program).get('coupon')
self.assertTrue(generated_coupons, "A coupon should have been generated")
self.assertEqual(
generated_coupons.partner_id, order.partner_id,
"The partner should be set on the coupon with program type 'next_order_coupons'",
)
self.assertTrue(generated_coupons.partner_id.is_public)
# Change partner from Public User to a known customer (e.g. a portal user logging in)
order.partner_id = self.partner
order._update_programs_and_rewards()
self.assertEqual(
generated_coupons.partner_id, self.partner,
"The coupon's partner_id should be updated if it was created for a Public User",
)
def test_change_reward_on_confirmed_order(self):
"""Check that changing rewards on a confirmed order restores points on the coupon.
Tested flow:
- have a coupon program with 2 discount rewards;
- have confirmed order;
- apply a 10% discount reward, costing 1 point;
- change to a 50% discount reward, costing 5 points;
- check that there are still 5 points left on the coupon.
"""
program = self.code_promotion_program_with_discount
program.update({
'rule_ids': [Command.clear()],
'reward_ids': [Command.create({
'discount': 50,
'discount_mode': 'percent',
'discount_applicability': 'order',
'required_points': 5,
})],
})
discount10, discount50 = program.reward_ids
self.env['loyalty.generate.wizard'].with_context(active_id=program.id).create({
'coupon_qty': 1,
'points_granted': 10,
}).generate_coupons()
coupon = program.coupon_ids
order = self.empty_order
order.order_line = [Command.create({'product_id': self.product_C.id})]
order.action_confirm()
order.order_line.product_updatable = True # in case `sale_project` is installed
order._apply_program_reward(discount10, coupon)
reward_line = order.order_line.filtered('is_reward_line')
self.assertEqual(order.amount_total, 90, "10% discount should be applied")
self.assertEqual(coupon.points, 9, "10% discount reward should use 1 point")
order.order_line.product_updatable = True # in case `sale_project` is installed
order._apply_program_reward(discount50, coupon)
self.assertIn(reward_line, order.order_line, "Reward line should be re-used")
self.assertEqual(order.amount_total, 50, "50% discount should be applied")
self.assertEqual(coupon.points, 5, "50% discount reward should use 5 points")
def test_edit_and_reapply_promotion_program(self):
# The flow:
# 1. Create a program auto applied, giving a fixed amount discount
@ -310,7 +443,6 @@ class TestProgramWithCodeOperations(TestSaleCouponCommon):
(0, False, {
'product_id': self.product_A.id,
'name': '1 Product A',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
@ -331,3 +463,148 @@ class TestProgramWithCodeOperations(TestSaleCouponCommon):
self._apply_promo_code(order, 'test')
# But the above line should not add any reward
self.assertEqual(len(order.order_line), 2, "You should get a discount line") # product + discount
def test_reapply_multiple_global_rewards_when_new_discount_greater(self):
""" Test applying the maximum reward discount from multiple rewards when the applied
coupon discount is lower.
1. Create a two loyalty program of type `coupons`.
2. Add multiple rewards to the second program.
3. Generate a coupon for each program.
2. Create a sale order and add a product to it.
3. Apply the Coupon to the sale order.
4. Try to apply the second coupon with multiple rewards.
5. Reward with best discount will be shown.
"""
self.code_promotion_program_with_discount.rule_ids.unlink()
coupon_1 = self._generate_coupons(self.code_promotion_program_with_discount)
coupon_2 = self._generate_coupons(self.discount_with_multi_rewards)
order = self.empty_order
self.assertEqual(order.amount_total, 0.0)
order.write({'order_line': [
Command.create({
'product_id': self.product_A.id,
'name': '1 Product A',
'product_uom_qty': 1.0,
}),
Command.create({
'product_id': self.product_B.id,
'name': '1 Product B',
'product_uom_qty': 1.0,
}),
]})
# The order line should be created with the correct price unit
self.assertEqual(len(order.order_line.ids), 2)
self.assertEqual(order.order_line[0].price_unit, 100.0)
self.assertEqual(order.order_line[1].price_unit, 5.0)
expected_total = order.amount_total * 0.80
# Apply the first coupon
self._apply_promo_code(order, coupon_1.code)
self.assertEqual(len(order.order_line.ids), 3)
msg = "The discount line should be the 10% discount on the sale order total."
self.assertEqual(order.order_line[2].price_unit, -10.5, msg=msg)
# Apply the second coupon with multiple rewards
self._apply_promo_code(order, coupon_2.code)
self.assertEqual(len(order.order_line.ids), 3)
msg = "The discount line should be the 20% discount on the sale order total."
self.assertEqual(order.order_line[2].price_unit, -21.0, msg=msg)
msg = "Order total should reflect the 20% discount"
self.assertAlmostEqual(order.amount_total, expected_total, msg=msg)
def test_reapply_multiple_higher_global_rewards_lets_choose_best(self):
""" Test applying the maximum reward discount from multiple rewards when the applied
coupon discount is lower.
1. Create a two loyalty program of type `coupons`.
2. Add multiple rewards of higher discount than the applied discount.
3. Generate a coupon for each program.
2. Create a sale order and add a product to it.
3. Apply the Coupon to the sale order.
4. Try to apply the second coupon with multiple rewards.
5. Reward with max discount will be shown.
"""
self.code_promotion_program_with_discount.rule_ids.unlink()
coupon_1 = self._generate_coupons(self.code_promotion_program_with_discount)
self.discount_with_multi_rewards.reward_ids[1].discount = 15
coupon_2 = self._generate_coupons(self.discount_with_multi_rewards)
order = self.empty_order
self.assertEqual(order.amount_total, 0.0)
order.write({'order_line': [
Command.create({
'product_id': self.product_A.id,
'name': '1 Product A',
'product_uom_qty': 1.0,
})
]})
# The order line should be created with the correct price unit
self.assertEqual(len(order.order_line.ids), 1)
self.assertEqual(order.order_line[0].price_unit, 100.0)
expected_total = order.amount_total * 0.80
# Apply the first coupon
self._apply_promo_code(order, coupon_1.code)
self.assertEqual(len(order.order_line.ids), 2)
msg = "The discount line should be the 10% discount on the sale order total."
self.assertEqual(order.order_line[1].price_unit, -10.0, msg=msg)
# Apply the second coupon with multiple rewards of higher discount
rewards = self._apply_promo_code(order, coupon_2.code)
self.assertEqual(len(rewards), 2)
# Choose the reward with the maximum discount
chosen_reward = rewards.filtered(lambda r: r.discount == 20)
order._apply_program_reward(chosen_reward, coupon_2)
self.assertEqual(len(order.order_line), 2)
msg = "The discount line should be the 20% discount on the sale order total."
self.assertEqual(order.order_line[1].price_unit, -20.0, msg=msg)
msg = "Order total should reflect the 20% discount"
self.assertAlmostEqual(order.amount_total, expected_total, msg=msg)
def test_reapplying_new_multiple_lower_global_rewards_discount_raise_validation(self):
""" Test raising validation when the new coupon discount from multiple rewards
is less than the applied coupon discount.
1. Create a two loyalty program of type `coupons`.
2. Add multiple rewards to the second program.
3. Generate a coupon for each program.
2. Create a sale order and add a product to it.
3. Apply the Coupon to the sale order.
4. Try to apply the second coupon with multiple rewards.
5. Verify that it raises a validation error.
"""
self.code_promotion_program_with_discount.rule_ids.unlink()
coupon_1 = self._generate_coupons(self.code_promotion_program_with_discount)
self.discount_with_multi_rewards.reward_ids[0].discount = 7
coupon_2 = self._generate_coupons(self.discount_with_multi_rewards)
order = self.empty_order
self.assertEqual(order.amount_total, 0.0)
order.write({'order_line': [
Command.create({
'product_id': self.product_A.id,
'name': '1 Product A',
'product_uom_qty': 1.0,
})
]})
# The order line should be created with the correct price unit
self.assertEqual(len(order.order_line.ids), 1)
self.assertEqual(order.order_line[0].price_unit, 100.0)
# Apply the first coupon
self._apply_promo_code(order, coupon_1.code)
msg = "The discount line should be the 10% discount on the sale order total."
self.assertEqual(order.order_line[1].price_unit, -10.0, msg=msg)
self.assertEqual(len(order.order_line.ids), 2)
# raise validation error when applying the second coupon with multiple rewards
# with a discount lower than the applied coupon discount
msg = "The new coupon discount should be greater than the applied coupon discount"
with self.assertRaises(ValidationError, msg=msg):
self._apply_promo_code(order, coupon_2.code)

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.sale_loyalty.tests.common import TestSaleCouponCommon
@ -18,7 +17,6 @@ class TestProgramWithoutCodeOperations(TestSaleCouponCommon):
(0, False, {
'product_id': self.product_A.id,
'name': '1 Product A',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
@ -31,7 +29,6 @@ class TestProgramWithoutCodeOperations(TestSaleCouponCommon):
(0, False, {
'product_id': self.product_B.id,
'name': '2 Product B',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})

View file

@ -0,0 +1,43 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.fields import Command
from odoo.tests import tagged
from odoo.addons.sale_loyalty.tests.common import TestSaleCouponCommon
@tagged('post_install', '-at_install')
class TestSaleAutoInvoice(TestSaleCouponCommon):
def test_automatic_invoice_on_zero_amount_order(self):
self.env['ir.config_parameter'].sudo().set_param('sale.automatic_invoice', 'True')
# Create a loyalty program with 100% discount
self.env['loyalty.program'].sudo().create({
'name': '100discount',
'program_type': 'promo_code',
'rule_ids': [
Command.create({
'code': "100dis",
'minimum_amount': 0,
})
],
'reward_ids': [
Command.create({
'discount': 100,
}),
],
})
# Add order line to order
self.env["sale.order.line"].create({
'order_id': self.empty_order.id,
'product_id': self.product_A.id,
'product_uom_qty': 1,
'price_unit': 200,
})
# Apply discount
self._apply_promo_code(self.empty_order, '100dis')
self.empty_order._validate_order()
self.assertTrue(
self.empty_order.invoice_ids,
"Invoices should be generated for orders with zero total amount",
)

View file

@ -1,10 +1,9 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.sale_loyalty.tests.common import TestSaleCouponCommon
from odoo.exceptions import UserError
from odoo.tests import tagged
from odoo.addons.sale_loyalty.tests.common import TestSaleCouponCommon
@tagged('post_install', '-at_install')
class TestSaleInvoicing(TestSaleCouponCommon):
@ -50,17 +49,19 @@ class TestSaleInvoicing(TestSaleCouponCommon):
order.action_confirm()
invoiceable_lines = order._get_invoiceable_lines()
# Product was not delivered, we cannot invoice
# the product line nor the promotion line
order._compute_invoice_status()
# Product was not delivered, the order invoice status is 'No' as invoicing it should not be
# promoted, but the reward line should still be invoiceable, if users wants to invoice it
self.assertEqual(order.invoice_status, 'no')
self.assertEqual(len(invoiceable_lines), 1)
inv = order._create_invoices()
self.assertEqual(len(inv.invoice_line_ids), 1)
invoiceable_lines = order._get_invoiceable_lines()
self.assertEqual(len(invoiceable_lines), 0)
with self.assertRaises(UserError):
order._create_invoices()
inv.button_cancel()
order.order_line[0].qty_delivered = 1
# Product is delivered, the two lines can be invoiced.
order._compute_invoice_status()
self.assertEqual(order.invoice_status, 'to invoice')
invoiceable_lines = order._get_invoiceable_lines()
self.assertEqual(order.order_line, invoiceable_lines)

View file

@ -1,9 +1,11 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import Command
from datetime import timedelta
from odoo.fields import Command, Date
from odoo.tests.common import tagged
from odoo.addons.sale_loyalty.tests.common import TestSaleCouponCommon
from odoo.tests.common import tagged
@tagged('-at_install', 'post_install')
@ -36,13 +38,11 @@ class TestUnlinkReward(TestSaleCouponCommon):
Command.create({
'product_id': self.product_A.id,
'name': 'Ordinary Product A',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
}),
Command.create({
'product_id': self.product_B.id,
'name': '2 Product B',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
}),
]})
@ -53,3 +53,19 @@ class TestUnlinkReward(TestSaleCouponCommon):
# Check that the reward is archived and not deleted
self.assertTrue(self.reward.exists())
self.assertFalse(self.reward.active)
def test_unlink_expired_coupon_line(self):
"""Ensure that lines linked to expired coupons get unlinked from the order."""
order = self.empty_order
order.order_line = [Command.create({'product_id': self.product_A.id})]
coupon_program = self.code_promotion_program
self.env['loyalty.generate.wizard'].with_context(active_id=coupon_program.id).create({
'coupon_qty': 1,
'points_granted': 1,
}).generate_coupons()
coupon = coupon_program.coupon_ids
self._apply_promo_code(order, coupon.code)
self.assertTrue(order.order_line.coupon_id)
coupon.expiration_date = Date.today() - timedelta(days=1)
order._update_programs_and_rewards()
self.assertFalse(order.order_line.coupon_id)