Skip to content

Commit

Permalink
[ADD] osi_partner_credit_limit: add module
Browse files Browse the repository at this point in the history
  • Loading branch information
cbeddies committed Sep 12, 2024
1 parent e827e6f commit ca973e7
Show file tree
Hide file tree
Showing 28 changed files with 839 additions and 0 deletions.
53 changes: 53 additions & 0 deletions osi_partner_credit_limit/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3

========
Overview
========

This module prevents users from shippings orders that would put customers
above their credit limit based on open invoices.

It adds a new group that allows certain users to manage credit hold of the
customers.

After this module is installed, credit limits will be verified for all
customers.

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

* Add users to the 'Credit Hold' group

Usage
=====

* Sales Hold on Customer: Disallows new sales orders and prevents confirmation
of existing quotations.

* Credit Hold on Customer: Allows new sales orders, confirmation of orders,
but prevents orders from being shipped.

* Credit Limit: Maximum allowed receivable balance for a customer.

* Grace period: Time allowed for the customer to make payment after the term
has expired.

* Credit Override: When set on sale order, allows shipment on the sale order
to be processed even if there is a hold on the sale order.

* Credit Override requires special permission to be set on the user.

Credits
=======

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

* OSI Dev Team <[email protected]>
* Sandeep Mangukiya <[email protected]>
* Maxime Chambreuil <[email protected]>
* Bhavesh Odedra <[email protected]>
* Balaji Kannan <[email protected]>
* Hardik Suthar <[email protected]>
4 changes: 4 additions & 0 deletions osi_partner_credit_limit/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Copyright (C) 2019 - 2021, Open Source Integrators
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from . import models
27 changes: 27 additions & 0 deletions osi_partner_credit_limit/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Copyright (C) 2019 - 2021, Open Source Integrators
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

{
"name": "OSI Partner Credit Limit",
"version": "17.0.1.0.0",
"license": "AGPL-3",
"author": "Open Source Integrators",
"category": "Sales",
"maintainer": "Open Source Integrators",
"summary": "Enforce Partner Credit Limit",
"website": "https://github.com/ursais/osi-addons",
"depends": [
"sale",
"sale_stock",
"stock",
],
"data": [
"security/osi_partner_credit_limit.xml",
"data/picking_data.xml",
"views/res_partner.xml",
"views/sale.xml",
"views/stock.xml",
],
"installable": True,
"maintainers": ["bodedra"],
}
17 changes: 17 additions & 0 deletions osi_partner_credit_limit/data/picking_data.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding='UTF-8'?>
<odoo>

<!-- Scheduler to update customer hold -->
<record model="ir.cron" id="credit_hold_out_picking_cron">
<field name="name">Customer Credit Hold on Delivery Orders</field>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field name="doall" eval="False"/>
<field name="model_id" ref="model_stock_picking"/>
<field name="code">model.compute_customer_hold()</field>
<field name="state">code</field>
<field eval="True" name="active" />
</record>

</odoo>
6 changes: 6 additions & 0 deletions osi_partner_credit_limit/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Copyright (C) 2019 - 2021, Open Source Integrators
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from . import res_partner
from . import sale_order
from . import stock_picking
141 changes: 141 additions & 0 deletions osi_partner_credit_limit/models/res_partner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# Copyright (C) 2019 - 2021, Open Source Integrators
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from datetime import datetime, timedelta

from odoo import fields, models

import logging
logger = logging.getLogger(__name__)


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

sales_hold = fields.Boolean(
string="Sales Hold",
default=False,
help="If checked, new quotations cannot be confirmed",
)
grace_period = fields.Integer(
string="Grace Period",
help="Grace period added on top of the customer \
payment term "
"(in days)",
)
credit_hold = fields.Boolean(
string="Credit Hold",
help="Place the customer on credit hold to prevent \
from shipping goods",
)
credit_used = fields.Monetary(string="Credit Used", compute="calculate_credit")
credit_available = fields.Monetary(
string="Credit Available", compute="calculate_credit"
)
ship_hold_days = fields.Integer(
string="Customer Credit Period",
help="Period past scheduled date for customer hold to verify credit card authorization",
)
def get_existing_invoice_balance(self, partner_id):
# Open invoices (unpaid or partially paid invoices --
# It is already included in partner.credit
invoice_ids = self.env["account.move"].search(
[
("partner_id", "=", partner_id.id),
# ('state', '=', 'draft'),
("state", "in", ["open", "posted"]),
("payment_state", "in", ["not_paid", "partial"]),
("move_type", "in", ["out_invoice", "out_refund"]),
]
)
# Invoices that are open (also shows up as part of partner.
# Credit, so must be deducted
now = fields.Datetime.to_string(datetime.now())
grace_period = timedelta(days=partner_id.grace_period)
existing_invoice_balance = sum(
invoice_ids.filtered(
lambda inv: (
inv.invoice_date_due
or inv.date_invoice
or inv.create_date + grace_period > now
)
).mapped("amount_residual")
)

