diff --git a/sale_validity_auto_cancel/README.rst b/sale_validity_auto_cancel/README.rst new file mode 100644 index 00000000000..91f7b3ec32f --- /dev/null +++ b/sale_validity_auto_cancel/README.rst @@ -0,0 +1,108 @@ +========================= +Sale Validity Auto-Cancel +========================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:7910bb4739717e98aa441c71911f7e6336e6b3a13e85dd7724bfcf69cc175b3f + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/licence-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fsale--workflow-lightgray.png?logo=github + :target: https://github.com/OCA/sale-workflow/tree/17.0/sale_validity_auto_cancel + :alt: OCA/sale-workflow +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/sale-workflow-17-0/sale-workflow-17-0-sale_validity_auto_cancel + :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/sale-workflow&target_branch=17.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module adds a scheduled action that automatically cancels +quotations after their expiration date. + +A company setting can be modified to decide how many days after the +expiration date the quotations are automatically cancelled. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +Go to the menu *Sale > Configuration > Settings*, in the section +*Quotations & Sales*, set the *Auto-cancel expired quotations after* in +days. + +Usage +===== + +By default, the scheduled action *Cancel Expired Quotations* is not +enabled in order to first configure the setting parameter for the number +of days. + +Once enabled, the scheduled action will run automatically once a day. + +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 +------- + +* ForgeFlow +* OERP Canada + +Contributors +------------ + +- Jordi Masvidal +- `OERP Canada `__: + + - Foram Darji + +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-JordiMForgeFlow| image:: https://github.com/JordiMForgeFlow.png?size=40px + :target: https://github.com/JordiMForgeFlow + :alt: JordiMForgeFlow + +Current `maintainer `__: + +|maintainer-JordiMForgeFlow| + +This module is part of the `OCA/sale-workflow `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/sale_validity_auto_cancel/__init__.py b/sale_validity_auto_cancel/__init__.py new file mode 100644 index 00000000000..7d768b54b0a --- /dev/null +++ b/sale_validity_auto_cancel/__init__.py @@ -0,0 +1,3 @@ +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from . import models diff --git a/sale_validity_auto_cancel/__manifest__.py b/sale_validity_auto_cancel/__manifest__.py new file mode 100644 index 00000000000..9f79c834fb2 --- /dev/null +++ b/sale_validity_auto_cancel/__manifest__.py @@ -0,0 +1,21 @@ +# Copyright 2023 ForgeFlow S.L. +# Copyright 2024 OERP Canada +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +{ + "name": "Sale Validity Auto-Cancel", + "version": "17.0.1.0.0", + "category": "Sales", + "license": "LGPL-3", + "summary": "Automatically cancel quotations after validity period.", + "depends": ["sale_management"], + "author": "ForgeFlow, OERP Canada, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/sale-workflow", + "data": [ + "data/ir_cron.xml", + "views/res_config_settings.xml", + "views/res_partner_views.xml", + ], + "installable": True, + "maintainers": ["JordiMForgeFlow"], +} diff --git a/sale_validity_auto_cancel/data/ir_cron.xml b/sale_validity_auto_cancel/data/ir_cron.xml new file mode 100644 index 00000000000..feee384ae1b --- /dev/null +++ b/sale_validity_auto_cancel/data/ir_cron.xml @@ -0,0 +1,18 @@ + + + + + Cancel Expired Quotations + 1 + days + -1 + False + + + code + model.cron_sale_validity_auto_cancel() + + diff --git a/sale_validity_auto_cancel/i18n/sale_validity_auto_cancel.pot b/sale_validity_auto_cancel/i18n/sale_validity_auto_cancel.pot new file mode 100644 index 00000000000..5361a534ebc --- /dev/null +++ b/sale_validity_auto_cancel/i18n/sale_validity_auto_cancel.pot @@ -0,0 +1,94 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * sale_validity_auto_cancel +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-07-02 19:37+0000\n" +"PO-Revision-Date: 2024-07-02 19:37+0000\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: sale_validity_auto_cancel +#: model_terms:ir.ui.view,arch_db:sale_validity_auto_cancel.res_config_settings_view_form +msgid " days" +msgstr "" + +#. module: sale_validity_auto_cancel +#: model:ir.model.fields,field_description:sale_validity_auto_cancel.field_sale_order__auto_cancel_expired_so +msgid "Auto Cancel" +msgstr "" + +#. module: sale_validity_auto_cancel +#: model:ir.model.fields,field_description:sale_validity_auto_cancel.field_res_partner__auto_cancel_expired_so +#: model:ir.model.fields,field_description:sale_validity_auto_cancel.field_res_users__auto_cancel_expired_so +msgid "Auto Cancel Expired SaleOrder" +msgstr "" + +#. module: sale_validity_auto_cancel +#: model:ir.model.fields,field_description:sale_validity_auto_cancel.field_res_company__sale_validity_auto_cancel_days +#: model:ir.model.fields,field_description:sale_validity_auto_cancel.field_res_config_settings__sale_validity_auto_cancel_days +msgid "Auto-cancel expired quotations after (days)" +msgstr "" + +#. module: sale_validity_auto_cancel +#: model:ir.actions.server,name:sale_validity_auto_cancel.cron_sale_validity_auto_cancel_ir_actions_server +msgid "Cancel Expired Quotations" +msgstr "" + +#. module: sale_validity_auto_cancel +#: model:ir.model,name:sale_validity_auto_cancel.model_res_company +msgid "Companies" +msgstr "" + +#. module: sale_validity_auto_cancel +#: model:ir.model,name:sale_validity_auto_cancel.model_res_config_settings +msgid "Config Settings" +msgstr "" + +#. module: sale_validity_auto_cancel +#: model:ir.model,name:sale_validity_auto_cancel.model_res_partner +msgid "Contact" +msgstr "" + +#. module: sale_validity_auto_cancel +#: model:ir.model.fields,help:sale_validity_auto_cancel.field_res_partner__auto_cancel_expired_so +#: model:ir.model.fields,help:sale_validity_auto_cancel.field_res_users__auto_cancel_expired_so +#: model:ir.model.fields,help:sale_validity_auto_cancel.field_sale_order__auto_cancel_expired_so +msgid "" +"If unchecked, you will be able to restrict this contact's expired SO from " +"being auto-canceled." +msgstr "" + +#. module: sale_validity_auto_cancel +#: model_terms:ir.ui.view,arch_db:sale_validity_auto_cancel.res_config_settings_view_form +msgid "" +"Quotations will be automatically cancelled after the expiration date plus " +"the number of days specified." +msgstr "" + +#. module: sale_validity_auto_cancel +#: model:ir.model.fields,help:sale_validity_auto_cancel.field_res_company__sale_validity_auto_cancel_days +#: model:ir.model.fields,help:sale_validity_auto_cancel.field_res_config_settings__sale_validity_auto_cancel_days +msgid "" +"Quotations will be cancelled after the specified number of days since the " +"expiration date." +msgstr "" + +#. module: sale_validity_auto_cancel +#: model:ir.model,name:sale_validity_auto_cancel.model_sale_order +msgid "Sales Order" +msgstr "" + +#. module: sale_validity_auto_cancel +#: model:ir.model.constraint,message:sale_validity_auto_cancel.constraint_res_company_sale_validity_auto_cancel_days_positive +msgid "" +"The value of the field 'Auto-cancel expired quotations after' must be " +"positive or 0." +msgstr "" diff --git a/sale_validity_auto_cancel/models/__init__.py b/sale_validity_auto_cancel/models/__init__.py new file mode 100644 index 00000000000..00c75a60546 --- /dev/null +++ b/sale_validity_auto_cancel/models/__init__.py @@ -0,0 +1,6 @@ +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from . import res_company +from . import res_config_settings +from . import res_partner +from . import sale_order diff --git a/sale_validity_auto_cancel/models/res_company.py b/sale_validity_auto_cancel/models/res_company.py new file mode 100644 index 00000000000..328040260e5 --- /dev/null +++ b/sale_validity_auto_cancel/models/res_company.py @@ -0,0 +1,25 @@ +# Copyright 2023 ForgeFlow S.L. +# Copyright 2024 OERP Canada +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo import fields, models + + +class ResCompany(models.Model): + _inherit = "res.company" + + sale_validity_auto_cancel_days = fields.Integer( + string="Auto-cancel expired quotations after (days)", + default=0, + help="Quotations will be cancelled after the specified number of" + " days since the expiration date.", + ) + + _sql_constraints = [ + ( + "sale_validity_auto_cancel_days_positive", + "CHECK (sale_validity_auto_cancel_days >= 0)", + "The value of the field 'Auto-cancel expired quotations after' " + "must be positive or 0.", + ), + ] diff --git a/sale_validity_auto_cancel/models/res_config_settings.py b/sale_validity_auto_cancel/models/res_config_settings.py new file mode 100644 index 00000000000..385d107e5d8 --- /dev/null +++ b/sale_validity_auto_cancel/models/res_config_settings.py @@ -0,0 +1,14 @@ +# Copyright 2023 ForgeFlow S.L. +# Copyright 2024 OERP Canada +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + sale_validity_auto_cancel_days = fields.Integer( + related="company_id.sale_validity_auto_cancel_days", + readonly=False, + ) diff --git a/sale_validity_auto_cancel/models/res_partner.py b/sale_validity_auto_cancel/models/res_partner.py new file mode 100644 index 00000000000..2346056348a --- /dev/null +++ b/sale_validity_auto_cancel/models/res_partner.py @@ -0,0 +1,16 @@ +# Copyright 2023 ForgeFlow S.L. +# Copyright 2024 OERP Canada +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResPartner(models.Model): + _inherit = "res.partner" + + auto_cancel_expired_so = fields.Boolean( + string="Auto Cancel Expired SaleOrder", + default=True, + help="If unchecked, you will be able to restrict \n" + "this contact's expired SO from being auto-canceled.", + ) diff --git a/sale_validity_auto_cancel/models/sale_order.py b/sale_validity_auto_cancel/models/sale_order.py new file mode 100644 index 00000000000..b604ef0ed3c --- /dev/null +++ b/sale_validity_auto_cancel/models/sale_order.py @@ -0,0 +1,42 @@ +# Copyright 2023 ForgeFlow S.L. +# Copyright 2024 OERP Canada +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +import logging + +from dateutil.relativedelta import relativedelta + +from odoo import fields, models + +_logger = logging.getLogger(__name__) + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + auto_cancel_expired_so = fields.Boolean( + related="partner_id.auto_cancel_expired_so", string="Auto Cancel" + ) + + def _get_expired_order_states(self): + # Can be inherited to exclude/include order states + return ["draft", "sent"] + + def cron_sale_validity_auto_cancel(self): + today = fields.Date.today() + for company in self.env["res.company"].search([]): + threshold = today - relativedelta( + days=company.sale_validity_auto_cancel_days + ) + expired_states = self._get_expired_order_states() + orders = self.env["sale.order"].search( + [ + ("state", "in", expired_states), + ("validity_date", "<", threshold), + ("auto_cancel_expired_so", "=", True), + ] + ) + for order in orders: + try: + order.with_context(company_id=company.id)._action_cancel() + except Exception as e: + _logger.error("Failed to auto-cancel %s: %s", (order.name, str(e))) diff --git a/sale_validity_auto_cancel/pyproject.toml b/sale_validity_auto_cancel/pyproject.toml new file mode 100644 index 00000000000..4231d0cccb3 --- /dev/null +++ b/sale_validity_auto_cancel/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/sale_validity_auto_cancel/readme/CONFIGURE.md b/sale_validity_auto_cancel/readme/CONFIGURE.md new file mode 100644 index 00000000000..77e279345d1 --- /dev/null +++ b/sale_validity_auto_cancel/readme/CONFIGURE.md @@ -0,0 +1,3 @@ +Go to the menu *Sale \> Configuration \> Settings*, in the section +*Quotations & Sales*, set the *Auto-cancel expired quotations after* in +days. diff --git a/sale_validity_auto_cancel/readme/CONTRIBUTORS.md b/sale_validity_auto_cancel/readme/CONTRIBUTORS.md new file mode 100644 index 00000000000..316bb6110c7 --- /dev/null +++ b/sale_validity_auto_cancel/readme/CONTRIBUTORS.md @@ -0,0 +1,3 @@ +- Jordi Masvidal \<\> +- [OERP Canada](https://www.oerp.ca/): + - Foram Darji \<\> diff --git a/sale_validity_auto_cancel/readme/DESCRIPTION.md b/sale_validity_auto_cancel/readme/DESCRIPTION.md new file mode 100644 index 00000000000..0be3aadc3b7 --- /dev/null +++ b/sale_validity_auto_cancel/readme/DESCRIPTION.md @@ -0,0 +1,5 @@ +This module adds a scheduled action that automatically cancels +quotations after their expiration date. + +A company setting can be modified to decide how many days after the +expiration date the quotations are automatically cancelled. diff --git a/sale_validity_auto_cancel/readme/USAGE.md b/sale_validity_auto_cancel/readme/USAGE.md new file mode 100644 index 00000000000..49ef172547a --- /dev/null +++ b/sale_validity_auto_cancel/readme/USAGE.md @@ -0,0 +1,5 @@ +By default, the scheduled action *Cancel Expired Quotations* is not +enabled in order to first configure the setting parameter for the number +of days. + +Once enabled, the scheduled action will run automatically once a day. diff --git a/sale_validity_auto_cancel/static/description/icon.png b/sale_validity_auto_cancel/static/description/icon.png new file mode 100644 index 00000000000..3a0328b516c Binary files /dev/null and b/sale_validity_auto_cancel/static/description/icon.png differ diff --git a/sale_validity_auto_cancel/static/description/index.html b/sale_validity_auto_cancel/static/description/index.html new file mode 100644 index 00000000000..1510dc15132 --- /dev/null +++ b/sale_validity_auto_cancel/static/description/index.html @@ -0,0 +1,445 @@ + + + + + +Sale Validity Auto-Cancel + + + +
+

