diff --git a/README.md b/README.md index 9c75be333e..0691927eac 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,11 @@ e-commerce [//]: # (addons) -This part will be replaced when running the oca-gen-addons-table script from OCA/maintainer-tools. +Available addons +---------------- +addon | version | maintainers | summary +--- | --- | --- | --- +[website_sale_cart_expire](website_sale_cart_expire/) | 19.0.1.0.0 | ivantodorovich | Cancel carts without activity after a configurable time [//]: # (end addons) diff --git a/setup/_metapackage/pyproject.toml b/setup/_metapackage/pyproject.toml new file mode 100644 index 0000000000..bc50fd483f --- /dev/null +++ b/setup/_metapackage/pyproject.toml @@ -0,0 +1,11 @@ +[project] +name = "odoo-addons-oca-e-commerce" +version = "19.0.20260319.0" +dependencies = [ + "odoo-addon-website_sale_cart_expire==19.0.*", +] +classifiers=[ + "Programming Language :: Python", + "Framework :: Odoo", + "Framework :: Odoo :: 19.0", +] diff --git a/website_sale_cart_expire/README.rst b/website_sale_cart_expire/README.rst new file mode 100644 index 0000000000..a8bdeb184a --- /dev/null +++ b/website_sale_cart_expire/README.rst @@ -0,0 +1,96 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +======================== +Website Sale Cart Expire +======================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:6cd8d8546507afbae383d3f912433c497edc0273f621ca48a86715e671ab687b + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fe--commerce-lightgray.png?logo=github + :target: https://github.com/OCA/e-commerce/tree/19.0/website_sale_cart_expire + :alt: OCA/e-commerce +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/e-commerce-19-0/e-commerce-19-0-website_sale_cart_expire + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/e-commerce&target_branch=19.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Allows to automatically cancel carts without activity after a +configurable time. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +Go to Website > Settings and set a delay for Expire Carts settings. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Camptocamp + +Contributors +------------ + +- `Camptocamp `__ + + - Iván Todorovich + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-ivantodorovich| image:: https://github.com/ivantodorovich.png?size=40px + :target: https://github.com/ivantodorovich + :alt: ivantodorovich + +Current `maintainer `__: + +|maintainer-ivantodorovich| + +This module is part of the `OCA/e-commerce `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/website_sale_cart_expire/__init__.py b/website_sale_cart_expire/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/website_sale_cart_expire/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/website_sale_cart_expire/__manifest__.py b/website_sale_cart_expire/__manifest__.py new file mode 100644 index 0000000000..30caab2d0d --- /dev/null +++ b/website_sale_cart_expire/__manifest__.py @@ -0,0 +1,19 @@ +# Copyright 2021 Camptocamp (http://www.camptocamp.com). +# @author Iván Todorovich +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Website Sale Cart Expire", + "summary": "Cancel carts without activity after a configurable time", + "version": "19.0.1.0.0", + "author": "Camptocamp, Odoo Community Association (OCA)", + "maintainers": ["ivantodorovich"], + "website": "https://github.com/OCA/e-commerce", + "license": "AGPL-3", + "category": "Website", + "depends": ["website_sale"], + "data": [ + "data/ir_cron.xml", + "views/res_config_settings.xml", + ], +} diff --git a/website_sale_cart_expire/data/ir_cron.xml b/website_sale_cart_expire/data/ir_cron.xml new file mode 100644 index 0000000000..9728a851b1 --- /dev/null +++ b/website_sale_cart_expire/data/ir_cron.xml @@ -0,0 +1,16 @@ + + + + + Website: Expire Carts + + code + model._scheduler_website_expire_cart(autocommit=True) + 5 + minutes + + diff --git a/website_sale_cart_expire/i18n/es.po b/website_sale_cart_expire/i18n/es.po new file mode 100644 index 0000000000..d776daf35c --- /dev/null +++ b/website_sale_cart_expire/i18n/es.po @@ -0,0 +1,112 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * website_sale_cart_expire +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2026-02-25 07:30+0000\n" +"Last-Translator: Anonymous \n" +"Language-Team: none\n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.15.2\n" + +#. module: website_sale_cart_expire +#: model_terms:ir.ui.view,arch_db:website_sale_cart_expire.res_config_settings_view_form +msgid "Automatically cancel carts without activity after a period of time" +msgstr "" +"Cancelar automáticamente carritos sin actividad después de un período de " +"tiempo" + +#. module: website_sale_cart_expire +#: model:ir.model.fields,help:website_sale_cart_expire.field_res_config_settings__cart_expire_delay +#: model:ir.model.fields,help:website_sale_cart_expire.field_website__cart_expire_delay +msgid "" +"Automatically cancel website orders after the given time.\n" +"Set to 0 to disable this feature." +msgstr "" +"Cancelar automáticamente los pedidos del sitio web después del tiempo dado.\n" +"Establecer en 0 para desactivar esta función." + +#. module: website_sale_cart_expire +#: model:ir.model.fields,field_description:website_sale_cart_expire.field_sale_order__cart_expire_date +msgid "Cart Expire Date" +msgstr "Fecha de caducidad del carro" + +#. module: website_sale_cart_expire +#. odoo-python +#: code:addons/website_sale_cart_expire/models/website.py:0 +msgid "Cart expired" +msgstr "Carro caducado" + +#. module: website_sale_cart_expire +#: model_terms:ir.ui.view,arch_db:website_sale_cart_expire.res_config_settings_view_form +msgid "Cart is cancelled after" +msgstr "El carro ha sido cancelado después" + +#. module: website_sale_cart_expire +#: model:ir.model,name:website_sale_cart_expire.model_res_config_settings +msgid "Config Settings" +msgstr "Ajustes de Configuración" + +#. module: website_sale_cart_expire +#: model_terms:ir.ui.view,arch_db:website_sale_cart_expire.res_config_settings_view_form +msgid "Expire Carts" +msgstr "" + +#. module: website_sale_cart_expire +#: model:ir.model.fields,field_description:website_sale_cart_expire.field_res_config_settings__cart_expire_delay +#: model:ir.model.fields,field_description:website_sale_cart_expire.field_website__cart_expire_delay +msgid "Expire Delay" +msgstr "Expiración retardada" + +#. module: website_sale_cart_expire +#: model_terms:ir.ui.view,arch_db:website_sale_cart_expire.res_config_settings_view_form +msgid "Hours." +msgstr "" + +#. module: website_sale_cart_expire +#: model:ir.model,name:website_sale_cart_expire.model_sale_order +msgid "Sales Order" +msgstr "Órdenes de venta" + +#. module: website_sale_cart_expire +#: model:ir.model.fields,help:website_sale_cart_expire.field_sale_order__cart_expire_date +msgid "Technical field: The date this cart will automatically expire" +msgstr "Campo técnico: La fecha en que este carro caducará automáticamente" + +#. module: website_sale_cart_expire +#: model_terms:ir.ui.view,arch_db:website_sale_cart_expire.res_config_settings_view_form +msgid "Values set here are website-specific." +msgstr "" + +#. module: website_sale_cart_expire +#: model:ir.model,name:website_sale_cart_expire.model_website +msgid "Website" +msgstr "Página Web" + +#. module: website_sale_cart_expire +#: model:ir.actions.server,name:website_sale_cart_expire.ir_cron_cart_expire_ir_actions_server +msgid "Website: Expire Carts" +msgstr "Página web: Carros caducados" + +#~ msgid "" +#~ "Expire Carts\n" +#~ " " +#~ msgstr "" +#~ "Carro expirado\n" +#~ " " + +#~ msgid "Carts are cancelled after this delay." +#~ msgstr "Los carros se cancelan después de este plazo." + +#~ msgid "hours." +#~ msgstr "horas." diff --git a/website_sale_cart_expire/i18n/it.po b/website_sale_cart_expire/i18n/it.po new file mode 100644 index 0000000000..be4f18a4d4 --- /dev/null +++ b/website_sale_cart_expire/i18n/it.po @@ -0,0 +1,111 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * website_sale_cart_expire +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2026-02-25 07:29+0000\n" +"Last-Translator: Anonymous \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.15.2\n" + +#. module: website_sale_cart_expire +#: model_terms:ir.ui.view,arch_db:website_sale_cart_expire.res_config_settings_view_form +msgid "Automatically cancel carts without activity after a period of time" +msgstr "" +"Annulla automaticamente i carrelli senza attività dopo un periodo di tempo" + +#. module: website_sale_cart_expire +#: model:ir.model.fields,help:website_sale_cart_expire.field_res_config_settings__cart_expire_delay +#: model:ir.model.fields,help:website_sale_cart_expire.field_website__cart_expire_delay +msgid "" +"Automatically cancel website orders after the given time.\n" +"Set to 0 to disable this feature." +msgstr "" +"Annulla automaticamente ordini sito web dopo un certo periodo di tempo.\n" +"Impostare a 0 per disabilitare questa opzione." + +#. module: website_sale_cart_expire +#: model:ir.model.fields,field_description:website_sale_cart_expire.field_sale_order__cart_expire_date +msgid "Cart Expire Date" +msgstr "Data scadenza carrello" + +#. module: website_sale_cart_expire +#. odoo-python +#: code:addons/website_sale_cart_expire/models/website.py:0 +msgid "Cart expired" +msgstr "Carrello scaduto" + +#. module: website_sale_cart_expire +#: model_terms:ir.ui.view,arch_db:website_sale_cart_expire.res_config_settings_view_form +msgid "Cart is cancelled after" +msgstr "Il carrello viene annullato dopo" + +#. module: website_sale_cart_expire +#: model:ir.model,name:website_sale_cart_expire.model_res_config_settings +msgid "Config Settings" +msgstr "Impostazioni configurazione" + +#. module: website_sale_cart_expire +#: model_terms:ir.ui.view,arch_db:website_sale_cart_expire.res_config_settings_view_form +msgid "Expire Carts" +msgstr "Scadenza carrelli" + +#. module: website_sale_cart_expire +#: model:ir.model.fields,field_description:website_sale_cart_expire.field_res_config_settings__cart_expire_delay +#: model:ir.model.fields,field_description:website_sale_cart_expire.field_website__cart_expire_delay +msgid "Expire Delay" +msgstr "Ritardo scadenza" + +#. module: website_sale_cart_expire +#: model_terms:ir.ui.view,arch_db:website_sale_cart_expire.res_config_settings_view_form +msgid "Hours." +msgstr "Ore." + +#. module: website_sale_cart_expire +#: model:ir.model,name:website_sale_cart_expire.model_sale_order +msgid "Sales Order" +msgstr "Ordine di vendita" + +#. module: website_sale_cart_expire +#: model:ir.model.fields,help:website_sale_cart_expire.field_sale_order__cart_expire_date +msgid "Technical field: The date this cart will automatically expire" +msgstr "Campo tecnico: la data in cui questo carrello scade automaticamente" + +#. module: website_sale_cart_expire +#: model_terms:ir.ui.view,arch_db:website_sale_cart_expire.res_config_settings_view_form +msgid "Values set here are website-specific." +msgstr "I valori impostati qui sono specifici per sito web." + +#. module: website_sale_cart_expire +#: model:ir.model,name:website_sale_cart_expire.model_website +msgid "Website" +msgstr "Sito web" + +#. module: website_sale_cart_expire +#: model:ir.actions.server,name:website_sale_cart_expire.ir_cron_cart_expire_ir_actions_server +msgid "Website: Expire Carts" +msgstr "Sito web: scadenza carrelli" + +#~ msgid "" +#~ "Expire Carts\n" +#~ " " +#~ msgstr "" +#~ "Carrelli in scadenza\n" +#~ " " + +#~ msgid "Carts are cancelled after this delay." +#~ msgstr "I carrelli vengono annullati dopo questo ritardo." + +#~ msgid "hours." +#~ msgstr "ore." diff --git a/website_sale_cart_expire/i18n/website_sale_cart_expire.pot b/website_sale_cart_expire/i18n/website_sale_cart_expire.pot new file mode 100644 index 0000000000..8117e91110 --- /dev/null +++ b/website_sale_cart_expire/i18n/website_sale_cart_expire.pot @@ -0,0 +1,98 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * website_sale_cart_expire +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 19.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: website_sale_cart_expire +#: model_terms:ir.ui.view,arch_db:website_sale_cart_expire.res_config_settings_view_form +msgid "Automatically cancel carts without activity after a period of time." +msgstr "" + +#. module: website_sale_cart_expire +#: model:ir.model.fields,help:website_sale_cart_expire.field_res_config_settings__cart_expire_delay +#: model:ir.model.fields,help:website_sale_cart_expire.field_website__cart_expire_delay +msgid "" +"Automatically cancel website orders after the given time.\n" +"Set to 0 to disable this feature." +msgstr "" + +#. module: website_sale_cart_expire +#: model:ir.model.fields,field_description:website_sale_cart_expire.field_sale_order__cart_expire_date +msgid "Cart Expire Date" +msgstr "" + +#. module: website_sale_cart_expire +#. odoo-python +#: code:addons/website_sale_cart_expire/models/website.py:0 +msgid "Cart expired" +msgstr "" + +#. module: website_sale_cart_expire +#: model_terms:ir.ui.view,arch_db:website_sale_cart_expire.res_config_settings_view_form +msgid "Cart is cancelled after" +msgstr "" + +#. module: website_sale_cart_expire +#: model:ir.model,name:website_sale_cart_expire.model_res_config_settings +msgid "Config Settings" +msgstr "" + +#. module: website_sale_cart_expire +#: model:ir.model.fields,field_description:website_sale_cart_expire.field_res_config_settings__display_name +#: model:ir.model.fields,field_description:website_sale_cart_expire.field_sale_order__display_name +#: model:ir.model.fields,field_description:website_sale_cart_expire.field_website__display_name +msgid "Display Name" +msgstr "" + +#. module: website_sale_cart_expire +#: model_terms:ir.ui.view,arch_db:website_sale_cart_expire.res_config_settings_view_form +msgid "Expire Carts" +msgstr "" + +#. module: website_sale_cart_expire +#: model:ir.model.fields,field_description:website_sale_cart_expire.field_res_config_settings__cart_expire_delay +#: model:ir.model.fields,field_description:website_sale_cart_expire.field_website__cart_expire_delay +msgid "Expire Delay" +msgstr "" + +#. module: website_sale_cart_expire +#: model:ir.model.fields,field_description:website_sale_cart_expire.field_res_config_settings__id +#: model:ir.model.fields,field_description:website_sale_cart_expire.field_sale_order__id +#: model:ir.model.fields,field_description:website_sale_cart_expire.field_website__id +msgid "ID" +msgstr "" + +#. module: website_sale_cart_expire +#: model:ir.model,name:website_sale_cart_expire.model_sale_order +msgid "Sales Order" +msgstr "" + +#. module: website_sale_cart_expire +#: model:ir.model.fields,help:website_sale_cart_expire.field_sale_order__cart_expire_date +msgid "Technical field: The date this cart will automatically expire" +msgstr "" + +#. module: website_sale_cart_expire +#: model:ir.model,name:website_sale_cart_expire.model_website +msgid "Website" +msgstr "" + +#. module: website_sale_cart_expire +#: model:ir.actions.server,name:website_sale_cart_expire.ir_cron_cart_expire_ir_actions_server +msgid "Website: Expire Carts" +msgstr "" + +#. module: website_sale_cart_expire +#: model_terms:ir.ui.view,arch_db:website_sale_cart_expire.res_config_settings_view_form +msgid "hours." +msgstr "" diff --git a/website_sale_cart_expire/models/__init__.py b/website_sale_cart_expire/models/__init__.py new file mode 100644 index 0000000000..0cf62fc1fa --- /dev/null +++ b/website_sale_cart_expire/models/__init__.py @@ -0,0 +1,3 @@ +from . import res_config_settings +from . import sale_order +from . import website diff --git a/website_sale_cart_expire/models/res_config_settings.py b/website_sale_cart_expire/models/res_config_settings.py new file mode 100644 index 0000000000..72190d947a --- /dev/null +++ b/website_sale_cart_expire/models/res_config_settings.py @@ -0,0 +1,13 @@ +# Copyright 2021 Camptocamp (http://www.camptocamp.com). +# @author Iván Todorovich +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + cart_expire_delay = fields.Float( + related="website_id.cart_expire_delay", readonly=False + ) diff --git a/website_sale_cart_expire/models/sale_order.py b/website_sale_cart_expire/models/sale_order.py new file mode 100644 index 0000000000..6b57c10b4c --- /dev/null +++ b/website_sale_cart_expire/models/sale_order.py @@ -0,0 +1,51 @@ +# Copyright 2021 Camptocamp (http://www.camptocamp.com). +# @author Iván Todorovich +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from datetime import timedelta + +from odoo import api, fields, models + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + cart_expire_date = fields.Datetime( + compute="_compute_cart_expire_date", + help="Technical field: The date this cart will automatically expire", + ) + + def _should_bypass_cart_expiration(self): + """Hook method to prevent a cart from expiring""" + self.ensure_one() + # We don't want to cancel carts that are already in payment. + return any( + tx.state in ["pending", "authorized", "done"] for tx in self.transaction_ids + ) + + @api.depends( + "write_date", + "website_id.cart_expire_delay", + "transaction_ids.last_state_change", + ) + def _compute_cart_expire_date(self): + for rec in self: + if ( + rec.state == "draft" + and rec.website_id.cart_expire_delay + and not rec._should_bypass_cart_expiration() + ): + # In case of draft records, use current date + from_date = rec.write_date or fields.Datetime.now() + # In case or records with transactions, consider last tx date + if rec.transaction_ids: + last_tx_date = max( + rec.transaction_ids.mapped( + lambda x: x.last_state_change or x.write_date + ) + ) + from_date = max(from_date, last_tx_date) + expire_delta = timedelta(hours=rec.website_id.cart_expire_delay) + rec.cart_expire_date = from_date + expire_delta + elif rec.cart_expire_date: + rec.cart_expire_date = False diff --git a/website_sale_cart_expire/models/website.py b/website_sale_cart_expire/models/website.py new file mode 100644 index 0000000000..2c4a16c74b --- /dev/null +++ b/website_sale_cart_expire/models/website.py @@ -0,0 +1,62 @@ +# Copyright 2021 Camptocamp (http://www.camptocamp.com). +# @author Iván Todorovich +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging +from datetime import timedelta + +from odoo import api, fields, models +from odoo.fields import Domain + +_logger = logging.getLogger(__name__) + + +class Website(models.Model): + _inherit = "website" + + cart_expire_delay = fields.Float( + string="Expire Delay", + default=0.0, + help="Automatically cancel website orders after the given time.\n" + "Set to 0 to disable this feature.", + ) + + def _get_cart_expire_delay_domain(self): + self.ensure_one() + expire_date = fields.Datetime.now() - timedelta(hours=self.cart_expire_delay) + return ( + Domain("website_id", "=", self.id) + & Domain("state", "=", "draft") + & Domain("write_date", "<=", expire_date) + # We don't want to cancel carts that are already in payment. + & ( + Domain( + "transaction_ids", + "not any", + Domain("state", "in", ["pending", "authorized", "done"]), + ) + ) + ) + + @api.model + def _scheduler_website_expire_cart(self, autocommit=False): + websites = self.search(Domain("cart_expire_delay", ">", 0)) + if not websites: + return True + carts = self.env["sale.order"].search( + Domain.OR([website._get_cart_expire_delay_domain() for website in websites]) + ) + now = fields.Datetime.now() + for cart in carts: + if not cart.cart_expire_date or cart.cart_expire_date > now: + continue + try: + with self.env.cr.savepoint(): + cart.message_post(body=self.env._("Cart expired")) + cart.action_cancel() + except Exception as e: + _logger.exception("Unable to cancel expired cart %s: %s", cart, e) + else: + if autocommit: + self.env.cr.commit() # pylint: disable=invalid-commit + return True diff --git a/website_sale_cart_expire/pyproject.toml b/website_sale_cart_expire/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/website_sale_cart_expire/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/website_sale_cart_expire/readme/CONFIGURE.md b/website_sale_cart_expire/readme/CONFIGURE.md new file mode 100644 index 0000000000..6934c881bc --- /dev/null +++ b/website_sale_cart_expire/readme/CONFIGURE.md @@ -0,0 +1 @@ +Go to Website \> Settings and set a delay for Expire Carts settings. diff --git a/website_sale_cart_expire/readme/CONTRIBUTORS.md b/website_sale_cart_expire/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..06927a8502 --- /dev/null +++ b/website_sale_cart_expire/readme/CONTRIBUTORS.md @@ -0,0 +1,3 @@ +- [Camptocamp](https://www.camptocamp.com) + + > - Iván Todorovich \<\> diff --git a/website_sale_cart_expire/readme/DESCRIPTION.md b/website_sale_cart_expire/readme/DESCRIPTION.md new file mode 100644 index 0000000000..74ce4d23d2 --- /dev/null +++ b/website_sale_cart_expire/readme/DESCRIPTION.md @@ -0,0 +1,2 @@ +Allows to automatically cancel carts without activity after a +configurable time. diff --git a/website_sale_cart_expire/static/description/icon.png b/website_sale_cart_expire/static/description/icon.png new file mode 100644 index 0000000000..3a0328b516 Binary files /dev/null and b/website_sale_cart_expire/static/description/icon.png differ diff --git a/website_sale_cart_expire/static/description/index.html b/website_sale_cart_expire/static/description/index.html new file mode 100644 index 0000000000..ea08795ead --- /dev/null +++ b/website_sale_cart_expire/static/description/index.html @@ -0,0 +1,443 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Website Sale Cart Expire

+ +

Beta License: AGPL-3 OCA/e-commerce Translate me on Weblate Try me on Runboat

+

Allows to automatically cancel carts without activity after a +configurable time.

+

Table of contents

+ +
+

Configuration

+

Go to Website > Settings and set a delay for Expire Carts settings.

+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Camptocamp
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

ivantodorovich

+

This module is part of the OCA/e-commerce project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+
+ + diff --git a/website_sale_cart_expire/tests/__init__.py b/website_sale_cart_expire/tests/__init__.py new file mode 100644 index 0000000000..7043f0665e --- /dev/null +++ b/website_sale_cart_expire/tests/__init__.py @@ -0,0 +1 @@ +from . import test_website_sale_cart_expire diff --git a/website_sale_cart_expire/tests/test_website_sale_cart_expire.py b/website_sale_cart_expire/tests/test_website_sale_cart_expire.py new file mode 100644 index 0000000000..b12b9e0134 --- /dev/null +++ b/website_sale_cart_expire/tests/test_website_sale_cart_expire.py @@ -0,0 +1,154 @@ +# Copyright 2021 Camptocamp (http://www.camptocamp.com). +# @author Iván Todorovich +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from datetime import datetime, timedelta + +from freezegun import freeze_time + +from odoo import Command, fields + +from odoo.addons.base.tests.common import BaseCommon + + +class TestWebsiteSaleCartExpire(BaseCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.tx_counter = 0 + # Websites + cls.website_1 = cls.env.ref("website.default_website") + cls.website_2 = cls.env["website"].create( + {"name": "My Website 2", "sequence": 20} + ) + cls.website_1.cart_expire_delay = 0.00 # hours (= disabled) + cls.website_2.cart_expire_delay = 2.00 # hours + cls.partner = cls.env["res.partner"].create({"name": "Test"}) + cls.product = cls.env["product.product"].create( + { + "name": "Desk Combination", + "type": "consu", + } + ) + # Orders + cls.order_1 = cls._create_cart_order() + cls.order_2 = cls._create_cart_order() + cls.order_3 = cls._create_cart_order() + cls.order_4 = cls._create_cart_order() + cls.orders = cls.order_1 | cls.order_2 | cls.order_3 | cls.order_4 + cls.payment_method = cls.env["payment.method"].create( + {"name": "Test_method", "code": "123"} + ) + + @classmethod + def _create_cart_order(cls): + return cls.env["sale.order"].create( + { + "partner_id": cls.partner.id, + "partner_invoice_id": cls.partner.id, + "partner_shipping_id": cls.partner.id, + # Set to draft and assign all to website_2 + # (this also updates write_date to now()) + "website_id": cls.website_2.id, + "state": "draft", + "order_line": [ + Command.create( + { + "product_id": cls.product.id, + "product_uom_qty": 1.0, + "price_unit": 100.0, + }, + ) + ], + } + ) + + def _create_payment_transaction(self, order): + self.tx_counter += 1 + provider = self.env.ref("payment.payment_provider_transfer") + provider.write( + { + "state": "enabled", + "is_published": True, + } + ) + provider._transfer_ensure_pending_msg_is_set() + return self.env["payment.transaction"].create( + { + "provider_id": provider.id, + "reference": f"{order.name}-{self.tx_counter}", + "amount": order.amount_total, + "currency_id": order.currency_id.id, + "partner_id": order.partner_id.id, + "operation": "online_direct", + "sale_order_ids": [fields.Command.set([order.id])], + "payment_method_id": self.payment_method.id, + } + ) + + def test_expire_dates(self): + # Expire Date is set in the future + self.assertTrue(self.order_1.cart_expire_date) + # Changing to a website without expire delay should remove it + self.order_1.website_id = self.website_1 + self.assertFalse(self.order_1.cart_expire_date) + + def test_expire_scheduler(self): + # Case 1: We haven't reached the expire date yet + self.env["website"]._scheduler_website_expire_cart() + for order in self.orders: + self.assertEqual(order.state, "draft") + # Case 2: We have reached website 2 expire date + with freeze_time(datetime.now() + timedelta(hours=3)): + self.env["website"]._scheduler_website_expire_cart() + for order in self.orders: + self.assertEqual(order.state, "cancel") + + def test_expire_scheduler_multi_website(self): + # For this test, we split the orders among the 2 websites + (self.order_1 | self.order_2).write({"website_id": self.website_1.id}) + with freeze_time(datetime.now() + timedelta(hours=3)): + self.env["website"]._scheduler_website_expire_cart() + self.assertEqual(self.order_1.state, "draft", "No expire delay on website 1") + self.assertEqual(self.order_2.state, "draft", "No expire delay on website 1") + self.assertEqual(self.order_3.state, "cancel", "Should've been cancelled") + self.assertEqual(self.order_4.state, "cancel", "Should've been cancelled") + + @freeze_time(datetime.now() + timedelta(hours=3)) + def test_expire_scheduler_ignore_sent_quotation(self): + """Test that sent quotations aren't cancelled + + Quotations can be sent manually or automatically when using + wire transfer as payment. They shouldn't be cancelled. + """ + self.order_1.action_quotation_sent() + self.env["website"]._scheduler_website_expire_cart() + self.assertNotEqual(self.order_1.state, "cancel") + + @freeze_time(datetime.now() + timedelta(hours=3)) + def test_expire_scheduler_ignore_in_payment(self): + """Carts with a payment transaction in progress shouldn't expire""" + if self.env["ir.module.module"]._get("payment_custom").state != "installed": + self.skipTest("Transfer provider is not installed") + self._create_payment_transaction(self.order_1) + tx_2 = self._create_payment_transaction(self.order_2) + tx_2._set_pending() + tx_3 = self._create_payment_transaction(self.order_3) + tx_3._set_canceled() + tx_4 = self._create_payment_transaction(self.order_4) + tx_4._set_error("Something went wrong") + # Carts with transactions in progress are not canceled + # Even those with 'draft' or 'error' transactions, because + # the timer is reseted whenever a tx state changes. + self.env["website"]._scheduler_website_expire_cart() + self.assertNotEqual(self.order_1.state, "cancel") + self.assertNotEqual(self.order_2.state, "cancel") + self.assertNotEqual(self.order_3.state, "cancel") + self.assertNotEqual(self.order_4.state, "cancel") + # In case of error, another transaction can be initialized + # However for order_1, no more activity was detected, so it's canceled + with freeze_time(datetime.now() + timedelta(hours=3)): + self._create_payment_transaction(self.order_4) + self.env["website"]._scheduler_website_expire_cart() + self.assertEqual(self.order_1.state, "cancel") + self.assertNotEqual(self.order_4.state, "cancel") diff --git a/website_sale_cart_expire/views/res_config_settings.xml b/website_sale_cart_expire/views/res_config_settings.xml new file mode 100644 index 0000000000..b08ce55c5a --- /dev/null +++ b/website_sale_cart_expire/views/res_config_settings.xml @@ -0,0 +1,40 @@ + + + + + res.config.settings + + + + +
+
+
+
+
+
+
+
+
+
+