From 6bd244cf673005157254a902dd7fe4f1138f204f Mon Sep 17 00:00:00 2001 From: John Date: Sun, 13 Aug 2023 15:33:08 +0200 Subject: [PATCH 1/2] [IMP] sale_automatic_workflow: add send invoice to workflow --- sale_automatic_workflow/README.rst | 76 +++++++++--------- .../data/automatic_workflow_data.xml | 13 ++++ .../models/automatic_workflow_job.py | 78 ++++++++++++++++++- .../models/sale_workflow_process.py | 31 ++++++++ .../readme/CONTRIBUTORS.rst | 12 +++ .../readme/DESCRIPTION.rst | 22 ++++++ sale_automatic_workflow/tests/common.py | 18 ++++- .../tests/test_automatic_workflow.py | 70 ++++++++++++++++- .../tests/test_multicompany.py | 6 ++ .../views/sale_workflow_process_views.xml | 36 +++++++++ 10 files changed, 317 insertions(+), 45 deletions(-) create mode 100644 sale_automatic_workflow/readme/CONTRIBUTORS.rst create mode 100644 sale_automatic_workflow/readme/DESCRIPTION.rst diff --git a/sale_automatic_workflow/README.rst b/sale_automatic_workflow/README.rst index 0655875884f..3ee4c94f133 100644 --- a/sale_automatic_workflow/README.rst +++ b/sale_automatic_workflow/README.rst @@ -2,12 +2,12 @@ Sale Automatic Workflow ======================= -.. +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:2e02c7c445382c52a5a523e511d78a5d40cecafe955493cbe280b78597066a5a + !! source digest: sha256:6c461bb0f25bae8120a4bba197db362e406de92f6fa19a1a3f8609971609b1f8 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png @@ -17,13 +17,13 @@ Sale Automatic Workflow :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/17.0/sale_automatic_workflow + :target: https://github.com/OCA/sale-workflow/tree/16.0/sale_automatic_workflow :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_automatic_workflow + :target: https://translation.odoo-community.org/projects/sale-workflow-16-0/sale-workflow-16-0-sale_automatic_workflow :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 + :target: https://runboat.odoo-community.org/builds?repo=OCA/sale-workflow&target_branch=16.0 :alt: Try me on Runboat |badge1| |badge2| |badge3| |badge4| |badge5| @@ -33,23 +33,22 @@ orders. A workflow can: -- Apply default values: +- Apply default values: - - Shipping Policy (Deliver each product when available or Deliver - all products at once) - - Set the invoice's date to the sale order's date - - Set a sales team + * Shipping Policy (Deliver each product when available or Deliver all products at once) + * Set the invoice's date to the sale order's date + * Set a sales team -- Apply automatic actions: +- Apply automatic actions: - - Validate the order (only if paid, always, never) - - Send order confirmation mail (only when order confirmed) - - Create an invoice - - Validate the invoice - - Confirm the picking + * Validate the order (only if paid, always, never) + * Send order confirmation mail (only when order confirmed) + * Create an invoice + * Validate the invoice + * Confirm the picking -This module is used by Magentoerpconnect and Prestashoperpconnect. It is -well suited for other E-Commerce connectors as well. +This module is used by Magentoerpconnect and Prestashoperpconnect. +It is well suited for other E-Commerce connectors as well. **Table of contents** @@ -62,7 +61,7 @@ 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 `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -70,38 +69,37 @@ Credits ======= Authors -------- +~~~~~~~ * Akretion * Camptocamp * Sodexis Contributors ------------- - -- Guewen Baconnier -- Beau Sebastien -- Leonardo Pistone -- Stéphane Bidoul -- Damien Crier -- Alexandre Fayolle -- Sodexis -- Dave Lasley -- Akim Juillerat -- Thomas Fossoul -- Phuc Tran Thanh -- Sander Lienaerts -- Tri Doan +~~~~~~~~~~~~ + +* Guewen Baconnier +* Beau Sebastien +* Leonardo Pistone +* Stéphane Bidoul +* Damien Crier +* Alexandre Fayolle +* Sodexis +* Dave Lasley +* Akim Juillerat +* Thomas Fossoul +* Phuc Tran Thanh +* John Herholz Other credits -------------- +~~~~~~~~~~~~~ The development of this module has been financially supported by: -- Camptocamp +* Camptocamp Maintainers ------------ +~~~~~~~~~~~ This module is maintained by the OCA. @@ -113,6 +111,6 @@ 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. +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/data/automatic_workflow_data.xml b/sale_automatic_workflow/data/automatic_workflow_data.xml index ece7f7aff48..859ec59937d 100644 --- a/sale_automatic_workflow/data/automatic_workflow_data.xml +++ b/sale_automatic_workflow/data/automatic_workflow_data.xml @@ -26,6 +26,14 @@ >[('state', '=', 'draft'), ('posted_before', '=', False)] + + Automatic Workflow Send Invoice Filter + account.move + [('state', '=', 'posted'), ('is_move_sent', '=', False), ('move_type', '=', 'out_invoice')] + + Automatic Workflow Sale Done Filter sale.order @@ -57,6 +65,11 @@ name="validate_invoice_filter_id" eval="automatic_workflow_validate_invoice_filter" /> + + diff --git a/sale_automatic_workflow/models/automatic_workflow_job.py b/sale_automatic_workflow/models/automatic_workflow_job.py index e1e30011fda..18f12fc3155 100644 --- a/sale_automatic_workflow/models/automatic_workflow_job.py +++ b/sale_automatic_workflow/models/automatic_workflow_job.py @@ -112,14 +112,80 @@ def _validate_invoices(self, validate_invoice_filter): invoice.with_company(invoice.company_id), validate_invoice_filter ) + def _do_send_invoice(self, invoice, domain_filter): + """Validate an invoice, filter ensure no duplication""" + if not self.env["account.move"].search_count( + [("id", "=", invoice.id)] + domain_filter + ): + return "{} {} job bypassed".format(invoice.display_name, invoice) + + # take the context from the actual action_invoice_sent method + action = invoice.action_invoice_sent() + action_context = action["context"] + + # Create the email using the wizard + invoice_send_wizard = ( + self.env["account.invoice.send"] + .with_context( + action_context, + mark_invoice_as_sent=True, + active_ids=[invoice.id], + force_email=True, + ) + .create( + { + "is_print": False, + "composition_mode": "comment", + "model": "account.move", + "res_id": invoice.id, + } + ) + ) + + invoice_send_wizard.onchange_is_email() + invoice_send_wizard._send_email() + + return "{} {} sent invoice successfully".format(invoice.display_name, invoice) + + @api.model + def _send_invoices(self, send_invoice_filter): + move_obj = self.env["account.move"] + invoices = move_obj.search(send_invoice_filter) + _logger.debug("Invoices to send: %s", invoices.ids) + for invoice in invoices: + with savepoint(self.env.cr): + self._do_send_invoice( + invoice.with_company(invoice.company_id), send_invoice_filter + ) + + def _do_validate_picking(self, picking, domain_filter): + """Validate a stock.picking, filter ensure no duplication""" + if not self.env["stock.picking"].search_count( + [("id", "=", picking.id)] + domain_filter + ): + return "{} {} job bypassed".format(picking.display_name, picking) + picking.validate_picking() + return "{} {} validate picking successfully".format( + picking.display_name, picking + ) + + @api.model + def _validate_pickings(self, picking_filter): + picking_obj = self.env["stock.picking"] + pickings = picking_obj.search(picking_filter) + _logger.debug("Pickings to validate: %s", pickings.ids) + for picking in pickings: + with savepoint(self.env.cr): + self._do_validate_picking(picking, picking_filter) + def _do_sale_done(self, sale, domain_filter): - """Lock a sales order, filter ensure no duplication""" + """Set a sales order to done, filter ensure no duplication""" if not self.env["sale.order"].search_count( [("id", "=", sale.id)] + domain_filter ): - return f"{sale.display_name} {sale} job bypassed" - sale.action_lock() - return f"{sale.display_name} {sale} locked successfully" + return "{} {} job bypassed".format(sale.display_name, sale) + sale.action_done() + return "{} {} set done successfully".format(sale.display_name, sale) @api.model def _sale_done(self, sale_done_filter): @@ -194,6 +260,10 @@ def run_with_workflow(self, sale_workflow): safe_eval(sale_workflow.validate_invoice_filter_id.domain) + workflow_domain ) + if sale_workflow.send_invoice: + self._send_invoices( + safe_eval(sale_workflow.send_invoice_filter_id.domain) + workflow_domain + ) if sale_workflow.sale_done: self._sale_done( safe_eval(sale_workflow.sale_done_filter_id.domain) + workflow_domain diff --git a/sale_automatic_workflow/models/sale_workflow_process.py b/sale_automatic_workflow/models/sale_workflow_process.py index 622b992c5d0..fb81adb2418 100644 --- a/sale_automatic_workflow/models/sale_workflow_process.py +++ b/sale_automatic_workflow/models/sale_workflow_process.py @@ -28,6 +28,14 @@ def _default_filter(self, xmlid): return self.env["ir.filters"].browse() name = fields.Char(required=True) + picking_policy = fields.Selection( + selection=[ + ("direct", "Deliver each product when available"), + ("one", "Deliver all products at once"), + ], + string="Shipping Policy", + default="direct", + ) validate_order = fields.Boolean() send_order_confirmation_mail = fields.Boolean( help="When checked, after order confirmation, a confirmation email will be " @@ -45,6 +53,15 @@ def _default_filter(self, xmlid): string="Validate Invoice Filter Domain", related="validate_invoice_filter_id.domain", ) + send_invoice = fields.Boolean() + send_invoice_filter_domain = fields.Text( + string="Send Invoice Filter Domain", + related="send_invoice_filter_id.domain", + ) + validate_picking = fields.Boolean(string="Confirm and Transfer Picking") + picking_filter_domain = fields.Text( + string="Picking Filter Domain", related="picking_filter_id.domain" + ) invoice_date_is_order_date = fields.Boolean( string="Force Invoice Date", help="When checked, the invoice date will be " "the same than the order's date", @@ -79,6 +96,13 @@ def _default_filter(self, xmlid): "sale_automatic_workflow.automatic_workflow_order_filter" ), ) + picking_filter_id = fields.Many2one( + "ir.filters", + string="Picking Filter", + default=lambda self: self._default_filter( + "sale_automatic_workflow.automatic_workflow_picking_filter" + ), + ) create_invoice_filter_id = fields.Many2one( "ir.filters", string="Create Invoice Filter", @@ -93,6 +117,13 @@ def _default_filter(self, xmlid): "sale_automatic_workflow." "automatic_workflow_validate_invoice_filter" ), ) + send_invoice_filter_id = fields.Many2one( + "ir.filters", + string="Send Invoice Filter", + default=lambda self: self._default_filter( + "sale_automatic_workflow." "automatic_workflow_send_invoice_filter" + ), + ) sale_done_filter_id = fields.Many2one( "ir.filters", string="Sale Done Filter", diff --git a/sale_automatic_workflow/readme/CONTRIBUTORS.rst b/sale_automatic_workflow/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..324b53be26d --- /dev/null +++ b/sale_automatic_workflow/readme/CONTRIBUTORS.rst @@ -0,0 +1,12 @@ +* Guewen Baconnier +* Beau Sebastien +* Leonardo Pistone +* Stéphane Bidoul +* Damien Crier +* Alexandre Fayolle +* Sodexis +* Dave Lasley +* Akim Juillerat +* Thomas Fossoul +* Phuc Tran Thanh +* John Herholz diff --git a/sale_automatic_workflow/readme/DESCRIPTION.rst b/sale_automatic_workflow/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..8a51def1e2f --- /dev/null +++ b/sale_automatic_workflow/readme/DESCRIPTION.rst @@ -0,0 +1,22 @@ +Create workflows with more or less automatization and apply it on sales +orders. + +A workflow can: + +- Apply default values: + + * Shipping Policy (Deliver each product when available or Deliver all products at once) + * Set the invoice's date to the sale order's date + * Set a sales team + +- Apply automatic actions: + + * Validate the order (only if paid, always, never) + * Send order confirmation mail (only when order confirmed) + * Create an invoice + * Validate the invoice + * Send the invoice via e-mail + * Confirm the picking + +This module is used by Magentoerpconnect and Prestashoperpconnect. +It is well suited for other E-Commerce connectors as well. diff --git a/sale_automatic_workflow/tests/common.py b/sale_automatic_workflow/tests/common.py index 1b4e360d38e..49ad2c8f298 100644 --- a/sale_automatic_workflow/tests/common.py +++ b/sale_automatic_workflow/tests/common.py @@ -13,13 +13,28 @@ class TestCommon(TransactionCase): def setUpClass(cls): super().setUpClass() cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.user = cls.env["res.users"].create( + { + "name": "Sales Person", + "login": "salesperson", + "password": "salesperson", + "groups_id": [ + (4, cls.env.ref("sales_team.group_sale_manager").id), + (4, cls.env.ref("account.group_account_manager").id), + ], + } + ) + cls.user.partner_id.email = "salesperson@example.com" class TestAutomaticWorkflowMixin: def create_sale_order(self, workflow, override=None, product_type="consu"): sale_obj = self.env["sale.order"] - partner_values = {"name": "Imperator Caius Julius Caesar Divus"} + partner_values = { + "name": "Imperator Caius Julius Caesar Divus", + "email": "test@example.com", + } partner = self.env["res.partner"].create(partner_values) product_values = {"name": "Bread", "list_price": 5, "type": product_type} @@ -54,6 +69,7 @@ def create_full_automatic(self, override=None): "validate_order": True, "create_invoice": True, "validate_invoice": True, + "send_invoice": True, "invoice_date_is_order_date": True, } ) diff --git a/sale_automatic_workflow/tests/test_automatic_workflow.py b/sale_automatic_workflow/tests/test_automatic_workflow.py index 8191fc4adcf..f395ceaab30 100644 --- a/sale_automatic_workflow/tests/test_automatic_workflow.py +++ b/sale_automatic_workflow/tests/test_automatic_workflow.py @@ -1,16 +1,20 @@ # Copyright 2014 Camptocamp SA (author: Guewen Baconnier) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import logging from datetime import timedelta from unittest import mock from odoo import fields from odoo.tests import tagged +from odoo.tools.safe_eval import safe_eval from .common import TestAutomaticWorkflowMixin, TestCommon +_logger = logging.getLogger(__name__) -@tagged("post_install", "-at_install") + +@tagged("post_install", "-at_install", "mail_composer") class TestAutomaticWorkflow(TestCommon, TestAutomaticWorkflowMixin): def setUp(self): super().setUp() @@ -152,3 +156,67 @@ def test_automatic_sale_order_confirmation_mail(self): lambda x: x.subtype_id == self.env.ref("mail.mt_comment") ) ) + + def test_automatic_invoice_send_mail(self): + workflow = self.create_full_automatic() + workflow.send_invoice = False + sale = self.create_sale_order(workflow) + sale.user_id = self.user.id + sale._onchange_workflow_process_id() + self.run_job() + invoice = sale.invoice_ids + invoice.message_subscribe(partner_ids=[invoice.partner_id.id]) + invoice.company_id.invoice_is_email = True + previous_message_ids = invoice.message_ids + workflow.send_invoice = True + sale._onchange_workflow_process_id() + self.run_job() + + new_messages = self.env["mail.message"].search( + [ + ("id", "in", invoice.message_ids.ids), + ("id", "not in", previous_message_ids.ids), + ] + ) + + self.assertTrue( + new_messages.filtered( + lambda x: x.subtype_id == self.env.ref("mail.mt_comment") + ) + ) + + def test_job_bypassing(self): + workflow = self.create_full_automatic() + workflow_job = self.env["automatic.workflow.job"] + sale = self.create_sale_order(workflow) + sale._onchange_workflow_process_id() + + create_invoice_filter = [ + ("state", "in", ["sale", "done"]), + ("invoice_status", "=", "to invoice"), + ("workflow_process_id", "=", sale.workflow_process_id.id), + ] + order_filter = safe_eval(workflow.order_filter_id.domain) + validate_invoice_filter = safe_eval(workflow.validate_invoice_filter_id.domain) + send_invoice_filter = safe_eval(workflow.send_invoice_filter_id.domain) + + # Trigger everything, then check if sale and invoice jobs are bypassed + self.run_job() + + invoice = sale.invoice_ids + + res_so_validate = workflow_job._do_validate_sale_order(sale, order_filter) + # TODO send confirmation bypassing is not working yet, needs fix + workflow_job._do_send_order_confirmation_mail(sale) + res_create_invoice = workflow_job._do_create_invoice( + sale, create_invoice_filter + ) + res_validate_invoice = workflow_job._do_validate_invoice( + invoice, validate_invoice_filter + ) + res_send_invoice = workflow_job._do_send_invoice(invoice, send_invoice_filter) + + self.assertIn("job bypassed", res_so_validate) + self.assertIn("job bypassed", res_create_invoice) + self.assertIn("job bypassed", res_validate_invoice) + self.assertIn("job bypassed", res_send_invoice) diff --git a/sale_automatic_workflow/tests/test_multicompany.py b/sale_automatic_workflow/tests/test_multicompany.py index 50f45abe5d6..7eb9f0d199e 100644 --- a/sale_automatic_workflow/tests/test_multicompany.py +++ b/sale_automatic_workflow/tests/test_multicompany.py @@ -33,6 +33,12 @@ def test_sale_order_multicompany(self): self.assertEqual(order_fr_daughter.state, "draft") self.env["automatic.workflow.job"].run() + self.assertTrue(order_fr.picking_ids) + self.assertTrue(order_ch.picking_ids) + self.assertTrue(order_be.picking_ids) + self.assertEqual(order_fr.picking_ids.state, "done") + self.assertEqual(order_ch.picking_ids.state, "done") + self.assertEqual(order_be.picking_ids.state, "done") invoice_fr = order_fr.invoice_ids invoice_ch = order_ch.invoice_ids invoice_be = order_be.invoice_ids diff --git a/sale_automatic_workflow/views/sale_workflow_process_views.xml b/sale_automatic_workflow/views/sale_workflow_process_views.xml index 50e246d4787..d4d7e82b648 100644 --- a/sale_automatic_workflow/views/sale_workflow_process_views.xml +++ b/sale_automatic_workflow/views/sale_workflow_process_views.xml @@ -160,6 +160,42 @@ +
+
+
+
+
+