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:
@@ -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.',
|
||||
|
||||
@@ -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/<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).
|
||||
# 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/<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)
|
||||
# ==========================================================================
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user