diff --git a/fusion_plating/fusion_plating_portal/controllers/portal.py b/fusion_plating/fusion_plating_portal/controllers/portal.py index 6ca07f0a..89b9ff2e 100644 --- a/fusion_plating/fusion_plating_portal/controllers/portal.py +++ b/fusion_plating/fusion_plating_portal/controllers/portal.py @@ -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//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//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 # ==========================================================================