Browse Source

[website_coupon] Factor out utility methods from controller to gift.coupon model

... to ease unit-testing and make the controller more readable and the logic reusable.

The coupon usage logic is also fixed: there was no clear rule to compute
the number of coupon uses from an order. It was an hard-coded 1 value
although the coupon amount was dependant of the coupon type (category),
but not of the product quantity. The rules are now clearly established
and unit-tested: a customer will consume as much coupons as available to
him to reduce its order as much as possible.
pull/83/head
Florent Cayré 7 years ago
parent
commit
b1bd9698a3
  1. 138
      website_coupon/controllers/main.py
  2. 118
      website_coupon/models/gift_voucher.py
  3. 1
      website_coupon/tests/__init__.py
  4. 194
      website_coupon/tests/test_coupon.py
  5. 3
      website_coupon/views/gift_voucher.xml

138
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')

118
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'

1
website_coupon/tests/__init__.py

@ -0,0 +1 @@
import test_coupon # noqa

194
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)

3
website_coupon/views/gift_voucher.xml

@ -18,6 +18,7 @@
<field name="min_value" />
<field name="max_value" />
<field name="expiry_date" />
<field name="gift_product_id" />
</group>
</group>
</sheet>
@ -122,4 +123,4 @@
<menuitem name="Gift Coupons" id="gift_coupon_main" parent="sales_team.menu_base_partner"/>
<menuitem name="Gift Coupon" id="gift_coupon" action="action_gift_coupon" parent="gift_coupon_main"/>
</data>
</odoo>
</odoo>

Loading…
Cancel
Save