diff --git a/fusion_plating/fusion_plating_portal/__manifest__.py b/fusion_plating/fusion_plating_portal/__manifest__.py index 7387002d..01e0288d 100644 --- a/fusion_plating/fusion_plating_portal/__manifest__.py +++ b/fusion_plating/fusion_plating_portal/__manifest__.py @@ -5,7 +5,7 @@ { 'name': 'Fusion Plating โ€” Customer Portal', - 'version': '19.0.3.5.0', + 'version': '19.0.3.6.0', 'category': 'Manufacturing/Plating', 'summary': 'Customer-facing portal for plating shops: online RFQ, job status, ' 'CoC downloads, invoice access.', diff --git a/fusion_plating/fusion_plating_portal/controllers/portal.py b/fusion_plating/fusion_plating_portal/controllers/portal.py index dea28e54..bb9ba9b5 100644 --- a/fusion_plating/fusion_plating_portal/controllers/portal.py +++ b/fusion_plating/fusion_plating_portal/controllers/portal.py @@ -236,12 +236,30 @@ class FpCustomerPortal(CustomerPortal): if backend_job and 'sale_order_id' in backend_job._fields: so = backend_job.sale_order_id if so: - # 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). + # 1) Customer uploads attached to the SO chatter (PO files, + # drawings, anything they sent over). Surface each as its + # own From-You doc so the customer can re-download what + # they sent us. Filter by mimetype to avoid surfacing + # internal-only files like signatures or logo overrides. + so_atts = self.env['ir.attachment'].sudo().search([ + ('res_model', '=', 'sale.order'), + ('res_id', '=', so.id), + ]) + for att in so_atts: + groups[0]['docs'].append({ + 'label': att.name, + 'sub': 'Uploaded ยท %s ยท %s' % ( + att.create_date and att.create_date.strftime('%b %d, %Y') or '', + self._fp_size_label(att), + ), + 'url': '/web/content/%s?download=true' % att.id, + 'icon_class': 'o_fp_doc_icon_input', + 'icon': '๐Ÿ“„', + }) + + # 2) Sales Order Confirmation - rendered FP report (not the + # standard sale.report_saleorder; that hits the + # sale_pdf_quote_builder gate per CLAUDE.md MEMORY.md). groups[0]['docs'].append({ 'label': 'Sales Order Confirmation ยท %s' % so.name, 'sub': 'EN Plating ยท %s' % ( @@ -259,6 +277,28 @@ class FpCustomerPortal(CustomerPortal): 'icon': '๐Ÿ“„', }) + # WORK ORDER DETAIL โ€” separate group between specs and quality. + # Pulls the EN Plating WO Detail PDF (fusion_plating_jobs report). + # Requires a linked backend fp.job; placeholder otherwise. + wo_group = {'key': 'work_order', 'label': 'Work Order', 'docs': []} + if backend_job: + wo_group['docs'].append({ + 'label': 'Work Order Detail ยท %s' % job.name, + 'sub': 'EN Plating ยท process scope + recipe summary', + 'url': '/my/jobs/%s/wo_detail' % job.id, + 'icon_class': 'o_fp_doc_icon_spec', + 'icon': '๐Ÿ› ', + }) + else: + wo_group['docs'].append({ + 'label': 'Work Order Detail', + 'sub': 'Will appear once production is confirmed', + 'pending': True, + 'icon': '๐Ÿ› ', + }) + # Insert WO Detail group between Specifications (idx 1) and Quality (idx 2). + groups.insert(2, wo_group) + # SPECIFICATIONS โ€” V1: placeholder (V2 will pull customer spec) groups[1]['docs'].append({ 'label': 'Customer Specification', @@ -891,6 +931,50 @@ class FpCustomerPortal(CustomerPortal): ], ) + # ========================================================================== + # JOBS -- download Work Order Detail PDF + # ========================================================================== + # Renders the fusion_plating_jobs WO Detail report with sudo so the + # template can walk fp.job + recipe nodes the portal user can't read + # directly. Customer-facing summary of process scope and recipe steps. + @http.route( + ['/my/jobs//wo_detail'], + type='http', + auth='user', + website=True, + ) + def portal_download_wo_detail(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') + + backend_job = ( + job_sudo.x_fc_job_id + if 'x_fc_job_id' in job_sudo._fields + else False + ) + if not backend_job: + return request.redirect('/my/jobs/%s' % job_id) + + pdf, _content_type = request.env['ir.actions.report'].sudo()._render_qweb_pdf( + 'fusion_plating_jobs.report_fp_job_wo_detail_template', + res_ids=[backend_job.id], + ) + filename = 'WO-Detail-%s.pdf' % job_sudo.name.replace('/', '-') + return request.make_response( + pdf, + headers=[ + ('Content-Type', 'application/pdf'), + ('Content-Length', str(len(pdf))), + ('Content-Disposition', 'attachment; filename="%s"' % filename), + ], + ) + # ========================================================================== # JOBS -- repeat order (clone into a new draft quote_request) # ========================================================================== diff --git a/fusion_plating/fusion_plating_portal/static/src/scss/fp_portal_buttons.scss b/fusion_plating/fusion_plating_portal/static/src/scss/fp_portal_buttons.scss index 19761ba4..e31a978f 100644 --- a/fusion_plating/fusion_plating_portal/static/src/scss/fp_portal_buttons.scss +++ b/fusion_plating/fusion_plating_portal/static/src/scss/fp_portal_buttons.scss @@ -86,3 +86,35 @@ // Size modifiers โ€” match Bootstrap btn-sm / btn-lg sizing .o_fp_btn_sm { padding: .25rem .5rem; font-size: .875rem; } .o_fp_btn_lg { padding: .5rem 1rem; font-size: 1.25rem; } + +// ============================================================================ +// Globally suppress browser-default underline-on-hover for portal +// surfaces. Bootstrap's Reboot puts `a:hover { text-decoration: underline }` +// on every anchor; for our flat-aesthetic chips / cards / pill buttons that +// reads as a buggy visual artifact. Hover signal lives in color + shadow +// instead. Specificity is high enough to win without !important. +// ============================================================================ +.o_fp_btn, +.o_fp_btn_primary, +.o_fp_btn_secondary, +.o_fp_btn_ghost, +.o_fp_btn_danger, +.o_fp_btn_mint, +.o_fp_dashboard a, +.o_fp_job_detail a, +.o_fp_job_card, +.o_fp_doc_chip, +.o_fp_doc_row, +.o_fp_panel_view_all, +.o_fp_panel_inline_cta, +.o_fp_kpi_hint, +.o_fp_view_all a, +.o_fp_status_tab, +.o_fp_related_links a { + text-decoration: none; + &:hover, + &:focus, + &:active { + text-decoration: none; + } +}