The packing slip report only existed for stock.picking (Delivery Orders),
but this shop ships via fusion.plating.delivery and has no pickings — so
packing slips never rendered for their flow, and the prior auto-generate +
email-notification paths pointed the stock.picking report at a delivery
(wrong model -> blank PDF).
Add a delivery-native variant: report_fp_packing_slip_delivery_portrait +
action_report_fp_packing_slip_delivery_portrait (bound to
fusion.plating.delivery -> shows in the delivery Print menu), resolving the
SO + lines from the delivery job_ref (same pattern as the BoL report) and
reusing the shared styles / address / signoff bits + a sale.order.line
items table. Repoint _fp_generate_packing_slip (dispatch auto-gen) and the
notification attachment to the new report.
Verified on entech: real content (customer, PO, items, PS#) for DLV-30102 —
142KB PDF vs prior blank 12.8KB.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fusion.plating.delivery already had packing_list_attachment_id + a viewer
action, but nothing populated it — shipping a local delivery produced no
packing slip. Add _fp_generate_packing_slip(): renders
fusion_plating_reports.action_report_fp_packing_slip_portrait and stores
it on packing_list_attachment_id. Hooked into action_start_route (the
dispatch / loaded-on-vehicle moment, so it travels with the goods) and as
a generate-if-missing catch-all on action_mark_delivered. Idempotent
(skips deliveries that already have one unless force=True) and best-effort
(a report glitch logs + continues, never blocks shipping). Report action
resolved at runtime so logistics keeps no hard dep on
fusion_plating_reports. Deployed + verified on entech (12.8KB PDF for
DLV-30097, rolled back).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
res.partner has no `mobile` field in this Odoo 19 build, so
_fp_resolve_delivery_defaults crashed with AttributeError when a job's
last shop step finished and auto-created a delivery
(button_finish -> _fp_check_advance_post_shop -> _fp_create_delivery).
This blocked operators from finishing the step at all.
Guard the read with the codebase's 'x' in obj._fields pattern so it
falls back to phone, and still picks up mobile on instances that define
it. Deployed + verified on entech (restart, no -u; pure Python change).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Internal Job Sticker is no longer a separate Layout A: it's now a COPY of the
External sticker (Layout B, one per box, logo + WO + BOX + QR + rail fields +
prominent PLATING THICKNESS banner) but feeds the INTERNAL description and
labels its notes "INTERNAL NOTES" so the shop copy can't be confused with the
customer copy. The old Layout-A body template is deleted.
- Removed the Internal sticker from the fp.job Print menu (binding_model_id ->
False); it now prints from the Receiving screen instead.
- Added "External Sticker" + "Internal Sticker" print buttons to the fp.receiving
form header (shown once a WO exists). Each renders one label per tracked box
for the receiving's work order (passes a single WO so the SO-scoped box loop
doesn't reprint each label per job).
Verified on entech (WO-30094 / RCV-30096): internal renders Layout B with the
internal description + INTERNAL NOTES; external unchanged; both receiving buttons
return the right report actions.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Caveat on the existing "custom-header reports need .article for UTF-8" rule:
the dpi=96 mm-based job stickers are the exception — adding .article re-routes
through Odoo's standard report CSS and blows up the mm/dpi layout. For those,
strip the non-ASCII glyph to ASCII in _clean() instead. (Learned fixing the
'375ºF' bake-text mojibake on the external sticker.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Relocate plating thickness out of the cramped rail (where it shared a row
with Due and wrapped) to a big 21pt "PLATING THICKNESS" banner at the top of
the main panel — the team's most-watched spec, now hard to miss. Gated on a
new has_thk flag (real value with a digit; skips empty/'N/A'). Due takes the
full rail row.
- Fix the bake-text degree mojibake: operators type 'º' (U+00BA) for "375ºF";
through this sticker's lightweight html_container path (no .article UTF-8
wrapper) it renders "375°F". Adding a .article wrapper fixes encoding but
blows up the dpi=96 mm layout (tested), so _clean() now strips º/°/˚ to clean
ASCII -> "375F".
Verified on entech (WO-30094).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The left rail (overflow:hidden, fixed height) was ~8mm over its budget, so the
last grid row (Due | Thk) fell off the bottom and rendered as an empty band.
Reclaimed the height: r-logo 11→9mm, r-wo 14→13mm, r-qrflags 36→32mm (+ qfwrap-full
33→31mm / qffull line-height 36→32mm to match), r-fld padding 1→0.7mm. Due/Thk
now render fully. Verified on entech (WO-30094, PO 980933709).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Drop the <br/> between label and value (.lbl is display:block, so the
value already sits beneath it; the <br/> added a blank-line gap).
- Rebalance widths: PO# 34→25%, Qty 16→13%, Due 30→26%, Thk 20→36% + nowrap,
so a thickness RANGE (e.g. 0.0025" - 0.0030") stays on ONE line instead of
wrapping. Verified on entech (WO-30096).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Internal Job Sticker (Layout A) redesign per operator feedback:
- Factory (ENTECH) logo added to the black header band on a white chip
(mirrors the external sticker; visible on the dark band + thermal print).
- Customer moved out of row 3 up next to Part# in row 2. Part# keeps the
dominant width (stripped customer name is short + consistent), MASK/BAKE
flags still float at the Part# edge.
- Row 3 now PO# / Qty / Due / Thk (4 fields, customer removed) with bigger
values (13/14/12/11pt) spread across the full width.
- Internal header QR trimmed 30→27mm so the QR-driven band is shorter; the
freed height flows to the NOTES block.
Rendered + verified on entech (WO-30072 / AMP-CANA).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The recipe-cert-toggles feature (fb6cccc8) taught
fp.job._resolve_required_cert_types to suppress thickness for recipes
with requires_thickness_report=False (passivation, chemical conversion,
anodize seal-only). But the actual thickness-data ENFORCEMENT never got
the memo: both fp.certificate.action_issue's hard gate AND the
Issue-Certs wizard's readiness hint re-derived 'needs thickness' from
partner flags only and ignored the recipe. Result: a passivation CoC for
a thickness/strict customer could never be issued — the gate demanded
Fischerscope data the process physically cannot produce.
Consolidate the partner-flag + recipe-suppression logic into one
fp.certificate._fp_needs_thickness_data() helper and route both the gate
and the wizard through it, so the cert-type resolver and the issue-time
gate can never drift again. Add regression tests: passivation recipe
suppresses the issue gate even for strict-thickness customers; a normal
recipe still enforces (control, guards aerospace).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Technician Service Booking & Auto-Quote: OWL 'Book a Service' wizard,
editable fusion.service.rate rate-card table, auto draft repair Sale Order
(call-out + per-km), and the fusion_tasks datetime-inverse tz fix. Clone-verified
GREEN and deployed to westin-v19 (fusion_claims 19.0.9.4.0) on 2026-06-04.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Hardened after the first real clone-verify on odoo-westin:
- Cleanup now generates an orphan-delete for EVERY single-column FK from PROD's
pg_constraint and applies it to the clone (was tax-tables-only). westin-v19 also
has deleted-company (payslip_tags_table, account_account_res_company_rel) and
deleted-journal (account_payment_method_line) orphans that broke the clone -u.
- run_odoo passes --http-port=0 --gevent-port=0 so --test-enable (which forces
http_spawn even with --no-http in Odoo 19) doesn't die on 'Address already in use'.
- TEST_TAGS scoped to this feature's classes (the broad tag also runs pre-existing
dashboard/wizard tests that fail in this prod-config runner, unrelated to this work).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Real install verified on the Westin clone; these were test-only bugs:
- Task-create tests hardcoded scheduled_date 2026-06-03, now in the past, which
the base _check_no_overlap rejects ('Cannot schedule tasks in the past'). Use
future dates (tz test pins a future July date so Toronto stays EDT for the
9:00->13:00 UTC assertion).
- Service-rate resolver tests created rows with seeded codes (callout_standard_normal,
per_km) -> UNIQUE(code) violation post-install. Assert against the seed instead.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The <template>_product_variant auto external-ID is not reliably created in this
Odoo 19 (only 5 exist on westin-v19; none for these products or product_labor_hourly),
so the rate rows' product_id refs failed at install: 'External ID not found:
..._product_variant'. Seed each product as model=product.product (the xmlid IS the
variant; name/price/uom/etc. delegate via _inherits) and reference it directly.
In-shop labour now uses a dedicated product_labour_inshop ($75) rather than reusing
product_labor_hourly, whose variant xmlid likewise does not exist. Caught on the
Westin clone install.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
product.template lost the separate purchase-UoM field uom_po_id in Odoo 19
(only uom_id remains). The plan's seed carried uom_po_id, which ParseErrors at
install: 'Invalid field uom_po_id in product.template'. Caught on first real
clone-install on the Westin Enterprise clone. The existing product_labor_hourly
uses uom_id only — match that.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Move the call-type select handler into onCallType() — OWL cannot compile a
multi-statement inline t-on body (was a render-breaking crash on mount).
- Replace color-mix() inside border shorthands with var(--sb-border) (Odoo-19
SCSS drops color-mix in a border shorthand).
- Technician placeholder option value '' (not 'false') so the required-tech
guard isn't bypassed.
- Remove dead setTiming(); null-coalesce the refdata onWillStart load.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review follow-up: the base fusion.technician.task.description is required=True and
non-in-store tasks require an address (_check_address_required). So:
- action_book_from_wizard now defaults description to 'Service booking' when the
payload carries neither description nor issue (avoids a required-field failure).
- test_task_without_order_is_allowed now sets description + is_in_store=True so it
exercises only the relaxed _check_order_link, not those unrelated base constraints.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Relaxes _check_order_link to a no-op (service bookings auto-create their SO;
in-shop/walk-in tasks may have none) and adds x_fc_is_service_repair on
sale.order. The 'Service Repair' crm.tag from the plan is intentionally
omitted: fusion_claims does not depend on crm and sale.order has no tag_ids;
the boolean flag is the repair-SO identity.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Clone-verify fixes: the HTTP request runs as base.user_admin, so set/read
x_fc_signature_image on that user (not self.env.user / uid 1); give the step a
recipe_node_id so button_finish passes the S21 no-recipe-link gate (also fixes
the pre-existing test_sign_off_finishes_step). 5/5 pass on an entech clone.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
onFinishStep: if the user has a saved Plating Signature, show FpSignatureConfirm
(one-tap, preview); otherwise open the draw-pad. Factored _openSignaturePad +
_commitSignOff (sends null data URI when using the saved signature).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Confirm-with-preview dialog (saved-signature preview + Sign & Finish + Use a
different signature). Registered after the signature_pad assets; version bump.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Kickoff brief, design spec, both implementation plans (rates foundation +
booking wizard), the UI mockup, and the hands-off Westin clone-verify/deploy
script for the Technician Service Booking feature.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
/fp/workspace/sign_off: signature_data_uri now optional; a supplied drawing
persists to res.users.x_fc_signature_image (SELF_WRITEABLE) and the wasted
per-step ir.attachment is dropped; no drawing + a saved signature just finishes.
/fp/workspace/load exposes user_has_plating_signature + user_plating_signature.
Merged 3 new tests into the existing TestWorkspaceSignOff.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Shop-floor sign-off currently makes operators redraw a signature every
time, and the drawing is discarded (reports read x_fc_signature_image).
Spec: use the saved Plating Signature (one-tap confirm-with-preview);
draw once when absent and persist it to x_fc_signature_image so future
sign-offs + reports reuse it. Tablet-workspace scope; no model/migration.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The jobs __manifest__.py data list references views/res_users_views.xml
(Plating Signature pad on the user preferences + full user form), and the
file was deployed live to entech, but it was never `git add`ed — so the
committed manifest pointed at a file absent from the repo. Fresh installs /
CI (and any clean-checkout deploy) failed with
`FileNotFoundError: .../fusion_plating_jobs/views/res_users_views.xml`.
Retrieved the live file from entech and committed it as-is.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Order-entry shortcut: when masking is toggled ON for an Express order line,
an amber "MASK" button appears to attach reference image(s)/PDF(s). The files
ride the existing _fp_apply_express_overrides_to_job path onto the job's
masking step, so the operator sees exactly what to mask — no recipe edit or
custom prompt needed.
- configurator: masking_attachment_ids on the wizard line + SO line;
action_upload_masking_ref; override branch writes refs onto mask steps;
amber multi-file MASK button (express_action_btns) shown when masking is on.
- jobs: x_fc_masking_attachment_ids on fp.job.step (per-step) + computed
rollup on fp.job; office "Masking Refs" form page (readonly preview).
- shopfloor: workspace step payload carries masking_refs (sudo'd attachment
read, rule 13m); operator sees thumbnail/PDF tiles on the mask step that
open in Odoo's full-screen FileViewer (zoom + swipe).
Verified end-to-end on entech: SO-line refs land on the mask step + job
rollup (WO-30091); payload mask_refs shape correct (is_image, /web/image).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Applied the same QR treatment to the Internal (Layout A) header QR: bumped the
box to 30mm and added the ~10% quiet-zone crop wrapper so the pattern fills the
box (finders intact), centered via the table cell. HD (1000px) already applied.
Verified live (WO-30072).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The no-tags QR used line-height centering, which wkhtmltopdf renders slightly
high (extra white at the bottom). Switched to a single-cell table with
vertical-align:middle (same mechanism as the with-tags case) so the QR centers
in its cell with balanced top/bottom margin. Verified live (WO-30072).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The barcode bakes a ~12% white quiet-zone border around the QR. Render the QR
oversized inside an overflow:hidden wrapper offset to clip ~10% off each edge
(under the quiet zone — finder patterns stay intact), so the black pattern
fills the box and reads bigger. Applied to both the full-width (no-tags) and
shared (with-tags) QR. White label cell around the wrapper preserves the scan
margin. Verified live (WO-30072, WO-30090) — finders intact.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
All three barcode_data_uri('QR', ...) calls bumped from 300px to 1000px
(under Odoo's 1.2M-pixel barcode cap, per rule 14). At the ~34mm display
size that's ~750 dpi — crisp on the label printer. Verified: PDF now embeds
a 1000x1000 QR XObject.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Taller QR row (30->36mm) and the QR now expands to a full-width centered ~34mm
when a job has neither masking nor baking (was leaving the right half empty);
when tags are present, QR ~32mm on the left with MASK/BAKE stacked on the right.
Logo/WO-band/field rows trimmed to fund the bigger QR. Verified live (WO-30072
no-tags full QR; WO-30090 BAKE tag).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
External Job Sticker rail: combined the separate QR row and MASK/BAKE flags
row into a single row — QR enlarged to ~28mm on the LEFT, MASK/BAKE badges
stacked on the RIGHT. WO band trimmed 18->16mm to free the vertical space.
Verified live on entech (WO-30090, BAKE present).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Box registry: new fp.box model (fusion_plating_receiving), one record per
received box, auto-created when a receiving is marked Counted (idempotent
_fp_sync_boxes — grows/shrinks with box_count_in, never touches an advanced
box). Status received -> racked -> in_process -> packed -> shipped, per-box
scannable QR (/fp/box/<id> controller). Backfill migration for receivings
counted before tracking shipped. Boxes list/kanban/form + receiving smart
button.
Job stickers redesigned (thermal label, 6x4 in / 152x102mm, mm layout @
paperformat dpi=96 so mm maps 1:1 in wkhtmltopdf — see rule 14):
- Internal Job Sticker = Layout A, ONE per job (shop notes from
x_fc_internal_description, job QR).
- External Job Sticker = Layout B, ONE per fp.box (BOX n/N, per-box QR,
factory company logo, customer-facing notes). Dynamic MASK badge
(x_fc_masking_enabled) + BAKE block (x_fc_bake_instructions), length-tiered
notes font. Display logic in fp.job._fp_sticker_data().
Also retains the SO/WO box-sticker MemoryError fix in report_fp_wo_sticker.xml
(per-box loop sourced from fp.receiving.box_count_in + 100-label safety cap).
Verified live on entech: 111 boxes backfilled (31 receivings), External renders
one page per box, Internal one per job, scan endpoint 303->login.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Surface switches between the plant kanban and job workspace used
doAction({..., target: "current"}), which APPENDS to Odoo 19's
controller/breadcrumb stack -- so the /odoo/... URL grew one segment
per switch, and the tablet lock/unlock window.location.reload()
preserved the bloat, compounding it every lock cycle. Switched those
navigations to target: "main" (Odoo sets clearBreadcrumbs when
action.target === "main" -> _computeStackIndex returns 0 -> stack
resets to a single action). The genuine one-level drill-down
(onJumpToBlocker -> hold/NCR form) keeps target: "current" so
breadcrumb-back still works there.
Also embeds the multi-rack racking panel inside the Racking step row
(gated on step.area_kind == 'racking') instead of a job-level section,
tying it to the recipe's Racking step.
19.0.37.0.1 -> 19.0.37.0.3. Both changes live on entech.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>