The customer's Purchase Order is the doc they send US — a separate
artifact, often a PDF attachment on the quote. What lives in our
system is the Sales Order we create in response. Labeling the SO
list as "Purchase Orders" in the customer portal was a wrong-side
mapping.
Reverts and renames in this commit:
- Sidebar item label: "Purchase Orders" → "Sales Orders" (key stays
odoo_orders; URL still /my/orders). _FP_SIDEBAR_LAYOUT.
- Dashboard KPI tile: "Active POs" → "Active Sales Orders". Link
hint: "View POs →" → "View orders →". Link target updated to the
current /my/orders (the legacy /my/purchase_orders still redirects
but we point at the canonical URL now).
- Dashboard panel: "Recent Purchase Orders" → "Recent Sales Orders".
Empty state: "No purchase orders yet." → "No sales orders yet."
View-all link target updated to /my/orders.
- Dashboard docs entries strip: "Purchase Orders" docs entry title
→ "Sales Orders"; URL → /my/orders.
- Removed the three Odoo template rename inherits from
fp_sale_order_portal.xml (sale.portal_my_home_menu_sale,
sale.portal_my_orders, sale.sale_order_portal_content). With those
gone the stock templates emit Odoo's native "Sales Order(s)" and
"Your Orders" wording on the list page header, breadcrumb, and
detail page <h2> — which is now the correct terminology.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1. Odoo's portal_order_page route calls _get_page_view_values which
doesn't touch _prepare_portal_layout_values, so our sidebar
context (fp_sidebar_items, fp_partner_display_name) was missing
on every Odoo detail page (SO, invoice, delivery, quote). Override
_get_page_view_values to setdefault our two keys into the values
dict — non-clobbering, covers every detail route.
2. Rename "Sales Order(s)" / "Your Orders" to "Purchase Order(s)" on
the customer portal so the wording matches the sidebar item and
the customer's perspective (they purchase from us). Inherits in
fp_sale_order_portal.xml replace the relevant text nodes in
sale.portal_my_home_menu_sale / sale.portal_my_orders /
sale.sale_order_portal_content.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the tab nav / portal.portal_searchbar on the 4 FP list
pages with the new fp_portal_list_controls macro (filter pills +
search input + sort dropdown) and drops portal_pager in favour of
client-side filtering of up to 500 records:
- Quote Requests (/my/quote_requests):
filters: All / Active / Converted / Declined
sorts: Newest / Reference / Status
extra search fields: contact_name, contact_email, line.part_number,
line.description, line.product_id.default_code
- Work Orders (/my/jobs, cards layout):
filters: All / Active / Ready to Ship / Complete
sorts: Newest / Reference / Status
extra search fields per card: part_catalog.part_number, part_catalog.name,
sale_order.name, sale_order.client_order_ref,
job.notes
- Certifications (/my/certifications):
no filters (all rows are terminal CoC jobs)
sorts: Newest / Reference
extra search fields: part name, processes (already in card text)
- Packing Slips / Deliveries (/my/deliveries):
no filters (all rows are state=done)
sorts: Newest / Reference
adds a visible Origin column (sale order ref) so customers can
locate a slip by the SO it came from
Each route accepts ?filter_state=... and ?sortby=... query params,
returns up to 500 records, and passes result_total + clipped to the
template so the macro can render a "showing latest 500 of N" notice
when the cap is hit.
Hidden <td class="d-none"> cells inside each row carry extra terms
that aren't displayed but are matched by the JS textContent scan.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
t-field requires a record.field_name access pattern. open_balance is a
Python float (returned by _fp_account_summary_open_balance), not a
recordset attribute, so QWeb threw AssertionError at render time and
the page 500'd. Format the value in the controller via tools.formatLang
and render it as a plain string with t-out instead.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Every FP portal route built `values = {...}` from scratch and called
`request.render(...)` directly, bypassing `_prepare_portal_layout_values`.
Our new `fp_sidebar_items` and `fp_partner_display_name` keys live in
that hook, so the sidebar template's `t-foreach` was a no-op on every
custom page (`/my/home`, `/my/jobs`, `/my/account_summary`, etc.) — the
sidebar rendered with the "My Account" fallback header and only the
Sign Out footer link visible.
Fix: each FP render now does
values = self._prepare_portal_layout_values()
values.update({...route-specific values...})
This puts the layout values in first (so `fp_sidebar_items` and
`fp_partner_display_name` always present), and the route's own
update wins on `page_name` and other collisions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New /my/account_summary route. Splits posted account.move into
Invoices (out_invoice) / Credit Memos (out_refund) / Statements
(V1 placeholder). Open Balance helper sums amount_residual across
open invoices for the partner's commercial tree.
Search filters name OR ref (customer PO). Sort options: date desc/asc,
amount desc/asc. Filter pills: open / closed / all.
Tests cover the tab partitioning, the open-balance sum, and the
search behaviour. Helpers use commercial_partner.env so they work
in both HTTP context and unit tests without requiring request.env.
Test scaffolding uses fp_from_so_invoice=True context flag and
invoice_payment_term_id to satisfy the fusion_plating_jobs and
fusion_plating_invoicing create/post gates.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- /my/fp_invoices -> /my/account_summary
- /my/purchase_orders -> /my/orders (Odoo default)
- /my/quote_requests/new (GET) -> /my/configurator/new
(POST handler preserved for back-compat with the existing RFQ form
button; will be removed after the form is fully retired)
Thin templates deleted: portal_my_fp_invoices, portal_my_purchase_orders.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drives the sidebar from a single Python data structure
(_FP_SIDEBAR_LAYOUT). Active state resolved by page_name lookup OR
URL-prefix match (so Odoo default pages like /my/orders and
/my/account light up correctly). _prepare_portal_layout_values
extends super() so existing counter injection (fp_quote_request_count
etc.) keeps firing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Portal users have read access to fp.portal.job but NOT to fp.job.
The new job-card macro traverses job.x_fc_job_id -> fp.job to surface
part info, sale_order, ship-to address — that raised AccessError for
real customers (admins were fine due to inherited groups).
Adding .sudo() to the three Job queries in home(), portal_my_jobs(),
and the certifications panel mirror lookup. Domain still filters to
the customer's commercial partner tree, so sudo doesn't widen
visibility — it just lets the template walk past the portal-job
boundary to the privileged backend models.
Same pattern is already used in the same file for sale.order,
account.move, and stock.picking queries.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Regression I introduced when adding the WO Detail group: the
groups.insert(2, wo_group) ran BEFORE the SPECIFICATIONS / QUALITY /
SHIPPING appends, so groups[2] shifted from 'quality' to 'work_order'
mid-helper. Result: the CoC got appended to the work_order group's
docs and shipping doc went into quality. Test caught it.
Restructured to declare the 5-group list up front in display order
and use stable indices throughout (0=from_you, 1=specs, 2=work_order,
3=quality, 4=shipping). Added a code comment warning future editors
that reordering means updating every groups[N] reference.
Test updated to expect 5 groups, asserting both 'work_order' and
'quality' keys are present + pending state in each.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
1. Pulse animation on the active step indicator:
- New @keyframes fp-pulse-teal / fp-pulse-amber in stepper.scss
- Applied to .o_fp_step_active / _warn and .o_fp_timeline_active
.o_fp_timeline_dot so dashboard stepper + detail-page timeline
breathe in sync. 1.8s ease-in-out, ring grows 4px -> 9px and
fades 20% -> 6% opacity. Two color variants so QC (warn) keeps
its amber meaning.
- prefers-reduced-motion: reduce kills the animation for users
who opted out.
2. Repeat Order button on /my/jobs/<id> detail page:
- New POST /my/jobs/<id>/repeat route that creates a draft
fusion.plating.quote.request seeded with the user's contact +
the job's quantity, posts a chatter link back to the original
job, redirects to the new RFQ for review/submit.
- Button placed in the detail footer next to 'Back to all jobs',
CSRF-protected via the form's csrf_token hidden field.
3. Dashboard expanded from 3 secondary panels to 5 (Recent Quote
Requests + Recent Purchase Orders added) so every previously-
designed customer page is reachable from /my/home.
- Auto-fit grid: 3+2 / 2+2+1 / single column depending on width.
- Every panel header gets a 'View all ->' link to its list page
(Quote Requests / POs / Certs / Deliveries / Invoices).
- Empty-state for Quote Requests gets an inline 'Get a quote ->'
CTA so first-time customers know where to start.
Version bump: 19.0.3.4.0 -> 19.0.3.5.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two changes to _fp_get_stage_timeline:
1. Format: 'May 16, 2026 \xb7 9:14 AM' (full year + space + uppercase
AM/PM) instead of 'may 16 \xb7 9:14a'. Matches the mockup the
user approved. Date-only render kicks in when the timestamp has
no time component (backfilled/interpolated midnight values), so
we don't show fake '12:00 AM' next to a date we only know to the
day.
2. Linear interpolation: records that pre-date Task 16's per-stage
Datetime hook had empty middle-stage timestamps. The new fallback
spreads done stages evenly between received_at (or received_date)
and now() so old records show a plausible progression instead of
gap-toothed empty rows. Records created post-hook hit the real
captured values and never reach the interpolation branch.
Helper imports datetime + time at module level since we need
datetime.combine for Date->Datetime conversion in the fallback chain.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Records created before Task 16 (per-stage Datetime fields + write
snapshot hook) have NULL for received_at/shipped_at/etc. SQL backfill
copies received_date -> received_at; this commit adds a runtime
fallback so if any record slips through (manual edits, future
imports) the timeline still surfaces what's available.
Also render date-only ('May 16, 2026') when the timestamp has no
time component, so backfilled-from-Date records don't show the
misleading 'may 16 · 12:00a' fake time.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Standard sale.report_saleorder hit the sale_pdf_quote_builder
header/footer merge bug (CLAUDE.md MEMORY.md gotcha) and produced
garbled PDFs on FP-customised sale orders. Switching to
fusion_plating_reports.report_fp_sale_portrait which is the
customer-facing FP template and bypasses the merge gate. Added
?download=true so the browser saves the PDF instead of trying to
embed it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1. /my now serves the FP dashboard (stock Odoo home was leaking
through because parent route declared ['/my', '/my/home'] but my
override only listed /my/home).
2. Button padding bumped to .5rem 1rem + font 1rem so o_fp_btn matches
Odoo's standard Bootstrap button rhythm. Ghost button drops its
custom padding override.
3. .o_fp_job_card on /my/home + /my/jobs is now an <a> wrapping the
whole card area — full row is the click target, not just the WO
number. Inner <a> on job.name dropped to avoid nested anchors;
focus-visible outline added for keyboard nav.
4. fp.job.write() now mirrors state -> fp.portal.job.state via new
_FP_JOB_STATE_TO_PORTAL_STATE map (confirmed->received,
in_progress->in_progress, done->ready_to_ship). Fixes the bug where
completed backend jobs left the portal stuck on 'in_progress'.
'on_hold' and 'cancelled' intentionally not mirrored — manager
choice what to surface.
5. Sales Order Confirmation now surfaces in the 'From You' group on
the job detail page, pulled via job.x_fc_job_id.sale_order_id ->
/report/pdf/sale.report_saleorder/<id>. Falls back to the upload
placeholder when no SO is linked.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two-column grid: vertical timeline (5 stages with per-stage timestamps)
on the left, grouped document panel (4 categories) on the right. Hero
header carries WO ref + part / qty / ETA / tracking facts.
Controller adds stage_timeline, doc_groups, and timeline_spine_pct
to the render context. Spine fill = done + half-credit for the
active stage (so the spine visually leads the eye to where the work
is happening).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
V1 surfaces only the fields directly on fp.portal.job (CoC + packing
list). Other 2 groups (From You, Specifications) render placeholder
rows. V2 will wire in sale.order linking for full doc surfacing.
Also adds _fp_size_label helper for friendly file-size strings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Builds a 5-entry list (label, status, started_at, time_label, notes)
ordered by stage. Labels match the dashboard stepper exactly
(Received/Inspected/Plating/QC/Shipped) so the two surfaces tell
the same story. Inspected and Plating share in_progress_started_at
since state in_progress means both transitions happened.
Time labels use lowercase am/pm matching the mockup typography.
'complete' state correctly shows all 5 stages as done (caught by
new test).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds active_job_count, awaiting_review_count, ready_to_ship_count
to the dashboard context. Tests verify partition is correct across
the fp.portal.job and fp.quote.request state machines.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The custom dashboard at fusion_plating_portal was rendering a 6-card
view at /my/home, but a method-name mismatch left the parent
portal.CustomerPortal.home() route active instead. Rename the
override to home() so Python MRO does the override naturally, and
add CLAUDE.md Critical Rule 16 documenting the gotcha so future
controller-override work doesn't trip on it.
Version bump: 19.0.2.2.0 -> 19.0.2.3.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>