diff --git a/account_credit_hold/__init__.py b/account_credit_hold/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/account_credit_hold/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/account_credit_hold/__manifest__.py b/account_credit_hold/__manifest__.py new file mode 100644 index 0000000..3fe567a --- /dev/null +++ b/account_credit_hold/__manifest__.py @@ -0,0 +1,20 @@ +{ + "name": "Account Credit Hold", + "version": "18.0.1.1.1", + "summary": "Allows setting clients on credit hold, blocking the ability confirm a new sales order.", + "category": "Accounting/Accounting", + "author": "Bemade Inc.", + "maintainer": "Marc Durepos ", + "website": "http://www.bemade.org", + "license": "LGPL-3", + "depends": ["sale", "account_followup", "stock"], + "data": [ + "views/account_followup_views.xml", + "views/sale_order_views.xml", + "views/res_partner_views.xml", + "views/stock_picking_views.xml", + ], + "demo": [], + "installable": True, + "auto_install": False, +} diff --git a/account_credit_hold/models/__init__.py b/account_credit_hold/models/__init__.py new file mode 100644 index 0000000..0c2c11c --- /dev/null +++ b/account_credit_hold/models/__init__.py @@ -0,0 +1,5 @@ +from . import account_followup +from . import res_partner +from . import sale_order +from . import stock_picking +from . import account_followup_report diff --git a/account_credit_hold/models/account_followup.py b/account_credit_hold/models/account_followup.py new file mode 100644 index 0000000..5af3363 --- /dev/null +++ b/account_credit_hold/models/account_followup.py @@ -0,0 +1,8 @@ +from odoo import fields, models, api + + +class FollowupLine(models.Model): + _inherit = 'account_followup.followup.line' + + account_hold = fields.Boolean(string="Place on Credit Hold", + help="Place clients on account hold, restricting confirmation of new orders.") diff --git a/account_credit_hold/models/account_followup_report.py b/account_credit_hold/models/account_followup_report.py new file mode 100644 index 0000000..a28efdc --- /dev/null +++ b/account_credit_hold/models/account_followup_report.py @@ -0,0 +1,12 @@ +from odoo import models, fields, api, _ + + +class FollowUpReport(models.AbstractModel): + _inherit = 'account.followup.report' + + def _get_line_info(self, followup_line): + res = super()._get_line_info(followup_line) + res.update({ + 'credit_hold': followup_line.account_hold + }) + return res diff --git a/account_credit_hold/models/res_partner.py b/account_credit_hold/models/res_partner.py new file mode 100644 index 0000000..7cdf9a1 --- /dev/null +++ b/account_credit_hold/models/res_partner.py @@ -0,0 +1,80 @@ +from odoo import fields, models, api, _ +from datetime import date + + +class Partner(models.Model): + _inherit = "res.partner" + + postpone_hold_until = fields.Date( + string="Postpone Hold", + help="Grace period specific to this partner despite unpaid invoices.", + tracking=True, + ) + + hold_bg = fields.Boolean( + string="Hold (technical)", + compute="_compute_hold_bg", + store=True, + default=False, + compute_sudo=True, + tracking=True, + ) + on_hold = fields.Boolean( + string="Account on Hold", + help="Client account is on hold for unpaid overdue invoices.", + compute="_compute_on_hold", + compute_sudo=True, + ) + + @api.depends("postpone_hold_until", "hold_bg") + def _compute_on_hold(self): + # manually re-compute hold_bg since followup_status doesn't get updated in Python but gets recalculated + # by an SQL query every time + self._compute_hold_bg() + for rec in self: + # If the parent company is on hold, so are all its sub-contacts and subsidiaries + if rec.commercial_partner_id and rec.commercial_partner_id.on_hold: + rec.on_hold = True + return + # If there is no parent company or the parent is not on hold, we compute for ourselves + if rec.hold_bg and not ( + rec.postpone_hold_until and rec.postpone_hold_until > date.today() + ): + rec.on_hold = True + else: + if rec.on_hold: + rec.message_post(_("Credit hold lifted.")) + rec.on_hold = False + + @api.autovacuum + def _cleanup_expired_hold_postponements(self): + expired_holds = self.search([("postpone_hold_until", "<=", date.today())]) + expired_holds.write({"postpone_hold_until": False}) + + def action_credit_hold(self): + for rec in self: + rec.hold_bg = True + rec.message_post(body=_("Placed on credit hold.")) + + def action_lift_credit_hold(self): + for rec in self: + rec.hold_bg = False + rec.message_post(body=_("Credit hold lifted.")) + + def _execute_followup_partner(self, options=None): + res = super()._execute_followup_partner(options) + if self.followup_status == "in_need_of_action": + if self.followup_line_id.account_hold: + self.action_credit_hold() + return res + + @api.depends("followup_status", "followup_line_id") + def _compute_hold_bg(self): + first_followup_level = self._get_first_followup_level() + for rec in self: + prev_hold_bg = rec.hold_bg + level = rec.followup_line_id + if rec.followup_status == "no_action_needed" and not level: + rec.hold_bg = False + else: + rec.hold_bg = prev_hold_bg diff --git a/account_credit_hold/models/sale_order.py b/account_credit_hold/models/sale_order.py new file mode 100644 index 0000000..390e97e --- /dev/null +++ b/account_credit_hold/models/sale_order.py @@ -0,0 +1,17 @@ +from odoo import fields, models, api, _ +from odoo.exceptions import UserError + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + client_on_hold = fields.Boolean(string='Client on Hold', + help="Whether or not a client has been put on hold due to unpaid invoices.", + related="partner_id.on_hold") + + @api.depends('client_on_hold') + def action_confirm(self): + if any(self.mapped('client_on_hold')): + raise UserError(_("This client is on credit hold. No new orders can be confirmed until past-due invoices " + "are paid or the accounting team postpones the hold.")) + super().action_confirm() diff --git a/account_credit_hold/models/stock_picking.py b/account_credit_hold/models/stock_picking.py new file mode 100644 index 0000000..0d46e32 --- /dev/null +++ b/account_credit_hold/models/stock_picking.py @@ -0,0 +1,9 @@ +from odoo import fields, models, api + + +class ModelName(models.Model): + _inherit = "stock.picking" + + client_on_hold = fields.Boolean(string='Client on Hold', + help="Whether or not a client has been put on hold due to unpaid invoices.", + related="partner_id.on_hold") diff --git a/account_credit_hold/readme.md b/account_credit_hold/readme.md new file mode 100644 index 0000000..db624c3 --- /dev/null +++ b/account_credit_hold/readme.md @@ -0,0 +1,40 @@ +# Overview + +This module adds the notion of placing clients on credit hold to the followup levels from the Odoo Enterprise +account_followup module. It adds an option to followup levels to mark clients matching the followup criteria as on +credit hold. This hold restricts the confirmation of new sales orders for these clients. + +Accountant and admin users can set a date until which the account hold will be +postponed on a specific partner's form view. This effectively gives clients an extra +grace period, allowing orders to be confirmed until the period ends. + +# Change Log + +## 17.0.1.0.0 (2024-05-15) + +Various modifications to adapt code to Odoo 17.0 + +## 15.0.2.0.0 (2023-05-04) + +Complete remake of the module, making the "Credit Hold" an action that is either manually or +automatically triggered from the Accounting > Followup Reports section or by setting the automatic application field +on followup levels. + +## 15.0.1.1.0 (2023-05-03) + +Adds a ribbon to stock pickings for clients on hold, and therefore a dependency on stock. + +## 15.0.1.0.2 (2023-05-03) + +Fix to sale order view and sale order confirmation for clients not on hold. + +## 15.0.1.0.1 (2023-05-03) + +Fix clients on hold when status is "outstanding_invoices". + +## 15.0.1.0.0 (2023-05-02) Initial Release + +Initial release of the module, including a setting on follow-up levels to toggle placing on credit hold. Blocks +the confirmation of sales orders for clients on credit hold. Red "Credit Hold" banner appears on sales orders and +partner form view when a client is on credit hold. Credit hold can be postponed by setting the "Postpone Hold" field +on the partner form view. \ No newline at end of file diff --git a/account_credit_hold/views/account_followup_views.xml b/account_credit_hold/views/account_followup_views.xml new file mode 100644 index 0000000..962cd79 --- /dev/null +++ b/account_credit_hold/views/account_followup_views.xml @@ -0,0 +1,49 @@ + + + + account_credit_hold.account_followup_line.form + account_followup.followup.line + + + + + + + + customer.statements.form.view.inherit + res.partner + + + + +