details: https://code.tryton.org/tryton/commit/9b5bd944259b
branch: default
user: Cédric Krier <[email protected]>
date: Wed Jul 29 15:48:59 2026 +0200
description:
Add scheduled task to cancel abandoned draft sale from web shop
Closes #14974
diffstat:
modules/web_shop/CHANGELOG | 1 +
modules/web_shop/ir.py | 16 ++++++++
modules/web_shop/sale.py | 19 ++++++++++
modules/web_shop/sale.xml | 8 ++++
modules/web_shop/tests/test_module.py | 64 +++++++++++++++++++++++++++++++++-
modules/web_shop/tryton.cfg | 1 +
modules/web_shop/view/shop_form.xml | 2 +
modules/web_shop/web.py | 11 ++++++
8 files changed, 120 insertions(+), 2 deletions(-)
diffs (209 lines):
diff -r bd2d0b0cc6fa -r 9b5bd944259b modules/web_shop/CHANGELOG
--- a/modules/web_shop/CHANGELOG Thu Jul 23 14:52:10 2026 +0200
+++ b/modules/web_shop/CHANGELOG Wed Jul 29 15:48:59 2026 +0200
@@ -1,3 +1,4 @@
+* Add scheduled task to cancel abandoned draft sale
Version 8.0.0 - 2026-04-20
--------------------------
diff -r bd2d0b0cc6fa -r 9b5bd944259b modules/web_shop/ir.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/web_shop/ir.py Wed Jul 29 15:48:59 2026 +0200
@@ -0,0 +1,16 @@
+# This file is part of Tryton. The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
+
+from trytond.pool import PoolMeta
+
+
+class Cron(metaclass=PoolMeta):
+ __name__ = 'ir.cron'
+
+ @classmethod
+ def __setup__(cls):
+ super().__setup__()
+ cls.method.selection.extend([
+ ('sale.sale|web_cancel_draft_abandoned',
+ "Cancel Abandoned Sales from Web Shop"),
+ ])
diff -r bd2d0b0cc6fa -r 9b5bd944259b modules/web_shop/sale.py
--- a/modules/web_shop/sale.py Thu Jul 23 14:52:10 2026 +0200
+++ b/modules/web_shop/sale.py Wed Jul 29 15:48:59 2026 +0200
@@ -1,5 +1,7 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
+
+import datetime as dt
from itertools import groupby
from secrets import token_hex
@@ -111,6 +113,23 @@
for web_shop, s_sales in groupby(sales, lambda s: s.web_shop):
web_shop.update_sales(list(s_sales))
+ @classmethod
+ def web_cancel_draft_abandoned(cls):
+ pool = Pool()
+ WebShop = pool.get('web.shop')
+ now = dt.datetime.now()
+ sales = []
+ for web_shop in WebShop.search([
+ ('sale_draft_abandon_delay', '!=', None),
+ ]):
+ sales.extend(cls.search([
+ ('state', '=', 'draft'),
+ ('web_shop', '=', web_shop),
+ ('last_modified_at', '<',
+ now - web_shop.sale_draft_abandon_delay),
+ ]))
+ cls.cancel(sales)
+
class Line_GiftCard(metaclass=PoolMeta):
__name__ = 'sale.line'
diff -r bd2d0b0cc6fa -r 9b5bd944259b modules/web_shop/sale.xml
--- a/modules/web_shop/sale.xml Thu Jul 23 14:52:10 2026 +0200
+++ b/modules/web_shop/sale.xml Wed Jul 29 15:48:59 2026 +0200
@@ -25,4 +25,12 @@
<field name="group" ref="sale.group_sale_admin"/>
</record>
</data>
+
+ <data noupdate="1">
+ <record model="ir.cron" id="cron_cancel_draft_abandoned">
+ <field name="method">sale.sale|web_cancel_draft_abandoned</field>
+ <field name="interval_number" eval="1"/>
+ <field name="interval_type">days</field>
+ </record>
+ </data>
</tryton>
diff -r bd2d0b0cc6fa -r 9b5bd944259b modules/web_shop/tests/test_module.py
--- a/modules/web_shop/tests/test_module.py Thu Jul 23 14:52:10 2026 +0200
+++ b/modules/web_shop/tests/test_module.py Wed Jul 29 15:48:59 2026 +0200
@@ -1,8 +1,12 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
-from trytond.modules.company.tests import CompanyTestMixin
-from trytond.tests.test_tryton import ModuleTestCase
+import datetime as dt
+
+from trytond.modules.company.tests import (
+ CompanyTestMixin, create_company, set_company)
+from trytond.pool import Pool
+from trytond.tests.test_tryton import ModuleTestCase, with_transaction
class WebShopTestCase(CompanyTestMixin, ModuleTestCase):
@@ -12,5 +16,61 @@
'account_tax_rule_country', 'product_attribute', 'product_image',
'sale_price_list']
+ @with_transaction()
+ def test_cancel_abandoned(self):
+ "Test cancel abandoned sale"
+ pool = Pool()
+ WebShop = pool.get('web.shop')
+ Party = pool.get('party.party')
+ Sale = pool.get('sale.sale')
+
+ guest = Party(name="Guest")
+ guest.save()
+ company = create_company()
+ with set_company(company):
+ web_shop = WebShop(
+ name="Test",
+ guest_party=guest,
+ sale_draft_abandon_delay=dt.timedelta(),
+ )
+ web_shop.save()
+
+ sale = web_shop.get_sale()
+ sale.save()
+ self.assertEqual(sale.state, 'draft')
+
+ Sale.web_cancel_draft_abandoned()
+
+ sale = Sale(sale.id)
+ self.assertEqual(sale.state, 'cancelled')
+
+ @with_transaction()
+ def test_not_cancel_abandoned_no_delay(self):
+ "Test not cancel abandoned sale without delay"
+ pool = Pool()
+ WebShop = pool.get('web.shop')
+ Party = pool.get('party.party')
+ Sale = pool.get('sale.sale')
+
+ guest = Party(name="Guest")
+ guest.save()
+ company = create_company()
+ with set_company(company):
+ web_shop = WebShop(
+ name="Test",
+ guest_party=guest,
+ sale_draft_abandon_delay=None,
+ )
+ web_shop.save()
+
+ sale = web_shop.get_sale()
+ sale.save()
+ self.assertEqual(sale.state, 'draft')
+
+ Sale.web_cancel_draft_abandoned()
+
+ sale = Sale(sale.id)
+ self.assertEqual(sale.state, 'draft')
+
del ModuleTestCase
diff -r bd2d0b0cc6fa -r 9b5bd944259b modules/web_shop/tryton.cfg
--- a/modules/web_shop/tryton.cfg Thu Jul 23 14:52:10 2026 +0200
+++ b/modules/web_shop/tryton.cfg Wed Jul 29 15:48:59 2026 +0200
@@ -25,6 +25,7 @@
[register]
model:
+ ir.Cron
web.Shop
web.Shop_Warehouse
web.Shop_Country
diff -r bd2d0b0cc6fa -r 9b5bd944259b modules/web_shop/view/shop_form.xml
--- a/modules/web_shop/view/shop_form.xml Thu Jul 23 14:52:10 2026 +0200
+++ b/modules/web_shop/view/shop_form.xml Wed Jul 29 15:48:59 2026 +0200
@@ -19,6 +19,8 @@
<label name="guest_party"/>
<field name="guest_party"/>
+ <label name="sale_draft_abandon_delay"/>
+ <field name="sale_draft_abandon_delay"/>
<notebook colspan="4">
<page name="warehouses">
diff -r bd2d0b0cc6fa -r 9b5bd944259b modules/web_shop/web.py
--- a/modules/web_shop/web.py Thu Jul 23 14:52:10 2026 +0200
+++ b/modules/web_shop/web.py Wed Jul 29 15:48:59 2026 +0200
@@ -1,5 +1,7 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
+
+import datetime as dt
from collections import defaultdict
from sql import Literal
@@ -70,6 +72,15 @@
'company': Eval('company', -1),
},
depends={'company'})
+ sale_draft_abandon_delay = fields.TimeDelta(
+ "Sale Draft Abandon Delay",
+ domain=['OR',
+ ('sale_draft_abandon_delay', '>=', dt.timedelta()),
+ ('sale_draft_abandon_delay', '=', None),
+ ],
+ help="The time period after which a sale made via the web shop "
+ "without modification is cancelled.\n"
+ "Leave empty to never cancel any draft sales.")
countries = fields.Many2Many(
'web.shop-country.country', 'shop', 'country', "Countries")