Initial commit

This commit is contained in:
gsinghpal
2026-02-22 01:22:18 -05:00
commit 5200d5baf0
2394 changed files with 386834 additions and 0 deletions

View File

@@ -0,0 +1,374 @@
# Fusion Accounting - Report Send Wizard
# Handles email dispatch and download of accounting reports to partners
from odoo import _, api, fields, models
from odoo.exceptions import UserError
from odoo.tools.misc import get_lang
class AccountReportSend(models.TransientModel):
"""Wizard providing a unified interface for sending accounting reports
via email and/or downloading them as PDF attachments."""
_name = 'account.report.send'
_description = "Fusion Accounting Report Dispatch Wizard"
partner_ids = fields.Many2many(
comodel_name='res.partner',
compute='_compute_partner_ids',
)
mode = fields.Selection(
selection=[
('single', "Single Recipient"),
('multi', "Multiple Recipients"),
],
compute='_compute_mode',
readonly=False,
store=True,
)
# -- Download options --
enable_download = fields.Boolean()
checkbox_download = fields.Boolean(string="Download")
# -- Email options --
enable_send_mail = fields.Boolean(default=True)
checkbox_send_mail = fields.Boolean(string="Email", default=True)
display_mail_composer = fields.Boolean(compute='_compute_mail_ui_state')
warnings = fields.Json(compute='_compute_warnings')
send_mail_readonly = fields.Boolean(compute='_compute_mail_ui_state')
mail_template_id = fields.Many2one(
comodel_name='mail.template',
string="Email template",
domain="[('model', '=', 'res.partner')]",
)
account_report_id = fields.Many2one(
comodel_name='account.report',
string="Report",
)
report_options = fields.Json()
mail_lang = fields.Char(
string="Lang",
compute='_compute_mail_lang',
)
mail_partner_ids = fields.Many2many(
comodel_name='res.partner',
string="Recipients",
compute='_compute_mail_partner_ids',
store=True,
readonly=False,
)
mail_subject = fields.Char(
string="Subject",
compute='_compute_mail_subject_body',
store=True,
readonly=False,
)
mail_body = fields.Html(
string="Contents",
sanitize_style=True,
compute='_compute_mail_subject_body',
store=True,
readonly=False,
)
mail_attachments_widget = fields.Json(
compute='_compute_mail_attachments_widget',
store=True,
readonly=False,
)
# -------------------------------------------------------------------------
# Default values
# -------------------------------------------------------------------------
@api.model
def default_get(self, fields_list):
# EXTENDS 'base'
defaults = super().default_get(fields_list)
ctx_options = self.env.context.get('default_report_options', {})
if 'account_report_id' in fields_list and 'account_report_id' not in defaults:
defaults['account_report_id'] = ctx_options.get('report_id', False)
defaults['report_options'] = ctx_options
return defaults
# -------------------------------------------------------------------------
# Helper methods
# -------------------------------------------------------------------------
@api.model
def _get_mail_field_value(self, partner, mail_template, mail_lang, field, **kwargs):
"""Render a single field from the mail template for a given partner."""
if not mail_template:
return
rendered = mail_template.with_context(lang=mail_lang)._render_field(
field, partner.ids, **kwargs
)
return rendered[partner._origin.id]
def _get_default_mail_attachments_widget(self, partner, mail_template):
"""Combine report placeholder attachments with template-defined attachments."""
placeholder_data = self._get_placeholder_mail_attachments_data(partner)
template_data = self._get_mail_template_attachments_data(mail_template)
return placeholder_data + template_data
def _get_wizard_values(self):
"""Serialize the current wizard state into a storable dictionary."""
self.ensure_one()
opts = self.report_options
if not opts.get('partner_ids', []):
opts['partner_ids'] = self.partner_ids.ids
return {
'mail_template_id': self.mail_template_id.id,
'checkbox_download': self.checkbox_download,
'checkbox_send_mail': self.checkbox_send_mail,
'report_options': opts,
}
def _get_placeholder_mail_attachments_data(self, partner):
"""Generate placeholder attachment metadata for the report PDF.
Returns a list with one dict per placeholder containing:
- id: unique placeholder identifier string
- name: display filename
- mimetype: MIME type of the file
- placeholder: True (prevents download/deletion in the widget)
"""
self.ensure_one()
pdf_name = (
f"{partner.name} - "
f"{self.account_report_id.get_default_report_filename(self.report_options, 'pdf')}"
)
return [{
'id': f'placeholder_{pdf_name}',
'name': pdf_name,
'mimetype': 'application/pdf',
'placeholder': True,
}]
@api.model
def _get_mail_template_attachments_data(self, mail_template):
"""Build attachment metadata from files linked to the mail template."""
return [
{
'id': att.id,
'name': att.name,
'mimetype': att.mimetype,
'placeholder': False,
'mail_template_id': mail_template.id,
}
for att in mail_template.attachment_ids
]
# -------------------------------------------------------------------------
# Computed fields
# -------------------------------------------------------------------------
@api.depends('partner_ids')
def _compute_mode(self):
for wiz in self:
wiz.mode = 'single' if len(wiz.partner_ids) == 1 else 'multi'
@api.depends('checkbox_send_mail')
def _compute_mail_ui_state(self):
"""Determine whether the full mail composer should be shown
and whether the send-mail checkbox should be read-only."""
for wiz in self:
wiz.display_mail_composer = wiz.mode == 'single'
recipients_missing_email = wiz.mail_partner_ids.filtered(lambda p: not p.email)
wiz.send_mail_readonly = recipients_missing_email == wiz.mail_partner_ids
@api.depends('mail_partner_ids', 'checkbox_send_mail', 'send_mail_readonly')
def _compute_warnings(self):
for wiz in self:
alert_map = {}
no_email_partners = wiz.mail_partner_ids.filtered(lambda p: not p.email)
if wiz.send_mail_readonly or (wiz.checkbox_send_mail and no_email_partners):
alert_map['account_missing_email'] = {
'message': _("Partner(s) should have an email address."),
'action_text': _("View Partner(s)"),
'action': no_email_partners._get_records_action(
name=_("Check Partner(s) Email(s)")
),
}
wiz.warnings = alert_map
@api.depends('partner_ids')
def _compute_mail_lang(self):
for wiz in self:
if wiz.mode == 'single':
wiz.mail_lang = wiz.partner_ids.lang
else:
wiz.mail_lang = get_lang(self.env).code
@api.depends('account_report_id', 'report_options')
def _compute_partner_ids(self):
for wiz in self:
wiz.partner_ids = wiz.account_report_id._get_report_send_recipients(
wiz.report_options
)
@api.depends('account_report_id', 'report_options')
def _compute_mail_partner_ids(self):
for wiz in self:
wiz.mail_partner_ids = wiz.partner_ids
@api.depends('mail_template_id', 'mail_lang', 'mode')
def _compute_mail_subject_body(self):
for wiz in self:
if wiz.mode == 'single' and wiz.mail_template_id:
wiz.mail_subject = self._get_mail_field_value(
wiz.mail_partner_ids, wiz.mail_template_id, wiz.mail_lang, 'subject'
)
wiz.mail_body = self._get_mail_field_value(
wiz.mail_partner_ids, wiz.mail_template_id, wiz.mail_lang,
'body_html', options={'post_process': True},
)
else:
wiz.mail_subject = wiz.mail_body = None
@api.depends('mail_template_id', 'mode')
def _compute_mail_attachments_widget(self):
for wiz in self:
if wiz.mode == 'single':
wiz.mail_attachments_widget = wiz._get_default_mail_attachments_widget(
wiz.mail_partner_ids, wiz.mail_template_id,
)
else:
wiz.mail_attachments_widget = []
# -------------------------------------------------------------------------
# Actions
# -------------------------------------------------------------------------
@api.model
def _action_download(self, attachments):
"""Return an action that triggers browser download of the given attachments,
or a zip archive when multiple files are present."""
return {
'type': 'ir.actions.act_url',
'url': f'/account_reports/download_attachments/{",".join(map(str, attachments.ids))}',
'close': True,
}
def _process_send_and_print(self, report, options, recipient_partner_ids=None, wizard=None):
"""Core processing logic: generates the report for each partner,
optionally emails it, and collects attachments for download.
:param report: account.report record to generate
:param options: dict of report generation options
:param recipient_partner_ids: explicit list of partner IDs to receive emails
:param wizard: the send wizard record (None when running via cron)
"""
stored_vals = report.send_and_print_values if not wizard else wizard._get_wizard_values()
should_email = stored_vals['checkbox_send_mail']
should_download = stored_vals['checkbox_download']
template = self.env['mail.template'].browse(stored_vals['mail_template_id'])
if wizard:
attachment_ids_from_widget = [
item['id']
for item in (wizard.mail_attachments_widget or [])
if not item['placeholder']
]
else:
attachment_ids_from_widget = template.attachment_ids.ids
options['unfold_all'] = True
target_partner_ids = options.get('partner_ids', [])
target_partners = self.env['res.partner'].browse(target_partner_ids)
if not recipient_partner_ids:
recipient_partner_ids = target_partners.filtered('email').ids
files_for_download = self.env['ir.attachment']
for partner_rec in target_partners:
options['partner_ids'] = partner_rec.ids
generated_attachment = partner_rec._get_partner_account_report_attachment(
report, options,
)
if should_email and recipient_partner_ids:
# Determine subject/body based on single vs multi mode
if wizard and wizard.mode == 'single':
email_subject = self.mail_subject
email_body = self.mail_body
else:
email_subject = self._get_mail_field_value(
partner_rec, template, partner_rec.lang, 'subject',
)
email_body = self._get_mail_field_value(
partner_rec, template, partner_rec.lang,
'body_html', options={'post_process': True},
)
partner_rec.message_post(
body=email_body,
subject=email_subject,
partner_ids=recipient_partner_ids,
attachment_ids=attachment_ids_from_widget + generated_attachment.ids,
)
if should_download:
files_for_download += generated_attachment
if files_for_download:
return self._action_download(files_for_download)
def action_send_and_print(self, force_synchronous=False):
"""Entry point: dispatch report generation, emailing, and/or downloading.
When sending to multiple recipients without download, processing is
deferred to a background cron job for better performance.
:param force_synchronous: bypass async processing when True
"""
self.ensure_one()
if self.mode == 'multi' and self.checkbox_send_mail and not self.mail_template_id:
raise UserError(
_('Please select a mail template to send multiple statements.')
)
# Download always requires synchronous processing
force_synchronous = force_synchronous or self.checkbox_download
defer_to_cron = self.mode == 'multi' and not force_synchronous
if defer_to_cron:
if self.account_report_id.send_and_print_values:
raise UserError(
_('There are currently reports waiting to be sent, please try again later.')
)
self.account_report_id.send_and_print_values = self._get_wizard_values()
self.env.ref('fusion_accounting.ir_cron_account_report_send')._trigger()
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'type': 'info',
'title': _('Sending statements'),
'message': _('Statements are being sent in the background.'),
'next': {'type': 'ir.actions.act_window_close'},
},
}
merged_options = {
**self.report_options,
'partner_ids': self.partner_ids.ids,
}
return self._process_send_and_print(
report=self.account_report_id,
options=merged_options,
recipient_partner_ids=self.mail_partner_ids.ids,
wizard=self,
)