Skip to content
Open
1 change: 1 addition & 0 deletions sale_automatic_workflow/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ A workflow can:
- 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
Expand Down
1 change: 1 addition & 0 deletions sale_automatic_workflow/data/automatic_workflow_data.xml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
<field name="name">Automatic</field>
<field name="picking_policy">one</field>
<field name="validate_order" eval="1" />
<field name="send_order_confirmation_mail" eval="1" />
<field name="order_filter_id" eval="automatic_workflow_order_filter" />
<field name="create_invoice" eval="1" />
<field
Expand Down
78 changes: 66 additions & 12 deletions sale_automatic_workflow/models/automatic_workflow_job.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import logging
from contextlib import contextmanager

from odoo import api, models
from odoo import api, fields, models
from odoo.tools.safe_eval import safe_eval

_logger = logging.getLogger(__name__)
Expand All @@ -25,16 +25,6 @@ def savepoint(cr):
_logger.exception("Error during an automatic workflow action.")


@contextmanager
def force_company(env, company_id):
user_company = env.user.company_id
env.user.update({"company_id": company_id})
try:
yield
finally:
env.user.update({"company_id": user_company})


class AutomaticWorkflowJob(models.Model):
"""Scheduler that will play automatically the validation of
invoices, pickings..."""
Expand All @@ -54,6 +44,20 @@ def _do_validate_sale_order(self, sale, domain_filter):
sale.action_confirm()
return "{} {} confirmed successfully".format(sale.display_name, sale)

def _do_send_order_confirmation_mail(self, sale):
"""Send order confirmation mail, while filtering to make sure the order is
confirmed with _do_validate_sale_order() function"""
if not self.env["sale.order"].search_count(
[("id", "=", sale.id), ("state", "=", "sale")]
):
return "{} {} job bypassed".format(sale.display_name, sale)
if sale.user_id:
sale = sale.with_user(sale.user_id)
sale._send_order_confirmation_mail()
return "{} {} send order confirmation mail successfully".format(
sale.display_name, sale
)

@api.model
def _validate_sale_orders(self, order_filter):
sale_obj = self.env["sale.order"]
Expand All @@ -64,6 +68,8 @@ def _validate_sale_orders(self, order_filter):
self._do_validate_sale_order(
sale.with_company(sale.company_id), order_filter
)
if self.env.context.get("send_order_confirmation_mail"):
self._do_send_order_confirmation_mail(sale)

def _do_create_invoice(self, sale, domain_filter):
"""Create an invoice for a sales order, filter ensure no duplication"""
Expand Down Expand Up @@ -146,11 +152,54 @@ def _sale_done(self, sale_done_filter):
with savepoint(self.env.cr):
self._do_sale_done(sale.with_company(sale.company_id), sale_done_filter)

def _prepare_dict_account_payment(self, invoice):
partner_type = (
invoice.move_type in ("out_invoice", "out_refund")
and "customer"
or "supplier"
)
return {
"reconciled_invoice_ids": [(6, 0, invoice.ids)],
"amount": invoice.amount_residual,
"partner_id": invoice.partner_id.id,
"partner_type": partner_type,
"date": fields.Date.context_today(self),
}

@api.model
def _register_payments(self, payment_filter):
invoice_obj = self.env["account.move"]
invoices = invoice_obj.search(payment_filter)
_logger.debug("Invoices to Register Payment: %s", invoices.ids)
for invoice in invoices:
self._register_payment_invoice(invoice)
return

def _register_payment_invoice(self, invoice):
with savepoint(self.env.cr):
payment = self.env["account.payment"].create(
self._prepare_dict_account_payment(invoice)
)
payment.action_post()

domain = [
("account_internal_type", "in", ("receivable", "payable")),
("reconciled", "=", False),
]
payment_lines = payment.line_ids.filtered_domain(domain)
lines = invoice.line_ids
for account in payment_lines.account_id:
(payment_lines + lines).filtered_domain(
[("account_id", "=", account.id), ("reconciled", "=", False)]
).reconcile()

@api.model
def run_with_workflow(self, sale_workflow):
workflow_domain = [("workflow_process_id", "=", sale_workflow.id)]
if sale_workflow.validate_order:
self._validate_sale_orders(
self.with_context(
send_order_confirmation_mail=sale_workflow.send_order_confirmation_mail
)._validate_sale_orders(
safe_eval(sale_workflow.order_filter_id.domain) + workflow_domain
)
if sale_workflow.validate_picking:
Expand All @@ -172,6 +221,11 @@ def run_with_workflow(self, sale_workflow):
safe_eval(sale_workflow.sale_done_filter_id.domain) + workflow_domain
)

