Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 171 additions & 0 deletions partner_credit_average_day/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
==========================
Partner Credit Average Day
==========================

..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:0527759d5d2b6138f9a6a73577d3b68b2fd292c959313643c72a57985454bb5b
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |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-LGPL--3-blue.png
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
:alt: License: LGPL-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/partner_credit_average_day
: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-partner_credit_average_day
: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|

This module extends customer credit management by introducing an
automatic credit check based on the average age of all open customer
invoices.

Key Features
------------

- Adds the following fields to partners:

- **Apply credit limit validation** — enables or disables checks.
- **Credit Limit (Days)** — maximum allowed average invoice age.
- **Average Credit Days** — computed field representing the average
age of outstanding posted customer invoices.

- Enhances **Sale Order** confirmation:

- Blocks confirmation when:

- The user is *not* in the **Credit Manager** group,
- The partner has validation enabled,
- The partner’s average invoice age exceeds the configured limit.

- Shows a detailed list of open invoices when blocking.

- Provides a **Credit Manager** group to bypass the rules.

This module helps organizations enforce stricter credit control before
allowing further sales to customers with aged outstanding invoices.

**Table of contents**

.. contents::
:local:

Configuration
=============

This module introduces partner-level configuration options that
determine whether a customer is allowed to confirm a sale order based on
the average age of their open invoices.

To configure the feature:

1. Go to **Contacts → Customers** and open any customer record.
2. Under the **Credit Control** section, configure:

- **Apply credit limit validation**
Enable this option to activate the credit rules for this customer.
- **Credit Limit (Days)**
Set the maximum allowed average age (in days) of all posted
customer invoices that still have a residual amount.

3. Ensure that the **Credit Manager** group contains the users who
should be allowed to bypass this credit check.

Usage
=====

This module is used to prevent confirming a Sale Order when a customer's
open invoices have an average age higher than a configured limit.

1. Configure the Partner
~~~~~~~~~~~~~~~~~~~~~~~~

1. Go to **Contacts → Customers**.
2. Open a customer.
3. Set:

- **Apply credit limit validation** = enabled
- **Credit Limit (Days)** = the maximum allowed average age

4. The computed **Average Credit Days** will display automatically.

2. Create or Review Open Invoices
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

1. Create customer invoices (or leave existing ones).
2. Post the invoices.
3. Ensure some invoices remain unpaid (residual > 0).

3. Create a Sale Order
~~~~~~~~~~~~~~~~~~~~~~

1. Create a new Sale Order for this customer.
2. Add products and confirm.

4. Resulting Behavior
~~~~~~~~~~~~~~~~~~~~~

- If the customer exceeds the allowed average credit age:

- **Non-Credit Manager users** → confirmation is **blocked**.
- A popup error shows:

- Customer name
- Average invoice age
- Allowed limit
- List of open invoices with dates, residuals, and ages

- If the user belongs to **Credit Manager**, the sale confirms normally.

Bug Tracker
===========

