Files
Odoo-Modules/Fusion Accounting/models/res_partner_followup.py
2026-02-22 01:22:18 -05:00

203 lines
7.6 KiB
Python

# 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