From 150376ff51e3def706d972c93d738ffebfbc7692 Mon Sep 17 00:00:00 2001 From: Quentin Groulard Date: Wed, 17 Jul 2019 16:13:08 +0200 Subject: [PATCH 1/3] [ADD] Auto process lower levels Add the possibility to mark a policy so that processing a line generated by this policy will also process lower level lines for the same partner (and policy). Also add a filter for credit control line tree view in order to only display highest level lines for each partner and policy. --- account_credit_control/README.rst | 14 ++ .../models/credit_control_communication.py | 88 +++++++----- .../models/credit_control_line.py | 131 +++++++++++++++++- .../models/credit_control_policy.py | 6 + account_credit_control/readme/CONFIGURE.md | 3 + account_credit_control/readme/USAGE.md | 10 ++ .../static/description/index.html | 12 ++ account_credit_control/tests/__init__.py | 1 + .../tests/test_credit_control_line.py | 124 +++++++++++++++++ .../views/credit_control_line.xml | 20 +++ .../views/credit_control_policy.xml | 2 + .../wizard/credit_control_emailer.py | 3 +- .../wizard/credit_control_marker.py | 3 +- .../wizard/credit_control_printer.py | 5 +- 14 files changed, 382 insertions(+), 40 deletions(-) create mode 100644 account_credit_control/tests/test_credit_control_line.py diff --git a/account_credit_control/README.rst b/account_credit_control/README.rst index f37b10d2d..a16d8f161 100644 --- a/account_credit_control/README.rst +++ b/account_credit_control/README.rst @@ -46,6 +46,10 @@ Configure the policies and policy levels in You can define as many policy levels as you need. You must set on which accounts are applied every Credit Control Policy under Accounts tab. +Select ``Auto Process Lower levels`` to auto-process lines from lower +level and same partner when a credit control line generated by this +policy is processed. + Configure a tolerance for the Credit control and a default policy applied on all partners in each company, under the General Information tab in your company form. @@ -83,6 +87,16 @@ communication processes that have been created and follow them. The 'Credit Control' followers of the linked partner will be automatically added as followers to the credit control lines. +The option ``Auto Process Lower Levels`` on ``Credit Control Policy`` +helps to manage high credit control line generation rates (e.g. if you +are selling monthly subscriptions). In such situation you may want to +avoid spamming reminders to customers with several credit control lines +of different levels, and only take action for the highest level. Thus, +an email sent for a high level will also contain the lower level lines. +Plus, you can use the filter ``Group Lines`` in the +``Credit Control Lines`` menu to hide lines that will be auto-processed +when a highest level line is. + Bug Tracker =========== diff --git a/account_credit_control/models/credit_control_communication.py b/account_credit_control/models/credit_control_communication.py index 00ca84288..831ae6adc 100644 --- a/account_credit_control/models/credit_control_communication.py +++ b/account_credit_control/models/credit_control_communication.py @@ -125,50 +125,66 @@ def _get_credit_lines( ) return cr_lines + @api.model + def _group_lines(self, lines): + ordered_lines = lines.search( + [("id", "in", lines.ids)], + order="partner_id, currency_id, policy_id, company_id, state, level DESC", + ) + prev_group = None + prev_policy_level = None + group_lines = self.env["credit.control.line"].browse() + for line in ordered_lines: + group = (line.partner_id, line.currency_id, line.policy_id, line.company_id) + policy_level = line.policy_level_id + if prev_group and ( + group != prev_group + or ( + not line.policy_id.autoprocess_lower_levels + and policy_level != prev_policy_level + ) + ): + yield ( + group_lines[0].partner_id, + group_lines[0].currency_id, + group_lines[0].policy_level_id, + group_lines[0].company_id, + group_lines, + ) + group_lines = self.env["credit.control.line"].browse() + if line not in group_lines: + group_lines |= line._get_lower_related_lines() or line + prev_group = group + prev_policy_level = policy_level + yield ( + group_lines[0].partner_id, + group_lines[0].currency_id, + group_lines[0].policy_level_id, + group_lines[0].company_id, + group_lines, + ) + @api.model def _aggregate_credit_lines(self, lines): """Aggregate credit control line by partner, level, and currency""" if not lines: return [] - # Needed for related stored fields - # are recomputed before executing the SQL - lines.flush_recordset() - sql = ( - "SELECT distinct partner_id, policy_level_id, " - " credit_control_line.currency_id, " - " credit_control_policy_level.level, " - " credit_control_line.company_id " - " FROM credit_control_line JOIN credit_control_policy_level " - " ON (credit_control_line.policy_level_id = " - " credit_control_policy_level.id)" - " WHERE credit_control_line.id in %s" - " ORDER by credit_control_policy_level.level, " - " credit_control_line.currency_id" - ) - cr = self.env.cr - cr.execute(sql, (tuple(lines.ids),)) - res = cr.dictfetchall() + company_currency = self.env.user.company_id.currency_id datas = [] - for group in res: + for ( + partner, + currency, + policy_level, + company, + grouped_lines, + ) in self._group_lines(lines): data = {} - level_lines = self._get_credit_lines( - lines.ids, - group["partner_id"], - group["policy_level_id"], - group["currency_id"], - group["company_id"], - ) - company = ( - self.env["res.company"].browse(group["company_id"]) - if group["company_id"] - else self.env.company - ) + company = company or self.env.company company_currency = company.currency_id - data["credit_control_line_ids"] = [(6, 0, level_lines.ids)] - data["partner_id"] = group["partner_id"] - data["policy_level_id"] = group["policy_level_id"] - data["currency_id"] = group["currency_id"] or company_currency.id - data["company_id"] = group["company_id"] or company.id + data["credit_control_line_ids"] = [(6, 0, grouped_lines.ids)] + data["partner_id"] = partner.id + data["policy_level_id"] = policy_level.id + data["currency_id"] = currency.id or company_currency.id datas.append(data) return datas diff --git a/account_credit_control/models/credit_control_line.py b/account_credit_control/models/credit_control_line.py index 6099afa57..49530d142 100644 --- a/account_credit_control/models/credit_control_line.py +++ b/account_credit_control/models/credit_control_line.py @@ -71,7 +71,7 @@ class CreditControlLine(models.Model): partner_id = fields.Many2one( comodel_name="res.partner", required=True, - readonly=False, + index=True, ) commercial_partner_id = fields.Many2one( comodel_name="res.partner", @@ -116,6 +116,7 @@ class CreditControlLine(models.Model): policy_id = fields.Many2one( comodel_name="credit.control.policy", related="policy_level_id.policy_id", + index=True, store=True, ) level = fields.Integer( @@ -132,6 +133,22 @@ class CreditControlLine(models.Model): compute="_compute_partner_user_id", store=True, ) + auto_process = fields.Selection( + selection=[ + ("no_auto_process", "No Auto Process"), + ("low_level", "Low Level"), + ("highest_level", "Highest Level"), + ], + help="'No Auto Process' lines are not automatically processed " + "with other lines.\n" + "'Low Level' lines are automatically processed " + "with higher level lines.\n" + "'Highest Level' indicates that all 'Low Level' lines for this " + "line's partner-policy combination will be automatically " + "processed with it.", + default="no_auto_process", + readonly=True, + ) @api.depends("partner_id.user_id") def _compute_partner_user_id(self): @@ -273,6 +290,16 @@ def create_or_update_from_mv_lines( return new_lines + def _update_auto_process(self, exclude_ids=None): + self.ensure_one() + if not self.policy_id.auto_process_lower_levels: + return + highest_related_line = self._get_highest_related_line(exclude_ids=exclude_ids) + highest_related_line.write({"auto_process": "highest_level"}) + self._get_related_lines( + exclude_ids=((exclude_ids or []) + highest_related_line.ids) + ).write({"auto_process": "low_level"}) + def unlink(self): for line in self: if line.state != "draft": @@ -282,10 +309,112 @@ def unlink(self): "line that is not in draft state." ) ) + line._update_auto_process(exclude_ids=line.ids) return super().unlink() def write(self, values): res = super().write(values) if "manual_followup" in values: self.partner_id.write({"manual_followup": values.get("manual_followup")}) + for line in self: + if "state" in values and values.get("state") == "sent": + line.write({"auto_process": "no_auto_process"}) + if "auto_process" not in values: + line._update_auto_process() return res + + @api.model_create_multi + def create(self, vals_list): + lines = super().create(vals_list) + for line in lines: + if line.state == "sent": + line.write({"auto_process": "no_auto_process"}) + else: + line._update_auto_process() + return lines + + def _get_highest_related_line(self, exclude_ids=None): + self.ensure_one() + return self._get_related_lines(exclude_ids=exclude_ids, limit=1) + + def _get_related_lines_domain(self, exclude_ids=None, level=None): + domain = [ + ("partner_id", "=", self.partner_id.id), + ("currency_id", "=", self.currency_id.id), + ("policy_id", "=", self.policy_id.id), + ("state", "in", ("draft", "to_be_sent")), + ] + if exclude_ids: + domain.append(("id", "not in", exclude_ids)) + if level: + domain.append(("level", "<=", level)) + return domain + + def _get_related_lines(self, exclude_ids=None, limit=None, level=None): + """ + Return lines from the same group if grouped + (ie with same partner, policy and currency). + + The most important line (ie the one to display to the user) + is the first one of the returned recordset. + """ + self.ensure_one() + if self.policy_id.auto_process_lower_levels: + return self.search( + self._get_related_lines_domain(exclude_ids, level), + limit=limit, + order="level DESC, date_due ASC", + ) + else: + return self + + def _get_lower_related_lines(self): + """ + Return lines that will receive the same treatment + (ie lines of lower level from the same group if grouped). + """ + self.ensure_one() + if self.policy_id.auto_process_lower_levels: + return self._get_related_lines(level=self.level) + else: + return self + + def button_schedule_activity(self): + ctx = self.env.context.copy() + ctx.update( + { + "default_res_id": self.ids[0], + "default_res_model": self._name, + } + ) + return { + "type": "ir.actions.act_window", + "name": _("Schedule activity"), + "res_model": "mail.activity", + "view_type": "form", + "view_mode": "form", + "res_id": self.activity_ids and self.activity_ids.ids[0] or False, + "views": [[False, "form"]], + "context": ctx, + "target": "new", + } + + def button_credit_control_line_form(self): + self.ensure_one() + action = self.env.ref("account_credit_control.credit_control_line_action") + form = self.env.ref("account_credit_control.credit_control_line_form") + action = action.read()[0] + action["views"] = [(form.id, "form")] + action["res_id"] = self.id + return action + + def act_show_auto_process_line(self): + self.ensure_one() + return { + "type": "ir.actions.act_window", + "name": _("Credit Control Lines"), + "res_model": "credit.control.line", + "domain": [("id", "in", self._get_lower_related_lines().ids)], + "view_mode": "list,form", + "context": self.env.context, + } diff --git a/account_credit_control/models/credit_control_policy.py b/account_credit_control/models/credit_control_policy.py index d030113fb..9ae8f9670 100644 --- a/account_credit_control/models/credit_control_policy.py +++ b/account_credit_control/models/credit_control_policy.py @@ -27,6 +27,12 @@ class CreditControlPolicy(models.Model): do_nothing = fields.Boolean( help="For policies which should not generate lines or are obsolete" ) + auto_process_lower_levels = fields.Boolean( + string="Auto Process Lower levels", + help="When an action is performed on a credit control line generated " + "by this policy, lower level lines for the same partner and policy " + "will also be processed.", + ) company_id = fields.Many2one(comodel_name="res.company") account_ids = fields.Many2many( comodel_name="account.account", diff --git a/account_credit_control/readme/CONFIGURE.md b/account_credit_control/readme/CONFIGURE.md index 7838d6b57..a279448ce 100644 --- a/account_credit_control/readme/CONFIGURE.md +++ b/account_credit_control/readme/CONFIGURE.md @@ -3,6 +3,9 @@ Configure the policies and policy levels in You can define as many policy levels as you need. You must set on which accounts are applied every Credit Control Policy under Accounts tab. +Select ``Auto Process Lower levels`` to auto-process lines from lower level and +same partner when a credit control line generated by this policy is processed. + Configure a tolerance for the Credit control and a default policy applied on all partners in each company, under the General Information tab in your company form. diff --git a/account_credit_control/readme/USAGE.md b/account_credit_control/readme/USAGE.md index e2256cf4b..8cab251fd 100644 --- a/account_credit_control/readme/USAGE.md +++ b/account_credit_control/readme/USAGE.md @@ -24,3 +24,13 @@ communication processes that have been created and follow them. The 'Credit Control' followers of the linked partner will be automatically added as followers to the credit control lines. + +The option ``Auto Process Lower Levels`` on ``Credit Control Policy`` helps to +manage high credit control line generation rates (e.g. if you are selling +monthly subscriptions). +In such situation you may want to avoid spamming reminders to customers with +several credit control lines of different levels, and only take action for the +highest level. Thus, an email sent for a high level will also contain the lower +level lines. +Plus, you can use the filter ``Group Lines`` in the ``Credit Control Lines`` +menu to hide lines that will be auto-processed when a highest level line is. diff --git a/account_credit_control/static/description/index.html b/account_credit_control/static/description/index.html index e7dfef6a3..9e8815dc5 100644 --- a/account_credit_control/static/description/index.html +++ b/account_credit_control/static/description/index.html @@ -394,6 +394,9 @@