if sale_workflow.register_payment:
self._register_payments(
safe_eval(sale_workflow.payment_filter_id.domain) + workflow_domain
)

@api.model
def run(self):
""" Must be called from ir.cron """
Expand Down
19 changes: 19 additions & 0 deletions sale_automatic_workflow/models/sale_workflow_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ def _default_filter(self, xmlid):
return record
return self.env["ir.filters"].browse()

@api.model
def _default_payment_filter_id(self):
return self.env["ir.filters"].browse()

name = fields.Char()
picking_policy = fields.Selection(
selection=[
Expand All @@ -37,6 +41,11 @@ def _default_filter(self, xmlid):
default="direct",
)
validate_order = fields.Boolean()
send_order_confirmation_mail = fields.Boolean(
string="Send order confirmation mail",
help="When checked, after order confirmation, a confirmation email will be "
"sent (if not already sent).",
)
order_filter_domain = fields.Text(
string="Order Filter Domain", related="order_filter_id.domain"
)
Expand Down Expand Up @@ -115,3 +124,13 @@ def _default_filter(self, xmlid):
"sale_automatic_workflow.automatic_workflow_sale_done_filter"
),
)
payment_filter_id = fields.Many2one(
comodel_name="ir.filters",
string="Register Payment Invoice Filter",
default=lambda x: x._default_payment_filter_id(),
)
register_payment = fields.Boolean(string="Register Payment")
payment_filter_domain = fields.Text(
string="Payment Filter Domain",
related="payment_filter_id.domain",
)
1 change: 1 addition & 0 deletions sale_automatic_workflow/readme/DESCRIPTION.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ A workflow can:
- 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
Expand Down
3 changes: 2 additions & 1 deletion sale_automatic_workflow/static/description/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils 0.15.1: http://docutils.sourceforge.net/" />
<meta name="generator" content="Docutils: http://docutils.sourceforge.net/" />
<title>Sale Automatic Workflow</title>
<style type="text/css">

Expand Down Expand Up @@ -382,6 +382,7 @@ <h1 class="title">Sale Automatic Workflow</h1>
</li>
<li>Apply automatic actions:<ul>
<li>Validate the order (only if paid, always, never)</li>
<li>Send order confirmation mail (only when order confirmed)</li>
<li>Create an invoice</li>
<li>Validate the invoice</li>
<li>Confirm the picking</li>
Expand Down
49 changes: 38 additions & 11 deletions sale_automatic_workflow/tests/test_automatic_workflow.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,31 @@
# Copyright 2014 Camptocamp SA (author: Guewen Baconnier)
# Copyright 2021 Tecnativa - Víctor Martínez
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from datetime import timedelta

import mock

from odoo import fields
from odoo.exceptions import UserError
from odoo.tests import tagged

from .common import TestAutomaticWorkflowMixin, TestCommon


@tagged("post_install", "-at_install")
class TestAutomaticWorkflow(TestCommon, TestAutomaticWorkflowMixin):
def setUp(self):
super().setUp()
self.env = self.env(
context=dict(
self.env.context,
tracking_disable=True,
# Compatibility with sale_automatic_workflow_job: even if
# the module is installed, ensure we don't delay a job.
# Thus, we test the usual flow.
_job_force_sync=True,
)
)

def test_full_automatic(self):
workflow = self.create_full_automatic()
sale = self.create_sale_order(workflow)
Expand Down Expand Up @@ -62,19 +75,13 @@ def test_create_invoice_from_sale_order(self):
self.assertFalse(workflow.invoice_service_delivery)
self.assertEqual(line.qty_delivered_method, "stock_move")
self.assertEqual(line.qty_delivered, 0.0)
# `_create_invoices` is already tested in `sale` module.
# Make sure this addon works properly in regards to it.
mock_path = "odoo.addons.sale.models.sale.SaleOrder._create_invoices"
with mock.patch(mock_path) as mocked:
with self.assertRaises(UserError):
sale._create_invoices()
mocked.assert_called()
self.assertEqual(line.qty_delivered, 0.0)

workflow.invoice_service_delivery = True
sale.state = "done"
line.qty_delivered_method = "manual"
with mock.patch(mock_path) as mocked:
sale._create_invoices()
mocked.assert_called()
sale._create_invoices()
self.assertEqual(line.qty_delivered, 1.0)

