# 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, )