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>
Spec-side picker (x_fc_customer_spec_id / customer_spec_id) added on:
- sale.order.line (via quality inherit — onchange autofill, create()
fallback to part default, _prepare_invoice_line carry)
- account.move.line (via quality inherit — invoice rendering)
- fp.part.catalog (via quality inherit — x_fc_default_customer_spec_id)
- fp.direct.order.line (via quality inherit — wizard picker + autofill)
- fp.direct.order.wizard (action_create_order post-creates spec on SO line)
Thickness picker switched to fp.recipe.thickness (replaces coating-scoped):
- sale.order.line.x_fc_thickness_id comodel + domain rewired to recipe
- account.move.line + fp.delivery same
- fp.direct.order.line.thickness_id same
View inherits in quality add Specification picker next to legacy
Primary Treatment column on:
- SO form line tree
- part catalog Default Treatments block
- direct-order wizard line tree + drawer
Wizard files (fp.contract.review.client.email.wizard) pulled from
entech into the repo — they were ahead of the repo. Quality __init__
now imports wizards/.
Legacy x_fc_coating_config_id + treatment_ids remain visible during
transition; Phase E removes them.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per client review: NADCAP-qualified recipes need manager-only edit
permission. Word-doc external approval workflow stays outside ERP;
this is the in-app enforcement.
- New field fp.process.node.is_locked (recipe root)
- write() override blocks non-manager edits when recipe root is_locked
Lock checks via recipe_root_id so child ops/steps are also protected
Manager bypass via group + env.su (sudo) bypass for system jobs
- Amber "LOCKED — Manager Edit Only" ribbon at top of recipe form
- Toggle on Specification & Bake page under "Change Control (NADCAP)"
- Spec doc updated with Decision 6.5 + backlog from client review:
approvals list, doc control auto-sync, oven recorder sync, SOP
word-doc workflow, final-inspection signoff on cert
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add fp.recipe.thickness model (replaces fp.coating.thickness, scoped to recipe root)
- Add spec metadata + bake-relief fields to fusion.plating.process.node (recipe root):
phosphorus_level, thickness_min/max/uom, thickness_option_ids,
requires_bake_relief + bake_window_hours/temperature/duration
- Add recipe_ids M2M + print_on_cert to fusion.plating.customer.spec
- Add applicable_spec_ids reverse M2M as inherit in fusion_plating_quality
(avoids circular dep — core can't reference customer.spec which lives in quality)
- Surface new fields on recipe form ("Specification & Bake" notebook page)
- Surface recipe linkage on customer spec form
Pure additive. Foundation for Phases B-E.
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>
Body Customer row now prints a 3-and-4 short code instead of the
full company name. Operators see "ABC-MANU" on the floor; visiting
customers / unauthorised passers-by can't immediately tell whose
parts are on which rack.
Rule (per user's reference design):
- First 3 chars of first word + "-" + first 4 chars of second word
- Single-word names → just first 3 chars
- All uppercase
- Strips non-alphanumeric per word so "St. John's Mfg." doesn't
leak punctuation into the slice
Logic lives in the shared inner template, so all 4 variants pick
it up automatically:
sale.order External + Internal Sticker
fp.job External + Internal Job Sticker
Verified on fp.job 2635: Customer row now reads "ABC-MANU" (was
"ABC Manufactoring").
Doesn't use the orphaned x_fc_short_code field on res.partner
(that field has no column or compute — broken Studio remnant).
A future spec can replace this inline computation with a proper
stored+inverse field if customers want per-partner overrides.
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>
Same 3-cell + body layout as External; Notes column reads
x_fc_internal_description (Sub 2 internal-description field on the
SO line) instead of line.name. Shop floor gets ops-facing notes
without leaking them to the customer-facing variant.
New action record action_report_fp_so_sticker_internal — binds to
sale.order, appears in the Print menu next to the existing External
sticker. New template report_fp_so_sticker_internal that pre-sets
_notes_content before t-calling the shared inner.
Verified on SO-30019 with a seeded internal_description: Notes
column reads "INTERNAL: rework if any dings on flange. Buff per
WI-104." — confirms the override path is wired through the
defaults-block initialiser, the inner's fallback chain, and the
new outer template.
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>
Inner sticker template gains two parameters that outer templates
pre-set:
_qty_total — total qty for the line/job. Inner wraps the body
in t-foreach="range(int(_qty_total or 1))" so a qty=5 line
produces 5 consecutive single-box stickers. Qty row in the
body switches from "5" to "1 / 5", "2 / 5", ... "5 / 5".
When _qty_total is missing/0/1, the Qty row keeps showing
the plain integer (regression-free).
_notes_content — Notes column source. Existing inner code
hard-read _line.name; new code accepts an outer override
and falls back to _line.name. External outers don't set it
(unchanged behaviour); the new Internal outers (Task 4+5)
pre-set it to x_fc_internal_description.
Defaults template initialises both new vars to False so the
inner's "outer-supplied OR fallback" pattern doesn't NameError
when called from existing outers that haven't been updated yet.
Verified regression-free: fp.job 2635 (qty=1) renders identically
to its pre-Task baseline — Qty row shows plain "1", Notes from
line.name as before.
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>
7 tasks, bite-sized steps with exact code + commands. TDD on the
backend grouping change (new test_so_confirm_splits_by_thickness);
deploy-and-render-PDF on the QWeb template changes. Each task
self-contained, pushes to entech LXC 111 via the standard pct
exec + cat-pipe path, bumps the module version, and commits.
Task 7 is verification-only — creates a multi-line test SO with
two different thicknesses, renders External + Internal stickers
on both the SO and each spawned fp.job, confirms the box loop
and the Notes variant pattern both work.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three problems on the box-sticker stack rolled into one spec:
1. Backend: _create_fp_jobs grouping key collapses lines with
different thicknesses or SNs into one job. Silent compliance
hole. Fix: add thickness_id + serial_id to the key tuple.
2. No per-box stickers: a line with qty=5 prints 1 page showing
"Qty: 5". Want 5 pages with "1 / 5", "2 / 5", ... "5 / 5".
3. No Internal variant: sticker always reads line.name (customer
facing). Want a parallel variant that reads
x_fc_internal_description (Sub 2 internal description field).
Renaming: existing actions keep their XML IDs (bookmarks /
binding_model_id records survive). Labels become:
sale.order: External Sticker + Internal Sticker (new)
fp.job: External Job Sticker + Internal Job Sticker (new)
All three changes share the same inner template, same files —
ship together. No data migration required; existing fp.jobs are
protected by the idempotency guard.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The values were structurally blank because the variable
resolution was reading the wrong field names:
Was: _line.x_fc_serial_number (doesn't exist)
_line.x_fc_thickness (doesn't exist)
Now: _line.x_fc_serial_id.name (M2O fp.serial)
_line.x_fc_thickness_id.display_name (M2O fp.coating.thickness)
Sub 5 shipped these as Many2one registries (fp.serial,
fp.coating.thickness) — the sticker was guessing at flat
Char-field equivalents that were never created.
Verified on SO-30019: SN # now prints "65767", Thickness now
prints "0.3-0.5 mils" (the en-dash in display_name mojibakes
to "â€"" through wkhtmltopdf's font path on entech, so we
replace en-dash + em-dash with ASCII hyphen-minus before
render — ASCII-only is what label printers want anyway).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SO sticker (report_fp_so_sticker):
Was: "SO-30019 / 10" (the "/ 10" was line.sequence — Odoo's
default increment-by-10 — meaningless to the operator)
Now: "SO-30019"
Multi-line SOs are disambiguated by the body fields (Part #,
Customer, etc.) which already differ per sticker, so the
suffix wasn't earning its keep.
Notes column size bumps:
- Label 44pt -> 48pt
- Content 30pt -> 36pt (+20%) — easier to read from across
the line. Line-height tightened 1.15 -> 1.1 to keep the
multi-paragraph wrap inside the body band.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
wkhtmltopdf renders CSS font-size at a smaller physical scale
than the em-square math predicts (a "30pt" cell text was only
~4mm tall visually). Pushing all type up significantly so it
actually reads at scan/print distance:
Text bumps:
- Body field text 30pt -> 50pt (+67%, label + value)
- WO# 56pt -> 72pt (+29%)
- Notes label 30pt -> 44pt
- Notes content 22pt -> 30pt (+36%)
- Muted rev tag 22pt -> 30pt
- Body cell padding 0 10px -> 0 8px (a touch more horizontal
room for long values now that the font is bigger)
QR + 30% as asked:
- Wrapper 280 -> 365px (+30.4%). Image 368 -> 480px, offset
-44 -> -58px (recomputed for the new quiet-zone crop).
Header re-balanced for the bigger content:
- Height 25% -> 32% (fits the +30% QR + bigger WO# + bigger
logo at 135px)
- Body band: 75% -> 68% (rows now ~9.6mm tall; line-height
1.0 keeps the 50pt body text snug inside)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Trimmed the header from 30% to 25% of page height to free up
vertical room for the body band's 7 rows. Each row is now
~10.45mm tall (was 9.88mm), so the field font fits comfortably
at the bigger size.
Size bumps:
- Body field text 26pt -> 30pt (label + value, +15%)
- Muted rev tag 18pt -> 22pt
- Notes label 26pt -> 30pt
- Notes content 19pt -> 22pt (+16%, wraps cleanly to 2 lines
when the customer description runs long)
Header re-fit (smaller cells, same content):
- Header height 30% -> 25%
- WO# font 62pt -> 56pt
- Logo max-height 135 -> 105px
- QR wrapper 340 -> 280px (image 447 -> 368px, offset -53 ->
-44px to keep the quiet-zone crop math right)
- High-def 600x600 QR source unchanged — still prints crisp
at the smaller wrapper size
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
WO# cell now just renders the number (e.g. "WO-30019") since the
"WO" is already baked into the doc index format — the redundant
prefix was eating cell width without adding information.
Size bumps:
- WO# 44pt -> 62pt (text is shorter so the cell can carry the
extra weight)
- Body field text 22pt -> 26pt, line-height 1.1 -> 1.0 so the
bigger font still fits 7 rows in the body band
- Notes label 22pt -> 26pt, content 16pt -> 19pt
- Logo max-height 120 -> 135px
- Muted rev tag 16pt -> 18pt
QR upgrades (both "bigger" and "high def" as asked):
- Source resolution 300x300 -> 600x600. At 300dpi print across
a 28.8mm wrapper, effective output is ~515ppi vs the prior
~256ppi. Scanners on the floor will read it cleanly even at
steeper angles / scuffed labels.
- Wrapper 290 -> 340px (+17%). Image 390 -> 447px, offset -50
-> -53px (recomputed quiet-zone crop: 600 * 0.12 = 72px
margin -> 456px effective QR data -> 340 * 600/456 = 447
scaled image -> (447-340)/2 = 53px offset).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Restores the original ENTECH sticker layout from the operator's
screenshot reference:
Header (3 horizontal cells, divided by vertical rules):
[Logo] | WO #WO-30019 | [QR]
Body (left side = field table, right side = Notes column):
PO #: 587854 | Notes:
SN #: - | <customer-facing description>
Customer: ABC Manufact. |
Part #: 9876... Rev A |
Due Date: May 17, 2026 |
Thickness: - |
Qty: 1 |
Changes from previous (stacked-left) layout:
- Header: 1-row 3-cell (Logo 28% | WO# 44% | QR 28%) replaces
the 2-cell w/ logo+WO# stacked on left.
- Body: 2-region (66% / 34%) replaces single 7-row table.
Notes column now spans full body height on the right.
- Fields: SN # and Thickness added; Process row removed.
- Labels: "PO (RO)" -> "PO #", "Part Number" -> "Part #".
- Notes content: switched from SO.x_fc_internal_note to the SO
line's `name` (= customer-facing description per Sub 2 Q6).
- SN # reads _line.x_fc_serial_number (Sub 5 field).
- Thickness reads _line.x_fc_thickness with coating.thickness
fallback (Sub 5 field, defensive 'in _fields' check).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both changes the operator asked for, applied to the original
ENTECH stacked-left layout (no other structural changes):
- QR wrapper 380px → 460px (image 510px → 620px, offset -65 → -80
to keep the white quiet-zone cropped). Roughly +21% surface area.
- Notes row height 14.28% → 24% (~2x). Other 6 rows shrink
proportionally from 14.28% to 12.67% each so the band still
totals 100%. Notes value also gets white-space: normal +
vertical-align: top so the operator's handwriting room sits at
the top of the cell and a long internal note can wrap.
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>
_clone_subtree() in fp_part_composer_controller built node vals
manually and never copied source.input_ids — so 'Load Template'
copied the recipe tree structure but dropped every custom prompt,
leaving operators with empty data-capture screens. The fix iterates
input_ids and calls .copy({'node_id': new_node.id}) so kind,
target_min/max/unit, compliance_tag, hint, selection_options,
sequence — every field on the input model — flows through.
Verified on entech: ENP-ALUM-BASIC clone now shows all 105 prompts.
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>
write() override raises UserError if name or x_fc_doc_index is in
vals and differs from the stored value (bypass: context flag
fp_allow_name_rename=True for the SO-confirm rename + bulk WO
creation paths). unlink() override raises UserError for records
that have been issued a name; applies to all users including
admins — cancellation must go through the state machine.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hold derives parent via job_id.sale_order_id; RMA via sale_order_id
directly — both get HOLD-<parent> / RMA-<parent> names. NCR and CAPA
have no SO link in core, so they fall back to their legacy sequences
(NCR/YYYY/NNN, CAPA/YYYY/NNN); future modules can override the
_fp_parent_sale_order hook to enable parent naming.
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>
Atomic counter via SELECT FOR UPDATE on the parent SO row. Composes
child names as PREFIX-PARENT (bare for first) or PREFIX-PARENT-NN
(zero-padded 2-digit, then unpadded past 99). Subclasses implement
three hooks: _fp_parent_sale_order, _fp_name_prefix, _fp_parent_counter_field.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Parent sequence starts at 30000. Quote sequence is Q + YYYYMM + non-resetting
counter starting at 200. Phase 1 Task 1 of the parent-number hierarchy.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Quote→SO→WO→IN→CoC→DLV→RCV→… all share a single parent number drawn
from the sale order. New abstract mixin centralises naming with atomic
counter increment, compliance-grade immutability, and a hard block on
direct invoice creation outside the SO workflow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two issues on the Process Tree client action:
1. Back to Work Order kept growing breadcrumbs (WO -> Tree -> WO ->
Tree -> ...) because onBack used action.doAction() which PUSHES
a new act_window onto the stack instead of popping. Fixed by
trying action.restore() first (pops the Tree off the stack and
returns to the parent WO/Step controller). Falls through to
explicit doAction only when there's no parent in the stack
(direct URL access).
2. The empty-state banner referenced productionId, a dead variable
from the bridge_mrp era when the tree was tied to mrp.production.
Since the component now uses jobId (fp.job context key), the
"No manufacturing order selected" message ALWAYS fired regardless
of whether a job was loaded. Fixed by using jobId and updating
the message to "No work order selected".
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>