# 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. :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), ('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