diff --git a/sale_automatic_workflow_job/README.rst b/sale_automatic_workflow_job/README.rst new file mode 100644 index 00000000000..5023f8717f3 --- /dev/null +++ b/sale_automatic_workflow_job/README.rst @@ -0,0 +1,89 @@ +=========================== +Sale Automatic Workflow Job +=========================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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-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%2Fsale--workflow-lightgray.png?logo=github + :target: https://github.com/OCA/sale-workflow/tree/15.0/sale_automatic_workflow_job + :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-15-0/sale-workflow-15-0-sale_automatic_workflow_job + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/167/15.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Use Queue Jobs to process the Sales Automatic Workflow actions. + +The default behavior of the automatic workflow module is to use a +scheduled action that searches all the record that need a workflow +action and sequentially process all of them. + +It can hit some limits when the number of records is too high. + +This module keeps the scheduled action to search the records, but +instead of directly executing the actions (confirm a sales order, +create invoices for a sales order, validate invoices, ...), it +creates one job per operation to do. + +It uses an identity key on the jobs so it will not create the same +job for the same record and same operation twice. + +**Table of contents** + +.. contents:: + :local: + +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 smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Camptocamp + +Contributors +~~~~~~~~~~~~ + +* Guewen Baconnier +* Saran Lim. +* Kitti U. + +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. + +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_automatic_workflow_job/__init__.py b/sale_automatic_workflow_job/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/sale_automatic_workflow_job/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/sale_automatic_workflow_job/__manifest__.py b/sale_automatic_workflow_job/__manifest__.py new file mode 100644 index 00000000000..b2ca9e514d5 --- /dev/null +++ b/sale_automatic_workflow_job/__manifest__.py @@ -0,0 +1,16 @@ +# Copyright 2020 Camptocamp (https://www.camptocamp.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Sale Automatic Workflow Job", + "summary": "Execute sale automatic workflows in queue jobs", + "version": "16.0.1.0.0", + "category": "Sales Management", + "license": "AGPL-3", + "author": "Camptocamp, " "Odoo Community Association (OCA)", + "website": "https://github.com/OCA/sale-workflow", + "depends": ["sale_automatic_workflow", "queue_job"], + "data": [ + "data/queue_job_data.xml", + ], +} diff --git a/sale_automatic_workflow_job/data/queue_job_data.xml b/sale_automatic_workflow_job/data/queue_job_data.xml new file mode 100644 index 00000000000..66f75cca078 --- /dev/null +++ b/sale_automatic_workflow_job/data/queue_job_data.xml @@ -0,0 +1,74 @@ + + + + + channel.sale.automatic.workflow + + + + + + + _do_validate_sale_order + + + + + + + _do_create_invoice + + + + + + + _do_validate_invoice + + + + + + + _do_validate_picking + + + + + + + _do_sale_done + + + + diff --git a/sale_automatic_workflow_job/i18n/sale_automatic_workflow_job.pot b/sale_automatic_workflow_job/i18n/sale_automatic_workflow_job.pot new file mode 100644 index 00000000000..ca3f191bf9c --- /dev/null +++ b/sale_automatic_workflow_job/i18n/sale_automatic_workflow_job.pot @@ -0,0 +1,62 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * sale_automatic_workflow_job +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.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: sale_automatic_workflow_job +#: code:addons/sale_automatic_workflow_job/models/automatic_workflow_job.py:0 +#, python-format +msgid "Create invoices for sales order {}" +msgstr "" + +#. module: sale_automatic_workflow_job +#: code:addons/sale_automatic_workflow_job/models/automatic_workflow_job.py:0 +#, python-format +msgid "Mark sales order {} as done" +msgstr "" + +#. module: sale_automatic_workflow_job +#: model:ir.model,name:sale_automatic_workflow_job.model_queue_job +msgid "Queue Job" +msgstr "" + +#. module: sale_automatic_workflow_job +#: code:addons/sale_automatic_workflow_job/models/queue_job.py:0 +#, python-format +msgid "Sale Automatic Workflow Job" +msgstr "" + +#. module: sale_automatic_workflow_job +#: model:ir.model,name:sale_automatic_workflow_job.model_automatic_workflow_job +msgid "" +"Scheduler that will play automatically the validation of invoices, " +"pickings..." +msgstr "" + +#. module: sale_automatic_workflow_job +#: code:addons/sale_automatic_workflow_job/models/automatic_workflow_job.py:0 +#, python-format +msgid "Validate invoice {}" +msgstr "" + +#. module: sale_automatic_workflow_job +#: code:addons/sale_automatic_workflow_job/models/automatic_workflow_job.py:0 +#, python-format +msgid "Validate sales order {}" +msgstr "" + +#. module: sale_automatic_workflow_job +#: code:addons/sale_automatic_workflow_job/models/automatic_workflow_job.py:0 +#, python-format +msgid "Validate transfer {}" +msgstr "" diff --git a/sale_automatic_workflow_job/models/__init__.py b/sale_automatic_workflow_job/models/__init__.py new file mode 100644 index 00000000000..6f2ac63f818 --- /dev/null +++ b/sale_automatic_workflow_job/models/__init__.py @@ -0,0 +1,2 @@ +from . import automatic_workflow_job +from . import queue_job diff --git a/sale_automatic_workflow_job/models/automatic_workflow_job.py b/sale_automatic_workflow_job/models/automatic_workflow_job.py new file mode 100644 index 00000000000..51a9f79912c --- /dev/null +++ b/sale_automatic_workflow_job/models/automatic_workflow_job.py @@ -0,0 +1,85 @@ +# Copyright 2020 Camptocamp (https://www.camptocamp.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import _, models + +from odoo.addons.queue_job.job import identity_exact + + +class AutomaticWorkflowJob(models.Model): + _inherit = "automatic.workflow.job" + + def _do_validate_sale_order_job_options(self, sale, domain_filter): + description = _("Validate sales order {}").format(sale.display_name) + return { + "description": description, + "identity_key": identity_exact, + } + + def _validate_sale_orders(self, domain_filter): + with_context = self.with_context(auto_delay_do_validation=True) + return super(AutomaticWorkflowJob, with_context)._validate_sale_orders( + domain_filter + ) + + def _do_create_invoice_job_options(self, sale, domain_filter): + description = _("Create invoices for sales order {}").format(sale.display_name) + return { + "description": description, + "identity_key": identity_exact, + } + + def _create_invoices(self, domain_filter): + with_context = self.with_context(auto_delay_do_create_invoice=True) + return super(AutomaticWorkflowJob, with_context)._create_invoices(domain_filter) + + def _do_validate_invoice_job_options(self, invoice, domain_filter): + description = _("Validate invoice {}").format(invoice.display_name) + return { + "description": description, + "identity_key": identity_exact, + } + + def _validate_invoices(self, domain_filter): + with_context = self.with_context(auto_delay_do_validation=True) + return super(AutomaticWorkflowJob, with_context)._validate_invoices( + domain_filter + ) + + def _do_validate_picking_job_options(self, picking, domain_filter): + description = _("Validate transfer {}").format(picking.display_name) + return { + "description": description, + "identity_key": identity_exact, + } + + def _validate_pickings(self, domain_filter): + with_context = self.with_context(auto_delay_do_validation=True) + return super(AutomaticWorkflowJob, with_context)._validate_pickings( + domain_filter + ) + + def _do_sale_done_job_options(self, sale, domain_filter): + description = _("Mark sales order {} as done").format(sale.display_name) + return { + "description": description, + "identity_key": identity_exact, + } + + def _sale_done(self, domain_filter): + with_context = self.with_context(auto_delay_do_sale_done=True) + return super(AutomaticWorkflowJob, with_context)._sale_done(domain_filter) + + def _register_hook(self): + mapping = { + "_do_validate_sale_order": "auto_delay_do_validation", + "_do_create_invoice": "auto_delay_do_create_invoice", + "_do_validate_invoice": "auto_delay_do_validation", + "_do_validate_picking": "auto_delay_do_validation", + "_do_sale_done": "auto_delay_do_sale_done", + } + for method_name, context_key in mapping.items(): + self._patch_method( + method_name, + self._patch_job_auto_delay(method_name, context_key=context_key), + ) + return super()._register_hook() diff --git a/sale_automatic_workflow_job/models/queue_job.py b/sale_automatic_workflow_job/models/queue_job.py new file mode 100644 index 00000000000..02e856a782d --- /dev/null +++ b/sale_automatic_workflow_job/models/queue_job.py @@ -0,0 +1,21 @@ +# Copyright 2020 Ecosoft Co., Ltd. (http://ecosoft.co.th) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, models + + +class QueueJob(models.Model): + """Job status and result""" + + _inherit = "queue.job" + + def _related_action_sale_automatic_workflow(self): + obj = self.args[0] + action = { + "name": _("Sale Automatic Workflow Job"), + "type": "ir.actions.act_window", + "res_model": obj._name, + "view_mode": "form", + "res_id": obj.id, + } + return action diff --git a/sale_automatic_workflow_job/readme/CONTRIBUTORS.rst b/sale_automatic_workflow_job/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..25aba8b01b8 --- /dev/null +++ b/sale_automatic_workflow_job/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* Guewen Baconnier +* Saran Lim. +* Kitti U. diff --git a/sale_automatic_workflow_job/readme/DESCRIPTION.rst b/sale_automatic_workflow_job/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..dad760e07c1 --- /dev/null +++ b/sale_automatic_workflow_job/readme/DESCRIPTION.rst @@ -0,0 +1,15 @@ +Use Queue Jobs to process the Sales Automatic Workflow actions. + +The default behavior of the automatic workflow module is to use a +scheduled action that searches all the record that need a workflow +action and sequentially process all of them. + +It can hit some limits when the number of records is too high. + +This module keeps the scheduled action to search the records, but +instead of directly executing the actions (confirm a sales order, +create invoices for a sales order, validate invoices, ...), it +creates one job per operation to do. + +It uses an identity key on the jobs so it will not create the same +job for the same record and same operation twice. diff --git a/sale_automatic_workflow_job/static/description/icon.png b/sale_automatic_workflow_job/static/description/icon.png new file mode 100644 index 00000000000..3a0328b516c Binary files /dev/null and b/sale_automatic_workflow_job/static/description/icon.png differ diff --git a/sale_automatic_workflow_job/static/description/index.html b/sale_automatic_workflow_job/static/description/index.html new file mode 100644 index 00000000000..ad84f62e493 --- /dev/null +++ b/sale_automatic_workflow_job/static/description/index.html @@ -0,0 +1,431 @@ + + + + + + +Sale Automatic Workflow Job + + + +
+

