Carriers post the shipping label and (for international) the commercial invoice
to the delivery order's chatter, where they were hard to find. Add two smart
buttons on the stock.picking form that open the latest of each via the PDF
preview dialog (fusion_pdf_preview when installed, else open in a new tab).
Document discovery is carrier-agnostic (computed from the picking's attachments):
labels match 'Label*'; invoices match '*CommercialInvoice*' (UPS/Canada Post) or
'ShippingDoc-*' (FedEx/DHL, _get_delivery_doc_prefix). Buttons hide when absent.
Verified on entech: real FedEx picking resolved its label (invoice correctly
none for a domestic ship); synthetic UPS names resolved label+invoice and the
invoice button fired fusion_pdf_preview.open_attachment.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Relabel UPS 'Bill My Account' -> "Bill recipient's UPS account" with clear
help (it bills the customer's own UPS account; $0 shipping line; falls back to
Bill Shipper when the customer has no account on file).
- Improve the customer 'UPS Account Number' field help (stored per-customer,
auto-recalled at ship time for Bill Receiver).
- Add ups_rest_documentation_type setting (No / UPS commercial invoice) on the
UPS REST carrier, mirroring FedEx. Default 'invoice' preserves the existing
auto-generate-on-international behaviour; gate require_invoice on it so it can
be turned off. Surfaced on the UPS REST config page.
Validated live on entech (UPS production): CA->US shipment generated the label
+ a 60KB commercial invoice PDF (country of origin auto = CA, HS code applied),
then voided. Bill Receiver request confirmed accepted by UPS.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The UPS REST ship request wrapped the reference as
{'Value': [{'Code':'BM','Value': picking.name}]}, but _ups_rest_prepare_shipping_data
already builds reference_number as a list of {Code, Value} dicts. UPS expects
ReferenceNumber to be such an object (or array) with a STRING Value and rejects
the double-wrapped form on the ship call. This branch fires for every non-US/US
(e.g. CA->CA, CA->US) shipment, so rating worked but label creation failed.
Pass the list directly. Validated end-to-end against UPS production from a
Canadian origin: rate + real label (tracking 1Z6W...6355, then voided).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fp.certificate.contact_partner_id (single 'Customer Contact') becomes
contact_partner_ids (Many2many) — same shape as the partner's Default CoC
Contacts, as requested.
- Auto-populate: at job creation (fp.job cert resolution) + lazy-fill at issue,
contact_partner_ids = the customer's x_fc_default_coc_contact_ids (ALL).
- Send: action_send_to_customer pre-fills the composer with exactly the cert's
contact_partner_ids, so the CoC goes to all the defined clients (fallback:
company).
- Primary: the FIRST contact prints on the CoC + is gated for email; report
uses contact_partner_ids[:1].
- Gate: requires >=1 Customer Contact + the primary has an email.
- View: many2many_tags.
- Migration 19.0.10.3.0: copies each cert's old single contact into the new M2m,
drops the orphaned column.
Deployed + verified on entech: migration copied 16 certs, old column dropped,
field is M2m, send pre-fills the cert contacts, CoC report renders. entech-only
part_line_ids preserved.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
res.partner.x_fc_default_coc_contact_id (single Many2one) becomes
x_fc_default_coc_contact_ids (self-referential Many2many 'Default CoC
Contacts') so a customer can list several contacts who need the CoC.
- res.partner: M2m field (rel fp_default_coc_contact_rel) + many2many_tags.
- Cert: contact_partner_id (primary addressee printed on the cert) is set to
the FIRST CoC contact at job creation + lazy-filled at issue.
- Send: action_send_to_customer pre-fills the email composer with ALL the
customer's CoC contacts (primary + the rest), falling back to the company.
- fp.job cert-default resolution + the action_issue gate wording updated.
- Migration 19.0.10.2.0: copies each partner's old single value into the new
M2m, then drops the orphaned column.
Deployed + verified on entech: migration copied 2 existing values, old column
dropped, field is M2m, send pre-fills all contacts. entech-only part_line_ids
/ multi-part resolver preserved.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Service-booking wizard CSS: scroll on small screens (height:100% so overflow
engages), padded fields (!important vs Odoo input normalisation), narrow-screen
sub-grid collapse. Also hardens scripts/verify_service_booking.sh with an
asset-bundle compile gate. Clone-verified GREEN (assets compile) + deployed to
westin-v19 (fusion_claims 19.0.9.5.0).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The compile gate's 'odoo shell' tried to bind 8069 (the running app holds it) and
died with 'Address already in use' before compiling, false-failing the gate. Add
--no-http --http-port=0 --gevent-port=0 (same as the test run) so the shell loads
the registry and force-compiles the bundles without binding a port.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Confirm->Receive (A): after a single interactive SO confirm, receiving's
action_confirm returns action_view_receiving() so the user lands straight
on the Receive Parts screen (opt-out via fp_no_receiving_redirect context).
- Lock recipe (1): recipe_id readonly on the WO form — stick to the
order-entry recipe.
- Hide spec (2): customer_spec_id invisible on the WO form.
- Reset step (3): new fp.job.step.button_reset (operator-usable, audited) +
an undo button next to Start. Resets to Ready, clears finish + sign-off,
closes open timelogs, keeps start audit + move/CoC history.
- Lock steps (4): steps list create=false delete=false (no Add a line / no
trash) — steps come from the recipe, only skippable, never deleted.
- Bake gate fix (5): _fp_missing_required_step_inputs now honours the node's
collect_measurements master switch, matching the Record-Inputs wizard.
collect_measurements=False + required prompts no longer blocks finish
(wizard shows 0 rows, so the gate must too). Unblocks WO-30098 + 63 other
affected nodes (bake steps).
Deployed + verified on entech (-u jobs; bake finishes, reset done->ready,
recipe readonly, spec hidden, steps locked, receiving redirect target OK).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reported on the live wizard: no scroll on small screens, not responsive, fields
look unpadded.
- .o_service_booking: min-height:100% -> height:100% so the root is capped to the
action area and overflow:auto scrolls INTERNALLY (min-height let it grow to
content height, so the clipping action container never scrolled).
- input/select/textarea.f: padding 10px 12px !important + line-height 1.4 so
Odoo's backend input normalisation can't strip the field padding.
- add a <=560px media query collapsing the .two/.three sub-grids, wrapping the
time picker, and tightening margins (the main .grid already collapses at 780px).
- bump version 19.0.9.4.0 -> 19.0.9.5.0 (asset cache-bust).
Also harden scripts/verify_service_booking.sh: force-compile web.assets_backend +
web.assets_web_dark on the clone after tests, so a broken SCSS fails the deploy
gate BEFORE prod (a bad stylesheet would break the whole backend bundle; -u does
not compile assets — Odoo compiles them lazily).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Generalize the delivery packing slip template to be model-agnostic
(branches on doc._name to resolve the sale order + ship-to for
sale.order / fp.job / fp.receiving / fusion.plating.delivery) and add
three report actions bound to sale.order, fp.job and fp.receiving so the
packing slip appears in each one's Print menu (delivery already had it).
Uses _scheduled / _notes so it never AttributeErrors on models without
scheduled_date / notes. Declare the fusion_plating_receiving dep on
reports (already transitive via logistics) for the fp.receiving binding.
Verified on entech: real content for SO-30102, WO-30102, RCV-30103; all
four Print-menu bindings live.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>