Initial commit: Sale packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:49 +02:00
commit 14e3d26998
6469 changed files with 2479670 additions and 0 deletions

View file

@ -0,0 +1,13 @@
# -*- 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_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_invoicing
from . import test_unlink_reward

View file

@ -0,0 +1,352 @@
# -*- 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.addons.sale.tests.test_sale_product_attribute_value_config import TestSaleProductAttributeValueCommon
class TestSaleCouponCommon(TestSaleProductAttributeValueCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
# 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
cls.tax_15pc_excl = cls.env['account.tax'].create({
'name': "Tax 15%",
'amount_type': 'percent',
'amount': 15,
'type_tax_use': 'sale',
})
cls.tax_10pc_incl = cls.env['account.tax'].create({
'name': "10% Tax incl",
'amount_type': 'percent',
'amount': 10,
'price_include': True,
})
cls.tax_10pc_base_incl = cls.env['account.tax'].create({
'name': "10% Tax incl base amount",
'amount_type': 'percent',
'amount': 10,
'price_include': True,
'include_base_amount': True,
})
cls.tax_10pc_excl = cls.env['account.tax'].create({
'name': "10% Tax excl",
'amount_type': 'percent',
'amount': 10,
'price_include': False,
})
cls.tax_20pc_excl = cls.env['account.tax'].create({
'name': "20% Tax excl",
'amount_type': 'percent',
'amount': 20,
'price_include': False,
})
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)],
})
#products
cls.product_A = cls.env['product.product'].create({
'name': 'Product A',
'list_price': 100,
'sale_ok': True,
'taxes_id': [(6, 0, [cls.tax_15pc_excl.id])],
})
cls.product_B = cls.env['product.product'].create({
'name': 'Product B',
'list_price': 5,
'sale_ok': True,
'taxes_id': [(6, 0, [cls.tax_15pc_excl.id])],
})
cls.product_C = cls.env['product.product'].create({
'name': 'Product C',
'list_price': 100,
'sale_ok': True,
'taxes_id': [(6, 0, [])],
})
cls.product_D = cls.env['product.product'].create({
'name': 'Product D',
'list_price': 100,
'sale_ok': True,
'taxes_id': [(6, 0, [cls.tax_group.id])],
})
cls.product_gift_card = cls.env['product.product'].create({
'name': 'Gift Card 50',
'detailed_type': 'service',
'list_price': 50,
'sale_ok': True,
'taxes_id': False,
})
# Immediate Program By A + B: get B free
# No Conditions
cls.program_gift_card = cls.env['loyalty.program'].create({
'name': 'Gift Cards',
'applies_on': 'future',
'program_type': 'gift_card',
'trigger': 'auto',
'rule_ids': [(0, 0, {
'product_ids': cls.product_gift_card,
'reward_point_amount': 1,
'reward_point_mode': 'money',
'reward_point_split': True,
})],
'reward_ids': [(0, 0, {
'reward_type': 'discount',
'discount': 1,
'discount_mode': 'per_point',
'discount_applicability': 'order',
})]
})
cls.immediate_promotion_program = cls.env['loyalty.program'].create({
'name': 'Buy A + 1 B, 1 B are free',
'program_type': 'promotion',
'applies_on': 'current',
'company_id': cls.env.company.id,
'trigger': 'auto',
'rule_ids': [(0, 0, {
'product_ids': cls.product_A,
'reward_point_amount': 1,
'reward_point_mode': 'order',
'minimum_qty': 1,
})],
'reward_ids': [(0, 0, {
'reward_type': 'product',
'reward_product_id': cls.product_B.id,
'reward_product_qty': 1,
'required_points': 1,
})],
})
cls.code_promotion_program = cls.env['loyalty.program'].create({
'name': 'Buy 1 A + Enter code, 1 A is free',
'program_type': 'coupons',
'trigger': 'with_code',
'applies_on': 'current',
'company_id': cls.env.company.id,
'rule_ids': [(0, 0, {
'product_ids': cls.product_A,
'reward_point_amount': 1,
'reward_point_mode': 'order',
'minimum_qty': 1,
})],
'reward_ids': [(0, 0, {
'reward_type': 'product',
'reward_product_id': cls.product_A.id,
'reward_product_qty': 1,
'required_points': 1,
})],
})
cls.code_promotion_program_with_discount = cls.env['loyalty.program'].create({
'name': 'Buy 1 C + Enter code, 10 percent discount on C',
'program_type': 'coupons',
'trigger': 'with_code',
'applies_on': 'current',
'company_id': cls.env.company.id,
'rule_ids': [(0, 0, {
'mode': 'with_code',
'code': 'promotion_code_disc',
'product_ids': cls.product_C,
'reward_point_amount': 1,
'reward_point_mode': 'order',
'minimum_qty': 1,
})],
'reward_ids': [(0, 0, {
'reward_type': 'discount',
'discount_mode': 'percent',
'discount': 10,
'discount_applicability': 'order',
'required_points': 1,
})],
})
def _extract_rewards_from_claimable(self, status):
rewards = self.env['loyalty.reward']
for info in status.values():
for reward_count in info['rewards']:
rewards |= reward_count[0]
def _apply_promo_code(self, order, code, no_reward_fail=True):
status = order._try_apply_code(code)
if 'error' in status:
raise ValidationError(status['error'])
if not status and no_reward_fail:
# Can happen if global discount got filtered out in `_get_claimable_rewards`
raise ValidationError('No reward to claim with this coupon')
coupons = self.env['loyalty.card']
rewards = self.env['loyalty.reward']
for coupon, coupon_rewards in status.items():
coupons |= coupon
rewards |= coupon_rewards
if len(coupons) == 1 and len(rewards) == 1:
status = order._apply_program_reward(rewards, coupons)
if 'error' in status:
raise ValidationError(status['error'])
def _claim_reward(self, order, program, coupon=False):
if len(program.reward_ids) != 1:
return False
coupon = coupon or order.coupon_point_ids.coupon_id.filtered(lambda c: c.program_id == program)
if len(coupon) != 1:
return False
status = order._apply_program_reward(program.reward_ids, coupon)
return 'error' not in status
def _auto_rewards(self, order, programs):
order._update_programs_and_rewards()
coupons_per_program = defaultdict(lambda: self.env['loyalty.card'])
for coupon in order.coupon_point_ids.coupon_id:
coupons_per_program[coupon.program_id] |= coupon
for program in programs:
if len(program.reward_ids) > 1 or len(coupons_per_program[program]) != 1 or not program.active:
continue
self._claim_reward(order, program, coupons_per_program[program])
class TestSaleCouponNumbersCommon(TestSaleCouponCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.largeCabinet = cls.env['product.product'].create({
'name': 'Large Cabinet',
'list_price': 320.0,
'taxes_id': False,
})
cls.conferenceChair = cls.env['product.product'].create({
'name': 'Conference Chair',
'list_price': 16.5,
'taxes_id': False,
})
cls.pedalBin = cls.env['product.product'].create({
'name': 'Pedal Bin',
'list_price': 47.0,
'taxes_id': False,
})
cls.drawerBlack = cls.env['product.product'].create({
'name': 'Drawer Black',
'list_price': 25.0,
'taxes_id': False,
})
cls.largeMeetingTable = cls.env['product.product'].create({
'name': 'Large Meeting Table',
'list_price': 40000.0,
'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',
'program_type': 'promotion',
'applies_on': 'current',
'rule_ids': [(0, 0, {
'mode': 'with_code',
'code': 'test_10pc',
})],
'reward_ids': [(0, 0, {
'reward_type': 'discount',
'discount_mode': 'percent',
'discount': 10,
'discount_applicability': 'order',
'required_points': 1,
})],
})
cls.p2 = cls.env['loyalty.program'].create({
'name': 'Buy 3 cabinets, get one for free',
'trigger': 'auto',
'program_type': 'promotion',
'applies_on': 'current',
'rule_ids': [(0, 0, {
'product_ids': cls.largeCabinet,
'reward_point_mode': 'unit',
'minimum_qty': 3,
})],
'reward_ids': [(0, 0, {
'reward_type': 'product',
'reward_product_id': cls.largeCabinet.id,
'reward_product_qty': 1,
'required_points': 3,
})],
})
cls.p3 = cls.env['loyalty.program'].create({
'name': 'Buy 1 drawer black, get a free Large Meeting Table',
'trigger': 'auto',
'program_type': 'promotion',
'applies_on': 'current',
'rule_ids': [(0, 0, {
'product_ids': cls.drawerBlack,
'reward_point_mode': 'order',
'minimum_qty': 1,
})],
'reward_ids': [(0, 0, {
'reward_type': 'product',
'reward_product_id': cls.largeMeetingTable.id,
'reward_product_qty': 1,
'required_points': 1,
})],
})
cls.discount_coupon_program = cls.env['loyalty.program'].create({
'name': '$100 coupon',
'program_type': 'coupons',
'trigger': 'with_code',
'applies_on': 'current',
'rule_ids': [(0, 0, {
'minimum_amount': 100,
})],
'reward_ids': [(0, 0, {
'reward_type': 'discount',
'discount_mode': 'per_point',
'discount': 100,
'discount_applicability': 'order',
'required_points': 1,
})],
})
cls.all_programs = cls.env['loyalty.program'].search([])

View file

@ -0,0 +1,78 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import Command
from odoo.addons.sale_loyalty.tests.common import TestSaleCouponCommon
from odoo.tests.common import tagged
@tagged('-at_install', 'post_install')
class TestBuyGiftCard(TestSaleCouponCommon):
def test_buying_gift_card(self):
order = self.empty_order
self.immediate_promotion_program.active = False
order.write({'order_line': [
(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,
})
]})
self.assertEqual(len(order.order_line.ids), 2)
self.assertEqual(len(order._get_reward_coupons()), 0)
order._update_programs_and_rewards()
self.assertEqual(len(order._get_reward_coupons()), 1)
order.order_line[1].product_uom_qty = 2
order._update_programs_and_rewards()
self.assertEqual(len(order._get_reward_coupons()), 2)
order.order_line[1].product_uom_qty = 1
order._update_programs_and_rewards()
self.assertEqual(len(order._get_reward_coupons()), 1)
def test_gift_card_email_sender(self):
"""Ensure that sending gift card emails have a sender.
Either the order's salesman if available, otherwise the order's company.
"""
mail_template = self.env['mail.template'].create({
'name': "Gift Card Mail",
'model_id': self.env.ref('loyalty.model_loyalty_card').id,
'auto_delete': False,
})
self.program_gift_card.communication_plan_ids = [Command.create({
'trigger': 'create',
'mail_template_id': mail_template.id,
})]
order = self.empty_order
salesman = order.user_id.partner_id.ensure_one()
salesman.email = "sales@company.co"
company = order.company_id.partner_id
company.email = "noreply@company.co"
order.write({
'order_line': [Command.create({'product_id': self.product_gift_card.id})],
})
order._update_programs_and_rewards()
# Create an order without salesman to test company-based fallback
orders = order + order.copy({'user_id': None})
# Clear out the mailbox before sending mail
self.env['mail.mail'].search([]).sudo().unlink()
# Confirm order as Public User to trigger loyalty mail
public_user = self.env.ref('base.public_user')
orders.with_user(public_user).with_company(order.company_id).sudo().action_confirm()
mails = self.env['mail.mail'].search([])
self.assertEqual(len(mails), 2)
salesman_mail = mails.filtered(lambda m: m.author_id == salesman).ensure_one()
company_mail = mails.filtered(lambda m: m.author_id == company).ensure_one()
self.assertEqual(salesman_mail.email_from, salesman.email_formatted)
self.assertEqual(company_mail.email_from, company.email_formatted)