Configuration

Invoicing  > Configuration > Credit Control > Credit Control Policies. You can define as many policy levels as you need. You must set on which accounts are applied every Credit Control Policy under Accounts tab.

+

Select Auto Process Lower levels to auto-process lines from lower +level and same partner when a credit control line generated by this +policy is processed.

Configure a tolerance for the Credit control and a default policy applied on all partners in each company, under the General Information tab in your company form.

@@ -424,6 +427,15 @@

Usage

communication processes that have been created and follow them.

The ‘Credit Control’ followers of the linked partner will be automatically added as followers to the credit control lines.

+

The option Auto Process Lower Levels on Credit Control Policy +helps to manage high credit control line generation rates (e.g. if you +are selling monthly subscriptions). In such situation you may want to +avoid spamming reminders to customers with several credit control lines +of different levels, and only take action for the highest level. Thus, +an email sent for a high level will also contain the lower level lines. +Plus, you can use the filter Group Lines in the +Credit Control Lines menu to hide lines that will be auto-processed +when a highest level line is.

Bug Tracker

diff --git a/account_credit_control/tests/__init__.py b/account_credit_control/tests/__init__.py index 72cadcddd..d06e05ef6 100644 --- a/account_credit_control/tests/__init__.py +++ b/account_credit_control/tests/__init__.py @@ -3,3 +3,4 @@ from . import test_res_partner from . import test_account_move from . import test_credit_control_run +from . import test_credit_control_line diff --git a/account_credit_control/tests/test_credit_control_line.py b/account_credit_control/tests/test_credit_control_line.py new file mode 100644 index 000000000..52c833be0 --- /dev/null +++ b/account_credit_control/tests/test_credit_control_line.py @@ -0,0 +1,124 @@ +# Copyright 2019 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from datetime import datetime + +from odoo.tests import tagged +from odoo.tests.common import TransactionCase + + +@tagged("post_install", "-at_install") +class TestCreditControlLine(TransactionCase): + def test_auto_process(self): + account = self.env["account.account"].create( + { + "code": "400001", + "name": "Customers (test)", + "account_type": "asset_receivable", + "reconcile": True, + } + ) + + move = self.env["account.move"].create( + { + "name": "Test move", + "date": datetime.today(), + "state": "draft", + } + ) + + move_line = self.env["account.move.line"].create( + { + "account_id": account.id, + "move_id": move.id, + } + ) + + policy = self.env.ref("account_credit_control.credit_control_3_time") + policy.write( + { + "account_ids": [(6, 0, [account.id])], + "auto_process_lower_levels": True, + } + ) + policy_level_1 = self.env.ref("account_credit_control.3_time_1") + policy_level_1.delay_days = 1 + + policy_level_2 = self.env.ref("account_credit_control.3_time_2") + policy_level_2.delay_days = 1 + + policy_level_3 = self.env.ref("account_credit_control.3_time_3") + policy_level_3.delay_days = 1 + + partner = self.env["res.partner"].create( + { + "name": "Partner", + "property_account_receivable_id": account.id, + } + ) + partner.credit_policy_id = policy.id + + ccl_1 = self.env["credit.control.line"].create( + { + "date": datetime.today(), + "date_due": datetime.today(), + "state": "draft", + "partner_id": partner.id, + "account_id": account.id, + "policy_level_id": policy_level_1.id, + "channel": "email", + "amount_due": 100, + "balance_due": 100, + "move_line_id": move_line.id, + } + ) + + self.assertEqual(ccl_1.auto_process, "highest_level") + + ccl_2 = self.env["credit.control.line"].create( + { + "date": datetime.today(), + "date_due": datetime.today(), + "state": "draft", + "partner_id": partner.id, + "account_id": account.id, + "policy_level_id": policy_level_2.id, + "channel": "email", + "amount_due": 100, + "balance_due": 100, + "move_line_id": move_line.id, + } + ) + + self.assertEqual(ccl_1.auto_process, "low_level") + self.assertEqual(ccl_2.auto_process, "highest_level") + self.assertTrue(ccl_1 in ccl_2._get_lower_related_lines()) + ccl_1.write({"policy_level_id": policy_level_3.id}) + + self.assertEqual(ccl_1.auto_process, "highest_level") + self.assertEqual(ccl_2.auto_process, "low_level") + self.assertTrue(ccl_2 in ccl_1._get_lower_related_lines()) + + ccl_1.unlink() + + self.assertEqual(ccl_2.auto_process, "highest_level") + + policy.write({"auto_process_lower_levels": False}) + + ccl_3 = self.env["credit.control.line"].create( + { + "date": datetime.today(), + "date_due": datetime.today(), + "state": "sent", + "partner_id": partner.id, + "account_id": account.id, + "policy_level_id": policy_level_2.id, + "channel": "email", + "amount_due": 100, + "balance_due": 100, + "move_line_id": move_line.id, + } + ) + + self.assertEqual(ccl_3.auto_process, "no_auto_process") + self.assertEqual(ccl_3, ccl_3._get_related_lines()) diff --git a/account_credit_control/views/credit_control_line.xml b/account_credit_control/views/credit_control_line.xml index c92549319..294f001c8 100644 --- a/account_credit_control/views/credit_control_line.xml +++ b/account_credit_control/views/credit_control_line.xml @@ -68,6 +68,12 @@ + + +