def test_invoice_from_picking_with_service_product(self):
Expand Down Expand Up @@ -141,3 +148,23 @@ def test_journal_on_invoice(self):
self.assertTrue(sale.invoice_ids)
invoice = sale.invoice_ids
self.assertEqual(invoice.journal_id.id, new_sale_journal.id)

def test_automatic_sale_order_confirmation_mail(self):
workflow = self.create_full_automatic()
workflow.send_order_confirmation_mail = True
sale = self.create_sale_order(workflow)
sale._onchange_workflow_process_id()
previous_message_ids = sale.message_ids
self.run_job()
self.assertEqual(sale.state, "sale")
new_messages = self.env["mail.message"].search(
[
("id", "in", sale.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")
)
)
24 changes: 23 additions & 1 deletion sale_automatic_workflow/tests/test_multicompany.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@

@tagged("post_install", "-at_install")
class TestMultiCompany(TestCommon):
def setUp(self):
super().setUp()

@classmethod
def create_company(cls, values):
return cls.env["res.company"].create(values)
Expand All @@ -21,6 +24,16 @@ def create_product(cls, values):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env = cls.env(
context=dict(
cls.env.context,
tracking_disable=True,
# Compatibility with sale_automatic_workflow_job: even if
# the module is installed, ensure we don't delay a job.
# Thus, we test the usual flow.
_job_force_sync=True,
)
)
coa = cls.env.user.company_id.chart_template_id
cls.company_fr = cls.create_company(
{
Expand Down Expand Up @@ -61,7 +74,11 @@ def setUpClass(cls):

cls.env.user.company_id = cls.company_fr.id
coa.try_loading(company=cls.env.user.company_id)
cls.customer_fr = cls.env["res.partner"].create({"name": "Customer FR"})
cls.customer_fr = (
cls.env["res.partner"]
.with_context(default_company_id=cls.company_fr.id)
.create({"name": "Customer FR"})
)
cls.product_fr = cls.create_product({"name": "Evian bottle", "list_price": 2.0})

cls.env.user.company_id = cls.company_ch.id
Expand Down Expand Up @@ -101,6 +118,10 @@ def setUpClass(cls):
cls.env.user.company_id = cls.env.ref("base.main_company")

def create_auto_wkf_order(self, company, customer, product, qty):
# We need to change to the proper company
# to pick up correct company dependent fields
current_company = self.env.user.company_id
self.env.user.company_id = company
SaleOrder = self.env["sale.order"]
warehouse = self.env["stock.warehouse"].search(
[("company_id", "=", company.id)], limit=1
Expand Down Expand Up @@ -130,6 +151,7 @@ def create_auto_wkf_order(self, company, customer, product, qty):
}
)
order._onchange_workflow_process_id()
self.env.user.company_id = current_company
return order

def test_sale_order_multicompany(self):
Expand Down
28 changes: 28 additions & 0 deletions sale_automatic_workflow/views/sale_workflow_process_view.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
/>
</div>
</div>
<field name="send_order_confirmation_mail" colspan="4" />
<field name="validate_picking" />
<label
for="picking_filter_id"
Expand Down Expand Up @@ -126,6 +127,32 @@
/>
</div>
</div>
<field name="register_payment" />
<label
for="payment_filter_id"
attrs="{'required':[('register_payment','=',True)], 'invisible':[('register_payment','!=',True)]}"
/>
<div
attrs="{'required':[('register_payment','=',True)], 'invisible':[('register_payment','!=',True)]}"
>
<field
name="payment_filter_domain"
widget="domain"
attrs="{'invisible': [('payment_filter_id', '=', False)]}"
options="{'model': 'account.move'}"
/>
<div class="oe_edit_only oe_inline">
Set selection based on a search filter:
<field
name="payment_filter_id"
domain="[('model_id', '=', 'account.move')]"
class="oe_inline"
context="{'default_model_id': 'account.move', 'default_active': False, 'active_test': False}"
can_create="true"
can_write="true"
/>
</div>
</div>
<field name="sale_done" />
<label
for="sale_done_filter_id"
Expand Down Expand Up @@ -178,6 +205,7 @@
<field name="validate_order" />
<field name="validate_picking" />
<field name="validate_invoice" />
<field name="register_payment" />
<field name="invoice_date_is_order_date" />
</tree>
</field>
Expand Down