# Part of Fusion Accounting. See LICENSE file for full copyright and licensing details. from odoo import api, fields, models, _ from odoo.exceptions import UserError class FusionPartnerFollowup(models.Model): """Extends the partner model with payment follow-up capabilities. Adds fields and methods for tracking overdue invoices, determining the appropriate follow-up level, and launching the follow-up workflow directly from the partner form. """ _inherit = 'res.partner' # ---- Follow-up Fields ---- followup_level_id = fields.Many2one( comodel_name='fusion.followup.level', string="Follow-up Level", company_dependent=True, tracking=True, help="Current follow-up escalation level for this partner.", ) followup_next_action_date = fields.Date( string="Next Follow-up Date", company_dependent=True, tracking=True, help="Date on which the next follow-up action should be performed.", ) followup_responsible_id = fields.Many2one( comodel_name='res.users', string="Follow-up Responsible", company_dependent=True, tracking=True, help="User responsible for managing payment collection for this partner.", ) # ---- Computed Indicators ---- fusion_overdue_amount = fields.Monetary( string="Total Overdue Amount", compute='_compute_fusion_overdue_amount', currency_field='currency_id', help="Total amount of overdue receivables for the current company.", ) fusion_overdue_count = fields.Integer( string="Overdue Invoice Count", compute='_compute_fusion_overdue_amount', help="Number of overdue invoices for the current company.", ) # -------------------------------------------------- # Computed Fields # -------------------------------------------------- @api.depends('credit') def _compute_fusion_overdue_amount(self): """Compute the total overdue receivable amount and invoice count. Uses the receivable move lines that are posted, unreconciled, and past their maturity date. """ today = fields.Date.context_today(self) for partner in self: overdue_data = partner.get_overdue_invoices() partner.fusion_overdue_amount = sum( line.amount_residual for line in overdue_data ) partner.fusion_overdue_count = len(overdue_data.mapped('move_id')) # -------------------------------------------------- # Public Methods # -------------------------------------------------- def get_overdue_invoices(self): """Return unpaid receivable move lines that are past due. Searches for posted, unreconciled journal items on receivable accounts where the maturity date is earlier than today and there is still an outstanding balance. :returns: An ``account.move.line`` recordset. """ self.ensure_one() today = fields.Date.context_today(self) return self.env['account.move.line'].search([ ('partner_id', '=', self.commercial_partner_id.id), ('company_id', '=', self.env.company.id), ('account_id.account_type', '=', 'asset_receivable'), ('parent_state', '=', 'posted'), ('reconciled', '=', False), ('amount_residual', '>', 0), ('date_maturity', '<', today), ]) def get_overdue_amount(self): """Return the total overdue receivable amount for this partner. :returns: A float representing the overdue monetary amount. """ self.ensure_one() overdue_lines = self.get_overdue_invoices() return sum(overdue_lines.mapped('amount_residual')) def action_open_followup(self): """Open the follow-up form view for this partner. Locates or creates the ``fusion.followup.line`` record for the current partner and company, then opens the form view for it. :returns: An ``ir.actions.act_window`` dictionary. """ self.ensure_one() followup_line = self.env['fusion.followup.line'].search([ ('partner_id', '=', self.id), ('company_id', '=', self.env.company.id), ], limit=1) if not followup_line: # Assign first level automatically first_level = self.env['fusion.followup.level'].search([ ('company_id', '=', self.env.company.id), ], order='sequence, id', limit=1) followup_line = self.env['fusion.followup.line'].create({ 'partner_id': self.id, 'company_id': self.env.company.id, 'followup_level_id': first_level.id if first_level else False, }) return { 'type': 'ir.actions.act_window', 'name': _("Payment Follow-up: %s", self.display_name), 'res_model': 'fusion.followup.line', 'res_id': followup_line.id, 'view_mode': 'form', 'target': 'current', } # -------------------------------------------------- # Scheduled Actions # -------------------------------------------------- @api.model def compute_partners_needing_followup(self): """Scheduled action: find partners with overdue invoices and create or update their follow-up tracking records. This method is called daily by ``ir.cron``. It scans for partners that have at least one overdue receivable and ensures each one has a ``fusion.followup.line`` record. Existing records are refreshed so their computed fields stay current. :returns: ``True`` """ today = fields.Date.context_today(self) companies = self.env['res.company'].search([]) for company in companies: # Find partners with overdue receivables in this company overdue_lines = self.env['account.move.line'].search([ ('company_id', '=', company.id), ('account_id.account_type', '=', 'asset_receivable'), ('parent_state', '=', 'posted'), ('reconciled', '=', False), ('date_maturity', '<', today), ('amount_residual', '>', 0), ]) partner_ids = overdue_lines.mapped( 'partner_id.commercial_partner_id' ).ids if not partner_ids: continue # Fetch existing tracking records for these partners existing_lines = self.env['fusion.followup.line'].search([ ('partner_id', 'in', partner_ids), ('company_id', '=', company.id), ]) existing_partner_ids = existing_lines.mapped('partner_id').ids # Determine the first follow-up level for new records first_level = self.env['fusion.followup.level'].search([ ('company_id', '=', company.id), ], order='sequence, id', limit=1) # Create tracking records for partners that don't have one yet new_partner_ids = set(partner_ids) - set(existing_partner_ids) if new_partner_ids: self.env['fusion.followup.line'].create([{ 'partner_id': pid, 'company_id': company.id, 'followup_level_id': first_level.id if first_level else False, } for pid in new_partner_ids]) # Refresh computed fields on all relevant records all_lines = self.env['fusion.followup.line'].search([ ('partner_id', 'in', partner_ids), ('company_id', '=', company.id), ]) all_lines.compute_followup_status() return True