Sale Automatic Workflow Job

+ + +

Beta License: AGPL-3 OCA/sale-workflow Translate me on Weblate Try me on Runbot

+

Use Queue Jobs to process the Sales Automatic Workflow actions.

+

The default behavior of the automatic workflow module is to use a +scheduled action that searches all the record that need a workflow +action and sequentially process all of them.

+

It can hit some limits when the number of records is too high.

+

This module keeps the scheduled action to search the records, but +instead of directly executing the actions (confirm a sales order, +create invoices for a sales order, validate invoices, …), it +creates one job per operation to do.

+

It uses an identity key on the jobs so it will not create the same +job for the same record and same operation twice.

+

Table of contents

+ +
+

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 smashing 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.

+

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_automatic_workflow_job/tests/__init__.py b/sale_automatic_workflow_job/tests/__init__.py new file mode 100644 index 00000000000..89a48ebc2e0 --- /dev/null +++ b/sale_automatic_workflow_job/tests/__init__.py @@ -0,0 +1 @@ +from . import test_auto_workflow_job diff --git a/sale_automatic_workflow_job/tests/test_auto_workflow_job.py b/sale_automatic_workflow_job/tests/test_auto_workflow_job.py new file mode 100644 index 00000000000..480bcd04fdf --- /dev/null +++ b/sale_automatic_workflow_job/tests/test_auto_workflow_job.py @@ -0,0 +1,138 @@ +# Copyright 2020 Camptocamp (https://www.camptocamp.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.tests import tagged + +from odoo.addons.queue_job.job import identity_exact +from odoo.addons.queue_job.tests.common import mock_with_delay +from odoo.addons.sale_automatic_workflow.tests.common import ( + TestAutomaticWorkflowMixin, + TestCommon, +) + + +@tagged("post_install", "-at_install") +class TestAutoWorkflowJob(TestCommon, TestAutomaticWorkflowMixin): + def create_sale_order(self, workflow, override=None): + order = super().create_sale_order(workflow, override) + order.order_line.product_id.invoice_policy = "order" + return order + + def assert_job_delayed(self, delayable_cls, delayable, method_name, args): + # .with_delay() has been called once + self.assertEqual(delayable_cls.call_count, 1) + delay_args, delay_kwargs = delayable_cls.call_args + # .with_delay() has been called on self.env["automatic.workflow.job"] + self.assertEqual(delay_args, (self.env["automatic.workflow.job"],)) + # .with_delay() with the following options + self.assertEqual(delay_kwargs.get("identity_key"), identity_exact) + # check what's passed to the job method + method = getattr(delayable, method_name) + self.assertEqual(method.call_count, 1) + delay_args, delay_kwargs = method.call_args + self.assertEqual(delay_args, args) + self.assertDictEqual(delay_kwargs, {}) + + def test_validate_sale_order(self): + workflow = self.create_full_automatic() + self.sale = self.create_sale_order(workflow) + with mock_with_delay() as (delayable_cls, delayable): + self.run_job() # run automatic workflow cron + args = ( + self.sale, + [ + ("state", "=", "draft"), + ("workflow_process_id", "=", self.sale.workflow_process_id.id), + ], + ) + self.assert_job_delayed( + delayable_cls, delayable, "_do_validate_sale_order", args + ) + + def test_create_invoice(self): + workflow = self.create_full_automatic() + self.sale = self.create_sale_order(workflow) + self.sale.action_confirm() + # don't care about transfers in this test + self.sale.picking_ids.state = "done" + with mock_with_delay() as (delayable_cls, delayable): + self.run_job() # run automatic workflow cron + args = ( + self.sale, + [ + ("state", "in", ["sale", "done"]), + ("invoice_status", "=", "to invoice"), + ("workflow_process_id", "=", self.sale.workflow_process_id.id), + ], + ) + self.assert_job_delayed( + delayable_cls, delayable, "_do_create_invoice", args + ) + + def test_validate_invoice(self): + workflow = self.create_full_automatic() + self.sale = self.create_sale_order(workflow) + self.sale.action_confirm() + # don't care about transfers in this test + self.sale.picking_ids.state = "done" + self.sale._create_invoices() + invoice = self.sale.invoice_ids + with mock_with_delay() as (delayable_cls, delayable): + self.run_job() # run automatic workflow cron + args = ( + invoice, + [ + ("state", "=", "draft"), + ("posted_before", "=", False), + ("workflow_process_id", "=", self.sale.workflow_process_id.id), + ], + ) + self.assert_job_delayed( + delayable_cls, delayable, "_do_validate_invoice", args + ) + + def test_validate_picking(self): + workflow = self.create_full_automatic() + self.sale = self.create_sale_order(workflow) + self.sale.action_confirm() + picking = self.sale.picking_ids + # disable invoice creation in this test + self.sale.workflow_process_id.create_invoice = False + with mock_with_delay() as (delayable_cls, delayable): + self.run_job() # run automatic workflow cron + args = ( + picking, + [ + ("state", "in", ["draft", "confirmed", "assigned"]), + ("workflow_process_id", "=", self.sale.workflow_process_id.id), + ], + ) + self.assert_job_delayed( + delayable_cls, delayable, "_do_validate_picking", args + ) + + def test_sale_done(self): + workflow = self.create_full_automatic() + self.sale = self.create_sale_order(workflow) + self.sale.action_confirm() + # don't care about transfers in this test + self.sale.picking_ids.state = "done" + self.sale._create_invoices() + + # disable invoice validation for we don't care + # in this test + self.sale.workflow_process_id.validate_invoice = False + # activate the 'sale done' workflow + self.sale.workflow_process_id.sale_done = True + + with mock_with_delay() as (delayable_cls, delayable): + self.run_job() # run automatic workflow cron + args = ( + self.sale, + [ + ("state", "=", "sale"), + ("invoice_status", "=", "invoiced"), + ("workflow_process_id", "=", self.sale.workflow_process_id.id), + ], + ) + self.assert_job_delayed(delayable_cls, delayable, "_do_sale_done", args) diff --git a/setup/sale_automatic_workflow_job/odoo/addons/sale_automatic_workflow_job b/setup/sale_automatic_workflow_job/odoo/addons/sale_automatic_workflow_job new file mode 120000 index 00000000000..babf49076bc --- /dev/null +++ b/setup/sale_automatic_workflow_job/odoo/addons/sale_automatic_workflow_job @@ -0,0 +1 @@ +../../../../sale_automatic_workflow_job \ No newline at end of file diff --git a/setup/sale_automatic_workflow_job/setup.py b/setup/sale_automatic_workflow_job/setup.py new file mode 100644 index 00000000000..28c57bb6403 --- /dev/null +++ b/setup/sale_automatic_workflow_job/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)