diff --git a/website_coupon/controllers/main.py b/website_coupon/controllers/main.py index abcbc7e2b..aa2e9099b 100644 --- a/website_coupon/controllers/main.py +++ b/website_coupon/controllers/main.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from datetime import datetime -from dateutil import parser from odoo import http from odoo.http import request @@ -55,124 +53,26 @@ class WebsiteCoupon(http.Controller): applied and coupon balance will also be updated. """ - curr_user = request.env.user coupon = request.env['gift.coupon'].sudo().search( - [('code', '=', promo_voucher)], limit=1) - flag = True - if coupon and coupon.total_avail > 0: - applied_coupons = request.env['partner.coupon'].sudo().search( - [('coupon', '=', promo_voucher), - ('partner_id', '=', curr_user.partner_id.id)], - limit=1) + [('code', '=', promo_voucher)]) + base_url = '/shop/cart' - # checking voucher date and limit for each user for this coupon - if coupon.partner_id: - if curr_user.partner_id.id != coupon.partner_id.id: - flag = False - today = datetime.now().date() - voucher_expiry = parser.parse(coupon.voucher.expiry_date).date() - if (flag - and applied_coupons.number < coupon.limit - and today <= voucher_expiry): - # checking coupon validity - # checking date of coupon - if coupon.start_date and coupon.end_date: - if (today < parser.parse(coupon.start_date).date() - or today > parser.parse(coupon.end_date).date()): - flag = False - elif coupon.start_date: - if today < parser.parse(coupon.start_date).date(): - flag = False - elif coupon.end_date: - if today > parser.parse(coupon.end_date).date(): - flag = False - else: - flag = False - else: - flag = False - if flag: - voucher_type = coupon.voucher.voucher_type - voucher_val = coupon.voucher_val - type = coupon.type - coupon_product = request.env['product.product'].sudo().search( - [('name', '=', 'Gift Coupon')], limit=1) - if coupon_product: - order = request.website.sale_get_order(force_create=1) - flag_product = False - for line in order.order_line: - if line.product_id.name == 'Gift Coupon': - flag = False - break - if flag and order.order_line: - if voucher_type == 'product': - # the voucher type is product - product_id = coupon.voucher.product_id - for line in order.order_line: - if line.product_id.name == product_id.name: - flag_product = True - elif voucher_type == 'category': - # the voucher type is category - voucher_cat = coupon.voucher.product_categ - for ol in order.order_line: - if ol.product_id.categ_id.name == voucher_cat.name: - flag_product = True - elif voucher_type == 'all': - # the voucher is applicable to all products - flag_product = True - if flag_product: - # the voucher is applicable - if type == 'fixed': - # coupon type is 'fixed' - if voucher_val < order.amount_total: - coupon_product.product_tmpl_id.write( - {'list_price': -voucher_val}) + if not coupon or not coupon.is_valid(request.env.user.partner_id): + return request.redirect(base_url + '?coupon_not_available=1') - else: - return request.redirect( - "/shop/cart?coupon_not_available=3") - elif type == 'percentage': - # coupon type is percentage - amount_final = 0 - factor = (voucher_val / 100) - if voucher_type == 'product': - for ol in order.order_line: - if ol.product_id.name == product_id.name: - amount_final = factor * ol.price_total - break - elif voucher_type == 'category': - for ol in order.order_line: - cat_name = ol.product_id.categ_id.name - if cat_name == voucher_cat.name: - amount_final += factor * ol.price_total - elif voucher_type == 'all': - amount_final = factor * order.amount_total - coupon_product.product_tmpl_id.write( - {'list_price': -amount_final}) - order._cart_update(product_id=coupon_product.id, - set_qty=1, add_qty=1) - # updating coupon balance - total = coupon.total_avail - 1 - coupon.write({'total_avail': total}) - # creating a record for this partner, - # i.e he is used this coupon once - if not applied_coupons: - curr_user.partner_id.write({ - 'applied_coupon': [(0, 0, { - 'partner_id': curr_user.partner_id.id, - 'coupon': coupon.code, - 'number': 1, - })] - }) - else: - applied_coupons.write( - {'number': applied_coupons.number + 1}) - else: - return request.redirect( - "/shop/cart?coupon_not_available=1") - else: - return request.redirect( - "/shop/cart?coupon_not_available=2") - else: - return request.redirect("/shop/cart?coupon_not_available=1") + order = request.website.sale_get_order(force_create=1) + gift_product = request.env['product.product'].sudo().search( + [('name', '=', 'Gift Coupon')], limit=1) - return request.redirect("/shop/cart") + # Make sure the gift product is not already in current order + if any(True for line in order.order_line + if line.product_id == gift_product): + return request.redirect(base_url + '?coupon_not_available=2') + + used, amount = coupon.consume_coupon(order) + if used: + gift_product.product_tmpl_id.list_price = -amount + order._cart_update(product_id=gift_product.id, set_qty=used) + return request.redirect(base_url) + else: + return request.redirect(base_url + '?coupon_not_available=1') diff --git a/website_coupon/models/gift_voucher.py b/website_coupon/models/gift_voucher.py index 9e5362232..c531f1817 100644 --- a/website_coupon/models/gift_voucher.py +++ b/website_coupon/models/gift_voucher.py @@ -21,6 +21,9 @@ # ############################################################################## +from datetime import datetime +from dateutil import parser + import string import random @@ -46,6 +49,19 @@ class GiftVoucher(models.Model): max_value = fields.Integer(string="Maximum Voucher Value", required=True) expiry_date = fields.Date(string="Expiry Date", required=True) + def is_order_line_eligible(self, order_line): + if self.voucher_type == 'product': + return order_line.product_id == self.product_id + elif self.voucher_type == 'category': + return order_line.product_id.categ_id == self.product_categ + elif self.voucher_type == 'all': + return True + + @api.multi + def eligible_lines(self, order): + self.ensure_one() + return filter(self.is_order_line_eligible, order.order_line or ()) + class GiftCoupon(models.Model): _name = 'gift.coupon' @@ -80,6 +96,108 @@ class GiftCoupon(models.Model): or self.voucher_val < self.voucher.min_value): raise UserError(_("Please check the voucher value")) + @api.multi + def amount(self, product_price): + if self.type == 'fixed': + return self.voucher_val + elif self.type == 'percentage': + return product_price * self.voucher_val / 100. + + @api.multi + def applied_coupons(self, partner): + self.ensure_one() + return self.env['partner.coupon'].sudo().search([ + ('coupon', '=', self.code), + ('partner_id', '=', partner.id), + ], limit=1) + + @api.multi + def available_coupons(self, partner): + if not self.limit > 0: + return self.total_avail + else: + return min(self.total_avail, + self.limit - self.applied_coupons(partner).number or 0) + + @api.multi + def consume_coupon(self, order): + """ Consume present coupon as much as possible to reduce given order's + price, and return a integer number of consumed coupons and a (positive) + amount. + """ + + # Determine coupon amount from eligible order lines + eligible_lines = self.voucher.eligible_lines(order) + if not eligible_lines: + return 0, 0 + + available = self.available_coupons(order.partner_id) + + coupons_used, coupons_amount = 0, 0. + for line in sorted(eligible_lines, reverse=True, + key=lambda l: l.product_id.list_price): + used_qtty = min(line.product_uom_qty, available) + if used_qtty: + coupons_amount += used_qtty * self.amount( + line.product_id.list_price) + coupons_used += used_qtty + available -= used_qtty + + if coupons_used: + # update coupon balance + self.write({'total_avail': self.total_avail - coupons_used}) + # create a record for this partner: he has used this coupon once + self.use_coupon(order.partner_id, coupons_used) + + return coupons_used, coupons_amount + + @api.multi + def use_coupon(self, partner, number): + """ Register given partner usage of the coupon `number` of times + and adjust the total_avail attribute of the coupon accordingly. + """ + self.ensure_one() + applied_coupons = self.applied_coupons(partner) + if not applied_coupons: + partner.write({ + 'applied_coupon': [(0, 0, { + 'partner_id': partner.id, + 'coupon': self.code, + 'number': number, + })] + }) + else: + applied_coupons.write( + {'number': applied_coupons.number + number}) + self.total_avail -= number + + @api.multi + def is_valid(self, partner): + self.ensure_one() + + if self.total_avail <= 0: + return False + + if self.partner_id and self.partner_id != partner: + return False + + if (self.limit > 0 + and self.applied_coupons(partner).number >= self.limit): + return False + + today = datetime.now().date() + + if today > parser.parse(self.voucher.expiry_date).date(): + return False + + if self.start_date and today < parser.parse(self.start_date).date(): + return False + + if self.end_date and today > parser.parse(self.end_date).date(): + return False + + return True + class CouponPartner(models.Model): _name = 'partner.coupon' diff --git a/website_coupon/tests/__init__.py b/website_coupon/tests/__init__.py new file mode 100644 index 000000000..065dc6ea0 --- /dev/null +++ b/website_coupon/tests/__init__.py @@ -0,0 +1 @@ +import test_coupon # noqa diff --git a/website_coupon/tests/test_coupon.py b/website_coupon/tests/test_coupon.py new file mode 100644 index 000000000..4ccca6ad3 --- /dev/null +++ b/website_coupon/tests/test_coupon.py @@ -0,0 +1,194 @@ +from datetime import datetime, timedelta + +from odoo.tests.common import TransactionCase, at_install, post_install + + +@at_install(False) +@post_install(True) +class CouponTC(TransactionCase): + + def setUp(self): + super(CouponTC, self).setUp() + self.voucher = self.env['gift.voucher'].create(self.base_voucher_vals) + self.coupon = self.env['gift.coupon'].create(self.base_coupon_vals) + self.partner1 = self.env.ref('base.res_partner_1') + self.partner2 = self.env.ref('base.res_partner_2') + + @property + def base_voucher_vals(self): + return { + 'name': 'test voucher', + 'voucher_type': 'all', + 'min_value': 0., + 'max_value': 100000., + 'expiry_date': '2100-01-01', + 'product_id': False, + 'product_categ': False, + } + + @property + def base_coupon_vals(self): + return { + 'name': 'test coupon', + 'code': 'TEST_CODE', + 'total_avail': '1000', + 'voucher_val': 10, + 'type': 'fixed', + 'voucher': self.voucher.id, + 'partner_id': False, + 'limit': 0, # means no limit + 'start_date': False, + 'end_date': False, + } + + def reset_coupon(self): + self.voucher.update(self.base_voucher_vals) + self.coupon.update(self.base_coupon_vals) + self.env['partner.coupon'].sudo().search([ + ('coupon', '=', self.coupon.code), + ]).unlink() + assert self.coupon.is_valid(self.partner1) + assert self.coupon.is_valid(self.partner2) + + def test_use_coupon(self): + self.reset_coupon() + + self.coupon.use_coupon(self.partner1, 10) + self.assertEqual(self.coupon.total_avail, 990) + self.coupon.use_coupon(self.partner1, 20) + self.assertEqual(self.coupon.total_avail, 970) + self.assertEqual(self.coupon.applied_coupons(self.partner1).number, 30) + + self.coupon.use_coupon(self.partner2, 50) + self.assertEqual(self.coupon.total_avail, 920) + self.assertEqual(self.coupon.applied_coupons(self.partner2).number, 50) + + def test_is_valid(self): + partner1 = self.partner1 + partner2 = self.partner2 + coupon = self.coupon + + self.reset_coupon() + + # Check partner field behaviour + coupon.partner_id = partner1.id + self.assertFalse(coupon.is_valid(partner2)) + self.assertTrue(coupon.is_valid(partner1)) + + # Check total_avail + partner.coupon behaviour + coupon.total_avail = 0 + self.assertFalse(coupon.is_valid(partner2)) + + self.reset_coupon() + + coupon.partner_id = partner2.id + coupon.use_coupon(partner2, 999) + self.assertTrue(coupon.is_valid(partner2)) + coupon.use_coupon(partner2, 1) + self.assertFalse(coupon.is_valid(partner2)) + + self.reset_coupon() + + # Check per-partner limit behaviour + coupon.limit = 10 + coupon.use_coupon(partner2, 9) + self.assertTrue(coupon.is_valid(partner2)) + coupon.use_coupon(partner2, 1) + self.assertFalse(coupon.is_valid(partner2)) + + self.reset_coupon() + + # Check time limits + today = datetime.now().date() + coupon.start_date = today + timedelta(days=1) + self.assertFalse(coupon.is_valid(partner2)) + + self.reset_coupon() + + coupon.end_date = today - timedelta(days=1) + self.assertFalse(coupon.is_valid(partner2)) + + coupon.start_date = today - timedelta(days=10) + coupon.end_date = today - timedelta(days=2) + self.assertFalse(coupon.is_valid(partner2)) + + coupon.start_date = today - timedelta(days=1) + coupon.end_date = today + timedelta(days=1) + self.assertTrue(coupon.is_valid(partner2)) + + def test_consume_coupon(self): + order = self.env['sale.order'].create({'partner_id': self.partner2.id}) + categ = self.env['product.category'].create({'name': 'test categ 1'}) + product1 = self.env['product.product'].create({ + 'name': 'test product 1', + 'list_price': 100., + 'categ_id': categ.id, + }) + product2 = self.env['product.product'].create({ + 'name': 'test product 2', + 'list_price': 300., + }) + product3 = self.env['product.product'].create({ + 'name': 'test product 3', + 'list_price': 200., + 'categ_id': categ.id, + }) + self.env['sale.order.line'].create({ + 'name': 'test line 1', + 'product_id': product1.id, + 'product_uom_qty': 10, + 'order_id': order.id, + 'product_uom': product1.uom_id.id, + 'price_unit': 120., + }) + self.env['sale.order.line'].create({ + 'name': 'test line 2', + 'product_id': product2.id, + 'product_uom_qty': 5, + 'order_id': order.id, + 'product_uom': product2.uom_id.id, + 'price_unit': 360., + }) + self.env['sale.order.line'].create({ + 'name': 'test line 3', + 'product_id': product3.id, + 'product_uom_qty': 20, + 'order_id': order.id, + 'product_uom': product3.uom_id.id, + 'price_unit': 240., + }) + + used, amount = self.coupon.consume_coupon(order) + self.assertEqual((used, amount), (35, 350)) + + self.reset_coupon() + + self.coupon.type = 'percentage' + self.coupon.voucher_val = 10 + used, amount = self.coupon.consume_coupon(order) + self.assertEqual((used, amount), (35, 10*10+5*30+20*20)) + + self.reset_coupon() + + self.coupon.type = 'percentage' + self.coupon.voucher_val = 10 + self.voucher.voucher_type = 'product' + self.voucher.product_id = product2.id + used, amount = self.coupon.consume_coupon(order) + self.assertEqual((used, amount), (5, 5*30)) + + self.reset_coupon() + + self.coupon.type = 'percentage' + self.coupon.voucher_val = 10 + self.voucher.voucher_type = 'category' + self.voucher.product_categ = categ.id + used, amount = self.coupon.consume_coupon(order) + self.assertEqual((used, amount), (10+20, 10*10+20*20)) + + self.reset_coupon() + + self.coupon.type = 'percentage' + self.coupon.voucher_val = 10 + self.coupon.total_avail = 7 + used, amount = self.coupon.consume_coupon(order) diff --git a/website_coupon/views/gift_voucher.xml b/website_coupon/views/gift_voucher.xml index 52562f663..6a362b215 100644 --- a/website_coupon/views/gift_voucher.xml +++ b/website_coupon/views/gift_voucher.xml @@ -18,6 +18,7 @@ + @@ -122,4 +123,4 @@ - \ No newline at end of file +