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 @@
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.
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.
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.
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/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 @@
-
-
-
-
-
diff --git a/account_credit_control/views/credit_control_policy.xml b/account_credit_control/views/credit_control_policy.xml
index 69c1f4d6a..f4d7ae713 100644
--- a/account_credit_control/views/credit_control_policy.xml
+++ b/account_credit_control/views/credit_control_policy.xml
@@ -17,7 +17,6 @@
-
@@ -100,7 +99,6 @@
-
diff --git a/account_credit_control/wizard/credit_control_emailer.py b/account_credit_control/wizard/credit_control_emailer.py
index 83f058c1b..f0ff8dbbc 100644
--- a/account_credit_control/wizard/credit_control_emailer.py
+++ b/account_credit_control/wizard/credit_control_emailer.py
@@ -36,10 +36,9 @@ def _get_line_ids(self):
def _filter_lines(self, lines):
"""filter lines to use in the wizard"""
line_obj = self.env["credit.control.line"]
- lines_and_related = lines.mapped(lambda l: l._get_lower_related_lines())
domain = [
("state", "=", "to_be_sent"),
- ("id", "in", lines_and_related.ids),
+ ("id", "in", lines.ids),
("channel", "=", "email"),
]
return line_obj.search(domain)
diff --git a/account_credit_control/wizard/credit_control_marker.py b/account_credit_control/wizard/credit_control_marker.py
index e8e2b94da..afa7efc35 100644
--- a/account_credit_control/wizard/credit_control_marker.py
+++ b/account_credit_control/wizard/credit_control_marker.py
@@ -46,8 +46,7 @@ def _default_lines(self):
def _filter_lines(self, lines):
"""get line to be marked filter done lines"""
line_obj = self.env["credit.control.line"]
- lines_and_related = lines.mapped(lambda l: l._get_lower_related_lines())
- domain = [("state", "!=", "sent"), ("id", "in", lines_and_related.ids)]
+ domain = [("state", "!=", "sent"), ("id", "in", lines.ids)]
return line_obj.search(domain)
@api.model
diff --git a/account_credit_control/wizard/credit_control_printer.py b/account_credit_control/wizard/credit_control_printer.py
index a090a2d07..ad8f9015e 100644
--- a/account_credit_control/wizard/credit_control_printer.py
+++ b/account_credit_control/wizard/credit_control_printer.py
@@ -16,13 +16,10 @@ class CreditControlPrinter(models.TransientModel):
@api.model
def _default_line_ids(self):
- line_obj = self.env["credit.control.line"]
context = self.env.context
if context.get("active_model") != "credit.control.line":
return False
- lines = line_obj.browse(context.get("active_ids", False))
- lines_and_related = lines.mapped(lambda l: l._get_lower_related_lines())
- return lines_and_related.ids
+ return context.get("active_ids")
mark_as_sent = fields.Boolean(
string="Mark letter lines as done",
diff --git a/account_credit_control_aggregate_level/README.rst b/account_credit_control_aggregate_level/README.rst
new file mode 100644
index 000000000..610430b0b
--- /dev/null
+++ b/account_credit_control_aggregate_level/README.rst
@@ -0,0 +1,102 @@
+======================
+Account Credit Control
+======================
+
+..
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ !! This file is generated by oca-gen-addon-readme !!
+ !! changes will be overwritten. !!
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ !! source digest: sha256:abaee855dd44ae136c041103b894cef9609b42a63c31df8f9da726b5caabb0d7
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+.. |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%2Fcredit--control-lightgray.png?logo=github
+ :target: https://github.com/OCA/credit-control/tree/18.0/account_credit_control_aggregate_level
+ :alt: OCA/credit-control
+.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
+ :target: https://translation.odoo-community.org/projects/credit-control-18-0/credit-control-18-0-account_credit_control_aggregate_level
+ :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/credit-control&target_branch=18.0
+ :alt: Try me on Runboat
+
+|badge1| |badge2| |badge3| |badge4| |badge5|
+
+Account Credit Control module is a part of Financial Tools used in
+business to ensure that once sales are made they are realised as cash.
+This module helps to identify outstanding debt beyond tolerance level
+and setup followup method.
+
+**Table of contents**
+
+.. contents::
+ :local:
+
+Configuration
+=============
+
+On credit policy enable ``Aggregate lower levels`` to aggregate lines
+from lower level and same partner when a credit control line generated
+by this policy is processed.
+
+Usage
+=====
+
+The option ``Aggregate 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, a
+communication is 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
+===========
+
+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
+-------
+
+* ACSONE SA/NV
+
+Contributors
+------------
+
+- `Acsone `__:
+
+ - Quentin Groulard
+ - Yannick Payot
+
+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/credit-control `_ project on GitHub.
+
+You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
diff --git a/account_credit_control_aggregate_level/__init__.py b/account_credit_control_aggregate_level/__init__.py
new file mode 100644
index 000000000..d6c56a95f
--- /dev/null
+++ b/account_credit_control_aggregate_level/__init__.py
@@ -0,0 +1,3 @@
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+from . import models
+from . import wizard
diff --git a/account_credit_control_aggregate_level/__manifest__.py b/account_credit_control_aggregate_level/__manifest__.py
new file mode 100644
index 000000000..78b48e04d
--- /dev/null
+++ b/account_credit_control_aggregate_level/__manifest__.py
@@ -0,0 +1,20 @@
+# Copyright 2019 ACSONE SA/NV
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+{
+ "name": "Account Credit Control",
+ "version": "18.0.1.0.0",
+ "author": "ACSONE SA/NV, Odoo Community Association (OCA)",
+ "maintainer": "ACSONE SA/NV",
+ "category": "Finance",
+ "depends": ["account_credit_control"],
+ "website": "https://github.com/OCA/credit-control",
+ "data": [
+ # Views
+ "views/credit_control_line.xml",
+ "views/credit_control_policy.xml",
+ ],
+ "demo": [],
+ "installable": True,
+ "license": "AGPL-3",
+ "application": True,
+}
diff --git a/account_credit_control_aggregate_level/models/__init__.py b/account_credit_control_aggregate_level/models/__init__.py
new file mode 100644
index 000000000..8ce9ff467
--- /dev/null
+++ b/account_credit_control_aggregate_level/models/__init__.py
@@ -0,0 +1,4 @@
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+from . import credit_control_communication
+from . import credit_control_line
+from . import credit_control_policy
diff --git a/account_credit_control_aggregate_level/models/credit_control_communication.py b/account_credit_control_aggregate_level/models/credit_control_communication.py
new file mode 100644
index 000000000..f8bbcfc9c
--- /dev/null
+++ b/account_credit_control_aggregate_level/models/credit_control_communication.py
@@ -0,0 +1,56 @@
+# Copyright 2019 ACSONE SA/NV
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from odoo import api, models
+
+
+class CreditControlCommunication(models.Model):
+ _inherit = "credit.control.communication"
+
+ @api.model
+ def _get_credit_lines(
+ self, line_ids, partner_id, level_id, currency_id, company_id
+ ):
+ # Handle case when no policy_level is provided.
+ if level_id:
+ return super()._get_credit_lines(
+ line_ids, partner_id, level_id, currency_id, company_id
+ )
+ cr_line_obj = self.env["credit.control.line"]
+ domain = [
+ ("id", "in", line_ids),
+ ("partner_id", "=", partner_id),
+ ("currency_id", "=", currency_id),
+ ("company_id", "=", company_id),
+ ]
+ return cr_line_obj.search(domain, order="level DESC")
+
+ @api.model
+ def _sql_credit_lines_groups(self):
+ # Remove level when aggregation is enabled on policy
+ # returns NULL instead of an ID for the level
+ return (
+ "SELECT DISTINCT"
+ " partner_id,"
+ " CASE"
+ " WHEN policy.aggregate_levels"
+ " THEN NULL"
+ " ELSE policy_level.id"
+ " END AS policy_level_id,"
+ " line.currency_id,"
+ " line.company_id"
+ " FROM credit_control_line AS line"
+ " JOIN credit_control_policy_level as policy_level"
+ " ON (line.policy_level_id = policy_level.id)"
+ " JOIN credit_control_policy as policy"
+ " ON (policy_level.policy_id = policy.id)"
+ " WHERE line.id in %s"
+ )
+
+ @api.model
+ def _prepare_communication_data(self, cr_lines):
+ # Redefine policy_level to ensure it's the highest
+ res = super()._prepare_communication_data(cr_lines)
+ highest_policy_lvl = cr_lines.mapped("policy_level_id").sorted("level")[0]
+ res["policy_level_id"] = highest_policy_lvl.id
+ return res
diff --git a/account_credit_control_aggregate_level/models/credit_control_line.py b/account_credit_control_aggregate_level/models/credit_control_line.py
new file mode 100644
index 000000000..64a16cf18
--- /dev/null
+++ b/account_credit_control_aggregate_level/models/credit_control_line.py
@@ -0,0 +1,143 @@
+# Copyright 2019 ACSONE SA/NV
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from odoo import _, api, fields, models
+
+
+class CreditControlLine(models.Model):
+ _inherit = "credit.control.line"
+
+ aggregation = fields.Selection(
+ selection=[
+ ("no", "No aggregation"),
+ ("low", "Low level"),
+ ("highest", "Highest level"),
+ ],
+ help="`No aggregation` lines are not automatically aggregated "
+ "with other lines.\n"
+ "'Low level' lines are attached under higher level lines.\n"
+ "'Highest level' indicates that all 'Low level' lines for this "
+ "line's partner-policy combination will be attached to it.",
+ default="no",
+ readonly=True,
+ )
+
+ def _update_aggregation(self, exclude_ids=None):
+ self.ensure_one()
+ if not self.policy_id.aggregate_levels:
+ return
+ highest_related_line = self._get_highest_related_line(exclude_ids=exclude_ids)
+ highest_related_line.write({"aggregation": "highest"})
+ self._get_related_lines(
+ exclude_ids=((exclude_ids or []) + highest_related_line.ids)
+ ).write({"aggregation": "low"})
+
+ def unlink(self):
+ for line in self:
+ line._update_aggregation(exclude_ids=line.ids)
+ return super().unlink()
+
+ def write(self, values):
+ res = super().write(values)
+ for line in self:
+ if "state" in values and values.get("state") == "sent":
+ line.write({"aggregation": "no"})
+ if "aggregation" not in values:
+ line._update_aggregation()
+ 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({"aggregation": "no"})
+ else:
+ line._update_aggregation()
+ 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.aggregate_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.aggregate_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_aggregated_lines(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_aggregate_level/models/credit_control_policy.py b/account_credit_control_aggregate_level/models/credit_control_policy.py
new file mode 100644
index 000000000..6db87e947
--- /dev/null
+++ b/account_credit_control_aggregate_level/models/credit_control_policy.py
@@ -0,0 +1,13 @@
+# Copyright 2019 ACSONE SA/NV
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+from odoo import fields, models
+
+
+class CreditControlPolicy(models.Model):
+ _inherit = "credit.control.policy"
+ aggregate_levels = fields.Boolean(
+ string="Aggregate 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.",
+ )
diff --git a/account_credit_control_aggregate_level/pyproject.toml b/account_credit_control_aggregate_level/pyproject.toml
new file mode 100644
index 000000000..4231d0ccc
--- /dev/null
+++ b/account_credit_control_aggregate_level/pyproject.toml
@@ -0,0 +1,3 @@
+[build-system]
+requires = ["whool"]
+build-backend = "whool.buildapi"
diff --git a/account_credit_control_aggregate_level/readme/CONFIGURE.md b/account_credit_control_aggregate_level/readme/CONFIGURE.md
new file mode 100644
index 000000000..ebef05c23
--- /dev/null
+++ b/account_credit_control_aggregate_level/readme/CONFIGURE.md
@@ -0,0 +1,2 @@
+On credit policy enable ``Aggregate lower levels`` to aggregate lines from lower level and
+same partner when a credit control line generated by this policy is processed.
diff --git a/account_credit_control_aggregate_level/readme/CONTRIBUTORS.md b/account_credit_control_aggregate_level/readme/CONTRIBUTORS.md
new file mode 100644
index 000000000..abef232cb
--- /dev/null
+++ b/account_credit_control_aggregate_level/readme/CONTRIBUTORS.md
@@ -0,0 +1,3 @@
+- [Acsone](https://www.acsone.eu):
+ - Quentin Groulard
+ - Yannick Payot
diff --git a/account_credit_control_aggregate_level/readme/DESCRIPTION.md b/account_credit_control_aggregate_level/readme/DESCRIPTION.md
new file mode 100644
index 000000000..9738e4eab
--- /dev/null
+++ b/account_credit_control_aggregate_level/readme/DESCRIPTION.md
@@ -0,0 +1,4 @@
+Account Credit Control module is a part of Financial Tools used in
+business to ensure that once sales are made they are realised as cash.
+This module helps to identify outstanding debt beyond tolerance level
+and setup followup method.
diff --git a/account_credit_control_aggregate_level/readme/USAGE.md b/account_credit_control_aggregate_level/readme/USAGE.md
new file mode 100644
index 000000000..dd260bed6
--- /dev/null
+++ b/account_credit_control_aggregate_level/readme/USAGE.md
@@ -0,0 +1,9 @@
+The option ``Aggregate 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, a communication is 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_aggregate_level/static/description/index.html b/account_credit_control_aggregate_level/static/description/index.html
new file mode 100644
index 000000000..0455a17b5
--- /dev/null
+++ b/account_credit_control_aggregate_level/static/description/index.html
@@ -0,0 +1,450 @@
+
+
+
+
+
+Account Credit Control
+
+
+
+
+
Account Credit Control
+
+
+
+
Account Credit Control module is a part of Financial Tools used in
+business to ensure that once sales are made they are realised as cash.
+This module helps to identify outstanding debt beyond tolerance level
+and setup followup method.
On credit policy enable Aggregate lower levels to aggregate lines
+from lower level and same partner when a credit control line generated
+by this policy is processed.
The option Aggregate 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, a
+communication is 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.
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.
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.