return existing_invoice_balance

def get_existing_order_balance(self, partner_id):
# Other orders for this partner
order_ids = self.env["sale.order"].search(
[
("partner_id", "=", partner_id.id),
("state", "=", "sale"),
("invoice_status", "!=", "invoiced"),
]
)

# Confirmed orders - invoiced - draft or open / not invoiced
existing_order_balance = sum(order_ids.mapped("amount_total"))

return existing_order_balance


def calculate_credit(self):
for partner_id in self:
existing_order_balance = self.get_existing_order_balance(partner_id)
existing_invoice_balance = self.get_existing_invoice_balance(partner_id)

# All open sale orders + partner credit (AR balance) -
# Open invoices (already included in partner credit)
partner_id.credit_used = existing_invoice_balance + existing_order_balance
partner_id.credit_available = (
partner_id.credit_limit - partner_id.credit_used
)
def write(self, vals):
res = super(Partner, self).write(vals)
if "credit_limit" or "credit_hold" in vals:
for partner in self:
order_ids = self.env["sale.order"].search(
[("partner_id", "=", partner.id)]
)
# only if partner is on credit hold, set sale orders on ship hold immediately
ship_hold = partner.credit_hold

# check for credit_limit
if partner.credit_limit > 0 and order_ids:
if not ship_hold and not self.check_limit(order_ids[0]):
ship_hold = False
else:
ship_hold = True

order_ids.write({"ship_hold": ship_hold})

# user reset credit authorization days
if "ship_hold_days" in vals:
pickings = self.env["stock.picking"].search(
[
("picking_type_code", "=", "outgoing"),
("partner_id.parent_id", "=", self.id),
("state", "in", ("assigned", "confirmed", "waiting")),
]
)
pickings.compute_customer_hold()

return res

def check_limit(self, sale_id):
partner_id = sale_id.partner_id
# Confirmed orders - invoiced - draft or open / not invoiced
existing_order_balance = self.get_existing_order_balance(partner_id)
existing_invoice_balance = self.get_existing_invoice_balance(partner_id)

# All open sale orders + partner credit (AR balance) -
# Open invoices (already included in partner credit)
if (
partner_id.credit_limit
and (existing_invoice_balance + existing_order_balance)
> partner_id.credit_limit
):
return True
else:
return False
55 changes: 55 additions & 0 deletions osi_partner_credit_limit/models/sale_order.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Copyright (C) 2019 - 2021, Open Source Integrators
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from odoo import _, fields, models
from odoo.exceptions import ValidationError


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

sales_hold = fields.Boolean(
related="partner_id.sales_hold", string="Customer Sales Hold"
)
credit_hold = fields.Boolean(
related="partner_id.credit_hold", string="Customer Credit Hold"
)
ship_hold = fields.Boolean(string="Delivery Hold", copy=False)
credit_override = fields.Boolean(
string="Override Hold", tracking=True, default=False
)
ship_hold_days = fields.Integer(
related="partner_id.ship_hold_days",
help="Period past scheduled date for customer hold to verify credit card authorization",
)

def action_confirm(self):
state = self.partner_id.check_limit(self)
if not state:
self.ship_hold = False
if self.sales_hold and not self.credit_override:
message = _("""Cannot confirm Order! The customer is on sales hold.""")
# Display that the customer is on sales hold
raise ValidationError(message)
elif self.ship_hold and not self.credit_override:
message = _(
"""Cannot confirm Order! The customer exceed available
credit limit and is on ship hold."""
)
raise ValidationError(message)
else:
# attempt to change the state of this order to be included in \
# the computation for check_limit function
prev_state = self.state
self.state = "sale"
if self.partner_id.check_limit(self) and not self.credit_override:
self.state = prev_state
self.ship_hold = True
message = _(
"""Cannot confirm Order!
This will exceed allowed Credit Limit.
To Override, check Override Sales/Credit/Delivery Hold"""
)
raise ValidationError(message)
self.state = prev_state
return super(SaleOrder, self).action_confirm()
Loading

0 comments on commit ca973e7

Please sign in to comment.