From d35ffd9ad955823dfd074ea6dd7a8439134a02f1 Mon Sep 17 00:00:00 2001 From: Romain Derie Date: Wed, 25 Sep 2019 14:54:20 +0000 Subject: [PATCH] [IMP] website_sale_coupon: garbage collect old order coupon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With this commit, when an ecommerce order is judged abandonned (more than 4 days without being touched, and in draft), we remove its coupon so it is available again. task-2058666 closes odoo/odoo#37441 Signed-off-by: Jérémy Kersten (jke) --- addons/website_sale_coupon/models/__init__.py | 1 + .../models/ir_autovacuum.py | 13 +++ .../website_sale_coupon/models/sale_order.py | 22 ++++- .../tests/test_shop_sale_coupon.py | 82 ++++++++++++++++++- 4 files changed, 112 insertions(+), 6 deletions(-) create mode 100644 addons/website_sale_coupon/models/ir_autovacuum.py diff --git a/addons/website_sale_coupon/models/__init__.py b/addons/website_sale_coupon/models/__init__.py index 28c756b4fed..d31916a0b3c 100644 --- a/addons/website_sale_coupon/models/__init__.py +++ b/addons/website_sale_coupon/models/__init__.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from . import ir_autovacuum from . import sale_coupon_program from . import sale_order diff --git a/addons/website_sale_coupon/models/ir_autovacuum.py b/addons/website_sale_coupon/models/ir_autovacuum.py new file mode 100644 index 00000000000..62b8b18cc48 --- /dev/null +++ b/addons/website_sale_coupon/models/ir_autovacuum.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, models + + +class AutoVacuum(models.AbstractModel): + _inherit = 'ir.autovacuum' + + @api.model + def power_on(self, *args, **kwargs): + self.env['sale.order']._garbage_collector(*args, **kwargs) + return super(AutoVacuum, self).power_on(*args, **kwargs) diff --git a/addons/website_sale_coupon/models/sale_order.py b/addons/website_sale_coupon/models/sale_order.py index 574e355455b..e9e07575bb2 100644 --- a/addons/website_sale_coupon/models/sale_order.py +++ b/addons/website_sale_coupon/models/sale_order.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- -from odoo import models, fields, api +from datetime import timedelta + +from odoo import api, fields, models from odoo.http import request @@ -50,7 +52,6 @@ class SaleOrder(models.Model): }) order.website_order_line -= program_lines - def _compute_cart_info(self): super(SaleOrder, self)._compute_cart_info() for order in self: @@ -75,3 +76,20 @@ class SaleOrder(models.Model): return self.env['sale.order.line'] free_shipping_product_ids = free_shipping_prgs_ids.mapped('discount_line_product_id') return self.order_line.filtered(lambda l: l.product_id in free_shipping_product_ids) + + @api.model + def _garbage_collector(self, *args, **kwargs): + """Remove/free coupon from abandonned ecommerce order.""" + ICP = self.env['ir.config_parameter'] + validity = ICP.get_param('website_sale_coupon.abandonned_coupon_validity', 4) + validity = fields.Datetime.to_string(fields.datetime.now() - timedelta(days=int(validity))) + coupon_to_reset = self.env['sale.coupon'].search([ + ('state', '=', 'used'), + ('sales_order_id.state', '=', 'draft'), + ('sales_order_id.write_date', '<', validity), + ('sales_order_id.website_id', '!=', False), + ]) + for coupon in coupon_to_reset: + coupon.sales_order_id.applied_coupon_ids -= coupon + coupon_to_reset.write({'state': 'new'}) + coupon_to_reset.mapped('sales_order_id').recompute_coupon_lines() diff --git a/addons/website_sale_coupon/tests/test_shop_sale_coupon.py b/addons/website_sale_coupon/tests/test_shop_sale_coupon.py index 76df76157a1..8cb5377ade3 100644 --- a/addons/website_sale_coupon/tests/test_shop_sale_coupon.py +++ b/addons/website_sale_coupon/tests/test_shop_sale_coupon.py @@ -1,15 +1,14 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. +from datetime import timedelta -from odoo.tests import HttpCase, tagged +from odoo import fields +from odoo.tests import HttpCase, tagged, TransactionCase from odoo.addons.sale.tests.test_sale_product_attribute_value_config import TestSaleProductAttributeValueSetup @tagged('post_install', '-at_install') class TestUi(TestSaleProductAttributeValueSetup, HttpCase): - post_install = True - at_install = False - def setUp(self): super(TestUi, self).setUp() @@ -22,3 +21,78 @@ class TestUi(TestSaleProductAttributeValueSetup, HttpCase): # pre enable "Show # found" option to avoid race condition... self.env.ref("website_sale.search_count_box").write({"active": True}) self.start_tour("/", 'shop_sale_coupon', login="admin") + + +@tagged('post_install', '-at_install') +class TestWebsiteSaleCoupon(TransactionCase): + + def setUp(self): + super(TestWebsiteSaleCoupon, self).setUp() + program = self.env['sale.coupon.program'].create({ + 'name': '10% TEST Discount', + 'promo_code_usage': 'code_needed', + 'discount_apply_on': 'on_order', + 'discount_type': 'percentage', + 'discount_percentage': 10.0, + 'program_type': 'coupon_program', + }) + + self.env['sale.coupon.generate'].with_context(active_id=program.id).create({}).generate_coupon() + self.coupon = program.coupon_ids[0] + + 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 + }) + + 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.env['sale.coupon.apply.code'].with_context(active_id=order.id).create({ + 'coupon_code': self.coupon.code + }).process_coupon() + order.recompute_coupon_lines() + + 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) + self.assertEqual(self.coupon.state, 'used') + + # 3. Test recent order -> Should not be removed + order._garbage_collector() + + self.assertEqual(len(order.applied_coupon_ids), 1, "The coupon shouldn't have been removed from the order no more than 4 days") + self.assertEqual(self.coupon.state, 'used', "Should not have been changed") + + # 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}) + order.flush() + 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._garbage_collector() + + 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") + self.assertEqual(self.coupon.state, 'used', "Should not have been changed (2)") + + # 5. Test order with no ICP and older then 4 default days -> Should be removed + icp_validity.unlink() + order._garbage_collector() + + self.assertEqual(len(order.applied_coupon_ids), 0, "The coupon should've been removed from the order as more than 4 days") + self.assertEqual(self.coupon.state, 'new', "Should have been reset.")