fix(portal): dedicated /my/jobs/<id>/so_confirmation route with sudo render

The FP sale report template (report_fp_sale_portrait) walks into
fp.part.catalog records, which portal users don't have ACL on -
they'd hit 'You are not allowed to access Fusion Plating - Part
Catalog' when rendering. Standard /report/pdf/ route runs as the
authed user, so the template traversal fails.

Mirror the portal_download_coc pattern: gate on _document_check_access
for the portal job (customer can only ever reach their own data),
then render the report via ir.actions.report.sudo()._render_qweb_pdf
so the QWeb template traversal bypasses ACL. Return the PDF as an
attachment with a friendly filename.

Updates _fp_group_documents to point the From-You SO Confirmation
link at this new route instead of /report/pdf/ directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-17 03:31:25 -04:00
parent 8e172132e7
commit 669ba0fd8a

View File

@@ -197,17 +197,18 @@ class FpCustomerPortal(CustomerPortal):
if backend_job and 'sale_order_id' in backend_job._fields:
so = backend_job.sale_order_id
if so:
# IMPORTANT: use the FP custom sale report, NOT sale.report_saleorder.
# Per CLAUDE.md, sale_pdf_quote_builder gates on report_name being
# 'sale.report_saleorder' EXACTLY and produces broken output when
# the standard template is hit on FP-customised sale orders.
# report_fp_sale_portrait is the customer-facing template.
# IMPORTANT: route through /my/jobs/<id>/so_confirmation, NOT
# /report/pdf/ directly. The FP sale report template walks into
# fp.part.catalog which portal users don't have ACL on; our
# controller renders with sudo() to bypass that. Also avoids
# the sale_pdf_quote_builder gate that broke the standard
# sale.report_saleorder (CLAUDE.md MEMORY.md gotcha).
groups[0]['docs'].append({
'label': 'Sales Order Confirmation · %s' % so.name,
'sub': 'EN Plating · %s' % (
so.date_order and so.date_order.strftime('%b %d, %Y') or ''
),
'url': '/report/pdf/fusion_plating_reports.report_fp_sale_portrait/%s?download=true' % so.id,
'url': '/my/jobs/%s/so_confirmation' % job.id,
'icon_class': 'o_fp_doc_icon_input',
'icon': '📄',
})
@@ -800,6 +801,57 @@ class FpCustomerPortal(CustomerPortal):
attachment, 'raw'
).get_response(as_attachment=True)
# ==========================================================================
# JOBS -- download Sales Order Confirmation PDF
# ==========================================================================
# Renders the FP custom sale report with sudo so the QWeb template can
# walk into restricted models (fp.part.catalog etc.) that portal users
# don't have direct ACL on. We still gate on _document_check_access for
# the portal job, so the customer only ever sees their own data.
@http.route(
['/my/jobs/<int:job_id>/so_confirmation'],
type='http',
auth='user',
website=True,
)
def portal_download_so_confirmation(self, job_id, access_token=None, **kw):
try:
job_sudo = self._document_check_access(
'fusion.plating.portal.job',
job_id,
access_token,
)
except (AccessError, MissingError):
return request.redirect('/my')
# Resolve SO via the backend fp.job link.
backend_job = (
job_sudo.x_fc_job_id
if 'x_fc_job_id' in job_sudo._fields
else False
)
so = (
backend_job.sale_order_id
if backend_job and 'sale_order_id' in backend_job._fields
else False
)
if not so:
return request.redirect('/my/jobs/%s' % job_id)
pdf, _content_type = request.env['ir.actions.report'].sudo()._render_qweb_pdf(
'fusion_plating_reports.report_fp_sale_portrait',
res_ids=[so.id],
)
filename = 'Sales-Order-%s.pdf' % so.name.replace('/', '-')
return request.make_response(
pdf,
headers=[
('Content-Type', 'application/pdf'),
('Content-Length', str(len(pdf))),
('Content-Disposition', 'attachment; filename="%s"' % filename),
],
)
# ==========================================================================
# PURCHASE ORDERS -- list
# ==========================================================================