# Fusion Accounting - Report Export Wizard # Provides multi-format export capabilities for accounting reports import base64 import json import types from urllib.parse import urlparse, parse_qs from odoo import _, api, fields, models from odoo.exceptions import UserError from odoo.models import check_method_name class ReportExportWizard(models.TransientModel): """Transient wizard that enables batch export of accounting reports into various file formats, stored as downloadable attachments.""" _name = 'account_reports.export.wizard' _description = "Fusion Accounting Report Export Wizard" export_format_ids = fields.Many2many( string="Target Formats", comodel_name='account_reports.export.wizard.format', relation="dms_acc_rep_export_wizard_format_rel", ) report_id = fields.Many2one( string="Source Report", comodel_name='account.report', required=True, ) doc_name = fields.Char( string="Output Filename", help="Base name applied to all generated output files.", ) @api.model_create_multi def create(self, vals_list): """Override creation to auto-populate available export formats from the report's configured action buttons.""" new_wizards = super().create(vals_list) for wiz in new_wizards: wiz.doc_name = wiz.report_id.name # Build format entries from the report's generation options generation_opts = self.env.context.get('account_report_generation_options', {}) available_buttons = generation_opts.get('buttons', []) for btn_config in available_buttons: export_type = btn_config.get('file_export_type') if export_type: self.env['account_reports.export.wizard.format'].create({ 'name': export_type, 'fun_to_call': btn_config['action'], 'fun_param': btn_config.get('action_param'), 'export_wizard_id': wiz.id, }) return new_wizards def export_report(self): """Execute the export process: generate files in each selected format and return a window action displaying the resulting attachments.""" self.ensure_one() saved_attachments = self.env['ir.attachment'] for attachment_data in self._build_attachment_values(): saved_attachments |= self.env['ir.attachment'].create(attachment_data) return { 'type': 'ir.actions.act_window', 'name': _('Generated Documents'), 'view_mode': 'kanban,form', 'res_model': 'ir.attachment', 'domain': [('id', 'in', saved_attachments.ids)], } def _build_attachment_values(self): """Iterate over selected formats, invoke the corresponding report generator, and collect attachment value dictionaries.""" self.ensure_one() attachment_vals_list = [] current_options = self.env.context['account_report_generation_options'] for fmt in self.export_format_ids: # Validate the callable name is a safe public method method_name = fmt.fun_to_call check_method_name(method_name) # Resolve whether the custom handler or base report owns the method target_report = self.report_id if target_report.custom_handler_model_id: handler_obj = self.env[target_report.custom_handler_model_name] if hasattr(handler_obj, method_name): callable_fn = getattr(handler_obj, method_name) else: callable_fn = getattr(target_report, method_name) else: callable_fn = getattr(target_report, method_name) extra_args = [fmt.fun_param] if fmt.fun_param else [] action_result = callable_fn(current_options, *extra_args) attachment_vals_list.append(fmt.apply_export(action_result)) return attachment_vals_list class ReportExportFormatOption(models.TransientModel): """Represents a single selectable export format within the export wizard, linking a display label to the callable that produces the file.""" _name = 'account_reports.export.wizard.format' _description = "Fusion Accounting Report Export Format" name = fields.Char(string="Format Label", required=True) fun_to_call = fields.Char(string="Generator Method", required=True) fun_param = fields.Char(string="Method Argument") export_wizard_id = fields.Many2one( string="Owning Wizard", comodel_name='account_reports.export.wizard', required=True, ondelete='cascade', ) def apply_export(self, action_payload): """Convert a report action response into attachment-ready values. Handles two action types: - ir_actions_account_report_download: direct file generation - ir.actions.act_url: fetch file from a wizard model via URL params """ self.ensure_one() if action_payload['type'] == 'ir_actions_account_report_download': opts_dict = json.loads(action_payload['data']['options']) # Resolve and invoke the file generator function generator_name = action_payload['data']['file_generator'] check_method_name(generator_name) source_report = self.export_wizard_id.report_id if source_report.custom_handler_model_id: handler_env = self.env[source_report.custom_handler_model_name] if hasattr(handler_env, generator_name): gen_callable = getattr(handler_env, generator_name) else: gen_callable = getattr(source_report, generator_name) else: gen_callable = getattr(source_report, generator_name) output = gen_callable(opts_dict) # Encode raw bytes content to base64; handle generator objects raw_content = output['file_content'] if isinstance(raw_content, bytes): encoded_data = base64.encodebytes(raw_content) elif isinstance(raw_content, types.GeneratorType): encoded_data = base64.encodebytes(b''.join(raw_content)) else: encoded_data = raw_content output_name = ( f"{self.export_wizard_id.doc_name or self.export_wizard_id.report_id.name}" f".{output['file_type']}" ) content_mime = self.export_wizard_id.report_id.get_export_mime_type( output['file_type'] ) elif action_payload['type'] == 'ir.actions.act_url': # Extract file data from a URL-based wizard action url_parts = urlparse(action_payload['url']) params = parse_qs(url_parts.query) target_model = params['model'][0] record_id = int(params['id'][0]) source_wizard = self.env[target_model].browse(record_id) output_name = source_wizard[params['filename_field'][0]] encoded_data = source_wizard[params['field'][0]] extension = output_name.split('.')[-1] content_mime = self.env['account.report'].get_export_mime_type(extension) opts_dict = {} else: raise UserError( _("The selected format cannot be exported as a document attachment.") ) return self._prepare_attachment_dict(output_name, encoded_data, content_mime, opts_dict) def _prepare_attachment_dict(self, filename, binary_data, mime_type, options_log): """Build the dictionary of values for creating an ir.attachment record.""" self.ensure_one() return { 'name': filename, 'company_id': self.env.company.id, 'datas': binary_data, 'mimetype': mime_type, 'description': json.dumps(options_log), }