Commit Graph

646 Commits

Author SHA1 Message Date
gsinghpal
dd88afdf53 feat(jobs): add lifecycle hooks — portal/QC/delivery/invoice (Tasks 2.6-2.9)
- Task 2.6: fp.job.action_confirm auto-creates fusion.plating.portal.job
  with x_fc_job_id back-reference. Idempotent (skip if already linked).
- Task 2.7: fp.job.action_confirm checks customer.x_fc_requires_qc
  and best-effort creates a fusion.plating.quality.check (the model
  lives in bridge_mrp; runtime-detected to avoid dep cycle).
- Task 2.8: fp.job.button_mark_done sets state='done', date_finished,
  auto-creates draft fusion.plating.delivery and best-effort triggers
  fp.certificate generation.
- Task 2.9: account.move.action_post links invoice -> fp.job via SO
  origin lookup, updates portal_job state to complete and stamps
  invoice_ref.

5 new tests cover: portal job creation + idempotency, mark_done state
+ delivery, cancel-then-mark-done blocked.

Best-effort patterns (try/except + runtime model detection) used for
QC + cert because their target models are in dependent modules
that this module doesn't depend on by design.

qc_check_id field on fp.job still deferred — adding it here would
require depending on bridge_mrp.

Manifest 19.0.1.4.0 -> 19.0.1.5.0.

Part of: native job model migration (spec 2026-04-25)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 23:27:38 -04:00
gsinghpal
294cea0e50 feat(jobs): add x_fc_use_native_jobs flag + SO confirm hook (Task 2.5)
Settings flag controls which SO confirm path runs. Default False
keeps the legacy bridge_mrp / mrp.production flow on entech.
Setting True diverts confirm into fp.job creation.

Both hooks coexist — bridge_mrp's _fp_auto_create_mo and the new
_fp_auto_create_job — but only one creates records per SO confirm
(controlled by the flag).

The new _fp_auto_create_job mirrors bridge_mrp's grouping logic
(x_fc_wo_group_tag), recipe resolution (coating → part), and
traceability fields (origin, sale_order_line_ids).

Settings UI shows the flag in a 'Fusion Plating Jobs' app section
of the standard Configuration menu.

3 new tests cover: flag off no-op, flag on creates job, idempotency.

Manifest 19.0.1.3.0 → 19.0.1.4.0.

Part of: native job model migration (spec 2026-04-25)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 23:22:41 -04:00
gsinghpal
3b7eae9b78 feat(jobs): add fp.job._generate_steps_from_recipe (Task 2.4)
Native port of fusion_plating_bridge_mrp's
_generate_workorders_from_recipe method. Walks the recipe tree,
creates one fp.job.step per 'operation' node, formats child 'step'
nodes as step instructions on chatter, respects opt-in/out
overrides from fp.job.node.override.

Adaptations from the original:
- Creates fp.job.step (not mrp.workorder)
- Maps fusion.plating.work.center to fp.work.centre via forward
  link (x_fc_fp_work_centre_id) or code fallback
- Uses native field names (job_id, work_centre_id, etc.)
- Drops work_role_id (not on fp.job.step yet — Task 2.6+)
- Drops _fp_autofill_default_equipment (not yet on step)

5 new tests cover: basic generation, idempotency, no-recipe skip,
opt-in override behaviour, recipe_node_id link.

Manifest 19.0.1.2.0 → 19.0.1.3.0.

Part of: native job model migration (spec 2026-04-25)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 23:17:47 -04:00
gsinghpal
4c68327b9c feat(jobs): add fp.job.node.override for per-job opt-in/out decisions
Mirror of fusion.plating.job.node.override from bridge_mrp, but
bound to fp.job. bridge_mrp's version stays alive for legacy MO
flow during the migration. Both coexist.

Adds override_ids One2many to fp.job via _inherit, plus
unique(job_id, node_id) constraint.

Note: spec-suggested _sql_constraints syntax is deprecated in
Odoo 19 ("Model attribute '_sql_constraints' is no longer
supported, please define model.Constraint on the model"). Used
the new class-attribute form: _unique_job_node = models.Constraint(...).
Verified the UNIQUE index is created on the table.

3 new tests: create, uniqueness, one2many backref.

Manifest 19.0.1.1.1 -> 19.0.1.2.0.

Part of: native job model migration (spec 2026-04-25)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 23:12:53 -04:00
gsinghpal
36b9f30528 refactor(jobs): drop index=True on part_catalog_id for consistency
Code review noted this was asymmetric — only part_catalog_id had
explicit index=True among the 5 new fields, and Phase 1 core fp.job
relies on Odoo's implicit FK btree for ALL Many2ones (no explicit
indexes). Removed for consistency.

Important I2 (no explicit ondelete= policies) is deferred to a
phase-end polish task that addresses both Phase 1 core and Phase 2
extension fields uniformly.

Manifest 19.0.1.1.0 → 19.0.1.1.1.

Part of: native job model migration (spec 2026-04-25)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 23:06:51 -04:00
gsinghpal
6e57b3576c feat(jobs): add cross-module fields to fp.job via _inherit (Task 2.2)
5 of the 6 deferred fields from Phase 1 Task 1.4 land here in
fusion_plating_jobs:
- part_catalog_id (fp.part.catalog from configurator)
- coating_config_id (fp.coating.config from configurator)
- customer_spec_id (fusion.plating.customer.spec from quality)
- portal_job_id (fusion.plating.portal.job from portal)
- delivery_id (fusion.plating.delivery from logistics)

qc_check_id deferred to Task 2.7 — its target model
(fusion.plating.quality.check) still lives in
fusion_plating_bridge_mrp and we don't depend on bridge_mrp from
this module. Task 2.7 will address QC sourcing.

6 unit tests (5 field-presence + 1 integration creating linked
records).

Manifest 19.0.1.0.0 → 19.0.1.1.0.

