You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

476 lines
17 KiB

  1. /* Copyright 2004-2010 OpenERP SA
  2. * Copyright 2017 RGB Consulting S.L. (https://www.rgbconsulting.com)
  3. * License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */
  4. odoo.define('pos_loyalty.loyalty_program', function(require) {
  5. "use strict"
  6. var models = require('point_of_sale.models');
  7. var screens = require('point_of_sale.screens');
  8. var utils = require('web.utils');
  9. var round_pr = utils.round_precision;
  10. var core = require('web.core');
  11. var QWeb = core.qweb;
  12. var _t = core._t;
  13. models.load_fields('res.partner', 'loyalty_points');
  14. models.load_models([{
  15. model: 'loyalty.program',
  16. condition: function(self) {
  17. return !!self.config.loyalty_id[0];
  18. },
  19. fields: ['name', 'pp_currency', 'pp_product', 'pp_order', 'rounding'],
  20. domain: function(self) {
  21. return [
  22. ['id', '=', self.config.loyalty_id[0]]
  23. ];
  24. },
  25. loaded: function(self, loyalties) {
  26. self.loyalty = loyalties[0];
  27. },
  28. }, {
  29. model: 'loyalty.rule',
  30. condition: function(self) {
  31. return !!self.loyalty;
  32. },
  33. fields: ['name', 'type', 'product_id', 'category_id', 'cumulative', 'pp_product', 'pp_currency'],
  34. domain: function(self) {
  35. return [
  36. ['loyalty_program_id', '=', self.loyalty.id]
  37. ];
  38. },
  39. loaded: function(self, rules) {
  40. self.loyalty.rules = rules;
  41. self.loyalty.rules_by_product_id = {};
  42. self.loyalty.rules_by_category_id = {};
  43. function update_rules(rules, rule, id) {
  44. if (!rules[id]) {
  45. rules[id] = [rule];
  46. } else if (rule.cumulative) {
  47. rules[id].unshift(rule);
  48. } else {
  49. rules[id].push(rule);
  50. }
  51. }
  52. _.each(rules, function(rule) {
  53. if (rule.type === 'product')
  54. update_rules(self.loyalty.rules_by_product_id, rule, rule.product_id[0])
  55. else if (rule.type === 'category')
  56. update_rules(self.loyalty.rules_by_category_id, rule, rule.category_id[0]);
  57. });
  58. },
  59. }, {
  60. model: 'loyalty.reward',
  61. condition: function(self) {
  62. return !!self.loyalty;
  63. },
  64. fields: ['name', 'type', 'minimum_points', 'gift_product_id', 'point_cost', 'discount_product_id', 'discount', 'discount_max', 'point_product_id'],
  65. domain: function(self) {
  66. return [
  67. ['loyalty_program_id', '=', self.loyalty.id]
  68. ];
  69. },
  70. loaded: function(self, rewards) {
  71. self.loyalty.rewards = rewards;
  72. self.loyalty.rewards_by_id = {};
  73. for (var i = 0; i < rewards.length; i++) {
  74. self.loyalty.rewards_by_id[rewards[i].id] = rewards[i];
  75. }
  76. },
  77. }, ], {
  78. 'after': 'product.product'
  79. });
  80. var _orderline_super = models.Orderline.prototype;
  81. models.Orderline = models.Orderline.extend({
  82. get_reward: function() {
  83. return this.pos.loyalty.rewards_by_id[this.reward_id];
  84. },
  85. set_reward: function(reward) {
  86. this.reward_id = reward.id;
  87. },
  88. export_as_JSON: function() {
  89. var json = _orderline_super.export_as_JSON.apply(this, arguments);
  90. json.reward_id = this.reward_id;
  91. return json;
  92. },
  93. init_from_JSON: function(json) {
  94. _orderline_super.init_from_JSON.apply(this, arguments);
  95. this.reward_id = json.reward_id;
  96. },
  97. });
  98. var _order_super = models.Order.prototype;
  99. models.Order = models.Order.extend({
  100. /* The total of points won, excluding the points spent on rewards */
  101. get_won_points: function() {
  102. if (!this.pos.loyalty || !this.get_client()) {
  103. return 0;
  104. }
  105. var orderLines = this.get_orderlines();
  106. var rounding = this.pos.loyalty.rounding;
  107. var product_sold = 0;
  108. var total_sold = 0;
  109. var total_points = 0;
  110. for (var i = 0; i < orderLines.length; i++) {
  111. var line = orderLines[i];
  112. var product = line.get_product();
  113. var rules = this.pos.loyalty.rules_by_product_id[product.id] || [];
  114. var overriden = false;
  115. if (line.get_reward()) { // Reward products are ignored
  116. continue;
  117. }
  118. for (var j = 0; j < rules.length; j++) {
  119. var rule = rules[j];
  120. total_points += round_pr(line.get_quantity() * rule.pp_product, rounding);
  121. total_points += round_pr(line.get_price_with_tax() * rule.pp_currency, rounding);
  122. // if affected by a non cumulative rule, skip the others. (non cumulative rules are put
  123. // at the beginning of the list when they are loaded )
  124. if (!rule.cumulative) {
  125. overriden = true;
  126. break;
  127. }
  128. }
  129. // Test the category rules
  130. if (product.pos_categ_id) {
  131. var category = this.pos.db.get_category_by_id(product.pos_categ_id[0]);
  132. while (category && !overriden) {
  133. var rules = this.pos.loyalty.rules_by_category_id[category.id] || [];
  134. for (var j = 0; j < rules.length; j++) {
  135. var rule = rules[j];
  136. total_points += round_pr(line.get_quantity() * rule.pp_product, rounding);
  137. total_points += round_pr(line.get_price_with_tax() * rule.pp_currency, rounding);
  138. if (!rule.cumulative) {
  139. overriden = true;
  140. break;
  141. }
  142. }
  143. var _category = category;
  144. category = this.pos.db.get_category_by_id(this.pos.db.get_category_parent_id(category.id));
  145. if (_category === category) {
  146. break;
  147. }
  148. }
  149. }
  150. if (!overriden) {
  151. product_sold += line.get_quantity();
  152. total_sold += line.get_price_with_tax();
  153. }
  154. }
  155. total_points += round_pr(total_sold * this.pos.loyalty.pp_currency, rounding);
  156. total_points += round_pr(product_sold * this.pos.loyalty.pp_product, rounding);
  157. total_points += round_pr(this.pos.loyalty.pp_order, rounding);
  158. return total_points;
  159. },
  160. /* The total number of points spent on rewards */
  161. get_spent_points: function() {
  162. if (!this.pos.loyalty || !this.get_client()) {
  163. return 0;
  164. } else {
  165. var lines = this.get_orderlines();
  166. var rounding = this.pos.loyalty.rounding;
  167. var points = 0;
  168. for (var i = 0; i < lines.length; i++) {
  169. var line = lines[i];
  170. var reward = line.get_reward();
  171. if (reward) {
  172. if (reward.type === 'gift') {
  173. points += round_pr(line.get_quantity() * reward.point_cost, rounding);
  174. } else if (reward.type === 'discount') {
  175. points += reward.point_cost;
  176. } else if (reward.type === 'resale') {
  177. points += (-line.get_quantity());
  178. }
  179. }
  180. }
  181. return points;
  182. }
  183. },
  184. /* The total number of points lost or won after the order is validated */
  185. get_new_points: function() {
  186. if (!this.pos.loyalty || !this.get_client()) {
  187. return 0;
  188. } else {
  189. return round_pr(this.get_won_points() - this.get_spent_points(), this.pos.loyalty.rounding);
  190. }
  191. },
  192. /* The total number of points that the customer will have after this order is validated */
  193. get_new_total_points: function() {
  194. if (!this.pos.loyalty || !this.get_client()) {
  195. return 0;
  196. } else {
  197. return round_pr(this.get_client().loyalty_points + this.get_new_points(), this.pos.loyalty.rounding);
  198. }
  199. },
  200. /* The number of loyalty points currently owned by the customer */
  201. get_current_points: function() {
  202. return this.get_client() ? this.get_client().loyalty_points : 0;
  203. },
  204. /* The total number of points spendable on rewards */
  205. get_spendable_points: function() {
  206. if (!this.pos.loyalty || !this.get_client()) {
  207. return 0;
  208. } else {
  209. return round_pr(this.get_client().loyalty_points - this.get_spent_points(), this.pos.loyalty.rounding);
  210. }
  211. },
  212. has_discount_reward: function() {
  213. var res = false;
  214. var lines = this.get_orderlines();
  215. for (var i = 0; i < lines.length; i++) {
  216. var line = lines[i];
  217. var reward = line.get_reward();
  218. if (reward && reward.type === 'discount') {
  219. res = true;
  220. break;
  221. }
  222. }
  223. return res;
  224. },
  225. /* The list of rewards that the current customer can get */
  226. get_available_rewards: function() {
  227. var client = this.get_client();
  228. if (!client) {
  229. return [];
  230. }
  231. var rewards = [];
  232. var discount_reward_set = this.has_discount_reward();
  233. for (var i = 0; i < this.pos.loyalty.rewards.length; i++) {
  234. var reward = this.pos.loyalty.rewards[i];
  235. if (reward.minimum_points > this.get_spendable_points()) {
  236. continue;
  237. } else if (reward.type === 'gift' &&
  238. reward.point_cost > this.get_spendable_points()) {
  239. continue;
  240. } else if (reward.type === 'discount' &&
  241. (discount_reward_set || reward.point_cost > this.get_spendable_points())) {
  242. continue;
  243. }
  244. rewards.push(reward);
  245. }
  246. return rewards;
  247. },
  248. apply_reward: function(reward) {
  249. var client = this.get_client();
  250. if (!client) {
  251. return;
  252. } else if (reward.type === 'gift') {
  253. var product = this.pos.db.get_product_by_id(reward.gift_product_id[0]);
  254. if (!product) {
  255. return;
  256. }
  257. this.add_product(product, {
  258. price: 0,
  259. quantity: 1,
  260. merge: false,
  261. extras: {
  262. reward_id: reward.id
  263. },
  264. });
  265. } else if (reward.type === 'discount') {
  266. var crounding = this.pos.currency.rounding;
  267. var order_total = this.get_total_with_tax();
  268. var discount = round_pr(order_total * reward.discount, crounding);
  269. var discount_max = reward.discount_max
  270. if (discount_max && discount > discount_max) {
  271. discount = discount_max;
  272. }
  273. var product = this.pos.db.get_product_by_id(reward.discount_product_id[0]);
  274. if (!product) {
  275. return;
  276. }
  277. this.add_product(product, {
  278. price: -discount,
  279. quantity: 1,
  280. merge: false,
  281. extras: {
  282. reward_id: reward.id
  283. },
  284. });
  285. } else if (reward.type === 'resale') {
  286. var lrounding = this.pos.loyalty.rounding;
  287. var crounding = this.pos.currency.rounding;
  288. var spendable = this.get_spendable_points();
  289. var order_total = this.get_total_with_tax();
  290. var product = this.pos.db.get_product_by_id(reward.point_product_id[0]);
  291. if (!product) {
  292. return;
  293. }
  294. if (round_pr(spendable * product.price, crounding) > order_total) {
  295. spendable = round_pr(Math.floor(order_total / product.price), lrounding);
  296. }
  297. if (spendable < 0.00001) {
  298. return;
  299. }
  300. this.add_product(product, {
  301. quantity: -spendable,
  302. merge: false,
  303. extras: {
  304. reward_id: reward.id
  305. },
  306. });
  307. }
  308. },
  309. finalize: function() {
  310. var client = this.get_client();
  311. if (client) {
  312. client.loyalty_points = this.get_new_total_points();
  313. this.pos.gui.screen_instances.clientlist.partner_cache.clear_node(client.id);
  314. }
  315. _order_super.finalize.apply(this, arguments);
  316. },
  317. export_for_printing: function() {
  318. var json = _order_super.export_for_printing.apply(this, arguments);
  319. if (this.pos.loyalty && this.get_client()) {
  320. json.loyalty = {
  321. rounding: this.pos.loyalty.rounding || 1,
  322. name: this.pos.loyalty.name,
  323. client: this.get_client().name,
  324. points_won: this.get_won_points(),
  325. points_spent: this.get_spent_points(),
  326. points_total: this.get_new_total_points(),
  327. };
  328. }
  329. return json;
  330. },
  331. export_as_JSON: function() {
  332. var json = _order_super.export_as_JSON.apply(this, arguments);
  333. json.loyalty_points = this.get_new_points();
  334. return json;
  335. },
  336. });
  337. var LoyaltyButton = screens.ActionButtonWidget.extend({
  338. template: 'LoyaltyButton',
  339. button_click: function() {
  340. var self = this;
  341. var order = this.pos.get_order();
  342. var client = order.get_client();
  343. if (!client) {
  344. this.gui.show_screen('clientlist');
  345. return;
  346. }
  347. var rewards = order.get_available_rewards();
  348. if (rewards.length === 0) {
  349. this.gui.show_popup('error', {
  350. 'title': _t('No Rewards Available'),
  351. 'body': _t('There are no rewards available for this customer as part of the loyalty program'),
  352. });
  353. } else if (rewards.length === 1 && this.pos.loyalty.rewards.length === 1) {
  354. order.apply_reward(rewards[0]);
  355. } else {
  356. var list = [];
  357. for (var i = 0; i < rewards.length; i++) {
  358. list.push({
  359. label: rewards[i].name,
  360. item: rewards[i],
  361. });
  362. }
  363. this.gui.show_popup('selection', {
  364. 'title': _t('Please select a reward'),
  365. 'list': list,
  366. 'confirm': function(reward) {
  367. order.apply_reward(reward);
  368. },
  369. });
  370. }
  371. },
  372. });
  373. screens.define_action_button({
  374. 'name': 'loyalty',
  375. 'widget': LoyaltyButton,
  376. 'condition': function() {
  377. return this.pos.loyalty && this.pos.loyalty.rewards.length;
  378. },
  379. });
  380. screens.OrderWidget.include({
  381. update_summary: function() {
  382. this._super();
  383. var order = this.pos.get_order();
  384. var $loypoints = $(this.el).find('.summary .loyalty-points');
  385. if (this.pos.loyalty && order.get_client()) {
  386. var points_won = order.get_won_points();
  387. var points_spent = order.get_spent_points();
  388. var points_total = order.get_new_total_points();
  389. $loypoints.replaceWith($(QWeb.render('LoyaltyPoints', {
  390. widget: this,
  391. rounding: this.pos.loyalty.rounding,
  392. points_won: points_won,
  393. points_spent: points_spent,
  394. points_total: points_total,
  395. })));
  396. $loypoints = $(this.el).find('.summary .loyalty-points');
  397. $loypoints.removeClass('oe_hidden');
  398. if (points_total < 0) {
  399. $loypoints.addClass('negative');
  400. } else {
  401. $loypoints.removeClass('negative');
  402. }
  403. } else {
  404. $loypoints.empty();
  405. $loypoints.addClass('oe_hidden');
  406. }
  407. if (this.pos.loyalty &&
  408. order.get_client() &&
  409. this.getParent().action_buttons &&
  410. this.getParent().action_buttons.loyalty) {
  411. var rewards = order.get_available_rewards();
  412. this.getParent().action_buttons.loyalty.highlight(!!rewards.length);
  413. }
  414. },
  415. });
  416. });