Files
Odoo-Modules/Fusion Accounting/wizard/followup_send_wizard.py
gsinghpal 84c009416e feat: add fusion_odoo_fixes module for default Odoo patches
- New standalone module to collect fixes for default Odoo behavior
- Fix #1: account_followup never clears followup_next_action_date
  when invoices are paid, causing collection emails to fully-paid
  clients. Hooks into _invoice_paid_hook to auto-clear stale data.
- Harden Fusion Accounting followup queries with amount_residual > 0
  filter and add balance check before sending emails

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 03:31:14 -05:00

258 lines
8.8 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 FusionFollowupSendWizard(models.TransientModel):
"""Wizard for previewing and manually sending payment follow-ups.
Allows the user to review the email content, modify the message
body, choose communication channels, and send the follow-up for
one or more partners at once.
"""
_name = 'fusion.followup.send.wizard'
_description = "Fusion Follow-up Send Wizard"
# ---- Context Fields ----
followup_line_id = fields.Many2one(
comodel_name='fusion.followup.line',
string="Follow-up Record",
readonly=True,
)
partner_id = fields.Many2one(
comodel_name='res.partner',
string="Partner",
related='followup_line_id.partner_id',
readonly=True,
)
followup_level_id = fields.Many2one(
comodel_name='fusion.followup.level',
string="Follow-up Level",
related='followup_line_id.followup_level_id',
readonly=True,
)
# ---- Editable Content ----
email_subject = fields.Char(
string="Subject",
compute='_compute_email_preview',
store=True,
readonly=False,
)
email_body = fields.Html(
string="Email Body",
compute='_compute_email_preview',
store=True,
readonly=False,
help="Edit the email body before sending. Changes apply only to this send.",
)
# ---- Channel Overrides ----
do_send_email = fields.Boolean(
string="Send Email",
compute='_compute_channel_defaults',
store=True,
readonly=False,
)
do_send_sms = fields.Boolean(
string="Send SMS",
compute='_compute_channel_defaults',
store=True,
readonly=False,
)
do_print_letter = fields.Boolean(
string="Print Letter",
compute='_compute_channel_defaults',
store=True,
readonly=False,
)
do_join_invoices = fields.Boolean(
string="Attach Invoices",
compute='_compute_channel_defaults',
store=True,
readonly=False,
)
# ---- Informational ----
overdue_amount = fields.Monetary(
string="Overdue Amount",
related='followup_line_id.overdue_amount',
readonly=True,
currency_field='currency_id',
)
overdue_count = fields.Integer(
string="Overdue Invoices",
related='followup_line_id.overdue_count',
readonly=True,
)
currency_id = fields.Many2one(
comodel_name='res.currency',
related='followup_line_id.currency_id',
readonly=True,
)
# --------------------------------------------------
# Default Values
# --------------------------------------------------
@api.model
def default_get(self, fields_list):
"""Populate the wizard from the active follow-up line."""
defaults = super().default_get(fields_list)
active_id = self.env.context.get('active_id')
if active_id and self.env.context.get('active_model') == 'fusion.followup.line':
defaults['followup_line_id'] = active_id
return defaults
# --------------------------------------------------
# Computed Fields
# --------------------------------------------------
@api.depends('followup_level_id', 'partner_id')
def _compute_email_preview(self):
"""Build a preview of the email subject and body.
Uses the level's email template if set, otherwise falls back to
the level description or a sensible default.
"""
for wizard in self:
level = wizard.followup_level_id
partner = wizard.partner_id
company = wizard.followup_line_id.company_id or self.env.company
if level and level.email_template_id:
template = level.email_template_id
# Render the template for the partner
rendered = template._render_field(
'body_html', [partner.id],
engine='inline_template',
compute_lang=True,
)
wizard.email_body = rendered.get(partner.id, '')
rendered_subject = template._render_field(
'subject', [partner.id],
engine='inline_template',
compute_lang=True,
)
wizard.email_subject = rendered_subject.get(partner.id, '')
else:
wizard.email_subject = _(
"%(company)s - Payment Reminder",
company=company.name,
)
if level and level.description:
wizard.email_body = level.description
else:
wizard.email_body = _(
"<p>Dear %(partner)s,</p>"
"<p>This is a reminder that your account has an outstanding "
"balance. Please arrange payment at your earliest convenience.</p>"
"<p>Best regards,<br/>%(company)s</p>",
partner=partner.name or _('Customer'),
company=company.name,
)
@api.depends('followup_level_id')
def _compute_channel_defaults(self):
"""Pre-fill channel toggles from the follow-up level configuration."""
for wizard in self:
level = wizard.followup_level_id
if level:
wizard.do_send_email = level.send_email
wizard.do_send_sms = level.send_sms
wizard.do_print_letter = level.send_letter
wizard.do_join_invoices = level.join_invoices
else:
wizard.do_send_email = True
wizard.do_send_sms = False
wizard.do_print_letter = False
wizard.do_join_invoices = False
# --------------------------------------------------
# Actions
# --------------------------------------------------
def action_send_followup(self):
"""Send the follow-up using the wizard-configured channels and content.
Sends the (possibly edited) email, optionally triggers SMS,
then advances the partner to the next follow-up level.
:returns: ``True`` to close the wizard.
:raises UserError: If no follow-up line is linked.
"""
self.ensure_one()
line = self.followup_line_id
if not line:
raise UserError(_("No follow-up record is linked to this wizard."))
line._compute_overdue_values()
if line.overdue_amount <= 0:
line.followup_status = 'no_action_needed'
raise UserError(_(
"Partner '%s' no longer has any overdue balance. "
"Follow-up cancelled.",
line.partner_id.display_name,
))
partner = line.partner_id
# ---- Email ----
if self.do_send_email:
attachment_ids = []
if self.do_join_invoices:
attachment_ids = line._get_invoice_attachments(partner)
mail_values = {
'subject': self.email_subject,
'body_html': self.email_body,
'email_from': (
line.company_id.email
or self.env.user.email_formatted
),
'email_to': partner.email,
'author_id': self.env.user.partner_id.id,
'res_id': partner.id,
'model': 'res.partner',
'attachment_ids': [(6, 0, attachment_ids)],
}
mail = self.env['mail.mail'].sudo().create(mail_values)
mail.send()
# ---- SMS ----
if self.do_send_sms and line.followup_level_id.sms_template_id:
try:
line.followup_level_id.sms_template_id._send_sms(partner.id)
except Exception:
pass
# ---- Advance Level ----
level = line.followup_level_id
if level:
next_level = level._get_next_level()
line.write({
'date': fields.Date.context_today(self),
'followup_level_id': next_level.id if next_level else level.id,
})
else:
line.write({'date': fields.Date.context_today(self)})
return {'type': 'ir.actions.act_window_close'}
def action_preview_email(self):
"""Recompute the email preview after manual edits to the level.
:returns: Action to reload the wizard form.
"""
self.ensure_one()
self._compute_email_preview()
return {
'type': 'ir.actions.act_window',
'res_model': self._name,
'res_id': self.id,
'view_mode': 'form',
'target': 'new',
}