Part of: native job model migration (spec 2026-04-25)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 23:01:23 -04:00
gsinghpal
4341a03891 feat(jobs): add fusion_plating_jobs module skeleton (Phase 2 Task 2.1)
Empty module that will host the native job bridge during Phase 2
of the migration. Coexists with fusion_plating_bridge_mrp during
the migration period — both can be installed simultaneously
without conflict.

Depends on:
- fusion_plating (fp.job, fp.job.step, fp.work.centre from Phase 1)
- fusion_plating_configurator (fp.part.catalog, fp.coating.config)
- fusion_plating_portal (fusion.plating.portal.job)
- fusion_plating_logistics (fusion.plating.delivery)
- fusion_plating_quality (fusion.plating.customer.spec)
- fusion_plating_certificates (fp.certificate)

These deps are why these fields couldn't live in fusion_plating
core (would invert the dep graph). All cross-module fields on
fp.job and fp.job.step land here via _inherit in subsequent tasks.

auto_install=False — opt-in only.

Part of: native job model migration (spec 2026-04-25)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 22:55:04 -04:00
gsinghpal
d1aa7a81e0 docs(jobs): detail Phase 2 task breakdown — parallel module strategy
Phase 2 was previously outlined as 'rename bridge_mrp → jobs'.
That's destructive on entech. Revised strategy: build
fusion_plating_jobs IN PARALLEL with bridge_mrp. A settings flag
(x_fc_use_native_jobs) controls which path SO confirm takes.
Default False = legacy MO flow stays. Cutover (Phase 9) flips
the flag.

Phase 2 breakdown into 11 tasks (2.1–2.11), totaling ~5 days
engineering. All preserve bridge_mrp untouched until cutover.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 22:49:34 -04:00
gsinghpal
1491f2367b refactor(jobs): address code review feedback on Task 1.8 admin views
- I1: Lock time_log_ids list inside step Audit tab to read-only
  (no create/edit/delete on the nested list). Audit timelog rows
  are produced exclusively by button_start / button_finish; if a
  manager could hand-edit them, cost_total rollups would silently
  drift.
- I2: Add explicit list view (decoration on state) and search view
  (filters by state/kind, group_by state/work_centre/job) for
  fp.job.step. The Steps (Admin) menu was using Odoo's default
  auto-list with no filter, which would be unusable after a few
  weeks of step accumulation. Action now references the search
  view explicitly.

Manifest 19.0.8.7.0 -> 19.0.8.7.1.

Part of: native job model migration (spec 2026-04-25)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 22:44:16 -04:00
gsinghpal
b2ae79b61f feat(jobs): add admin views and menu for Phase 1 models
Manager-only views during Phase 1 — operator UI rebuilt in Phase 6.
Top-level menu 'Plating Jobs (new)' (seq=47) groups the three
new act_window actions (Jobs, Steps Admin, Work Centres) so the
foundational models can be exercised through the UI without
touching the operator-facing menus that still serve mrp.production
and mrp.workorder.

Job form has Steps/Source/Costs notebook tabs. Step form has
Equipment/Plating Spec/Audit/Instructions tabs (Audit shows
the time log rows from Task 1.7). Search filters by state,
priority, partner, facility.

Manifest 19.0.8.6.1 → 19.0.8.7.0.

Part of: native job model migration (spec 2026-04-25)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 22:38:36 -04:00
gsinghpal
54068b3d18 refactor(jobs): address code review feedback on fp.job.step.timelog (Task 1.7)
- I1: Add supervisor ACL row on fp.job.step.timelog. The other
  job-related models all have a 3-tier (operator/supervisor/
  manager) pattern; timelog had been operator+manager only with
  no comment explaining why. Adding the supervisor row for
  consistency — supervisors can write timelogs the same as
  operators.
- I2: Capture fields.Datetime.now() once in button_start and
  reuse for both the step's first-start audit timestamp and the
  new timelog's date_started. Mirrors button_finish's pattern.
  Eliminates microsecond drift between two related timestamps.
- M6: Update stale comment in fp_job_step.py that said
  'duration_actual will be sum of timelog rows once Task 1.7
  lands' — Task 1.7 has landed; replace with current behaviour.

Manifest 19.0.8.6.0 → 19.0.8.6.1.

Part of: native job model migration (spec 2026-04-25)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 22:34:10 -04:00
gsinghpal
28892f56b5 feat(jobs): add fp.job.step.timelog for granular timer tracking
Each button_start opens a fresh timelog row; button_finish closes
the open row and recomputes step.duration_actual as the sum of all
interval durations. Replicates Odoo MRP's mrp.workorder.time_ids
granularity natively (no mrp dep).

Schema: step_id (M2O cascade), user_id, date_started,
date_finished, duration_minutes (computed, stored).

ACLs: operator get create permission on timelogs because
button_start creates them.

Tests: test_start_creates_timelog (asserts the log row exists,
date_finished is False, user_id is the current user) and
test_finish_closes_timelog (asserts log gets date_finished, has a
non-negative duration, and step.duration_actual matches).

Manifest 19.0.8.5.1 -> 19.0.8.6.0.

Part of: native job model migration (spec 2026-04-25)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 22:27:26 -04:00
gsinghpal
57a3aea16f refactor(jobs): address code review feedback on fp.job.step (Task 1.6)
- I1: Replace 'Task 1.6' markers in stub method comments and
  NotImplementedError messages with forward-looking phrasing.
  Task 1.6 is what just shipped (the field expansion); the action
  stubs are deferred to an unspecified future task. Stale markers
  would have confused future readers/operators.
- I2: Add test_cost_total_recomputes_when_rate_changes — insurance
  test that verifies @api.depends('cost_per_hour') triggers through
  the related-from-work_centre chain. Catches future Odoo upgrades
  that break related-depends.

Manifest 19.0.8.5.0 → 19.0.8.5.1.

