changes
This commit is contained in:
@@ -1 +1,2 @@
|
||||
from . import controllers
|
||||
from . import models
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
{
|
||||
'name': 'Fusion Reports — Templates',
|
||||
'version': '19.0.1.0.0',
|
||||
'version': '19.0.1.2.0',
|
||||
'category': 'Tools/Reports',
|
||||
'summary': 'Branded PDF templates for Quotation, Sales Order, Invoice, Delivery, Purchase Order, and Payment Receipt.',
|
||||
'description': """
|
||||
|
||||
1
fusion_reports_templates/controllers/__init__.py
Normal file
1
fusion_reports_templates/controllers/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import portal
|
||||
39
fusion_reports_templates/controllers/portal.py
Normal file
39
fusion_reports_templates/controllers/portal.py
Normal file
@@ -0,0 +1,39 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
"""Swap stock report refs for their Fusion-branded equivalents in customer/vendor
|
||||
portal routes.
|
||||
|
||||
The portal "View Details" link and the portal Download button in
|
||||
`sale/account/purchase/controllers/portal.py` hard-code the stock report
|
||||
(`sale.action_report_saleorder`, `account.account_invoices`, ...). All those
|
||||
routes funnel through `CustomerPortal._show_report` in
|
||||
`portal/controllers/portal.py`, so a single override on that method is enough
|
||||
to redirect every customer-facing render to the Fusion templates that
|
||||
`mail_template_override.xml` already uses for outbound email.
|
||||
"""
|
||||
from odoo.addons.portal.controllers import portal
|
||||
|
||||
|
||||
_PORTAL_REPORT_OVERRIDES = {
|
||||
# Sale: quotation / order
|
||||
'sale.action_report_saleorder': 'fusion_reports_templates.action_report_fr_sale_portrait',
|
||||
# Account: customer invoice + credit note (with-payment and without-payment variants)
|
||||
'account.account_invoices': 'fusion_reports_templates.action_report_fr_invoice_portrait',
|
||||
'account.account_invoices_without_payment': 'fusion_reports_templates.action_report_fr_invoice_portrait',
|
||||
# Purchase: PO + RFQ (vendor portal)
|
||||
'purchase.action_report_purchase_order': 'fusion_reports_templates.action_report_fr_purchase_portrait',
|
||||
'purchase.report_purchase_quotation': 'fusion_reports_templates.action_report_fr_purchase_portrait',
|
||||
}
|
||||
|
||||
|
||||
class CustomerPortal(portal.CustomerPortal):
|
||||
|
||||
def _show_report(self, model, report_type, report_ref, download=False):
|
||||
report_ref = _PORTAL_REPORT_OVERRIDES.get(report_ref, report_ref)
|
||||
return super()._show_report(
|
||||
model=model,
|
||||
report_type=report_type,
|
||||
report_ref=report_ref,
|
||||
download=download,
|
||||
)
|
||||
@@ -1,15 +1,28 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
"""Sort the Print-menu report bindings by `sequence`.
|
||||
"""Two orthogonal jobs:
|
||||
|
||||
Odoo 19 returns `ir.actions.report` bindings in raw insertion order, which
|
||||
pushes third-party reports to the bottom even when they are the primary
|
||||
customer-facing document. Adding a sequence field and sorting inside
|
||||
`ir.actions.actions._get_bindings` restores the expected ordering.
|
||||
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
|
||||
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):
|
||||
@@ -21,6 +34,72 @@ class IrActionsReport(models.Model):
|
||||
'(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'
|
||||
|
||||
Reference in New Issue
Block a user