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,8 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_apply_pending_coupon
from . import test_free_product_reward
from . import test_sale_coupon_multiwebsite
from . import test_shop_loyalty_payment
from . import test_shop_multi_reward
from . import test_shop_sale_coupon

View file

@ -0,0 +1,89 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import http
from odoo.tests import tagged, HttpCase
from odoo.addons.sale_loyalty.tests.common import TestSaleCouponNumbersCommon
from odoo.addons.website.tools import MockRequest
from odoo.addons.website_sale_loyalty.controllers.main import WebsiteSale
@tagged('-at_install', 'post_install')
class TestSaleCouponApplyPending(HttpCase, TestSaleCouponNumbersCommon):
def setUp(self):
super().setUp()
self.WebsiteSaleController = WebsiteSale()
self.website = self.env['website'].browse(1)
self.global_program = self.p1
self.coupon_program = self.env['loyalty.program'].create({
'name': 'One Free Product',
'program_type': 'coupons',
'rule_ids': [(0, 0, {
'minimum_qty': 2,
})],
'reward_ids': [(0, 0, {
'reward_type': 'product',
'reward_product_id': self.largeCabinet.id,
})]
})
self.env['loyalty.generate.wizard'].with_context(active_id=self.coupon_program.id).create({
'coupon_qty': 1,
'points_granted': 1,
}).generate_coupons()
self.coupon = self.coupon_program.coupon_ids[0]
installed_modules = set(self.env['ir.module.module'].search([
('state', '=', 'installed'),
]).mapped('name'))
for _ in http._generate_routing_rules(installed_modules, nodb_only=False):
pass
def test_01_activate_coupon_with_existing_program(self):
order = self.empty_order
self.env['product.pricelist.item'].search([]).unlink()
with MockRequest(self.env, website=self.website, sale_order_id=order.id, website_sale_current_pl=1) as request:
self.WebsiteSaleController.cart_update_json(self.largeCabinet.id, set_qty=2)
self.WebsiteSaleController.pricelist(self.global_program.rule_ids.code)
self.assertEqual(order.amount_total, 576, "The order total should equal 576: 2*320 - 10% discount ")
self.assertEqual(len(order.order_line), 2, "There should be 2 lines 1 for the product and 1 for the discount")
self.WebsiteSaleController.activate_coupon(self.coupon.code)
promo_code = request.session.get('pending_coupon_code')
self.assertFalse(promo_code, "The promo code should be removed from the pending coupon dict")
self.assertEqual(order.amount_total, 576, "The order total should equal 576: 2*320 - 0 (free product) - 10%")
self.assertEqual(len(order.order_line), 3, "There should be 3 lines 1 for the product, 1 for the free product and 1 for the discount")
def test_02_pending_coupon_with_existing_program(self):
order = self.empty_order
self.env['product.pricelist.item'].search([]).unlink()
with MockRequest(self.env, website=self.website, sale_order_id=order.id, website_sale_current_pl=1) as request:
self.WebsiteSaleController.cart_update_json(self.largeCabinet.id, set_qty=1)
self.WebsiteSaleController.pricelist(self.global_program.rule_ids.code)
self.assertEqual(self.largeCabinet.lst_price, 320)
cabinet_sol = order.order_line.filtered(lambda sol: sol.product_id == self.largeCabinet)
promo_sol = (order.order_line - cabinet_sol)
self.assertTrue(cabinet_sol)
self.assertEqual(cabinet_sol.price_unit, 320)
self.assertEqual(cabinet_sol.price_total, 320)
self.assertEqual(promo_sol.price_total, -32)
self.assertEqual(order.amount_tax, 0)
self.assertEqual(order.cart_quantity, 1)
self.assertEqual(order.amount_total, 288, "The order total should equal 288: 320 - 10%")
self.WebsiteSaleController.activate_coupon(self.coupon.code)
promo_code = request.session.get('pending_coupon_code')
self.assertEqual(order.amount_tax, 0)
self.assertEqual(order.cart_quantity, 1)
self.assertEqual(order.amount_total, 288, "The order total should still equal 288 as the coupon for free product can't be applied since it requires 2 min qty")
self.assertEqual(promo_code, self.coupon.code, "The promo code should be set in the pending coupon dict as it couldn't be applied, we save it for later reuse")
self.WebsiteSaleController.cart_update_json(self.largeCabinet.id, add_qty=1)
promo_code = request.session.get('pending_coupon_code')
self.assertFalse(promo_code, "The promo code should be removed from the pending coupon dict as it should have been applied")
self.assertEqual(order.amount_tax, 0)
self.assertEqual(order.cart_quantity, 2)
self.assertEqual(order.amount_total, 576, "The order total should equal 576: 2*320 - 0 (free product) - 10%")

