Adds /fusion_clock/kiosk/nfc/enroll (jsonrpc, auth=user) that validates
the enroll password, normalises the card UID, checks for duplicate
assignments, writes x_fclk_nfc_card_uid, and creates a card_enrollment
activity log entry. 4 new tests; 21 total passing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add _normalize_uid static method to FusionClockNfcKiosk that strips
whitespace, uppercases, removes separators, validates hex-only content,
and reformats to canonical colon-separated pairs; returns None for
empty/invalid input. Covered by 7 new TransactionCase unit tests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extends res.config.settings with 5 NFC kiosk fields (enable toggle,
photo required, enroll password, debug mode, kiosk location via
related company field) and adds the corresponding settings view block
with conditional sub-fields hidden until the kiosk is enabled.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add domain filter on x_fclk_nfc_kiosk_location_id so the dropdown
only shows locations belonging to the current company in multi-company
setups. Replace shared-company mutation in test with a fresh company
to prevent cross-test state leakage.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds x_fclk_nfc_kiosk_location_id (Many2one → fusion.clock.location) to
res.company so each company can designate which NFC kiosk location it uses.
Two tests cover field assignment and default-false behaviour.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move ('nfc_kiosk', 'NFC Kiosk') to sit between kiosk and system in the
source Selection field, matching the spec's semantic grouping of
interactive sources before the automated system source.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add 'nfc_kiosk' to x_fclk_clock_source selection on hr.attendance
- Add x_fclk_check_in_photo and x_fclk_check_out_photo Binary fields (attachment=True)
- Add 'card_enrollment' and 'unknown_card_tap' to activity log log_type selection
- Add 'nfc_kiosk' to activity log source selection
- Add TestNfcAttendanceFields test class (3 tests); all 6 fusion_clock tests pass
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the NFC card UID field (Char, unique, manager-only) that the kiosk
will use to identify employees by card tap. Includes the tests package
with three post-install tests covering write, uniqueness, and nullable
multi-row behaviour.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
20-task TDD plan for the NFC clock kiosk feature spec'd in
2026-05-13-nfc-clock-kiosk-design.md. Bite-sized steps with full code
in each, ordered: data model -> config -> backend endpoints ->
SCSS+template -> JS state machine -> NFC + camera -> Enroll Mode ->
debug shortcut -> version bump.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Design for tap-to-clock NFC kiosk in fusion_clock. Pilot scope: 1
station per company, Samsung Galaxy Tab Active 5 Pro running Web NFC
in Chrome kiosk mode. Reuses Ubiquiti-issued cards. Silent photo
verification via front camera. Backend reuses FusionClockAPI helpers.
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>
- 'Customer Project' plan (renamed from 'Project' to avoid duplicate with
project module's auto-created plan) — mandatory
- 'Department' plan (mandatory) — seeded with DEPT-DEV, DEPT-SALES,
DEPT-ADMIN, DEPT-HOSTING
- 'SR&ED Tag' plan (optional) — seeded with 7 tag values:
SRED-T4-DEV-SALARY, SRED-SPECIFIED-EMPLOYEE,
SRED-CONTRACTOR-CA-ARM-LENGTH, SRED-CONTRACTOR-CA-NON-ARM-LENGTH,
SRED-MATERIALS-CONSUMED, SRED-OVERHEAD-PROXY-BASIS, NOT-ELIGIBLE
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bakes the staging-side one-off collision clearing into the module install
itself so production install will execute the same sweep automatically.
For each of the 29 l10n_ca codes that conflict with Nexa's planned chart:
- If the account has zero postings: suffix code with '.OLD', mark inactive,
rename to '(l10n_ca LEGACY) <original>'
- If the account has postings (currently 115100 AR control with 240 lines
and 511100 Inside Purchases with 1 line): leave alone (Nexa renumbered
to 119100 / 511105 in the XML)
Idempotent — pre_init_hook re-running has no effect (already-suffixed
codes are skipped).
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>
Renumbered to avoid collisions with pre-loaded l10n_ca codes:
- Due From Shareholder/Associated: 115xxx → 119xxx range (115100/115110 already
held l10n_ca AR control accounts with 240 postings)
- Cloud Infrastructure: 511100 → 511105 (511100 was l10n_ca 'Inside Purchases'
with 1 historical posting)
All other 28 colliding l10n_ca codes (118xxx, 213xxx, 214xxx, 221xxx, 311xxx,
411xxx, 413xxx, 511110-511210, 512100-512200, 611100-300, 612xxx) had zero
postings and were cleared in-place by suffixing existing codes with '.OLD'
via a one-off odoo-shell script on staging.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The post_init_hook attempt to set fiscalyear_lock_date=2025-12-31 fails
with RedirectWarning when unreconciled bank statement lines exist in
the period. Catch RedirectWarning/UserError/ValidationError, log a
clear instruction to set the lock manually after reconciliation, and
let install continue.
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>
Bite-sized task plan to implement the CoA design against odoo-nexa
nexamain database. 12 phases:
0. Safety backup + staging clone
1. Module skeleton (nexa_coa_setup)
2. Chart of accounts (~110 new accounts across 1-6xxxxx)
3. Analytic plans (Project, Department, SR&ED Tag)
4. Hooks for archive-unused / rename-legacy
5. Tax cleanup
6. 8 fiscal positions with auto-detect
7. Service product categories
8. Westin/Divine partner records (RP-Associated tag)
9. 8 bank reconciliation rules
10. End-to-end test invoices (ON, US, intercompany)
11. Apply to production (with explicit GO/NO-GO gate)
12. Operating runbook
Each task has a verify-before / change / verify-after / commit cycle.
Staging clone (nexamain_staging) used for every phase before prod.
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>