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..564b9bd --- /dev/null +++ b/account_credit_hold/__manifest__.py @@ -0,0 +1,30 @@ +{ + 'name': 'Account Credit Hold', + 'version': '15.0.2.0.0.1', + 'summary': 'Allows setting clients on credit hold, blocking the ability confirm a new sales order.', + 'description': 'Allows setting clients on 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', + ], + 'assets': { + 'web.assets_backend': [ + 'account_credit_hold/static/src/js/followup_form_model.js', + 'account_credit_hold/static/src/js/followup_form_controller.js', + ], + 'web.assets_qweb': [ + 'account_credit_hold/static/src/xml/account_followup_template.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..9c9b852 --- /dev/null +++ b/account_credit_hold/models/account_followup_report.py @@ -0,0 +1,14 @@ +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 + @api.model + def credit_hold(self, options): + partner_id = options['partner_id'] + partner = self.env['res.partner'].browse(partner_id) + partner.action_credit_hold() diff --git a/account_credit_hold/models/res_partner.py b/account_credit_hold/models/res_partner.py new file mode 100644 index 0000000..f7fbded --- /dev/null +++ b/account_credit_hold/models/res_partner.py @@ -0,0 +1,59 @@ +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.", ) + + hold_bg = fields.Boolean(string="Hold (technical)", + compute="_compute_hold_bg", + store=True, + default=False) + on_hold = fields.Boolean(string="Account on Hold", + help="Client account is on hold for unpaid overdue invoices.", + compute="_compute_on_hold") + + @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 rec.hold_bg and not (rec.postpone_hold_until and rec.postpone_hold_until > date.today()): + rec.on_hold = True + else: + 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): + message = _('Placed on credit hold') + for rec in self: + rec.hold_bg = True + rec.message_post() + + def _execute_followup_partner(self): + res = super()._execute_followup_partner() + if self.followup_status == 'in_need_of_action': + if self.followup_line_id.account_hold: + self.action_credit_hold() + return res + + # BV: FOR MIGRATION + #@api.depends('followup_status', 'followup_level') + def _compute_hold_bg(self): + first_followup_level = self.env['account_followup.followup.line'].search( + [('company_id', '=', self.env.company.id)], order="delay asc", limit=1) + for rec in self: + prev_hold_bg = rec.hold_bg + level = rec.followup_line_id + if rec.followup_status == 'no_action_needed' and level == first_followup_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..b11668c --- /dev/null +++ b/account_credit_hold/readme.md @@ -0,0 +1,35 @@ +# 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 +## 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/static/src/js/followup_form_controller.js b/account_credit_hold/static/src/js/followup_form_controller.js new file mode 100644 index 0000000..39b26d0 --- /dev/null +++ b/account_credit_hold/static/src/js/followup_form_controller.js @@ -0,0 +1,48 @@ +/** @odoo-module **/ + +var FollowupFormController = require('account_followup.FollowupFormController'); +import { patch } from '@web/core/utils/patch'; + +var PatchedController = patch(FollowupFormController.prototype, "followup_form_controller", { + events: _.extend({}, FollowupFormController.prototype.events, { + 'click .o_account_followup_credit_hold_button': '_onCreditHold', + }), + updateButtons() { + this._super(...arguments); + let setButtonClass = (button, primary) => { + /* Set class 'btn-primary' if parameter `primary` is true + * 'btn-secondary' otherwise + */ + let addedClass = primary ? 'btn-primary' : 'btn-secondary' + let removedClass = !primary ? 'btn-secondary' : 'btn-primary' + this.$buttons.find(`button.${button}`) + .removeClass(removedClass).addClass(addedClass); + } + if (!this.$buttons) { + return; + } + let followupLevel = this.model.localData[this.handle].data.followup_level; + setButtonClass('o_account_followup_credit_hold_button', followupLevel.credit_hold); + }, + _onCreditHold: function() { + var self = this; + this.model.doCreditHold(this.handle); + this.options = { + partner_id: this._getPartner() + }; + this._rpc({ + model: 'account.followup.report', + method: 'credit_hold', + args: [this.options], + }).then(function (result) { + self._removeHighlightCreditHold(); + self._displayDone(); + }); + }, + _removeHighlightCreditHold: function() { + this.$buttons.find('button.o_account_followup_credit_hold_button') + .removeClass('btn-primary').addClass('btn-secondary'); + }, +}); + +export { PatchedController }; \ No newline at end of file diff --git a/account_credit_hold/static/src/js/followup_form_model.js b/account_credit_hold/static/src/js/followup_form_model.js new file mode 100644 index 0000000..bd84e76 --- /dev/null +++ b/account_credit_hold/static/src/js/followup_form_model.js @@ -0,0 +1,14 @@ +/** @odoo-module **/ + +var FollowupFormModel = require('account_followup.FollowupFormModel'); +import { patch } from '@web/core/utils/patch'; + +var PatchedModel = patch(FollowupFormModel.prototype, 'followup_form_model', { + doCreditHold: function(handle) { + var level = this.localData[handle].data.followup_level; + if(level && level.credit_hold) { + level.credit_hold = false; + } + }, +}); +export { PatchedModel }; \ No newline at end of file diff --git a/account_credit_hold/static/src/xml/account_followup_template.xml b/account_credit_hold/static/src/xml/account_followup_template.xml new file mode 100644 index 0000000..50f74ee --- /dev/null +++ b/account_credit_hold/static/src/xml/account_followup_template.xml @@ -0,0 +1,10 @@ + + + + + + + + \ 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..61caad9 --- /dev/null +++ b/account_credit_hold/views/account_followup_views.xml @@ -0,0 +1,16 @@ + + + + + + account_credit_hold.account_followup_line.form + account_followup.followup.line + + + + + + + + + \ No newline at end of file diff --git a/account_credit_hold/views/res_partner_views.xml b/account_credit_hold/views/res_partner_views.xml new file mode 100644 index 0000000..4e86f5e --- /dev/null +++ b/account_credit_hold/views/res_partner_views.xml @@ -0,0 +1,33 @@ + + + + + + account_credit_hold.res_partner.form + res.partner + + + + + + + + + + + account_credit_hold.view_partner_property_form + res.partner + + + + + + + + + + + \ No newline at end of file diff --git a/account_credit_hold/views/sale_order_views.xml b/account_credit_hold/views/sale_order_views.xml new file mode 100644 index 0000000..bf8af69 --- /dev/null +++ b/account_credit_hold/views/sale_order_views.xml @@ -0,0 +1,18 @@ + + + + + + account_credit_hold.sale_order.form + sale.order + + + + + + + + + + \ No newline at end of file diff --git a/account_credit_hold/views/stock_picking_views.xml b/account_credit_hold/views/stock_picking_views.xml new file mode 100644 index 0000000..2b96e89 --- /dev/null +++ b/account_credit_hold/views/stock_picking_views.xml @@ -0,0 +1,20 @@ + + + + + + account_credit_hold.stock_picking.form + stock.picking + + + + + + + + + + + + \ No newline at end of file