View file

@ -0,0 +1,81 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import http
from odoo.tests.common import HttpCase
from odoo.tests import tagged
from odoo.addons.website.tools import MockRequest
from odoo.addons.website_sale_loyalty.controllers.main import WebsiteSale
@tagged('post_install', '-at_install')
class TestFreeProductReward(HttpCase):
def setUp(self):
super().setUp()
self.WebsiteSaleController = WebsiteSale()
self.website = self.env['website'].browse(1)
self.sofa = self.env['product.product'].create({
'name': 'Test Sofa',
'list_price': 2950.0,
'website_published': True,
})
self.carpet = self.env['product.product'].create({
'name': 'Test Carpet',
'list_price': 500.0,
'website_published': True,
})
# Disable any other program
self.program = self.env['loyalty.program'].search([]).write({'active': False})
self.program = self.env['loyalty.program'].create({
'name': 'Get a product for free',
'program_type': 'promotion',
'applies_on': 'current',
'trigger': 'auto',
'rule_ids': [(0, 0, {
'minimum_qty': 1,
'minimum_amount': 0.00,
'reward_point_amount': 1,
'reward_point_mode': 'order',
'product_ids': self.sofa,
})],
'reward_ids': [(0, 0, {
'reward_type': 'product',
'reward_product_id': self.carpet.id,
'reward_product_qty': 1,
'required_points': 1,
})],
})
self.steve = self.env['res.partner'].create({
'name': 'Steve Bucknor',
'email': 'steve.bucknor@example.com',
})
self.empty_order = self.env['sale.order'].create({
'partner_id': self.steve.id
})
installed_modules = set(self.env['ir.module.module'].search([
('state', '=', 'installed'),
]).mapped('name'))
for _ in http._generate_routing_rules(installed_modules, nodb_only=False):
pass
def test_add_product_to_cart_when_it_exist_as_free_product(self):
# This test the flow when we claim a reward in the cart page and then we
# want to add the product again
order = self.empty_order
with MockRequest(self.env, website=self.website, sale_order_id=order.id, website_sale_current_pl=1):
self.WebsiteSaleController.cart_update_json(self.sofa.id, set_qty=1)
self.WebsiteSaleController.claim_reward(self.program.reward_ids[0].id)
self.WebsiteSaleController.cart_update_json(self.carpet.id, set_qty=1)
sofa_line = order.order_line.filtered(lambda line: line.product_id.id == self.sofa.id)
carpet_reward_line = order.order_line.filtered(lambda line: line.product_id.id == self.carpet.id and line.is_reward_line)
carpet_line = order.order_line.filtered(lambda line: line.product_id.id == self.carpet.id and not line.is_reward_line)
self.assertEqual(sofa_line.product_uom_qty, 1, "Should have only 1 qty of Sofa")
self.assertEqual(carpet_reward_line.product_uom_qty, 1, "Should have only 1 qty for the carpet as reward")
self.assertEqual(carpet_line.product_uom_qty, 1, "Should have only 1 qty for carpet as non reward")

View file

