# -*- coding: utf-8 -*- # Copyright 2026 Nexa Systems Inc. # License OPL-1 (Odoo Proprietary License v1.0) """Two orthogonal jobs: 1. Sequence the Print-menu report bindings so Fusion reports float to the top — Odoo 19 returns `ir.actions.report` bindings in raw insertion order, which buries third-party reports. 2. Bridge `sale_pdf_quote_builder` to our Fusion sale-order reports. Upstream gates its header/footer PDF merge on `report_name == 'sale.report_saleorder'`, so the merge silently no-ops when the user prints with our Portrait/Landscape variants. We replay the same merge here for those report names. """ import io from odoo import api, fields, models from odoo.tools import frozendict, pdf, str2bool _FUSION_SALE_REPORTS = frozenset({ 'fusion_reports_templates.report_fr_sale_portrait', 'fusion_reports_templates.report_fr_sale_landscape', }) class IrActionsReport(models.Model): _inherit = 'ir.actions.report' sequence = fields.Integer( default=100, help='Order in which this report appears in the Print menu ' '(lower = higher in the list).', ) def _render_qweb_pdf_prepare_streams(self, report_ref, data, res_ids=None): result = super()._render_qweb_pdf_prepare_streams(report_ref, data, res_ids=res_ids) if not res_ids: return result report = self._get_report(report_ref) if report.report_name not in _FUSION_SALE_REPORTS: return result # quotation_document_ids is added by sale_pdf_quote_builder; bail if # that module isn't installed (nothing to merge). if 'quotation_document_ids' not in self.env['sale.order']._fields: return result try: from odoo.tools.pdf import PdfFileWriter except ImportError: return result always_include = str2bool( self.env['ir.config_parameter'].sudo().get_param( 'sale.always_include_selected_documents' ) ) orders = self.env['sale.order'].browse(res_ids) for order in orders: initial_stream = result.get(order.id, {}).get('stream') if not initial_stream: continue if order.state == 'sale' and not always_include: continue quotation_documents = order.quotation_document_ids headers = quotation_documents.filtered(lambda d: d.document_type == 'header') footers = quotation_documents - headers has_product_document = any( line.product_document_ids for line in order.order_line ) if not headers and not has_product_document and not footers: continue form_fields_values_mapping = {} writer = PdfFileWriter() self_with_order_context = self.with_context( use_babel=True, lang=order._get_lang() or self.env.user.lang ) for header in headers: prefix = f'quotation_document_id_{header.id}__' self_with_order_context._update_mapping_and_add_pages_to_writer( writer, header, form_fields_values_mapping, prefix, order ) if has_product_document: for line in order.order_line: for doc in line.product_document_ids: prefix = f'sol_id_{line.id}_product_document_id_{doc.id}__' self_with_order_context._update_mapping_and_add_pages_to_writer( writer, doc, form_fields_values_mapping, prefix, order, line ) self._add_pages_to_writer(writer, initial_stream.getvalue()) for footer in footers: prefix = f'quotation_document_id_{footer.id}__' self_with_order_context._update_mapping_and_add_pages_to_writer( writer, footer, form_fields_values_mapping, prefix, order ) pdf.fill_form_fields_pdf(writer, form_fields=form_fields_values_mapping) buffer = io.BytesIO() writer.write(buffer) result[order.id]['stream'] = io.BytesIO(buffer.getvalue()) return result class IrActionsActions(models.Model): _inherit = 'ir.actions.actions' @api.model def _get_bindings(self, model_name): result = super()._get_bindings(model_name) if not result.get('report'): return result sorted_reports = tuple(sorted( result['report'], key=lambda vals: ( vals.get('sequence', 100), (vals.get('name') or '').lower(), ), )) new_result = dict(result) new_result['report'] = sorted_reports return frozendict(new_result)