From a5fc6326825944aea2c9ccebb3913711cde22936 Mon Sep 17 00:00:00 2001 From: Benoit Socias Date: Thu, 12 Nov 2020 13:03:44 +0000 Subject: [PATCH] [FIX] website_sale: find abandoned cart using specific delay per website MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before this commit the search for abandoned carts was always using a delay of one hour if no ebsite in the context. After this commit the search for abandoned carts uses the specific cart abandoned delay configured for each website and the related public partner_id. This commit closes #59709, #54932 closes odoo/odoo#61671 Signed-off-by: Jérémy Kersten (jke) --- addons/website_sale/models/sale_order.py | 18 ++- addons/website_sale/tests/__init__.py | 1 + .../tests/test_website_sale_cart_abandoned.py | 130 ++++++++++++++++++ 3 files changed, 142 insertions(+), 7 deletions(-) create mode 100644 addons/website_sale/tests/test_website_sale_cart_abandoned.py diff --git a/addons/website_sale/models/sale_order.py b/addons/website_sale/models/sale_order.py index c71a6201f9f..ec01b4afad7 100644 --- a/addons/website_sale/models/sale_order.py +++ b/addons/website_sale/models/sale_order.py @@ -55,15 +55,19 @@ class SaleOrder(models.Model): order.is_abandoned_cart = False def _search_abandoned_cart(self, operator, value): - abandoned_delay = self.website_id and self.website_id.cart_abandoned_delay or 1.0 - abandoned_datetime = fields.Datetime.to_string(datetime.utcnow() - relativedelta(hours=abandoned_delay)) - abandoned_domain = expression.normalize_domain([ - ('date_order', '<=', abandoned_datetime), - ('website_id', '!=', False), + website_ids = self.env['website'].search_read(fields=['id', 'cart_abandoned_delay', 'partner_id']) + deadlines = [[ + '&', '&', + ('website_id', '=', website_id['id']), + ('date_order', '<=', fields.Datetime.to_string(datetime.utcnow() - relativedelta(hours=website_id['cart_abandoned_delay'] or 1.0))), + ('partner_id', '!=', website_id['partner_id'][0]) + ] for website_id in website_ids] + abandoned_domain = [ ('state', '=', 'draft'), - ('partner_id', '!=', self.env.ref('base.public_partner').id), ('order_line', '!=', False) - ]) + ] + abandoned_domain.extend(expression.OR(deadlines)) + abandoned_domain = expression.normalize_domain(abandoned_domain) # is_abandoned domain possibilities if (operator not in expression.NEGATIVE_TERM_OPERATORS and value) or (operator in expression.NEGATIVE_TERM_OPERATORS and not value): return abandoned_domain diff --git a/addons/website_sale/tests/__init__.py b/addons/website_sale/tests/__init__.py index dc94da2711a..fdc363141e7 100644 --- a/addons/website_sale/tests/__init__.py +++ b/addons/website_sale/tests/__init__.py @@ -4,6 +4,7 @@ from . import test_customize from . import test_sale_process from . import test_sitemap +from . import test_website_sale_cart_abandoned from . import test_website_sale_cart_recovery from . import test_website_sale_mail from . import test_website_sale_pricelist diff --git a/addons/website_sale/tests/test_website_sale_cart_abandoned.py b/addons/website_sale/tests/test_website_sale_cart_abandoned.py new file mode 100644 index 00000000000..8957017bd36 --- /dev/null +++ b/addons/website_sale/tests/test_website_sale_cart_abandoned.py @@ -0,0 +1,130 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import datetime +from dateutil.relativedelta import relativedelta + +from odoo.tests import tagged +from odoo.addons.base.tests.common import HttpCaseWithUserPortal + +@tagged('post_install', '-at_install', 'bso') +class TestWebsiteSaleCartAbandoned(HttpCaseWithUserPortal): + def setUp(self): + res = super(TestWebsiteSaleCartAbandoned, self).setUp() + now = datetime.utcnow() + self.customer = self.env['res.partner'].create({ + 'name': 'a', + 'email': 'a@example.com', + }) + self.public_partner = self.env['res.partner'].create({ + 'name': 'public', + 'email': 'public@example.com', + }) + self.public_user = self.env['res.users'].create({ + 'name': 'Foo', 'login': 'foo', + 'partner_id': self.public_partner.id, + }) + self.website0 = self.env['website'].create({ + 'name': 'web0', + 'cart_abandoned_delay': 1.0, # 1 hour + }) + self.website1 = self.env['website'].create({ + 'name': 'web1', + 'cart_abandoned_delay': 0.5, # 30 minutes + }) + self.website2 = self.env['website'].create({ + 'name': 'web2', + 'cart_abandoned_delay': 24.0, # 1 day + 'user_id': self.public_user.id, # specific public user + }) + product = self.env['product.product'].create({ + 'name': 'The Product' + }) + add_order_line = [[0, 0, { + 'name': 'The Product', + 'product_id': product.id, + 'product_uom_qty': 1, + }]] + self.so0before = self.env['sale.order'].create({ + 'partner_id': self.customer.id, + 'website_id': self.website0.id, + 'state': 'draft', + 'date_order': (now - relativedelta(hours=1)) - relativedelta(minutes=1), + 'order_line': add_order_line, + }) + self.so0after = self.env['sale.order'].create({ + 'partner_id': self.customer.id, + 'website_id': self.website0.id, + 'state': 'draft', + 'date_order': (now - relativedelta(hours=1)) + relativedelta(minutes=1), + 'order_line': add_order_line, + }) + self.so1before = self.env['sale.order'].create({ + 'partner_id': self.customer.id, + 'website_id': self.website1.id, + 'state': 'draft', + 'date_order': (now - relativedelta(minutes=30)) - relativedelta(minutes=1), + 'order_line': add_order_line, + }) + self.so1after = self.env['sale.order'].create({ + 'partner_id': self.customer.id, + 'website_id': self.website1.id, + 'state': 'draft', + 'date_order': (now - relativedelta(minutes=30)) + relativedelta(minutes=1), + 'order_line': add_order_line, + }) + self.so2before = self.env['sale.order'].create({ + 'partner_id': self.customer.id, + 'website_id': self.website2.id, + 'state': 'draft', + 'date_order': (now - relativedelta(hours=24)) - relativedelta(minutes=1), + 'order_line': add_order_line, + }) + self.so2after = self.env['sale.order'].create({ + 'partner_id': self.customer.id, + 'website_id': self.website2.id, + 'state': 'draft', + 'date_order': (now - relativedelta(hours=24)) + relativedelta(minutes=1), + 'order_line': add_order_line, + }) + self.so2before_but_public = self.env['sale.order'].create({ + 'partner_id': self.public_partner.id, + 'website_id': self.website2.id, + 'state': 'draft', + 'date_order': (now - relativedelta(hours=24)) - relativedelta(minutes=1), + 'order_line': add_order_line, + }) + + # Must behave like so1before because public partner is not the one of website1 + self.so1before_but_other_public = self.env['sale.order'].create({ + 'partner_id': self.public_partner.id, + 'website_id': self.website1.id, + 'state': 'draft', + 'date_order': (now - relativedelta(minutes=30)) - relativedelta(minutes=1), + 'order_line': add_order_line, + }) + + return res + + def test_search_abandoned_cart(self): + """Make sure the search for abandoned carts uses the delay and public partner specified in each website.""" + SaleOrder = self.env['sale.order'] + abandoned = SaleOrder.search([('is_abandoned_cart', '=', True)]).ids + self.assertTrue(self.so0before.id in abandoned) + self.assertTrue(self.so1before.id in abandoned) + self.assertTrue(self.so1before_but_other_public.id in abandoned) + self.assertTrue(self.so2before.id in abandoned) + self.assertFalse(self.so0after.id in abandoned) + self.assertFalse(self.so1after.id in abandoned) + self.assertFalse(self.so2after.id in abandoned) + self.assertFalse(self.so2before_but_public.id in abandoned) + + non_abandoned = SaleOrder.search([('is_abandoned_cart', '=', False)]).ids + self.assertFalse(self.so0before.id in non_abandoned) + self.assertFalse(self.so1before.id in non_abandoned) + self.assertFalse(self.so1before_but_other_public.id in non_abandoned) + self.assertFalse(self.so2before.id in non_abandoned) + self.assertTrue(self.so0after.id in non_abandoned) + self.assertTrue(self.so1after.id in non_abandoned) + self.assertTrue(self.so2after.id in non_abandoned) + self.assertFalse(self.so2before_but_public.id in abandoned)