@ -0,0 +1,172 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.sale_loyalty.tests.common import TestSaleCouponNumbersCommon
from odoo.addons.website.tools import MockRequest
from odoo.exceptions import UserError
from odoo.tests import tagged
@tagged('-at_install', 'post_install')
class TestSaleCouponMultiwebsite(TestSaleCouponNumbersCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.website = cls.env['website'].browse(1)
cls.website2 = cls.env['website'].create({'name': 'website 2'})
def test_01_multiwebsite_checks(self):
""" Ensure the multi website compliance of programs and coupons, both in
backend and frontend.
"""
order = self.empty_order
self.env['sale.order.line'].create({
'product_id': self.largeCabinet.id,
'name': 'Large Cabinet',
'product_uom_qty': 2.0,
'order_id': order.id,
})
def _remove_reward():
order.order_line.filtered('is_reward_line').unlink()
self.assertEqual(len(order.order_line.ids), 1, "Program should have been removed")
def _apply_code(code, backend=True):
try:
self._apply_promo_code(order, code)
except UserError as e:
if backend:
raise e
# ==========================================
# ========== Programs (with code) ==========
# ==========================================
# 1. Backend - Generic
_apply_code(self.p1.rule_ids.code)
self.assertEqual(len(order.order_line.ids), 2, "Should get the discount line as it is a generic promo program")
_remove_reward()
# 2. Frontend - Generic
with MockRequest(self.env, website=self.website):
_apply_code(self.p1.rule_ids.code, False)
self.assertEqual(len(order.order_line.ids), 2, "Should get the discount line as it is a generic promo program (2)")
_remove_reward()
# make program specific
self.p1.website_id = self.website.id
# 3. Backend - Specific - sale_ok disabled
self.p1.sale_ok = False
with self.assertRaises(UserError):
_apply_code(self.p1.rule_ids.code) # the program is not enabled for Sales (backend)
# 3.5. Backend - Specific - sale_ok enabled
self.p1.sale_ok = True
_apply_code(self.p1.rule_ids.code)
self.assertEqual(len(order.order_line.ids), 2, "Should get the discount line as it is enabled for Sales(backend)")
_remove_reward()
# 4. Frontend - Specific - Correct website
order.website_id = self.website.id
with MockRequest(self.env, website=self.website):
_apply_code(self.p1.rule_ids.code, False)
self.assertEqual(len(order.order_line.ids), 2, "Should get the discount line as it is a specific promo program for the correct website")
_remove_reward()
# 5. Frontend - Specific - Wrong website
self.p1.website_id = self.website2.id
with MockRequest(self.env, website=self.website):
_apply_code(self.p1.rule_ids.code, False)
self.assertEqual(len(order.order_line.ids), 1, "Should not get the reward as wrong website")
# ==============================
# =========== Coupons ==========
# ==============================
order.website_id = False
self.env['loyalty.generate.wizard'].with_context(active_id=self.discount_coupon_program.id).create({
'coupon_qty': 4,
'points_granted': 1,
}).generate_coupons()
coupons = self.discount_coupon_program.coupon_ids
# 1. Backend - Generic
_apply_code(coupons[0].code)
self.assertEqual(len(order.order_line.ids), 2, "Should get the discount line as it is a generic coupon program")
_remove_reward()
# 2. Frontend - Generic
with MockRequest(self.env, website=self.website):
_apply_code(coupons[1].code, False)
self.assertEqual(len(order.order_line.ids), 2, "Should get the discount line as it is a generic coupon program (2)")
_remove_reward()
# make program specific
self.discount_coupon_program.website_id = self.website.id
# 3. Backend - Specific - sale_ok disabled
self.discount_coupon_program.sale_ok = False
with self.assertRaises(UserError):
_apply_code(coupons[2].code) # the program is not enabled for Sales (backend)
# 3.5. Backend - Specific - sale_ok enabled
self.discount_coupon_program.sale_ok = True
_apply_code(coupons[2].code)
self.assertEqual(len(order.order_line.ids), 2, "Should get the discount line as it is enabled for Sales(backend)")
_remove_reward()
# 4. Frontend - Specific - Correct website
order.website_id = self.website.id
with MockRequest(self.env, website=self.website):
_apply_code(coupons[2].code, False)
self.assertEqual(len(order.order_line.ids), 2, "Should get the discount line as it is a specific coupon program for the correct website")
_remove_reward()
# 5. Frontend - Specific - Wrong website
self.discount_coupon_program.website_id = self.website2.id
with MockRequest(self.env, website=self.website):
_apply_code(coupons[3].code, False)
self.assertEqual(len(order.order_line.ids), 1, "Should not get the reward as wrong website")
# ========================================
# ========== Programs (no code) ==========
# ========================================
order.website_id = False
self.p1.website_id = False
self.p1.rule_ids.code = False
self.p1.trigger = 'auto'
self.p1.rule_ids.mode = 'auto'
# 1. Backend - Generic
all_programs = self.env['loyalty.program'].search([])
self._auto_rewards(order, all_programs)
self.assertEqual(len(order.order_line.ids), 2, "Should get the discount line as it is a generic promo program")
# 2. Frontend - Generic
with MockRequest(self.env, website=self.website):
self._auto_rewards(order, all_programs)
self.assertEqual(len(order.order_line.ids), 2, "Should get the discount line as it is a generic promo program (2)")
# make program specific
self.p1.website_id = self.website.id
# 3. Backend - Specific
self.p1.sale_ok = False
self._auto_rewards(order, all_programs)
self.assertEqual(len(order.order_line.ids), 1, "The program is not enabled for Sales (backend)")
# 3.5. Backend - Specific - sale_ok enabled
self.p1.sale_ok = True
self._auto_rewards(order, all_programs)
self.assertEqual(len(order.order_line.ids), 2, "Should get the discount line as it is a generic promo program")
# 4. Frontend - Specific - Correct website
order.website_id = self.website.id
with MockRequest(self.env, website=self.website):
self._auto_rewards(order, all_programs)
self.assertEqual(len(order.order_line.ids), 2, "Should get the discount line as it is a specific promo program for the correct website")
# 5. Frontend - Specific - Wrong website
self.p1.website_id = self.website2.id
with MockRequest(self.env, website=self.website):
self._auto_rewards(order, all_programs)
self.assertEqual(len(order.order_line.ids), 1, "Should not get the reward as wrong website")

View file

@ -0,0 +1,74 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import date, timedelta
from freezegun import freeze_time
from odoo import Command
from odoo.tests import tagged
from odoo.tools import mute_logger
from odoo.addons.payment.tests.http_common import PaymentHttpCommon
from odoo.addons.sale_loyalty.tests.common import TestSaleCouponCommon
@tagged('post_install', '-at_install')
class TestShopLoyaltyTransaction(PaymentHttpCommon, TestSaleCouponCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.website = cls.env.company.website_id
if not cls.website:
cls.website = cls.env.ref('website.default_website')
cls.website.company_id = cls.env.company
@mute_logger('odoo.http')
def test_expired_reward_validation(self):
"""Ensure payments don't process if any applied reward is no longer valid."""
order = self.empty_order
program = self.program_gift_card
program.date_to = date.today() # set program to expire after today
self.product_a.type = 'service' # prevent need for delivery method
self.env['loyalty.generate.wizard'].with_context(active_id=program.id).create({
'coupon_qty': 1,
'points_granted': 100,
}).generate_coupons()
order.write({
'partner_id': self.portal_partner.id,
'website_id': self.website.id,
'message_partner_ids': self.portal_partner.ids,
'order_line': [Command.create({
'product_id': self.product_a.id,
})],
})
self._apply_promo_code(order, program.coupon_ids.code)
with freeze_time(program.date_to + timedelta(days=1)):
self.authenticate(self.portal_user.login, self.portal_user.login)
tx_response = self._make_json_rpc_request(
self._build_url(f'/shop/payment/transaction/{order.id}'),
{
'order_id': order.id,
'access_token': None,
'amount': order.amount_total,
'currency_id': order.currency_id.id,
'payment_option_id': self.provider.id,
'flow': 'direct',
'tokenization_requested': False,
'landing_route': order.get_portal_url(),
},
).json()
self.assertIn(
'error',
tx_response,
"Attempting to initate payment with an expired reward should raise an error.",
)
self.assertEqual(
tx_response['error']['data']['message'],
"Cannot process payment: applied reward was changed or has expired.",
)

View file

@ -0,0 +1,69 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details
from odoo.fields import Command
from odoo.tests import TransactionCase, tagged
from odoo.addons.website.tools import MockRequest
from odoo.addons.website_sale_loyalty.controllers.main import WebsiteSale
@tagged('post_install', '-at_install')
class TestClaimReward(TransactionCase):
def test_claim_reward_with_multi_product(self):
WebsiteSaleController = WebsiteSale()
tag = self.env['product.tag'].create({
'name': 'multi reward',
})
product1, product2 = self.env['product.product'].create([
{
'name': 'Test Product',
'list_price': 10.0,
'product_tag_ids': tag,
}, {
'name': 'Test Product 2',
'list_price': 20.0,
'product_tag_ids': tag,
}])
partner = self.env['res.partner'].create({
'name': 'Test Customer',
'email': 'test@example.com',
})
promo_program = self.env['loyalty.program'].create({
'name': 'Free Products',
'program_type': 'promotion',
'applies_on': 'current',
'trigger': 'auto',
'rule_ids': [Command.create({
'minimum_qty': 1,
'minimum_amount': 0.00,
'reward_point_amount': 3,
})],
'reward_ids': [Command.create({
'reward_type': 'product',
'reward_product_tag_id': tag.id,
'reward_product_qty': 1,
'required_points': 1,
})]
})
website = self.env['website'].browse(1)
order = self.env['sale.order'].create({
'website_id': website.id,
'partner_id': partner.id,
'order_line': [Command.create({
'product_id': product1.id,
'product_uom_qty': 1,
})],
})
order._update_programs_and_rewards()
with MockRequest(self.env, website=website, sale_order_id=order.id):
WebsiteSaleController.claim_reward(promo_program.reward_ids[:1].id, product_id=str(product2.id))
self.assertEqual(len(order.order_line), 2, 'reward line should be added to order')
self.assertEqual(order.order_line[1].product_id, product2, 'added reward line should should contain product 2')

View file

@ -0,0 +1,475 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import timedelta
from odoo import fields
from odoo.exceptions import ValidationError
from odoo.fields import Command
from odoo.tests import HttpCase, TransactionCase, tagged
from odoo.addons.sale.tests.test_sale_product_attribute_value_config import (
TestSaleProductAttributeValueCommon,
)
@tagged('post_install', '-at_install')
class WebsiteSaleLoyaltyTestUi(TestSaleProductAttributeValueCommon, HttpCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env.ref('base.user_admin').write({
'company_id': cls.env.company.id,
'company_ids': [(4, cls.env.company.id)],
'name': 'Mitchell Admin',
'street': '215 Vine St',
'phone': '+1 555-555-5555',
'city': 'Scranton',
'zip': '18503',
'country_id': cls.env.ref('base.us').id,
'state_id': cls.env.ref('base.state_us_39').id,
})
cls.env.ref('base.user_admin').sudo().partner_id.company_id = cls.env.company
cls.env.ref('website.default_website').company_id = cls.env.company
# set currency to not rely on demo data and avoid possible race condition
cls.currency_ratio = 1.0
pricelist = cls.env.ref('product.list0')
new_currency = cls._setup_currency(cls.currency_ratio)
pricelist.currency_id = new_currency
cls.env.user.partner_id.write({
'property_product_pricelist': pricelist.id,
})
(cls.env['product.pricelist'].search([]) - pricelist).write({'active': False})
cls.env.flush_all()
def test_01_admin_shop_sale_loyalty_tour(self):
if self.env['ir.module.module']._get('payment_custom').state != 'installed':
self.skipTest("Transfer provider is not installed")
transfer_provider = self.env.ref('payment.payment_provider_transfer')
transfer_provider.sudo().write({
'state': 'enabled',
'is_published': True,
'company_id': self.env.company.id,
})
transfer_provider._transfer_ensure_pending_msg_is_set()
# pre enable "Show # found" option to avoid race condition...
public_category = self.env['product.public.category'].create({'name': 'Public Category'})
large_cabinet = self.env['product.product'].create({
'name': 'Small Cabinet',
'list_price': 320.0,
'type': 'consu',
'is_published': True,
'sale_ok': True,
'public_categ_ids': [(4, public_category.id)],
'taxes_id': False,
})
free_large_cabinet = self.env['product.product'].create({
'name': 'Free Product - Small Cabinet',
'type': 'service',
'supplier_taxes_id': False,
'sale_ok': False,
'purchase_ok': False,
'invoice_policy': 'order',
'default_code': 'FREELARGECABINET',
'categ_id': self.env.ref('product.product_category_all').id,
'taxes_id': False,
})
ten_percent = self.env['product.product'].create({
'name': '10.0% discount on total amount',
'type': 'service',
'supplier_taxes_id': False,
'sale_ok': False,
'purchase_ok': False,
'invoice_policy': 'order',
'default_code': '10PERCENTDISC',
'categ_id': self.env.ref('product.product_category_all').id,
'taxes_id': False,
})
self.env['loyalty.program'].search([]).write({'active': False})
self.env['loyalty.program'].create({
'name': 'Buy 4 Small Cabinets, get one for free',
'trigger': 'auto',
'rule_ids': [(0, 0, {
'minimum_qty': 4,
'product_ids': large_cabinet,
})],
'reward_ids': [(0, 0, {
'reward_type': 'product',
'reward_product_id': large_cabinet.id,
'discount_line_product_id': free_large_cabinet.id,
})]
})
self.env['loyalty.program'].create({
'name': 'Code for 10% on orders',
'trigger': 'with_code',
'rule_ids': [(0, 0, {
'mode': 'with_code',
'code': 'testcode',
})],
'reward_ids': [(0, 0, {
'reward_type': 'discount',
'discount': 10,
'discount_mode': 'percent',
'discount_applicability': 'order',
'discount_line_product_id': ten_percent.id,
})],
})
vip_program = self.env['loyalty.program'].create({
'name': 'VIP',
'trigger': 'auto',
'program_type': 'loyalty',
'portal_visible': True,
'applies_on': 'both',
'rule_ids': [(0, 0, {
'mode': 'auto',
})],
'reward_ids': [(0, 0, {
'reward_type': 'discount',
'discount': 21,
'discount_mode': 'percent',
'discount_applicability': 'order',
'required_points': 50,
})],
})
self.env['loyalty.card'].create({
'partner_id': self.env.ref('base.partner_admin').id,
'program_id': vip_program.id,
'point_name': "Points",
'points': 371.03,
})
self.env.ref("website_sale.reduction_code").write({"active": True})
self.start_tour("/", 'shop_sale_loyalty', login="admin")
def test_02_admin_shop_gift_card_tour(self):
# pre enable "Show # found" option to avoid race condition...
public_category = self.env['product.public.category'].create({'name': 'Public Category'})
gift_card = self.env['product.product'].create({
'name': 'TEST - Gift Card',
'list_price': 50,
'type': 'service',
'is_published': True,
'sale_ok': True,
'public_categ_ids': [(4, public_category.id)],
'taxes_id': False,
})
self.env['product.product'].create({
'name': 'TEST - Small Drawer',
'list_price': 50,
'type': 'consu',
'is_published': True,
'sale_ok': True,
'public_categ_ids': [(4, public_category.id)],
'taxes_id': False,
})
# Disable any other program
self.env['loyalty.program'].search([]).write({'active': False})
gift_card_program = self.env['loyalty.program'].create({
'name': 'Gift Cards',
'program_type': 'gift_card',
'applies_on': 'future',
'trigger': 'auto',
'rule_ids': [(0, 0, {
'reward_point_amount': 1,
'reward_point_mode': 'money',
'reward_point_split': True,
'product_ids': gift_card,
})],
'reward_ids': [(0, 0, {
'reward_type': 'discount',
'discount_mode': 'per_point',
'discount': 1,
'discount_applicability': 'order',
'required_points': 1,
'description': 'PAY WITH GIFT CARD',
})],
})
# Another program for good measure
self.env['loyalty.program'].create({
'name': '10% Discount',
'applies_on': 'current',
'trigger': 'with_code',
'program_type': 'promotion',
'rule_ids': [(0, 0, {
'mode': 'with_code',
'code': '10PERCENT',
})],
'reward_ids': [(0, 0, {
'reward_type': 'discount',
'discount': 10,
'discount_mode': 'percent',
'discount_applicability': 'order',
})],
})
# Create a gift card to be used
self.env['loyalty.card'].create({
'program_id': gift_card_program.id,
'points': 50,
'code': 'GIFT_CARD',
})
self.env.ref("website_sale.reduction_code").write({"active": True})
self.start_tour('/', 'shop_sale_gift_card', login='admin')
self.assertEqual(len(gift_card_program.coupon_ids), 2, 'There should be two coupons, one with points, one without')
self.assertEqual(len(gift_card_program.coupon_ids.filtered('points')), 1, 'There should be two coupons, one with points, one without')
@tagged('post_install', '-at_install')
class TestWebsiteSaleCoupon(TransactionCase):
@classmethod
def setUpClass(cls):
super(TestWebsiteSaleCoupon, cls).setUpClass()
program = cls.env['loyalty.program'].create({
'name': '10% TEST Discount',
'trigger': 'with_code',
'applies_on': 'current',
'rule_ids': [(0, 0, {})],
'reward_ids': [(0, 0, {
'reward_type': 'discount',
'discount': 10,
'discount_mode': 'percent',
})],
})
cls.env['loyalty.generate.wizard'].with_context(active_id=program.id).create({
'coupon_qty': 1,
'points_granted': 1
}).generate_coupons()
cls.coupon = program.coupon_ids[0]
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
})
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 test_01_gc_coupon(self):
# 1. Simulate a frontend order (website, product)
order = self.empty_order
order.website_id = self.env['website'].browse(1)
self.env['sale.order.line'].create({
'product_id': self.env['product.product'].create({
'name': 'Product A',
'list_price': 100,
'sale_ok': True,
}).id,
'name': 'Product A',
'product_uom_qty': 2.0,
'order_id': order.id,
})
# 2. Apply the coupon
self._apply_promo_code(order, self.coupon.code)
self.assertEqual(len(order.applied_coupon_ids), 1, "The coupon should've been applied on the order")
self.assertEqual(self.coupon, order.applied_coupon_ids)
# 3. Test recent order -> Should not be removed
order._gc_abandoned_coupons()
self.assertEqual(len(order.applied_coupon_ids), 1, "The coupon shouldn't have been removed from the order no more than 4 days")
# 4. Test order not older than ICP validity -> Should not be removed
ICP = self.env['ir.config_parameter']
icp_validity = ICP.create({'key': 'website_sale_coupon.abandonned_coupon_validity', 'value': 5})
self.env.flush_all()
query = """UPDATE %s SET write_date = %%s WHERE id = %%s""" % (order._table,)
self.env.cr.execute(query, (fields.Datetime.to_string(fields.datetime.now() - timedelta(days=4, hours=2)), order.id))
order._gc_abandoned_coupons()
self.assertEqual(len(order.applied_coupon_ids), 1, "The coupon shouldn't have been removed from the order the order is 4 days old but icp validity is 5 days")
# 5. Test order with no ICP and older then 4 default days -> Should be removed
icp_validity.unlink()
order._gc_abandoned_coupons()
self.assertEqual(len(order.applied_coupon_ids), 0, "The coupon should've been removed from the order as more than 4 days")
def test_02_remove_coupon(self):
# 1. Simulate a frontend order (website, product)
order = self.empty_order
order.website_id = self.env['website'].browse(1)
self.env['sale.order.line'].create({
'product_id': self.env['product.product'].create({
'name': 'Product A', 'list_price': 100, 'sale_ok': True
}).id,
'name': 'Product A',
'order_id': order.id,
})
# 2. Apply the coupon
self._apply_promo_code(order, self.coupon.code)
# 3. Remove the coupon
coupon_line = order.website_order_line.filtered(
lambda l: l.coupon_id and l.coupon_id.id == self.coupon.id
)
order._cart_update(coupon_line.product_id.id, add_qty=None)
msg = "The coupon should've been removed from the order"
self.assertEqual(len(order.applied_coupon_ids), 0, msg=msg)
def test_03_remove_coupon_with_different_taxes_on_products(self):
"""
Tests the removal of a coupon from an order containing products with various tax rates,
ensuring that the system correctly handles multiple coupon lines created
for each unique tax scenario.
Background:
An order may include products with different tax implications,
such as non-taxed products, products with a single tax rate,
and products with multiple tax rates. When a coupon is applied,
it creates separate coupon lines for each distinct tax situation
(non-taxed, individual taxes, and combinations of taxes).
This test verifies that the coupon deletion process accurately removes
all associated coupon lines, maintaining the financial accuracy of the order.
Steps:
1. Create an order with products subject to different tax scenarios:
- Non-taxed product 'Product A'
- Product 'Product B' with Tax A
- Product 'Product C' with Tax B
- Product 'Product D' subject to both Tax A and Tax B
2. Apply a coupon, which generates four distinct coupon lines
to reflect each tax scenario.
3. Remove the coupon and verify that all coupon lines are removed and
that no coupons remain applied.
"""
# Create 2 Taxes
tax_a = self.env['account.tax'].create({
'name': 'Tax A',
'type_tax_use': 'sale',
'amount_type': 'percent',
'amount': 15,
})
tax_b = tax_a.copy({'name': 'Tax B'})
# Create 4 products subject to different tax
products_data = [
('Product A', []),
('Product B', [tax_a.id]),
('Product C', [tax_b.id]),
('Product D', [tax_a.id, tax_b.id]),
]
products = self.env['product.product'].create(
[{
'name': name,
'list_price': 100,
'sale_ok': True,
'taxes_id': [Command.set(taxes_id)],
} for name, taxes_id in products_data]
)
order = self.empty_order
order.write({
'website_id': self.env['website'].browse(1),
'order_line': [Command.create({'product_id': product.id}) for product in products],
})
msg = "There should only be 4 lines for the 4 products."
self.assertEqual(len(order.order_line), 4, msg=msg)
# 2. Apply the coupon
self._apply_promo_code(order, self.coupon.code)
msg = (
"4 additional lines should have been added to the sale orders"
"after application of the coupon for each separate tax situation."
)
self.assertEqual(len(order.order_line), 8, msg=msg)
# 3. Remove the coupon
coupon_line = order.website_order_line.filtered(
lambda line: line.coupon_id and line.coupon_id.id == self.coupon.id
)
order._cart_update(
product_id=coupon_line.product_id.id,
line_id=None,
add_qty=None,
set_qty=0,
)
msg = "All coupon lines should have been removed from the order."
self.assertEqual(len(order.applied_coupon_ids), 0, msg=msg)
self.assertEqual(len(order.order_line), 4, msg=msg)
def test_confirm_points_as_public_user(self):
test_product = self.env['product.product'].create({
'name': "Test Product",
'list_price': 100,
'sale_ok': True,
})
test_partner = self.env['res.partner'].create({
'name': 'Test Partner'
})
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': 1,
'product_ids': [test_product.id],
})],
'reward_ids': [Command.create({
'reward_type': 'discount',
'discount': 1.5,
'discount_mode': 'per_point',
'discount_applicability': 'order',
'required_points': 3,
})],
})
order = self.env['sale.order'].create({
'partner_id': test_partner.id,
'order_line': [Command.create({
'product_id': test_product.id,
'product_uom_qty': 1,
})]
})
loyalty_card = self.env['loyalty.card'].create({
'program_id': loyalty_program.id,
'partner_id': test_partner.id,
'points': 0,
})
public_user = self.env.ref('base.public_user')
order.action_quotation_send()
order.with_context(access_token=order.access_token, user=public_user).action_confirm()
self.assertEqual(loyalty_card.points, 1)