Part of: native job model migration (spec 2026-04-25)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 22:22:15 -04:00
gsinghpal
91767f9f03 feat(jobs): add equipment, audit, plating-spec fields to fp.job.step
Equipment: bath_id, tank_id, rack_id (all in core). oven_id deferred
to a bridge module — fusion.plating.bake.oven lives in shopfloor and
core can't depend on it. masking_material_id deferred too — model
fusion.plating.masking.material does not yet exist anywhere; will be
added when the masking model lands.

Audit: signoff_user_id (readonly), facility_id (related from
work_centre_id, stored).

Plating spec: thickness_target, thickness_uom (um/mil/in),
dwell_time_minutes, bake_setpoint_temp, bake_actual_duration,
bake_chart_recorder_ref (Nadcap audit trail).

Recipe-related: requires_signoff, auto_complete, is_manual,
customer_visible (all related from recipe_node_id, stored, so
operator sees current values without re-querying process.node).

Cost rollup: cost_per_hour related from work_centre_id, cost_total
computed (duration_actual / 60 x rate), currency_id related too.
Full rollup-from-timelogs lands in Task 1.7.

Tests cover: facility_id related-field, thickness_uom default,
cost_total zero/non-zero paths.

Manifest 19.0.8.4.1 -> 19.0.8.5.0.

Part of: native job model migration (spec 2026-04-25)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 22:15:26 -04:00
gsinghpal
688fe8317c refactor(jobs): address code review feedback on fp.job.step (Task 1.5)
- I2: Add TODO comment block + stub button_pause/button_skip/
  button_cancel that raise NotImplementedError. Makes the missing
  state-machine paths explicit instead of invisible gaps. Future
  Task 1.6 wires the real implementations; shop-floor buttons in
  Task 1.8 can already point to the right method names.
- I3: button_finish now preserves first-finish audit timestamp
  via 'if not step.date_finished:' guard, mirroring button_start.
  Future rework flow that re-opens a step won't lose the original
  finish data. The duration_actual rollup landing in Task 1.7 will
  use timelog rows for the most-recent interval if needed.
- I4: step_count and step_done_count are now store=True so list
  views and stat buttons (Task 1.8) don't recompute across all
  job rows on every render. step_progress_pct and current_step_id
  stay non-stored - they're cheap derivatives. Split compute methods
  so stored + non-stored fields don't share one method (Odoo flags
  the mix as inconsistent and recomputes stored fields whenever the
  non-stored one is read, defeating the perf gain).

Manifest 19.0.8.4.0 -> 19.0.8.4.1.

Part of: native job model migration (spec 2026-04-25)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 22:09:16 -04:00
gsinghpal
c41a488b58 feat(jobs): add fp.job.step model with state machine
Per-operation model replacing mrp.workorder for plating. Each step
instantiates from a recipe operation node (recipe_node_id link).
Container/step nodes from the recipe template are rendered at view
time via that link — they don't get rows here. See spec §5.2
Option A.

7-state machine: pending → ready → in_progress → done, plus
paused, skipped, cancelled. button_start/button_finish enforce
the transitions.

Job header gets step_ids + step_count, step_done_count,
step_progress_pct, current_step_id (computed from steps).

Equipment, audit fields, plating-spec fields, time logs, and
release-ready validation come in Tasks 1.6 and 1.7.

Manifest 19.0.8.3.1 → 19.0.8.4.0.

Part of: native job model migration (spec 2026-04-25)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 21:59:07 -04:00
gsinghpal
f7a4cba5a8 docs(jobs): split fp.job §5.1 fields by module ownership (Task 1.4)
Originally Task 1.4 was to add all spec §5.1 extension fields to
fp.job in core. The dependency-graph audit during implementation
revealed that 6 of those fields point to models in dependent
modules (configurator, quality, portal, logistics, bridge_mrp).
Adding them in core would invert the dependency graph.

Spec §5.1 now has a Module column. Core-safe fields stay in
fusion_plating/models/fp_job.py; cross-module fields are deferred
to their owning modules via _inherit = 'fp.job' in Phase 2.

Plan Task 1.4 narrative updated to reflect the reduced scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 21:54:35 -04:00
gsinghpal
e4111ad000 refactor(jobs): address code review feedback on fp.job (Task 1.4)
- Move NOTE comment from between @api.depends and the function
  into a proper docstring on _compute_costs.
- Make currency_id required=True (default still in place — only
  a programmatic write would null it, but Monetary fields need a
  non-null anchor).
- Add test_current_location_for_confirmed to cover the title-case
  fallback branch in _compute_current_location.

Manifest 19.0.8.3.0 → 19.0.8.3.1.

Part of: native job model migration (spec 2026-04-25)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 21:51:23 -04:00
gsinghpal
335dc2488e feat(jobs): add core-safe extension fields to fp.job
Scope was reduced from spec's full §5.1 list because 6 of the 10
plating-specific fields point to models in dependent modules
(configurator, quality, portal, logistics, bridge_mrp). Adding
those Many2ones in core would invert the dependency graph. They
move to their owning modules via _inherit = 'fp.job' and get
re-bundled by fusion_plating_jobs in Phase 2.

This commit lands the core-safe subset:
- sale_order_line_ids (sale_management is in core depends)
- recipe_id, start_at_node_id (process.node is in core)
- invoice_ids (account is reachable via sale_management → sale)
- Cost rollup: quoted_revenue / actual_cost / margin / margin_pct
  with placeholder compute (actual_cost = 0 until Task 1.5 wires
  fp.job.step.cost_total)
- current_location stub (full Bath/Oven rendering in Task 1.6)

Tests cover the cost-rollup math and the current_location stub.
Spec §5.1 has been re-tabulated with explicit 'Module' column.

Manifest 19.0.8.2.1 → 19.0.8.3.0.

Part of: native job model migration (spec 2026-04-25)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 21:44:28 -04:00
gsinghpal
b45a134aa4 refactor(jobs): address code review feedback on fp.job
- Sequence: add noupdate="1" to fp_job_sequences.xml. Without it,
  every module update resets number_next to 1, corrupting the live
  job-number stream. Matches fp_sequence_data.xml convention.