View file

@ -0,0 +1,915 @@
# 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 odoo.tools.float_utils import float_compare
from odoo.addons.sale_loyalty.tests.common import TestSaleCouponCommon
@tagged('post_install', '-at_install')
class TestLoyalty(TestSaleCouponCommon):
@classmethod
def setUpClass(cls):
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.ewallet_program = cls.env['loyalty.program'].create({
'name': 'eWallet Program',
'program_type': 'ewallet',
'trigger': 'auto',
'applies_on': 'future',
'reward_ids': [Command.create({
'reward_type': 'discount',
'discount_mode': 'per_point',
'discount': 1,
})],
'rule_ids': [Command.create({
'reward_point_amount': '1',
'reward_point_mode': 'money',
'product_ids': cls.env.ref('loyalty.ewallet_product_50'),
})],
'trigger_product_ids': cls.env.ref('loyalty.ewallet_product_50'),
})
cls.ewallet = cls.env['loyalty.card'].create({
'program_id': cls.ewallet_program.id,
'partner_id': cls.partner_a.id,
'points': 10,
})
cls.ewallet_program.coupon_ids = [Command.set([cls.ewallet.id])]
cls.user_salemanager = new_test_user(cls.env, login='user_salemanager', groups='sales_team.group_sale_manager')
cls.promotion_code_10pc = cls.env['loyalty.program'].create({
'name': "Code for 10% on orders",
'trigger': 'with_code',
'program_type': 'promotion',
'applies_on': 'current',
'rule_ids': [Command.create({
'mode': 'with_code',
'code': 'test_10pc',
})],
'reward_ids': [Command.create({
'reward_type': 'discount',
'discount_mode': 'percent',
'discount': 10,
'discount_applicability': 'order',
'required_points': 1,
})],
})
def test_nominative_programs(self):
loyalty_program = self.env['loyalty.program'].create({
'name': 'Loyalty Program',
'program_type': 'loyalty',
'trigger': 'auto',
'applies_on': 'both',
'rule_ids': [(0, 0, {
'reward_point_mode': 'unit',
'reward_point_amount': 1,
'product_ids': [self.product_a.id],
})],
'reward_ids': [(0, 0, {
'reward_type': 'discount',
'discount': 1.5,
'discount_mode': 'per_point',
'discount_applicability': 'order',
'required_points': 3,
})],
})
order = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
})
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,
'points': 10,
})
self.ewallet.points = 0
order.write({
'order_line': [(0, 0, {
'product_id': self.product_a.id,
'product_uom_qty': 1,
})]
})
order._update_programs_and_rewards()
claimable_rewards = order._get_claimable_rewards()
self.assertEqual(len(claimable_rewards), 1, "The ewallet program should not be applicable since the card has no points.")
vals = order._get_reward_values_discount(loyalty_program.reward_ids[0], loyalty_card)
self.assertEqual(
vals[0]['points_cost'] % loyalty_program.reward_ids.required_points,
0,
"Can only use a whole number of required points",
)
self.assertEqual(vals[0]['points_cost'], 9, "Use maximum available points for the reward")
self.ewallet.points = 50
order._update_programs_and_rewards()
claimable_rewards = order._get_claimable_rewards()
self.assertEqual(len(claimable_rewards), 2, "Now that the ewallet has some points they should both be applicable.")
def test_cancel_order_with_coupons(self):
"""This test ensure that creating an order with coupons will not
raise an access error on POS line modele when canceling the order."""
self.env['loyalty.program'].create({
'name': '10% Discount',
'program_type': 'coupons',
'applies_on': 'current',
'trigger': 'auto',
'rule_ids': [(0, 0, {})],
'reward_ids': [(0, 0, {
'reward_type': 'discount',
'discount': 10,
'discount_mode': 'percent',
'discount_applicability': 'order',
})]
})
order = self.env['sale.order'].with_user(self.user_salemanager).create({
'partner_id': self.partner_a.id,
'order_line': [
(0, 0, {
'product_id': self.product_a.id,
})
]
})
order._update_programs_and_rewards()
self.assertTrue(order.coupon_point_ids)
# Canceling the order should not raise an access error:
# During the cancel process, we are trying to get `use_count` of the coupon,
# and we call the `_compute_use_count` that is also in pos_loyalty.
# This last one will try to find related POS lines while user have not access to POS.
order._action_cancel()
self.assertFalse(order.coupon_point_ids)
def test_distribution_amount_payment_programs(self):
"""
Check how the amount of a payment reward is distributed.
An ewallet should not be used to refund taxes.
Its amount must be distributed between the products.
"""
# Create two products
product_a, product_b = self.env['product.product'].create([
{
'name': 'Product A',
'list_price': 100,
'sale_ok': True,
'taxes_id': [Command.set(self.tax_15pc_excl.ids)],
},
{
'name': 'Product B',
'list_price': 100,
'sale_ok': True,
'taxes_id': [Command.set(self.tax_15pc_excl.ids)],
},
])
# Create a coupon and a ewallet
coupon_program, ewallet_program = self.env['loyalty.program'].create([
{
'name': 'Coupon Program',
'program_type': 'coupons',
'trigger': 'with_code',
'applies_on': 'both',
'reward_ids': [Command.create({
'reward_type': 'discount',
'discount': 100.0,
'discount_applicability': 'specific',
'discount_product_domain': '[("name", "=", "Product A")]',
})],
},
{
'name': 'eWallet Program',
'program_type': 'ewallet',
'applies_on': 'future',
'trigger': 'auto',
'rule_ids': [Command.create({
'reward_point_mode': 'money',
})],
'reward_ids': [Command.create({
'discount_mode': 'per_point',
'discount': 1,
'discount_applicability': 'order',
})],
}
])
coupon_partner, _ = self.env['loyalty.card'].create([
{
'program_id': coupon_program.id,
'partner_id': self.partner_a.id,
'points': 1,
'code': '5555',
},
{
'program_id': ewallet_program.id,
'partner_id': self.partner_a.id,
'points': 115,
},
])
# Create the order
order = self.env['sale.order'].with_user(self.user_salemanager).create({
'partner_id': self.partner_a.id,
'order_line': [
Command.create({
'product_id': product_a.id,
}),
Command.create({
'product_id': product_b.id,
}),
]
})
self.assertEqual(order.amount_total, 230.0)
self.assertEqual(order.amount_untaxed, 200.0)
self.assertEqual(order.amount_tax, 30.0)
# Apply the eWallet
order._update_programs_and_rewards()
self._claim_reward(order, ewallet_program)
self.assertEqual(order.amount_total, 115.0)
self.assertEqual(order.amount_untaxed, 85.0)
self.assertEqual(order.amount_tax, 30.0)
self.assertEqual(order.reward_amount, -115.0)
# Apply the coupon
self._apply_promo_code(order, coupon_partner.code)
self.assertEqual(order.amount_total, 0.0)
self.assertEqual(order.amount_untaxed, -15.0)
self.assertEqual(order.amount_tax, 15.0)
self.assertEqual(order.reward_amount, -215.0)
def test_discount_max_amount_on_specific_product(self):
product_a = self.product_A
product_b = self.product_B
product_a.write({'taxes_id': [Command.set(self.tax_20pc_excl.ids)]})
product_b.write({'list_price': -20, 'taxes_id': [Command.set(self.tax_20pc_excl.ids)]})
self.env['loyalty.program'].search([]).write({'active': False})
promotion = self.env['loyalty.program'].create({
'name': '10% Discount',
'program_type': 'promotion',
'trigger': 'auto',
'rule_ids': [Command.create({'reward_point_amount': 1, 'reward_point_mode': 'unit'})],
'reward_ids': [Command.create({
'discount': 10.0,
'discount_max_amount': 9,
'discount_applicability': 'specific',
'discount_product_ids': [product_a.id],
})],
})
order = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
'order_line': [Command.create({'product_id': product_a.id})],
})
self.assertEqual(order.reward_amount, 0)
self._auto_rewards(order, promotion)
reward_amount_tax_included = sum(l.price_total for l in order.order_line if l.reward_id)
msg = "Max discount amount reached, the reward amount should be the max amount value."
self.assertEqual(reward_amount_tax_included, -9, msg)
order.order_line = [Command.clear(), Command.create({'product_id': product_b.id})]
self._auto_rewards(order, promotion)
reward_amount_tax_included = sum(l.price_total for l in order.order_line if l.reward_id)
msg = "This product is not eligible to the discount."
self.assertEqual(reward_amount_tax_included, 0, msg=msg)
order.order_line = [
Command.clear(),
Command.create({'product_id': product_a.id}), # price_total = 120
Command.create({'product_id': product_b.id}), # price_total = -20
]
self._auto_rewards(order, promotion)
reward_amount_tax_included = sum(l.price_total for l in order.order_line if l.reward_id)
msg = "Reward amount above the max amount, the reward should be the max amount value."
self.assertEqual(reward_amount_tax_included, -9, msg)
order.order_line = [
Command.clear(),
Command.create({'product_id': product_a.id}), # price_total = 120
Command.create({'product_id': product_b.id, 'price_unit': -95}), # price_total = -114
]
self._auto_rewards(order, promotion)
reward_amount_tax_included = sum(l.price_total for l in order.order_line if l.reward_id)
msg = "Reward amount should never surpass the order's current total amount."
self.assertEqual(reward_amount_tax_included, -6, msg)
order.order_line = [
Command.clear(),
Command.create({'product_id': product_a.id, 'price_unit': 50}), # price_total = 60
Command.create({'product_id': product_b.id, 'price_unit': -5}), # price_total = -6
]
self._auto_rewards(order, promotion)
reward_amount_tax_included = sum(l.price_total for l in order.order_line if l.reward_id)
msg = "Reward amount should be the percentage one if under the max amount discount."
self.assertEqual(reward_amount_tax_included, -6, msg)
def test_points_awarded_global_discount_code_no_domain_program(self):
"""
Check the calculation for points awarded when there is a global discount applied and the
loyalty program applies on all products (no domain).
"""
LoyaltyProgram = self.env['loyalty.program']
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,
'points': 0,
})
order = self.env['sale.order'].with_user(self.user_salemanager).create({
'partner_id': self.partner_a.id,
'order_line': [
Command.create({
'product_id': self.product_A.id,
'tax_id': False,
}),
]
})
promotion_program = self.env['loyalty.program'].create([{
'name': "Coupon Program",
'program_type': 'promotion',
'trigger': 'auto',
'applies_on': 'current',
'rule_ids': [Command.create({
'reward_point_amount': 1,
'reward_point_mode': 'order',
'minimum_amount': 10,
})],
'reward_ids': [Command.create({
'reward_type': 'discount',
'discount': 10.0,
'discount_applicability': 'order',
'required_points': 1,
})],
}])
self.assertEqual(order.amount_total, 100)
self._auto_rewards(order, promotion_program)
self.assertEqual(order.amount_total, 90)
order.action_confirm()
self.assertEqual(loyalty_card.points, 90)
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
loyalty program applies on all products (no domain).
"""
LoyaltyProgram = self.env['loyalty.program']
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,
'points': 0,
})
order = self.env['sale.order'].with_user(self.user_salemanager).create({
'partner_id': self.partner_a.id,
'order_line': [
Command.create({
'product_id': self.product_A.id,
'tax_id': False,
}),
]
})
self.assertEqual(order.amount_total, 100)
self._apply_promo_code(order, "test_10pc")
self.assertEqual(order.amount_total, 90)
order.action_confirm()
self.assertEqual(loyalty_card.points, 90)
def test_points_awarded_general_discount_code_specific_domain_program(self):
"""
Check the calculation for points awarded when there is a discount coupon applied and the
loyalty program applies on a specific domain. The discount code has no domain. The product
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
self.product_B.list_price = 50
LoyaltyProgram = self.env['loyalty.program']
loyalty_program = LoyaltyProgram.create(LoyaltyProgram._get_template_values()['loyalty'])
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,
'points': 0,
})
order = self.env['sale.order'].with_user(self.user_salemanager).create({
'partner_id': self.partner_a.id,
'order_line': [
Command.create({
'product_id': self.product_A.id,
'tax_id': False,
}),
Command.create({
'product_id': self.product_B.id,
'tax_id': False,
}),
]
})
self.assertEqual(order.amount_total, 150)
self._apply_promo_code(order, "test_10pc")
self.assertEqual(order.amount_total, 135) # (product_A + product_B) * 0.9
order.action_confirm()
self.assertEqual(loyalty_card.points, 100)
def test_points_awarded_specific_discount_code_specific_domain_program(self):
"""
Check the calculation for points awarded when there is a discount coupon applied and the
loyalty program applies on a specific domain. The discount code has the same domain as the
loyalty program. The product related to that discount code is set up to be included in the
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
self.product_B.list_price = 50
LoyaltyProgram = self.env['loyalty.program']
loyalty_program = LoyaltyProgram.create(LoyaltyProgram._get_template_values()['loyalty'])
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,
'points': 0,
})
self.promotion_code_10pc.rule_ids.product_category_id = product_category_food.id
self.promotion_code_10pc.reward_ids.discount_applicability = 'specific'
self.promotion_code_10pc.reward_ids.discount_product_category_id = product_category_food.id
discount_product = self.env['product.product'].search([('id', '=', self.promotion_code_10pc.reward_ids.discount_line_product_id.id)])
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,
'order_line': [
Command.create({
'product_id': self.product_A.id,
'tax_id': False,
}),
Command.create({
'product_id': self.product_B.id,
'tax_id': False,
}),
]
})
self.assertEqual(order.amount_total, 150)
self._apply_promo_code(order, "test_10pc")
self.assertEqual(order.amount_total, 140) # (product_A * 0.9 ) + product_B
order.action_confirm()
self.assertEqual(loyalty_card.points, 90)
def test_points_awarded_ewallet(self):
"""
Check the calculation for point awarded when using ewallet
"""
LoyaltyProgram = self.env['loyalty.program']
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,
'points': 0,
})
order = self.env['sale.order'].with_user(self.user_salemanager).create({
'partner_id': self.partner_a.id,
'order_line': [
Command.create({
'product_id': self.product_A.id,
'tax_id': False,
}),
]
})
self.assertEqual(order.amount_total, 100)
order._update_programs_and_rewards()
self._claim_reward(order, self.ewallet_program, coupon=self.ewallet)
self.assertEqual(order.amount_total, 90)
order.action_confirm()
self.assertEqual(loyalty_card.points, 100)
def test_points_awarded_giftcard(self):
"""
Check the calculation for point awarded when using a gift card
"""
LoyaltyProgram = self.env['loyalty.program']
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,
'points': 0,
})
program_gift_card = self.env['loyalty.program'].create({
'name': "Gift Cards",
'applies_on': 'future',
'program_type': 'gift_card',
'trigger': 'auto',
'reward_ids': [(0, 0, {
'reward_type': 'discount',
'discount': 1,
'discount_mode': 'per_point',
'discount_applicability': 'order',
})]
})
self.env['loyalty.generate.wizard'].with_context(active_id=program_gift_card.id).create({
'coupon_qty': 1,
'points_granted': 50,
}).generate_coupons()
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,
'order_line': [
Command.create({
'product_id': self.product_A.id,
'tax_id': False,
}),
]
})
self.assertEqual(order.amount_total, 100)
self._apply_promo_code(order, gift_card.code)
self.assertEqual(order.amount_total, 50)
order.action_confirm()
self.assertEqual(loyalty_card.points, 100)
def test_multiple_discount_specific(self):
"""
Check the discount calculation if it is based on the remaining amount
"""
product_A = self.env['product.product'].create({
'name': 'Product A',
'list_price': 100,
'sale_ok': True,
'taxes_id': [],
})
coupon_program = self.env['loyalty.program'].create([{
'name': 'Coupon Program',
'program_type': 'promotion',
'trigger': 'auto',
'applies_on': 'current',
'rule_ids': [Command.create({
'reward_point_amount': 1,
'reward_point_mode': 'unit',
})],
'reward_ids': [Command.create({
'reward_type': 'discount',
'discount': 10.0,
'discount_applicability': 'specific',
'required_points': 1,
})],
}])
order = self.env['sale.order'].with_user(self.user_salemanager).create({
'partner_id': self.partner_a.id,
'order_line': [Command.create({
'product_id': product_A.id,
'product_uom_qty': 3,
})]
})
self.assertEqual(float_compare(order.amount_total, 300, precision_rounding=3), 0)
order._update_programs_and_rewards()
self._claim_reward(order, coupon_program)
self.assertEqual(float_compare(order.amount_total, 270, precision_rounding=3), 0, "300 * 0.9 = 270")
order._update_programs_and_rewards()
self._claim_reward(order, coupon_program)
self.assertEqual(float_compare(order.amount_total, 243, precision_rounding=3), 0, "300 * 0.9 * 0.9 = 243")
order._update_programs_and_rewards()
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_specific_promotion_on_free_product(self):
product_A = self.env['product.product'].create({
'name': 'Product A',
'list_price': 100,
'sale_ok': True,
'taxes_id': [],
})
promotion_program = self.env['loyalty.program'].create([{
'name': 'Promotion Program',
'program_type': 'promotion',
'trigger': 'auto',
'applies_on': 'current',
'rule_ids': [Command.create({
'reward_point_amount': 1,
'reward_point_mode': 'unit',
})],
'reward_ids': [Command.create({
'reward_type': 'discount',
'discount': 10.0,
'discount_applicability': 'specific',
'discount_product_ids': [product_A.id],
'required_points': 1,
})],
}])
order = self.env['sale.order'].with_user(self.user_salemanager).create({
'partner_id': self.partner_a.id,
'order_line': [
Command.create({
'product_id': product_A.id,
}),
Command.create({
'product_id': product_A.id,
'discount': 100,
}),
]
})
order._update_programs_and_rewards()
self._claim_reward(order, promotion_program)
self.assertEqual(order.amount_total, 90)
def test_gift_card_program_without_product(self):
product_A = self.env['product.product'].create({
'name': 'Product A',
'list_price': 100,
'sale_ok': True,
'taxes_id': [],
})
giftcard_program = self.env['loyalty.program'].create([{
'name': 'Gift Card Program',
'program_type': 'gift_card',
'trigger': 'auto',
'applies_on': 'current',
'rule_ids': [Command.create({
'reward_point_amount': 1,
'reward_point_mode': 'unit',
})],
}])
order = self.env['sale.order'].with_user(self.user_salemanager).create({
'partner_id': self.partner_a.id,
'order_line': [
Command.create({
'product_id': product_A.id,
}),
]
})
order._update_programs_and_rewards()
self._claim_reward(order, giftcard_program)
self.assertEqual(giftcard_program.coupon_count, 0)
def test_100_percent_discount(self):
"""
Check whether a program offering 100% discount on an order reduces the order's total amount
to zero.
Assumes global tax rounding, as there's no good way to ensure the tax of the reward product
equals the sum of taxes of the lines when each of them gets rounded.
"""
self.env.company.tax_calculation_rounding_method = 'round_globally'
loyalty_program = self.env['loyalty.program'].create([{
'name': 'Full Discount',
'program_type': 'loyalty',
'trigger': 'auto',
'applies_on': 'both',
'rule_ids': [(0, 0, {
'reward_point_mode': 'unit',
'reward_point_amount': 1,
'product_ids': [self.product_a.id],
})],
'reward_ids': [(0, 0, {
'reward_type': 'discount',
'discount': 100,
'discount_mode': 'percent',
'discount_applicability': 'order',
'required_points': 1,
})],
}])
self.env['loyalty.card'].create({
'program_id': loyalty_program.id, 'partner_id': self.partner_a.id, 'points': 2
})
order = self.env['sale.order'].create({
'partner_id': self.partner_a.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)],
})
order._update_programs_and_rewards()
self._claim_reward(order, loyalty_program)
msg = "100% discount on order should reduce total amount to 0"
self.assertEqual(order.amount_total, 0, msg=msg)
def test_discount_on_taxes_with_child_tax(self):
"""
Check whether a program discount properly apply when product contain group of tax.
"""
self.env.company.tax_calculation_rounding_method = 'round_globally'
loyalty_program = self.env['loyalty.program'].create([{
'name': '90% Discount',
'program_type': 'loyalty',
'trigger': 'auto',
'applies_on': 'both',
'rule_ids': [(0, 0, {
'reward_point_mode': 'unit',
'reward_point_amount': 1,
'product_ids': [self.product_a.id],
})],
'reward_ids': [(0, 0, {
'reward_type': 'discount',
'discount': 90,
'discount_mode': 'percent',
'discount_applicability': 'order',
'required_points': 1,
})],
}])
self.env['loyalty.card'].create({'program_id': loyalty_program.id, 'partner_id': self.partner_a.id, 'points': 2})
order = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
'order_line': [(0, 0, {'product_id': self.product_D.id, 'product_uom_qty': 1})],
})
order._update_programs_and_rewards()
self._claim_reward(order, loyalty_program)
msg = "Discountable should take child tax amount into account"
self.assertEqual(order.amount_total, 10, msg=msg)
def test_ewallet_program_without_trigger_product(self):
self.ewallet_program.trigger_product_ids = [Command.clear()]
self.ewallet.points = 1000
order = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
'order_line': [Command.create({
'product_id': self.product_a.id,
'points_cost': 100,
'product_uom_qty': 1,
})],
})
order._update_programs_and_rewards()
self._claim_reward(order, self.ewallet_program, coupon=self.ewallet)
order.action_confirm()
self.assertEqual(self.ewallet.points, 900)
def test_ewallet_applied_ewallet_topup_in_order(self):
self.ewallet.points = 10
order = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
'order_line': [Command.create({
'product_id': self.product_a.id,
'points_cost': 100,
'product_uom_qty': 1,
}), Command.create({
'product_id': self.env.ref('loyalty.ewallet_product_50').id,
'product_uom_qty': 1,
})],
})
order._update_programs_and_rewards()
self._claim_reward(order, self.ewallet_program, coupon=self.ewallet)
order.action_confirm()
self.assertEqual(self.ewallet.points, 50)
def test_archived_reward_products(self):
"""
Check that we do not use loyalty rewards that have no active reward product.
In the case where the reward is based on reward_product_tag_id we also check
the case where at least one reward is active.
"""
LoyaltyProgram = self.env['loyalty.program']
loyalty_program = LoyaltyProgram.create(LoyaltyProgram._get_template_values()['loyalty'])
loyalty_program_tag = LoyaltyProgram.create(LoyaltyProgram._get_template_values()['loyalty'])
free_product_tag = self.env['product.tag'].create({'name': 'Free Product'})
self.product_b.write({'product_tag_ids': [(4, free_product_tag.id)]})
product_c = self.env['product.template'].create(
{
'name': 'Free Product C',
'list_price': 1,
'product_tag_ids': [(4, free_product_tag.id)],
}
)
loyalty_program.reward_ids[0].write({
'reward_type': 'product',
'required_points': 1,
'reward_product_id': self.product_b,
})
loyalty_program_tag.reward_ids[0].write({
'reward_type': 'product',
'required_points': 1,
'reward_product_tag_id': free_product_tag.id,
})
self.product_b.active = False
product_c.active = False
order = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
'order_line': [
Command.create({
'product_id': self.product_a.id,
}),
]
})
order._update_programs_and_rewards()
rewards = [value.ids for value in order._get_claimable_rewards().values()]
self.assertTrue(all(loyalty_program.reward_ids[0].id not in r for r in rewards))
self.assertTrue(all(loyalty_program_tag.reward_ids[0].id not in r for r in rewards))
product_c.active = True
order._update_programs_and_rewards()
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):
"""
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_a.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)

