test_frontend.py 45 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166
  1. # -*- coding: utf-8 -*-
  2. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  3. from datetime import date, timedelta
  4. from odoo.addons.point_of_sale.tests.test_frontend import TestPointOfSaleHttpCommon
  5. from odoo.tests import tagged
  6. from odoo import Command
  7. @tagged("post_install", "-at_install")
  8. class TestUi(TestPointOfSaleHttpCommon):
  9. @classmethod
  10. def setUpClass(cls):
  11. super().setUpClass()
  12. # Disable any programs during the test
  13. cls.env['loyalty.program'].search([]).write({'active': False})
  14. cls.promo_programs = cls.env["loyalty.program"]
  15. # code promo program -> discount on specific products
  16. cls.code_promo_program = cls.env['loyalty.program'].create({
  17. 'name': 'Promo Code Program - Discount on Specific Products',
  18. 'program_type': 'promotion',
  19. 'trigger': 'with_code',
  20. 'applies_on': 'current',
  21. 'rule_ids': [(0, 0, {
  22. 'mode': 'with_code',
  23. 'code': 'promocode',
  24. })],
  25. 'reward_ids': [(0, 0, {
  26. 'reward_type': 'discount',
  27. 'discount': 50,
  28. 'discount_mode': 'percent',
  29. 'discount_applicability': 'specific',
  30. 'discount_product_ids': cls.whiteboard_pen | cls.magnetic_board | cls.desk_organizer,
  31. })],
  32. })
  33. cls.promo_programs |= cls.code_promo_program
  34. # auto promo program on current order
  35. # -> discount on cheapest product
  36. cls.auto_promo_program_current = cls.env['loyalty.program'].create({
  37. 'name': 'Auto Promo Program - Cheapest Product',
  38. 'program_type': 'promotion',
  39. 'trigger': 'auto',
  40. 'rule_ids': [(0, 0, {})],
  41. 'reward_ids': [(0, 0, {
  42. 'reward_type': 'discount',
  43. 'discount': 90,
  44. 'discount_mode': 'percent',
  45. 'discount_applicability': 'cheapest',
  46. })]
  47. })
  48. cls.promo_programs |= cls.auto_promo_program_current
  49. # auto promo program on next order
  50. # -> discount on order (global discount)
  51. cls.auto_promo_program_next = cls.env['loyalty.program'].create({
  52. 'name': 'Auto Promo Program - Global Discount',
  53. 'program_type': 'promotion',
  54. 'trigger': 'auto',
  55. 'applies_on': 'future',
  56. 'rule_ids': [(0, 0, {})],
  57. 'reward_ids': [(0, 0, {
  58. 'reward_type': 'discount',
  59. 'discount': 10,
  60. 'discount_mode': 'percent',
  61. 'discount_applicability': 'order',
  62. })]
  63. })
  64. cls.promo_programs |= cls.auto_promo_program_next
  65. cls.promo_programs.write({
  66. 'pos_config_ids': [Command.link(cls.main_pos_config.id)],
  67. })
  68. # coupon program -> free product
  69. cls.coupon_program = cls.env['loyalty.program'].create({
  70. 'name': 'Coupon Program - Buy 3 Take 2 Free Product',
  71. 'program_type': 'coupons',
  72. 'trigger': 'with_code',
  73. 'applies_on': 'current',
  74. 'rule_ids': [(0, 0, {
  75. 'product_ids': cls.desk_organizer,
  76. 'reward_point_mode': 'unit',
  77. 'minimum_qty': 3,
  78. })],
  79. 'reward_ids': [(0, 0, {
  80. 'reward_type': 'product',
  81. 'reward_product_id': cls.desk_organizer.id,
  82. 'reward_product_qty': 1,
  83. 'required_points': 1.5,
  84. })],
  85. 'pos_config_ids': [Command.link(cls.main_pos_config.id)],
  86. })
  87. # Create coupons for the coupon program and change the code
  88. # to be able to use them in the frontend tour.
  89. cls.env["loyalty.generate.wizard"].with_context(
  90. {"active_id": cls.coupon_program.id}
  91. ).create({"coupon_qty": 4, 'points_granted': 4.5}).generate_coupons()
  92. cls.coupon1, cls.coupon2, cls.coupon3, cls.coupon4 = cls.coupon_program.coupon_ids
  93. cls.coupon1.write({"code": "1234"})
  94. cls.coupon2.write({"code": "5678"})
  95. cls.coupon3.write({"code": "1357"})
  96. cls.coupon4.write({"code": "2468"})
  97. def setUp(self):
  98. super().setUp()
  99. # Set the programs to the pos config.
  100. # Remove fiscal position and pricelist.
  101. self.main_pos_config.write({
  102. 'tax_regime_selection': False,
  103. 'use_pricelist': False,
  104. })
  105. self.main_pos_config.open_ui()
  106. def create_programs(self, details):
  107. """
  108. Create loyalty programs based on the details given.
  109. :param details: list of tuple ('name': str, 'program_type': 'gift_card' or 'ewallet')
  110. """
  111. LoyaltyProgram = self.env['loyalty.program']
  112. programs = {} # map: name -> program
  113. for (name, program_type) in details:
  114. program_id = LoyaltyProgram.create_from_template(program_type)['res_id']
  115. program = LoyaltyProgram.browse(program_id)
  116. program.write({'name': name})
  117. programs[name] = program
  118. return programs
  119. def test_pos_loyalty_tour_basic(self):
  120. """PoS Loyalty Basic Tour"""
  121. ##
  122. # Tour Part 1
  123. # This part will generate coupons for `auto_promo_program_next`
  124. # that will be used in the second part of the tour.
  125. #
  126. self.start_tour(
  127. "/pos/web?config_id=%d" % self.main_pos_config.id,
  128. "PosLoyaltyTour1",
  129. login="accountman",
  130. )
  131. # check coupon usage
  132. self.assertEqual(self.coupon1.points, 0, 'The coupon should have consumed its points.')
  133. self.assertEqual(self.coupon2.points, 4.5, 'The coupon was used but never validated.')
  134. # check pos_order_count in each program
  135. self.assertEqual(self.auto_promo_program_current.pos_order_count, 3)
  136. self.assertEqual(self.auto_promo_program_next.pos_order_count, 0)
  137. self.assertEqual(self.code_promo_program.pos_order_count, 1)
  138. self.assertEqual(self.coupon_program.pos_order_count, 1)
  139. # check number of generated coupons
  140. self.assertEqual(len(self.auto_promo_program_next.coupon_ids), 5)
  141. # check number of orders in the session
  142. pos_session = self.main_pos_config.current_session_id
  143. self.assertEqual(
  144. len(pos_session.order_ids), 5, msg="5 orders were made in tour part1."
  145. )
  146. ##
  147. # Tour Part 2
  148. # The coupons generated in the first part will be used in this tour.
  149. #
  150. # Manually set the code for some `auto_promo_program_next` coupons
  151. # to be able to use them in defining the part2 tour.
  152. (
  153. promo_coupon1,
  154. promo_coupon2,
  155. promo_coupon3,
  156. promo_coupon4,
  157. *_,
  158. ) = self.auto_promo_program_next.coupon_ids
  159. promo_coupon1.write({"code": "123456"})
  160. promo_coupon2.write({"code": "345678"})
  161. promo_coupon3.write({"code": "567890"})
  162. promo_coupon4.write({"code": "098765"})
  163. self.coupon2.points = 6
  164. self.coupon3.points = 3
  165. # use here the generated coupon
  166. self.start_tour(
  167. "/pos/web?config_id=%d" % self.main_pos_config.id,
  168. "PosLoyaltyTour2",
  169. login="accountman",
  170. )
  171. # check pos_order_count in each program
  172. self.assertEqual(self.auto_promo_program_current.pos_order_count, 6)
  173. self.assertEqual(self.auto_promo_program_next.pos_order_count, 2)
  174. self.assertEqual(self.code_promo_program.pos_order_count, 2)
  175. self.assertEqual(self.coupon_program.pos_order_count, 3)
  176. def test_loyalty_validity_dates_and_use(self):
  177. # Tests date validity and max usage for an automatic program.
  178. self.auto_promo_program_current.write({
  179. 'date_to': date.today() - timedelta(days=2),
  180. 'limit_usage': True,
  181. 'max_usage': 1,
  182. })
  183. # First tour check that the promotion is not applied
  184. self.start_tour(
  185. "/pos/web?config_id=%d" % self.main_pos_config.id,
  186. "PosLoyaltyValidity1",
  187. login="accountman",
  188. )
  189. self.auto_promo_program_current.write({
  190. 'date_to': date.today() + timedelta(days=2),
  191. })
  192. # Second tour that does 2 orders, the first should have the rewards, the second should not
  193. self.start_tour(
  194. "/pos/web?config_id=%d" % self.main_pos_config.id,
  195. "PosLoyaltyValidity2",
  196. login="accountman",
  197. )
  198. def test_loyalty_free_product_rewards(self):
  199. free_product = self.env['loyalty.program'].create({
  200. 'name': 'Buy 2 Take 1 desk_organizer',
  201. 'program_type': 'promotion',
  202. 'trigger': 'auto',
  203. 'applies_on': 'current',
  204. 'rule_ids': [(0, 0, {
  205. 'product_ids': self.desk_organizer,
  206. 'reward_point_mode': 'unit',
  207. 'minimum_qty': 0,
  208. })],
  209. 'reward_ids': [(0, 0, {
  210. 'reward_type': 'product',
  211. 'reward_product_id': self.desk_organizer.id,
  212. 'reward_product_qty': 1,
  213. 'required_points': 2,
  214. })],
  215. })
  216. free_other_product = self.env['loyalty.program'].create({
  217. 'name': 'Buy 3 magnetic_board, Take 1 whiteboard_pen',
  218. 'program_type': 'promotion',
  219. 'trigger': 'auto',
  220. 'applies_on': 'current',
  221. 'rule_ids': [(0, 0, {
  222. 'product_ids': self.magnetic_board,
  223. 'reward_point_mode': 'unit',
  224. 'minimum_qty': 0,
  225. })],
  226. 'reward_ids': [(0, 0, {
  227. 'reward_type': 'product',
  228. 'reward_product_id': self.whiteboard_pen.id,
  229. 'reward_product_qty': 1,
  230. 'required_points': 3,
  231. })],
  232. })
  233. free_multi_product = self.env['loyalty.program'].create({
  234. 'name': '2 items of shelves, get desk_pad/monitor_stand free',
  235. 'program_type': 'promotion',
  236. 'trigger': 'auto',
  237. 'applies_on': 'current',
  238. 'rule_ids': [(0, 0, {
  239. 'product_ids': (self.wall_shelf | self.small_shelf).ids,
  240. 'reward_point_mode': 'unit',
  241. 'minimum_qty': 0,
  242. })],
  243. 'reward_ids': [(0, 0, {
  244. 'reward_type': 'product',
  245. 'reward_product_tag_id': self.env['product.tag'].create({
  246. 'name': 'reward_product_tag',
  247. 'product_product_ids': (self.desk_pad | self.monitor_stand).ids,
  248. }).id,
  249. 'reward_product_qty': 1,
  250. 'required_points': 2,
  251. })],
  252. })
  253. (self.promo_programs | self.coupon_program).write({'active': False})
  254. self.start_tour(
  255. "/pos/web?config_id=%d" % self.main_pos_config.id,
  256. "PosLoyaltyFreeProductTour",
  257. login="accountman",
  258. )
  259. # Keep the tour to generate 4 orders for the free_product and free_other_product programs.
  260. # 2 of them don't use a program.
  261. # 1 uses free_product.
  262. # 1 uses free_other_product.
  263. # This is to take into account the fact that during tours, we can't test the "non-occurence" of something.
  264. # It would be nice to have a check like: Validate that a reward is "not" there.
  265. self.assertEqual(free_product.pos_order_count, 1)
  266. self.assertEqual(free_other_product.pos_order_count, 2)
  267. # There is the 5th order that tests multi_product reward.
  268. # It attempted to add one reward product, removed it, then add the second.
  269. # The second reward was synced with the order.
  270. self.assertEqual(free_multi_product.pos_order_count, 1)
  271. def test_loyalty_free_product_loyalty_program(self):
  272. # In this program, each whiteboard pen gives 1 point.
  273. # 4 points can be used to get a free whiteboard pen.
  274. loyalty_program = self.env['loyalty.program'].create({
  275. 'name': 'Buy 4 whiteboard_pen, Take 1 whiteboard_pen',
  276. 'program_type': 'loyalty',
  277. 'trigger': 'auto',
  278. 'applies_on': 'both',
  279. 'rule_ids': [(0, 0, {
  280. 'product_ids': self.whiteboard_pen.ids,
  281. 'reward_point_mode': 'unit',
  282. 'minimum_qty': 1,
  283. })],
  284. 'reward_ids': [(0, 0, {
  285. 'reward_type': 'product',
  286. 'reward_product_id': self.whiteboard_pen.id,
  287. 'reward_product_qty': 1,
  288. 'required_points': 4,
  289. })],
  290. })
  291. (self.promo_programs | self.coupon_program).write({'active': False})
  292. partner_aaa = self.env['res.partner'].create({'name': 'Test Partner AAA'})
  293. partner_bbb = self.env['res.partner'].create({'name': 'Test Partner BBB'})
  294. partner_ccc = self.env['res.partner'].create({'name': 'Test Partner CCC'})
  295. # Part 1
  296. self.start_tour(
  297. "/pos/web?config_id=%d" % self.main_pos_config.id,
  298. "PosLoyaltyLoyaltyProgram1",
  299. login="accountman",
  300. )
  301. aaa_loyalty_card = loyalty_program.coupon_ids.filtered(lambda coupon: coupon.partner_id.id == partner_aaa.id)
  302. self.assertEqual(loyalty_program.pos_order_count, 1)
  303. self.assertAlmostEqual(aaa_loyalty_card.points, 4)
  304. # Part 2
  305. self.start_tour(
  306. "/pos/web?config_id=%d" % self.main_pos_config.id,
  307. "PosLoyaltyLoyaltyProgram2",
  308. login="accountman",
  309. )
  310. self.assertEqual(loyalty_program.pos_order_count, 2, msg='Only 2 orders should have reward lines.')
  311. self.assertAlmostEqual(aaa_loyalty_card.points, 1)
  312. bbb_loyalty_card = loyalty_program.coupon_ids.filtered(lambda coupon: coupon.partner_id.id == partner_bbb.id)
  313. ccc_loyalty_card = loyalty_program.coupon_ids.filtered(lambda coupon: coupon.partner_id.id == partner_ccc.id)
  314. self.assertAlmostEqual(bbb_loyalty_card.points, 3, msg='Reference: Order3_BBB')
  315. self.assertAlmostEqual(ccc_loyalty_card.points, 4, msg='Reference: Order2_CCC')
  316. reward_orderline = self.main_pos_config.current_session_id.order_ids[-1].lines.filtered(lambda line: line.is_reward_line)
  317. self.assertEqual(len(reward_orderline.ids), 0, msg='Reference: Order4_no_reward. Last order should have no reward line.')
  318. def test_loyalty_free_product_zero_sale_price_loyalty_program(self):
  319. # In this program, each $ spent gives 1 point.
  320. # 5 points can be used to get a free whiteboard pen.
  321. # and the whiteboard pen sale price is zero
  322. self.whiteboard_pen.write({'lst_price': 0})
  323. loyalty_program = self.env['loyalty.program'].create({
  324. 'name': 'Loyalty Program',
  325. 'program_type': 'loyalty',
  326. 'trigger': 'auto',
  327. 'applies_on': 'both',
  328. 'rule_ids': [(0, 0, {
  329. 'reward_point_amount': 1,
  330. 'reward_point_mode': 'money',
  331. 'minimum_qty': 1,
  332. })],
  333. 'reward_ids': [(0, 0, {
  334. 'reward_type': 'product',
  335. 'reward_product_id': self.whiteboard_pen.id,
  336. 'reward_product_qty': 1,
  337. 'required_points': 5,
  338. })],
  339. })
  340. (self.promo_programs | self.coupon_program).write({'active': False})
  341. partner_aaa = self.env['res.partner'].create({'name': 'Test Partner AAA'})
  342. # Part 1
  343. self.start_tour(
  344. "/pos/web?config_id=%d" % self.main_pos_config.id,
  345. "PosLoyaltyLoyaltyProgram3",
  346. login="accountman",
  347. )
  348. aaa_loyalty_card = loyalty_program.coupon_ids.filtered(lambda coupon: coupon.partner_id.id == partner_aaa.id)
  349. self.assertEqual(loyalty_program.pos_order_count, 1)
  350. self.assertAlmostEqual(aaa_loyalty_card.points, 5.2)
  351. def test_pos_loyalty_tour_max_amount(self):
  352. """Test the loyalty program with a maximum amount and product with different taxe."""
  353. self.env['loyalty.program'].search([]).write({'active': False})
  354. self.promo_product = self.env["product.product"].create(
  355. {
  356. "name": "Promo Product",
  357. "type": "service",
  358. "list_price": 30,
  359. "available_in_pos": True,
  360. }
  361. )
  362. tax01 = self.env["account.tax"].create({
  363. "name": "C01 Tax",
  364. "amount": "0.00",
  365. })
  366. tax02 = self.env["account.tax"].create({
  367. "name": "C02 Tax",
  368. "amount": "0.00",
  369. })
  370. self.productA = self.env["product.product"].create(
  371. {
  372. "name": "Product A",
  373. "type": "product",
  374. "list_price": 15,
  375. "available_in_pos": True,
  376. "taxes_id": [(6, 0, [tax01.id])],
  377. }
  378. )
  379. # create another product with different taxes_id
  380. self.productB = self.env["product.product"].create(
  381. {
  382. "name": "Product B",
  383. "type": "product",
  384. "list_price": 25,
  385. "available_in_pos": True,
  386. "taxes_id": [(6, 0, [tax02.id])]
  387. }
  388. )
  389. self.env['loyalty.program'].create({
  390. 'name': 'Promo Program - Max Amount',
  391. 'program_type': 'promotion',
  392. 'trigger': 'auto',
  393. 'applies_on': 'current',
  394. 'rule_ids': [(0, 0, {
  395. 'product_domain': '[["product_variant_ids.name","=","Promo Product"]]',
  396. 'reward_point_mode': 'unit',
  397. 'minimum_qty': 1,
  398. })],
  399. 'reward_ids': [(0, 0, {
  400. 'reward_type': 'discount',
  401. 'discount_product_ids': (self.productA | self.productB).ids,
  402. 'required_points': 1,
  403. 'discount': 100,
  404. 'discount_mode': 'percent',
  405. 'discount_applicability': 'specific',
  406. 'discount_max_amount': 40,
  407. })],
  408. 'pos_config_ids': [Command.link(self.main_pos_config.id)],
  409. })
  410. self.start_tour(
  411. "/pos/web?config_id=%d" % self.main_pos_config.id,
  412. "PosLoyaltyTour3",
  413. login="accountman",
  414. )
  415. def test_gift_card_program_create_set(self):
  416. """
  417. Test for gift card program when pos.config.gift_card_settings == 'create_set'.
  418. """
  419. LoyaltyProgram = self.env['loyalty.program']
  420. # Deactivate all other programs to avoid interference
  421. (LoyaltyProgram.search([])).write({'pos_ok': False})
  422. # But activate the gift_card_product_50 because it's shared among new gift card programs.
  423. self.env.ref('loyalty.gift_card_product_50').write({'active': True})
  424. # Create gift card program
  425. gift_card_program = self.create_programs([('arbitrary_name', 'gift_card')])['arbitrary_name']
  426. # Change the gift card program settings
  427. self.main_pos_config.write({'gift_card_settings': 'create_set'})
  428. # Run the tour to create a gift card
  429. self.start_tour(
  430. "/pos/web?config_id=%d" % self.main_pos_config.id,
  431. "GiftCardProgramCreateSetTour1",
  432. login="accountman",
  433. )
  434. # Check that gift cards are created
  435. self.assertEqual(len(gift_card_program.coupon_ids), 1)
  436. # Change the code to 044123456 so that we can use it in the next tour.
  437. # Make sure it starts with 044 because it's the prefix of the loyalty cards.
  438. gift_card_program.coupon_ids.code = '044123456'
  439. # Run the tour to use the gift card
  440. self.start_tour(
  441. "/pos/web?config_id=%d" % self.main_pos_config.id,
  442. "GiftCardProgramCreateSetTour2",
  443. login="accountman",
  444. )
  445. # Check that gift cards are used
  446. self.assertEqual(gift_card_program.coupon_ids.points, 46.8)
  447. def test_gift_card_program_scan_use(self):
  448. """
  449. Test for gift card program with pos.config.gift_card_settings == 'scan_use'.
  450. - The gift card coupon codes are known before opening pos.
  451. - They will be scanned and paid by the customer which links the coupon to the order.
  452. - Meaning, it's paid.
  453. - Then it will be scanned for usage.
  454. """
  455. LoyaltyProgram = self.env['loyalty.program']
  456. # Deactivate all other programs to avoid interference
  457. (LoyaltyProgram.search([])).write({'pos_ok': False})
  458. # But activate the gift_card_product_50 because it's shared among new gift card programs.
  459. self.env.ref('loyalty.gift_card_product_50').write({'active': True})
  460. # Create gift card program
  461. gift_card_program = self.create_programs([('arbitrary_name', 'gift_card')])['arbitrary_name']
  462. # Change the gift card program settings
  463. self.main_pos_config.write({'gift_card_settings': 'scan_use'})
  464. # Generate 5$ gift card.
  465. self.env["loyalty.generate.wizard"].with_context(
  466. {"active_id": gift_card_program.id}
  467. ).create({"coupon_qty": 1, 'points_granted': 5}).generate_coupons()
  468. # Change the code of the gift card.
  469. gift_card_program.coupon_ids.code = '044123456'
  470. # Run the tour. It will pay the gift card and use it.
  471. self.start_tour(
  472. "/pos/web?config_id=%d" % self.main_pos_config.id,
  473. "GiftCardProgramScanUseTour",
  474. login="accountman",
  475. )
  476. # Check that gift cards are used
  477. self.assertAlmostEqual(gift_card_program.coupon_ids.points, 0, places=2)
  478. # 3 order should be created.
  479. self.assertEqual(len(self.main_pos_config.current_session_id.order_ids), 3)
  480. def test_ewallet_program(self):
  481. """
  482. Test for ewallet program.
  483. - Collect points in EWalletProgramTour1.
  484. - Use points in EWalletProgramTour2.
  485. """
  486. LoyaltyProgram = self.env['loyalty.program']
  487. # Deactivate all other programs to avoid interference
  488. (LoyaltyProgram.search([])).write({'pos_ok': False})
  489. # But activate the ewallet_product_50 because it's shared among new ewallet programs.
  490. self.env.ref('loyalty.ewallet_product_50').write({'active': True})
  491. # Create ewallet program
  492. ewallet_program = self.create_programs([('arbitrary_name', 'ewallet')])['arbitrary_name']
  493. # Create test partners
  494. partner_aaa = self.env['res.partner'].create({'name': 'AAAAAAA'})
  495. partner_bbb = self.env['res.partner'].create({'name': 'BBBBBBB'})
  496. # Run the tour to topup ewallets.
  497. self.start_tour(
  498. "/pos/web?config_id=%d" % self.main_pos_config.id,
  499. "EWalletProgramTour1",
  500. login="accountman",
  501. )
  502. # Check that ewallets are created for partner_aaa.
  503. ewallet_aaa = self.env['loyalty.card'].search([('partner_id', '=', partner_aaa.id), ('program_id', '=', ewallet_program.id)])
  504. self.assertEqual(len(ewallet_aaa), 1)
  505. self.assertAlmostEqual(ewallet_aaa.points, 50, places=2)
  506. # Check that ewallets are created for partner_bbb.
  507. ewallet_bbb = self.env['loyalty.card'].search([('partner_id', '=', partner_bbb.id), ('program_id', '=', ewallet_program.id)])
  508. self.assertEqual(len(ewallet_bbb), 1)
  509. self.assertAlmostEqual(ewallet_bbb.points, 10, places=2)
  510. # Run the tour consume ewallets.
  511. self.start_tour(
  512. "/pos/web?config_id=%d" % self.main_pos_config.id,
  513. "EWalletProgramTour2",
  514. login="accountman",
  515. )
  516. # Check that ewallets are consumed for partner_aaa.
  517. self.assertAlmostEqual(ewallet_aaa.points, 0, places=2)
  518. # Check final balance after consumption and refund eWallet for partner_bbb.
  519. self.assertAlmostEqual(ewallet_bbb.points, 20, places=2)
  520. def test_multiple_gift_wallet_programs(self):
  521. """
  522. Test for multiple gift_card and ewallet programs.
  523. """
  524. LoyaltyProgram = self.env['loyalty.program']
  525. # Deactivate all other programs to avoid interference
  526. (LoyaltyProgram.search([])).write({'pos_ok': False})
  527. # But activate the gift_card_product_50 and ewallet_product_50 because they're shared among new programs.
  528. self.env.ref('loyalty.gift_card_product_50').write({'active': True})
  529. self.env.ref('loyalty.ewallet_product_50').write({'active': True})
  530. # Create programs
  531. programs = self.create_programs([
  532. ('gift_card_1', 'gift_card'),
  533. ('gift_card_2', 'gift_card'),
  534. ('ewallet_1', 'ewallet'),
  535. ('ewallet_2', 'ewallet')
  536. ])
  537. # Change the gift card program settings
  538. self.main_pos_config.write({'gift_card_settings': 'create_set'})
  539. # Create test partners
  540. partner_aaa = self.env['res.partner'].create({'name': 'AAAAAAA'})
  541. partner_bbb = self.env['res.partner'].create({'name': 'BBBBBBB'})
  542. # Run the tour to topup ewallets.
  543. self.start_tour(
  544. "/pos/web?config_id=%d" % self.main_pos_config.id,
  545. "MultipleGiftWalletProgramsTour",
  546. login="accountman",
  547. )
  548. # Check the created gift cards.
  549. self.assertEqual(len(programs['gift_card_1'].coupon_ids), 1)
  550. self.assertAlmostEqual(programs['gift_card_1'].coupon_ids.points, 10)
  551. self.assertEqual(len(programs['gift_card_2'].coupon_ids), 1)
  552. self.assertAlmostEqual(programs['gift_card_2'].coupon_ids.points, 20)
  553. # Check the created ewallets.
  554. ewallet_1_aaa = self.env['loyalty.card'].search([('partner_id', '=', partner_aaa.id), ('program_id', '=', programs['ewallet_1'].id)])
  555. self.assertEqual(len(ewallet_1_aaa), 1)
  556. self.assertAlmostEqual(ewallet_1_aaa.points, 18, places=2)
  557. ewallet_2_aaa = self.env['loyalty.card'].search([('partner_id', '=', partner_aaa.id), ('program_id', '=', programs['ewallet_2'].id)])
  558. self.assertEqual(len(ewallet_2_aaa), 1)
  559. self.assertAlmostEqual(ewallet_2_aaa.points, 40, places=2)
  560. ewallet_1_bbb = self.env['loyalty.card'].search([('partner_id', '=', partner_bbb.id), ('program_id', '=', programs['ewallet_1'].id)])
  561. self.assertEqual(len(ewallet_1_bbb), 1)
  562. self.assertAlmostEqual(ewallet_1_bbb.points, 50, places=2)
  563. ewallet_2_bbb = self.env['loyalty.card'].search([('partner_id', '=', partner_bbb.id), ('program_id', '=', programs['ewallet_2'].id)])
  564. self.assertEqual(len(ewallet_2_bbb), 1)
  565. self.assertAlmostEqual(ewallet_2_bbb.points, 0, places=2)
  566. def test_coupon_change_pricelist(self):
  567. """Test coupon program with different pricelists."""
  568. product_1 = self.env["product.product"].create(
  569. {
  570. "name": "Test Product 1",
  571. "type": "product",
  572. "list_price": 25,
  573. "available_in_pos": True,
  574. }
  575. )
  576. tax01 = self.env["account.tax"].create({
  577. "name": "C01 Tax",
  578. "amount": "0.00",
  579. })
  580. product_2 = self.env["product.product"].create(
  581. {
  582. "name": "Test Product 2",
  583. "type": "product",
  584. "list_price": 25,
  585. "available_in_pos": True,
  586. "taxes_id": [(6, 0, [tax01.id])],
  587. }
  588. )
  589. pricelist = self.env["product.pricelist"].create({
  590. "name": "Test multi-currency",
  591. "discount_policy": "without_discount",
  592. "currency_id": self.env.ref("base.USD").id,
  593. "item_ids": [
  594. (0, 0, {
  595. "base": "standard_price",
  596. "product_id": product_1.id,
  597. "compute_price": "percentage",
  598. "percent_price": 50,
  599. }),
  600. (0, 0, {
  601. "base": "standard_price",
  602. "product_id": product_2.id,
  603. "compute_price": "percentage",
  604. "percent_price": 50,
  605. })
  606. ]
  607. })
  608. self.main_pos_config2 = self.main_pos_config.copy()
  609. loyalty_program = self.env['loyalty.program'].create({
  610. 'name': 'Coupon Program - Pricelist',
  611. 'program_type': 'coupons',
  612. 'trigger': 'with_code',
  613. 'applies_on': 'current',
  614. 'pos_ok': True,
  615. 'pos_config_ids': [Command.link(self.main_pos_config2.id)],
  616. 'rule_ids': [(0, 0, {
  617. 'reward_point_mode': 'order',
  618. 'reward_point_amount': 1,
  619. 'minimum_amount': 0,
  620. })],
  621. 'reward_ids': [(0, 0, {
  622. 'reward_type': 'discount',
  623. 'required_points': 1,
  624. 'discount': 100,
  625. 'discount_mode': 'percent',
  626. 'discount_applicability': 'order',
  627. })],
  628. })
  629. self.env["loyalty.generate.wizard"].with_context(
  630. {"active_id": loyalty_program.id}
  631. ).create({"coupon_qty": 1, 'points_granted': 4.5}).generate_coupons()
  632. self.coupon1 = loyalty_program.coupon_ids
  633. self.coupon1.write({"code": "abcda"})
  634. self.main_pos_config2.write({
  635. 'use_pricelist': True,
  636. 'available_pricelist_ids': [(4, pricelist.id), (4, self.main_pos_config.pricelist_id.id)],
  637. 'pricelist_id': pricelist.id,
  638. })
  639. self.main_pos_config2.open_ui()
  640. self.start_tour(
  641. "/pos/web?config_id=%d" % self.main_pos_config2.id,
  642. "PosLoyaltyTour4",
  643. login="accountman",
  644. )
  645. def test_promotion_program_with_global_discount(self):
  646. """
  647. - Create a promotion with a discount of 10%
  648. - Create a product with no taxes
  649. - Enable the global discount feature, and make sure the Discount product
  650. has a tax set on it.
  651. """
  652. if not self.env["ir.module.module"].search([("name", "=", "pos_discount"), ("state", "=", "installed")]):
  653. self.skipTest("pos_discount module is required for this test")
  654. tax01 = self.env["account.tax"].create({
  655. "name": "C01 Tax",
  656. "amount": "0.00"
  657. })
  658. self.discount_product = self.env["product.product"].create(
  659. {
  660. "name": "Discount Product",
  661. "type": "service",
  662. "list_price": 0,
  663. "available_in_pos": True,
  664. "taxes_id": [(6, 0, tax01.ids)],
  665. }
  666. )
  667. self.main_pos_config2 = self.main_pos_config.copy()
  668. self.main_pos_config2.write({
  669. 'module_pos_discount': True,
  670. 'discount_product_id': self.discount_product.id,
  671. 'discount_pc': 20,
  672. })
  673. self.loyalty_program = self.env['loyalty.program'].create({
  674. 'name': 'Coupon Program - Pricelist',
  675. 'program_type': 'coupons',
  676. 'trigger': 'with_code',
  677. 'applies_on': 'current',
  678. 'pos_ok': True,
  679. 'pos_config_ids': [Command.link(self.main_pos_config2.id)],
  680. 'rule_ids': [(0, 0, {
  681. 'reward_point_mode': 'order',
  682. 'reward_point_amount': 1,
  683. 'minimum_amount': 0,
  684. })],
  685. 'reward_ids': [(0, 0, {
  686. 'reward_type': 'discount',
  687. 'required_points': 1,
  688. 'discount': 10,
  689. 'discount_mode': 'percent',
  690. 'discount_applicability': 'order',
  691. })],
  692. })
  693. self.product = self.env["product.product"].create(
  694. {
  695. "name": "Test Product 1",
  696. "type": "product",
  697. "list_price": 100,
  698. "available_in_pos": True,
  699. }
  700. )
  701. self.main_pos_config2.open_ui()
  702. self.start_tour(
  703. "/pos/web?config_id=%d" % self.main_pos_config2.id,
  704. "PosLoyaltyTour5",
  705. login="accountman",
  706. )
  707. def test_loyalty_program_using_same_product(self):
  708. """
  709. - Create a loyalty program giving free product A for 30 points
  710. - Trigger the condition of the program using the same product A
  711. """
  712. LoyaltyProgram = self.env['loyalty.program']
  713. (LoyaltyProgram.search([])).write({'pos_ok': False})
  714. self.product_a = self.env["product.product"].create({
  715. "name": "Test Product A",
  716. "type": "product",
  717. "list_price": 10,
  718. "available_in_pos": True,
  719. })
  720. self.loyalty_program = self.env['loyalty.program'].create({
  721. 'name': 'Loyalty Program Test',
  722. 'program_type': 'loyalty',
  723. 'trigger': 'auto',
  724. 'applies_on': 'both',
  725. 'pos_ok': True,
  726. 'pos_config_ids': [Command.link(self.main_pos_config.id)],
  727. 'rule_ids': [(0, 0, {
  728. 'reward_point_mode': 'order',
  729. 'reward_point_amount': 10,
  730. 'minimum_amount': 5,
  731. 'minimum_qty': 1,
  732. })],
  733. 'reward_ids': [(0, 0, {
  734. 'reward_type': 'product',
  735. 'required_points': 30,
  736. 'reward_product_id': self.product_a.id,
  737. 'reward_product_qty': 1,
  738. })],
  739. })
  740. partner_aaa = self.env['res.partner'].create({'name': 'AAA Partner'})
  741. self.env['loyalty.card'].create({
  742. 'partner_id': partner_aaa.id,
  743. 'program_id': self.loyalty_program.id,
  744. 'points': 30,
  745. })
  746. self.main_pos_config.open_ui()
  747. self.start_tour(
  748. "/pos/web?config_id=%d" % self.main_pos_config.id,
  749. "PosLoyaltyFreeProductTour2",
  750. login="accountman",
  751. )
  752. def test_loyalty_program_specific_product(self):
  753. #create a loyalty program with a rules of minimum 2 qty that applies on produt A and B and reward 5 points. The reward is 10$ per order in exchange of 2 points on product A and B
  754. LoyaltyProgram = self.env['loyalty.program']
  755. (LoyaltyProgram.search([])).write({'pos_ok': False})
  756. self.product_a = self.env["product.product"].create({
  757. "name": "Test Product A",
  758. "type": "product",
  759. "list_price": 40,
  760. "available_in_pos": True,
  761. "taxes_id": False,
  762. })
  763. self.product_b = self.env["product.product"].create({
  764. "name": "Test Product B",
  765. "type": "product",
  766. "list_price": 40,
  767. "available_in_pos": True,
  768. "taxes_id": False,
  769. })
  770. self.loyalty_program = self.env['loyalty.program'].create({
  771. 'name': 'Loyalty Program Test',
  772. 'program_type': 'loyalty',
  773. 'trigger': 'auto',
  774. 'pos_ok': True,
  775. 'rule_ids': [(0, 0, {
  776. 'reward_point_mode': 'order',
  777. 'reward_point_amount': 5,
  778. 'minimum_qty': 2,
  779. 'product_ids': [(6, 0, [self.product_a.id, self.product_b.id])],
  780. })],
  781. 'reward_ids': [(0, 0, {
  782. 'reward_type': 'discount',
  783. 'discount_mode': 'per_order',
  784. 'required_points': 2,
  785. 'discount': 10,
  786. 'discount_applicability': 'specific',
  787. 'discount_product_ids': (self.product_a | self.product_b).ids,
  788. })],
  789. })
  790. self.main_pos_config.open_ui()
  791. self.start_tour("/pos/web?config_id=%d" % self.main_pos_config.id, "PosLoyaltySpecificDiscountTour", login="accountman")
  792. def test_discount_specific_product_with_free_product(self):
  793. LoyaltyProgram = self.env['loyalty.program']
  794. (LoyaltyProgram.search([])).write({'pos_ok': False})
  795. self.product_a = self.env['product.product'].create({
  796. 'name': 'Test Product A',
  797. 'type': 'product',
  798. 'list_price': 40,
  799. 'available_in_pos': True,
  800. 'taxes_id': False,
  801. })
  802. self.product_b = self.env['product.product'].create({
  803. 'name': 'Test Product B',
  804. 'type': 'product',
  805. 'list_price': 80,
  806. 'available_in_pos': True,
  807. 'taxes_id': False,
  808. })
  809. self.product_c = self.env['product.product'].create({
  810. 'name': 'Test Product C',
  811. 'type': 'product',
  812. 'list_price': 100,
  813. 'available_in_pos': True,
  814. 'taxes_id': False,
  815. })
  816. self.env['loyalty.program'].create({
  817. 'name': 'Discount 10%',
  818. 'program_type': 'promotion',
  819. 'trigger': 'auto',
  820. 'applies_on': 'current',
  821. 'rule_ids': [(0, 0, {
  822. 'reward_point_mode': 'order',
  823. 'reward_point_amount': 1,
  824. 'minimum_amount': 10,
  825. })],
  826. 'reward_ids': [(0, 0, {
  827. 'reward_type': 'discount',
  828. 'discount_product_ids': self.product_c.ids,
  829. 'required_points': 1,
  830. 'discount': 10,
  831. 'discount_mode': 'percent',
  832. 'discount_applicability': 'specific',
  833. })],
  834. 'pos_config_ids': [Command.link(self.main_pos_config.id)],
  835. })
  836. self.env['loyalty.program'].create({
  837. 'name': 'Buy product_a Take product_b',
  838. 'program_type': 'buy_x_get_y',
  839. 'trigger': 'auto',
  840. 'applies_on': 'current',
  841. 'rule_ids': [(0, 0, {
  842. 'product_ids': self.product_a.ids,
  843. 'reward_point_mode': 'unit',
  844. 'minimum_qty': 1,
  845. })],
  846. 'reward_ids': [(0, 0, {
  847. 'reward_type': 'product',
  848. 'reward_product_id': self.product_b.id,
  849. 'reward_product_qty': 1,
  850. 'required_points': 1,
  851. })],
  852. 'pos_config_ids': [Command.link(self.main_pos_config.id)],
  853. })
  854. self.main_pos_config.open_ui()
  855. self.start_tour('/pos/web?config_id=%d' % self.main_pos_config.id, 'PosLoyaltySpecificDiscountWithFreeProductTour', login='accountman')
  856. def test_point_per_money_spent(self):
  857. """Test the point per $ spent feature"""
  858. LoyaltyProgram = self.env['loyalty.program']
  859. (LoyaltyProgram.search([])).write({'pos_ok': False})
  860. self.loyalty_program = self.env['loyalty.program'].create({
  861. 'name': 'Loyalty Program Test',
  862. 'program_type': 'loyalty',
  863. 'trigger': 'auto',
  864. 'applies_on': 'both',
  865. 'pos_ok': True,
  866. 'pos_config_ids': [Command.link(self.main_pos_config.id)],
  867. 'rule_ids': [(0, 0, {
  868. 'reward_point_mode': 'money',
  869. 'reward_point_amount': 0.1,
  870. 'minimum_amount': 1,
  871. })],
  872. 'reward_ids': [(0, 0, {
  873. 'reward_type': 'discount',
  874. 'required_points': 1,
  875. 'discount': 1,
  876. 'discount_mode': 'per_point',
  877. })],
  878. })
  879. self.product_a = self.env["product.product"].create({
  880. "name": "Test Product A",
  881. "type": "product",
  882. "list_price": 265,
  883. "available_in_pos": True,
  884. "taxes_id": False,
  885. })
  886. partner_aaa = self.env['res.partner'].create({'name': 'AAA Partner'})
  887. self.env['loyalty.card'].create({
  888. 'partner_id': partner_aaa.id,
  889. 'program_id': self.loyalty_program.id,
  890. 'points': 100,
  891. })
  892. self.main_pos_config.open_ui()
  893. self.start_tour(
  894. "/pos/web?config_id=%d" % self.main_pos_config.id,
  895. "PosLoyaltyTour6",
  896. login="accountman",
  897. )
  898. def test_coupon_program_without_rules(self):
  899. self.env['loyalty.program'].search([]).write({'active': False})
  900. self.env["product.product"].create(
  901. {
  902. "name": "Test Product",
  903. "type": "product",
  904. "list_price": 100,
  905. "available_in_pos": True,
  906. "taxes_id": False,
  907. }
  908. )
  909. # creating a coupon program without any rule
  910. loyalty_program = self.env['loyalty.program'].create({
  911. 'name': 'Coupon Program without rules',
  912. 'program_type': 'coupons',
  913. 'trigger': 'with_code',
  914. 'applies_on': 'current',
  915. 'pos_ok': True,
  916. 'rule_ids': [],
  917. 'reward_ids': [(0, 0, {
  918. 'reward_type': 'discount',
  919. 'discount': 10,
  920. 'discount_mode': 'percent',
  921. 'discount_applicability': 'order',
  922. })],
  923. })
  924. self.env["loyalty.generate.wizard"].with_context(
  925. {"active_id": loyalty_program.id}
  926. ).create({"coupon_qty": 1, 'points_granted': 1}).generate_coupons()
  927. self.coupon1 = loyalty_program.coupon_ids
  928. self.coupon1.write({"code": "abcda"})
  929. self.main_pos_config.open_ui()
  930. self.start_tour(
  931. "/pos/web?config_id=%d" % self.main_pos_config.id,
  932. "PosLoyaltyTour7",
  933. login="accountman",
  934. )
  935. def test_promotion_program_with_loyalty_program(self):
  936. """
  937. - Create a promotion with a discount of 10%
  938. - Create a loyalty program with a fixed discount of 10€
  939. - Apply both programs to the order
  940. - Check that no "infinity" discount is applied
  941. """
  942. self.env['loyalty.program'].search([]).write({'active': False})
  943. self.promo_program = self.env['loyalty.program'].create({
  944. 'name': 'Promo Program',
  945. 'program_type': 'promotion',
  946. 'pos_ok': True,
  947. 'rule_ids': [(0, 0, {
  948. 'minimum_amount': 0,
  949. 'minimum_qty': 0
  950. })],
  951. 'reward_ids': [(0, 0, {
  952. 'reward_type': 'discount',
  953. 'discount': 10,
  954. 'discount_mode': 'percent',
  955. 'discount_applicability': 'order',
  956. })],
  957. })
  958. self.discount_product = self.env["product.product"].create(
  959. {
  960. "name": "Discount Product",
  961. "type": "service",
  962. "list_price": 0,
  963. "available_in_pos": True,
  964. "taxes_id": False,
  965. }
  966. )
  967. self.test_product = self.env["product.product"].create(
  968. {
  969. "name": "Test Product 1",
  970. "type": "product",
  971. "list_price": 100,
  972. "available_in_pos": True,
  973. "taxes_id": False,
  974. }
  975. )
  976. self.loyalty_program = self.env["loyalty.program"].create(
  977. {
  978. "name": "Loyalty Program",
  979. "program_type": "loyalty",
  980. "pos_ok": True,
  981. "rule_ids": [(0, 0, {
  982. "minimum_amount": 1,
  983. "minimum_qty": 1,
  984. "reward_point_mode": "order",
  985. "reward_point_amount": 500,
  986. })],
  987. "reward_ids": [(0, 0, {
  988. "required_points": 500,
  989. "reward_type": "discount",
  990. "discount": "10",
  991. "discount_mode": "per_order",
  992. })],
  993. }
  994. )
  995. partner = self.env['res.partner'].create({'name': 'AAA Partner'})
  996. self.env['loyalty.card'].create({
  997. 'partner_id': partner.id,
  998. 'program_id': self.loyalty_program.id,
  999. 'points': 500,
  1000. })
  1001. self.main_pos_config.open_ui()
  1002. self.start_tour(
  1003. "/pos/web?config_id=%d" % self.main_pos_config.id,
  1004. "PosLoyaltyPromotion",
  1005. login="accountman",
  1006. )
  1007. def test_discount_with_reward_product_domain(self):
  1008. self.env['loyalty.program'].search([]).write({'active': False})
  1009. product_category_base = self.env.ref('product.product_category_1')
  1010. product_category_1 = self.env['product.category'].create({
  1011. 'name': 'Office furnitures',
  1012. 'parent_id': product_category_base.id
  1013. })
  1014. self.productA = self.env['product.product'].create(
  1015. {
  1016. 'name': 'Product A',
  1017. 'type': 'product',
  1018. 'list_price': 15,
  1019. 'available_in_pos': True,
  1020. 'taxes_id': False,
  1021. 'categ_id': product_category_base.id
  1022. }
  1023. )
  1024. self.productB = self.env['product.product'].create(
  1025. {
  1026. 'name': 'Product B',
  1027. 'type': 'product',
  1028. 'list_price': 50,
  1029. 'available_in_pos': True,
  1030. 'taxes_id': False,
  1031. 'categ_id': product_category_1.id
  1032. }
  1033. )
  1034. self.env['loyalty.program'].create({
  1035. 'name': 'Discount on Specific Products',
  1036. 'program_type': 'promotion',
  1037. 'trigger': 'auto',
  1038. 'applies_on': 'current',
  1039. 'rule_ids': [(0, 0, {
  1040. 'reward_point_mode': 'order',
  1041. 'minimum_qty': 1,
  1042. })],
  1043. 'reward_ids': [(0, 0, {
  1044. 'reward_type': 'discount',
  1045. 'required_points': 1,
  1046. 'discount': 50,
  1047. 'discount_mode': 'percent',
  1048. 'discount_applicability': 'specific',
  1049. 'discount_product_domain': '[("categ_id", "ilike", "office")]',
  1050. })],
  1051. 'pos_config_ids': [Command.link(self.main_pos_config.id)],
  1052. })
  1053. self.main_pos_config.open_ui()
  1054. self.start_tour(
  1055. "/pos/web?config_id=%d" % self.main_pos_config.id,
  1056. "PosLoyaltySpecificDiscountWithRewardProductDomainTour",
  1057. login="accountman",
  1058. )