feat(portal): customer PO/uploads + WO Detail PDF + hover-underline fix

1. From-You group now surfaces ANY ir.attachment attached to the
   linked sale.order (sudo'd) so customer-uploaded PO + drawings
   appear automatically. Each shows file name + upload date + size,
   downloads via /web/content/<id>?download=true. Falls through to
   the Sales Order Confirmation entry as before.

2. New 'Work Order' document group between Specifications and Quality,
   surfacing the EN Plating WO Detail PDF via new route
   /my/jobs/<id>/wo_detail. Sudo'd render of report_fp_job_wo_detail_
   template so the template can read backend fp.job + recipe nodes.
   Placeholder rendered when there's no linked backend job yet.

3. Hover underline gone: Bootstrap Reboot puts
   text-decoration: underline on a:hover for every anchor, which read
   as buggy on our flat chips / pill buttons / dashboard cards. Added
   a catch-all selector list in fp_portal_buttons.scss that pins
   text-decoration: none across hover/focus/active for every brand
   element. Hover signal lives in color + shadow only.

Version bump: 19.0.3.5.0 -> 19.0.3.6.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-17 12:06:41 -04:00
parent 27badff570
commit 6cad69cb86
3 changed files with 123 additions and 7 deletions

View File

@@ -5,7 +5,7 @@
{ {
'name': 'Fusion Plating — Customer Portal', 'name': 'Fusion Plating — Customer Portal',
'version': '19.0.3.5.0', 'version': '19.0.3.6.0',
'category': 'Manufacturing/Plating', 'category': 'Manufacturing/Plating',
'summary': 'Customer-facing portal for plating shops: online RFQ, job status, ' 'summary': 'Customer-facing portal for plating shops: online RFQ, job status, '
'CoC downloads, invoice access.', 'CoC downloads, invoice access.',

View File

@@ -236,12 +236,30 @@ class FpCustomerPortal(CustomerPortal):
if backend_job and 'sale_order_id' in backend_job._fields: if backend_job and 'sale_order_id' in backend_job._fields:
so = backend_job.sale_order_id so = backend_job.sale_order_id
if so: if so:
# IMPORTANT: route through /my/jobs/<id>/so_confirmation, NOT # 1) Customer uploads attached to the SO chatter (PO files,
# /report/pdf/ directly. The FP sale report template walks into # drawings, anything they sent over). Surface each as its
# fp.part.catalog which portal users don't have ACL on; our # own From-You doc so the customer can re-download what
# controller renders with sudo() to bypass that. Also avoids # they sent us. Filter by mimetype to avoid surfacing
# the sale_pdf_quote_builder gate that broke the standard # internal-only files like signatures or logo overrides.
# sale.report_saleorder (CLAUDE.md MEMORY.md gotcha). 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({ groups[0]['docs'].append({
'label': 'Sales Order Confirmation · %s' % so.name, 'label': 'Sales Order Confirmation · %s' % so.name,
'sub': 'EN Plating · %s' % ( 'sub': 'EN Plating · %s' % (
@@ -259,6 +277,28 @@ class FpCustomerPortal(CustomerPortal):
'icon': '📄', '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) # SPECIFICATIONS — V1: placeholder (V2 will pull customer spec)
groups[1]['docs'].append({ groups[1]['docs'].append({
'label': 'Customer Specification', '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/<int:job_id>/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) # JOBS -- repeat order (clone into a new draft quote_request)
# ========================================================================== # ==========================================================================

View File

@@ -86,3 +86,35 @@
// Size modifiers — match Bootstrap btn-sm / btn-lg sizing // Size modifiers — match Bootstrap btn-sm / btn-lg sizing
.o_fp_btn_sm { padding: .25rem .5rem; font-size: .875rem; } .o_fp_btn_sm { padding: .25rem .5rem; font-size: .875rem; }
.o_fp_btn_lg { padding: .5rem 1rem; font-size: 1.25rem; } .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;
}
}