View file

@ -0,0 +1,262 @@
# 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('-at_install', 'post_install')
class TestPayWithGiftCard(TestSaleCouponCommon):
def test_paying_with_single_gift_card_over(self):
self.env['loyalty.generate.wizard'].with_context(active_id=self.program_gift_card.id).create({
'coupon_qty': 1,
'points_granted': 100,
}).generate_coupons()
gift_card = self.program_gift_card.coupon_ids[0]
order = self.empty_order
order.write({'order_line': [
Command.create({
'product_id': self.product_A.id,
'name': 'Ordinary Product A',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
before_gift_card_payment = order.amount_total
self.assertNotEqual(before_gift_card_payment, 0)
self._apply_promo_code(order, gift_card.code)
order.action_confirm()
self.assertEqual(before_gift_card_payment - order.amount_total, 100 - gift_card.points)
def test_paying_with_single_gift_card_under(self):
self.env['loyalty.generate.wizard'].with_context(active_id=self.program_gift_card.id).create({
'coupon_qty': 1,
'points_granted': 100,
}).generate_coupons()
gift_card = self.program_gift_card.coupon_ids[0]
order = self.empty_order
order.write({'order_line': [
Command.create({
'product_id': self.product_B.id,
'name': 'Ordinary Product b',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
before_gift_card_payment = order.amount_total
self.assertNotEqual(before_gift_card_payment, 0)
self._apply_promo_code(order, gift_card.code)
order.action_confirm()
self.assertEqual(before_gift_card_payment - order.amount_total, 100 - gift_card.points)
def test_paying_with_multiple_gift_card(self):
self.env['loyalty.generate.wizard'].with_context(active_id=self.program_gift_card.id).create({
'coupon_qty': 2,
'points_granted': 100,
}).generate_coupons()
gift_card_1, gift_card_2 = self.program_gift_card.coupon_ids
order = self.empty_order
order.write({'order_line': [
Command.create({
'product_id': self.product_A.id,
'name': 'Ordinary Product A',
'product_uom': self.uom_unit.id,
'product_uom_qty': 20.0,
})
]})
before_gift_card_payment = order.amount_total
self._apply_promo_code(order, gift_card_1.code)
self._apply_promo_code(order, gift_card_2.code)
self.assertEqual(order.amount_total, before_gift_card_payment - 200)
def test_paying_with_gift_card_and_discount(self):
# Test that discounts take precedence on payment rewards
self.env['loyalty.generate.wizard'].with_context(active_id=self.program_gift_card.id).create({
'coupon_qty': 1,
'points_granted': 50,
}).generate_coupons()
gift_card_1 = self.program_gift_card.coupon_ids
order = self.empty_order
order.write({'order_line': [
Command.create({
'product_id': self.product_C.id,
'name': 'Ordinary Product C',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
self.env['loyalty.program'].create({
'name': 'Code for 10% on orders',
'trigger': 'with_code',
'program_type': 'promotion',
'applies_on': 'current',
'rule_ids': [(0, 0, {
'mode': 'with_code',
'code': 'test_10pc',
})],
'reward_ids': [(0, 0, {
'reward_type': 'discount',
'discount_mode': 'percent',
'discount': 10,
'discount_applicability': 'order',
'required_points': 1,
})],
})
self.assertEqual(order.amount_total, 100)
self._apply_promo_code(order, gift_card_1.code)
self.assertEqual(order.amount_total, 50)
self._apply_promo_code(order, "test_10pc")
# real flows also have to update the programs and rewards
order._update_programs_and_rewards()
self.assertEqual(order.amount_total, 40) # 100 - 10% - 50
def test_paying_with_gift_card_blocking_discount(self):
# Test that a payment program making the order total 0 still allows the user to claim discounts
self.env['loyalty.generate.wizard'].with_context(active_id=self.program_gift_card.id).create({
'coupon_qty': 1,
'points_granted': 100,
}).generate_coupons()
gift_card_1 = self.program_gift_card.coupon_ids
order = self.empty_order
order.write({'order_line': [
Command.create({
'product_id': self.product_C.id,
'name': 'Ordinary Product C',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
self.env['loyalty.program'].create({
'name': 'Code for 10% on orders',
'trigger': 'with_code',
'program_type': 'promotion',
'applies_on': 'current',
'rule_ids': [(0, 0, {
'mode': 'with_code',
'code': 'test_10pc',
})],
'reward_ids': [(0, 0, {
'reward_type': 'discount',
'discount_mode': 'percent',
'discount': 10,
'discount_applicability': 'order',
'required_points': 1,
})],
})
self.assertEqual(order.amount_total, 100)
self._apply_promo_code(order, gift_card_1.code)
self.assertEqual(order.amount_total, 0)
self._apply_promo_code(order, "test_10pc")
# real flows also have to update the programs and rewards
order._update_programs_and_rewards()
self.assertEqual(order.amount_total, 0) # 100 - 10% - 90
def test_gift_card_product_has_no_taxes_on_creation(self):
gift_card_program = self.env['loyalty.program'].create({
'name': 'Gift Cards',
'applies_on': 'future',
'program_type': 'gift_card',
'trigger': 'auto',
'rule_ids': [Command.create({
'product_ids': self.product_gift_card,
'reward_point_amount': 1,
'reward_point_mode': 'money',
'reward_point_split': True,
})],
'reward_ids': [Command.create({
'reward_type': 'discount',
'discount': 1,
'discount_mode': 'per_point',
'discount_applicability': 'order',
})]
})
self.assertFalse(gift_card_program.reward_ids.discount_line_product_id.taxes_id)
def test_paying_with_gift_card_uses_gift_card_product_taxes(self):
order = self.empty_order
order.order_line = [
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,
})
]
sol = order.order_line
before_gift_card_payment = order.amount_total
self.assertNotEqual(before_gift_card_payment, 0)
self.env['loyalty.generate.wizard'].with_context(active_id=self.program_gift_card.id).create({
'coupon_qty': 1,
'points_granted': 100,
}).generate_coupons()
gift_card = self.program_gift_card.coupon_ids[0]
# TODO check amount total of gift_card_line
# TAX EXCL
self.program_gift_card.reward_ids.discount_line_product_id.taxes_id = [
Command.link(self.tax_15pc_excl.id)
]
self._apply_promo_code(order, gift_card.code)
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)
# TAX INCL
gift_card_line.unlink() # Remove gift card
self.program_gift_card.reward_ids.discount_line_product_id.taxes_id = [
Command.set(self.tax_10pc_incl.ids)
]
self._apply_promo_code(order, gift_card.code)
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)
# TAX INCL + TAX EXCL
gift_card_line.unlink() # Remove gift card
self.program_gift_card.reward_ids.discount_line_product_id.taxes_id = [
Command.link(self.tax_15pc_excl.id)
]
self._apply_promo_code(order, gift_card.code)
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)
def test_paying_with_gift_card_fixed_tax(self):
""" Test payment of sale order with fixed tax using gift card """
self.env['loyalty.generate.wizard'].with_context(active_id=self.program_gift_card.id).create({
'coupon_qty': 1,
'points_granted': 100,
}).generate_coupons()
gift_card = self.program_gift_card.coupon_ids[0]
tax_10_fixed = self.env['account.tax'].create({
'name': "10$ Fixed tax",
'amount_type': 'fixed',
'amount': 10,
})
self.product_A.write({'list_price': 90})
self.product_A.taxes_id = tax_10_fixed
order = self.empty_order
order.write({'order_line': [
Command.create({
'product_id': self.product_A.id,
'name': "Ordinary Product A",
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
self._apply_promo_code(order, gift_card.code)
order.action_confirm()
self.assertEqual(order.amount_total, 0, "The order should be totally paid")

View file

@ -0,0 +1,114 @@
# -*- 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 import Command
@tagged('post_install', '-at_install')
class TestSaleCouponMultiCompany(TestSaleCouponCommon):
def setUp(self):
super(TestSaleCouponMultiCompany, self).setUp()
self.company_a = self.env.company
self.company_b = self.env['res.company'].create(dict(name="TEST"))
self.immediate_promotion_program_c2 = self.env['loyalty.program'].create({
'name': 'Buy A + 1 B, 1 B are free',
'trigger': 'auto',
'program_type': 'promotion',
'applies_on': 'current',
'company_id': self.company_b.id,
'rule_ids': [(0, 0, {
'product_ids': self.product_A,
'reward_point_amount': 1,
'reward_point_mode': 'order',
})],
'reward_ids': [(0, 0, {
'reward_type': 'product',
'reward_product_id': self.product_B.id,
'reward_product_qty': 1,
'required_points': 1,
})],
})
def _get_applicable_programs(self, order):
return self.env['loyalty.program'].browse(p.id for p in order._get_applicable_program_points())
def test_applicable_programs(self):
order = self.empty_order
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': 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,
})
]})
order._update_programs_and_rewards()
self.assertNotIn(self.immediate_promotion_program_c2, self._get_applicable_programs(order))
self.assertNotIn(self.immediate_promotion_program_c2, order._get_applied_programs())
order_b = self.env["sale.order"].create({
'company_id': self.company_b.id,
'partner_id': order.partner_id.id,
})
order_b.write({'order_line': [
(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,
})
]})
self.assertNotIn(self.immediate_promotion_program, self._get_applicable_programs(order_b))
order_b._update_programs_and_rewards()
self.assertIn(self.immediate_promotion_program_c2, order_b._get_applied_programs())
self.assertNotIn(self.immediate_promotion_program, order_b._get_applied_programs())
def test_applicable_programs_on_branch(self):
# create a branch
branch_a = self.env['res.company'].create(
{'name': 'Branch A', 'parent_id': self.company_a.id}
)
# create an order
order = self.env['sale.order'].create(
{'order_line': [
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
}
)
order._update_programs_and_rewards()
self.assertIn(self.immediate_promotion_program, order._get_applied_programs())

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,361 @@
# -*- 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
class TestProgramRules(TestSaleCouponCommon):
# 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
def test_program_rules_minimum_purchased_amount(self):
# Test case: Based on the minimum purchased
self.immediate_promotion_program.rule_ids.write({
'product_ids': False,
'minimum_amount': 1006,
'minimum_amount_tax_mode': 'excl'
})
order = self.empty_order
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': 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,
})
]})
order._update_programs_and_rewards()
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.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,
})
]})
order._update_programs_and_rewards()
self._claim_reward(order, self.immediate_promotion_program)
# 10*100 + 5 = 1005
self.assertEqual(len(order.order_line.ids), 2, "The promo offer should not be applied as the purchased amount is not enough")
self.immediate_promotion_program.rule_ids.minimum_amount = 1005
order._update_programs_and_rewards()
self._claim_reward(order, self.immediate_promotion_program)
self.assertEqual(len(order.order_line.ids), 3, "The promo offer should be applied as the purchased amount is now enough")
# 10*(100*1.15) + (5*1.15) = 10*115 + 5.75 = 1155.75
self.immediate_promotion_program.rule_ids.minimum_amount = 1006
self.immediate_promotion_program.rule_ids.minimum_amount_tax_mode = 'incl'
order._update_programs_and_rewards()
self._claim_reward(order, self.immediate_promotion_program)
self.assertEqual(len(order.order_line.ids), 3, "The promo offer should be applied as the initial amount required is now tax included")
def test_program_rules_min_amount_not_reached_and_specific_product(self):
"""
Test that the discount isn't applied if the min amount isn't reached for the specified
product.
"""
self.env['loyalty.program'].search([]).active = False
order = self.empty_order
program = self.env['loyalty.program'].create({
'name': "Discount on Product A",
'program_type': 'promotion',
'trigger': 'auto',
'applies_on': 'current',
'rule_ids': [Command.create({
'minimum_amount': 110,
'minimum_amount_tax_mode': 'excl',
'product_ids': [Command.set(self.product_A.ids)],
})],
'reward_ids': [Command.create({
'reward_type': 'discount',
'discount': 10,
'discount_mode': 'percent',
'discount_applicability': 'specific',
'discount_product_ids': [Command.set(self.product_A.ids)],
})],
})
self.env['sale.order.line'].create([{
'product_id': self.product_A.id,
'product_uom_qty': 1.0,
'order_id': order.id,
}, {
'product_id': self.product_B.id,
'product_uom_qty': 40.0,
'order_id': order.id,
}])
self.assertEqual(len(order.order_line), 2)
self.assertEqual(order.amount_untaxed, 300)
order._update_programs_and_rewards()
self._claim_reward(order, program)
self.assertEqual(len(order.order_line), 2)
self.assertEqual(order.amount_untaxed, 300)
def test_program_rules_min_amount_reached_and_specific_product(self):
"""
Test that the discount is applied if the min amount is reached for the specified product.
"""
self.env['loyalty.program'].search([]).active = False
order = self.empty_order
program = self.env['loyalty.program'].create({
'name': "Discount on Product A",
'program_type': 'promotion',
'trigger': 'auto',
'applies_on': 'current',
'rule_ids': [Command.create({
'minimum_amount': 110,
'minimum_amount_tax_mode': 'excl',
'product_ids': [Command.set(self.product_A.ids)],
})],
'reward_ids': [Command.create({
'reward_type': 'discount',
'discount': 10,
'discount_mode': 'percent',
'discount_applicability': 'specific',
'discount_product_ids': [Command.set(self.product_A.ids)],
})],
})
self.env['sale.order.line'].create([{
'product_id': self.product_A.id,
'product_uom_qty': 2.0,
'order_id': order.id,
}, {
'product_id': self.product_B.id,
'product_uom_qty': 20.0,
'order_id': order.id,
}])
self.assertEqual(len(order.order_line), 2)
self.assertEqual(order.amount_untaxed, 300)
order._update_programs_and_rewards()
self._claim_reward(order, program)
self.assertEqual(len(order.order_line), 3)
self.assertEqual(order.amount_untaxed, 280)
def test_program_rules_coupon_qty_and_amount_remove_not_eligible(self):
''' This test will:
* Check quantity and amount requirements works as expected (since it's slightly different from a promotion_program)
* Ensure that if a reward from a coupon_program was allowed and the conditions are not met anymore,
the reward will be removed on recompute.
'''
self.immediate_promotion_program.active = False # Avoid having this program to add rewards on this test
order = self.empty_order
program = self.env['loyalty.program'].create({
'name': 'Get 10% discount if buy at least 4 Product A and $320',
'program_type': 'coupons',
'applies_on': 'current',
'trigger': 'with_code',
'rule_ids': [(0, 0, {
'product_ids': self.product_A,
'minimum_qty': 3,
'minimum_amount': 320,
})],
'reward_ids': [(0, 0, {
'reward_type': 'discount',
'discount_mode': 'percent',
'discount': 10,
'discount_applicability': 'order',
})],
})
sol1 = self.env['sale.order.line'].create({
'product_id': self.product_A.id,
'name': 'Product A',
'product_uom_qty': 2.0,
'order_id': order.id,
})
sol2 = self.env['sale.order.line'].create({
'product_id': self.product_B.id,
'name': 'Product B',
'product_uom_qty': 4.0,
'order_id': order.id,
})
# Default value for coupon generate wizard is generate by quantity and generate only one coupon
self.env['loyalty.generate.wizard'].with_context(active_id=program.id).create({'coupon_qty': 1, 'points_granted': 1}).generate_coupons()
coupon = program.coupon_ids[0]
# Not enough amount since we only have 220 (100*2 + 5*4)
with self.assertRaises(ValidationError):
self._apply_promo_code(order, coupon.code)
sol2.product_uom_qty = 24
# Not enough qty since we only have 3 Product A (Amount is ok: 100*2 + 5*24 = 320)
with self.assertRaises(ValidationError):
self._apply_promo_code(order, coupon.code)
sol1.product_uom_qty = 3
self._apply_promo_code(order, coupon.code)
self._claim_reward(order, program, coupon)
self.assertEqual(len(order.order_line.ids), 3, "The order should contain the Product A line, the Product B line and the discount line")
sol1.product_uom_qty = 2
order._update_programs_and_rewards()
self.assertEqual(len(order.order_line.ids), 2, "The discount line should have been removed as we don't meet the program requirements")
def test_program_rules_promotion_use_best(self):
''' This test verifies that only the best global discount is applied.
'''
self.immediate_promotion_program.active = False # Avoid having this program to add rewards on this test
order = self.empty_order
p1 = self.env['loyalty.program'].create({
'name': 'Get 5% discount if buy at least 2 Product',
'trigger': 'auto',
'program_type': 'promotion',
'applies_on': 'current',
'rule_ids': [(0, 0, {
'reward_point_mode': 'order',
'minimum_qty': 2,
})],
'reward_ids': [(0, 0, {
'reward_type': 'discount',
'discount': 5,
'discount_mode': 'percent',
'discount_applicability': 'order',
'required_points': 1,
})],
})
p2 = self.env['loyalty.program'].create({
'name': 'Get 10% discount if buy at least 4 Product',
'trigger': 'auto',
'program_type': 'promotion',
'applies_on': 'current',
'rule_ids': [(0, 0, {
'reward_point_mode': 'order',
'minimum_qty': 4,
})],
'reward_ids': [(0, 0, {
'reward_type': 'discount',
'discount': 10,
'discount_mode': 'percent',
'discount_applicability': 'order',
'required_points': 1,
})],
})
sol = self.env['sale.order.line'].create({
'product_id': self.product_A.id,
'name': 'Product A',
'product_uom_qty': 1.0,
'order_id': order.id,
})
order._update_programs_and_rewards()
self._claim_reward(order, p1)
self._claim_reward(order, p2)
self.assertEqual(len(order.order_line.ids), 1, "The order should only contains the Product A line")
sol.product_uom_qty = 3
order._update_programs_and_rewards()
self._claim_reward(order, p1)
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")
# 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")
sol.product_uom_qty = 5
order._update_programs_and_rewards()
self._claim_reward(order, p1)
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")
def test_program_rules_validity_dates_and_uses(self):
# Test case: Based on the validity dates and the number of allowed uses
self.immediate_promotion_program.write({
'date_to': date.today() - timedelta(days=2),
'limit_usage': True,
'max_usage': 1,
})
order = self.empty_order
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': 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.immediate_promotion_program.write({
'date_to': date.today() + timedelta(days=2),
})
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, {
'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")

View file

@ -0,0 +1,333 @@
# -*- 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
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.
def test_program_usability(self):
# After clicking "Generate coupons", there is no domain so it shows "Match all records".
# But when you click, domain is false (default field value; empty string) so it won't generate anything.
# This is even more weird because if you add something in the domain and then delete it,
# you visually come back to the initial state except the domain became '[]' instead of ''.
# In this case, it will generate the coupon for every partner.
# Thus, we should ensure that if you leave the domain untouched, it generates a coupon for each partner
# as hinted on the screen ('Match all records (X records)')
self.env['loyalty.generate.wizard'].with_context(active_id=self.code_promotion_program.id).create({
'mode': 'selected',
}).generate_coupons()
self.assertEqual(len(self.code_promotion_program.coupon_ids), len(self.env['res.partner'].search([])), "It should have generated a coupon for every partner")
def test_program_basic_operation_coupon_code(self):
# Test case: Generate a coupon for my customer, and add a reward then remove it automatically
self.immediate_promotion_program.active = False
self.code_promotion_program.reward_ids.reward_type = 'discount'
self.code_promotion_program.reward_ids.discount = 10
self.env['loyalty.generate.wizard'].with_context(active_id=self.code_promotion_program.id).create({
'mode': 'selected',
'customer_ids': self.steve,
'points_granted': 1,
}).generate_coupons()
coupon = self.code_promotion_program.coupon_ids
# Test the valid code on a wrong sales order
wrong_partner_order = self.env['sale.order'].create({
'partner_id': self.env['res.partner'].create({'name': 'My Partner'}).id,
})
with self.assertRaises(ValidationError):
self._apply_promo_code(wrong_partner_order, coupon.code)
# Test now on a valid sales order
order = self.empty_order
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': 1.0,
})
]})
self._apply_promo_code(order, coupon.code)
self.assertEqual(len(order.order_line.ids), 2)
# Remove the product A from the sale order
order.write({'order_line': [(2, order.order_line[0].id, False)]})
order._update_programs_and_rewards()
self.assertEqual(len(order.order_line.ids), 0)
def test_program_coupon_double_consuming(self):
# Test case:
# - Generate a coupon
# - add to a sale order A, cancel the sale order
# - add to a sale order B, confirm the order
# - go back to A, reset to draft and confirm
self.immediate_promotion_program.active = False
self.code_promotion_program.applies_on = 'future'
self.code_promotion_program.reward_ids.reward_type = 'discount'
self.code_promotion_program.reward_ids.discount = 10
self.env['loyalty.generate.wizard'].with_context(active_id=self.code_promotion_program.id).create({
'coupon_qty': 1,
'points_granted': 1,
}).generate_coupons()
coupon = self.code_promotion_program.coupon_ids
sale_order_a = self.empty_order.copy()
sale_order_b = self.empty_order.copy()
sale_order_a.write({'order_line': [
(0, False, {
'product_id': self.product_A.id,
'name': '1 Product A',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
self._apply_promo_code(sale_order_a, coupon.code)
self.assertEqual(len(sale_order_a.order_line.ids), 2)
sale_order_a._action_cancel()
sale_order_b.write({'order_line': [
(0, False, {
'product_id': self.product_A.id,
'name': '1 Product A',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
self._apply_promo_code(sale_order_b, coupon.code)
self.assertEqual(len(sale_order_b.order_line.ids), 2)
sale_order_b.action_confirm()
sale_order_a.action_draft()
sale_order_a.action_confirm()
# reward line removed automatically
self.assertEqual(len(sale_order_a.order_line.ids), 1)
def test_coupon_code_with_pricelist(self):
# Test case: Generate a coupon (10% discount) and apply it on an order with a specific pricelist (10% discount)
self.code_promotion_program_with_discount.applies_on = 'future'
self.env['loyalty.generate.wizard'].with_context(active_id=self.code_promotion_program_with_discount.id).create({
'coupon_qty': 1,
'points_granted': 1,
}).generate_coupons()
coupon = self.code_promotion_program_with_discount.coupon_ids
first_pricelist = self.env['product.pricelist'].create({
'name': 'First pricelist',
'discount_policy': 'with_discount',
'item_ids': [(0, 0, {
'compute_price': 'percentage',
'base': 'list_price',
'percent_price': 10,
'applied_on': '3_global',
'name': 'First discount'
})]
})
order = self.empty_order
order.pricelist_id = first_pricelist
order.write({'order_line': [
(0, False, {
'product_id': self.product_C.id,
'name': '1 Product C',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
self._apply_promo_code(order, coupon.code)
self.assertEqual(len(order.order_line.ids), 2)
self.assertEqual(order.amount_total, 81, "SO total should be 81: (10% of 100 with pricelist) + 10% of 90 with coupon code")
def test_on_next_order_reward_promotion_program(self):
# The flow:
# 1. Create a program `A` that gives a free `Product B` on next order if you buy a an `product A`
# This program should be code_needed with code `free_B_on_next_order`
# 2. Create a program `B` that gives 10% discount on next order automatically
# 3. Create a SO with a `third product` and recompute coupon, you SHOULD get a coupon (from program `B`) for your next order that will discount 10%
# 4. Try to apply `A`, it should error since we did not buy any product A.
# 5. Add a product A to the cart and try to apply `A` again, this time it should work
# 6. Verify you have 2 generated coupons and validate the SO (so the 2 generated coupons will be valid)
# 7. Create a new SO (with the same partner)
# 8. Add a Product B in the cart
# 9. Try to apply once again coupon generated by `A`, it should give you the free product B
# 10. Try to apply coupon generated by `B`, it should give you 10% discount.
# => SO will then be 0$ until we recompute the order lines
# 1.
self.immediate_promotion_program.write({
'applies_on': 'future',
'trigger': 'with_code',
})
self.immediate_promotion_program.rule_ids.write({
'mode': 'with_code',
'code': 'free_B_on_next_order',
})
# 2.
self.p1 = self.env['loyalty.program'].create({
'name': 'Code for 10% on next order',
'program_type': 'promotion',
'applies_on': 'future',
'trigger': 'auto',
'rule_ids': [(0, 0, {})],
'reward_ids': [(0, 0, {
'reward_type': 'discount',
'discount': 10,
'discount_mode': 'percent',
'discount_applicability': 'order',
})],
})
# 3.
order = self.empty_order.copy()
self.third_product = self.env['product.product'].create({
'name': 'Thrid Product',
'list_price': 5,
'sale_ok': True
})
order.write({'order_line': [
(0, False, {
'product_id': self.third_product.id,
'name': '1 Third Product',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
order._update_programs_and_rewards()
self.assertEqual(len(self.p1.coupon_ids.ids), 1, "You should get a coupon for you next order that will offer 10% discount")
# 4.
with self.assertRaises(ValidationError):
self._apply_promo_code(order, 'free_B_on_next_order')
# 5.
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': 1.0,
})
]})
self._apply_promo_code(order, 'free_B_on_next_order', no_reward_fail=False)
# 6.
self.assertEqual(len(order._get_reward_coupons()), 2, "You should get a second coupon for your next order that will offer a free Product B")
order.action_confirm()
# 7.
order_bis = self.empty_order
# 8.
order_bis.write({'order_line': [
(0, False, {
'product_id': self.product_B.id,
'name': '1 Product B',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
# 9.
self._apply_promo_code(order_bis, order._get_reward_coupons()[1].code)
self.assertEqual(len(order_bis.order_line), 2, "You should get a free Product B")
# 10.
self._apply_promo_code(order_bis, order._get_reward_coupons()[0].code)
self.assertEqual(len(order_bis.order_line), 3, "You should get a 10% discount line")
self.assertAlmostEqual(order_bis.amount_total, order_bis.order_line[0].price_total * 0.9, 2, "SO total should be null: (Paid product - Free product = 0) + 10% of nothing")
def test_on_next_order_reward_promotion_program_with_requirements(self):
self.immediate_promotion_program.write({
'applies_on': 'future',
'trigger': 'with_code',
})
self.immediate_promotion_program.rule_ids.write({
'minimum_amount': 700,
'minimum_amount_tax_mode': 'excl',
'mode': 'with_code',
'code': 'free_B_on_next_order',
})
order = self.empty_order.copy()
self.product_A.lst_price = 700
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': 1.0,
})
]})
self._apply_promo_code(order, 'free_B_on_next_order', no_reward_fail=False)
self.assertEqual(len(self.immediate_promotion_program.coupon_ids.ids), 1, "You should get a coupon for you next order that will offer a free product B")
order_bis = self.empty_order
order_bis.write({'order_line': [
(0, False, {
'product_id': self.product_B.id,
'name': '1 Product B',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
with self.assertRaises(ValidationError):
# It should error since we did not validate the previous SO, so the coupon is `reserved` but not `new`
self._apply_promo_code(order_bis, order._get_reward_coupons()[0].code)
order.action_confirm()
# It should not error even if the SO does not have the requirements (700$ and 1 product A), since these requirements where only used to generate the coupon that we are now applying
self._apply_promo_code(order_bis, order._get_reward_coupons()[0].code, no_reward_fail=False)
self.assertEqual(len(order_bis.order_line), 2, "You should get 1 regular product_B and 1 free product_B")
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_edit_and_reapply_promotion_program(self):
# The flow:
# 1. Create a program auto applied, giving a fixed amount discount
# 2. Create a SO and apply the program
# 3. Change the program, requiring a mandatory code
# 4. Reapply the program on the same SO via code
self.immediate_promotion_program.active = False
# 1.
self.p1 = self.env['loyalty.program'].create({
'name': 'Promo fixed amount',
'trigger': 'auto',
'program_type': 'promotion',
'rule_ids': [(0, 0, {})],
'reward_ids': [(0, 0, {
'reward_type': 'discount',
'discount': 10,
'discount_mode': 'per_point',
'discount_applicability': 'order',
})]
})
# 2.
order = self.empty_order.copy()
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': 1.0,
})
]})
order._update_programs_and_rewards()
self._claim_reward(order, self.p1)
self.assertEqual(len(order.order_line), 2, "You should get a discount line") # product + discount
# 3.
self.p1.write({
'trigger': 'with_code',
})
self.p1.rule_ids.write({
'mode': 'with_code',
'code': 'test',
})
order._update_programs_and_rewards()
self.assertEqual(len(order.order_line), 1, "You loose a discount line")
# 4.
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

View file

@ -0,0 +1,64 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.sale_loyalty.tests.common import TestSaleCouponCommon
class TestProgramWithoutCodeOperations(TestSaleCouponCommon):
# Test some basic operation (create, write, unlink) on an immediate coupon program on which we should
# apply or remove the reward automatically, as there's no program code.
def test_immediate_program_basic_operation(self):
# 2 products A are needed
self.immediate_promotion_program.rule_ids.write({'minimum_qty': 2.0})
order = self.empty_order
# Test case 1 (1 A): Assert that no reward is given, as the product B is missing
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': 1.0,
})
]})
order._update_programs_and_rewards()
self._claim_reward(order, self.immediate_promotion_program)
self.assertEqual(len(order.order_line.ids), 1, "The promo offer shouldn't have been applied as the product B isn't in the order")
# Test case 2 (1 A 1 B): Assert that no reward is given, as the product A is not present in the correct quantity
order.write({'order_line': [
(0, False, {
'product_id': self.product_B.id,
'name': '2 Product B',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
order._update_programs_and_rewards()
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 2 product A aren't in the order")
# Test case 3 (2 A 1 B): Assert that the reward is given as the product B is now in the order
order.write({'order_line': [(1, order.order_line[0].id, {'product_uom_qty': 2.0})]})
order._update_programs_and_rewards()
self._claim_reward(order, self.immediate_promotion_program)
self.assertEqual(len(order.order_line.ids), 3, "The promo offer should have been applied, the discount is not created")
# Test case 4 (1 A 1 B): Assert that the reward is removed as we don't buy 2 products B anymore
order.write({'order_line': [(1, order.order_line[0].id, {'product_uom_qty': 1.0})]})
order._update_programs_and_rewards()
self._claim_reward(order, self.immediate_promotion_program)
self.assertEqual(len(order.order_line.ids), 2, "The promo reward should have been removed as the rules are not matched anymore")
self.assertEqual(order.order_line[0].product_id.id, self.product_A.id, "The wrong line has been removed")
self.assertEqual(order.order_line[1].product_id.id, self.product_B.id, "The wrong line has been removed")
# Test case 5 (1 B): Assert that the reward is removed when the order is modified and doesn't match the rules anymore
order.write({'order_line': [
(1, order.order_line[0].id, {'product_uom_qty': 2.0}),
(2, order.order_line[0].id, False)
]})
order._update_programs_and_rewards()
self._claim_reward(order, self.immediate_promotion_program)
self.assertEqual(len(order.order_line.ids), 1, "The promo reward should have been removed as the rules are not matched anymore")
self.assertEqual(order.order_line.product_id.id, self.product_B.id, "The wrong line has been removed")

View file

@ -0,0 +1,120 @@
# -*- 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
@tagged('post_install', '-at_install')
class TestSaleInvoicing(TestSaleCouponCommon):
def test_invoicing_order_with_promotions(self):
discount_coupon_program = self.env['loyalty.program'].create({
'name': '10% Discount',
'program_type': 'coupons',
'applies_on': 'current',
'trigger': 'auto',
'rule_ids': [(0, 0, {})],
'reward_ids': [(0, 0, {
'reward_type': 'discount',
'discount': 10,
'discount_mode': 'percent',
'discount_applicability': 'order',
})]
})
product = self.env['product.product'].create({
'invoice_policy': 'delivery',
'name': 'Product invoiced on delivery',
'lst_price': 500,
})
order = self.empty_order
order.write({
'order_line': [
(0, 0, {
'product_id': product.id,
})
]
})
#Check default invoice_policy on discount product
self.assertEqual(discount_coupon_program.reward_ids.discount_line_product_id.invoice_policy, 'order')
order._update_programs_and_rewards()
self._claim_reward(order, discount_coupon_program)
# Order is not confirmed, there shouldn't be any invoiceable line
invoiceable_lines = order._get_invoiceable_lines()
self.assertEqual(len(invoiceable_lines), 0)
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()
self.assertEqual(order.invoice_status, 'no')
self.assertEqual(len(invoiceable_lines), 0)
with self.assertRaises(UserError):
order._create_invoices()
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)
account_move = order._create_invoices()
self.assertEqual(len(account_move.invoice_line_ids), 2)
def test_coupon_on_order_sequence(self):
discount_coupon_program = self.env['loyalty.program'].create({
'name': '10% Discount',
'program_type': 'coupons',
'applies_on': 'current',
'trigger': 'auto',
'rule_ids': [(0, 0, {})],
'reward_ids': [(0, 0, {
'reward_type': 'discount',
'discount': 10,
'discount_mode': 'percent',
'discount_applicability': 'order',
})]
})
order = self.empty_order
product_6 = self.env['product.product'].create({
'name': 'Large Cabinet',
})
# orderline1
self.env['sale.order.line'].create({
'product_id': product_6.id,
'name': 'largeCabinet',
'product_uom_qty': 1.0,
'order_id': order.id,
})
#Check default invoice_policy on discount product
self.assertEqual(discount_coupon_program.reward_ids.discount_line_product_id.invoice_policy, 'order')
self._auto_rewards(order, discount_coupon_program)
self.assertEqual(len(order.order_line), 2, 'Coupon correctly applied')
product_11 = self.env['product.product'].create({
'name': 'Conference Chair',
})
# orderline2
self.env['sale.order.line'].create({
'product_id': product_11.id,
'name': 'conferenceChair',
'product_uom_qty': 1.0,
'order_id': order.id,
})
order._update_programs_and_rewards()
self.assertEqual(len(order.order_line), 3, 'Coupon correctly applied')
self.assertTrue(order.order_line.sorted(lambda x: x.sequence)[-1].is_reward_line, 'Global coupons appear on the last line')

View file

@ -0,0 +1,55 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import Command
from odoo.addons.sale_loyalty.tests.common import TestSaleCouponCommon
from odoo.tests.common import tagged
@tagged('-at_install', 'post_install')
class TestUnlinkReward(TestSaleCouponCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.promotion_program = cls.env['loyalty.program'].create({
'name': 'Buy A + 1 B, 1 B are free',
'program_type': 'promotion',
'applies_on': 'current',
'company_id': cls.env.company.id,
'trigger': 'auto',
'rule_ids': [Command.create({
'product_ids': cls.product_A,
'reward_point_amount': 1,
'reward_point_mode': 'order',
'minimum_qty': 1,
})],
})
cls.reward = cls.env['loyalty.reward'].create({
'program_id': cls.promotion_program.id,
'reward_type': 'discount',
})
def test_sale_unlink_reward(self):
order = self.empty_order
order.write({'order_line': [
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,
}),
]})
order._update_programs_and_rewards()
self._claim_reward(order, self.promotion_program)
self.reward.unlink()
# Check that the reward is archived and not deleted
self.assertTrue(self.reward.exists())
self.assertFalse(self.reward.active)