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

204 lines
7.9 KiB
Python

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