Bug Tracker

@@ -466,7 +454,11 @@

Contributors

  • Akim Juillerat (Camptocamp) <akim.juillerat@camptocamp.com>
  • Kinner Vachhani (Access Bookings Ltd) <kin.vachhani@gmail.com>
  • Raf Ven <raf.ven@dynapps.be>
  • -
  • Quentin Groulard (ACSONE) <quentin.groulard@acsone.eu>
  • +
  • Acsone:
      +
    • Quentin Groulard
    • +
    • Yannick Payot
    • +
    +
  • Tecnativa:
    • Vicent Cubells
    • Manuel Calero
    • diff --git a/account_credit_control/tests/__init__.py b/account_credit_control/tests/__init__.py index d06e05ef6..72cadcddd 100644 --- a/account_credit_control/tests/__init__.py +++ b/account_credit_control/tests/__init__.py @@ -3,4 +3,3 @@ from . import test_res_partner from . import test_account_move from . import test_credit_control_run -from . import test_credit_control_line diff --git a/account_credit_control/views/credit_control_line.xml b/account_credit_control/views/credit_control_line.xml index 294f001c8..c92549319 100644 --- a/account_credit_control/views/credit_control_line.xml +++ b/account_credit_control/views/credit_control_line.xml @@ -68,12 +68,6 @@ - - -