Bugs are tracked on `GitHub Issues <https://github.com/OCA/credit-control/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 <https://github.com/OCA/credit-control/issues/new?body=module:%20partner_credit_average_day%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

Do not contact contributors directly about support or help with technical issues.

Credits
=======

Authors
-------

* Open Source Integrators

Contributors
------------

- Maxime Chambreuil <mchambreuil@opensourceintegrators.com>
- Nikul Chaudhary <nchaudhary@opensourceintegrators.com>

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 <https://github.com/OCA/credit-control/tree/18.0/partner_credit_average_day>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
3 changes: 3 additions & 0 deletions partner_credit_average_day/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Copyright 2025 Open Source Integrators
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
from . import models
19 changes: 19 additions & 0 deletions partner_credit_average_day/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Copyright 2025 Open Source Integrators
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
{
"name": "Partner Credit Average Day",
"version": "18.0.1.0.0",
"summary": """Block sale confirmation when partner average
open invoice age exceeds configured days""",
"category": "Accounting",
"author": "Open Source Integrators, Odoo Community Association (OCA)",
"maintainer": "Open Source Integrators",
"license": "LGPL-3",
"website": "https://github.com/OCA/credit-control",
"depends": ["sale", "account"],
"data": [
"security/groups.xml",
"views/res_partner_views.xml",
],
"installable": True,
}
4 changes: 4 additions & 0 deletions partner_credit_average_day/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Copyright 2025 Open Source Integrators
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
from . import res_partner
from . import sale_order
73 changes: 73 additions & 0 deletions partner_credit_average_day/models/res_partner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Copyright 2025 Open Source Integrators
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
from odoo import fields, models


class ResPartner(models.Model):
_inherit = "res.partner"

credit_limit_exception = fields.Boolean(
string="Apply Credit Limit Validation",
default=True,
help="""When checked, the partner will be validated against
average credit days on order confirmation.""",
)
credit_limit_day = fields.Integer(
string="Credit Limit (Days)",
help="Maximum allowed average age (in days) of open invoices for this partner.",
)
credit_average_day = fields.Float(
string="Average Credit Days",
compute="_compute_credit_average_day",
store=False,
help="Computed average age (in days) of the partner open invoices.",
)

def _compute_credit_average_day(self):
"""
Compute the average age (in days) of all open customer invoices.
An invoice is considered when:
- move_type = 'out_invoice'
- state = 'posted'
- amount_residual > 0 (still unpaid or partially paid)

The age of an invoice = today - invoice_date.
If invoice_date is missing, fallback to move.date or create_date.
"""
AccountMove = self.env["account.move"]
today = fields.Date.context_today(self)
for partner in self:
# Search unpaid posted customer invoices
moves = AccountMove.search(
[
("partner_id", "=", partner.id),
("move_type", "=", "out_invoice"),
("state", "=", "posted"),
("amount_residual", ">", 0),
]
)
ages = []

for move in moves:
# Determine invoice date using a safe fallback chain
invoice_date = (
move.invoice_date
or move.date
or (move.create_date.date() if move.create_date else None)
)

if isinstance(invoice_date, str):
invoice_date = fields.Date.from_string(invoice_date)

# If still no date found, skip invoice
if not invoice_date:
continue

# Calculate age
age_days = (today - invoice_date).days

# Avoid negative values if invoice date is in the future
ages.append(max(age_days, 0))

# Assign computed average or 0 if no invoices
partner.credit_average_day = (sum(ages) / len(ages)) if ages else 0.0
89 changes: 89 additions & 0 deletions partner_credit_average_day/models/sale_order.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Copyright 2025 Open Source Integrators
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
from odoo import _, fields, models
from odoo.exceptions import UserError


class SaleOrder(models.Model):
_inherit = "sale.order"

def action_confirm(self):
"""
Extend Sale Order confirmation to enforce customer credit rules:
- If `credit_limit_exception` is enabled on the partner,
- And user is NOT in the Credit Manager group,
- And partner's average invoice age exceeds the configured limit,
Then block confirmation and show a detailed list of open invoices.
"""
AccountMove = self.env["account.move"]
today = fields.Date.context_today(self)

for order in self:
partner = order.partner_id

if (
not partner.credit_limit_exception
or self.env.user.has_group(
"partner_credit_average_day.group_credit_manager"
)
or not partner.credit_limit_day
):
continue

avg_days = float(partner.credit_average_day or 0.0)

# If average exceeds limit → BLOCK ORDER CONFIRMATION
if avg_days > float(partner.credit_limit_day):
# Fetch all open posted customer invoices
open_invoices = AccountMove.search(
[
("partner_id", "=", partner.id),
("move_type", "=", "out_invoice"),
("state", "=", "posted"),
("amount_residual", ">", 0),
],
order="invoice_date asc",
)

# Header lines
message_lines = [
_(
'The customer "%(name)s" exceeds the allowed credit limit.\n'
"Average invoice age: %(avg).2f days\n"
"Allowed limit: %(limit)s days\n"
)
% {
"name": partner.display_name,
"avg": avg_days,
"limit": partner.credit_limit_day,
},
_("Open invoices:"),
"",
]

# Detailed invoice list
for inv in open_invoices:
invoice_date = (
inv.invoice_date
or inv.date
or (inv.create_date.date() if inv.create_date else None)
)
if isinstance(invoice_date, str):
invoice_date = fields.Date.from_string(invoice_date)
if not invoice_date:
continue

age_days = (today - invoice_date).days
age_days = max(age_days, 0)

message_lines.append(
f"- {inv.name or inv.display_name} | "
f"Date: {invoice_date} | "
f"Residual: {inv.amount_residual:.2f} | "
f"Age: {age_days} days"
)

# Raise compiled message
raise UserError("\n".join(message_lines))

return super().action_confirm()
3 changes: 3 additions & 0 deletions partner_credit_average_day/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"
15 changes: 15 additions & 0 deletions partner_credit_average_day/readme/CONFIGURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
This module introduces partner-level configuration options that determine
whether a customer is allowed to confirm a sale order based on the
average age of their open invoices.

To configure the feature:

1. Go to **Contacts → Customers** and open any customer record.
2. Under the **Credit Control** section, configure:
- **Apply credit limit validation**
Enable this option to activate the credit rules for this customer.
- **Credit Limit (Days)**
Set the maximum allowed average age (in days) of all posted customer invoices
that still have a residual amount.
3. Ensure that the **Credit Manager** group contains the users
who should be allowed to bypass this credit check.
2 changes: 2 additions & 0 deletions partner_credit_average_day/readme/CONTRIBUTORS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- Maxime Chambreuil \<<mchambreuil@opensourceintegrators.com>\>
- Nikul Chaudhary \<<nchaudhary@opensourceintegrators.com>\>
Loading
Loading