- action_cancel now raises UserError on an already-cancelled job
  instead of silently rewriting state. Audit-grade traceability
  expects explicit failures.
- Added TODO marker for action_hold / action_resume /
  action_revert_to_confirmed so future authors don't bypass the
  state-machine guards.
- Tests: added cannot_cancel_done (covers the dead-code UserError
  branch) and cannot_cancel_already_cancelled.

Manifest version bumped 19.0.8.2.0 -> 19.0.8.2.1.

Part of: native job model migration (spec 2026-04-25)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 21:36:58 -04:00
gsinghpal
93e0be4b48 docs(jobs): tighten spec/plan after Task 1.2 review
- Spec §5.3: document that default_oven_id is deferred to the
  fusion_plating_jobs bridge module (fusion.plating.bake.oven lives
  in shopfloor; core can't depend on it).
- Plan: align ACL blocks for Tasks 1.2/1.3/1.5/1.7 to use
  group_fusion_plating_operator for the lowest tier instead of
  base.group_user. Caught by the code-quality reviewer on Task 1.2;
  this prevents the same bug recurring in later tasks.
- Plan Task 1.2 test name corrected:
  test_facility_required_for_active_centre →
  test_facility_optional_at_create.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 21:30:23 -04:00
gsinghpal
26928713d5 feat(jobs): add fp.job native model with state machine
Header model replacing mrp.production. mail.thread for chatter,
priority/state/deadline tracking, sequence WH/JOB/00001+. Tests
cover create, confirm, cancel, and forbidden double-confirm.

State machine:
  draft -> confirmed -> in_progress -> done
              v                          ^
          cancelled              (rework reverts here)
  on_hold can be entered from confirmed or in_progress.

Step relations come in Task 1.5; SO/recipe/portal/cost extension
fields come in Task 1.4.

Part of: native job model migration (spec 2026-04-25)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 21:29:36 -04:00
gsinghpal
5970dfe57b refactor(jobs): address code review feedback on fp.work.centre
- ACL: use group_fusion_plating_operator for read-only tier instead of
  base.group_user, matching the established hierarchy in the rest of
  ir.model.access.csv
- Test: rename test_facility_required_for_active_centre →
  test_facility_optional_at_create. Old name claimed the opposite of
  what the assertion checks.
- Manifest version bumped 19.0.8.1.0 → 19.0.8.1.1.

Part of: native job model migration (spec 2026-04-25)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 21:19:59 -04:00
gsinghpal
37f917824a feat(jobs): add fp.work.centre native model
Replaces mrp.workcenter for plating. Domain-specific kinds
(wet_line/bake/mask/rack/inspect/other) drive release-ready
validation on steps. ACLs follow existing supervisor/manager
hierarchy.

Note: spec listed default_oven_id Many2one to fusion.plating.oven
but that model does not exist in core (the bake-oven model
fusion.plating.bake.oven lives in fusion_plating_shopfloor, which
core cannot depend on). Field omitted; the bridge that adds
fp.job/fp.job.step can re-introduce it via _inherit later.

Part of: native job model migration (spec 2026-04-25)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 21:12:32 -04:00
gsinghpal
41d0908ade changes 2026-04-24 21:04:38 -04:00
gsinghpal
0eab4b4efb fix(reports): sticker PDF KeyError: 'quote' on fusion_pdf_preview path
The QR <img> src used `% quote(_scan_url)` to URL-encode the value,
but `quote` isn't always in the QWeb render context — particularly
on the fusion_pdf_preview / account render chains. Result:
KeyError: 'quote' when printing from the browser.

_scan_url is always base_url + '/fp/wo/<int>' — no characters that
need encoding. Replaced the % + quote() formatting with simple
string concatenation, dropping the quote dependency entirely.

Smoke verified on entech: MO + WO stickers render cleanly at 27KB
each (with QR image included).

fusion_plating_reports → 19.0.7.2.0

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 10:58:24 -04:00
gsinghpal
e32ff4b056 feat(reports): MO-bound WO sticker + polished professional layout
User reported two issues with the sticker:

1. "Print → WO Box Sticker" didn't appear on the MO form
   (WH/MO/00067). The operator workflow lives on the MO form, not
   the WO — binding only to mrp.workorder meant they couldn't see
   the option. Now bound to BOTH:
     * mrp.workorder (per-WO sticker)
     * mrp.production (per-MO sticker — prints the MO friendly
       name after "WO #" so it reads naturally in shop-floor
       vocabulary)
   Internal refactor: factored the layout into a shared inner
   template report_fp_wo_sticker_inner; the two outer templates
   normalise their input to the same _order_id / _scan_id / _mo
   variables and t-call the inner.

2. Design polish. The previous layout was a plain label/value
   table that looked rough. Redesigned with:
     * Proper sticker chrome: 0.5mm black border, 1.5mm rounded
       corners, edge padding.
     * Header row with bottom border rule separating logo+WO-# on
       the left from QR+caption on the right.
     * Grid rows now alternate white / #f4f5f7 zebra-striping with
       a right-aligned vertical rule between label and value.
     * ALL-CAPS, letter-spaced, gray-333 labels at 7.5pt; values
       at 8.5pt with strong (9.5pt, 700) emphasis on the key data
       (PO, Part Number, Qty) so it reads at a glance from across
       the warehouse.
     * Helvetica Neue font stack.
     * "SCAN TO OPEN" caption under the QR.

Scan endpoint updated: /fp/wo/<id> now tries mrp.production first
(operator home form) then falls back to mrp.workorder. Numeric
collisions between the two id spaces are possible; MO wins because
the MO view carries the full context.

fusion_plating_reports → 19.0.7.1.0

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 10:52:22 -04:00
gsinghpal
be33a76ad2 feat(reports): WO box sticker + QR-scan-to-WO endpoint
Client is migrating from Steelhead and needs to keep the small
parts-box sticker format the warehouse crew already knows. Two
pieces shipped together so scanning is seamless from day one:

1. report_fp_wo_sticker — 4x3" QWeb label bound to mrp.workorder.
   Layout mirrors the Steelhead sticker:
     * ENTECH logo top-left (via env.company.logo)
     * QR code top-right encoding /fp/wo/<id>
     * Grid: PO (RO) / Customer / Process / Part Number / Due
       Date / Qty / Notes
   Dedicated paperformat_fp_wo_sticker at 102x76mm, 300 DPI,
   landscape, 3mm margins — sized for thermal / inkjet label
   printers without shrink-to-fit.
   Binding added so "Print → WO Box Sticker" appears on every
   mrp.workorder record.

2. FpWoScanController — GET /fp/wo/<int:wo_id> redirects the
   scanner straight to the work-order form
   (/odoo/action-mrp.action_mrp_workorder/<id>). auth='user' so
   logged-in scanners land on the WO immediately; others bounce
   through Odoo's login and return to the same URL. No custom
   client work needed — any phone camera, handheld barcode
   scanner, or tablet browser opens the URL on scan.

Process row resolution chain: part.default_process_id →
coating.recipe_id → fallback. So the sticker prints whichever
process is actually going to drive WO generation for this line,
matching the direct-order wizard's Effective Process column.

fusion_plating_reports → 19.0.7.0.0

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 10:39:35 -04:00
gsinghpal
eddf803d4c feat(reports): split customer-facing line header into Part Number + Description columns
User request: on customer-facing PDFs the single "Part" column was
stacking part number + revision + description together. Split into
two distinct columns so customers see Part Number in its own field
and the Description column carries only the prose.

Macro split:
  * customer_line_part_number — strong part number + "(Rev X)"
  * customer_line_description  — line.name + any populated line
    metadata (serial, job#, thickness)
  * customer_line_header (legacy) kept as a thin wrapper that
    t-calls both macros stacked so older reports still render.

Reports updated — each gains a new first "PART NUMBER" column and
the old "PART" column renamed to "DESCRIPTION":
  * report_fp_sale (both portrait variants)
  * report_fp_invoice (both portrait variants)
  * report_fp_packing_slip (both variants — delivery + MO-origin)

Column widths rebalanced per report. Section / note colspans bumped
to account for the extra column.

report_fp_bol left as-is — its "Description of Goods" td is a
freight-convention combined field, intentionally keeps the stacked
layout via the legacy customer_line_header macro.

fusion_plating_reports → 19.0.6.0.0

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 10:17:56 -04:00
gsinghpal
8142bd229a feat(configurator): "PO Pending" escape hatch for customers who send PO later
Customer feedback: some customers don't send their PO with the
initial order — they send it days or weeks later. The system was
blocking SO confirmation without a PO, which forced the shop to
either wait on paperwork or ask a manager for a formal override.

New estimator-level path: a "PO Pending" boolean on sale.order +
an optional "PO Expected By" date.

  * Confirm without a PO# / PO document when PO Pending is ticked.
  * action_confirm skips the hard error if po_pending OR po_override
    is set (keeps the existing manager-override path too).
  * On confirm with PO Pending, the system schedules a chase
    activity for po_expected_date (or +3 days if blank), assigned
    via mail.activity so it shows up in the sales user's activity
    list. Chatter note logged so audit is obvious.
  * Direct-order wizard: po_number and po_attachment_file become
    optional. Ticking "PO Pending" in the wizard is the trade-in;
    a help note under the toggle explains the chase behaviour.
  * Once the PO arrives, user fills in the PO# / uploads the doc,
    and turns PO Pending off — existing downstream flow resumes.

Difference from x_fc_po_override (kept):
  * PO Override = manager waiver, permanent ("handshake deal").
  * PO Pending = estimator flag, time-boxed ("customer will send it
    by Friday").

fusion_plating_configurator → 19.0.14.0.0
fusion_plating_invoicing    → 19.0.3.0.0

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 10:08:00 -04:00
gsinghpal
2d3ee03f86 fix(configurator): widen direct-order wizard via Odoo's dialog_size context
Second attempt at widening the wizard modal after the :has() CSS
approach broke layout. This time using Odoo's built-in
dialog_size mechanism — just drop 'dialog_size': 'extra-large'
into the action's context and Odoo applies its own modal-xl
class, sized for wide content. About 30% wider than the default,
which gives the 10+ line columns breathing room without any
custom CSS.

fusion_plating_configurator → 19.0.13.6.0

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 10:01:00 -04:00
gsinghpal
d9526dc050 revert: drop fp_direct_order_wizard.scss — :has() approach broke the modal
Reverting the previous wizard-width attempt. The
`.modal-dialog:has(.o_fp_direct_order_wizard)` selector combined
with the fixed width override produced broken layout (modal no
longer rendering correctly).

Better path for widening this wizard is still TBD — the built-in
maximize icon in the modal header already lets users go full-
screen as a workaround until a safe width override lands.

fusion_plating_configurator → 19.0.13.5.0

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 09:58:56 -04:00
gsinghpal
470c44ee5d fix(configurator): widen direct-order wizard modal so line columns breathe
User complaint: the direct-order wizard's line table was crammed —
"Primary Treatment" showed as "Primary T..." and "Serial Number"
as "Serial Nu..." at the default Odoo modal width (~992px).

Added a dedicated stylesheet that targets the wizard via a new
o_fp_direct_order_wizard class on the form element. The modal
dialog containing the wizard now opens at min(1600px, 95vw) —
wide enough for the 10+ columns (Part, Primary Treatment, Process,
Thickness, Serial Number, Qty, Unit Price, Subtotal, Part Deadline,
WO Group) to fit without truncation on typical desktop displays,
while still adapting to narrower screens.

Also nudged per-cell horizontal padding from Bootstrap's default
to 10px left/right so the columns have a bit more breathing room
inside the wider table.

fusion_plating_configurator → 19.0.13.4.0

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 09:56:33 -04:00
gsinghpal
488b0655a7 fix(configurator): wizard gap between "Add From" buttons + force view reload
Two small fixes on the direct-order wizard:

1. User reported the Process column from the previous commit didn't
   appear on the line list. Root cause: module view definitions
   only reload on -u, not on systemctl restart. The earlier ship
   bumped the version + ran -u, but the Process column inside the
   list view required a fresh view load. Re-running -u now resolves
   it (no code change needed for that field).

2. The "+ Add From Prior SO" and "+ Add From Quotes" buttons were
   rendering edge-to-edge because their container div used the
   default Bootstrap flow (margin-right: 0). Swapped the wrapping
   div class from `mb-2` to `mb-2 d-flex gap-2` so Bootstrap lays
   the two buttons side-by-side with a consistent 0.5rem gap.

fusion_plating_configurator → 19.0.13.3.0

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 09:25:56 -04:00
gsinghpal
381a750902 feat(configurator): direct-order line shows effective Process alongside Treatment
User asked why the direct-order wizard only showed "Primary
Treatment" and not Process — aren't they the same? They're not,
but the distinction was invisible on the order line until now.

Mental model (preserved here to keep future decisions aligned with
the user's question):

  * Primary Treatment (fp.coating.config) = "WHAT coating" (process
    type, thickness range, spec reference — the contractual
    deliverable).
  * Process (fusion.plating.process.node tree) = "HOW we make it"
    (the floor-level sequence of operations and steps that WO
    generation turns into work).

  Each coating carries a default process (recipe_id). Parts can
  override that via the Process Composer, storing a part-scoped
  clone (default_process_id on fp.part.catalog). Resolution:
  part's composed process wins; coating default is the fallback.

Added a computed read-only `effective_process_id` field on
fp.direct.order.line that displays exactly what process will drive
WO generation for the line, plus a one-line `effective_process_source`
showing whether it came from the part ("customised") or the
coating ("default"). Both surfaced on the wizard list and form so
the estimator can verify before confirming the order.

No behaviour change — this is pure visibility. WO generation still
uses the same resolution chain it did before.

fusion_plating_configurator → 19.0.13.2.0

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 09:20:40 -04:00
gsinghpal
625f6560f1 feat(plating): split templates vs part-scoped processes + Process smart button
Two user-reported gaps:

1. The Process Recipes list was about to be flooded by part-scoped
   clones as soon as parts started carrying customised processes —
   thousands of clones would bury the handful of real shared
   templates (General Processing, Anodize, etc.).

   Fix: the main Process Recipes action now narrows to
   part_catalog_id = False so shared templates stay alone in that
   view. A sibling menu "Part Processes" (Plating → Operations →
   Part Processes) shows the inverse list — every part-cloned
   process, grouped by part — so admins can audit clones without
   cluttering the templates list.

   Search view gains two toggle filters (Shared Templates /
   Part-Scoped) and a "Group by Part" option. The node list gains
   an optional "Part" column.

   Split across modules: core owns the base search / tree / action
   (unchanged); configurator owns all the part_catalog_id-dependent
   pieces (filter extensions, list column, narrower domain,
   "Part Processes" action + menu). Keeps the dependency direction
   clean — configurator always depends on core, never the other way.

2. Added a "Process" smart button to the part form's button box.
   Shows either a green check (composed) or "None" (not yet
   composed) and opens the part-scoped Composer on click. Gives
   users one-tap access from any part form without hunting through
   the Process tab.

fusion_plating → 19.0.8.0.0
fusion_plating_configurator → 19.0.13.1.0

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 08:45:24 -04:00
gsinghpal
acb406950f feat(configurator): part-scoped process name shows part id + revision
User feedback: the Process tab on fp.part.catalog was displaying a
bare template name ("General Processing"), making it impossible to
tell at a glance that the clone belonged to this specific part.

Root clone now inherits the template name with a part-identifier
suffix appended:
  "General Processing — 1234567 Rev 2"

Only the ROOT gets the suffix — child nodes keep their clean source
names so the tree-editor canvas doesn't get cluttered.

Suffix logic avoids doubling "Rev": if part.revision is already
prefixed with "Rev " (e.g. "Rev 2"), we don't prepend another one.

Post-upgrade hook backfills existing part-cloned roots that
pre-date this change so users see the new format without having
to re-compose (which would otherwise wipe their customisations).

fusion_plating_configurator → 19.0.13.0.0

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 08:33:07 -04:00
gsinghpal
274236d34c fix(plating): rename opt_in_out labels — 'Required' replaces 'Disabled'
User feedback: "Disabled" was confusing; it looked like the step
was turned off entirely when in fact the semantic meant "every job
runs this, cannot be removed". Aligning labels with Steelhead's
own terminology and the mental model the shop floor already uses:

  Python Selection values (unchanged: disabled / opt_out / opt_in)
    disabled  →  "Required"
    opt_out   →  "Opt-Out (included by default, can be removed per job)"
    opt_in    →  "Opt-In (excluded by default, can be added per job)"

  Tree-editor side panel inline labels match, plus a short helper
  line under the dropdown:
    "Required — every job runs this step.
     Opt-Out — ships included, estimator can remove per job.
     Opt-In — ships excluded, estimator can add per job."

Field string also flipped from "Opt In/Out" to "Step Usage" — the
new header reads closer to what the field actually controls.

Column order also flipped so the Opt-Out option appears above
Opt-In — matches the frequency in real recipes (most optional
steps are included by default and sometimes skipped, not the other
way around).

fusion_plating → 19.0.7.4.0

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 08:24:57 -04:00
gsinghpal
7629af4e00 fix(plating): tree editor — expand newly imported nodes + Expand/Collapse All
Two related gaps:

1. loadTree() only auto-expanded nodes when expandedNodes was
   completely empty — that was "first-load only" behaviour. After
   an import, the subsequent loadTree() found the map populated
   and skipped the expand pass entirely, so every freshly imported
   node came in collapsed ("2 hidden"). Users had to click each
   one open by hand.

   Fix: iterate the whole tree on every load and default any node
   whose id isn't yet in expandedNodes to true. Nodes the user
   explicitly collapsed stay that way (their id IS in the map,
   set to false).

2. Added "Expand all" and "Collapse all" buttons to the tree-
   editor header so operators can get a full view (or a tight
   overview) without clicking node by node. Collapse all keeps
   the recipe root expanded — otherwise the canvas goes blank.

fusion_plating → 19.0.7.3.0

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 08:20:39 -04:00
gsinghpal
9d7b7daf5a feat(plating): tree-editor import supports insert-before position
The import feature appended every imported node to the end of the
target recipe. That's wrong for the common case — General Processing
has Shipping as its last operation, so importing an Electroless
Nickel pack should land BEFORE Shipping, not after it. The user
would otherwise have to click Move Up dozens of times.

Controller: /fp/recipe/node/import_children now accepts
insert_before_id:
  null/missing  → append at end (default, unchanged)
  0             → insert at the start
  <positive id> → insert right before that top-level child

Implementation reorders target's top-level children in one pass
after Phase 1 creates the copies (placeholder sequence=0). Phase 2
splits existing vs. new, finds the anchor index in the existing
list, and reassigns sequences 10/20/30/... across the merged list.
Collisions on the old max_seq-based append strategy are eliminated.

JS: state.importInsertBefore drives a new "Insert:" dropdown in the
toolbar with options:
  — At the end — (default)
  — At the start —
  Before <each top-level child name>

Smoke on entech (3-case): insert-before-middle, insert-at-start,
insert-at-end all produce the expected ordering.

fusion_plating → 19.0.7.2.0

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 08:16:05 -04:00
gsinghpal
03f41422de fix(plating): tree editor — title wrapping + import hierarchy
Two bugs reported on the tree editor after the move/import feature
shipped:

1. Card titles truncated to "Contrac…" because .o_fp_re_title had
   white-space: nowrap + text-overflow: ellipsis. Swapped to
   white-space: normal + overflow-wrap: anywhere so long names
   wrap onto multiple lines inside the card. Widened card max-
   width 380→460px and bumped min-width 240→260px so wrapped
   titles have room.

2. Import-children was flattening the tree — all operations AND
   their step children landed at the top level instead of staying
   nested under their operations.

   Root cause: src_node.copy({'parent_id': new_parent.id, ...})
   on a _parent_store model behaved unpredictably — in some runs
   the override in copy_vals didn't stick and child recursion
   ended up with a wrong parent_id. Rewrote _copy_subtree to use
   copy_data() + Node.create() so parent_id is set explicitly and
   child_ids / parent_path are stripped (we recurse ourselves).

   Smoke verified on entech: General Processing (1 root + 5 ops
   + 7 steps = 13 nodes) imports with hierarchy bit-identical to
   source.

fusion_plating → 19.0.7.1.0

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 08:08:55 -04:00
gsinghpal
8853cdd0c6 feat(plating): tree editor — move up/down buttons + import from recipe
Two gaps closed on the recipe tree editor based on user feedback:

1. Explicit Move Up / Move Down buttons on every non-recipe node
   row, driven by a new POST /fp/recipe/node/move_sibling endpoint.
   DnD already existed but couldn't reliably move a node above a
   sibling when the drop zone overlapped the node being dragged.
   The button-based flow sidesteps that entirely and makes "nudge
   one slot" a single click.

2. Inline "Import from recipe" toolbar that appears when the
   Import button is clicked in the header. User picks a source
   recipe from a dropdown (POST /fp/recipe/list, excludes the
   current one), toggles "Skip duplicate names", and clicks
   Import. POST /fp/recipe/node/import_children deep-copies every
   top-level child of the source under the current recipe,
   preserving the sub-tree structure. Dedupe is on by default so
   re-running the import on an already-populated recipe is a
   no-op; users who want to merge identical-named branches can
   untick the checkbox.

Controller endpoints:
- /fp/recipe/list (list recipe roots for the picker)
- /fp/recipe/node/move_sibling (swap with neighbour by direction)
- /fp/recipe/node/import_children (deep-copy subtree with dedupe)

Smoke verified on entech: 5-child recipe imported cleanly, dedupe
blocks re-import, sequence swap works.

fusion_plating → 19.0.7.0.0

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 08:00:24 -04:00
gsinghpal
c76bbd85eb fix(plating): Process Composer/Editor breadcrumb accumulation
Two separate issues were stacking up on the breadcrumb trail:

1. The composer was launching the tree editor with the name
   "Process Composer — …" — identical to its own label. The
   breadcrumb trail ended up showing "Process Composer / Process
   Composer" because both pages claimed the same name. Renamed the
   tree-editor instance to "Process Editor — …" so the two pages
   read distinctly.

2. Every "Back to part" click pushed a new entry onto the
   breadcrumb stack instead of resetting. After one round-trip the
   trail looked like "… / Composer / Editor / Part"; after two it
   was "… / Composer / Editor / Part / Composer / Editor / Part".
   Added clearBreadcrumbs: true to both back paths (composer's
   backToPart and tree-editor's onBackToList) so a RETURN action
   actually resets the stack.

fusion_plating → 19.0.6.2.0
fusion_plating_configurator → 19.0.12.4.0

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 07:48:38 -04:00
gsinghpal
7d44af7d77 fix(plating): tree-editor back button honours part-scoped context
When the Recipe Tree Editor is opened from the part-scoped Process
Composer (Sub 3), the composer already passes part_id via the
action context. The editor was ignoring it and routing onBackToList
to the generic Recipes list, stranding the user away from the part
they came from.

Capture ctx.part_id at onMounted, expose a state.fromPart flag, and
branch onBackToList: if part_id is set, open the fp.part.catalog
form; otherwise keep the current Recipes-list behaviour. XML button
label flips "Recipes" → "Part" accordingly so the user knows where
the button will take them.

fusion_plating → 19.0.6.1.0

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 07:45:12 -04:00
gsinghpal
33e35fd213 fix(configurator): centre Process Editor button icon + make icon white
- Icon + label now share an inline-flex row with align-items: center,
  so both sit on the button's vertical midline regardless of line-
  height inheritance.
- Dedicated gap (10px) replaces the me-2 utility so the _hint block's
  .fa { margin-bottom: 16px } can't bleed in and push the icon off.
- Icon colour forced to #ffffff to match the button's white label —
  contrast against btn-primary green is now clean.

fusion_plating_configurator → 19.0.12.3.0

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 07:41:24 -04:00
gsinghpal
18f45d50f5 fix(configurator): Process Composer tree-editor button polish
- Icon: fa-edit → fa-sitemap (pencil suggests inline editing; the
  button actually navigates to a separate tree editor — sitemap
  matches the intent).
- Icon spacing: swap the fragile leading-space-in-span for the
  Bootstrap me-2 utility so the icon-to-text gap is consistent.
- Label: "Open Tree Editor" → "Open Process Editor" to match the
  page title ("Process Composer") and the menu vocabulary the rest
  of the product uses.

fusion_plating_configurator → 19.0.12.2.0

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 07:37:35 -04:00
gsinghpal
d0eefe6392 fix(configurator): Process Composer dark-mode palette (Sub 3 follow-up)
Process Composer was rendering a white card background on dark-mode
tenants because the SCSS relied on var(--bs-body-bg, #ffffff) — the
Bootstrap CSS variable doesn't flip reliably in Odoo 19's backend,
and the hardcoded fallback kicked in.

Rewrote with the shopfloor token pattern from CLAUDE.md: branch on
$o-webclient-color-scheme == dark at compile time, expose via CSS
custom properties (--fp-composer-card / -border / -panel / -text /
-muted), drop every --bs-* fallback for layout colours.

Cleared the ir_attachment asset cache on entech so both the backend
and web_dark bundles recompile cleanly.

fusion_plating_configurator → 19.0.12.1.0

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 07:35:11 -04:00
gsinghpal
2bfabfe135 feat(plating): Sub 8 — split receiving vs inspection + box parity
fp.receiving simplifies to box-count-only (new primary state
machine: draft → counted → staged → closed). Legacy
inspecting/accepted/discrepancy/resolved states stay in the
Selection so existing records load without error but are surfaced
behind a manager-only toggle. New box_count_in field + banner
that tells the receiver "count boxes only — parts are inspected
by the racking crew."

New fp.racking.inspection + fp.racking.inspection.line models —
one record per MO, auto-created by mrp.production.create() with
one line per contributing SO line (qty_expected seeded, qty_found
+ condition filled in by the racking crew when they open the boxes).
State: draft → inspecting → done | discrepancy_flagged (flagged
when any line has a non-ok condition or qty variance). Reopen
restricted to Plating Manager.

WO soft gate: first plating WO button_start raises a UserError
when the MO's racking inspection is still Draft or Inspecting.
Plating Manager bypasses; later WOs are not gated.

fp.delivery gains x_fc_box_count_out. action_mark_delivered calls
_fp_check_box_parity which posts a non-blocking chatter warning
when boxes out ≠ boxes in (resolved via job_ref → MO.origin → SO
→ receiving). Warning only — never blocks shipping.

Menu entry: Plating → Operations → Racking Inspection.

Module version bumps:
  fusion_plating_receiving  → 19.0.3.0.0
  fusion_plating_logistics  → 19.0.3.0.0
  fusion_plating_bridge_mrp → 19.0.12.0.0 (+depends receiving)

Smoke on entech: 12/12 assertions pass (one gate test skipped —
MO had no WOs to test) including box-count state machine, inspection
auto-create, lifecycle, discrepancy flag, and box-parity chatter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 00:30:36 -04:00
gsinghpal
392359d2c4 docs(receiving): Sub 8 design spec — split receiving vs inspection + box parity
Receiving simplifies to box-count-only (draft → counted → staged →
closed). New fp.racking.inspection + line models capture the per-part
inspection the racking crew does when they open the boxes, linked to
MO not to receiving. Soft gate on first WO start when the inspection
is still pending (manager override). fp.delivery gains box_count_out;
action_mark_delivered posts a non-blocking chatter warning when
boxes out ≠ boxes in.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 00:21:02 -04:00
gsinghpal
0342535b9f feat(notifications): Sub 6 — contact profiles + communication routing
Five new boolean flags on res.partner applied to CHILD contacts:
x_fc_receives_certs, _qc, _quotes_so, _invoices, and _is_global_contact.
Single resolver helper res.partner._fp_resolve_notification_recipients
(stream, delivery_location=None) walks location contacts first then
company contacts, returning emails for contacts that opted into the
stream (or flagged themselves global). Falls back to partner.email
when no contact opts in so existing customers keep their exact
pre-Sub-6 routing.

fp.notification.template._dispatch now maps each trigger event to a
stream (so_confirmed→quotes_so, invoice_posted→invoices, shipped→
certs, etc.) and overrides the mail_template's email_to with the
resolved list. fp.delivery passes its delivery_address_id so the
shipped/CoC email routes through location-scoped contacts when they
exist.

Partner form gets a new "Communication Routing" tab on child contact
forms with the 5 flags (hides the per-stream checkboxes when
Global Contact is on, since it overrides them).

fusion_plating_certificates → 19.0.4.0.0 (adds the flag fields)
fusion_plating_notifications → 19.0.5.0.0 (+depends certificates)

Smoke on entech: 11/11 assertions pass including per-stream routing,
delivery-location scoping, zero-flag fallback, email-less skip,
unknown-stream + global behaviour, and case-insensitive dedup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 00:01:15 -04:00