Sale Validity Auto-Cancel

+ + +

Beta License: LGPL-3 OCA/sale-workflow Translate me on Weblate Try me on Runboat

+

This module adds a scheduled action that automatically cancels +quotations after their expiration date.

+

A company setting can be modified to decide how many days after the +expiration date the quotations are automatically cancelled.

+

Table of contents

+ +
+

Configuration

+

Go to the menu Sale > Configuration > Settings, in the section +Quotations & Sales, set the Auto-cancel expired quotations after in +days.

+
+
+

Usage

+

By default, the scheduled action Cancel Expired Quotations is not +enabled in order to first configure the setting parameter for the number +of days.

+

Once enabled, the scheduled action will run automatically once a day.

+
+
+

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

+
    +
  • ForgeFlow
  • +
  • OERP Canada
  • +
+
+
+

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:

+

JordiMForgeFlow

+

This module is part of the OCA/sale-workflow project on GitHub.

+

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

+
+
+
+ + diff --git a/sale_validity_auto_cancel/tests/__init__.py b/sale_validity_auto_cancel/tests/__init__.py new file mode 100644 index 00000000000..9dfc86a8c0b --- /dev/null +++ b/sale_validity_auto_cancel/tests/__init__.py @@ -0,0 +1,3 @@ +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from . import test_sale_validity_auto_cancel diff --git a/sale_validity_auto_cancel/tests/test_sale_validity_auto_cancel.py b/sale_validity_auto_cancel/tests/test_sale_validity_auto_cancel.py new file mode 100644 index 00000000000..b4aa571b1ab --- /dev/null +++ b/sale_validity_auto_cancel/tests/test_sale_validity_auto_cancel.py @@ -0,0 +1,50 @@ +# Copyright 2023 ForgeFlow S.L. +# Copyright 2024 OERP Canada +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from dateutil.relativedelta import relativedelta + +from odoo import fields +from odoo.tests.common import TransactionCase + + +class TestSaleValidityAutoCancel(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.SaleOrder = cls.env["sale.order"] + cls.company = cls.env.ref("base.main_company") + cls.company.sale_validity_auto_cancel_days = 10 + cls.partner = cls.env.ref("base.res_partner_2") + cls.product = cls.env.ref("product.product_product_7") + + def create_so(self): + vals = { + "partner_id": self.partner.id, + "validity_date": fields.Date.today() - relativedelta(days=11), + "order_line": [ + ( + 0, + 0, + { + "product_id": self.product.id, + "product_uom_qty": 8, + }, + ) + ], + } + so = self.SaleOrder.create(vals) + return so + + def test_sale_validity_auto_cancel(self): + so = self.create_so() + self.assertEqual(so.state, "draft") + so.cron_sale_validity_auto_cancel() + self.assertEqual(so.state, "cancel") + + def test_sale_validity_auto_cancel_keep_so(self): + self.partner.write({"auto_cancel_expired_so": False}) + so = self.create_so() + self.assertEqual(so.state, "draft") + so.cron_sale_validity_auto_cancel() + self.assertEqual(so.state, "draft") diff --git a/sale_validity_auto_cancel/views/res_config_settings.xml b/sale_validity_auto_cancel/views/res_config_settings.xml new file mode 100644 index 00000000000..35f20c9dbda --- /dev/null +++ b/sale_validity_auto_cancel/views/res_config_settings.xml @@ -0,0 +1,28 @@ + + + + + res.config.settings.view.form (in sale_validity_auto_cancel) + res.config.settings + + + + + days + + + + + diff --git a/sale_validity_auto_cancel/views/res_partner_views.xml b/sale_validity_auto_cancel/views/res_partner_views.xml new file mode 100644 index 00000000000..21495aa23b4 --- /dev/null +++ b/sale_validity_auto_cancel/views/res_partner_views.xml @@ -0,0 +1,18 @@ + + + + + res.partner.form + res.partner + + + + + + + +