_fp_create_portal_job hardcoded state='in_progress'. Now uses the
same _FP_JOB_STATE_TO_PORTAL_STATE map as write(), so a portal job
created for an already-confirmed (but not yet started) fp.job lands
in 'received' instead of jumping to 'in_progress'. Falls back to
'received' for unmapped states.
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>
Per client direction: every order is a thickness RANGE (e.g.
"0.0005-0.0008 mils" or "5-10 mils"), never a single value. The
old picker model (fp.recipe.thickness with a single 'value' Float)
was modelling the wrong concept and overcrowding the order entry
UI. Replaced with one free-text Char field that auto-fills from
last-used or part default.
DELETED entirely:
- fp.recipe.thickness model (file + view + ACL + manifest entry)
- recipe.thickness_option_ids One2many (the picker source)
- "Thickness Options" inline list on the recipe form
- sale.order.line.x_fc_thickness_id (M2O picker)
- account.move.line.x_fc_thickness_id
- fp.delivery.x_fc_thickness_id
- fp.direct.order.line.thickness_id
ADDED:
- sale.order.line.x_fc_thickness_range (Char) — operator types range
- account.move.line.x_fc_thickness_range — for invoice rendering
- fp.delivery.x_fc_thickness_range — for packing slip
- fp.direct.order.line.thickness_range — for the wizard
- fp.part.catalog.x_fc_default_thickness_range — part default
AUTO-FILL CHAIN (sale.order.line + wizard line):
1. Operator already typed → keep
2. Most recent SO line for (this part, this customer) with a
non-empty thickness_range → copy that
3. part.x_fc_default_thickness_range → copy
4. Blank — operator types
Implemented as both an @api.onchange (interactive) AND a
create() override (programmatic — wizard, sale_mrp bridge,
imports). Same logic in both paths.
WIZARD push-to-defaults: when "Save as Default" toggle is ticked
on a wizard line, persist the line's thickness_range to
part.x_fc_default_thickness_range so future first-customer orders
get a sensible starting point.
REPORTS: customer_line_header.xml + report_fp_wo_sticker.xml now
print the Char range as-typed (no display_name lookup needed).
KEPT (admin documentation only — doesn't affect order entry):
- recipe.thickness_min, thickness_max, thickness_uom on the recipe
root: documents the recipe's CAPABILITY range. No UI gate; just
for spec authors to record what the chemistry can produce.
JOB GROUPING: fp.job auto-create groups SO lines by (recipe, part,
spec, thickness, serial). Updated to key on the thickness_range
Char (stripped) instead of the deleted thickness_id integer.
DB cleanup: --update=base ran on the upgrade, dropping the
fp_recipe_thickness table + the four x_fc_thickness_id columns.
Existing data was already nulled in earlier dev work.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Specifications menu (urgent — workflow blocker for estimators):
- Moved from Configuration → Quality & Documents (manager-only) up
to Plating → Quality (sequence 70). Now visible to estimator,
supervisor, and manager.
- Renamed "Customer Specs" → "Specifications" — the seeded library
includes industry standards (AMS, MIL, ASTM, BAC) not just
customer-private specs.
- Action display name updated: "Customer Specifications" → "Specifications".
- Added action.help HTML so the empty-state placeholder explains
the Specifications library purpose to first-time users.
- Old xmlid (menu_fp_config_customer_spec) preserved so existing
links / breadcrumbs / search references continue to resolve.
Other clarifying renames:
- Safety: "JHSC" / "JHSC Meetings" → "H&S Committee (JHSC)" /
"H&S Committee Meetings" — acronym was opaque to non-Canadian
H&S folks.
- Operations: "Move Log" → "Parts & Rack Move Log" — generic name
could be confused with chatter messages or stock moves.
- Configuration → Recipes & Steps: "Workflow States" →
"Job Workflow Stages" — generic name; clarifies these are job
state milestones (passed-stage tracking), not generic workflow.
- Compliance → General: child folder "Configuration" → "Reference
Data" — three levels of "Configuration" nesting (Plating>Config
vs Plating>Compliance>General>Config) was confusing.
No model / data changes. Pure menu metadata.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reports updated to print Specification (with revision via display_name):
- report_fp_sale.xml — header sections show "SPECIFICATION" instead
of "COATING CONFIG", reads doc.x_fc_customer_spec_id (added on
sale.order via quality inherit, computed from line.customer_spec_id)
- report_fp_wo_sticker.xml — propagates _spec alongside _coating
- fusion_plating_reports/report_fp_job_traveller.xml — header row
now shows Specification (falls back to coating)
- fusion_plating_jobs/report_fp_job_traveller.xml — same fall-back
- fusion_plating_jobs/report_fp_job_sticker.xml — _spec added
sale.order.x_fc_customer_spec_id added as a stored compute on
sale.order (in quality) so reports can render order-level spec.
Mirrors the line's first spec; updates on line edit.
Tablet payload (shopfloor_controller.py):
- spec_label added to the job payload dict
- defensive 'customer_spec_id' in job._fields check (shopfloor doesn't
depend on quality — circular if added)
Portal: deferred (same circular-dep issue, more substantial UI rewrite
needed; Phase E backlog item).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pricing:
- Quality inherit on fp.pricing.rule adds customer_spec_id + recipe_id
- Quality inherit on fp.quote.configurator adds customer_spec_id field
+ extends _find_matching_rule with priority chain:
spec (+8) > recipe (+6) > coating (+4) > material (+2) > cert (+1)
- View inherit surfaces both new pickers on the rule form
Quality points:
- fp.quality.point now has customer_spec_ids + recipe_ids M2M filters
- Matcher (_matches + _find_matching) accepts new args
- Hook overrides on SO confirm + job confirm/done + step finish
pass spec/recipe context through to the matcher
- View surfaces both new M2M widgets
Job:
- jobs/sale_order.py wires x_fc_customer_spec_id from SO line to
fp.job.customer_spec_id on action_confirm
Cert:
- Quality inherit on fp.certificate adds customer_spec_id field +
create() override auto-fills spec_reference from spec.code+revision
Resolution priority: explicit spec_reference > cert.customer_spec_id
> SO line spec (with print_on_cert) > legacy coating fallback
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four message_post calls were passing strings with HTML tags as
plain `body=_(...)` instead of `body=Markup(_(...))`. Odoo escapes
non-Markup strings, so the chatter rendered "<b>QA Review failed</b>"
as literal text instead of bolding it.
Original bug surfaced via the Contract Review (QA-005) flow:
body: "<b>QA Review failed</b> by Garry Singh. Awaiting
client information.<br/><b>Reason:</b><br/>
<div data-oe-version=\"2.0\">Need to get updated
drawing...</div>"
Audit scan turned up three more identical patterns:
fusion_plating/models/fp_parent_numbered_mixin.py:118
"Issued <strong>%s</strong> to ..."
fusion_plating_jobs/models/sale_order.py:282
"Confirmed quote <strong>%s</strong> as <strong>%s</strong>."
fusion_plating_quality/models/fp_contract_review.py:430
"<b>QA Review failed</b> by ... <b>Reason:</b><br/>%(reason)s"
fusion_plating_quality/models/fp_contract_review.py:524
"<b>QA Review completed</b> by ... <b>Special Instructions
captured:</b><br/>%(notes)s"
Fixes:
- Wrapped each body=_(...) with Markup(_(...)) using the
Markup(template) % values pattern (auto-escapes the substituted
values; user-supplied free text stays safe).
- For Html-field substitutions (qa_failure_reason,
special_instructions), explicitly wrapped the value in Markup()
so already-formatted HTML editor content (with data-oe-version="2.0"
wrapper divs) flows through without being re-escaped.
- Added `from markupsafe import Markup` to the two files that
didn't already import it (mixin + contract_review).
Drift cleanup: pulled the 180-line newer fp_contract_review.py
from entech to the local repo (added action_qa_review_failed,
action_open_client_email_wizard, action_view_client_emails,
action_complete_after_info, awaiting_info state, qa_failure_reason
+ special_instructions Html fields, etc. that had been edited on
entech without being committed).
Tested by re-posting via odoo shell on review 10: body now stores
"<b>QA Review failed</b>..." with literal HTML tags instead of
the double-escaped "<b>..." entities. Old chatter records
with the bad escape stay as-is in the audit trail.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirror of the SO Internal variant for fp.job. Same body fields,
same per-box loop; Notes column reads x_fc_internal_description
from the first linked SO line (job.sale_order_line_ids[:1]).
Operator on the shop floor sees ops-internal notes without those
ever appearing on the customer-facing External sticker.
Verified on fp.job 2635 with seeded internal_description: Notes
column reads "INTERNAL JOB: handle with care, no rework on this
batch" — confirms the Job Internal variant's override path mirrors
the SO Internal variant's.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Activates the per-box loop landed in the prior commit. SO External
reads line.product_uom_qty; Job External reads job.qty. Inner
template now renders one sticker per physical box, marking each
with "X / N" in the Qty row.
Verified on fp.job 2635 (qty temporarily set to 3): 3-page PDF
with Qty rows "1 / 3", "2 / 3", "3 / 3" — each page identical
otherwise (same WO#, same QR, same body fields).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The _fp_auto_create_job grouping key was (recipe, part, coating).
Lines that shared all three but differed in thickness (or serial)
silently collapsed into one fp.job — the second line's thickness/SN
was lost, and any downstream cert printed the first line's values
across both batches. Silent mis-attestation = compliance hole.
Extended the key tuple to (recipe, part, coating, thickness, serial).
Single-line SOs and same-(thickness, SN) multi-line SOs collapse
identically to before. Only lines that previously merged when they
shouldn't have now split into their own fp.jobs.
TDD via test_so_confirm_splits_by_thickness:
- seeds the part with default_process_id so both lines hit the
`if recipe:` branch (where the bug lived — the no_recipe branch
already split correctly per line)
- confirms 2 jobs after action_confirm with each carrying its
own thickness via the linked SO line
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous fix swapped t-field -> t-esc so the QWeb error stopped,
but the report still printed blank. Root cause: Odoo looks up the
report data model via env['report.<report_name>'], but our model was
named 'report.fusion_plating_jobs.report_fp_job_margin' while the
action's report_name is 'fusion_plating_jobs.report_fp_job_margin_template'.
The model lookup missed, _get_report_values never fired, and the
template rendered with no 'rows' in scope — empty foreach -> empty
page.
Renamed the model to report.fusion_plating_jobs.report_fp_job_margin_template.
Verified: PDF size jumped from 1229 bytes (blank) to 125880 bytes
(fully populated). HTML now contains 'Job Margin', 'Step Breakdown',
and the actual WO name.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The template used 't-field="step['rate']"' for monetary values pulled
from dict rows. Odoo 19's QWeb asserts t-field has at least one dot
(it's strictly for record.field_name lookups). Replaced six bare-dict
t-field usages with t-esc; the existing t-options widget=monetary +
display_currency still applies for currency formatting.
Verified by rendering report for WO-30019 — 1229-byte valid PDF, no
QWeb error.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- B1: Add Credit Note wizard path was blocked because invoice_origin
has copy=False and the wizard doesn't set fp_from_so_invoice. Now
the validator allows reversals when reversed_entry_id points at a
customer-facing move that itself went through the validator at
original creation time. account.move._fp_parent_sale_order also
walks self.reversed_entry_id._fp_parent_sale_order so the credit
note inherits the parent number (CN-<parent>).
- Bug 1: sale.order.unlink() now blocks deletion when x_fc_parent_number
is set (matches spec §6.2). Draft quotes remain freely deletable
per Odoo standard. Applies to all users including admins.
- Bug 2: out_receipt added to CUSTOMER_TYPES so POS-style receipts
hit the same SO-flow gate as out_invoice / out_refund.
- C1: WO grouping key changed from recipe.id to (recipe.id, part.id,
coating.id). Bundling lines with different parts under one WO put
first_line's part_number on the CoC header — silent compliance
mis-attestation. Now distinct parts always get distinct WOs even
when they share a recipe.
- C3: SQL whitelist (_FP_COUNTER_FIELD_RE) on _fp_assign_parent_name's
interpolated counter field name. No user input today; defence in
depth for future subclasses that might read the name from context.
Verified on entech: parent=30017, credit note = CN-30017,
multi-part SO produces 2 WOs (one per part), confirmed-SO unlink
blocked, out_receipt blocked, whitelist regex enforced.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Verified pass on entech (parent=30015): all linked docs share the
parent number, immutability + unlink-block + direct-invoice-block
all enforced. NCR/CAPA fall back to legacy sequences as designed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
short_wo now handles both naming schemes: new WO-NNNNN[-NN] (strips
WO-) and legacy WH/JOB/NNNNN (last slash segment). Customer-facing
Work Order column shows '30000-02' instead of 'WO-30000-02'.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A small grey 'Originally quoted as Q202605-200' line appears below
the SO heading once the order is confirmed. Uses invisible= on the
wrapper div (Odoo 19 forbids t-if in standard form views).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per-model counter fields on sale.order renamed to x_fc_pn_*_count
to avoid collision with pre-existing compute fields of the same
short name in bridge_mrp / receiving / configurator (silent
compute-override was suppressing the storage). 4 child models
(fp.certificate, fp.receiving, fusion.plating.delivery,
fusion.plating.pickup.request) now derive names as PFX-<parent>
with -NN suffix from the 2nd onward.
fusion.plating.pickup.request gains a sale_order_id field
(optional) so pickups created against an SO get parent-derived
names, while standalone pickups (pre-SO) fall back to PU/YYYY/NNNN.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Customer invoices (out_invoice / out_refund) can only be created via
sale.order._create_invoices() or with an invoice_origin matching an
existing SO. Applies to ALL users including admins. Once created,
the move's name is derived from the SO's parent number: IN-30000,
IN-30000-02, CN-30000, ... Pre-existing portal-job link on
action_post() preserved.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces x_fc_wo_group_tag grouping with resolved-recipe grouping.
Bare WO-<parent> when 1 recipe, WO-<parent>-NN zero-padded for N>1
ordered by min line sequence. fp.job inherits parent-numbered mixin
for the manual-add path; bulk SO-confirm sets names explicitly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two compounding issues introduced by the persistence audit work:
1. fields.Text mismatch: sale.order.x_fc_internal_note and
x_fc_external_note are actually fields.Html. I declared the
sale.order.line related mirrors and the fp.job stored copies as
fields.Text, so setup_related raised:
TypeError: Type of related field
sale.order.line.x_fc_internal_note is inconsistent with
sale.order.x_fc_internal_note
Fixed by switching both Note fields on fp.job and sale.order.line
to fields.Html.
2. Module-load-order: Tier 3 fields (x_fc_delivery_method,
x_fc_ship_via, x_fc_invoice_strategy) are defined in
fusion_plating_jobs (related to sale.order via _inherit), but I
referenced them in fusion_plating core's fp_job_views.xml — which
loads BEFORE fusion_plating_jobs registers the fields. View
validator raised "Field x_fc_delivery_method does not exist".
Fixed by removing those 3 fields from the core view group and
adding them via xpath in fusion_plating_jobs's fp_job_form_inherit
(which loads after the fields are registered).
Both fixes deployed and verified — registry loads in 2s, all field
types match, related path resolves correctly. No data loss; the
fp.job rows that already had stored Text content for internal_note /
external_note will carry over into the Html field intact.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tier 3 of the SO->fp.job persistence audit. Three logistics/billing
fields surface on fp.job as related read-only (not stored) mirrors:
- x_fc_delivery_method - Local Delivery / Shipping Partner / Customer
Pickup. Cargo classification used by logistics planning.
- x_fc_ship_via - Carrier name (UPS, FedEx, customer pickup, etc.).
- x_fc_invoice_strategy - Deposit / Progress / Net Terms / COD-Prepay.
Read by the invoicing module's hooks; mirroring on the WO is for
manager visibility only.
These were intentionally chosen as related (not stored persisted)
because the SO is the authoritative source - the existing downstream
code (delivery + invoicing modules) already reads them off SO directly.
A stored copy would risk drift. Related auto-follows SO updates.
Same three fields also mirrored on sale.order.line as stored related
for per-line list visibility.
Closes the SO->fp.job persistence audit. All 10 operational fields
identified now flow through to the WO (7 stored + populated at confirm,
3 related read-only).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tier 2 of the SO->fp.job persistence audit. Four operational metadata
fields mirrored from sale.order:
- x_fc_internal_deadline (Date) - shop's internal target finish date,
ahead of the customer-facing deadline. Kept separate from
date_deadline (which scheduling code may adjust).
- x_fc_planned_start_date (Date) - customer-quoted planned start date.
Kept separate from date_planned_start (Datetime, capacity-adjusted).
- x_fc_internal_note (Text) - shop-internal notes from the order.
- x_fc_external_note (Text) - customer-facing notes, printed on
traveller / BoL / cert.
All four populate at SO confirm via _fp_auto_create_job, and surface
on sale.order.line as stored related fields for per-line visibility.
fp.job form view gets a Notes group alongside the Customer References
group from Tier 1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tier 1 of the SO->fp.job persistence audit. Three customer-reference
fields entered on sale.order's Plating tab were not flowing through
to fp.job (or SO lines), so the shop floor and printed paperwork
(traveller, BoL, cert) had to round-trip via sale_order_id every time.
Changes:
- fp.job: new x_fc_customer_job_number (Char, tracking), x_fc_po_number
(Char, tracking), x_fc_rush_order (Boolean, tracking). All three
populated by _fp_auto_create_job at SO confirm time.
- sale.order.line: x_fc_customer_job_number / x_fc_po_number added as
stored related fields off order_id so per-line list views show the
customer's references without navigating to the order header
(x_fc_rush_order was already on lines).
- fp.job form view: small Customer References group under the title
surfaces the three fields where the user expects them.
Verified end-to-end: SO -> SO line related fields -> fp.job direct
fields all carry the same value.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two improvements to the Process Tree visualization opened from the
Work Order's Process Tree header button:
1. Back button returns to the Work Order (job form) instead of
Plant Overview. fp.job.action_open_process_tree now passes
back_job_id in the client-action context; process_tree.js
reads it via a new backJobId getter, updates the button label
to "Back to Work Order", and routes onBack to fp.job form.
The Plant Overview fallback stays for callers that don't pass
either back_step_id or back_job_id.
2. Completed operation/step cards now have a green fill (#1e8449)
and a subtle pulsing glow (box-shadow animation, 2.6s alternate)
so finished work pops against still-pending dark cards. Hover
pauses the animation so the click target is steady. Reuses the
same green the workflow-state slice already used.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User wanted Finish & Next to drop its text label like the other row
buttons, but stand out visually as the primary action. Solution:
icon-only with a vivid green color and a subtle pulse animation.
- New SCSS: fp_finish_btn.scss with branch-on-$o-webclient-color-
scheme so the dark bundle uses green-400 (pops on dark bg) and
light bundle uses green-600. Pulse animation 1.8s ease-in-out
infinite, scale 1.0 ↔ 1.18. Pauses on hover/focus so the click
target is steady.
- Registered in both web.assets_backend and web.assets_web_dark
per the project's dark-mode rule (CLAUDE.md).
- View: string="Finish & Next" → title="Finish & Next",
class drops "text-primary", gains "o_fp_finish_btn".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
UI: secondary row buttons in the embedded step list are now icon-only
with tooltips (Pause / Complete 1 → Next / Record / Skip / Move…).
Saves ~70% horizontal space. "Finish & Next" stays text+icon as the
primary action.
Fix: removed the racking-inspection gate from button_finish. Racking
is now a recipe step (not a separate inspection workflow), so the
"Racking inspection for ... is Inspecting — must be Done" error no
longer fires. _fp_check_racking_inspection_complete() helper is
preserved for diagnostics but no longer called from the finish path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User feedback: operators with small parts (e.g. valve bodies) batch
them through the whole recipe. The previous behavior — Finish & Next
raising "use Complete 1 → Next or Move..." when qty>1 — forced N
clicks for a workflow that's naturally one click.
Change: _fp_record_one_piece_auto_move now ALWAYS bulk-moves
qty_at_step parts to the next step in one move record, regardless of
whether the qty is seed-only (first / paperwork step) or real (parked
from an upstream move). Audit trail is preserved (one move row per
finish), operator gets one click.
Three buttons now map cleanly to the three workflows:
- Finish & Next: bulk all parts forward, finish, auto-start next
- Complete 1 -> Next: streaming flow, move 1 part, stay open
- Move...: explicit qty + destination wizard for partial batches
Verified end-to-end on entech: seed qty=6 + real-incoming qty=6 both
move forward in a single click each.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The qty gate I added refused Finish on steps where qty_at_step > 0,
to force operators to move parts forward first. But the first-step
seed in _compute_qty_at_step gives the earliest non-terminal step
a notional qty = job.qty — a UI hint, not actual parked parts.
Paperwork steps (Contract Review, Inspection-by-paperwork, etc.)
sit on that seed, and the gate was blocking Finish with a misleading
error.
Fixes:
- button_finish gate now checks for REAL incoming moves before
refusing. Seed-only qty (no incoming_move_ids filtered to non-
self-loop) is exempt.
- _fp_record_one_piece_auto_move detects seed-only qty and bulk-
moves ALL parts in one shot to the downstream step. Correct for
paperwork / first steps where parts don't physically wait
per-piece — one click finishes the paperwork and pushes the whole
batch forward.
For steps with REAL incoming moves (parts actually moved here via
a Move record), the original gate semantics still apply: qty == 1
auto-moves one part; qty > 1 raises with the "use Complete 1 → Next
or Move…" message.
Verified on entech: Contract Review with seed qty=6 now finishes
cleanly, bulk-moving all 6 parts to the next step in one move.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Received milestone was tied to recipe steps tagged
default_kind='receiving'. But receiving in this system is a pre-
recipe inbound logistics flow (fp.receiving model in
fusion_plating_receiving). When parts physically arrive, the flow
sets sale_order.x_fc_receiving_status to partial or received.
Changes:
- New trigger_on_parts_received Boolean on fp.job.workflow.state.
- _fp_is_passed_for_job branch: passes when sale_order's
x_fc_receiving_status is in (partial, received).
- _compute_workflow_state_id depends extended with
sale_order_id.x_fc_receiving_status so the bar recomputes
automatically when the receiving flow updates the SO.
- DB seed update: Received state drops trigger_default_kinds=
'receiving' and gains trigger_on_parts_received=True.
Verified end-to-end on entech: bar moves Confirmed → Received on
status change, regresses on rollback, accepts both 'partial' and
'received' as satisfying the milestone.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two coupled fixes so the workflow bar shows "In Progress" when work
is actually underway, even on recipes without kind tagging:
B. Auto-promote fp.job.state on first step start.
fp.job.step.write hook detects step transitions to in_progress
and promotes parent job state from 'confirmed' → 'in_progress'.
Without this, fp.job.state never reached 'in_progress' anywhere
in the codebase, so the trigger_on_job_state='in_progress'
path was dead code.
C. Smarter trigger_first_step_started for untagged recipes.
For tagged recipes (any step has kind in wet/bake/mask/rack),
keep the strict kind-based check. For untagged recipes (all
steps kind='other' or similar), fall back to "any step in
in_progress/paused/done" so the milestone fires regardless of
recipe authoring quality.
Verified end-to-end on entech with untagged steps:
- confirmed → in_progress when first step starts
- workflow bar tracks at in_progress through the work
- workflow bar advances to done when all steps done/skipped
Recipe authoring still encouraged for full Received / Inspected
intermediate states (those keep their default_kind triggers).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Root cause: two compounding bugs in fp.job.workflow_state_id.
- The "Confirmed" state seed has no trigger fields set, so it
never passes _fp_is_passed_for_job.
- The _compute_workflow_state_id loop breaks on the first
non-passed state — so when Confirmed fails, every later
state stays unevaluated and the bar is stuck at Draft.
Fixes:
- Add trigger_on_job_state Selection field on fp.job.workflow.state
with values confirmed/in_progress/done. Passes when fp.job.state
>= the chosen value ("at least" semantics with explicit ordering
that treats on_hold==in_progress and cancelled outside the
progression). Lets workflow states key off the job's own state
when recipe default_kind tagging isn't present.
- Extend _fp_is_passed_for_job with the new branch.
- Change _compute_workflow_state_id from first-non-pass-breaks to
highest-passed-wins. Untagged/not-applicable states no longer
block the cascade — the bar shows the furthest milestone the
job has actually reached.
- Seed update (DB-side, since data is noupdate=1): Confirmed now
has trigger_on_job_state='confirmed'.
Result: Work Order # 00011 (state=confirmed, all 11 steps done/
skipped) now correctly shows the bar at "Done" instead of "Draft"
(via the existing trigger_all_steps_done on Done). Mid-flight
confirmed jobs without recipe tagging will show at least
"Confirmed" now.
Recipe authoring note (out of scope here): for accurate Received /
In Progress / Inspected intermediate states, recipe nodes still need
default_kind tagging (receiving / wet|bake|mask|rack / final_inspect).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a supervisor edits a timelog's date_started/date_finished (or
deletes a stale timelog), the parent step's "Actual Min" column
was showing stale data — duration_actual is a regular Float set
once by button_finish.
Adds:
- fp.job.step._fp_resum_duration_actual: quiet helper that re-sums
duration_actual from time_log_ids.duration_minutes. Skip no-op
updates so write traffic is minimised.
- fp.job.step.timelog.create/write/unlink hooks: call the helper
on the affected parent step(s) so duration_actual stays
consistent. Write hook only fires when date_started/date_finished/
step_id changed (notes edits skip resync). step_id reassignment
resyncs both old and new parent.
- Existing action_recompute_duration_from_timelogs (manual button)
still posts a chatter entry for audit-trail use cases.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three coupled shop-floor corrections:
- fp.job._compute_display_name: renders "Work Order # 00011" in
form header, breadcrumbs, M2O dropdowns, and error messages.
DB name stays as WH/JOB/00011 - existing chatter/cert/delivery
references unchanged.
- fp.job.step.button_finish: refuses if qty_at_step > 0 AND a
downstream pending/ready step exists. Last runnable step is
exempt (parts complete in place). Manager bypass via
fp_skip_qty_gate=True context key.
- fp.job.step.action_complete_one_to_next: new per-row button
"Complete 1 -> Next" for streaming flow (large parts going
one-by-one). Records move(qty=1) to next step; if drain takes
qty_at_step to 0, auto-finishes source + auto-starts destination
via existing action_finish_and_advance.
- fp.job.step._fp_record_one_piece_auto_move: auto-move shim
wired into action_finish_and_advance. qty=1 + downstream =>
silently record move(1). qty>1 + downstream => raise pointing
at Complete 1 -> Next. Last step always allowed.
- 16 new TestQtyGate tests covering gate / shim / auto-finish /
last-step exemption / display rename / Move wizard zero-qty.
Spec: docs/superpowers/specs/2026-05-12-step-qty-gate-and-display-rename-design.md
Plan: docs/superpowers/plans/2026-05-12-step-qty-gate-and-display-rename.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements the milestone-cascade design (Phase 1) and catches the
fusion_plating_jobs / fusion_plating_certificates source up to entech.
Milestone cascade (this PR's core):
- fp.job: new computes all_steps_terminal, next_milestone_action,
next_milestone_label; dispatcher action_advance_next_milestone with
3 helpers (_action_open_draft_certs, _action_open_draft_delivery,
_action_mark_active_delivery_delivered); _resolve_required_cert_types
resolver; _fp_create_certificates rewritten to honour
part.certificate_requirement + partner flags + loop over resolved
cert types
- fp.job.workflow.state: new trigger_on_delivery_state Boolean;
_fp_is_passed_for_job extended with delivery-state branch;
Shipped state seed reroutes from default_kind=ship to the new trigger
- View: hide Finish & Next when all_steps_terminal; add 4 mutually-
exclusive milestone buttons (Mark Job Done / Issue Certs / Schedule
Delivery / Mark Shipped) bound to one dispatcher
- Cert gate (fusion_plating_certificates/models/fp_delivery.py):
action_mark_delivered hard-blocks on draft certs; manager bypass
via fp_skip_cert_gate=True context key
- 24 unit tests in test_fp_job_milestone_cascade.py covering computes,
resolver, dispatcher, cert gate
- Spec: docs/superpowers/specs/2026-05-12-job-milestone-cascade-design.md
- Plan: docs/superpowers/plans/2026-05-12-job-milestone-cascade.md
Other entech changes caught up in this sync (from earlier session
patches not previously committed):
- fp.job version bump series 18.x → 19.0
- res_users_views.xml addition (signature widget in user prefs)
- racking inspection smart button removal
- various view/manifest touch-ups
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three issues from user testing on entech:
1. RPC error: column fp_step_template.triggers_workflow_state_id
does not exist
Root cause: the field was declared in fusion_plating CORE, but
its target model fp.job.workflow.state lives in fusion_plating_jobs.
Odoo loads core BEFORE jobs (jobs depends on core), so when core's
field declaration runs, the comodel doesn't exist yet — and Odoo
silently skips creating the column.
Fix: moved the field to fusion_plating_jobs/models/fp_job.py via
_inherit. Now the column is added when jobs loads (after core),
and the FK target is resolvable.
2. No chatter on the Workflow State form
Added _inherit = ['mail.thread', 'mail.activity.mixin'] to
fp.job.workflow.state. Tracking enabled on name/code/sequence so
admins see who changed the milestone vocabulary. <chatter/> widget
added to the form view.
3. Form layout still showed cramped 2-col help text
The XML file on disk had my new alert-info card, but Odoo's DB
ir_ui_view still held the old arch. The -u didn't refresh it
(likely because the file's mtime didn't change between deploys).
Fix: bump version + the next deploy will run a SQL DELETE on the
ir_ui_view record so Odoo recreates it from XML on -u.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two follow-ups on the workflow state work:
1) Form layout
The "How triggers combine" help text was crammed into a 2-column
group, taking ~25% of the available width. Pulled it out of the
group and rendered as a full-width <div class="alert alert-info">
below the trigger fields. Same fix applied to Notes — uses a
<separator> + bare <field> for full sheet width.
2) Simple Recipe Editor support
The trigger field was only exposed in the Tree Editor. Added it
to the Simple Editor's inline library form too:
* fp.step.template.triggers_workflow_state_id (new Many2one) —
per-template default, snapshot-copied to recipe nodes when
dropped into a recipe (added to _SNAPSHOT_FIELDS).
* /fp/simple_recipe/workflow_states/list — new endpoint to feed
the dropdown. Soft-fails when fusion_plating_jobs isn't
installed (returns []).
* Library editor JS — _fpEnsureWorkflowStatesLoaded helper
caches the catalog on first open (create + edit paths both
warm it). Save vals carry the trigger id.
* Library editor XML — dropdown rendered after the flag
checkboxes. Hidden when the catalog is empty so the form
doesn't show a useless "— None —" pick.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The compute appended '[code]' so admin pages could disambiguate
states at a glance. But display_name is what the status-bar widget
uses to render each pill, so every pill came out as 'Received
[received]', 'In Progress [in_progress]', etc.
Removed the compute. Admin list view already shows code as a
separate column.
Earlier commit parented the new menu directly under menu_fp_config,
making it appear at the top alongside the 7 themed buckets instead
of inside one. Workflow milestones map directly to recipe-step
kinds, so 'Recipes & Steps' is the natural home.
Replaces the generic Draft/Confirmed/In Progress/Done statusbar with
a shop-configurable list of plating-specific milestones. Bar advances
automatically as recipe steps complete; no manual button clicks.
What ships
==========
* New model: fp.job.workflow.state
Catalog of milestones (name, code, sequence, color, triggers).
Triggers can be:
- trigger_default_kinds: "receiving,inspect" matches by step.default_kind
- trigger_first_step_started: any wet/bake/mask/rack step started
- trigger_all_steps_done: every non-cancelled step in done/skipped
- block_when_quality_hold: held back while NCR/hold open
Plus per-recipe-node override (see below).
* Default 7-state seed (data/fp_workflow_state_data.xml):
Draft → Confirmed → Received → In Progress → Inspected → Shipped → Done
noupdate=1 so per-shop edits survive module upgrade.
* Recipe-side trigger field on fusion.plating.process.node:
triggers_workflow_state_id (Many2one, optional)
Wins over default_kind matching. Lets the recipe author pin a
specific step as a milestone trigger even when default_kind isn't
set or doesn't match. Exposed in the Recipe Tree Editor properties
panel (dropdown sourced from the catalog).
* fp.job.workflow_state_id (computed, stored)
Iterates the catalog in sequence order; lands at the highest passed
milestone. Recomputes on step state / kind / recipe_node / quality
hold changes. Replaces fp.job.state on the form's statusbar.
* Settings UI: Configuration > Workflow States
Standard list+form pages so admins can add / edit / deactivate
states. Manager-group write permission, supervisor read.
What this does NOT do
=====================
* Doesn't drop fp.job.state — that field still drives the internal
state machine (button_confirm, action_cancel, etc.). Only the
UI statusbar is reassigned.
* No migration for existing jobs — they auto-recompute on next read
because workflow_state_id is a stored compute with the right
api.depends. Existing WH/JOB/00342 will display its current
workflow state on next page load.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User feedback: operators kept asking why their work order said "Step 10"
for the first row. The 10-spacing was originally there to allow midpoint
inserts (insert sequence 15 between 10 and 20 without renumbering).
Tradeoff is operator confusion, and recipe authors rarely insert in the
middle anyway. Switching to 1-based contiguous sequences.
Files changed (every step-sequence allocation in the codebase):
fusion_plating_jobs/models/fp_job.py
_generate_steps_from_recipe — seq_counter starts at 1, increments by 1.
This is the path that builds fp.job.step records, so new jobs now show
Step 1, 2, 3, ... in the work order.
fusion_plating_bridge_mrp/models/mrp_production.py
Same change for the legacy MRP bridge so customers still on
mrp.production also get 1-based numbering.
fusion_plating/controllers/recipe_controller.py
- create_node: max_seq + 1
- reorder_nodes: idx + 1
- swap renumber: i (was i * 10)
- paste-import renumber: i (was i * 10)
- move_node: max_seq + 1
- _copy_subtree (recipe duplicate/import): i (was i * 10)
fusion_plating/controllers/simple_recipe_controller.py
- _sequence_for_position rewritten — always renumbers siblings to
keep them contiguous. Returns pos + 1 for the inserted node.
Old code used midpoint-with-fallback-to-renumber (10/20/30 spacing).
- step_reorder: i (was i * 10)
- library_input_add + step_add_input: existing_max + 1
What this DOESN'T do
Existing fp.job.step records keep their old sequences (10, 20, ...).
Re-confirm the SO to spawn a fresh job if you want the clean 1-based
numbering on a current test job. No data migration — we're in dev
and the user explicitly said test data is disposable.
What this DOES do
Every NEW job created from this commit forward shows Step 1, 2, 3, ...
Every NEW recipe step inserted via the simple editor / tree editor
also gets sequence 1, 2, 3, ...
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>