chore(plating): de-dash shipped code + intake-neutral customer emails
Replace em-dashes and en-dashes with hyphens across 789 shipped source files (py/xml/js/scss) so the delivered module reads as human-written; em-dashes had become a recognizable AI-generated tell. Internal .md dev notes are excluded. The WO-sticker mojibake strippers keep their dash search targets (now written — / –). No logic changes: comments and display strings only; validated with py_compile + lxml parse. Rewrite the 7 customer notification emails to be intake-neutral (ship-in / drop-off / pickup) and repair-aware, and fix the Shipped email documents line (packing slip vs bill of lading; certificate only when issued). Subjects use a hyphen separator. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -18,13 +18,13 @@ def post_init_hook(env):
|
|||||||
Does several things, each guarded by an "is this already done?"
|
Does several things, each guarded by an "is this already done?"
|
||||||
check so re-running the hook doesn't clobber state:
|
check so re-running the hook doesn't clobber state:
|
||||||
1. Auto-detect a sensible default timezone (original behavior).
|
1. Auto-detect a sensible default timezone (original behavior).
|
||||||
2. Sub 12a — backfill `kind='step_input'` on existing
|
2. Sub 12a - backfill `kind='step_input'` on existing
|
||||||
fusion.plating.process.node.input rows that pre-date the
|
fusion.plating.process.node.input rows that pre-date the
|
||||||
`kind` field.
|
`kind` field.
|
||||||
3. Sub 12a — seed fp.step.template with starter library entries
|
3. Sub 12a - seed fp.step.template with starter library entries
|
||||||
derived from ENP-ALUM-BASIC if the library is currently empty.
|
derived from ENP-ALUM-BASIC if the library is currently empty.
|
||||||
4. Sub 12b — seed 4 starter rack tags if the registry is empty.
|
4. Sub 12b - seed 4 starter rack tags if the registry is empty.
|
||||||
5. Phase H — create a pending fp.migration.preview if any user
|
5. Phase H - create a pending fp.migration.preview if any user
|
||||||
still holds an old plating-role group + notify Owners.
|
still holds an old plating-role group + notify Owners.
|
||||||
"""
|
"""
|
||||||
_seed_default_timezone(env)
|
_seed_default_timezone(env)
|
||||||
@@ -43,7 +43,7 @@ def post_init_hook(env):
|
|||||||
# from uninstalled modules so this is safe across configurations.
|
# from uninstalled modules so this is safe across configurations.
|
||||||
# Kept visible to technicians (NOT in this list): Discuss, To-do,
|
# Kept visible to technicians (NOT in this list): Discuss, To-do,
|
||||||
# Plating, AI, Maintenance, Time Off. Settings/Apps/Tests are admin-
|
# Plating, AI, Maintenance, Time Off. Settings/Apps/Tests are admin-
|
||||||
# restricted upstream — also not in this list.
|
# restricted upstream - also not in this list.
|
||||||
# See security/fp_menu_visibility.xml for the design rationale.
|
# See security/fp_menu_visibility.xml for the design rationale.
|
||||||
MENU_HIDE_FROM_TECHNICIANS = [
|
MENU_HIDE_FROM_TECHNICIANS = [
|
||||||
'calendar.mail_menu_calendar',
|
'calendar.mail_menu_calendar',
|
||||||
@@ -81,12 +81,12 @@ def _fp_apply_office_user_menu_visibility(env):
|
|||||||
MENU_HIDE_FROM_TECHNICIANS that exists in this DB.
|
MENU_HIDE_FROM_TECHNICIANS that exists in this DB.
|
||||||
|
|
||||||
Field is `group_ids` on ir.ui.menu in Odoo 19 (was `groups_id` in
|
Field is `group_ids` on ir.ui.menu in Odoo 19 (was `groups_id` in
|
||||||
earlier versions — Odoo 18 renamed it). Same naming-rename pattern
|
earlier versions - Odoo 18 renamed it). Same naming-rename pattern
|
||||||
as res.users (CLAUDE.md Critical Rule 13c).
|
as res.users (CLAUDE.md Critical Rule 13c).
|
||||||
|
|
||||||
Idempotent: if a menu already has only the office_user group, no
|
Idempotent: if a menu already has only the office_user group, no
|
||||||
change is made. If it has additional groups (e.g. a previous custom
|
change is made. If it has additional groups (e.g. a previous custom
|
||||||
restriction), they're REPLACED — the design accepts this trade-off
|
restriction), they're REPLACED - the design accepts this trade-off
|
||||||
because office_user is implied by every fp role above Technician,
|
because office_user is implied by every fp role above Technician,
|
||||||
so non-fp users keep their access on entech.
|
so non-fp users keep their access on entech.
|
||||||
|
|
||||||
@@ -156,7 +156,7 @@ def _seed_starter_recipes_once(env):
|
|||||||
Before 19.0.20.5.0 the recipe XML files (ENP-STEEL-BASIC, ENP-SP,
|
Before 19.0.20.5.0 the recipe XML files (ENP-STEEL-BASIC, ENP-SP,
|
||||||
ENP-ALUM-BASIC, etc.) lived in the manifest's ``data`` list. With
|
ENP-ALUM-BASIC, etc.) lived in the manifest's ``data`` list. With
|
||||||
``noupdate="1"`` we expected user edits / deletions to survive
|
``noupdate="1"`` we expected user edits / deletions to survive
|
||||||
module upgrades — but Odoo only treats noupdate=1 as "don't update
|
module upgrades - but Odoo only treats noupdate=1 as "don't update
|
||||||
existing records". If a record's ir.model.data row is deleted via
|
existing records". If a record's ir.model.data row is deleted via
|
||||||
unlink, Odoo on the next ``-u`` sees the xmlid as missing and
|
unlink, Odoo on the next ``-u`` sees the xmlid as missing and
|
||||||
RE-CREATES the record from XML. Bug reported 2026-05-20: every
|
RE-CREATES the record from XML. Bug reported 2026-05-20: every
|
||||||
@@ -167,7 +167,7 @@ def _seed_starter_recipes_once(env):
|
|||||||
here via convert_file ONCE per xmlid. Each file gets a sentinel
|
here via convert_file ONCE per xmlid. Each file gets a sentinel
|
||||||
check (does the root recipe's xmlid exist in ir.model.data?); if
|
check (does the root recipe's xmlid exist in ir.model.data?); if
|
||||||
yes, skip. The hook is itself idempotent so it's safe to run on
|
yes, skip. The hook is itself idempotent so it's safe to run on
|
||||||
every upgrade as well — but the sentinel ensures recipe content
|
every upgrade as well - but the sentinel ensures recipe content
|
||||||
is only seeded the very first time.
|
is only seeded the very first time.
|
||||||
"""
|
"""
|
||||||
from odoo.tools import convert
|
from odoo.tools import convert
|
||||||
@@ -197,7 +197,7 @@ def _seed_starter_recipes_once(env):
|
|||||||
module_name, name = xmlid.split('.', 1)
|
module_name, name = xmlid.split('.', 1)
|
||||||
if IMD.search_count([('module', '=', module_name), ('name', '=', name)]):
|
if IMD.search_count([('module', '=', module_name), ('name', '=', name)]):
|
||||||
# Recipe already in DB (either from a previous install, or
|
# Recipe already in DB (either from a previous install, or
|
||||||
# already loaded by an earlier hook run). Don't touch — user
|
# already loaded by an earlier hook run). Don't touch - user
|
||||||
# may have made edits.
|
# may have made edits.
|
||||||
continue
|
continue
|
||||||
# File not yet loaded for this DB. Run it once.
|
# File not yet loaded for this DB. Run it once.
|
||||||
@@ -235,7 +235,7 @@ def _resolve_kind_id(env, code):
|
|||||||
|
|
||||||
|
|
||||||
def _backfill_contract_review_template(env):
|
def _backfill_contract_review_template(env):
|
||||||
"""Idempotent — ensure the Contract Review library template exists.
|
"""Idempotent - ensure the Contract Review library template exists.
|
||||||
|
|
||||||
`_seed_step_library_if_empty` only fires on a fresh DB; existing DBs
|
`_seed_step_library_if_empty` only fires on a fresh DB; existing DBs
|
||||||
upgraded from pre-Policy-B versions still have a populated library
|
upgraded from pre-Policy-B versions still have a populated library
|
||||||
@@ -271,7 +271,7 @@ def _seed_default_timezone(env):
|
|||||||
|
|
||||||
|
|
||||||
def _backfill_node_input_kind(env):
|
def _backfill_node_input_kind(env):
|
||||||
"""Sub 12a — set kind='step_input' on rows that have NULL kind."""
|
"""Sub 12a - set kind='step_input' on rows that have NULL kind."""
|
||||||
cr = env.cr
|
cr = env.cr
|
||||||
cr.execute(
|
cr.execute(
|
||||||
"UPDATE fusion_plating_process_node_input "
|
"UPDATE fusion_plating_process_node_input "
|
||||||
@@ -287,7 +287,7 @@ def _backfill_node_input_kind(env):
|
|||||||
# Mapping of recipe-step name → default_kind. Drives sane-default
|
# Mapping of recipe-step name → default_kind. Drives sane-default
|
||||||
# input seeding on the starter library entries.
|
# input seeding on the starter library entries.
|
||||||
_STARTER_KIND_BY_NAME = {
|
_STARTER_KIND_BY_NAME = {
|
||||||
# Policy B (2026-04-28) — recipe-side Contract Review step.
|
# Policy B (2026-04-28) - recipe-side Contract Review step.
|
||||||
# When an author drops this template into a recipe, fp.job.step.button_*
|
# When an author drops this template into a recipe, fp.job.step.button_*
|
||||||
# hooks in fusion_plating_jobs detect the kind=='contract_review' and
|
# hooks in fusion_plating_jobs detect the kind=='contract_review' and
|
||||||
# auto-open / gate the QA-005 audit form (fp.contract.review).
|
# auto-open / gate the QA-005 audit form (fp.contract.review).
|
||||||
@@ -363,7 +363,7 @@ _STARTER_KIND_BY_NAME = {
|
|||||||
'ready for post-plate inspection': 'gating',
|
'ready for post-plate inspection': 'gating',
|
||||||
'ready for final inspection': 'gating',
|
'ready for final inspection': 'gating',
|
||||||
'ready for shipping': 'gating',
|
'ready for shipping': 'gating',
|
||||||
# 2026-05-24 — Recipe cleanup additions (spec
|
# 2026-05-24 - Recipe cleanup additions (spec
|
||||||
# 2026-05-24-recipe-cleanup-design.md). Covers names the existing
|
# 2026-05-24-recipe-cleanup-design.md). Covers names the existing
|
||||||
# resolver didn't know that turned up in the entech recipes audit.
|
# resolver didn't know that turned up in the entech recipes audit.
|
||||||
# Blasting variants
|
# Blasting variants
|
||||||
@@ -413,13 +413,13 @@ def fp_resolve_step_kind(name):
|
|||||||
key = name.strip().lower()
|
key = name.strip().lower()
|
||||||
if key in _STARTER_KIND_BY_NAME:
|
if key in _STARTER_KIND_BY_NAME:
|
||||||
return _STARTER_KIND_BY_NAME[key]
|
return _STARTER_KIND_BY_NAME[key]
|
||||||
# Parenthetical strip — "Masking (If Required)" → "masking",
|
# Parenthetical strip - "Masking (If Required)" → "masking",
|
||||||
# "Incoming Inspection (Standard)" → "incoming inspection",
|
# "Incoming Inspection (Standard)" → "incoming inspection",
|
||||||
# "Trivalent Chromate Conversion (A-14 / A)" → "trivalent chromate conversion".
|
# "Trivalent Chromate Conversion (A-14 / A)" → "trivalent chromate conversion".
|
||||||
bare = re.sub(r'\s*\([^)]*\)\s*', ' ', key).strip()
|
bare = re.sub(r'\s*\([^)]*\)\s*', ' ', key).strip()
|
||||||
if bare and bare != key and bare in _STARTER_KIND_BY_NAME:
|
if bare and bare != key and bare in _STARTER_KIND_BY_NAME:
|
||||||
return _STARTER_KIND_BY_NAME[bare]
|
return _STARTER_KIND_BY_NAME[bare]
|
||||||
# Gating "Ready for / Ready For" prefix — anything starting with that
|
# Gating "Ready for / Ready For" prefix - anything starting with that
|
||||||
# is a gating node regardless of the destination step name.
|
# is a gating node regardless of the destination step name.
|
||||||
if key.startswith('ready for ') or key.startswith('ready '):
|
if key.startswith('ready for ') or key.startswith('ready '):
|
||||||
return 'gating'
|
return 'gating'
|
||||||
@@ -429,7 +429,7 @@ def fp_resolve_step_kind(name):
|
|||||||
# Translates resolver kind output to the active fp.step.kind.code values.
|
# Translates resolver kind output to the active fp.step.kind.code values.
|
||||||
# The resolver still returns the OLD vocabulary (cleaning, electroclean,
|
# The resolver still returns the OLD vocabulary (cleaning, electroclean,
|
||||||
# etch, rinse, strike, dry, wbf_test) which were deactivated in
|
# etch, rinse, strike, dry, wbf_test) which were deactivated in
|
||||||
# 19.0.20.6.0 — those roll up to the active wet_process kind. Other
|
# 19.0.20.6.0 - those roll up to the active wet_process kind. Other
|
||||||
# codes pass through 1:1. Used by the auto-classify hook on
|
# codes pass through 1:1. Used by the auto-classify hook on
|
||||||
# fusion.plating.process.node + the recipe-cleanup migration
|
# fusion.plating.process.node + the recipe-cleanup migration
|
||||||
# (fusion_plating_jobs 19.0.10.26.0).
|
# (fusion_plating_jobs 19.0.10.26.0).
|
||||||
@@ -446,7 +446,7 @@ RESOLVER_KIND_TO_ACTIVE_KIND = {
|
|||||||
# for "Strip Process - AL", "Chemical
|
# for "Strip Process - AL", "Chemical
|
||||||
# Conversion", "Trivalent Chromate
|
# Conversion", "Trivalent Chromate
|
||||||
# Conversion" maps DIRECTLY to
|
# Conversion" maps DIRECTLY to
|
||||||
# 'wet_process' — this passthrough
|
# 'wet_process' - this passthrough
|
||||||
# entry lets those land correctly.
|
# entry lets those land correctly.
|
||||||
# 1:1 mappings (kind exists and is active)
|
# 1:1 mappings (kind exists and is active)
|
||||||
'contract_review': 'contract_review',
|
'contract_review': 'contract_review',
|
||||||
@@ -465,10 +465,10 @@ RESOLVER_KIND_TO_ACTIVE_KIND = {
|
|||||||
|
|
||||||
|
|
||||||
def _seed_step_library_if_empty(env):
|
def _seed_step_library_if_empty(env):
|
||||||
"""Sub 12a — seed fp.step.template starter library.
|
"""Sub 12a - seed fp.step.template starter library.
|
||||||
|
|
||||||
Source priority:
|
Source priority:
|
||||||
1. ENP-ALUM-BASIC recipe's child nodes (best — reuses the
|
1. ENP-ALUM-BASIC recipe's child nodes (best - reuses the
|
||||||
author-curated step set).
|
author-curated step set).
|
||||||
2. Hard-coded minimal list (fallback for fresh DBs).
|
2. Hard-coded minimal list (fallback for fresh DBs).
|
||||||
"""
|
"""
|
||||||
@@ -600,7 +600,7 @@ def _migrate_legacy_uom_columns(env):
|
|||||||
|
|
||||||
|
|
||||||
def _seed_rack_tags_if_empty(env):
|
def _seed_rack_tags_if_empty(env):
|
||||||
"""Sub 12b — seed 4 starter rack tags."""
|
"""Sub 12b - seed 4 starter rack tags."""
|
||||||
Tag = env['fp.rack.tag']
|
Tag = env['fp.rack.tag']
|
||||||
if Tag.search_count([]):
|
if Tag.search_count([]):
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
'category': 'Manufacturing/Plating',
|
'category': 'Manufacturing/Plating',
|
||||||
'summary': 'Core plating / metal finishing ERP: facilities, processes, tanks, baths, jobs, operators.',
|
'summary': 'Core plating / metal finishing ERP: facilities, processes, tanks, baths, jobs, operators.',
|
||||||
'description': """
|
'description': """
|
||||||
Fusion Plating — Core
|
Fusion Plating - Core
|
||||||
=====================
|
=====================
|
||||||
|
|
||||||
Part of the Fusion Plating product family by Nexa Systems Inc.
|
Part of the Fusion Plating product family by Nexa Systems Inc.
|
||||||
@@ -19,35 +19,35 @@ finishing shops. This core module provides the process-agnostic foundation that
|
|||||||
every shop needs regardless of size, process mix, jurisdiction, or industry.
|
every shop needs regardless of size, process mix, jurisdiction, or industry.
|
||||||
|
|
||||||
The core ships intentionally empty of region-specific or process-specific
|
The core ships intentionally empty of region-specific or process-specific
|
||||||
content — that comes from add-on modules:
|
content - that comes from add-on modules:
|
||||||
|
|
||||||
* fusion_plating_process_en — Electroless nickel plating
|
* fusion_plating_process_en - Electroless nickel plating
|
||||||
* fusion_plating_process_chrome — Chrome coating (hex or trivalent)
|
* fusion_plating_process_chrome - Chrome coating (hex or trivalent)
|
||||||
* fusion_plating_process_anodize — Aluminum anodizing (Type II, III)
|
* fusion_plating_process_anodize - Aluminum anodizing (Type II, III)
|
||||||
* fusion_plating_process_black_oxide — Black oxidizing
|
* fusion_plating_process_black_oxide - Black oxidizing
|
||||||
* fusion_plating_quality — QMS (NCR, CAPA, calibration, CoC, doc control)
|
* fusion_plating_quality - QMS (NCR, CAPA, calibration, CoC, doc control)
|
||||||
* fusion_plating_compliance — Generic compliance framework
|
* fusion_plating_compliance - Generic compliance framework
|
||||||
* fusion_plating_compliance_on — Ontario regulatory pack
|
* fusion_plating_compliance_on - Ontario regulatory pack
|
||||||
* fusion_plating_compliance_tor — Toronto Ch. 681 municipal pack
|
* fusion_plating_compliance_tor - Toronto Ch. 681 municipal pack
|
||||||
* fusion_plating_safety — SDS, WHMIS/TDG training, JHSC, exposure
|
* fusion_plating_safety - SDS, WHMIS/TDG training, JHSC, exposure
|
||||||
* fusion_plating_shopfloor — Tablet operator stations, QR scanning
|
* fusion_plating_shopfloor - Tablet operator stations, QR scanning
|
||||||
* fusion_plating_portal — Customer portal
|
* fusion_plating_portal - Customer portal
|
||||||
* fusion_plating_aerospace — AS9100 + Nadcap AC7108 pack
|
* fusion_plating_aerospace - AS9100 + Nadcap AC7108 pack
|
||||||
* fusion_plating_nuclear — CSA N299, CNSC, NQA-1 pack
|
* fusion_plating_nuclear - CSA N299, CNSC, NQA-1 pack
|
||||||
* fusion_plating_cgp — Controlled Goods Program pack
|
* fusion_plating_cgp - Controlled Goods Program pack
|
||||||
* fusion_plating_logistics — Pickup & delivery
|
* fusion_plating_logistics - Pickup & delivery
|
||||||
* fusion_plating_culture — Values / fundamentals framework
|
* fusion_plating_culture - Values / fundamentals framework
|
||||||
|
|
||||||
Core concepts
|
Core concepts
|
||||||
-------------
|
-------------
|
||||||
* Facility — a physical site with its own tanks, operators, compliance profile
|
* Facility - a physical site with its own tanks, operators, compliance profile
|
||||||
* Process Type — extensible taxonomy of finishing processes
|
* Process Type - extensible taxonomy of finishing processes
|
||||||
* Work Center — production line or station within a facility
|
* Work Center - production line or station within a facility
|
||||||
* Tank — physical vessel with QR code and state
|
* Tank - physical vessel with QR code and state
|
||||||
* Bath — the chemistry currently in a tank, with its own lifecycle
|
* Bath - the chemistry currently in a tank, with its own lifecycle
|
||||||
* Bath Log — daily chemistry readings with pass/fail vs target
|
* Bath Log - daily chemistry readings with pass/fail vs target
|
||||||
* KPI — configurable headline metrics per shop
|
* KPI - configurable headline metrics per shop
|
||||||
* Delegation Inbox — single pane of "things waiting for someone"
|
* Delegation Inbox - single pane of "things waiting for someone"
|
||||||
|
|
||||||
Design principles
|
Design principles
|
||||||
-----------------
|
-----------------
|
||||||
@@ -82,7 +82,7 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
|
|||||||
'security/fp_security.xml',
|
'security/fp_security.xml',
|
||||||
'security/fp_security_v2.xml',
|
'security/fp_security_v2.xml',
|
||||||
'security/ir.model.access.csv',
|
'security/ir.model.access.csv',
|
||||||
# Menu visibility — loads after fp_security_v2.xml so the role
|
# Menu visibility - loads after fp_security_v2.xml so the role
|
||||||
# group xmlids exist when we add office_user to their
|
# group xmlids exist when we add office_user to their
|
||||||
# implied_ids. Loads after fp_menu.xml in spirit BUT references
|
# implied_ids. Loads after fp_menu.xml in spirit BUT references
|
||||||
# cross-module menus (calendar, sale, hr, etc.) which exist by
|
# cross-module menus (calendar, sale, hr, etc.) which exist by
|
||||||
@@ -95,7 +95,7 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
|
|||||||
'data/fp_numbering_sequences.xml',
|
'data/fp_numbering_sequences.xml',
|
||||||
'data/fp_rack_load_sequence.xml',
|
'data/fp_rack_load_sequence.xml',
|
||||||
'data/fp_process_category_data.xml',
|
'data/fp_process_category_data.xml',
|
||||||
# fp_menu.xml MUST load early — defines menu_fp_root, menu_fp_config,
|
# fp_menu.xml MUST load early - defines menu_fp_root, menu_fp_config,
|
||||||
# menu_fp_compliance_hub, plus the 7 Phase-2 Configuration sub-folder
|
# menu_fp_compliance_hub, plus the 7 Phase-2 Configuration sub-folder
|
||||||
# buckets. Every other view file (in this module and downstream)
|
# buckets. Every other view file (in this module and downstream)
|
||||||
# that creates a child menu under those buckets references them
|
# that creates a child menu under those buckets references them
|
||||||
@@ -108,7 +108,7 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
|
|||||||
'views/fp_facility_views.xml',
|
'views/fp_facility_views.xml',
|
||||||
'views/fp_bath_views.xml',
|
'views/fp_bath_views.xml',
|
||||||
'views/fp_process_node_views.xml',
|
'views/fp_process_node_views.xml',
|
||||||
# Sub 14b — fp.step.kind catalog. MUST load before
|
# Sub 14b - fp.step.kind catalog. MUST load before
|
||||||
# fp_step_template_data.xml (templates reference kinds via
|
# fp_step_template_data.xml (templates reference kinds via
|
||||||
# kind_id) AND before fp_step_template_views.xml (the form
|
# kind_id) AND before fp_step_template_views.xml (the form
|
||||||
# references the kind action menu).
|
# references the kind action menu).
|
||||||
@@ -123,7 +123,7 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
|
|||||||
'views/fp_operator_certification_views.xml',
|
'views/fp_operator_certification_views.xml',
|
||||||
'views/res_config_settings_views.xml',
|
'views/res_config_settings_views.xml',
|
||||||
'views/fp_landing_views.xml',
|
'views/fp_landing_views.xml',
|
||||||
# Phase F — Owner-only Team page + Designated Officials on res.company.
|
# Phase F - Owner-only Team page + Designated Officials on res.company.
|
||||||
# Both reference menu_fp_config (Configuration root) and Phase 1
|
# Both reference menu_fp_config (Configuration root) and Phase 1
|
||||||
# role groups, all loaded earlier (fp_menu.xml + fp_security_v2.xml).
|
# role groups, all loaded earlier (fp_menu.xml + fp_security_v2.xml).
|
||||||
'views/fp_team_views.xml',
|
'views/fp_team_views.xml',
|
||||||
@@ -139,7 +139,7 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
|
|||||||
# so user edits / deletions survive every -u upgrade. Putting
|
# so user edits / deletions survive every -u upgrade. Putting
|
||||||
# them back here would re-create deleted nodes on every
|
# them back here would re-create deleted nodes on every
|
||||||
# module upgrade (the noupdate="1" flag only blocks UPDATE,
|
# module upgrade (the noupdate="1" flag only blocks UPDATE,
|
||||||
# not CREATE-when-missing — Odoo treats a missing ir.model.data
|
# not CREATE-when-missing - Odoo treats a missing ir.model.data
|
||||||
# record as "needs creating").
|
# record as "needs creating").
|
||||||
# 'data/fp_recipe_enp_alum_basic.xml',
|
# 'data/fp_recipe_enp_alum_basic.xml',
|
||||||
# 'data/fp_recipe_enp_steel_basic.xml',
|
# 'data/fp_recipe_enp_steel_basic.xml',
|
||||||
@@ -148,7 +148,7 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
|
|||||||
# 'data/fp_recipe_anodize.xml',
|
# 'data/fp_recipe_anodize.xml',
|
||||||
# 'data/fp_recipe_chem_conversion.xml',
|
# 'data/fp_recipe_chem_conversion.xml',
|
||||||
'data/fp_step_template_data.xml',
|
'data/fp_step_template_data.xml',
|
||||||
# Phase H — Owner-approval migration workflow.
|
# Phase H - Owner-approval migration workflow.
|
||||||
# Views file declares the action + menu; cron declares the
|
# Views file declares the action + menu; cron declares the
|
||||||
# daily 30-day expiry purge. Both reference model_fp_migration_preview
|
# daily 30-day expiry purge. Both reference model_fp_migration_preview
|
||||||
# which Odoo's model autoload makes available before data load.
|
# which Odoo's model autoload makes available before data load.
|
||||||
@@ -162,7 +162,7 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
|
|||||||
'fusion_plating/static/src/scss/recipe_tree_editor.scss',
|
'fusion_plating/static/src/scss/recipe_tree_editor.scss',
|
||||||
'fusion_plating/static/src/scss/fp_chatter_dark.scss',
|
'fusion_plating/static/src/scss/fp_chatter_dark.scss',
|
||||||
'fusion_plating/static/src/scss/simple_recipe_editor.scss',
|
'fusion_plating/static/src/scss/simple_recipe_editor.scss',
|
||||||
# Sub 14b — visual icon picker for fp.step.kind etc.
|
# Sub 14b - visual icon picker for fp.step.kind etc.
|
||||||
'fusion_plating/static/src/scss/fp_icon_picker.scss',
|
'fusion_plating/static/src/scss/fp_icon_picker.scss',
|
||||||
'fusion_plating/static/src/xml/recipe_tree_editor.xml',
|
'fusion_plating/static/src/xml/recipe_tree_editor.xml',
|
||||||
'fusion_plating/static/src/xml/simple_recipe_editor.xml',
|
'fusion_plating/static/src/xml/simple_recipe_editor.xml',
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class FpRecipeController(http.Controller):
|
|||||||
"""JSON-RPC endpoints for the process recipe tree editor."""
|
"""JSON-RPC endpoints for the process recipe tree editor."""
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Read — full tree
|
# Read - full tree
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@http.route('/fp/recipe/tree', type='jsonrpc', auth='user')
|
@http.route('/fp/recipe/tree', type='jsonrpc', auth='user')
|
||||||
def get_tree(self, recipe_id):
|
def get_tree(self, recipe_id):
|
||||||
@@ -27,7 +27,7 @@ class FpRecipeController(http.Controller):
|
|||||||
recipe = Node.browse(int(recipe_id))
|
recipe = Node.browse(int(recipe_id))
|
||||||
if not recipe.exists():
|
if not recipe.exists():
|
||||||
return {'ok': False, 'error': f'Recipe {recipe_id} not found.'}
|
return {'ok': False, 'error': f'Recipe {recipe_id} not found.'}
|
||||||
# Workflow states for the dropdown — runtime-detect the model
|
# Workflow states for the dropdown - runtime-detect the model
|
||||||
# so the tree editor still works on installs without
|
# so the tree editor still works on installs without
|
||||||
# fusion_plating_jobs (where the model lives).
|
# fusion_plating_jobs (where the model lives).
|
||||||
workflow_states = []
|
workflow_states = []
|
||||||
@@ -103,9 +103,9 @@ class FpRecipeController(http.Controller):
|
|||||||
'estimated_duration',
|
'estimated_duration',
|
||||||
'auto_complete', 'customer_visible', 'is_manual',
|
'auto_complete', 'customer_visible', 'is_manual',
|
||||||
'requires_signoff', 'opt_in_out', 'sequence', 'version',
|
'requires_signoff', 'opt_in_out', 'sequence', 'version',
|
||||||
# Sub 13 — sequential enforcement
|
# Sub 13 - sequential enforcement
|
||||||
'enforce_sequential', 'parallel_start',
|
'enforce_sequential', 'parallel_start',
|
||||||
# Sub 14 — workflow milestone trigger
|
# Sub 14 - workflow milestone trigger
|
||||||
'triggers_workflow_state_id',
|
'triggers_workflow_state_id',
|
||||||
}
|
}
|
||||||
safe_vals = {k: v for k, v in vals.items() if k in allowed}
|
safe_vals = {k: v for k, v in vals.items() if k in allowed}
|
||||||
@@ -164,7 +164,7 @@ class FpRecipeController(http.Controller):
|
|||||||
def list_recipes(self, exclude_id=None):
|
def list_recipes(self, exclude_id=None):
|
||||||
"""Return all recipe-roots available for the import picker.
|
"""Return all recipe-roots available for the import picker.
|
||||||
|
|
||||||
exclude_id: optional — skip this recipe (usually the currently-
|
exclude_id: optional - skip this recipe (usually the currently-
|
||||||
open one, so the user can't import from themselves).
|
open one, so the user can't import from themselves).
|
||||||
"""
|
"""
|
||||||
Node = request.env['fusion.plating.process.node']
|
Node = request.env['fusion.plating.process.node']
|
||||||
@@ -181,7 +181,7 @@ class FpRecipeController(http.Controller):
|
|||||||
}
|
}
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Move sibling up / down — explicit button-driven reorder
|
# Move sibling up / down - explicit button-driven reorder
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@http.route('/fp/recipe/node/move_sibling', type='jsonrpc', auth='user')
|
@http.route('/fp/recipe/node/move_sibling', type='jsonrpc', auth='user')
|
||||||
def move_sibling(self, node_id, direction):
|
def move_sibling(self, node_id, direction):
|
||||||
@@ -212,7 +212,7 @@ class FpRecipeController(http.Controller):
|
|||||||
# Swap the two sequence values
|
# Swap the two sequence values
|
||||||
a_seq, b_seq = node.sequence, other.sequence
|
a_seq, b_seq = node.sequence, other.sequence
|
||||||
if a_seq == b_seq:
|
if a_seq == b_seq:
|
||||||
# Sequences collided — renumber everyone cleanly, then swap
|
# Sequences collided - renumber everyone cleanly, then swap
|
||||||
for i, s in enumerate(siblings, 1):
|
for i, s in enumerate(siblings, 1):
|
||||||
s.sequence = i * 10
|
s.sequence = i * 10
|
||||||
a_seq, b_seq = node.sequence, other.sequence
|
a_seq, b_seq = node.sequence, other.sequence
|
||||||
@@ -240,7 +240,7 @@ class FpRecipeController(http.Controller):
|
|||||||
* 0 → insert at the start.
|
* 0 → insert at the start.
|
||||||
* <positive int> → insert right before that child id.
|
* <positive int> → insert right before that child id.
|
||||||
Needed because "General Processing" has Shipping as the
|
Needed because "General Processing" has Shipping as the
|
||||||
LAST operation — importing a plating pack belongs between
|
LAST operation - importing a plating pack belongs between
|
||||||
Scheduling and Shipping, not after Shipping.
|
Scheduling and Shipping, not after Shipping.
|
||||||
|
|
||||||
Returns: {ok, imported_count, skipped_count}
|
Returns: {ok, imported_count, skipped_count}
|
||||||
@@ -251,7 +251,7 @@ class FpRecipeController(http.Controller):
|
|||||||
if not source.exists() or not target.exists():
|
if not source.exists() or not target.exists():
|
||||||
return {'ok': False, 'error': 'Source or target not found.'}
|
return {'ok': False, 'error': 'Source or target not found.'}
|
||||||
if f'/{source.id}/' in (target.parent_path or ''):
|
if f'/{source.id}/' in (target.parent_path or ''):
|
||||||
return {'ok': False, 'error': 'Target is inside source — would loop.'}
|
return {'ok': False, 'error': 'Target is inside source - would loop.'}
|
||||||
|
|
||||||
existing_names = set()
|
existing_names = set()
|
||||||
if dedupe_by_name:
|
if dedupe_by_name:
|
||||||
@@ -282,7 +282,7 @@ class FpRecipeController(http.Controller):
|
|||||||
_copy_subtree(child, new_node, i * 10)
|
_copy_subtree(child, new_node, i * 10)
|
||||||
return new_node
|
return new_node
|
||||||
|
|
||||||
# Phase 1 — create every copied top-level child, tracking their
|
# Phase 1 - create every copied top-level child, tracking their
|
||||||
# ids so we can separate them from the original children when
|
# ids so we can separate them from the original children when
|
||||||
# reordering below.
|
# reordering below.
|
||||||
new_top_level_ids = []
|
new_top_level_ids = []
|
||||||
@@ -298,7 +298,7 @@ class FpRecipeController(http.Controller):
|
|||||||
if key:
|
if key:
|
||||||
existing_names.add(key)
|
existing_names.add(key)
|
||||||
|
|
||||||
# Phase 2 — compute the final top-level ordering and reassign
|
# Phase 2 - compute the final top-level ordering and reassign
|
||||||
# sequences so imported nodes land at the requested position
|
# sequences so imported nodes land at the requested position
|
||||||
# instead of always appearing after every existing child.
|
# instead of always appearing after every existing child.
|
||||||
target.invalidate_recordset(['child_ids'])
|
target.invalidate_recordset(['child_ids'])
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from odoo.http import request
|
|||||||
|
|
||||||
|
|
||||||
# Field list copied from a library template into a new recipe step on
|
# Field list copied from a library template into a new recipe step on
|
||||||
# drag-drop. Snapshot semantics (Q4 from the design doc — editing a
|
# drag-drop. Snapshot semantics (Q4 from the design doc - editing a
|
||||||
# library template later does NOT change recipes already built).
|
# library template later does NOT change recipes already built).
|
||||||
_SNAPSHOT_FIELDS = [
|
_SNAPSHOT_FIELDS = [
|
||||||
'name', 'code', 'description', 'icon',
|
'name', 'code', 'description', 'icon',
|
||||||
@@ -24,9 +24,9 @@ _SNAPSHOT_FIELDS = [
|
|||||||
'voltage_target', 'viscosity_target',
|
'voltage_target', 'viscosity_target',
|
||||||
'requires_signoff', 'requires_predecessor_done',
|
'requires_signoff', 'requires_predecessor_done',
|
||||||
'parallel_start',
|
'parallel_start',
|
||||||
'triggers_workflow_state_id', # Sub 14 — workflow milestone trigger
|
'triggers_workflow_state_id', # Sub 14 - workflow milestone trigger
|
||||||
'requires_rack_assignment', 'requires_transition_form',
|
'requires_rack_assignment', 'requires_transition_form',
|
||||||
'kind_id', # Sub 14b — replaces default_kind (now a related Char)
|
'kind_id', # Sub 14b - replaces default_kind (now a related Char)
|
||||||
]
|
]
|
||||||
|
|
||||||
# Fields on fp.step.template.input that copy 1:1 into
|
# Fields on fp.step.template.input that copy 1:1 into
|
||||||
@@ -40,7 +40,7 @@ _INPUT_SNAPSHOT_FIELDS = [
|
|||||||
def _copy_snapshot_fields(source, fields):
|
def _copy_snapshot_fields(source, fields):
|
||||||
"""Copy ``fields`` from ``source`` record into a write-ready dict.
|
"""Copy ``fields`` from ``source`` record into a write-ready dict.
|
||||||
|
|
||||||
Many2one values must be unwrapped to their integer id — passing a
|
Many2one values must be unwrapped to their integer id - passing a
|
||||||
recordset to ``create`` triggers psycopg2 ``can't adapt type X``
|
recordset to ``create`` triggers psycopg2 ``can't adapt type X``
|
||||||
because the SQL adapter doesn't know how to serialize a recordset.
|
because the SQL adapter doesn't know how to serialize a recordset.
|
||||||
Scalar fields pass through untouched.
|
Scalar fields pass through untouched.
|
||||||
@@ -66,7 +66,7 @@ class SimpleRecipeController(http.Controller):
|
|||||||
# Tree-Editor-authored recipes carry FOUR node levels:
|
# Tree-Editor-authored recipes carry FOUR node levels:
|
||||||
# recipe → sub_process → operation → step
|
# recipe → sub_process → operation → step
|
||||||
# The Tree Editor shows all of them. The Simple Editor used to
|
# The Tree Editor shows all of them. The Simple Editor used to
|
||||||
# only show direct children of the recipe — so for
|
# only show direct children of the recipe - so for
|
||||||
# ENP-STEEL-BASIC (1 sub_process + 16 operations + 26 step
|
# ENP-STEEL-BASIC (1 sub_process + 16 operations + 26 step
|
||||||
# nodes), authors saw 10 rows out of 43. Work-order generation
|
# nodes), authors saw 10 rows out of 43. Work-order generation
|
||||||
# walked the full tree and emitted operations as fp.job.step
|
# walked the full tree and emitted operations as fp.job.step
|
||||||
@@ -98,7 +98,7 @@ class SimpleRecipeController(http.Controller):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def _flatten_recipe_operations(self, recipe):
|
def _flatten_recipe_operations(self, recipe):
|
||||||
"""Legacy helper — returns ONLY operations.
|
"""Legacy helper - returns ONLY operations.
|
||||||
|
|
||||||
Kept for back-compat with callers and tests that asked for the
|
Kept for back-compat with callers and tests that asked for the
|
||||||
operations-only view. Most paths should now use
|
operations-only view. Most paths should now use
|
||||||
@@ -144,7 +144,7 @@ class SimpleRecipeController(http.Controller):
|
|||||||
for child in node.child_ids.sorted('sequence'):
|
for child in node.child_ids.sorted('sequence'):
|
||||||
_walk(child, sub_path)
|
_walk(child, sub_path)
|
||||||
# `step` nodes that are direct children of a recipe (rare,
|
# `step` nodes that are direct children of a recipe (rare,
|
||||||
# legacy seed data) are silently dropped — _generate_steps
|
# legacy seed data) are silently dropped - _generate_steps
|
||||||
# has always skipped them.
|
# has always skipped them.
|
||||||
|
|
||||||
_walk(recipe, '')
|
_walk(recipe, '')
|
||||||
@@ -161,7 +161,7 @@ class SimpleRecipeController(http.Controller):
|
|||||||
[recipe.process_type_id.id, recipe.process_type_id.name]
|
[recipe.process_type_id.id, recipe.process_type_id.name]
|
||||||
if recipe.process_type_id else False
|
if recipe.process_type_id else False
|
||||||
),
|
),
|
||||||
# 2026-05-20 — drives the visibility of admin-only affordances
|
# 2026-05-20 - drives the visibility of admin-only affordances
|
||||||
# in the Simple Editor (e.g. "+ New kind…" inline create).
|
# in the Simple Editor (e.g. "+ New kind…" inline create).
|
||||||
'user_is_manager': request.env.user.has_group(
|
'user_is_manager': request.env.user.has_group(
|
||||||
'fusion_plating.group_fusion_plating_manager'
|
'fusion_plating.group_fusion_plating_manager'
|
||||||
@@ -169,7 +169,7 @@ class SimpleRecipeController(http.Controller):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def _step_payload(self, step):
|
def _step_payload(self, step):
|
||||||
# Sub 12d — measurement prompts. Filter to step_input only (transition
|
# Sub 12d - measurement prompts. Filter to step_input only (transition
|
||||||
# prompts live on the move dialog). Sort by sequence so the editor
|
# prompts live on the move dialog). Sort by sequence so the editor
|
||||||
# renders them in author order.
|
# renders them in author order.
|
||||||
step_inputs = step.input_ids.filtered(
|
step_inputs = step.input_ids.filtered(
|
||||||
@@ -209,9 +209,9 @@ class SimpleRecipeController(http.Controller):
|
|||||||
'work_center_id': step.work_center_id.id if step.work_center_id else False,
|
'work_center_id': step.work_center_id.id if step.work_center_id else False,
|
||||||
'source_template_id': step.source_template_id.id or False,
|
'source_template_id': step.source_template_id.id or False,
|
||||||
'collect_measurements': bool(step.collect_measurements),
|
'collect_measurements': bool(step.collect_measurements),
|
||||||
# Sub 13 — per-step opt-out of the sequential gate
|
# Sub 13 - per-step opt-out of the sequential gate
|
||||||
'parallel_start': bool(step.parallel_start),
|
'parallel_start': bool(step.parallel_start),
|
||||||
# Sub 14 — workflow milestone trigger override
|
# Sub 14 - workflow milestone trigger override
|
||||||
'triggers_workflow_state_id': (
|
'triggers_workflow_state_id': (
|
||||||
step.triggers_workflow_state_id.id
|
step.triggers_workflow_state_id.id
|
||||||
if 'triggers_workflow_state_id' in step._fields
|
if 'triggers_workflow_state_id' in step._fields
|
||||||
@@ -222,7 +222,7 @@ class SimpleRecipeController(http.Controller):
|
|||||||
'measurements_badge_class': badge_class,
|
'measurements_badge_class': badge_class,
|
||||||
# Reference images attached to the step. Operators see
|
# Reference images attached to the step. Operators see
|
||||||
# these in the Record Inputs dialog and the step quick-look
|
# these in the Record Inputs dialog and the step quick-look
|
||||||
# modal — recipe authors upload via the inline edit panel.
|
# modal - recipe authors upload via the inline edit panel.
|
||||||
'instruction_images': [
|
'instruction_images': [
|
||||||
{
|
{
|
||||||
'id': att.id,
|
'id': att.id,
|
||||||
@@ -328,7 +328,7 @@ class SimpleRecipeController(http.Controller):
|
|||||||
'requires_signoff': tpl.requires_signoff,
|
'requires_signoff': tpl.requires_signoff,
|
||||||
'requires_predecessor_done': tpl.requires_predecessor_done,
|
'requires_predecessor_done': tpl.requires_predecessor_done,
|
||||||
'parallel_start': tpl.parallel_start,
|
'parallel_start': tpl.parallel_start,
|
||||||
# Sub 14 — workflow trigger (id + name for display)
|
# Sub 14 - workflow trigger (id + name for display)
|
||||||
'triggers_workflow_state_id': (
|
'triggers_workflow_state_id': (
|
||||||
tpl.triggers_workflow_state_id.id
|
tpl.triggers_workflow_state_id.id
|
||||||
if tpl.triggers_workflow_state_id else False
|
if tpl.triggers_workflow_state_id else False
|
||||||
@@ -367,9 +367,9 @@ class SimpleRecipeController(http.Controller):
|
|||||||
refresh in one round-trip.
|
refresh in one round-trip.
|
||||||
"""
|
"""
|
||||||
Tpl = request.env['fp.step.template']
|
Tpl = request.env['fp.step.template']
|
||||||
# Whitelist — never trust client-provided write_uid / id / etc.
|
# Whitelist - never trust client-provided write_uid / id / etc.
|
||||||
# Sub 14b: `default_kind` is now a related read-only Char. The
|
# Sub 14b: `default_kind` is now a related read-only Char. The
|
||||||
# client may still send it as a string code for back-compat — we
|
# client may still send it as a string code for back-compat - we
|
||||||
# translate it to kind_id below.
|
# translate it to kind_id below.
|
||||||
allowed = {
|
allowed = {
|
||||||
'name', 'code', 'icon', 'kind_id', 'description',
|
'name', 'code', 'icon', 'kind_id', 'description',
|
||||||
@@ -408,7 +408,7 @@ class SimpleRecipeController(http.Controller):
|
|||||||
|
|
||||||
@http.route('/fp/simple_recipe/library/seed_defaults', type='jsonrpc', auth='user')
|
@http.route('/fp/simple_recipe/library/seed_defaults', type='jsonrpc', auth='user')
|
||||||
def library_seed_defaults(self, template_id):
|
def library_seed_defaults(self, template_id):
|
||||||
"""Run action_seed_default_inputs on this template. Idempotent —
|
"""Run action_seed_default_inputs on this template. Idempotent -
|
||||||
only adds prompts whose name doesn't already exist.
|
only adds prompts whose name doesn't already exist.
|
||||||
"""
|
"""
|
||||||
tpl = request.env['fp.step.template'].browse(int(template_id))
|
tpl = request.env['fp.step.template'].browse(int(template_id))
|
||||||
@@ -483,12 +483,12 @@ class SimpleRecipeController(http.Controller):
|
|||||||
@http.route('/fp/simple_recipe/kinds/list',
|
@http.route('/fp/simple_recipe/kinds/list',
|
||||||
type='jsonrpc', auth='user')
|
type='jsonrpc', auth='user')
|
||||||
def kinds_list(self):
|
def kinds_list(self):
|
||||||
"""Sub 14b — Step Kind dropdown options for the inline library
|
"""Sub 14b - Step Kind dropdown options for the inline library
|
||||||
form. User-extensible via /fp/simple_recipe/kinds/create.
|
form. User-extensible via /fp/simple_recipe/kinds/create.
|
||||||
|
|
||||||
2026-05-24 — payload now includes `area_kind` + a humanized
|
2026-05-24 - payload now includes `area_kind` + a humanized
|
||||||
`area_kind_label` so the Simple Editor picker can render
|
`area_kind_label` so the Simple Editor picker can render
|
||||||
"Masking — Masking column" and authors see which Shop Floor
|
"Masking - Masking column" and authors see which Shop Floor
|
||||||
column they're routing the step to.
|
column they're routing the step to.
|
||||||
"""
|
"""
|
||||||
Kind = request.env['fp.step.kind']
|
Kind = request.env['fp.step.kind']
|
||||||
@@ -517,7 +517,7 @@ class SimpleRecipeController(http.Controller):
|
|||||||
Auto-derives a code from the name if blank.
|
Auto-derives a code from the name if blank.
|
||||||
|
|
||||||
2026-05-20 lockdown: manager group only. Kinds drive gates,
|
2026-05-20 lockdown: manager group only. Kinds drive gates,
|
||||||
milestones, and operator routing — a user-created kind with no
|
milestones, and operator routing - a user-created kind with no
|
||||||
corresponding behaviour is a silent foot-gun. The dropdown is
|
corresponding behaviour is a silent foot-gun. The dropdown is
|
||||||
the curated catalog; adding a new kind requires manager
|
the curated catalog; adding a new kind requires manager
|
||||||
approval and follow-up code work to wire the new code into the
|
approval and follow-up code work to wire the new code into the
|
||||||
@@ -535,7 +535,7 @@ class SimpleRecipeController(http.Controller):
|
|||||||
'Only Plating Managers can add new Step Kinds. The '
|
'Only Plating Managers can add new Step Kinds. The '
|
||||||
'catalog is curated because each kind drives gates, '
|
'catalog is curated because each kind drives gates, '
|
||||||
'milestones, and operator routing. Pick "Other" if '
|
'milestones, and operator routing. Pick "Other" if '
|
||||||
'no existing kind fits — or ask a manager to add the '
|
'no existing kind fits - or ask a manager to add the '
|
||||||
'new kind once the downstream behaviour is wired up.'
|
'new kind once the downstream behaviour is wired up.'
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
@@ -561,12 +561,12 @@ class SimpleRecipeController(http.Controller):
|
|||||||
@http.route('/fp/simple_recipe/workflow_states/list',
|
@http.route('/fp/simple_recipe/workflow_states/list',
|
||||||
type='jsonrpc', auth='user')
|
type='jsonrpc', auth='user')
|
||||||
def workflow_states_list(self):
|
def workflow_states_list(self):
|
||||||
"""Sub 14 — workflow-state picker for the inline library form.
|
"""Sub 14 - workflow-state picker for the inline library form.
|
||||||
Returns active states ordered by sequence so the dropdown
|
Returns active states ordered by sequence so the dropdown
|
||||||
renders left-to-right matching the status bar.
|
renders left-to-right matching the status bar.
|
||||||
|
|
||||||
Soft-fail when fp.job.workflow.state isn't installed (rare,
|
Soft-fail when fp.job.workflow.state isn't installed (rare,
|
||||||
only when fusion_plating_jobs is missing) — empty list lets the
|
only when fusion_plating_jobs is missing) - empty list lets the
|
||||||
dropdown render disabled instead of throwing.
|
dropdown render disabled instead of throwing.
|
||||||
"""
|
"""
|
||||||
WS = request.env.get('fp.job.workflow.state')
|
WS = request.env.get('fp.job.workflow.state')
|
||||||
@@ -594,7 +594,7 @@ class SimpleRecipeController(http.Controller):
|
|||||||
|
|
||||||
new_vals = {
|
new_vals = {
|
||||||
'parent_id': recipe.id,
|
'parent_id': recipe.id,
|
||||||
# Must be 'operation' — fp.job._generate_steps() only creates
|
# Must be 'operation' - fp.job._generate_steps() only creates
|
||||||
# fp.job.step rows for operation nodes. Flat 'step' children
|
# fp.job.step rows for operation nodes. Flat 'step' children
|
||||||
# of a recipe were silently skipped pre-19.0.18.8.0.
|
# of a recipe were silently skipped pre-19.0.18.8.0.
|
||||||
'node_type': 'operation',
|
'node_type': 'operation',
|
||||||
@@ -666,7 +666,7 @@ class SimpleRecipeController(http.Controller):
|
|||||||
"""Update fields on an existing recipe step (operation node).
|
"""Update fields on an existing recipe step (operation node).
|
||||||
|
|
||||||
Whitelisted to the fields the inline edit panel actually surfaces
|
Whitelisted to the fields the inline edit panel actually surfaces
|
||||||
— never trust client-provided node_type / parent_id / etc.
|
- never trust client-provided node_type / parent_id / etc.
|
||||||
"""
|
"""
|
||||||
node = request.env['fusion.plating.process.node'].browse(int(node_id))
|
node = request.env['fusion.plating.process.node'].browse(int(node_id))
|
||||||
if not node.exists():
|
if not node.exists():
|
||||||
@@ -674,7 +674,7 @@ class SimpleRecipeController(http.Controller):
|
|||||||
node.check_access('write')
|
node.check_access('write')
|
||||||
allowed = {
|
allowed = {
|
||||||
'name', 'description', 'icon',
|
'name', 'description', 'icon',
|
||||||
'kind_id', # Sub 14b — replaces default_kind
|
'kind_id', # Sub 14b - replaces default_kind
|
||||||
'requires_signoff', 'requires_predecessor_done',
|
'requires_signoff', 'requires_predecessor_done',
|
||||||
'parallel_start', # Sub 13
|
'parallel_start', # Sub 13
|
||||||
'triggers_workflow_state_id', # Sub 14
|
'triggers_workflow_state_id', # Sub 14
|
||||||
@@ -705,14 +705,14 @@ class SimpleRecipeController(http.Controller):
|
|||||||
|
|
||||||
Naive version (pre-19.0.20.5.0): renumber the entire flat list
|
Naive version (pre-19.0.20.5.0): renumber the entire flat list
|
||||||
1..N regardless of parent. Broke when the flat list mixed
|
1..N regardless of parent. Broke when the flat list mixed
|
||||||
operations and substeps — siblings got out-of-order numbers
|
operations and substeps - siblings got out-of-order numbers
|
||||||
because the list interleaved them.
|
because the list interleaved them.
|
||||||
|
|
||||||
New version: group node ids by their parent_id, then renumber
|
New version: group node ids by their parent_id, then renumber
|
||||||
within each parent. Substeps stay sequenced under their
|
within each parent. Substeps stay sequenced under their
|
||||||
operation; operations stay sequenced under the recipe / sub-
|
operation; operations stay sequenced under the recipe / sub-
|
||||||
process. Drop-across-parent shows up as a same-position no-op
|
process. Drop-across-parent shows up as a same-position no-op
|
||||||
— the UI's Promote/Demote buttons are the way to change
|
- the UI's Promote/Demote buttons are the way to change
|
||||||
parents.
|
parents.
|
||||||
"""
|
"""
|
||||||
Node = request.env['fusion.plating.process.node']
|
Node = request.env['fusion.plating.process.node']
|
||||||
@@ -782,7 +782,7 @@ class SimpleRecipeController(http.Controller):
|
|||||||
If ``target_op_id`` is provided, the node becomes a substep of
|
If ``target_op_id`` is provided, the node becomes a substep of
|
||||||
that operation. Otherwise it falls under the operation
|
that operation. Otherwise it falls under the operation
|
||||||
immediately preceding it in the editor list (most common case
|
immediately preceding it in the editor list (most common case
|
||||||
— author drops a header into the preceding section).
|
- author drops a header into the preceding section).
|
||||||
"""
|
"""
|
||||||
Node = request.env['fusion.plating.process.node']
|
Node = request.env['fusion.plating.process.node']
|
||||||
node = Node.browse(int(node_id))
|
node = Node.browse(int(node_id))
|
||||||
@@ -792,7 +792,7 @@ class SimpleRecipeController(http.Controller):
|
|||||||
if node.node_type != 'operation':
|
if node.node_type != 'operation':
|
||||||
return {'ok': False, 'error': 'not_an_operation',
|
return {'ok': False, 'error': 'not_an_operation',
|
||||||
'message': 'Only operations can be demoted to substeps.'}
|
'message': 'Only operations can be demoted to substeps.'}
|
||||||
# Substeps of operations don't recurse further — bail if this
|
# Substeps of operations don't recurse further - bail if this
|
||||||
# operation has its own step children (would lose them on demote).
|
# operation has its own step children (would lose them on demote).
|
||||||
if node.child_ids:
|
if node.child_ids:
|
||||||
return {'ok': False, 'error': 'has_children',
|
return {'ok': False, 'error': 'has_children',
|
||||||
@@ -864,7 +864,7 @@ class SimpleRecipeController(http.Controller):
|
|||||||
Node = request.env['fusion.plating.process.node']
|
Node = request.env['fusion.plating.process.node']
|
||||||
new_vals = {
|
new_vals = {
|
||||||
'parent_id': target_recipe.id,
|
'parent_id': target_recipe.id,
|
||||||
# See _SNAPSHOT_FIELDS comment — operation, not step.
|
# See _SNAPSHOT_FIELDS comment - operation, not step.
|
||||||
'node_type': 'operation',
|
'node_type': 'operation',
|
||||||
'sequence': src_node.sequence,
|
'sequence': src_node.sequence,
|
||||||
'source_template_id': src_node.source_template_id.id or False,
|
'source_template_id': src_node.source_template_id.id or False,
|
||||||
@@ -894,12 +894,12 @@ class SimpleRecipeController(http.Controller):
|
|||||||
})
|
})
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Sub 12d — per-recipe configurability endpoints
|
# Sub 12d - per-recipe configurability endpoints
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
@http.route('/fp/simple_recipe/step/toggle_collect', type='jsonrpc', auth='user')
|
@http.route('/fp/simple_recipe/step/toggle_collect', type='jsonrpc', auth='user')
|
||||||
def step_toggle_collect(self, node_id, collect):
|
def step_toggle_collect(self, node_id, collect):
|
||||||
"""Master switch — toggle collect_measurements on a recipe step."""
|
"""Master switch - toggle collect_measurements on a recipe step."""
|
||||||
node = request.env['fusion.plating.process.node'].browse(int(node_id))
|
node = request.env['fusion.plating.process.node'].browse(int(node_id))
|
||||||
node.check_access('write')
|
node.check_access('write')
|
||||||
node.collect_measurements = bool(collect)
|
node.collect_measurements = bool(collect)
|
||||||
@@ -945,7 +945,7 @@ class SimpleRecipeController(http.Controller):
|
|||||||
@http.route('/fp/simple_recipe/step/remove_input', type='jsonrpc', auth='user')
|
@http.route('/fp/simple_recipe/step/remove_input', type='jsonrpc', auth='user')
|
||||||
def step_remove_input(self, input_id):
|
def step_remove_input(self, input_id):
|
||||||
"""Delete a custom prompt. Library-sourced rows are protected
|
"""Delete a custom prompt. Library-sourced rows are protected
|
||||||
— recipe authors should toggle collect=False instead of deleting."""
|
- recipe authors should toggle collect=False instead of deleting."""
|
||||||
Input = request.env['fusion.plating.process.node.input']
|
Input = request.env['fusion.plating.process.node.input']
|
||||||
rec = Input.browse(int(input_id))
|
rec = Input.browse(int(input_id))
|
||||||
if not rec.exists():
|
if not rec.exists():
|
||||||
@@ -961,7 +961,7 @@ class SimpleRecipeController(http.Controller):
|
|||||||
return {'ok': True}
|
return {'ok': True}
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Step instruction images — recipe authors attach reference photos
|
# Step instruction images - recipe authors attach reference photos
|
||||||
# / screenshots / diagrams to a step from the Simple Editor's inline
|
# / screenshots / diagrams to a step from the Simple Editor's inline
|
||||||
# edit panel. Operators see them on the Record Inputs dialog and
|
# edit panel. Operators see them on the Record Inputs dialog and
|
||||||
# the step quick-look modal at runtime.
|
# the step quick-look modal at runtime.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!--
|
<!--
|
||||||
Copyright 2026 Nexa Systems Inc. — DEMO DATA (temporary)
|
Copyright 2026 Nexa Systems Inc. - DEMO DATA (temporary)
|
||||||
Remove this file and its manifest entry before production release.
|
Remove this file and its manifest entry before production release.
|
||||||
-->
|
-->
|
||||||
<odoo noupdate="1">
|
<odoo noupdate="1">
|
||||||
@@ -35,13 +35,13 @@
|
|||||||
|
|
||||||
<!-- ========== FACILITIES ========== -->
|
<!-- ========== FACILITIES ========== -->
|
||||||
<record id="demo_facility_main" model="fusion.plating.facility">
|
<record id="demo_facility_main" model="fusion.plating.facility">
|
||||||
<field name="name">Fusion Plating — Main Plant</field>
|
<field name="name">Fusion Plating - Main Plant</field>
|
||||||
<field name="code">FP-MAIN</field>
|
<field name="code">FP-MAIN</field>
|
||||||
<field name="sequence">10</field>
|
<field name="sequence">10</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="demo_facility_east" model="fusion.plating.facility">
|
<record id="demo_facility_east" model="fusion.plating.facility">
|
||||||
<field name="name">Fusion Plating — East Annex</field>
|
<field name="name">Fusion Plating - East Annex</field>
|
||||||
<field name="code">FP-EAST</field>
|
<field name="code">FP-EAST</field>
|
||||||
<field name="sequence">20</field>
|
<field name="sequence">20</field>
|
||||||
</record>
|
</record>
|
||||||
@@ -85,7 +85,7 @@
|
|||||||
<!-- ========== TANKS ========== -->
|
<!-- ========== TANKS ========== -->
|
||||||
<!-- EN Line -->
|
<!-- EN Line -->
|
||||||
<record id="demo_tank_en1" model="fusion.plating.tank">
|
<record id="demo_tank_en1" model="fusion.plating.tank">
|
||||||
<field name="name">EN Tank 1 — Mid-Phos</field>
|
<field name="name">EN Tank 1 - Mid-Phos</field>
|
||||||
<field name="code">T-EN-01</field>
|
<field name="code">T-EN-01</field>
|
||||||
<field name="facility_id" ref="demo_facility_main"/>
|
<field name="facility_id" ref="demo_facility_main"/>
|
||||||
<field name="work_center_id" ref="demo_wc_en_line"/>
|
<field name="work_center_id" ref="demo_wc_en_line"/>
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="demo_tank_en2" model="fusion.plating.tank">
|
<record id="demo_tank_en2" model="fusion.plating.tank">
|
||||||
<field name="name">EN Tank 2 — High-Phos</field>
|
<field name="name">EN Tank 2 - High-Phos</field>
|
||||||
<field name="code">T-EN-02</field>
|
<field name="code">T-EN-02</field>
|
||||||
<field name="facility_id" ref="demo_facility_main"/>
|
<field name="facility_id" ref="demo_facility_main"/>
|
||||||
<field name="work_center_id" ref="demo_wc_en_line"/>
|
<field name="work_center_id" ref="demo_wc_en_line"/>
|
||||||
@@ -212,7 +212,7 @@
|
|||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="demo_tank_an_dye" model="fusion.plating.tank">
|
<record id="demo_tank_an_dye" model="fusion.plating.tank">
|
||||||
<field name="name">Dye Immersion Tank — Black</field>
|
<field name="name">Dye Immersion Tank - Black</field>
|
||||||
<field name="code">T-AN-DYE</field>
|
<field name="code">T-AN-DYE</field>
|
||||||
<field name="facility_id" ref="demo_facility_main"/>
|
<field name="facility_id" ref="demo_facility_main"/>
|
||||||
<field name="work_center_id" ref="demo_wc_anodize_line"/>
|
<field name="work_center_id" ref="demo_wc_anodize_line"/>
|
||||||
|
|||||||
@@ -2,14 +2,14 @@
|
|||||||
<!--
|
<!--
|
||||||
Copyright 2026 Nexa Systems Inc.
|
Copyright 2026 Nexa Systems Inc.
|
||||||
License OPL-1 (Odoo Proprietary License v1.0)
|
License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
Demo recipe: Electroless Nickel Plating — Steel Line
|
Demo recipe: Electroless Nickel Plating - Steel Line
|
||||||
-->
|
-->
|
||||||
<odoo>
|
<odoo>
|
||||||
<data noupdate="1">
|
<data noupdate="1">
|
||||||
|
|
||||||
<!-- ===== ROOT: Electroless Nickel Plating — Steel Line ===== -->
|
<!-- ===== ROOT: Electroless Nickel Plating - Steel Line ===== -->
|
||||||
<record id="demo_recipe_en_steel" model="fusion.plating.process.node">
|
<record id="demo_recipe_en_steel" model="fusion.plating.process.node">
|
||||||
<field name="name">Electroless Nickel Plating — Steel Line</field>
|
<field name="name">Electroless Nickel Plating - Steel Line</field>
|
||||||
<field name="code">EN_STEEL</field>
|
<field name="code">EN_STEEL</field>
|
||||||
<field name="node_type">recipe</field>
|
<field name="node_type">recipe</field>
|
||||||
<field name="icon">fa-flask</field>
|
<field name="icon">fa-flask</field>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!-- noupdate="1" is REQUIRED — without it, every -u fusion_plating
|
<!-- noupdate="1" is REQUIRED - without it, every -u fusion_plating
|
||||||
resets number_next back to 1, which corrupts the live sequence
|
resets number_next back to 1, which corrupts the live sequence
|
||||||
on every module update. Matches the convention in fp_sequence_data.xml. -->
|
on every module update. Matches the convention in fp_sequence_data.xml. -->
|
||||||
<odoo noupdate="1">
|
<odoo noupdate="1">
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
License OPL-1 (Odoo Proprietary License v1.0)
|
License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
Part of the Fusion Plating product family.
|
Part of the Fusion Plating product family.
|
||||||
|
|
||||||
Phase 1 — Plating landing-page resolver.
|
Phase 1 - Plating landing-page resolver.
|
||||||
|
|
||||||
The Plating app's root menu (menu_fp_root) calls this server action
|
The Plating app's root menu (menu_fp_root) calls this server action
|
||||||
on click. It resolves which window action to open in this priority
|
on click. It resolves which window action to open in this priority
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
<odoo noupdate="0">
|
<odoo noupdate="0">
|
||||||
|
|
||||||
<record id="action_fp_resolve_plating_landing" model="ir.actions.server">
|
<record id="action_fp_resolve_plating_landing" model="ir.actions.server">
|
||||||
<field name="name">Plating — Open Landing Page</field>
|
<field name="name">Plating - Open Landing Page</field>
|
||||||
<field name="model_id" ref="base.model_res_users"/>
|
<field name="model_id" ref="base.model_res_users"/>
|
||||||
<field name="state">code</field>
|
<field name="state">code</field>
|
||||||
<field name="code"><![CDATA[
|
<field name="code"><![CDATA[
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
Part of the Fusion Plating product family.
|
Part of the Fusion Plating product family.
|
||||||
|
|
||||||
Seed process categories. Categories are the one pinch of generic
|
Seed process categories. Categories are the one pinch of generic
|
||||||
taxonomy core ships with — specific process types themselves are
|
taxonomy core ships with - specific process types themselves are
|
||||||
loaded by process packs (fusion_plating_process_en, etc.).
|
loaded by process packs (fusion_plating_process_en, etc.).
|
||||||
-->
|
-->
|
||||||
<odoo noupdate="1">
|
<odoo noupdate="1">
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
<field name="name">Preparation</field>
|
<field name="name">Preparation</field>
|
||||||
<field name="code">prep</field>
|
<field name="code">prep</field>
|
||||||
<field name="sequence">50</field>
|
<field name="sequence">50</field>
|
||||||
<field name="description">Cleaning, degreasing, etching, activation — surface prep before the main finishing step.</field>
|
<field name="description">Cleaning, degreasing, etching, activation - surface prep before the main finishing step.</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="pcat_strip" model="fusion.plating.process.category">
|
<record id="pcat_strip" model="fusion.plating.process.category">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<!--
|
<!--
|
||||||
Copyright 2026 Nexa Systems Inc.
|
Copyright 2026 Nexa Systems Inc.
|
||||||
License OPL-1 (Odoo Proprietary License v1.0)
|
License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
Recipe: ANODIZE (Sulfuric Anodize — Type II)
|
Recipe: ANODIZE (Sulfuric Anodize - Type II)
|
||||||
Source: Client's Steelhead export (April 2026 transcription).
|
Source: Client's Steelhead export (April 2026 transcription).
|
||||||
|
|
||||||
Anodize is an umbrella workflow covering pre-treatment, the wet
|
Anodize is an umbrella workflow covering pre-treatment, the wet
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<!--
|
<!--
|
||||||
Copyright 2026 Nexa Systems Inc.
|
Copyright 2026 Nexa Systems Inc.
|
||||||
License OPL-1 (Odoo Proprietary License v1.0)
|
License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
Recipe: ENP-ALUM-BASIC (Electroless Nickel Plating — Aluminium Basic)
|
Recipe: ENP-ALUM-BASIC (Electroless Nickel Plating - Aluminium Basic)
|
||||||
Source: Client's Steelhead export
|
Source: Client's Steelhead export
|
||||||
-->
|
-->
|
||||||
<odoo>
|
<odoo>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
Notes:
|
Notes:
|
||||||
- Steelhead allows a node to appear at multiple positions in the
|
- Steelhead allows a node to appear at multiple positions in the
|
||||||
same recipe ("occurrence #2"). Our parent_id model is strict
|
same recipe ("occurrence #2"). Our parent_id model is strict
|
||||||
single-parent so we duplicate the node — see "Oven baking"
|
single-parent so we duplicate the node - see "Oven baking"
|
||||||
appearing first as #1 and again later in the flow.
|
appearing first as #1 and again later in the flow.
|
||||||
- The Electroless Nickel Plating sub-process holds the wet line;
|
- The Electroless Nickel Plating sub-process holds the wet line;
|
||||||
everything inside it is a separate plating step (cleaner →
|
everything inside it is a separate plating step (cleaner →
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
Tree:
|
Tree:
|
||||||
ENP-SP (recipe)
|
ENP-SP (recipe)
|
||||||
├── Oven baking (op, signoff, auto) — first oven cycle
|
├── Oven baking (op, signoff, auto) - first oven cycle
|
||||||
│ ├── Ready for bake
|
│ ├── Ready for bake
|
||||||
│ └── Bake (customer-visible)
|
│ └── Bake (customer-visible)
|
||||||
├── Adhesion Test Coupon (op, opt-out)
|
├── Adhesion Test Coupon (op, opt-out)
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
│ ├── E-Nickel Plate (Hi-Phos) (SP-8) (opt-out)
|
│ ├── E-Nickel Plate (Hi-Phos) (SP-8) (opt-out)
|
||||||
│ │ └── Rinse (SP-11)
|
│ │ └── Rinse (SP-11)
|
||||||
│ └── Drying
|
│ └── Drying
|
||||||
├── Oven baking (op, signoff, auto) — second oven cycle (#2)
|
├── Oven baking (op, signoff, auto) - second oven cycle (#2)
|
||||||
│ ├── Ready for bake
|
│ ├── Ready for bake
|
||||||
│ └── Bake (customer-visible)
|
│ └── Bake (customer-visible)
|
||||||
├── De-racking (op, auto)
|
├── De-racking (op, auto)
|
||||||
@@ -310,7 +310,7 @@
|
|||||||
<field name="customer_visible">True</field>
|
<field name="customer_visible">True</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<!-- 7e. E-Nickel Plate (Hi-Phos) (SP-8) — opt-out -->
|
<!-- 7e. E-Nickel Plate (Hi-Phos) (SP-8) - opt-out -->
|
||||||
<record id="enp_sp_enp_hi_phos" model="fusion.plating.process.node">
|
<record id="enp_sp_enp_hi_phos" model="fusion.plating.process.node">
|
||||||
<field name="name">E-Nickel Plate (Hi-Phos) (SP-8)</field>
|
<field name="name">E-Nickel Plate (Hi-Phos) (SP-8)</field>
|
||||||
<field name="node_type">operation</field>
|
<field name="node_type">operation</field>
|
||||||
@@ -343,7 +343,7 @@
|
|||||||
<!-- ========================= 8. Post-plate Bake (H2 Embrittlement Relief) =========================
|
<!-- ========================= 8. Post-plate Bake (H2 Embrittlement Relief) =========================
|
||||||
Drives out hydrogen absorbed during plating. Must START within
|
Drives out hydrogen absorbed during plating. Must START within
|
||||||
~4 hours of plate exit (ASTM B850 / AMS 2759). Parts still on
|
~4 hours of plate exit (ASTM B850 / AMS 2759). Parts still on
|
||||||
rack — de-rack happens after this bake. -->
|
rack - de-rack happens after this bake. -->
|
||||||
<record id="enp_sp_oven_baking_2" model="fusion.plating.process.node">
|
<record id="enp_sp_oven_baking_2" model="fusion.plating.process.node">
|
||||||
<field name="name">Post-plate Bake (H2 Embrittlement Relief)</field>
|
<field name="name">Post-plate Bake (H2 Embrittlement Relief)</field>
|
||||||
<field name="node_type">operation</field>
|
<field name="node_type">operation</field>
|
||||||
|
|||||||
@@ -248,7 +248,7 @@
|
|||||||
<field name="customer_visible">True</field>
|
<field name="customer_visible">True</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<!-- 5d. E-Nickel Plate (Mid Phos)(S-9) — opt-out (chosen per job) -->
|
<!-- 5d. E-Nickel Plate (Mid Phos)(S-9) - opt-out (chosen per job) -->
|
||||||
<record id="enp_sb_sl_enp_mid_phos" model="fusion.plating.process.node">
|
<record id="enp_sb_sl_enp_mid_phos" model="fusion.plating.process.node">
|
||||||
<field name="name">E-Nickel Plate (Mid Phos)(S-9)</field>
|
<field name="name">E-Nickel Plate (Mid Phos)(S-9)</field>
|
||||||
<field name="node_type">operation</field>
|
<field name="node_type">operation</field>
|
||||||
@@ -276,7 +276,7 @@
|
|||||||
<field name="customer_visible">True</field>
|
<field name="customer_visible">True</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<!-- 5e. E-Nickel Plate (S-10) — primary plating -->
|
<!-- 5e. E-Nickel Plate (S-10) - primary plating -->
|
||||||
<record id="enp_sb_sl_enp_s10" model="fusion.plating.process.node">
|
<record id="enp_sb_sl_enp_s10" model="fusion.plating.process.node">
|
||||||
<field name="name">E-Nickel Plate (S-10)</field>
|
<field name="name">E-Nickel Plate (S-10)</field>
|
||||||
<field name="node_type">operation</field>
|
<field name="node_type">operation</field>
|
||||||
@@ -328,7 +328,7 @@
|
|||||||
<!-- ========================= 6. Post-plate Bake (H2 Embrittlement Relief) =========================
|
<!-- ========================= 6. Post-plate Bake (H2 Embrittlement Relief) =========================
|
||||||
Drives out hydrogen absorbed during plating. Must START within
|
Drives out hydrogen absorbed during plating. Must START within
|
||||||
~4 hours of plate exit (ASTM B850 / AMS 2759). Parts still on
|
~4 hours of plate exit (ASTM B850 / AMS 2759). Parts still on
|
||||||
rack — de-rack happens after this bake. -->
|
rack - de-rack happens after this bake. -->
|
||||||
<record id="enp_sb_oven_baking" model="fusion.plating.process.node">
|
<record id="enp_sb_oven_baking" model="fusion.plating.process.node">
|
||||||
<field name="name">Post-plate Bake (H2 Embrittlement Relief)</field>
|
<field name="name">Post-plate Bake (H2 Embrittlement Relief)</field>
|
||||||
<field name="node_type">operation</field>
|
<field name="node_type">operation</field>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<!--
|
<!--
|
||||||
Copyright 2026 Nexa Systems Inc.
|
Copyright 2026 Nexa Systems Inc.
|
||||||
License OPL-1 (Odoo Proprietary License v1.0)
|
License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
Recipe: GENERAL_PROCESSING (General Processing — common workflow umbrella)
|
Recipe: GENERAL_PROCESSING (General Processing - common workflow umbrella)
|
||||||
Source: Client's Steelhead export (April 2026 transcription).
|
Source: Client's Steelhead export (April 2026 transcription).
|
||||||
|
|
||||||
This is the "envelope" workflow that sits AROUND every job: contract
|
This is the "envelope" workflow that sits AROUND every job: contract
|
||||||
@@ -11,10 +11,10 @@
|
|||||||
ENP-SP, ENP-ALUM, etc.) handle the technical processing in between.
|
ENP-SP, ENP-ALUM, etc.) handle the technical processing in between.
|
||||||
|
|
||||||
Steelhead-only features that did NOT migrate (we don't have these
|
Steelhead-only features that did NOT migrate (we don't have these
|
||||||
wired up yet — let me know if you want them):
|
wired up yet - let me know if you want them):
|
||||||
- "Default Lead Time" on the recipe
|
- "Default Lead Time" on the recipe
|
||||||
- "Product" linkage from the recipe to a product/service record
|
- "Product" linkage from the recipe to a product/service record
|
||||||
- "Contract Review Users" — list of users who must approve
|
- "Contract Review Users" - list of users who must approve
|
||||||
contract review before the job can advance
|
contract review before the job can advance
|
||||||
- Treatment Groups / "Use Price Builders" hook into pricing
|
- Treatment Groups / "Use Price Builders" hook into pricing
|
||||||
|
|
||||||
|
|||||||
@@ -22,9 +22,9 @@
|
|||||||
<field name="company_id" eval="False"/>
|
<field name="company_id" eval="False"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<!-- Sub 12b — Move Parts / Move Rack chain-of-custody log -->
|
<!-- Sub 12b - Move Parts / Move Rack chain-of-custody log -->
|
||||||
<record id="seq_fp_job_step_move" model="ir.sequence">
|
<record id="seq_fp_job_step_move" model="ir.sequence">
|
||||||
<field name="name">FP — Move Log</field>
|
<field name="name">FP - Move Log</field>
|
||||||
<field name="code">fp.job.step.move</field>
|
<field name="code">fp.job.step.move</field>
|
||||||
<field name="prefix">FP/MOVE/%(year)s/</field>
|
<field name="prefix">FP/MOVE/%(year)s/</field>
|
||||||
<field name="padding">5</field>
|
<field name="padding">5</field>
|
||||||
|
|||||||
@@ -12,15 +12,15 @@
|
|||||||
adhesion_test, salt_spray, packaging, gating) are kept
|
adhesion_test, salt_spray, packaging, gating) are kept
|
||||||
in this XML for history but flipped active=False by the
|
in this XML for history but flipped active=False by the
|
||||||
migration script so they no longer appear in the
|
migration script so they no longer appear in the
|
||||||
dropdown — and bulk-remapped onto the new `other` /
|
dropdown - and bulk-remapped onto the new `other` /
|
||||||
`wet_process` kinds.
|
`wet_process` kinds.
|
||||||
- New: `other` (catch-all, default) and `wet_process`
|
- New: `other` (catch-all, default) and `wet_process`
|
||||||
(covers all bath-based steps).
|
(covers all bath-based steps).
|
||||||
- `mask` covers Masking + De-Masking, `racking` covers
|
- `mask` covers Masking + De-Masking, `racking` covers
|
||||||
Racking + De-Racking — operators differentiate by the
|
Racking + De-Racking - operators differentiate by the
|
||||||
step name.
|
step name.
|
||||||
|
|
||||||
2026-05-24 update (19.0.21.2.0 — Shop Floor live-step fix):
|
2026-05-24 update (19.0.21.2.0 - Shop Floor live-step fix):
|
||||||
- New `area_kind` field on fp.step.kind drives plant-view
|
- New `area_kind` field on fp.step.kind drives plant-view
|
||||||
column routing. Every record below carries an
|
column routing. Every record below carries an
|
||||||
area_kind. New `blast` kind for the Blasting column.
|
area_kind. New `blast` kind for the Blasting column.
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
now since they're meant to be active going forward). -->
|
now since they're meant to be active going forward). -->
|
||||||
|
|
||||||
<!-- ============================================================ -->
|
<!-- ============================================================ -->
|
||||||
<!-- ACTIVE KINDS — visible in dropdown -->
|
<!-- ACTIVE KINDS - visible in dropdown -->
|
||||||
<!-- ============================================================ -->
|
<!-- ============================================================ -->
|
||||||
|
|
||||||
<record id="step_kind_other" model="fp.step.kind">
|
<record id="step_kind_other" model="fp.step.kind">
|
||||||
@@ -224,7 +224,7 @@
|
|||||||
</record>
|
</record>
|
||||||
|
|
||||||
<!-- ============================================================
|
<!-- ============================================================
|
||||||
Default inputs per kind — 1:1 port of DEFAULT_INPUTS_BY_KIND
|
Default inputs per kind - 1:1 port of DEFAULT_INPUTS_BY_KIND
|
||||||
============================================================ -->
|
============================================================ -->
|
||||||
|
|
||||||
<!-- receiving -->
|
<!-- receiving -->
|
||||||
@@ -407,7 +407,7 @@
|
|||||||
<field name="kind_id" ref="step_kind_rinse"/>
|
<field name="kind_id" ref="step_kind_rinse"/>
|
||||||
<field name="name">Conductivity</field>
|
<field name="name">Conductivity</field>
|
||||||
<field name="input_type">number</field>
|
<field name="input_type">number</field>
|
||||||
<field name="hint">µS/cm — required for DI rinses</field>
|
<field name="hint">µS/cm - required for DI rinses</field>
|
||||||
<field name="sequence">20</field>
|
<field name="sequence">20</field>
|
||||||
</record>
|
</record>
|
||||||
<record id="step_kind_input_rinse_time" model="fp.step.kind.default.input">
|
<record id="step_kind_input_rinse_time" model="fp.step.kind.default.input">
|
||||||
@@ -499,7 +499,7 @@
|
|||||||
<field name="kind_id" ref="step_kind_plate"/>
|
<field name="kind_id" ref="step_kind_plate"/>
|
||||||
<field name="name">Current Density</field>
|
<field name="name">Current Density</field>
|
||||||
<field name="input_type">number</field>
|
<field name="input_type">number</field>
|
||||||
<field name="hint">ASF — electroplate only</field>
|
<field name="hint">ASF - electroplate only</field>
|
||||||
<field name="sequence">60</field>
|
<field name="sequence">60</field>
|
||||||
</record>
|
</record>
|
||||||
<record id="step_kind_input_plate_thick" model="fp.step.kind.default.input">
|
<record id="step_kind_input_plate_thick" model="fp.step.kind.default.input">
|
||||||
|
|||||||
@@ -103,7 +103,7 @@
|
|||||||
]]></field>
|
]]></field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<!-- 2026-05-24 additions (19.0.21.2.0 — Shop Floor live-step fix) -->
|
<!-- 2026-05-24 additions (19.0.21.2.0 - Shop Floor live-step fix) -->
|
||||||
|
|
||||||
<record id="fp_step_template_hwp_a15" model="fp.step.template">
|
<record id="fp_step_template_hwp_a15" model="fp.step.template">
|
||||||
<field name="name">Hot Water Porosity Test (A-15)</field>
|
<field name="name">Hot Water Porosity Test (A-15)</field>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
<field name="code">plating_op</field>
|
<field name="code">plating_op</field>
|
||||||
<field name="sequence">30</field>
|
<field name="sequence">30</field>
|
||||||
<field name="icon">fa-flask</field>
|
<field name="icon">fa-flask</field>
|
||||||
<field name="description">Runs the plating line — chemistry checks, dwell, thickness.</field>
|
<field name="description">Runs the plating line - chemistry checks, dwell, thickness.</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="work_role_demask" model="fp.work.role">
|
<record id="work_role_demask" model="fp.work.role">
|
||||||
|
|||||||
@@ -3,12 +3,12 @@
|
|||||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
# Part of the Fusion Plating product family.
|
# Part of the Fusion Plating product family.
|
||||||
|
|
||||||
"""19.0.12.1.0 — Convert every free-text UoM column to the curated
|
"""19.0.12.1.0 - Convert every free-text UoM column to the curated
|
||||||
selection keys defined in models/_fp_uom_selection.py.
|
selection keys defined in models/_fp_uom_selection.py.
|
||||||
|
|
||||||
Runs after fusion_plating's tables have been re-described (so the
|
Runs after fusion_plating's tables have been re-described (so the
|
||||||
columns are now Selection-typed at the ORM level), but before users
|
columns are now Selection-typed at the ORM level), but before users
|
||||||
hit the new views. Idempotent — re-running maps already-converted
|
hit the new views. Idempotent - re-running maps already-converted
|
||||||
values to themselves and leaves them in place.
|
values to themselves and leaves them in place.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ def migrate(cr, version):
|
|||||||
('fusion_plating_process_node_input', 'uom', 'process node input'),
|
('fusion_plating_process_node_input', 'uom', 'process node input'),
|
||||||
('fusion_plating_process_node_input', 'target_unit', 'process node target'),
|
('fusion_plating_process_node_input', 'target_unit', 'process node target'),
|
||||||
('fp_step_template_input', 'target_unit', 'step template input target'),
|
('fp_step_template_input', 'target_unit', 'step template input target'),
|
||||||
# compliance (only migrated when the module is installed — the
|
# compliance (only migrated when the module is installed - the
|
||||||
# helper is no-op when the table doesn't exist)
|
# helper is no-op when the table doesn't exist)
|
||||||
('fusion_plating_discharge_limit', 'uom', 'discharge limit'),
|
('fusion_plating_discharge_limit', 'uom', 'discharge limit'),
|
||||||
('fusion_plating_discharge_sample_line', 'uom', 'discharge sample line'),
|
('fusion_plating_discharge_sample_line', 'uom', 'discharge sample line'),
|
||||||
@@ -50,7 +50,7 @@ def migrate(cr, version):
|
|||||||
total_cleared += cleared
|
total_cleared += cleared
|
||||||
|
|
||||||
_logger.info(
|
_logger.info(
|
||||||
'Fusion Plating 19.0.12.1.0 — UoM migration complete: '
|
'Fusion Plating 19.0.12.1.0 - UoM migration complete: '
|
||||||
'%s rewritten, %s cleared (across %s columns).',
|
'%s rewritten, %s cleared (across %s columns).',
|
||||||
total_rewritten, total_cleared, len(targets),
|
total_rewritten, total_cleared, len(targets),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
# Part of the Fusion Plating product family.
|
# Part of the Fusion Plating product family.
|
||||||
|
|
||||||
"""19.0.12.4.0 — Step-library polish + Policy B Contract Review backfill.
|
"""19.0.12.4.0 - Step-library polish + Policy B Contract Review backfill.
|
||||||
|
|
||||||
post_init_hook only fires on fresh install. Existing DBs upgrading
|
post_init_hook only fires on fresh install. Existing DBs upgrading
|
||||||
from pre-Policy-B versions need this migration to:
|
from pre-Policy-B versions need this migration to:
|
||||||
@@ -21,12 +21,12 @@ from pre-Policy-B versions need this migration to:
|
|||||||
|
|
||||||
3. Add canonical missing entries (Soak Clean, Rinse, Etch, Acid Dip,
|
3. Add canonical missing entries (Soak Clean, Rinse, Etch, Acid Dip,
|
||||||
Drying, Inspection, Shipping, Water Break Test, Desmut, Zincate)
|
Drying, Inspection, Shipping, Water Break Test, Desmut, Zincate)
|
||||||
that ENP-ALUM-BASIC's seed didn't include — these are the names
|
that ENP-ALUM-BASIC's seed didn't include - these are the names
|
||||||
a fresh estimator would expect to find when they open the library
|
a fresh estimator would expect to find when they open the library
|
||||||
from scratch. Without them, an empty recipe has no obvious starting
|
from scratch. Without them, an empty recipe has no obvious starting
|
||||||
templates for cleaning / rinsing / standard inspection.
|
templates for cleaning / rinsing / standard inspection.
|
||||||
|
|
||||||
All three steps are idempotent — re-running on an already-fixed DB
|
All three steps are idempotent - re-running on an already-fixed DB
|
||||||
is a no-op.
|
is a no-op.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
# Part of the Fusion Plating product family.
|
# Part of the Fusion Plating product family.
|
||||||
|
|
||||||
"""19.0.12.5.0 — Backfill default_kind on existing recipe nodes.
|
"""19.0.12.5.0 - Backfill default_kind on existing recipe nodes.
|
||||||
|
|
||||||
The Page-2 audit (2026-04-28) showed that pre-Sub-12a recipe nodes
|
The Page-2 audit (2026-04-28) showed that pre-Sub-12a recipe nodes
|
||||||
have NULL `default_kind` because the field was added later. The
|
have NULL `default_kind` because the field was added later. The
|
||||||
@@ -19,7 +19,7 @@ default_kind, resolves a sensible kind via the central
|
|||||||
It also walks `fp.job.step` rows whose `kind` is the legacy 'other'
|
It also walks `fp.job.step` rows whose `kind` is the legacy 'other'
|
||||||
placeholder and re-derives `kind` from `recipe_node_id.default_kind`
|
placeholder and re-derives `kind` from `recipe_node_id.default_kind`
|
||||||
(after the node-side backfill above sets it). Non-other kinds are
|
(after the node-side backfill above sets it). Non-other kinds are
|
||||||
left alone — operator may have set them deliberately.
|
left alone - operator may have set them deliberately.
|
||||||
|
|
||||||
Idempotent.
|
Idempotent.
|
||||||
"""
|
"""
|
||||||
@@ -33,7 +33,7 @@ from odoo.addons.fusion_plating import fp_resolve_step_kind
|
|||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
# Same mapping as in fp_job.py — keep them in sync.
|
# Same mapping as in fp_job.py - keep them in sync.
|
||||||
_NODE_KIND_TO_STEP_KIND = {
|
_NODE_KIND_TO_STEP_KIND = {
|
||||||
'cleaning': 'wet',
|
'cleaning': 'wet',
|
||||||
'etch': 'wet',
|
'etch': 'wet',
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
# Part of the Fusion Plating product family.
|
# Part of the Fusion Plating product family.
|
||||||
"""
|
"""
|
||||||
Migration 19.0.18.12.0 — Sub 13 sequential step enforcement.
|
Migration 19.0.18.12.0 - Sub 13 sequential step enforcement.
|
||||||
|
|
||||||
Background:
|
Background:
|
||||||
The legacy per-step `requires_predecessor_done` opt-in defaulted to
|
The legacy per-step `requires_predecessor_done` opt-in defaulted to
|
||||||
False, so 98.7% of operations system-wide had no enforcement and
|
False, so 98.7% of operations system-wide had no enforcement and
|
||||||
operators were able to start arbitrary steps out of order (e.g. job
|
operators were able to start arbitrary steps out of order (e.g. job
|
||||||
WH/JOB/00339 — Incoming Inspection ran while Contract Review was
|
WH/JOB/00339 - Incoming Inspection ran while Contract Review was
|
||||||
still in progress).
|
still in progress).
|
||||||
|
|
||||||
This migration:
|
This migration:
|
||||||
@@ -19,7 +19,7 @@ This migration:
|
|||||||
and continue to work for any recipe whose author opts back into
|
and continue to work for any recipe whose author opts back into
|
||||||
free-flow mode (sets enforce_sequential = False).
|
free-flow mode (sets enforce_sequential = False).
|
||||||
|
|
||||||
Idempotent — safe to re-run.
|
Idempotent - safe to re-run.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@@ -29,14 +29,14 @@ _logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
def migrate(cr, version):
|
def migrate(cr, version):
|
||||||
if not version:
|
if not version:
|
||||||
return # Brand new install — defaults already correct.
|
return # Brand new install - defaults already correct.
|
||||||
|
|
||||||
_logger.info(
|
_logger.info(
|
||||||
'[migration 19.0.18.12.0] Promoting all existing recipes to '
|
'[migration 19.0.18.12.0] Promoting all existing recipes to '
|
||||||
'enforce_sequential=True (Sub 13 — sequential step enforcement).'
|
'enforce_sequential=True (Sub 13 - sequential step enforcement).'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Direct SQL — avoids loading the ORM at this stage and is fast.
|
# Direct SQL - avoids loading the ORM at this stage and is fast.
|
||||||
# We intentionally only flip nodes that are CURRENTLY False; nodes
|
# We intentionally only flip nodes that are CURRENTLY False; nodes
|
||||||
# already set True (or set by a manual install of a newer version)
|
# already set True (or set by a manual install of a newer version)
|
||||||
# are skipped so the operation is clean to re-run.
|
# are skipped so the operation is clean to re-run.
|
||||||
|
|||||||
@@ -2,19 +2,19 @@
|
|||||||
# Copyright 2026 Nexa Systems Inc.
|
# Copyright 2026 Nexa Systems Inc.
|
||||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
# Part of the Fusion Plating product family.
|
# Part of the Fusion Plating product family.
|
||||||
"""19.0.18.13.0 — Backfill kind_id from legacy default_kind text values.
|
"""19.0.18.13.0 - Backfill kind_id from legacy default_kind text values.
|
||||||
|
|
||||||
Sub 14b — `default_kind` was a Selection on fp.step.template and
|
Sub 14b - `default_kind` was a Selection on fp.step.template and
|
||||||
fusion.plating.process.node. It is now a stored related Char that
|
fusion.plating.process.node. It is now a stored related Char that
|
||||||
reads from kind_id.code on a new fp.step.kind catalog.
|
reads from kind_id.code on a new fp.step.kind catalog.
|
||||||
|
|
||||||
When a Selection field is converted into a Many2one in code, Odoo's
|
When a Selection field is converted into a Many2one in code, Odoo's
|
||||||
ORM does NOT auto-migrate the data — the new column starts empty. This
|
ORM does NOT auto-migrate the data - the new column starts empty. This
|
||||||
script reads the (still-present) text values out of the OLD `default_kind`
|
script reads the (still-present) text values out of the OLD `default_kind`
|
||||||
column and points kind_id at the seeded fp.step.kind record whose code
|
column and points kind_id at the seeded fp.step.kind record whose code
|
||||||
matches.
|
matches.
|
||||||
|
|
||||||
Idempotent — running it twice is a no-op.
|
Idempotent - running it twice is a no-op.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@@ -29,7 +29,7 @@ def migrate(cr, version):
|
|||||||
code_to_id = {code: kid for kid, code in cr.fetchall()}
|
code_to_id = {code: kid for kid, code in cr.fetchall()}
|
||||||
if not code_to_id:
|
if not code_to_id:
|
||||||
_logger.warning(
|
_logger.warning(
|
||||||
'19.0.18.13.0: fp_step_kind is empty — seed data did not '
|
'19.0.18.13.0: fp_step_kind is empty - seed data did not '
|
||||||
'load before post-migrate. Skipping backfill.',
|
'load before post-migrate. Skipping backfill.',
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
@@ -74,7 +74,7 @@ def _backfill(cr, table, text_col, m2o_col, code_to_id):
|
|||||||
"""Generic backfill helper. Reads `text_col` text values, updates
|
"""Generic backfill helper. Reads `text_col` text values, updates
|
||||||
`m2o_col` per row using the supplied code→id lookup table.
|
`m2o_col` per row using the supplied code→id lookup table.
|
||||||
"""
|
"""
|
||||||
# Defensive — if either column is missing (fresh install, never had
|
# Defensive - if either column is missing (fresh install, never had
|
||||||
# default_kind data) skip silently.
|
# default_kind data) skip silently.
|
||||||
cr.execute("""
|
cr.execute("""
|
||||||
SELECT column_name FROM information_schema.columns
|
SELECT column_name FROM information_schema.columns
|
||||||
@@ -89,7 +89,7 @@ def _backfill(cr, table, text_col, m2o_col, code_to_id):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Pull every row with a non-null default_kind that doesn't yet have
|
# Pull every row with a non-null default_kind that doesn't yet have
|
||||||
# a kind_id — leaves manually-edited rows alone if migration is
|
# a kind_id - leaves manually-edited rows alone if migration is
|
||||||
# rerun.
|
# rerun.
|
||||||
cr.execute(f"""
|
cr.execute(f"""
|
||||||
SELECT id, {text_col} FROM {table}
|
SELECT id, {text_col} FROM {table}
|
||||||
@@ -120,7 +120,7 @@ def _backfill(cr, table, text_col, m2o_col, code_to_id):
|
|||||||
updated += len(ids)
|
updated += len(ids)
|
||||||
|
|
||||||
_logger.info(
|
_logger.info(
|
||||||
'19.0.18.13.0: backfilled %s.%s on %s rows (%s skipped — code '
|
'19.0.18.13.0: backfilled %s.%s on %s rows (%s skipped - code '
|
||||||
'not found in fp_step_kind seed data)',
|
'not found in fp_step_kind seed data)',
|
||||||
table, m2o_col, updated, skipped,
|
table, m2o_col, updated, skipped,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
# Copyright 2026 Nexa Systems Inc.
|
# Copyright 2026 Nexa Systems Inc.
|
||||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
# Part of the Fusion Plating product family.
|
# Part of the Fusion Plating product family.
|
||||||
"""19.0.18.13.0 — Snapshot legacy default_kind values BEFORE the field
|
"""19.0.18.13.0 - Snapshot legacy default_kind values BEFORE the field
|
||||||
type change wipes them.
|
type change wipes them.
|
||||||
|
|
||||||
Sub 14b — `default_kind` was a Selection on fp.step.template and
|
Sub 14b - `default_kind` was a Selection on fp.step.template and
|
||||||
fusion.plating.process.node. The new model code defines it as a
|
fusion.plating.process.node. The new model code defines it as a
|
||||||
stored related Char (`related='kind_id.code', store=True`). On first
|
stored related Char (`related='kind_id.code', store=True`). On first
|
||||||
ORM access after upgrade, Odoo recomputes the stored related from
|
ORM access after upgrade, Odoo recomputes the stored related from
|
||||||
@@ -13,7 +13,7 @@ kind_id (which is NULL → wipes the column). We must snapshot the
|
|||||||
original text values now, so post-migrate can read them and resolve
|
original text values now, so post-migrate can read them and resolve
|
||||||
kind_id.
|
kind_id.
|
||||||
|
|
||||||
Idempotent — running it twice is a no-op (snapshot column gets
|
Idempotent - running it twice is a no-op (snapshot column gets
|
||||||
re-written with the same data).
|
re-written with the same data).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2026 Nexa Systems Inc.
|
# Copyright 2026 Nexa Systems Inc.
|
||||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
"""Post-migration for 19.0.18.7.0 — Step Library audit expansion.
|
"""Post-migration for 19.0.18.7.0 - Step Library audit expansion.
|
||||||
|
|
||||||
1. Default `collect=True` on all existing recipe-step inputs.
|
1. Default `collect=True` on all existing recipe-step inputs.
|
||||||
2. Default `collect_measurements=True` on all existing recipe steps.
|
2. Default `collect_measurements=True` on all existing recipe steps.
|
||||||
3. Re-run action_seed_default_inputs on every existing template to
|
3. Re-run action_seed_default_inputs on every existing template to
|
||||||
pull in the newly-added prompts (idempotent — skips rows whose
|
pull in the newly-added prompts (idempotent - skips rows whose
|
||||||
name is already present, so user edits survive).
|
name is already present, so user edits survive).
|
||||||
4. Backfill template_input_id by name-matching against the linked
|
4. Backfill template_input_id by name-matching against the linked
|
||||||
library template (best-effort).
|
library template (best-effort).
|
||||||
@@ -55,7 +55,7 @@ def migrate(cr, version):
|
|||||||
)
|
)
|
||||||
_logger.info("Re-seeded defaults on %s templates", len(templates))
|
_logger.info("Re-seeded defaults on %s templates", len(templates))
|
||||||
|
|
||||||
# 4. Backfill template_input_id — name-match recipe-node inputs against
|
# 4. Backfill template_input_id - name-match recipe-node inputs against
|
||||||
# their parent recipe's source library template.
|
# their parent recipe's source library template.
|
||||||
# Note: fusion_plating_process_node_input.name is plain varchar;
|
# Note: fusion_plating_process_node_input.name is plain varchar;
|
||||||
# fp_step_template_input.name is translatable JSONB (use ->>'en_US').
|
# fp_step_template_input.name is translatable JSONB (use ->>'en_US').
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2026 Nexa Systems Inc.
|
# Copyright 2026 Nexa Systems Inc.
|
||||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
"""Post-migration for 19.0.18.8.0 — Simple Editor node_type fix.
|
"""Post-migration for 19.0.18.8.0 - Simple Editor node_type fix.
|
||||||
|
|
||||||
Background
|
Background
|
||||||
----------
|
----------
|
||||||
@@ -10,14 +10,14 @@ into a recipe with `node_type='step'` directly under the recipe root.
|
|||||||
But fp.job._generate_steps() (in fusion_plating_jobs/models/fp_job.py)
|
But fp.job._generate_steps() (in fusion_plating_jobs/models/fp_job.py)
|
||||||
only creates fp.job.step rows for nodes whose node_type is 'operation'.
|
only creates fp.job.step rows for nodes whose node_type is 'operation'.
|
||||||
Top-level 'step' children of a recipe were silently skipped, meaning
|
Top-level 'step' children of a recipe were silently skipped, meaning
|
||||||
Simple-Editor recipes generated zero job steps — no traveller content,
|
Simple-Editor recipes generated zero job steps - no traveller content,
|
||||||
no shopfloor tablet entries, no CoC moves.
|
no shopfloor tablet entries, no CoC moves.
|
||||||
|
|
||||||
Fix
|
Fix
|
||||||
---
|
---
|
||||||
Promote every `step` node whose direct parent is a `recipe` to
|
Promote every `step` node whose direct parent is a `recipe` to
|
||||||
`node_type='operation'`. Tree-editor authored 'step' nodes (which sit
|
`node_type='operation'`. Tree-editor authored 'step' nodes (which sit
|
||||||
under `operation` parents) are left untouched — the filter is on
|
under `operation` parents) are left untouched - the filter is on
|
||||||
`parent.node_type='recipe'`.
|
`parent.node_type='recipe'`.
|
||||||
|
|
||||||
Idempotent: a second run finds no `step` children of recipes and is
|
Idempotent: a second run finds no `step` children of recipes and is
|
||||||
@@ -39,7 +39,7 @@ _AUDIT_BODY = Markup(
|
|||||||
'<p><strong>Recipe migrated to v19.0.18.8.0 step layout.</strong></p>'
|
'<p><strong>Recipe migrated to v19.0.18.8.0 step layout.</strong></p>'
|
||||||
'<p>Step nodes that were direct children of this recipe (Simple '
|
'<p>Step nodes that were direct children of this recipe (Simple '
|
||||||
'Editor authoring) have been promoted to operation nodes so they '
|
'Editor authoring) have been promoted to operation nodes so they '
|
||||||
'generate work-order steps correctly. No data was lost — only '
|
'generate work-order steps correctly. No data was lost - only '
|
||||||
'<code>node_type</code> changed.</p>'
|
'<code>node_type</code> changed.</p>'
|
||||||
'<p>If this recipe was authored via the Tree Editor with explicit '
|
'<p>If this recipe was authored via the Tree Editor with explicit '
|
||||||
'sub-process / operation hierarchy, this migration was a no-op '
|
'sub-process / operation hierarchy, this migration was a no-op '
|
||||||
@@ -73,7 +73,7 @@ def migrate(cr, version):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Flip the node_type. Filter is intentionally narrow — only direct
|
# Flip the node_type. Filter is intentionally narrow - only direct
|
||||||
# children of a recipe get promoted. Tree-editor sub-step rows
|
# children of a recipe get promoted. Tree-editor sub-step rows
|
||||||
# (parent.node_type='operation') are untouched.
|
# (parent.node_type='operation') are untouched.
|
||||||
cr.execute("""
|
cr.execute("""
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# Copyright 2026 Nexa Systems Inc.
|
# Copyright 2026 Nexa Systems Inc.
|
||||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
#
|
#
|
||||||
# 2026-05-20 Step Kind curation — post-migrate.
|
# 2026-05-20 Step Kind curation - post-migrate.
|
||||||
#
|
#
|
||||||
# Runs AFTER the schema settles. Marks the 15 retired kinds inactive so
|
# Runs AFTER the schema settles. Marks the 15 retired kinds inactive so
|
||||||
# they no longer appear in the dropdown. We keep them in the DB rather
|
# they no longer appear in the dropdown. We keep them in the DB rather
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
#
|
#
|
||||||
# Pre-migrate has already re-mapped every template + node pointing at
|
# Pre-migrate has already re-mapped every template + node pointing at
|
||||||
# these kinds, so flipping active=False has no operator-facing data
|
# these kinds, so flipping active=False has no operator-facing data
|
||||||
# impact — it only hides them from pickers.
|
# impact - it only hides them from pickers.
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# Copyright 2026 Nexa Systems Inc.
|
# Copyright 2026 Nexa Systems Inc.
|
||||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
#
|
#
|
||||||
# 2026-05-20 Step Kind curation — pre-migrate.
|
# 2026-05-20 Step Kind curation - pre-migrate.
|
||||||
#
|
#
|
||||||
# Runs BEFORE the model schema is applied so `kind_id` can become
|
# Runs BEFORE the model schema is applied so `kind_id` can become
|
||||||
# required=True without choking on existing NULL rows. Three jobs:
|
# required=True without choking on existing NULL rows. Three jobs:
|
||||||
@@ -37,7 +37,7 @@ import logging
|
|||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
# -- Remap table — retired-kind code -> new-kind code ----------------------
|
# -- Remap table - retired-kind code -> new-kind code ----------------------
|
||||||
# IMPORTANT: `plate` stays active (its own milestone trigger). Only the
|
# IMPORTANT: `plate` stays active (its own milestone trigger). Only the
|
||||||
# wet-bath specialisations roll up into wet_process.
|
# wet-bath specialisations roll up into wet_process.
|
||||||
_REMAP = {
|
_REMAP = {
|
||||||
@@ -60,7 +60,7 @@ _REMAP = {
|
|||||||
|
|
||||||
# -- Name-match heuristic for NULL backfill --------------------------------
|
# -- Name-match heuristic for NULL backfill --------------------------------
|
||||||
# Each rule: (substring to match in lower(name), target kind code). First
|
# Each rule: (substring to match in lower(name), target kind code). First
|
||||||
# match wins. Order matters — more specific patterns come first.
|
# match wins. Order matters - more specific patterns come first.
|
||||||
_NAME_HEURISTIC = [
|
_NAME_HEURISTIC = [
|
||||||
# Most specific
|
# Most specific
|
||||||
('qa-005', 'contract_review'),
|
('qa-005', 'contract_review'),
|
||||||
@@ -111,7 +111,7 @@ _NAME_HEURISTIC = [
|
|||||||
('dry', 'wet_process'),
|
('dry', 'wet_process'),
|
||||||
('water break', 'wet_process'),
|
('water break', 'wet_process'),
|
||||||
('wbf', 'wet_process'),
|
('wbf', 'wet_process'),
|
||||||
# Gating / ready / wait — soft sequencers, no behaviour
|
# Gating / ready / wait - soft sequencers, no behaviour
|
||||||
('ready for', 'other'),
|
('ready for', 'other'),
|
||||||
('ready to', 'other'),
|
('ready to', 'other'),
|
||||||
]
|
]
|
||||||
@@ -127,19 +127,19 @@ def migrate(cr, version):
|
|||||||
cr.execute("SELECT id, code FROM fp_step_kind")
|
cr.execute("SELECT id, code FROM fp_step_kind")
|
||||||
by_code = {code: kid for kid, code in cr.fetchall()}
|
by_code = {code: kid for kid, code in cr.fetchall()}
|
||||||
if 'other' not in by_code:
|
if 'other' not in by_code:
|
||||||
_logger.error('pre-migrate: `other` kind missing after _ensure_kind — aborting')
|
_logger.error('pre-migrate: `other` kind missing after _ensure_kind - aborting')
|
||||||
return
|
return
|
||||||
other_id = by_code['other']
|
other_id = by_code['other']
|
||||||
|
|
||||||
# 3. Re-map references to retired kinds.
|
# 3. Re-map references to retired kinds.
|
||||||
# `default_kind` is a stored related on `kind_id.code` — updating
|
# `default_kind` is a stored related on `kind_id.code` - updating
|
||||||
# kind_id via SQL doesn't auto-recompute the stored copy, so we
|
# kind_id via SQL doesn't auto-recompute the stored copy, so we
|
||||||
# write both columns together.
|
# write both columns together.
|
||||||
for retired_code, new_code in _REMAP.items():
|
for retired_code, new_code in _REMAP.items():
|
||||||
retired_id = by_code.get(retired_code)
|
retired_id = by_code.get(retired_code)
|
||||||
new_id = by_code.get(new_code) or other_id
|
new_id = by_code.get(new_code) or other_id
|
||||||
if not retired_id:
|
if not retired_id:
|
||||||
continue # not in this DB — nothing to remap
|
continue # not in this DB - nothing to remap
|
||||||
cr.execute("""
|
cr.execute("""
|
||||||
UPDATE fp_step_template
|
UPDATE fp_step_template
|
||||||
SET kind_id = %s, default_kind = %s
|
SET kind_id = %s, default_kind = %s
|
||||||
@@ -197,7 +197,7 @@ def migrate(cr, version):
|
|||||||
(kid, by_id.get(kid, 'other'), ids),
|
(kid, by_id.get(kid, 'other'), ids),
|
||||||
)
|
)
|
||||||
_logger.info(
|
_logger.info(
|
||||||
'Step Kind curation: backfilled %d %s row(s) — '
|
'Step Kind curation: backfilled %d %s row(s) - '
|
||||||
'distribution: %s',
|
'distribution: %s',
|
||||||
len(rows), table,
|
len(rows), table,
|
||||||
{next(c for c, i in by_code.items() if i == k): len(v)
|
{next(c for c, i in by_code.items() if i == k): len(v)
|
||||||
@@ -224,7 +224,7 @@ def _ensure_kind(cr, code, name, icon, sequence):
|
|||||||
|
|
||||||
Also registers the ir.model.data entry so the subsequent XML data
|
Also registers the ir.model.data entry so the subsequent XML data
|
||||||
load (which runs AFTER pre-migrate) sees the xmlid as already
|
load (which runs AFTER pre-migrate) sees the xmlid as already
|
||||||
bound and skips creation — otherwise we get duplicate records.
|
bound and skips creation - otherwise we get duplicate records.
|
||||||
"""
|
"""
|
||||||
cr.execute("SELECT id FROM fp_step_kind WHERE code = %s", (code,))
|
cr.execute("SELECT id FROM fp_step_kind WHERE code = %s", (code,))
|
||||||
row = cr.fetchone()
|
row = cr.fetchone()
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# Copyright 2026 Nexa Systems Inc.
|
# Copyright 2026 Nexa Systems Inc.
|
||||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
#
|
#
|
||||||
# 19.0.21.0.0 — Plant-view Shop Floor kanban redesign.
|
# 19.0.21.0.0 - Plant-view Shop Floor kanban redesign.
|
||||||
# Backfill fp.work.centre.area_kind from the existing `kind` taxonomy so
|
# Backfill fp.work.centre.area_kind from the existing `kind` taxonomy so
|
||||||
# every routing station has a defined Floor Column on day 1. Admins can
|
# every routing station has a defined Floor Column on day 1. Admins can
|
||||||
# override afterwards via Configuration → Shop Setup → Routing Stations.
|
# override afterwards via Configuration → Shop Setup → Routing Stations.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""Phase H: fire role-migration preview creation on `-u fusion_plating`.
|
"""Phase H: fire role-migration preview creation on `-u fusion_plating`.
|
||||||
|
|
||||||
Odoo 19's `post_init_hook` ONLY fires on fresh install — never on
|
Odoo 19's `post_init_hook` ONLY fires on fresh install - never on
|
||||||
upgrade. So on entech (and any other already-installed deployment),
|
upgrade. So on entech (and any other already-installed deployment),
|
||||||
`-u fusion_plating` after this branch lands would otherwise leave the
|
`-u fusion_plating` after this branch lands would otherwise leave the
|
||||||
post_init_hook's `_fp_post_init_role_migration` un-fired and the
|
post_init_hook's `_fp_post_init_role_migration` un-fired and the
|
||||||
@@ -27,7 +27,7 @@ def migrate(cr, version):
|
|||||||
'Fusion Plating: role-migration preview check ran via post-migrate.py'
|
'Fusion Plating: role-migration preview check ran via post-migrate.py'
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Migration scripts must not block module upgrade — log and swallow
|
# Migration scripts must not block module upgrade - log and swallow
|
||||||
_logger.exception(
|
_logger.exception(
|
||||||
'Failed to run role-migration preview check (non-fatal): %s', e
|
'Failed to run role-migration preview check (non-fatal): %s', e
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2026 Nexa Systems Inc.
|
# Copyright 2026 Nexa Systems Inc.
|
||||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
"""19.0.21.2.0 — Shop Floor live-step + kind taxonomy.
|
"""19.0.21.2.0 - Shop Floor live-step + kind taxonomy.
|
||||||
|
|
||||||
Seeds fp.step.kind.area_kind on existing kinds BEFORE the required
|
Seeds fp.step.kind.area_kind on existing kinds BEFORE the required
|
||||||
NOT NULL constraint on the new field hits the schema. Also activates
|
NOT NULL constraint on the new field hits the schema. Also activates
|
||||||
@@ -50,14 +50,14 @@ KIND_TO_AREA = {
|
|||||||
|
|
||||||
|
|
||||||
def migrate(cr, version):
|
def migrate(cr, version):
|
||||||
# Phase 1 — Pre-create the column NULL-permitting so we can seed it
|
# Phase 1 - Pre-create the column NULL-permitting so we can seed it
|
||||||
# BEFORE Odoo's schema sync tries to enforce NOT NULL.
|
# BEFORE Odoo's schema sync tries to enforce NOT NULL.
|
||||||
cr.execute(
|
cr.execute(
|
||||||
"ALTER TABLE fp_step_kind "
|
"ALTER TABLE fp_step_kind "
|
||||||
"ADD COLUMN IF NOT EXISTS area_kind VARCHAR"
|
"ADD COLUMN IF NOT EXISTS area_kind VARCHAR"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Phase 2 — Seed area_kind on existing kinds. Idempotent: only fills
|
# Phase 2 - Seed area_kind on existing kinds. Idempotent: only fills
|
||||||
# NULLs, so re-running -u is safe.
|
# NULLs, so re-running -u is safe.
|
||||||
seeded = 0
|
seeded = 0
|
||||||
for code, area in KIND_TO_AREA.items():
|
for code, area in KIND_TO_AREA.items():
|
||||||
@@ -73,7 +73,7 @@ def migrate(cr, version):
|
|||||||
seeded,
|
seeded,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Phase 3 — Fallback: any user-created custom kinds not in our seed
|
# Phase 3 - Fallback: any user-created custom kinds not in our seed
|
||||||
# map → 'plating'. Clears the NOT NULL constraint for any leftover.
|
# map → 'plating'. Clears the NOT NULL constraint for any leftover.
|
||||||
cr.execute(
|
cr.execute(
|
||||||
"UPDATE fp_step_kind SET area_kind = 'plating' "
|
"UPDATE fp_step_kind SET area_kind = 'plating' "
|
||||||
@@ -85,7 +85,7 @@ def migrate(cr, version):
|
|||||||
cr.rowcount,
|
cr.rowcount,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Phase 4 — Activate kinds we need for full coverage.
|
# Phase 4 - Activate kinds we need for full coverage.
|
||||||
activated = 0
|
activated = 0
|
||||||
for code in ('derack', 'demask', 'gating'):
|
for code in ('derack', 'demask', 'gating'):
|
||||||
cr.execute(
|
cr.execute(
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2026 Nexa Systems Inc.
|
# Copyright 2026 Nexa Systems Inc.
|
||||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
"""19.0.21.4.0 — Apply office-user menu visibility on -u.
|
"""19.0.21.4.0 - Apply office-user menu visibility on -u.
|
||||||
|
|
||||||
post_init_hook only fires on FIRST install (CLAUDE.md Rule 13d).
|
post_init_hook only fires on FIRST install (CLAUDE.md Rule 13d).
|
||||||
This script runs the same helper on every -u so existing installs
|
This script runs the same helper on every -u so existing installs
|
||||||
get the menu restrictions applied without needing to uninstall +
|
get the menu restrictions applied without needing to uninstall +
|
||||||
reinstall. Idempotent — the helper checks current state and skips
|
reinstall. Idempotent - the helper checks current state and skips
|
||||||
already-restricted menus.
|
already-restricted menus.
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2026 Nexa Systems Inc.
|
# Copyright 2026 Nexa Systems Inc.
|
||||||
# License OPL-1
|
# License OPL-1
|
||||||
"""Post-migrate for 19.0.22.0.0 — Recipe-level cert suppression Booleans.
|
"""Post-migrate for 19.0.22.0.0 - Recipe-level cert suppression Booleans.
|
||||||
|
|
||||||
Backfills NULL -> TRUE on the five new requires_* columns on
|
Backfills NULL -> TRUE on the five new requires_* columns on
|
||||||
fusion.plating.process.node (requires_coc, requires_thickness_report,
|
fusion.plating.process.node (requires_coc, requires_thickness_report,
|
||||||
requires_nadcap_cert, requires_mill_test, requires_customer_specific).
|
requires_nadcap_cert, requires_mill_test, requires_customer_specific).
|
||||||
|
|
||||||
Default TRUE = inherit current behaviour for every existing recipe
|
Default TRUE = inherit current behaviour for every existing recipe
|
||||||
(zero migration surprises — every existing recipe keeps producing
|
(zero migration surprises - every existing recipe keeps producing
|
||||||
the same cert set it produces today).
|
the same cert set it produces today).
|
||||||
|
|
||||||
Idempotent: safe to re-run.
|
Idempotent: safe to re-run.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# Copyright 2026 Nexa Systems Inc.
|
# Copyright 2026 Nexa Systems Inc.
|
||||||
# License OPL-1
|
# License OPL-1
|
||||||
#
|
#
|
||||||
# Phase 1 (Sub 11) — relocate fp.work.role, fp.operator.proficiency,
|
# Phase 1 (Sub 11) - relocate fp.work.role, fp.operator.proficiency,
|
||||||
# and the hr.employee shop-roles inherit from fusion_plating_bridge_mrp
|
# and the hr.employee shop-roles inherit from fusion_plating_bridge_mrp
|
||||||
# into fusion_plating core. Re-key all related ir.model.data so the
|
# into fusion_plating core. Re-key all related ir.model.data so the
|
||||||
# new module owner picks up the existing records cleanly.
|
# new module owner picks up the existing records cleanly.
|
||||||
@@ -14,7 +14,7 @@ _logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
def migrate(cr, version):
|
def migrate(cr, version):
|
||||||
if not version:
|
if not version:
|
||||||
return # Fresh install — nothing to migrate
|
return # Fresh install - nothing to migrate
|
||||||
|
|
||||||
patterns = [
|
patterns = [
|
||||||
'model_fp_work_role',
|
'model_fp_work_role',
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ from . import res_company
|
|||||||
from . import res_users
|
from . import res_users
|
||||||
from . import res_config_settings
|
from . import res_config_settings
|
||||||
|
|
||||||
# Phase 1 (Sub 11) — relocated from fusion_plating_bridge_mrp via
|
# Phase 1 (Sub 11) - relocated from fusion_plating_bridge_mrp via
|
||||||
# fusion_plating_jobs to core, so other downstream modules
|
# fusion_plating_jobs to core, so other downstream modules
|
||||||
# (fusion_plating_cgp, etc.) that touch hr.employee can see the
|
# (fusion_plating_cgp, etc.) that touch hr.employee can see the
|
||||||
# shop-roles fields without a transitive dep on jobs.
|
# shop-roles fields without a transitive dep on jobs.
|
||||||
@@ -37,20 +37,20 @@ from . import fp_proficiency
|
|||||||
from . import hr_employee
|
from . import hr_employee
|
||||||
from . import fp_process_node_inherit
|
from . import fp_process_node_inherit
|
||||||
|
|
||||||
# Sub 12a — Simple Recipe Editor + Step Library
|
# Sub 12a - Simple Recipe Editor + Step Library
|
||||||
from . import fp_step_kind # MUST load before fp_step_template (dependency)
|
from . import fp_step_kind # MUST load before fp_step_template (dependency)
|
||||||
from . import fp_step_template
|
from . import fp_step_template
|
||||||
from . import fp_step_template_input
|
from . import fp_step_template_input
|
||||||
from . import fp_step_template_transition_input
|
from . import fp_step_template_transition_input
|
||||||
|
|
||||||
# Sub 12b — Rack-aware moves + persistent labor reconciliation
|
# Sub 12b - Rack-aware moves + persistent labor reconciliation
|
||||||
from . import fp_rack_tag
|
from . import fp_rack_tag
|
||||||
from . import fp_job_step_move
|
from . import fp_job_step_move
|
||||||
|
|
||||||
# Phase 1 — Plating landing-page resolver
|
# Phase 1 - Plating landing-page resolver
|
||||||
from . import fp_landing
|
from . import fp_landing
|
||||||
|
|
||||||
# Phase H — dry-run + Owner-approval role migration workflow.
|
# Phase H - dry-run + Owner-approval role migration workflow.
|
||||||
# fp_role_constants MUST be imported before fp_migration (the latter
|
# fp_role_constants MUST be imported before fp_migration (the latter
|
||||||
# imports the predicate chain + xmlid maps from the former).
|
# imports the predicate chain + xmlid maps from the former).
|
||||||
from . import fp_role_constants
|
from . import fp_role_constants
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ quantities, and process inputs.
|
|||||||
|
|
||||||
Free-text unit fields invite typos ("kgs", "Kg", "kilo", "KG") that
|
Free-text unit fields invite typos ("kgs", "Kg", "kilo", "KG") that
|
||||||
break filters, reports, and trend graphs. Every UoM in the plating
|
break filters, reports, and trend graphs. Every UoM in the plating
|
||||||
domain — chemistry, mass, volume, length, area, electrical, time,
|
domain - chemistry, mass, volume, length, area, electrical, time,
|
||||||
pressure, dimensionless — lives here as a curated selection so users
|
pressure, dimensionless - lives here as a curated selection so users
|
||||||
pick from a known list instead of typing.
|
pick from a known list instead of typing.
|
||||||
|
|
||||||
Re-use:
|
Re-use:
|
||||||
@@ -23,7 +23,7 @@ Migration:
|
|||||||
the map gets cleared (NULL) so the user is forced to pick.
|
the map gets cleared (NULL) so the user is forced to pick.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Single source of truth — keep alphabetised within each section.
|
# Single source of truth - keep alphabetised within each section.
|
||||||
FP_UOM_SELECTION = [
|
FP_UOM_SELECTION = [
|
||||||
# --- Concentration / chemistry ---------------------------------------
|
# --- Concentration / chemistry ---------------------------------------
|
||||||
('g_l', 'g/L'),
|
('g_l', 'g/L'),
|
||||||
@@ -51,7 +51,7 @@ FP_UOM_SELECTION = [
|
|||||||
('ph', 'pH'),
|
('ph', 'pH'),
|
||||||
('su', 'SU (Standard Units)'),
|
('su', 'SU (Standard Units)'),
|
||||||
('ratio', 'Ratio (e.g. 5:1)'),
|
('ratio', 'Ratio (e.g. 5:1)'),
|
||||||
('none', '— (none)'),
|
('none', '- (none)'),
|
||||||
|
|
||||||
# --- Conductivity / turbidity ----------------------------------------
|
# --- Conductivity / turbidity ----------------------------------------
|
||||||
('us_cm', 'µS/cm'),
|
('us_cm', 'µS/cm'),
|
||||||
@@ -369,7 +369,7 @@ def fp_migrate_uom_column(env, table, column, label_for_log=None):
|
|||||||
selection keys. Unmapped values are set to NULL so the user is forced
|
selection keys. Unmapped values are set to NULL so the user is forced
|
||||||
to pick a valid one.
|
to pick a valid one.
|
||||||
|
|
||||||
Idempotent — running on a column that's already converted is a no-op
|
Idempotent - running on a column that's already converted is a no-op
|
||||||
because all values will already be selection keys (which are a subset
|
because all values will already be selection keys (which are a subset
|
||||||
of FP_UOM_LEGACY_MAP via identity mappings like 'g_l' → 'g_l').
|
of FP_UOM_LEGACY_MAP via identity mappings like 'g_l' → 'g_l').
|
||||||
|
|
||||||
@@ -411,7 +411,7 @@ def fp_migrate_uom_column(env, table, column, label_for_log=None):
|
|||||||
import logging
|
import logging
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
_logger.info(
|
_logger.info(
|
||||||
'Fusion Plating UoM migration — %s.%s%s: %s rewritten, %s cleared',
|
'Fusion Plating UoM migration - %s.%s%s: %s rewritten, %s cleared',
|
||||||
table, column, f' ({label_for_log})' if label_for_log else '',
|
table, column, f' ({label_for_log})' if label_for_log else '',
|
||||||
rewritten, cleared,
|
rewritten, cleared,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class FpBath(models.Model):
|
|||||||
without touching the generic bath model.
|
without touching the generic bath model.
|
||||||
"""
|
"""
|
||||||
_name = 'fusion.plating.bath'
|
_name = 'fusion.plating.bath'
|
||||||
_description = 'Fusion Plating — Bath'
|
_description = 'Fusion Plating - Bath'
|
||||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||||
_order = 'state, makeup_date desc, id desc'
|
_order = 'state, makeup_date desc, id desc'
|
||||||
_rec_name = 'display_name'
|
_rec_name = 'display_name'
|
||||||
@@ -190,7 +190,7 @@ class FpBath(models.Model):
|
|||||||
|
|
||||||
@api.depends('state', 'last_log_status')
|
@api.depends('state', 'last_log_status')
|
||||||
def _compute_status_color(self):
|
def _compute_status_color(self):
|
||||||
"""Kanban colour index — neutral palette that works in light + dark.
|
"""Kanban colour index - neutral palette that works in light + dark.
|
||||||
|
|
||||||
Uses Odoo's built-in color index rather than hex codes, so themes
|
Uses Odoo's built-in color index rather than hex codes, so themes
|
||||||
control the final rendering.
|
control the final rendering.
|
||||||
@@ -237,7 +237,7 @@ class FpBath(models.Model):
|
|||||||
class FpBathTarget(models.Model):
|
class FpBathTarget(models.Model):
|
||||||
"""Per-bath target range for a chemistry parameter."""
|
"""Per-bath target range for a chemistry parameter."""
|
||||||
_name = 'fusion.plating.bath.target'
|
_name = 'fusion.plating.bath.target'
|
||||||
_description = 'Fusion Plating — Bath Target'
|
_description = 'Fusion Plating - Bath Target'
|
||||||
_order = 'bath_id, sequence, parameter_id'
|
_order = 'bath_id, sequence, parameter_id'
|
||||||
|
|
||||||
bath_id = fields.Many2one(
|
bath_id = fields.Many2one(
|
||||||
|
|||||||
@@ -15,12 +15,12 @@ class FpBathLog(models.Model):
|
|||||||
Each log has one or more lines (one per parameter).
|
Each log has one or more lines (one per parameter).
|
||||||
|
|
||||||
Overall log status is rolled up from the lines:
|
Overall log status is rolled up from the lines:
|
||||||
* ok — every line is within target
|
* ok - every line is within target
|
||||||
* warning — at least one line is within warning tolerance
|
* warning - at least one line is within warning tolerance
|
||||||
* out_of_spec — at least one line is outside target
|
* out_of_spec - at least one line is outside target
|
||||||
"""
|
"""
|
||||||
_name = 'fusion.plating.bath.log'
|
_name = 'fusion.plating.bath.log'
|
||||||
_description = 'Fusion Plating — Bath Chemistry Log'
|
_description = 'Fusion Plating - Bath Chemistry Log'
|
||||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||||
_order = 'log_date desc, id desc'
|
_order = 'log_date desc, id desc'
|
||||||
_rec_name = 'display_name'
|
_rec_name = 'display_name'
|
||||||
@@ -118,7 +118,7 @@ class FpBathLog(models.Model):
|
|||||||
|
|
||||||
@api.constrains('line_ids')
|
@api.constrains('line_ids')
|
||||||
def _check_has_readings(self):
|
def _check_has_readings(self):
|
||||||
"""A bath log without readings is a useless empty record — it
|
"""A bath log without readings is a useless empty record - it
|
||||||
pollutes daily-chemistry reports and the trend graphs assume
|
pollutes daily-chemistry reports and the trend graphs assume
|
||||||
every log carries data. Block save until at least one reading.
|
every log carries data. Block save until at least one reading.
|
||||||
|
|
||||||
@@ -155,7 +155,7 @@ class FpBathLog(models.Model):
|
|||||||
parts.append(rec.bath_id.name)
|
parts.append(rec.bath_id.name)
|
||||||
if rec.log_date:
|
if rec.log_date:
|
||||||
parts.append(fields.Datetime.to_string(rec.log_date))
|
parts.append(fields.Datetime.to_string(rec.log_date))
|
||||||
rec.display_name = ' — '.join(parts) if parts else rec.name
|
rec.display_name = ' - '.join(parts) if parts else rec.name
|
||||||
|
|
||||||
@api.depends('line_ids', 'line_ids.status')
|
@api.depends('line_ids', 'line_ids.status')
|
||||||
def _compute_status(self):
|
def _compute_status(self):
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class FpBathLogLine(models.Model):
|
|||||||
up to the parent log.
|
up to the parent log.
|
||||||
"""
|
"""
|
||||||
_name = 'fusion.plating.bath.log.line'
|
_name = 'fusion.plating.bath.log.line'
|
||||||
_description = 'Fusion Plating — Bath Log Reading'
|
_description = 'Fusion Plating - Bath Log Reading'
|
||||||
_order = 'log_id, sequence, id'
|
_order = 'log_id, sequence, id'
|
||||||
|
|
||||||
log_id = fields.Many2one(
|
log_id = fields.Many2one(
|
||||||
@@ -87,7 +87,7 @@ class FpBathLogLine(models.Model):
|
|||||||
|
|
||||||
This means the operator (or backend user) hits "add reading", picks
|
This means the operator (or backend user) hits "add reading", picks
|
||||||
Temperature, and the tank's `default_temperature` lands in the value
|
Temperature, and the tank's `default_temperature` lands in the value
|
||||||
column automatically — they confirm with one tap or nudge with
|
column automatically - they confirm with one tap or nudge with
|
||||||
keyboard arrows. Avoids retyping the same number every shift.
|
keyboard arrows. Avoids retyping the same number every shift.
|
||||||
|
|
||||||
Fires only when value is currently empty so the user's edits aren't
|
Fires only when value is currently empty so the user's edits aren't
|
||||||
@@ -137,7 +137,7 @@ class FpBathLogLine(models.Model):
|
|||||||
rec.status = 'ok'
|
rec.status = 'ok'
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# T1.2 — Auto-suggest replenishment on every log line
|
# T1.2 - Auto-suggest replenishment on every log line
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@api.model_create_multi
|
@api.model_create_multi
|
||||||
def create(self, vals_list):
|
def create(self, vals_list):
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class FpBathParameter(models.Model):
|
|||||||
or on the bath recipe.
|
or on the bath recipe.
|
||||||
"""
|
"""
|
||||||
_name = 'fusion.plating.bath.parameter'
|
_name = 'fusion.plating.bath.parameter'
|
||||||
_description = 'Fusion Plating — Bath Parameter'
|
_description = 'Fusion Plating - Bath Parameter'
|
||||||
_order = 'sequence, name'
|
_order = 'sequence, name'
|
||||||
|
|
||||||
name = fields.Char(
|
name = fields.Char(
|
||||||
@@ -62,7 +62,7 @@ class FpBathParameter(models.Model):
|
|||||||
string='Unit (display)',
|
string='Unit (display)',
|
||||||
compute='_compute_uom_display',
|
compute='_compute_uom_display',
|
||||||
help='Resolved display string for the chosen unit '
|
help='Resolved display string for the chosen unit '
|
||||||
'(e.g. "g/L", "°C") — used by views that need plain text.',
|
'(e.g. "g/L", "°C") - used by views that need plain text.',
|
||||||
)
|
)
|
||||||
target_min = fields.Float(
|
target_min = fields.Float(
|
||||||
string='Default Target Min',
|
string='Default Target Min',
|
||||||
@@ -79,7 +79,7 @@ class FpBathParameter(models.Model):
|
|||||||
target_value = fields.Float(
|
target_value = fields.Float(
|
||||||
string='Default Setpoint / Optimum',
|
string='Default Setpoint / Optimum',
|
||||||
help='The IDEAL operating value, expressed in the unit selected '
|
help='The IDEAL operating value, expressed in the unit selected '
|
||||||
'above — what the heater/chiller controls toward, what '
|
'above - what the heater/chiller controls toward, what '
|
||||||
'dashboards compare against. Sits between Target Min and '
|
'dashboards compare against. Sits between Target Min and '
|
||||||
'Target Max. Per-sensor override via '
|
'Target Max. Per-sensor override via '
|
||||||
'fp.tank.sensor.target_value_override.',
|
'fp.tank.sensor.target_value_override.',
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class FpBathReplenishmentRule(models.Model):
|
|||||||
Shops wanting non-linear or piecewise rules can extend this model.
|
Shops wanting non-linear or piecewise rules can extend this model.
|
||||||
"""
|
"""
|
||||||
_name = 'fusion.plating.bath.replenishment.rule'
|
_name = 'fusion.plating.bath.replenishment.rule'
|
||||||
_description = 'Fusion Plating — Replenishment Rule'
|
_description = 'Fusion Plating - Replenishment Rule'
|
||||||
_order = 'process_type_id, parameter_id'
|
_order = 'process_type_id, parameter_id'
|
||||||
|
|
||||||
name = fields.Char(string='Rule Name', required=True)
|
name = fields.Char(string='Rule Name', required=True)
|
||||||
@@ -42,7 +42,7 @@ class FpBathReplenishmentRule(models.Model):
|
|||||||
)
|
)
|
||||||
product_name = fields.Char(
|
product_name = fields.Char(
|
||||||
string='Replenisher Name', required=True,
|
string='Replenisher Name', required=True,
|
||||||
help='Human-readable chemical name, e.g. "Nickel Sulfamate 30% — Grade A"',
|
help='Human-readable chemical name, e.g. "Nickel Sulfamate 30% - Grade A"',
|
||||||
)
|
)
|
||||||
product_id = fields.Many2one(
|
product_id = fields.Many2one(
|
||||||
'product.product', string='Product (Inventory)',
|
'product.product', string='Product (Inventory)',
|
||||||
@@ -111,7 +111,7 @@ class FpBathReplenishmentSuggestion(models.Model):
|
|||||||
"""One suggestion generated from a bath-log reading. Operators mark
|
"""One suggestion generated from a bath-log reading. Operators mark
|
||||||
them applied or dismissed once the dose has been added."""
|
them applied or dismissed once the dose has been added."""
|
||||||
_name = 'fusion.plating.bath.replenishment.suggestion'
|
_name = 'fusion.plating.bath.replenishment.suggestion'
|
||||||
_description = 'Fusion Plating — Replenishment Suggestion'
|
_description = 'Fusion Plating - Replenishment Suggestion'
|
||||||
_inherit = ['mail.thread']
|
_inherit = ['mail.thread']
|
||||||
_order = 'create_date desc, id desc'
|
_order = 'create_date desc, id desc'
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class FpFacility(models.Model):
|
|||||||
model with jurisdiction-specific fields via inheritance.
|
model with jurisdiction-specific fields via inheritance.
|
||||||
"""
|
"""
|
||||||
_name = 'fusion.plating.facility'
|
_name = 'fusion.plating.facility'
|
||||||
_description = 'Fusion Plating — Facility'
|
_description = 'Fusion Plating - Facility'
|
||||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||||
_order = 'sequence, name'
|
_order = 'sequence, name'
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
# Copyright 2026 Nexa Systems Inc.
|
# Copyright 2026 Nexa Systems Inc.
|
||||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
#
|
#
|
||||||
# fp.job — native plating job model.
|
# fp.job - native plating job model.
|
||||||
#
|
#
|
||||||
# Replaces mrp.production for plating. One record per shop-floor job.
|
# Replaces mrp.production for plating. One record per shop-floor job.
|
||||||
# Header data lives here; per-operation detail on fp.job.step (Task 1.5).
|
# Header data lives here; per-operation detail on fp.job.step (Task 1.5).
|
||||||
# Recipe template (fusion.plating.process.node) is unchanged — this
|
# Recipe template (fusion.plating.process.node) is unchanged - this
|
||||||
# model just instantiates from it via fp.job.step.recipe_node_id.
|
# model just instantiates from it via fp.job.step.recipe_node_id.
|
||||||
#
|
#
|
||||||
# State machine:
|
# State machine:
|
||||||
@@ -52,7 +52,7 @@ class FpJob(models.Model):
|
|||||||
return dt.astimezone(tz).strftime(fmt)
|
return dt.astimezone(tz).strftime(fmt)
|
||||||
_description = 'Work Order'
|
_description = 'Work Order'
|
||||||
_inherit = ['mail.thread', 'mail.activity.mixin', 'fp.parent.numbered.mixin']
|
_inherit = ['mail.thread', 'mail.activity.mixin', 'fp.parent.numbered.mixin']
|
||||||
# Sub 12d — state-aware sort. Active work bubbles to the top
|
# Sub 12d - state-aware sort. Active work bubbles to the top
|
||||||
# (in_progress → confirmed/draft → on_hold → done → cancelled),
|
# (in_progress → confirmed/draft → on_hold → done → cancelled),
|
||||||
# then high-priority first within each state, then nearest deadline.
|
# then high-priority first within each state, then nearest deadline.
|
||||||
# state_priority is a small stored compute below.
|
# state_priority is a small stored compute below.
|
||||||
@@ -96,7 +96,7 @@ class FpJob(models.Model):
|
|||||||
tracking=True,
|
tracking=True,
|
||||||
index=True,
|
index=True,
|
||||||
)
|
)
|
||||||
# Sub 12d — drives the default sort so active jobs surface above
|
# Sub 12d - drives the default sort so active jobs surface above
|
||||||
# closed jobs. Lower number = sorted earlier. Stored + indexed so
|
# closed jobs. Lower number = sorted earlier. Stored + indexed so
|
||||||
# SQL ORDER BY hits an index and doesn't recompute per row.
|
# SQL ORDER BY hits an index and doesn't recompute per row.
|
||||||
state_priority = fields.Integer(
|
state_priority = fields.Integer(
|
||||||
@@ -154,7 +154,7 @@ class FpJob(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Source / recipe / invoicing — core-safe (target models reachable
|
# Source / recipe / invoicing - core-safe (target models reachable
|
||||||
# via current depends: sale_management → sale → account, and our
|
# via current depends: sale_management → sale → account, and our
|
||||||
# own fusion.plating.process.node).
|
# own fusion.plating.process.node).
|
||||||
#
|
#
|
||||||
@@ -187,7 +187,7 @@ class FpJob(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Cost rollup — actual_cost stays at 0 until Task 1.5 wires step
|
# Cost rollup - actual_cost stays at 0 until Task 1.5 wires step
|
||||||
# time × work_centre.cost_per_hour. quoted_revenue is a manual entry
|
# time × work_centre.cost_per_hour. quoted_revenue is a manual entry
|
||||||
# for now (will be filled by the SO → job hook in Phase 2).
|
# for now (will be filled by the SO → job hook in Phase 2).
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@@ -229,7 +229,7 @@ class FpJob(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# current_location — operator-readable status string. Stub here;
|
# current_location - operator-readable status string. Stub here;
|
||||||
# full "Queued: Bath 3" / "In progress: Oven A" rendering needs
|
# full "Queued: Bath 3" / "In progress: Oven A" rendering needs
|
||||||
# fp.job.step + fp.work.centre, which lands in Tasks 1.5/1.6.
|
# fp.job.step + fp.work.centre, which lands in Tasks 1.5/1.6.
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@@ -250,7 +250,7 @@ class FpJob(models.Model):
|
|||||||
job.current_location = job.state.replace('_', ' ').title()
|
job.current_location = job.state.replace('_', ' ').title()
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Steps — One2many to fp.job.step (Task 1.5)
|
# Steps - One2many to fp.job.step (Task 1.5)
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
step_ids = fields.One2many(
|
step_ids = fields.One2many(
|
||||||
'fp.job.step',
|
'fp.job.step',
|
||||||
@@ -258,7 +258,7 @@ class FpJob(models.Model):
|
|||||||
string='Steps',
|
string='Steps',
|
||||||
)
|
)
|
||||||
|
|
||||||
# ===== Sub 12b — traveller header + active timer ========================
|
# ===== Sub 12b - traveller header + active timer ========================
|
||||||
# Header counters mirror the paper traveller's "Qty Rec." / "VIS INSP."
|
# Header counters mirror the paper traveller's "Qty Rec." / "VIS INSP."
|
||||||
# / "Rework" columns (screens 16-18). Sub 12c's traveller report pulls
|
# / "Rework" columns (screens 16-18). Sub 12c's traveller report pulls
|
||||||
# these into the printed header.
|
# these into the printed header.
|
||||||
@@ -285,13 +285,13 @@ class FpJob(models.Model):
|
|||||||
string='Rush Order',
|
string='Rush Order',
|
||||||
tracking=True,
|
tracking=True,
|
||||||
help='High-priority flag mirrored from sale.order. Operators see '
|
help='High-priority flag mirrored from sale.order. Operators see '
|
||||||
'this on the queue / tablet at-a-glance — saves lifting the '
|
'this on the queue / tablet at-a-glance - saves lifting the '
|
||||||
'job form to know it\'s rush.',
|
'job form to know it\'s rush.',
|
||||||
)
|
)
|
||||||
|
|
||||||
# ---- Scheduling targets mirrored from sale.order -----------------
|
# ---- Scheduling targets mirrored from sale.order -----------------
|
||||||
# These are kept separate from the operational date_planned_start /
|
# These are kept separate from the operational date_planned_start /
|
||||||
# date_deadline fields (which may be tweaked by scheduling logic) —
|
# date_deadline fields (which may be tweaked by scheduling logic) -
|
||||||
# this preserves the ORIGINAL customer-facing dates entered on the SO.
|
# this preserves the ORIGINAL customer-facing dates entered on the SO.
|
||||||
x_fc_internal_deadline = fields.Date(
|
x_fc_internal_deadline = fields.Date(
|
||||||
string='Internal Deadline',
|
string='Internal Deadline',
|
||||||
@@ -342,7 +342,7 @@ class FpJob(models.Model):
|
|||||||
'job_id',
|
'job_id',
|
||||||
string='Active Timers',
|
string='Active Timers',
|
||||||
domain=[('state', 'in', ('running', 'paused'))],
|
domain=[('state', 'in', ('running', 'paused'))],
|
||||||
help='Sub 12b — used by tablet for live timer badges. Filtered '
|
help='Sub 12b - used by tablet for live timer badges. Filtered '
|
||||||
'on state by Task 7\'s state field.',
|
'on state by Task 7\'s state field.',
|
||||||
)
|
)
|
||||||
move_ids = fields.One2many(
|
move_ids = fields.One2many(
|
||||||
@@ -350,7 +350,7 @@ class FpJob(models.Model):
|
|||||||
string='Move Log',
|
string='Move Log',
|
||||||
)
|
)
|
||||||
# step_count + step_done_count are stored (drive list views / stat
|
# step_count + step_done_count are stored (drive list views / stat
|
||||||
# buttons in Task 1.8). step_progress_pct stays non-stored — it's a
|
# buttons in Task 1.8). step_progress_pct stays non-stored - it's a
|
||||||
# cheap derivative. Odoo flags as inconsistent when stored and
|
# cheap derivative. Odoo flags as inconsistent when stored and
|
||||||
# non-stored fields share a compute method, so they get distinct
|
# non-stored fields share a compute method, so they get distinct
|
||||||
# methods below.
|
# methods below.
|
||||||
@@ -424,7 +424,7 @@ class FpJob(models.Model):
|
|||||||
continue # caller set an explicit name (e.g. bulk SO confirm)
|
continue # caller set an explicit name (e.g. bulk SO confirm)
|
||||||
if not rec._fp_assign_parent_name():
|
if not rec._fp_assign_parent_name():
|
||||||
seq = self.env['ir.sequence'].next_by_code('fp.job') or _('New')
|
seq = self.env['ir.sequence'].next_by_code('fp.job') or _('New')
|
||||||
# Raw SQL — fp.job has no immutability guard yet in this
|
# Raw SQL - fp.job has no immutability guard yet in this
|
||||||
# task, but Task 11 will add one. Using SQL here keeps the
|
# task, but Task 11 will add one. Using SQL here keeps the
|
||||||
# fallback path consistent across all child models.
|
# fallback path consistent across all child models.
|
||||||
self.env.cr.execute(
|
self.env.cr.execute(
|
||||||
@@ -435,10 +435,10 @@ class FpJob(models.Model):
|
|||||||
return records
|
return records
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# State machine — actions
|
# State machine - actions
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# TODO(fp.job state-machine completeness): action_hold, action_resume,
|
# TODO(fp.job state-machine completeness): action_hold, action_resume,
|
||||||
# action_revert_to_confirmed (rework path) — to be added when shopfloor
|
# action_revert_to_confirmed (rework path) - to be added when shopfloor
|
||||||
# / rework workflows are wired up. For now, draft → confirmed and the
|
# / rework workflows are wired up. For now, draft → confirmed and the
|
||||||
# cancel paths are the only enforced transitions; everything else is
|
# cancel paths are the only enforced transitions; everything else is
|
||||||
# an explicit `state` write by privileged code.
|
# an explicit `state` write by privileged code.
|
||||||
@@ -450,7 +450,7 @@ class FpJob(models.Model):
|
|||||||
) % (job.name, job.state))
|
) % (job.name, job.state))
|
||||||
job.state = 'confirmed'
|
job.state = 'confirmed'
|
||||||
# Step auto-promote happens in the fusion_plating_jobs override
|
# Step auto-promote happens in the fusion_plating_jobs override
|
||||||
# AFTER _generate_steps_from_recipe runs — at this point step_ids
|
# AFTER _generate_steps_from_recipe runs - at this point step_ids
|
||||||
# is empty for any newly-confirmed job.
|
# is empty for any newly-confirmed job.
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -458,7 +458,7 @@ class FpJob(models.Model):
|
|||||||
for job in self:
|
for job in self:
|
||||||
if job.state == 'done':
|
if job.state == 'done':
|
||||||
raise UserError(_(
|
raise UserError(_(
|
||||||
"Job %s is done — cannot cancel."
|
"Job %s is done - cannot cancel."
|
||||||
) % job.name)
|
) % job.name)
|
||||||
if job.state == 'cancelled':
|
if job.state == 'cancelled':
|
||||||
raise UserError(_(
|
raise UserError(_(
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
# Copyright 2026 Nexa Systems Inc.
|
# Copyright 2026 Nexa Systems Inc.
|
||||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
#
|
#
|
||||||
# fp.job.step — one operation within a plating job.
|
# fp.job.step - one operation within a plating job.
|
||||||
#
|
#
|
||||||
# Replaces mrp.workorder. Each step instantiates from a recipe
|
# Replaces mrp.workorder. Each step instantiates from a recipe
|
||||||
# operation node (recipe_node_id). Container nodes (recipe,
|
# operation node (recipe_node_id). Container nodes (recipe,
|
||||||
# sub_process) and step nodes (instructions) are NOT rows here —
|
# sub_process) and step nodes (instructions) are NOT rows here -
|
||||||
# they live on the recipe template and are used at view-render time
|
# they live on the recipe template and are used at view-render time
|
||||||
# to display hierarchy. See spec §5.2 (Option A — operations only).
|
# to display hierarchy. See spec §5.2 (Option A - operations only).
|
||||||
#
|
#
|
||||||
# State machine:
|
# State machine:
|
||||||
# pending → ready → in_progress → done
|
# pending → ready → in_progress → done
|
||||||
@@ -83,9 +83,9 @@ class FpJobStep(models.Model):
|
|||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Equipment + audit (Task 1.6)
|
# Equipment + audit (Task 1.6)
|
||||||
# oven_id is deferred to a bridge module — fusion.plating.bake.oven
|
# oven_id is deferred to a bridge module - fusion.plating.bake.oven
|
||||||
# lives in fusion_plating_shopfloor and core can't depend on it.
|
# lives in fusion_plating_shopfloor and core can't depend on it.
|
||||||
# masking_material_id is deferred — fusion.plating.masking.material
|
# masking_material_id is deferred - fusion.plating.masking.material
|
||||||
# does not yet exist in any installed module; will be added when
|
# does not yet exist in any installed module; will be added when
|
||||||
# the masking model lands (likely in fusion_plating_process_en
|
# the masking model lands (likely in fusion_plating_process_en
|
||||||
# or a future fusion_plating_masking module).
|
# or a future fusion_plating_masking module).
|
||||||
@@ -109,7 +109,7 @@ class FpJobStep(models.Model):
|
|||||||
default='um',
|
default='um',
|
||||||
)
|
)
|
||||||
dwell_time_minutes = fields.Float()
|
dwell_time_minutes = fields.Float()
|
||||||
# Label intentionally has no unit suffix — the unit follows the
|
# Label intentionally has no unit suffix - the unit follows the
|
||||||
# company's `x_fc_default_temp_uom` setting and is surfaced via the
|
# company's `x_fc_default_temp_uom` setting and is surfaced via the
|
||||||
# adjacent `bake_setpoint_temp_uom_display` compute. Hardcoding °C
|
# adjacent `bake_setpoint_temp_uom_display` compute. Hardcoding °C
|
||||||
# in the label was the most visible "Celsius leaks everywhere"
|
# in the label was the most visible "Celsius leaks everywhere"
|
||||||
@@ -158,7 +158,7 @@ class FpJobStep(models.Model):
|
|||||||
'enforce_sequential=False (free-flow recipe with one '
|
'enforce_sequential=False (free-flow recipe with one '
|
||||||
'specific step that needs to wait).',
|
'specific step that needs to wait).',
|
||||||
)
|
)
|
||||||
# Sub 13 — sequential enforcement (recipe + per-step). New default
|
# Sub 13 - sequential enforcement (recipe + per-step). New default
|
||||||
# behaviour is "every step waits for predecessors", with two escape
|
# behaviour is "every step waits for predecessors", with two escape
|
||||||
# hatches: enforce_sequential=False on the recipe (free-flow), or
|
# hatches: enforce_sequential=False on the recipe (free-flow), or
|
||||||
# parallel_start=True on this specific step (explicit parallelism).
|
# parallel_start=True on this specific step (explicit parallelism).
|
||||||
@@ -175,8 +175,8 @@ class FpJobStep(models.Model):
|
|||||||
'parent recipe has enforce_sequential=True.',
|
'parent recipe has enforce_sequential=True.',
|
||||||
)
|
)
|
||||||
|
|
||||||
# ===== Sub 12b — chain-of-custody + rack awareness =====================
|
# ===== Sub 12b - chain-of-custody + rack awareness =====================
|
||||||
# Note: rack_id (line 95 above) already exists — reused as the "current
|
# Note: rack_id (line 95 above) already exists - reused as the "current
|
||||||
# rack on this step" pointer. Sub 12b builds the runtime guards on top.
|
# rack on this step" pointer. Sub 12b builds the runtime guards on top.
|
||||||
requires_rack_assignment = fields.Boolean(
|
requires_rack_assignment = fields.Boolean(
|
||||||
related='recipe_node_id.requires_rack_assignment',
|
related='recipe_node_id.requires_rack_assignment',
|
||||||
@@ -199,12 +199,12 @@ class FpJobStep(models.Model):
|
|||||||
)
|
)
|
||||||
is_racked = fields.Boolean(
|
is_racked = fields.Boolean(
|
||||||
string='Racked', compute='_compute_is_racked', store=True,
|
string='Racked', compute='_compute_is_racked', store=True,
|
||||||
help='True when rack_id is set — drives the tablet rack-vs-parts '
|
help='True when rack_id is set - drives the tablet rack-vs-parts '
|
||||||
'button-state guard (Move Parts greys out).',
|
'button-state guard (Move Parts greys out).',
|
||||||
)
|
)
|
||||||
qty_at_step_start = fields.Integer(string='Qty at Step Start')
|
qty_at_step_start = fields.Integer(string='Qty at Step Start')
|
||||||
qty_at_step_finish = fields.Integer(string='Qty at Step Finish')
|
qty_at_step_finish = fields.Integer(string='Qty at Step Finish')
|
||||||
# Live "qty currently parked at this step" — drives partial-qty
|
# Live "qty currently parked at this step" - drives partial-qty
|
||||||
# workflows. = (incoming moves' qty − outgoing moves' qty), with a
|
# workflows. = (incoming moves' qty − outgoing moves' qty), with a
|
||||||
# first-step seed: the lowest-sequence step on a confirmed job
|
# first-step seed: the lowest-sequence step on a confirmed job
|
||||||
# implicitly receives the full job qty when the job starts (no
|
# implicitly receives the full job qty when the job starts (no
|
||||||
@@ -227,7 +227,7 @@ class FpJobStep(models.Model):
|
|||||||
def _compute_qty_at_step(self):
|
def _compute_qty_at_step(self):
|
||||||
for rec in self:
|
for rec in self:
|
||||||
# Terminal states: nothing parked here anymore. Operators
|
# Terminal states: nothing parked here anymore. Operators
|
||||||
# don't care if "done" steps technically have qty residue —
|
# don't care if "done" steps technically have qty residue -
|
||||||
# surfacing zero keeps the column readable.
|
# surfacing zero keeps the column readable.
|
||||||
if rec.state in ('done', 'cancelled', 'skipped'):
|
if rec.state in ('done', 'cancelled', 'skipped'):
|
||||||
rec.qty_at_step = 0
|
rec.qty_at_step = 0
|
||||||
@@ -287,7 +287,7 @@ class FpJobStep(models.Model):
|
|||||||
step.cost_total = (step.duration_actual / 60.0) * step.cost_per_hour
|
step.cost_total = (step.duration_actual / 60.0) * step.cost_per_hour
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# State machine — actions
|
# State machine - actions
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Implemented: button_start (ready/paused → in_progress),
|
# Implemented: button_start (ready/paused → in_progress),
|
||||||
# button_finish (in_progress → done).
|
# button_finish (in_progress → done).
|
||||||
@@ -308,7 +308,7 @@ class FpJobStep(models.Model):
|
|||||||
for step in self:
|
for step in self:
|
||||||
if step.state != 'in_progress':
|
if step.state != 'in_progress':
|
||||||
raise UserError(_(
|
raise UserError(_(
|
||||||
"Step '%s' is in state '%s' — only in-progress steps can pause."
|
"Step '%s' is in state '%s' - only in-progress steps can pause."
|
||||||
) % (step.name, step.state))
|
) % (step.name, step.state))
|
||||||
now = fields.Datetime.now()
|
now = fields.Datetime.now()
|
||||||
open_log = step.time_log_ids.filtered(lambda l: not l.date_finished)
|
open_log = step.time_log_ids.filtered(lambda l: not l.date_finished)
|
||||||
@@ -318,25 +318,25 @@ class FpJobStep(models.Model):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def button_resume(self):
|
def button_resume(self):
|
||||||
"""Resume a paused step — thin alias over button_start so views
|
"""Resume a paused step - thin alias over button_start so views
|
||||||
can show distinct labels (Resume vs Start) without duplicating
|
can show distinct labels (Resume vs Start) without duplicating
|
||||||
the state-machine logic."""
|
the state-machine logic."""
|
||||||
for step in self:
|
for step in self:
|
||||||
if step.state != 'paused':
|
if step.state != 'paused':
|
||||||
raise UserError(_(
|
raise UserError(_(
|
||||||
"Step '%s' is in state '%s' — only paused steps can resume."
|
"Step '%s' is in state '%s' - only paused steps can resume."
|
||||||
) % (step.name, step.state))
|
) % (step.name, step.state))
|
||||||
return self.button_start()
|
return self.button_start()
|
||||||
|
|
||||||
def button_skip(self):
|
def button_skip(self):
|
||||||
"""Skip an opt-in step that wasn't activated for this job. Allowed
|
"""Skip an opt-in step that wasn't activated for this job. Allowed
|
||||||
from pending or ready only — a step that's already running shouldn't
|
from pending or ready only - a step that's already running shouldn't
|
||||||
be skipped without an audit narrative (use button_cancel for that).
|
be skipped without an audit narrative (use button_cancel for that).
|
||||||
"""
|
"""
|
||||||
for step in self:
|
for step in self:
|
||||||
if step.state not in ('pending', 'ready'):
|
if step.state not in ('pending', 'ready'):
|
||||||
raise UserError(_(
|
raise UserError(_(
|
||||||
"Step '%s' is in state '%s' — only pending/ready steps can skip."
|
"Step '%s' is in state '%s' - only pending/ready steps can skip."
|
||||||
) % (step.name, step.state))
|
) % (step.name, step.state))
|
||||||
step.state = 'skipped'
|
step.state = 'skipped'
|
||||||
step.message_post(body=_('Step skipped by %s') % self.env.user.name)
|
step.message_post(body=_('Step skipped by %s') % self.env.user.name)
|
||||||
@@ -351,7 +351,7 @@ class FpJobStep(models.Model):
|
|||||||
for step in self:
|
for step in self:
|
||||||
if step.state in ('done', 'cancelled'):
|
if step.state in ('done', 'cancelled'):
|
||||||
raise UserError(_(
|
raise UserError(_(
|
||||||
"Step '%s' is in state '%s' — cannot cancel."
|
"Step '%s' is in state '%s' - cannot cancel."
|
||||||
) % (step.name, step.state))
|
) % (step.name, step.state))
|
||||||
now = fields.Datetime.now()
|
now = fields.Datetime.now()
|
||||||
open_log = step.time_log_ids.filtered(lambda l: not l.date_finished)
|
open_log = step.time_log_ids.filtered(lambda l: not l.date_finished)
|
||||||
@@ -364,7 +364,7 @@ class FpJobStep(models.Model):
|
|||||||
for step in self:
|
for step in self:
|
||||||
if step.state not in ('ready', 'paused'):
|
if step.state not in ('ready', 'paused'):
|
||||||
raise UserError(_(
|
raise UserError(_(
|
||||||
"Step '%s' is in state '%s' — only ready/paused steps can start."
|
"Step '%s' is in state '%s' - only ready/paused steps can start."
|
||||||
) % (step.name, step.state))
|
) % (step.name, step.state))
|
||||||
now = fields.Datetime.now()
|
now = fields.Datetime.now()
|
||||||
step.state = 'in_progress'
|
step.state = 'in_progress'
|
||||||
@@ -372,7 +372,7 @@ class FpJobStep(models.Model):
|
|||||||
if not step.date_started:
|
if not step.date_started:
|
||||||
step.date_started = now
|
step.date_started = now
|
||||||
step.started_by_user_id = self.env.user
|
step.started_by_user_id = self.env.user
|
||||||
# Open a fresh timelog row for this start interval — uses the
|
# Open a fresh timelog row for this start interval - uses the
|
||||||
# same `now` as the first-start stamp so the step and its
|
# same `now` as the first-start stamp so the step and its
|
||||||
# first log share a single instant.
|
# first log share a single instant.
|
||||||
self.env['fp.job.step.timelog'].create({
|
self.env['fp.job.step.timelog'].create({
|
||||||
@@ -387,17 +387,17 @@ class FpJobStep(models.Model):
|
|||||||
for step in self:
|
for step in self:
|
||||||
if step.state != 'in_progress':
|
if step.state != 'in_progress':
|
||||||
raise UserError(_(
|
raise UserError(_(
|
||||||
"Step '%s' is in state '%s' — only in-progress steps can finish."
|
"Step '%s' is in state '%s' - only in-progress steps can finish."
|
||||||
) % (step.name, step.state))
|
) % (step.name, step.state))
|
||||||
# Quantity gate: refuses if parts still parked AND there's
|
# Quantity gate: refuses if parts still parked AND there's
|
||||||
# a downstream step to move them into. Last runnable step
|
# a downstream step to move them into. Last runnable step
|
||||||
# is exempt — parts finishing there complete in place
|
# is exempt - parts finishing there complete in place
|
||||||
# (qty_done reconciliation at job close is the catch-net).
|
# (qty_done reconciliation at job close is the catch-net).
|
||||||
#
|
#
|
||||||
# Seed-only exemption: the first-step seed in
|
# Seed-only exemption: the first-step seed in
|
||||||
# _compute_qty_at_step gives the earliest non-terminal step
|
# _compute_qty_at_step gives the earliest non-terminal step
|
||||||
# a notional qty = job.qty. That's a UI hint, not a real
|
# a notional qty = job.qty. That's a UI hint, not a real
|
||||||
# parked batch — no incoming move record backs it. Paperwork
|
# parked batch - no incoming move record backs it. Paperwork
|
||||||
# steps (Contract Review, Inspection, etc.) sit on that seed.
|
# steps (Contract Review, Inspection, etc.) sit on that seed.
|
||||||
# If the step has no REAL incoming moves, skip the gate.
|
# If the step has no REAL incoming moves, skip the gate.
|
||||||
if not skip_qty_gate and step.qty_at_step > 0:
|
if not skip_qty_gate and step.qty_at_step > 0:
|
||||||
@@ -413,7 +413,7 @@ class FpJobStep(models.Model):
|
|||||||
if has_downstream and has_real_incoming:
|
if has_downstream and has_real_incoming:
|
||||||
raise UserError(_(
|
raise UserError(_(
|
||||||
"Step '%(name)s' still has %(n)d part(s) "
|
"Step '%(name)s' still has %(n)d part(s) "
|
||||||
"parked — move them to the next step before "
|
"parked - move them to the next step before "
|
||||||
"finishing. Use the row's 'Complete 1 → Next' "
|
"finishing. Use the row's 'Complete 1 → Next' "
|
||||||
"or 'Move…' button."
|
"or 'Move…' button."
|
||||||
) % {'name': step.name, 'n': step.qty_at_step})
|
) % {'name': step.name, 'n': step.qty_at_step})
|
||||||
@@ -453,7 +453,7 @@ class FpJobStep(models.Model):
|
|||||||
) % step.name)
|
) % step.name)
|
||||||
prev_state = step.state
|
prev_state = step.state
|
||||||
now = fields.Datetime.now()
|
now = fields.Datetime.now()
|
||||||
# Close any open timelogs first — labour already incurred
|
# Close any open timelogs first - labour already incurred
|
||||||
# stays in the audit even when we shortcut to done.
|
# stays in the audit even when we shortcut to done.
|
||||||
open_log = step.time_log_ids.filtered(
|
open_log = step.time_log_ids.filtered(
|
||||||
lambda l: not l.date_finished
|
lambda l: not l.date_finished
|
||||||
@@ -484,7 +484,7 @@ class FpJobStep(models.Model):
|
|||||||
next button_finish writes fresh first-finish stamps instead
|
next button_finish writes fresh first-finish stamps instead
|
||||||
of preserving stale ones.
|
of preserving stale ones.
|
||||||
|
|
||||||
date_started + started_by_user_id are preserved across resets —
|
date_started + started_by_user_id are preserved across resets -
|
||||||
they record the first start ever (audit), and duration_actual is
|
they record the first start ever (audit), and duration_actual is
|
||||||
computed from the sum of timelogs, not (finish - start), so the
|
computed from the sum of timelogs, not (finish - start), so the
|
||||||
elapsed math remains correct."""
|
elapsed math remains correct."""
|
||||||
@@ -502,7 +502,7 @@ class FpJobStep(models.Model):
|
|||||||
prev_state = step.state
|
prev_state = step.state
|
||||||
vals = {'state': 'ready'}
|
vals = {'state': 'ready'}
|
||||||
|
|
||||||
# Close any still-open timelog (defensive — usually only
|
# Close any still-open timelog (defensive - usually only
|
||||||
# in_progress/paused will have one).
|
# in_progress/paused will have one).
|
||||||
open_log = step.time_log_ids.filtered(
|
open_log = step.time_log_ids.filtered(
|
||||||
lambda l: not l.date_finished
|
lambda l: not l.date_finished
|
||||||
@@ -512,7 +512,7 @@ class FpJobStep(models.Model):
|
|||||||
|
|
||||||
# If the step had been completed, wipe the finish stamps so
|
# If the step had been completed, wipe the finish stamps so
|
||||||
# the next Finish records fresh audit values. Skip this for
|
# the next Finish records fresh audit values. Skip this for
|
||||||
# in_progress / paused / skipped / cancelled / pending — they
|
# in_progress / paused / skipped / cancelled / pending - they
|
||||||
# either have no finish stamp or shouldn't have one cleared.
|
# either have no finish stamp or shouldn't have one cleared.
|
||||||
if step.state == 'done':
|
if step.state == 'done':
|
||||||
vals['date_finished'] = False
|
vals['date_finished'] = False
|
||||||
@@ -538,7 +538,7 @@ class FpJobStep(models.Model):
|
|||||||
) % self.name)
|
) % self.name)
|
||||||
if self.qty_at_step < 1:
|
if self.qty_at_step < 1:
|
||||||
raise UserError(_(
|
raise UserError(_(
|
||||||
"No parts parked at step '%s' — nothing to complete."
|
"No parts parked at step '%s' - nothing to complete."
|
||||||
) % self.name)
|
) % self.name)
|
||||||
next_step = self.job_id.step_ids.filtered(
|
next_step = self.job_id.step_ids.filtered(
|
||||||
lambda s: s.sequence > self.sequence
|
lambda s: s.sequence > self.sequence
|
||||||
@@ -546,7 +546,7 @@ class FpJobStep(models.Model):
|
|||||||
).sorted('sequence')[:1]
|
).sorted('sequence')[:1]
|
||||||
if not next_step:
|
if not next_step:
|
||||||
raise UserError(_(
|
raise UserError(_(
|
||||||
"Step '%s' is the last runnable step on the job — "
|
"Step '%s' is the last runnable step on the job - "
|
||||||
"no downstream step to move into. Finish the step "
|
"no downstream step to move into. Finish the step "
|
||||||
"instead (it will close out the job)."
|
"instead (it will close out the job)."
|
||||||
) % self.name)
|
) % self.name)
|
||||||
|
|||||||
@@ -10,14 +10,14 @@ from odoo.exceptions import UserError
|
|||||||
|
|
||||||
|
|
||||||
class FpJobStepMove(models.Model):
|
class FpJobStepMove(models.Model):
|
||||||
"""Chain-of-custody log — one row per part-batch move.
|
"""Chain-of-custody log - one row per part-batch move.
|
||||||
|
|
||||||
Sub 12b: every Move Parts / Move Rack click commits one (or, for
|
Sub 12b: every Move Parts / Move Rack click commits one (or, for
|
||||||
rack moves, one-per-batch atomic) row here. Sub 12c walks these in
|
rack moves, one-per-batch atomic) row here. Sub 12c walks these in
|
||||||
chronological order to render the customer CoC PDF.
|
chronological order to render the customer CoC PDF.
|
||||||
"""
|
"""
|
||||||
_name = 'fp.job.step.move'
|
_name = 'fp.job.step.move'
|
||||||
_description = 'Fusion Plating — Job Step Move (Chain-of-Custody)'
|
_description = 'Fusion Plating - Job Step Move (Chain-of-Custody)'
|
||||||
_inherit = ['mail.thread']
|
_inherit = ['mail.thread']
|
||||||
_order = 'move_datetime desc, id desc'
|
_order = 'move_datetime desc, id desc'
|
||||||
|
|
||||||
@@ -99,12 +99,12 @@ class FpJobStepMove(models.Model):
|
|||||||
return moves
|
return moves
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# S23 — required transition-input gate
|
# S23 - required transition-input gate
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# When the destination step has requires_transition_form=True, the
|
# When the destination step has requires_transition_form=True, the
|
||||||
# recipe author wants chain-of-custody attestations captured on the
|
# recipe author wants chain-of-custody attestations captured on the
|
||||||
# move (location, photo, customer WO #, etc.). Same dormant-field
|
# move (location, photo, customer WO #, etc.). Same dormant-field
|
||||||
# shape as S22's signoff bug — the field existed but nothing enforced
|
# shape as S22's signoff bug - the field existed but nothing enforced
|
||||||
# it. Callers (tablet controllers, future backend wizards) MUST call
|
# it. Callers (tablet controllers, future backend wizards) MUST call
|
||||||
# _fp_check_transition_inputs_complete() after writing values to
|
# _fp_check_transition_inputs_complete() after writing values to
|
||||||
# transition_input_value_ids.
|
# transition_input_value_ids.
|
||||||
@@ -117,7 +117,7 @@ class FpJobStepMove(models.Model):
|
|||||||
def _fp_missing_required_transition_inputs(self):
|
def _fp_missing_required_transition_inputs(self):
|
||||||
"""Return the recordset of required transition_input prompts on
|
"""Return the recordset of required transition_input prompts on
|
||||||
the to_step's recipe node that have NO captured value on this
|
the to_step's recipe node that have NO captured value on this
|
||||||
move. Centralised helper — used by the gate below and by future
|
move. Centralised helper - used by the gate below and by future
|
||||||
diagnostics."""
|
diagnostics."""
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
Prompt = self.env['fusion.plating.process.node.input']
|
Prompt = self.env['fusion.plating.process.node.input']
|
||||||
@@ -145,7 +145,7 @@ class FpJobStepMove(models.Model):
|
|||||||
def _fp_check_transition_inputs_complete(self):
|
def _fp_check_transition_inputs_complete(self):
|
||||||
"""Raise UserError when the destination step has
|
"""Raise UserError when the destination step has
|
||||||
requires_transition_form=True and required transition_input
|
requires_transition_form=True and required transition_input
|
||||||
prompts haven't been recorded on this move. Audit gate — same
|
prompts haven't been recorded on this move. Audit gate - same
|
||||||
shape as fp.job.step._fp_check_step_inputs_complete (S21) and
|
shape as fp.job.step._fp_check_step_inputs_complete (S21) and
|
||||||
._fp_check_signoff_complete (S22).
|
._fp_check_signoff_complete (S22).
|
||||||
|
|
||||||
@@ -160,7 +160,7 @@ class FpJobStepMove(models.Model):
|
|||||||
continue
|
continue
|
||||||
move.message_post(body=Markup(_(
|
move.message_post(body=Markup(_(
|
||||||
'Transition-form gate bypassed by %s. '
|
'Transition-form gate bypassed by %s. '
|
||||||
'Documented deviation — required prompts not '
|
'Documented deviation - required prompts not '
|
||||||
'recorded on this move.'
|
'recorded on this move.'
|
||||||
)) % self.env.user.name)
|
)) % self.env.user.name)
|
||||||
return
|
return
|
||||||
@@ -172,7 +172,7 @@ class FpJobStepMove(models.Model):
|
|||||||
'"%s"' % (p.name or '').strip() for p in missing
|
'"%s"' % (p.name or '').strip() for p in missing
|
||||||
)
|
)
|
||||||
raise UserError(_(
|
raise UserError(_(
|
||||||
'Move to step "%(step)s" cannot be committed — '
|
'Move to step "%(step)s" cannot be committed - '
|
||||||
'%(n)s required transition prompt(s) not recorded: '
|
'%(n)s required transition prompt(s) not recorded: '
|
||||||
'%(names)s. Fill them in the Move dialog before '
|
'%(names)s. Fill them in the Move dialog before '
|
||||||
'committing. Managers can override via context flag '
|
'committing. Managers can override via context flag '
|
||||||
@@ -192,7 +192,7 @@ class FpJobStepMoveInputValue(models.Model):
|
|||||||
the operator typed at move-time. Used by Sub 12c CoC report.
|
the operator typed at move-time. Used by Sub 12c CoC report.
|
||||||
"""
|
"""
|
||||||
_name = 'fp.job.step.move.input.value'
|
_name = 'fp.job.step.move.input.value'
|
||||||
_description = 'Fusion Plating — Captured Transition Input Value'
|
_description = 'Fusion Plating - Captured Transition Input Value'
|
||||||
_order = 'move_id, id'
|
_order = 'move_id, id'
|
||||||
|
|
||||||
move_id = fields.Many2one('fp.job.step.move', string='Move',
|
move_id = fields.Many2one('fp.job.step.move', string='Move',
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# Copyright 2026 Nexa Systems Inc.
|
# Copyright 2026 Nexa Systems Inc.
|
||||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
#
|
#
|
||||||
# fp.job.step.timelog — granular start/stop intervals for a step.
|
# fp.job.step.timelog - granular start/stop intervals for a step.
|
||||||
#
|
#
|
||||||
# Each step.button_start() opens a fresh timelog row. Each
|
# Each step.button_start() opens a fresh timelog row. Each
|
||||||
# step.button_finish() (or button_pause once added) closes the open
|
# step.button_finish() (or button_pause once added) closes the open
|
||||||
@@ -55,7 +55,7 @@ class FpJobStepTimeLog(models.Model):
|
|||||||
rec_bits.append(mins)
|
rec_bits.append(mins)
|
||||||
log.display_name = ' · '.join(rec_bits)
|
log.display_name = ' · '.join(rec_bits)
|
||||||
|
|
||||||
# ===== Sub 12b — persistent timer state machine =========================
|
# ===== Sub 12b - persistent timer state machine =========================
|
||||||
# Extends the existing timelog (used by S1/S2 battle tests) with a state
|
# Extends the existing timelog (used by S1/S2 battle tests) with a state
|
||||||
# field + reconciliation columns. Default state='running' → existing
|
# field + reconciliation columns. Default state='running' → existing
|
||||||
# battle tests are unaffected. Stop Timer dialog (Task 13) flips to
|
# battle tests are unaffected. Stop Timer dialog (Task 13) flips to
|
||||||
@@ -214,7 +214,7 @@ class FpJobStepTimeLog(models.Model):
|
|||||||
"""Manager+ only: rewind a closed or stuck timelog back to running.
|
"""Manager+ only: rewind a closed or stuck timelog back to running.
|
||||||
Clears date_finished, last_paused_at, total_paused_seconds so accrued
|
Clears date_finished, last_paused_at, total_paused_seconds so accrued
|
||||||
starts fresh from the original date_started. Use for genuine corrections
|
starts fresh from the original date_started. Use for genuine corrections
|
||||||
only — the audit-trail entry names who did it."""
|
only - the audit-trail entry names who did it."""
|
||||||
if not self.env.user.has_group(
|
if not self.env.user.has_group(
|
||||||
'fusion_plating.group_fusion_plating_manager'):
|
'fusion_plating.group_fusion_plating_manager'):
|
||||||
raise AccessError(_(
|
raise AccessError(_(
|
||||||
@@ -234,7 +234,7 @@ class FpJobStepTimeLog(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Auto-resync hooks — keep fp.job.step.duration_actual in sync with
|
# Auto-resync hooks - keep fp.job.step.duration_actual in sync with
|
||||||
# the timelog rows. Without these, editing a timelog's start/end
|
# the timelog rows. Without these, editing a timelog's start/end
|
||||||
# leaves the step's "Actual Min" column showing stale data.
|
# leaves the step's "Actual Min" column showing stale data.
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|||||||
@@ -2,28 +2,28 @@
|
|||||||
# Copyright 2026 Nexa Systems Inc.
|
# Copyright 2026 Nexa Systems Inc.
|
||||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
# Part of the Fusion Plating product family.
|
# Part of the Fusion Plating product family.
|
||||||
"""Phase 1 + Phase E — Plating landing-page resolver.
|
"""Phase 1 + Phase E - Plating landing-page resolver.
|
||||||
|
|
||||||
Layers:
|
Layers:
|
||||||
|
|
||||||
1. ``ir.actions.act_window.x_fc_pickable_landing`` AND
|
1. ``ir.actions.act_window.x_fc_pickable_landing`` AND
|
||||||
``ir.actions.client.x_fc_pickable_landing`` — Boolean tag on BOTH
|
``ir.actions.client.x_fc_pickable_landing`` - Boolean tag on BOTH
|
||||||
action types. Mark a curated set of plating actions (Sale Orders,
|
action types. Mark a curated set of plating actions (Sale Orders,
|
||||||
Quotations, Manager Desk, Plant Kanban, Quality Dashboard, etc.) so
|
Quotations, Manager Desk, Plant Kanban, Quality Dashboard, etc.) so
|
||||||
the landing-page dropdown only offers sensible options, not all 200+
|
the landing-page dropdown only offers sensible options, not all 200+
|
||||||
action records in the DB.
|
action records in the DB.
|
||||||
|
|
||||||
2. ``res.company.x_fc_default_landing_action_id`` — admin sets the
|
2. ``res.company.x_fc_default_landing_action_id`` - admin sets the
|
||||||
fallback for users who don't pick a preference. References
|
fallback for users who don't pick a preference. References
|
||||||
``ir.actions.act_window`` (only act_window actions can be selected
|
``ir.actions.act_window`` (only act_window actions can be selected
|
||||||
as the company default since they're navigable from the menu tree).
|
as the company default since they're navigable from the menu tree).
|
||||||
|
|
||||||
3. ``res.users.x_fc_plating_landing_action_id`` — each user's own
|
3. ``res.users.x_fc_plating_landing_action_id`` - each user's own
|
||||||
override. References ``ir.actions.act_window`` and is filtered by
|
override. References ``ir.actions.act_window`` and is filtered by
|
||||||
the user's actually-accessible actions (Technician can't pick
|
the user's actually-accessible actions (Technician can't pick
|
||||||
"Manager Desk" if they can't see it).
|
"Manager Desk" if they can't see it).
|
||||||
|
|
||||||
4. ``ir.actions.act_window._fp_resolve_landing_for_current_user()`` —
|
4. ``ir.actions.act_window._fp_resolve_landing_for_current_user()`` -
|
||||||
role-based dispatch resolver. Section 3 of the permissions design
|
role-based dispatch resolver. Section 3 of the permissions design
|
||||||
spec. Returns an action dict suitable for the
|
spec. Returns an action dict suitable for the
|
||||||
``action_fp_resolve_plating_landing`` server action.
|
``action_fp_resolve_plating_landing`` server action.
|
||||||
@@ -58,7 +58,7 @@ class IrActionsActions(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _render_resolved(self):
|
def _render_resolved(self):
|
||||||
"""Dispatcher — render this action as a dict for the landing resolver.
|
"""Dispatcher - render this action as a dict for the landing resolver.
|
||||||
Routes to the correct subclass based on `type` so both act_window
|
Routes to the correct subclass based on `type` so both act_window
|
||||||
and client actions resolve correctly."""
|
and client actions resolve correctly."""
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
@@ -66,7 +66,7 @@ class IrActionsActions(models.Model):
|
|||||||
return self.env['ir.actions.client'].browse(self.id)._render_resolved()
|
return self.env['ir.actions.client'].browse(self.id)._render_resolved()
|
||||||
if self.type == 'ir.actions.act_window':
|
if self.type == 'ir.actions.act_window':
|
||||||
return self.env['ir.actions.act_window'].browse(self.id)._render_resolved()
|
return self.env['ir.actions.act_window'].browse(self.id)._render_resolved()
|
||||||
# URL / server / report — generic dict
|
# URL / server / report - generic dict
|
||||||
action = self.sudo().read()[0]
|
action = self.sudo().read()[0]
|
||||||
action.pop('id', None)
|
action.pop('id', None)
|
||||||
action['xml_id'] = self.get_external_id().get(self.id) or None
|
action['xml_id'] = self.get_external_id().get(self.id) or None
|
||||||
@@ -77,7 +77,7 @@ class IrActionsActWindow(models.Model):
|
|||||||
_inherit = 'ir.actions.act_window'
|
_inherit = 'ir.actions.act_window'
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Resolver — role-based dispatch (Phase E)
|
# Resolver - role-based dispatch (Phase E)
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@api.model
|
@api.model
|
||||||
def _fp_resolve_landing_for_current_user(self):
|
def _fp_resolve_landing_for_current_user(self):
|
||||||
@@ -108,7 +108,7 @@ class IrActionsActWindow(models.Model):
|
|||||||
and company.x_fc_default_landing_action_id:
|
and company.x_fc_default_landing_action_id:
|
||||||
return company.x_fc_default_landing_action_id._render_resolved()
|
return company.x_fc_default_landing_action_id._render_resolved()
|
||||||
|
|
||||||
# 4. Hardcoded last-ditch — Sale Orders
|
# 4. Hardcoded last-ditch - Sale Orders
|
||||||
fallback = self.env.ref(
|
fallback = self.env.ref(
|
||||||
'fusion_plating_configurator.action_fp_sale_orders',
|
'fusion_plating_configurator.action_fp_sale_orders',
|
||||||
raise_if_not_found=False,
|
raise_if_not_found=False,
|
||||||
@@ -152,7 +152,7 @@ class IrActionsActWindow(models.Model):
|
|||||||
|
|
||||||
Returns ``action_fp_plant_kanban`` (the 2026-05-23 plant view).
|
Returns ``action_fp_plant_kanban`` (the 2026-05-23 plant view).
|
||||||
The legacy ``fp_shopfloor_landing`` component was retired
|
The legacy ``fp_shopfloor_landing`` component was retired
|
||||||
2026-05-25 (one feature ported across — the inline QR scanner).
|
2026-05-25 (one feature ported across - the inline QR scanner).
|
||||||
The ``fusion_plating_shopfloor.layout`` ir.config_parameter
|
The ``fusion_plating_shopfloor.layout`` ir.config_parameter
|
||||||
survives orphaned for one release cycle so we can ship a
|
survives orphaned for one release cycle so we can ship a
|
||||||
settings-UI cleanup separately; flipping it has no effect.
|
settings-UI cleanup separately; flipping it has no effect.
|
||||||
@@ -179,7 +179,7 @@ class IrActionsActWindow(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class IrActionsClient(models.Model):
|
class IrActionsClient(models.Model):
|
||||||
"""Client actions also need to be tagged as pickable landings —
|
"""Client actions also need to be tagged as pickable landings -
|
||||||
Manager Desk, Plant Kanban, Quality Dashboard are all client
|
Manager Desk, Plant Kanban, Quality Dashboard are all client
|
||||||
actions, not act_window records.
|
actions, not act_window records.
|
||||||
|
|
||||||
@@ -189,7 +189,7 @@ class IrActionsClient(models.Model):
|
|||||||
"""
|
"""
|
||||||
_inherit = 'ir.actions.client'
|
_inherit = 'ir.actions.client'
|
||||||
|
|
||||||
# x_fc_pickable_landing moved to ir.actions.actions base — see IrActionsActions
|
# x_fc_pickable_landing moved to ir.actions.actions base - see IrActionsActions
|
||||||
# above. This subclass keeps _render_resolved for the dispatcher to call.
|
# above. This subclass keeps _render_resolved for the dispatcher to call.
|
||||||
|
|
||||||
def _render_resolved(self):
|
def _render_resolved(self):
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""Phase H — dry-run + Owner-approval migration workflow."""
|
"""Phase H - dry-run + Owner-approval migration workflow."""
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
@@ -96,7 +96,7 @@ class FpMigrationPreview(models.Model):
|
|||||||
|
|
||||||
def _fp_notify_owners(self):
|
def _fp_notify_owners(self):
|
||||||
"""Schedule a 'Review Fusion Plating role migration' activity on
|
"""Schedule a 'Review Fusion Plating role migration' activity on
|
||||||
every Owner user. Idempotent — won't double-schedule."""
|
every Owner user. Idempotent - won't double-schedule."""
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
owner_grp = self.env.ref('fusion_plating.group_fp_owner', raise_if_not_found=False)
|
owner_grp = self.env.ref('fusion_plating.group_fp_owner', raise_if_not_found=False)
|
||||||
if not owner_grp:
|
if not owner_grp:
|
||||||
@@ -214,7 +214,7 @@ class FpMigrationPreview(models.Model):
|
|||||||
# Clear snapshots (no more rollback possible)
|
# Clear snapshots (no more rollback possible)
|
||||||
for preview in expired:
|
for preview in expired:
|
||||||
preview.line_ids.write({'applied_groups_snapshot': False})
|
preview.line_ids.write({'applied_groups_snapshot': False})
|
||||||
# Unlink old plating groups (now confirmed unused — every user is
|
# Unlink old plating groups (now confirmed unused - every user is
|
||||||
# on the new groups; backward-compat implied_ids chains can drop)
|
# on the new groups; backward-compat implied_ids chains can drop)
|
||||||
old_group_ids = []
|
old_group_ids = []
|
||||||
for xmlid in _FP_OLD_GROUP_XMLIDS:
|
for xmlid in _FP_OLD_GROUP_XMLIDS:
|
||||||
@@ -222,7 +222,7 @@ class FpMigrationPreview(models.Model):
|
|||||||
if g:
|
if g:
|
||||||
old_group_ids.append(g.id)
|
old_group_ids.append(g.id)
|
||||||
if old_group_ids:
|
if old_group_ids:
|
||||||
# I6 safety check — never unlink a group that still has active
|
# I6 safety check - never unlink a group that still has active
|
||||||
# internal users on it. If anyone still references the group
|
# internal users on it. If anyone still references the group
|
||||||
# we'd cascade-strip them silently from their permissions.
|
# we'd cascade-strip them silently from their permissions.
|
||||||
safe_to_unlink = []
|
safe_to_unlink = []
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class FpOperatorCertification(models.Model):
|
|||||||
for that process.
|
for that process.
|
||||||
"""
|
"""
|
||||||
_name = 'fp.operator.certification'
|
_name = 'fp.operator.certification'
|
||||||
_description = 'Fusion Plating — Operator Certification'
|
_description = 'Fusion Plating - Operator Certification'
|
||||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||||
_order = 'employee_id, process_type_id'
|
_order = 'employee_id, process_type_id'
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ class FpOperatorCertification(models.Model):
|
|||||||
('revoked', 'Revoked')],
|
('revoked', 'Revoked')],
|
||||||
string='Status',
|
string='Status',
|
||||||
compute='_compute_state', store=True, tracking=True,
|
compute='_compute_state', store=True, tracking=True,
|
||||||
# NOT readonly=False — this is purely derived from revoked + expires_date
|
# NOT readonly=False - this is purely derived from revoked + expires_date
|
||||||
# so the nightly recompute never fights with manual edits.
|
# so the nightly recompute never fights with manual edits.
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ class FpOperatorCertification(models.Model):
|
|||||||
continue
|
continue
|
||||||
if rec.expires_date and rec.expires_date < today:
|
if rec.expires_date and rec.expires_date < today:
|
||||||
continue
|
continue
|
||||||
# This record is active — look for another active sibling
|
# This record is active - look for another active sibling
|
||||||
dupes = self.search_count([
|
dupes = self.search_count([
|
||||||
('id', '!=', rec.id),
|
('id', '!=', rec.id),
|
||||||
('employee_id', '=', rec.employee_id.id),
|
('employee_id', '=', rec.employee_id.id),
|
||||||
@@ -108,7 +108,7 @@ class FpOperatorCertification(models.Model):
|
|||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def has_active_cert(self, employee_id, process_type_id):
|
def has_active_cert(self, employee_id, process_type_id):
|
||||||
"""Utility — True if this employee holds a current certification.
|
"""Utility - True if this employee holds a current certification.
|
||||||
|
|
||||||
Checks revoked + expires_date directly instead of the computed
|
Checks revoked + expires_date directly instead of the computed
|
||||||
`state` column, so even a certification that expired yesterday
|
`state` column, so even a certification that expired yesterday
|
||||||
|
|||||||
@@ -84,21 +84,21 @@ class FpParentNumberedMixin(models.AbstractModel):
|
|||||||
# downstream modules (e.g. fusion_plating_receiving) inherit the
|
# downstream modules (e.g. fusion_plating_receiving) inherit the
|
||||||
# mixin but don't depend on jobs, so so.x_fc_parent_number can
|
# mixin but don't depend on jobs, so so.x_fc_parent_number can
|
||||||
# raise AttributeError at test time. hasattr keeps the mixin safe
|
# raise AttributeError at test time. hasattr keeps the mixin safe
|
||||||
# in either install topology — falls through to the legacy
|
# in either install topology - falls through to the legacy
|
||||||
# sequence when the column isn't there.
|
# sequence when the column isn't there.
|
||||||
if not so or 'x_fc_parent_number' not in so._fields:
|
if not so or 'x_fc_parent_number' not in so._fields:
|
||||||
return False
|
return False
|
||||||
if not so.x_fc_parent_number:
|
if not so.x_fc_parent_number:
|
||||||
return False
|
return False
|
||||||
counter_field = self._fp_parent_counter_field()
|
counter_field = self._fp_parent_counter_field()
|
||||||
# Whitelist check — the field name is interpolated directly into
|
# Whitelist check - the field name is interpolated directly into
|
||||||
# SQL below, so we never trust an arbitrary string. All current
|
# SQL below, so we never trust an arbitrary string. All current
|
||||||
# subclasses return a literal; this guard exists so a future
|
# subclasses return a literal; this guard exists so a future
|
||||||
# subclass that reads the field name from context / Selection /
|
# subclass that reads the field name from context / Selection /
|
||||||
# user input can't smuggle a SQL fragment in.
|
# user input can't smuggle a SQL fragment in.
|
||||||
if not _FP_COUNTER_FIELD_RE.match(counter_field or ''):
|
if not _FP_COUNTER_FIELD_RE.match(counter_field or ''):
|
||||||
raise UserError(_(
|
raise UserError(_(
|
||||||
'Invalid parent-counter field name %r — must match '
|
'Invalid parent-counter field name %r - must match '
|
||||||
'pattern x_fc_pn_*_count.'
|
'pattern x_fc_pn_*_count.'
|
||||||
) % counter_field)
|
) % counter_field)
|
||||||
# SELECT FOR UPDATE - locks the SO row until commit, so a
|
# SELECT FOR UPDATE - locks the SO row until commit, so a
|
||||||
@@ -163,12 +163,12 @@ class FpParentNumberedMixin(models.AbstractModel):
|
|||||||
# Cancellation must go through the state machine so the audit trail
|
# Cancellation must go through the state machine so the audit trail
|
||||||
# keeps the issued number tied to its cancellation reason. Hard
|
# keeps the issued number tied to its cancellation reason. Hard
|
||||||
# delete would leave a phantom gap in the counter. Applies to ALL
|
# delete would leave a phantom gap in the counter. Applies to ALL
|
||||||
# users including admins — no group bypass.
|
# users including admins - no group bypass.
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
def unlink(self):
|
def unlink(self):
|
||||||
for rec in self:
|
for rec in self:
|
||||||
# Records still in their initial 'New' state (no number
|
# Records still in their initial 'New' state (no number
|
||||||
# ever issued) are fine to delete — they're not yet in
|
# ever issued) are fine to delete - they're not yet in
|
||||||
# the audit trail. Once x_fc_doc_index is non-zero OR
|
# the audit trail. Once x_fc_doc_index is non-zero OR
|
||||||
# name is something other than 'New' / '/', the record
|
# name is something other than 'New' / '/', the record
|
||||||
# has been issued and is permanent.
|
# has been issued and is permanent.
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class FpProcessCategory(models.Model):
|
|||||||
load specific process types.
|
load specific process types.
|
||||||
"""
|
"""
|
||||||
_name = 'fusion.plating.process.category'
|
_name = 'fusion.plating.process.category'
|
||||||
_description = 'Fusion Plating — Process Category'
|
_description = 'Fusion Plating - Process Category'
|
||||||
_order = 'sequence, name'
|
_order = 'sequence, name'
|
||||||
|
|
||||||
name = fields.Char(
|
name = fields.Char(
|
||||||
|
|||||||
@@ -19,15 +19,15 @@ class FpProcessNode(models.Model):
|
|||||||
|
|
||||||
Node types
|
Node types
|
||||||
----------
|
----------
|
||||||
* recipe — top-level root (e.g. "Electroless Nickel — Steel Line")
|
* recipe - top-level root (e.g. "Electroless Nickel - Steel Line")
|
||||||
* sub_process — a group of operations (e.g. "Steel Line", "Cleaner")
|
* sub_process - a group of operations (e.g. "Steel Line", "Cleaner")
|
||||||
* operation — a single production step (e.g. "Acid Dip", "Nickel Strike")
|
* operation - a single production step (e.g. "Acid Dip", "Nickel Strike")
|
||||||
* step — a sub-step within an operation (e.g. "Ready for Blast", "Blast")
|
* step - a sub-step within an operation (e.g. "Ready for Blast", "Blast")
|
||||||
|
|
||||||
Hierarchy uses Odoo's _parent_store for efficient tree queries.
|
Hierarchy uses Odoo's _parent_store for efficient tree queries.
|
||||||
"""
|
"""
|
||||||
_name = 'fusion.plating.process.node'
|
_name = 'fusion.plating.process.node'
|
||||||
_description = 'Fusion Plating — Process Node'
|
_description = 'Fusion Plating - Process Node'
|
||||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||||
_parent_store = True
|
_parent_store = True
|
||||||
_parent_name = 'parent_id'
|
_parent_name = 'parent_id'
|
||||||
@@ -108,7 +108,7 @@ class FpProcessNode(models.Model):
|
|||||||
string='Description',
|
string='Description',
|
||||||
help='Rich text instructions for this step.',
|
help='Rich text instructions for this step.',
|
||||||
)
|
)
|
||||||
# Sub 12d — master switch for runtime data collection. When False the
|
# Sub 12d - master switch for runtime data collection. When False the
|
||||||
# operator wizard skips this step entirely (no input prompts shown).
|
# operator wizard skips this step entirely (no input prompts shown).
|
||||||
collect_measurements = fields.Boolean(
|
collect_measurements = fields.Boolean(
|
||||||
string='Collect Measurements at Runtime',
|
string='Collect Measurements at Runtime',
|
||||||
@@ -178,7 +178,7 @@ class FpProcessNode(models.Model):
|
|||||||
# Recipe authors attach photos and screenshots here so operators see
|
# Recipe authors attach photos and screenshots here so operators see
|
||||||
# them on the shop floor when running the step. Anything from a
|
# them on the shop floor when running the step. Anything from a
|
||||||
# process diagram, masking-line photo, or annotated screenshot of the
|
# process diagram, masking-line photo, or annotated screenshot of the
|
||||||
# WI document. Many2many — supports zero, one, or many images.
|
# WI document. Many2many - supports zero, one, or many images.
|
||||||
instruction_attachment_ids = fields.Many2many(
|
instruction_attachment_ids = fields.Many2many(
|
||||||
'ir.attachment',
|
'ir.attachment',
|
||||||
'fp_node_instruction_attachment_rel',
|
'fp_node_instruction_attachment_rel',
|
||||||
@@ -187,7 +187,7 @@ class FpProcessNode(models.Model):
|
|||||||
domain=[('mimetype', 'ilike', 'image/')],
|
domain=[('mimetype', 'ilike', 'image/')],
|
||||||
help='Reference photos and screenshots that operators see at '
|
help='Reference photos and screenshots that operators see at '
|
||||||
'runtime. Anything visual that helps them execute the step '
|
'runtime. Anything visual that helps them execute the step '
|
||||||
'correctly — fixture orientation, masking pattern, gauge '
|
'correctly - fixture orientation, masking pattern, gauge '
|
||||||
'reading. Supports multiple images per step.',
|
'reading. Supports multiple images per step.',
|
||||||
)
|
)
|
||||||
instruction_attachment_count = fields.Integer(
|
instruction_attachment_count = fields.Integer(
|
||||||
@@ -227,7 +227,7 @@ class FpProcessNode(models.Model):
|
|||||||
requires_signoff = fields.Boolean(
|
requires_signoff = fields.Boolean(
|
||||||
string='Requires Sign-Off',
|
string='Requires Sign-Off',
|
||||||
default=False,
|
default=False,
|
||||||
help='Quality hold point — requires operator sign-off.',
|
help='Quality hold point - requires operator sign-off.',
|
||||||
)
|
)
|
||||||
requires_predecessor_done = fields.Boolean(
|
requires_predecessor_done = fields.Boolean(
|
||||||
string='Requires Predecessor Done (legacy)',
|
string='Requires Predecessor Done (legacy)',
|
||||||
@@ -235,16 +235,16 @@ class FpProcessNode(models.Model):
|
|||||||
help='LEGACY per-step opt-in for predecessor enforcement. As of '
|
help='LEGACY per-step opt-in for predecessor enforcement. As of '
|
||||||
'19.0.X, recipes default to enforce_sequential=True so every '
|
'19.0.X, recipes default to enforce_sequential=True so every '
|
||||||
'step naturally waits for its predecessors. This flag still '
|
'step naturally waits for its predecessors. This flag still '
|
||||||
'works on recipes whose enforce_sequential is False — turn '
|
'works on recipes whose enforce_sequential is False - turn '
|
||||||
'it on to make a single step block in an otherwise free-flow '
|
'it on to make a single step block in an otherwise free-flow '
|
||||||
'recipe.',
|
'recipe.',
|
||||||
)
|
)
|
||||||
# ===== Sub 13 — sequential step enforcement (recipe + per-step) ==========
|
# ===== Sub 13 - sequential step enforcement (recipe + per-step) ==========
|
||||||
# Replaces the unused per-step requires_predecessor_done as the primary
|
# Replaces the unused per-step requires_predecessor_done as the primary
|
||||||
# enforcement vector. Two layers:
|
# enforcement vector. Two layers:
|
||||||
# 1. enforce_sequential (recipe root) — entire recipe is sequential
|
# 1. enforce_sequential (recipe root) - entire recipe is sequential
|
||||||
# by default. Author can disable for free-flow recipes.
|
# by default. Author can disable for free-flow recipes.
|
||||||
# 2. parallel_start (operation step) — escape hatch within a
|
# 2. parallel_start (operation step) - escape hatch within a
|
||||||
# sequential recipe, for steps that legitimately run in parallel
|
# sequential recipe, for steps that legitimately run in parallel
|
||||||
# (e.g. paperwork that doesn't need previous step done).
|
# (e.g. paperwork that doesn't need previous step done).
|
||||||
enforce_sequential = fields.Boolean(
|
enforce_sequential = fields.Boolean(
|
||||||
@@ -286,10 +286,10 @@ class FpProcessNode(models.Model):
|
|||||||
default='disabled',
|
default='disabled',
|
||||||
help='Controls whether this step can be skipped or added on a '
|
help='Controls whether this step can be skipped or added on a '
|
||||||
'per-job basis:\n'
|
'per-job basis:\n'
|
||||||
' * Required — every job runs this step. Cannot be removed.\n'
|
' * Required - every job runs this step. Cannot be removed.\n'
|
||||||
' * Opt-Out — included by default; an estimator can remove '
|
' * Opt-Out - included by default; an estimator can remove '
|
||||||
'it per job when the customer doesn\'t need it.\n'
|
'it per job when the customer doesn\'t need it.\n'
|
||||||
' * Opt-In — excluded by default; an estimator can add it '
|
' * Opt-In - excluded by default; an estimator can add it '
|
||||||
'per job when the customer specifically asks for it.',
|
'per job when the customer specifically asks for it.',
|
||||||
tracking=True,
|
tracking=True,
|
||||||
)
|
)
|
||||||
@@ -314,7 +314,7 @@ class FpProcessNode(models.Model):
|
|||||||
# ---- Part ownership & provenance (Sub 3) --------------------------------
|
# ---- Part ownership & provenance (Sub 3) --------------------------------
|
||||||
|
|
||||||
# Sub 3 fields (part_catalog_id, cloned_from_id, treatment_uom) are
|
# Sub 3 fields (part_catalog_id, cloned_from_id, treatment_uom) are
|
||||||
# declared as an inherit in fusion_plating_configurator — they need
|
# declared as an inherit in fusion_plating_configurator - they need
|
||||||
# to reference fp.part.catalog, which lives in configurator (a child
|
# to reference fp.part.catalog, which lives in configurator (a child
|
||||||
# module). Adding them here would create a circular dependency.
|
# module). Adding them here would create a circular dependency.
|
||||||
# See fusion_plating_configurator/models/fp_process_node_inherit.py.
|
# See fusion_plating_configurator/models/fp_process_node_inherit.py.
|
||||||
@@ -354,9 +354,9 @@ class FpProcessNode(models.Model):
|
|||||||
# NB. `pricing_rule_ids` lives in fusion_plating_configurator
|
# NB. `pricing_rule_ids` lives in fusion_plating_configurator
|
||||||
# (added there so this core module doesn't depend on the configurator).
|
# (added there so this core module doesn't depend on the configurator).
|
||||||
|
|
||||||
# ---- Spec-derived metadata (recipe-root only — Promote Customer Spec) ----
|
# ---- Spec-derived metadata (recipe-root only - Promote Customer Spec) ----
|
||||||
# These were on fp.coating.config (since retired). They describe the
|
# These were on fp.coating.config (since retired). They describe the
|
||||||
# PROCESS the recipe runs, not the customer-facing specification —
|
# PROCESS the recipe runs, not the customer-facing specification -
|
||||||
# specs live on fusion.plating.customer.spec.
|
# specs live on fusion.plating.customer.spec.
|
||||||
phosphorus_level = fields.Selection(
|
phosphorus_level = fields.Selection(
|
||||||
[('low_phos', 'Low Phosphorus (2-5%)'),
|
[('low_phos', 'Low Phosphorus (2-5%)'),
|
||||||
@@ -375,12 +375,12 @@ class FpProcessNode(models.Model):
|
|||||||
[('mils', 'mils'), ('microns', 'microns'), ('inches', 'inches')],
|
[('mils', 'mils'), ('microns', 'microns'), ('inches', 'inches')],
|
||||||
string='Thickness UoM', default='mils',
|
string='Thickness UoM', default='mils',
|
||||||
)
|
)
|
||||||
# thickness_option_ids removed — fp.recipe.thickness model deleted.
|
# thickness_option_ids removed - fp.recipe.thickness model deleted.
|
||||||
# Thickness on the SO line is now a free-text Char range (e.g.
|
# Thickness on the SO line is now a free-text Char range (e.g.
|
||||||
# "0.0005-0.0008 mils") that auto-fills from last-used per
|
# "0.0005-0.0008 mils") that auto-fills from last-used per
|
||||||
# (part, customer) or the part's x_fc_default_thickness_range.
|
# (part, customer) or the part's x_fc_default_thickness_range.
|
||||||
|
|
||||||
# ---- Bake relief — AMS 2759/9 hydrogen embrittlement (recipe root) ----
|
# ---- Bake relief - AMS 2759/9 hydrogen embrittlement (recipe root) ----
|
||||||
requires_bake_relief = fields.Boolean(
|
requires_bake_relief = fields.Boolean(
|
||||||
string='Requires Bake Relief',
|
string='Requires Bake Relief',
|
||||||
help='Hydrogen embrittlement relief bake required (high-strength '
|
help='Hydrogen embrittlement relief bake required (high-strength '
|
||||||
@@ -454,7 +454,7 @@ class FpProcessNode(models.Model):
|
|||||||
copy=True,
|
copy=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# ===== Sub 12a — Simple Editor + Step Library extensions =================
|
# ===== Sub 12a - Simple Editor + Step Library extensions =================
|
||||||
# All fields are additive; tree editor + runtime are unaffected. Drag-drop
|
# All fields are additive; tree editor + runtime are unaffected. Drag-drop
|
||||||
# from the library snapshot-copies these into a new node (no live ref).
|
# from the library snapshot-copies these into a new node (no live ref).
|
||||||
|
|
||||||
@@ -468,7 +468,7 @@ class FpProcessNode(models.Model):
|
|||||||
string='Source Library Template',
|
string='Source Library Template',
|
||||||
ondelete='set null',
|
ondelete='set null',
|
||||||
index=True,
|
index=True,
|
||||||
help='Snapshot trace — set when this node was created by dragging '
|
help='Snapshot trace - set when this node was created by dragging '
|
||||||
'a library step in. Editing the template later does not change '
|
'a library step in. Editing the template later does not change '
|
||||||
'this node (snapshot semantics).',
|
'this node (snapshot semantics).',
|
||||||
)
|
)
|
||||||
@@ -499,14 +499,14 @@ class FpProcessNode(models.Model):
|
|||||||
viscosity_target = fields.Float(string='Viscosity Target')
|
viscosity_target = fields.Float(string='Viscosity Target')
|
||||||
requires_rack_assignment = fields.Boolean(
|
requires_rack_assignment = fields.Boolean(
|
||||||
string='Requires Rack Assignment',
|
string='Requires Rack Assignment',
|
||||||
help='Sub 12b — triggers Rack Parts sub-dialog at runtime.',
|
help='Sub 12b - triggers Rack Parts sub-dialog at runtime.',
|
||||||
)
|
)
|
||||||
requires_transition_form = fields.Boolean(
|
requires_transition_form = fields.Boolean(
|
||||||
string='Requires Transition Form',
|
string='Requires Transition Form',
|
||||||
help='Sub 12b — opens the transition form before Mark Done.',
|
help='Sub 12b - opens the transition form before Mark Done.',
|
||||||
)
|
)
|
||||||
|
|
||||||
# Certificate Output — recipe-level cert suppression (2026-05-27 sub
|
# Certificate Output - recipe-level cert suppression (2026-05-27 sub
|
||||||
# docs/superpowers/specs/2026-05-27-recipe-cert-toggles-design.md).
|
# docs/superpowers/specs/2026-05-27-recipe-cert-toggles-design.md).
|
||||||
# Default True for all five so existing recipes keep producing the
|
# Default True for all five so existing recipes keep producing the
|
||||||
# same cert set they produce today. A recipe author flips OFF only
|
# same cert set they produce today. A recipe author flips OFF only
|
||||||
@@ -532,7 +532,7 @@ class FpProcessNode(models.Model):
|
|||||||
default=True,
|
default=True,
|
||||||
help='When False, this recipe never produces a thickness report. '
|
help='When False, this recipe never produces a thickness report. '
|
||||||
'Use for passivation, chemical conversion, anodize seal-only, '
|
'Use for passivation, chemical conversion, anodize seal-only, '
|
||||||
'etc. — processes that physically have no plating thickness '
|
'etc. - processes that physically have no plating thickness '
|
||||||
'to measure.',
|
'to measure.',
|
||||||
)
|
)
|
||||||
requires_nadcap_cert = fields.Boolean(
|
requires_nadcap_cert = fields.Boolean(
|
||||||
@@ -555,8 +555,8 @@ class FpProcessNode(models.Model):
|
|||||||
'cert.',
|
'cert.',
|
||||||
)
|
)
|
||||||
|
|
||||||
# Sub 14b — User-extensible Step Kinds (was Selection of 24).
|
# Sub 14b - User-extensible Step Kinds (was Selection of 24).
|
||||||
# 2026-05-20: required + ondelete='restrict' — kind drives gates,
|
# 2026-05-20: required + ondelete='restrict' - kind drives gates,
|
||||||
# workflow milestones, and operator routing. Optional was a foot-gun
|
# workflow milestones, and operator routing. Optional was a foot-gun
|
||||||
# (operators silently picked Generic / nothing). Pre-migrate
|
# (operators silently picked Generic / nothing). Pre-migrate
|
||||||
# 19.0.20.6.0 backfills every existing row before this NOT NULL
|
# 19.0.20.6.0 backfills every existing row before this NOT NULL
|
||||||
@@ -642,7 +642,7 @@ class FpProcessNode(models.Model):
|
|||||||
# how stable a recipe is and (later) lets a job pin to a specific
|
# how stable a recipe is and (later) lets a job pin to a specific
|
||||||
# recipe revision so already-running MOs don't see mid-flight changes.
|
# recipe revision so already-running MOs don't see mid-flight changes.
|
||||||
|
|
||||||
# Fields that don't represent a "meaningful" change — adjusting these
|
# Fields that don't represent a "meaningful" change - adjusting these
|
||||||
# alone does not bump the version. `version` itself is in the list to
|
# alone does not bump the version. `version` itself is in the list to
|
||||||
# avoid an infinite write loop.
|
# avoid an infinite write loop.
|
||||||
_FP_NON_VERSIONED_FIELDS = {
|
_FP_NON_VERSIONED_FIELDS = {
|
||||||
@@ -656,7 +656,7 @@ class FpProcessNode(models.Model):
|
|||||||
the current recordset."""
|
the current recordset."""
|
||||||
roots = self.mapped('recipe_root_id')
|
roots = self.mapped('recipe_root_id')
|
||||||
# _compute_recipe_root_id falls back to self for nodes whose
|
# _compute_recipe_root_id falls back to self for nodes whose
|
||||||
# parent_path isn't yet stored — pick those up too.
|
# parent_path isn't yet stored - pick those up too.
|
||||||
for rec in self:
|
for rec in self:
|
||||||
if not rec.recipe_root_id and rec.node_type == 'recipe':
|
if not rec.recipe_root_id and rec.node_type == 'recipe':
|
||||||
roots |= rec
|
roots |= rec
|
||||||
@@ -676,7 +676,7 @@ class FpProcessNode(models.Model):
|
|||||||
@api.model_create_multi
|
@api.model_create_multi
|
||||||
def create(self, vals_list):
|
def create(self, vals_list):
|
||||||
records = super().create(vals_list)
|
records = super().create(vals_list)
|
||||||
# Skip non-recipe roots — only count when the new node lives
|
# Skip non-recipe roots - only count when the new node lives
|
||||||
# inside an existing recipe.
|
# inside an existing recipe.
|
||||||
descendants = records.filtered(lambda r: r.node_type != 'recipe')
|
descendants = records.filtered(lambda r: r.node_type != 'recipe')
|
||||||
if descendants:
|
if descendants:
|
||||||
@@ -684,7 +684,7 @@ class FpProcessNode(models.Model):
|
|||||||
return records
|
return records
|
||||||
|
|
||||||
def write(self, vals):
|
def write(self, vals):
|
||||||
# NADCAP / change-control lock — block writes on locked recipes
|
# NADCAP / change-control lock - block writes on locked recipes
|
||||||
# (and their descendants) for non-manager users. Manager bypass
|
# (and their descendants) for non-manager users. Manager bypass
|
||||||
# so the lock can be toggled off.
|
# so the lock can be toggled off.
|
||||||
if (self
|
if (self
|
||||||
@@ -759,11 +759,11 @@ class FpProcessNode(models.Model):
|
|||||||
'customer_visible': self.customer_visible,
|
'customer_visible': self.customer_visible,
|
||||||
'is_manual': self.is_manual,
|
'is_manual': self.is_manual,
|
||||||
'requires_signoff': self.requires_signoff,
|
'requires_signoff': self.requires_signoff,
|
||||||
# Sub 13 — sequential enforcement
|
# Sub 13 - sequential enforcement
|
||||||
'enforce_sequential': self.enforce_sequential,
|
'enforce_sequential': self.enforce_sequential,
|
||||||
'parallel_start': self.parallel_start,
|
'parallel_start': self.parallel_start,
|
||||||
'requires_predecessor_done': self.requires_predecessor_done,
|
'requires_predecessor_done': self.requires_predecessor_done,
|
||||||
# Sub 14 — workflow milestone trigger (Many2one or False)
|
# Sub 14 - workflow milestone trigger (Many2one or False)
|
||||||
'triggers_workflow_state_id': (
|
'triggers_workflow_state_id': (
|
||||||
self.triggers_workflow_state_id.id
|
self.triggers_workflow_state_id.id
|
||||||
if 'triggers_workflow_state_id' in self._fields
|
if 'triggers_workflow_state_id' in self._fields
|
||||||
@@ -817,7 +817,7 @@ class FpProcessNode(models.Model):
|
|||||||
return {
|
return {
|
||||||
'type': 'ir.actions.client',
|
'type': 'ir.actions.client',
|
||||||
'tag': 'fp_recipe_tree_editor',
|
'tag': 'fp_recipe_tree_editor',
|
||||||
'name': f'Recipe — {root.name}',
|
'name': f'Recipe - {root.name}',
|
||||||
'context': {'recipe_id': root.id},
|
'context': {'recipe_id': root.id},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -828,7 +828,7 @@ class FpProcessNode(models.Model):
|
|||||||
return {
|
return {
|
||||||
'type': 'ir.actions.client',
|
'type': 'ir.actions.client',
|
||||||
'tag': 'fp_simple_recipe_editor',
|
'tag': 'fp_simple_recipe_editor',
|
||||||
'name': f'Recipe — {root.name}',
|
'name': f'Recipe - {root.name}',
|
||||||
'context': {'recipe_id': root.id},
|
'context': {'recipe_id': root.id},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -846,7 +846,7 @@ class FpProcessNode(models.Model):
|
|||||||
def action_open_recipe_with_preferred_editor(self):
|
def action_open_recipe_with_preferred_editor(self):
|
||||||
"""Routes to whichever editor the recipe (or company) prefers.
|
"""Routes to whichever editor the recipe (or company) prefers.
|
||||||
|
|
||||||
Used by menu actions / context-menu opens — gives the
|
Used by menu actions / context-menu opens - gives the
|
||||||
simple-loving foreman a one-click path that respects their
|
simple-loving foreman a one-click path that respects their
|
||||||
preference without forcing a tree-loving engineer to pick
|
preference without forcing a tree-loving engineer to pick
|
||||||
between two buttons every time.
|
between two buttons every time.
|
||||||
@@ -930,10 +930,10 @@ class FpProcessNodeInput(models.Model):
|
|||||||
"""An operator input definition attached to a process node.
|
"""An operator input definition attached to a process node.
|
||||||
|
|
||||||
These define what the operator needs to record when executing this
|
These define what the operator needs to record when executing this
|
||||||
step — temperature readings, visual inspections, timing, etc.
|
step - temperature readings, visual inspections, timing, etc.
|
||||||
"""
|
"""
|
||||||
_name = 'fusion.plating.process.node.input'
|
_name = 'fusion.plating.process.node.input'
|
||||||
_description = 'Fusion Plating — Process Node Input'
|
_description = 'Fusion Plating - Process Node Input'
|
||||||
_order = 'sequence, id'
|
_order = 'sequence, id'
|
||||||
|
|
||||||
name = fields.Char(
|
name = fields.Char(
|
||||||
@@ -954,7 +954,7 @@ class FpProcessNodeInput(models.Model):
|
|||||||
('boolean', 'Yes / No'),
|
('boolean', 'Yes / No'),
|
||||||
('selection', 'Selection'),
|
('selection', 'Selection'),
|
||||||
('photo', 'Photo'),
|
('photo', 'Photo'),
|
||||||
# Sub 12a — typed inputs the simple editor + traveller need
|
# Sub 12a - typed inputs the simple editor + traveller need
|
||||||
('time_hms', 'Time (HH:MM:SS)'),
|
('time_hms', 'Time (HH:MM:SS)'),
|
||||||
('time_seconds', 'Time (seconds)'),
|
('time_seconds', 'Time (seconds)'),
|
||||||
('temperature', 'Temperature'),
|
('temperature', 'Temperature'),
|
||||||
@@ -992,11 +992,11 @@ class FpProcessNodeInput(models.Model):
|
|||||||
uom = fields.Selection(
|
uom = fields.Selection(
|
||||||
FP_UOM_SELECTION,
|
FP_UOM_SELECTION,
|
||||||
string='Unit',
|
string='Unit',
|
||||||
help='Unit the operator is recording in (pick from the curated list — '
|
help='Unit the operator is recording in (pick from the curated list - '
|
||||||
'avoids "kg" vs "kgs" vs "kilo" inconsistencies).',
|
'avoids "kg" vs "kgs" vs "kilo" inconsistencies).',
|
||||||
)
|
)
|
||||||
|
|
||||||
# ===== Sub 12a — kind + target ranges + compliance tag ==================
|
# ===== Sub 12a - kind + target ranges + compliance tag ==================
|
||||||
kind = fields.Selection(
|
kind = fields.Selection(
|
||||||
[
|
[
|
||||||
('step_input', 'Step Measurement'),
|
('step_input', 'Step Measurement'),
|
||||||
@@ -1036,7 +1036,7 @@ class FpProcessNodeInput(models.Model):
|
|||||||
string='Compliance Tag', default='none',
|
string='Compliance Tag', default='none',
|
||||||
)
|
)
|
||||||
|
|
||||||
# ===== Sub 12d — per-recipe configurability =============================
|
# ===== Sub 12d - per-recipe configurability =============================
|
||||||
collect = fields.Boolean(
|
collect = fields.Boolean(
|
||||||
string='Collect This Measurement',
|
string='Collect This Measurement',
|
||||||
default=True,
|
default=True,
|
||||||
@@ -1049,7 +1049,7 @@ class FpProcessNodeInput(models.Model):
|
|||||||
string='Source Library Prompt',
|
string='Source Library Prompt',
|
||||||
ondelete='set null',
|
ondelete='set null',
|
||||||
help='Set when this row was snapshot-copied from a library template '
|
help='Set when this row was snapshot-copied from a library template '
|
||||||
'prompt. Powers "Reset to Library Defaults" — rows where this '
|
'prompt. Powers "Reset to Library Defaults" - rows where this '
|
||||||
'is False are treated as recipe-only custom prompts and survive '
|
'is False are treated as recipe-only custom prompts and survive '
|
||||||
'the reset.',
|
'the reset.',
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -19,14 +19,14 @@ class FpProcessType(models.Model):
|
|||||||
and linked here via parameter_ids.
|
and linked here via parameter_ids.
|
||||||
"""
|
"""
|
||||||
_name = 'fusion.plating.process.type'
|
_name = 'fusion.plating.process.type'
|
||||||
_description = 'Fusion Plating — Process Type'
|
_description = 'Fusion Plating - Process Type'
|
||||||
_order = 'sequence, name'
|
_order = 'sequence, name'
|
||||||
|
|
||||||
name = fields.Char(
|
name = fields.Char(
|
||||||
string='Process',
|
string='Process',
|
||||||
required=True,
|
required=True,
|
||||||
translate=True,
|
translate=True,
|
||||||
help='Display name (e.g. "Electroless Nickel — Mid Phosphorus").',
|
help='Display name (e.g. "Electroless Nickel - Mid Phosphorus").',
|
||||||
)
|
)
|
||||||
code = fields.Char(
|
code = fields.Char(
|
||||||
string='Code',
|
string='Code',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
# Part of the Fusion Plating product family.
|
# Part of the Fusion Plating product family.
|
||||||
#
|
#
|
||||||
# Phase 1 (Sub 11) — relocated from fusion_plating_bridge_mrp. The model
|
# Phase 1 (Sub 11) - relocated from fusion_plating_bridge_mrp. The model
|
||||||
# never had MRP fields; the bridge module was just its initial home.
|
# never had MRP fields; the bridge module was just its initial home.
|
||||||
|
|
||||||
from markupsafe import Markup
|
from markupsafe import Markup
|
||||||
@@ -12,7 +12,7 @@ from odoo import _, api, fields, models
|
|||||||
|
|
||||||
|
|
||||||
class FpOperatorProficiency(models.Model):
|
class FpOperatorProficiency(models.Model):
|
||||||
"""Operator proficiency tracker — counts successful step completions
|
"""Operator proficiency tracker - counts successful step completions
|
||||||
per (employee, role) pair and auto-promotes the employee once the
|
per (employee, role) pair and auto-promotes the employee once the
|
||||||
role's mastery threshold is crossed.
|
role's mastery threshold is crossed.
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ class FpOperatorProficiency(models.Model):
|
|||||||
in a form; their growing skill set just unlocks itself.
|
in a form; their growing skill set just unlocks itself.
|
||||||
"""
|
"""
|
||||||
_name = 'fp.operator.proficiency'
|
_name = 'fp.operator.proficiency'
|
||||||
_description = 'Fusion Plating — Operator Task Proficiency'
|
_description = 'Fusion Plating - Operator Task Proficiency'
|
||||||
_rec_name = 'display_name'
|
_rec_name = 'display_name'
|
||||||
_order = 'employee_id, role_id'
|
_order = 'employee_id, role_id'
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ class FpOperatorProficiency(models.Model):
|
|||||||
index=True,
|
index=True,
|
||||||
help='True once the role has been added to the operator\'s Shop '
|
help='True once the role has been added to the operator\'s Shop '
|
||||||
'Roles automatically. Stays True even if a manager removes '
|
'Roles automatically. Stays True even if a manager removes '
|
||||||
'the role afterwards — the count and promotion history are '
|
'the role afterwards - the count and promotion history are '
|
||||||
'preserved as a training record.',
|
'preserved as a training record.',
|
||||||
)
|
)
|
||||||
promoted_at = fields.Datetime(
|
promoted_at = fields.Datetime(
|
||||||
@@ -83,7 +83,7 @@ class FpOperatorProficiency(models.Model):
|
|||||||
def _compute_display_name(self):
|
def _compute_display_name(self):
|
||||||
for rec in self:
|
for rec in self:
|
||||||
rec.display_name = (
|
rec.display_name = (
|
||||||
f'{rec.employee_id.name or "?"} — {rec.role_id.name or "?"}'
|
f'{rec.employee_id.name or "?"} - {rec.role_id.name or "?"}'
|
||||||
)
|
)
|
||||||
|
|
||||||
@api.depends('completed_count', 'role_id.mastery_required')
|
@api.depends('completed_count', 'role_id.mastery_required')
|
||||||
@@ -99,7 +99,7 @@ class FpOperatorProficiency(models.Model):
|
|||||||
def _record_completion(self, employee, role):
|
def _record_completion(self, employee, role):
|
||||||
"""Increment the (employee, role) tally and promote if at threshold.
|
"""Increment the (employee, role) tally and promote if at threshold.
|
||||||
|
|
||||||
Idempotent for the (employee, role) pair — if no record exists,
|
Idempotent for the (employee, role) pair - if no record exists,
|
||||||
we create one. Always uses sudo() because the worker may not
|
we create one. Always uses sudo() because the worker may not
|
||||||
have write access to their own profile.
|
have write access to their own profile.
|
||||||
"""
|
"""
|
||||||
@@ -151,7 +151,7 @@ class FpOperatorProficiency(models.Model):
|
|||||||
})
|
})
|
||||||
employee.message_post(
|
employee.message_post(
|
||||||
body=Markup(_(
|
body=Markup(_(
|
||||||
'<b>%(name)s promoted</b> — qualified for '
|
'<b>%(name)s promoted</b> - qualified for '
|
||||||
'<b>%(role)s</b> after %(count)s successful '
|
'<b>%(role)s</b> after %(count)s successful '
|
||||||
'completions.'
|
'completions.'
|
||||||
)) % {
|
)) % {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class FpRack(models.Model):
|
|||||||
on parts.
|
on parts.
|
||||||
"""
|
"""
|
||||||
_name = 'fusion.plating.rack'
|
_name = 'fusion.plating.rack'
|
||||||
_description = 'Fusion Plating — Rack / Fixture'
|
_description = 'Fusion Plating - Rack / Fixture'
|
||||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||||
_order = 'facility_id, rack_type, name'
|
_order = 'facility_id, rack_type, name'
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ class FpRack(models.Model):
|
|||||||
def _compute_state(self):
|
def _compute_state(self):
|
||||||
for rec in self:
|
for rec in self:
|
||||||
if rec.state in ('stripping', 'retired'):
|
if rec.state in ('stripping', 'retired'):
|
||||||
continue # Manually set — don't override
|
continue # Manually set - don't override
|
||||||
if rec.strip_interval_mto and rec.mto_count >= rec.strip_interval_mto:
|
if rec.strip_interval_mto and rec.mto_count >= rec.strip_interval_mto:
|
||||||
rec.state = 'needs_strip'
|
rec.state = 'needs_strip'
|
||||||
elif rec.state != 'active':
|
elif rec.state != 'active':
|
||||||
@@ -116,7 +116,7 @@ class FpRack(models.Model):
|
|||||||
for rec in self:
|
for rec in self:
|
||||||
rec.mto_count = (rec.mto_count or 0.0) + delta
|
rec.mto_count = (rec.mto_count or 0.0) + delta
|
||||||
|
|
||||||
# ===== Sub 12b — racking lifecycle (orthogonal to wear-tracking state) =
|
# ===== Sub 12b - racking lifecycle (orthogonal to wear-tracking state) =
|
||||||
racking_state = fields.Selection(
|
racking_state = fields.Selection(
|
||||||
[
|
[
|
||||||
('empty', 'Empty'),
|
('empty', 'Empty'),
|
||||||
@@ -136,8 +136,8 @@ class FpRack(models.Model):
|
|||||||
string='Tags',
|
string='Tags',
|
||||||
)
|
)
|
||||||
capacity_count = fields.Integer(
|
capacity_count = fields.Integer(
|
||||||
string='Capacity (parts) — soft warn',
|
string='Capacity (parts) - soft warn',
|
||||||
help='Soft warning threshold — runtime informs operator when '
|
help='Soft warning threshold - runtime informs operator when '
|
||||||
'rack is loaded beyond this. Not enforced. Distinct from '
|
'rack is loaded beyond this. Not enforced. Distinct from '
|
||||||
'`capacity` field (planning capacity).',
|
'`capacity` field (planning capacity).',
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
# Part of the Fusion Plating product family.
|
# Part of the Fusion Plating product family.
|
||||||
#
|
#
|
||||||
# Multi-rack splitting + WO grouping at Racking — Phase 1 core models.
|
# Multi-rack splitting + WO grouping at Racking - Phase 1 core models.
|
||||||
# Spec: docs/superpowers/specs/2026-06-03-racking-multi-rack-wo-grouping-design.md
|
# Spec: docs/superpowers/specs/2026-06-03-racking-multi-rack-wo-grouping-design.md
|
||||||
# Plan: docs/superpowers/plans/2026-06-03-racking-multi-rack-phase1.md
|
# Plan: docs/superpowers/plans/2026-06-03-racking-multi-rack-phase1.md
|
||||||
#
|
#
|
||||||
@@ -60,7 +60,7 @@ class FpRackLoad(models.Model):
|
|||||||
return super().create(vals_list)
|
return super().create(vals_list)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Pure division math (no DB) — verifiable in isolation.
|
# Pure division math (no DB) - verifiable in isolation.
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@api.model
|
@api.model
|
||||||
def _fp_equal_split(self, total, n):
|
def _fp_equal_split(self, total, n):
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ from odoo import fields, models
|
|||||||
class FpRackTag(models.Model):
|
class FpRackTag(models.Model):
|
||||||
"""Operator-visible labels applied to physical racks.
|
"""Operator-visible labels applied to physical racks.
|
||||||
|
|
||||||
"Rush" / "Hold for QC" / "Customer-Amphenol" / "Damaged" — the
|
"Rush" / "Hold for QC" / "Customer-Amphenol" / "Damaged" - the
|
||||||
coloured tag chips that appear in the Move Rack dialog and on the
|
coloured tag chips that appear in the Move Rack dialog and on the
|
||||||
plant-overview rack rows. M2M; one rack can carry many tags.
|
plant-overview rack rows. M2M; one rack can carry many tags.
|
||||||
"""
|
"""
|
||||||
_name = 'fp.rack.tag'
|
_name = 'fp.rack.tag'
|
||||||
_description = 'Fusion Plating — Rack Tag'
|
_description = 'Fusion Plating - Rack Tag'
|
||||||
_order = 'sequence, name'
|
_order = 'sequence, name'
|
||||||
|
|
||||||
name = fields.Char(string='Tag', required=True, translate=True)
|
name = fields.Char(string='Tag', required=True, translate=True)
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ _NEW_ROLE_XMLID = {
|
|||||||
# Predicate is a callable taking a res.users record; returns bool.
|
# Predicate is a callable taking a res.users record; returns bool.
|
||||||
_FP_ROLE_MAPPING_RULES = [
|
_FP_ROLE_MAPPING_RULES = [
|
||||||
# cgp_designated_official MUST be first so admin/uid_1/uid_2 users who ALSO
|
# cgp_designated_official MUST be first so admin/uid_1/uid_2 users who ALSO
|
||||||
# hold the DO group still get the capability_delta marker — which is what
|
# hold the DO group still get the capability_delta marker - which is what
|
||||||
# triggers action_approve_and_run to set res.company.x_fc_cgp_designated_official_id.
|
# triggers action_approve_and_run to set res.company.x_fc_cgp_designated_official_id.
|
||||||
# If admin matched first, the DO field would never get populated for shops
|
# If admin matched first, the DO field would never get populated for shops
|
||||||
# where the admin is also the registered PSPC Designated Official.
|
# where the admin is also the registered PSPC Designated Official.
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class FpStepKind(models.Model):
|
|||||||
inputs that get seeded onto a step template when the kind is picked.
|
inputs that get seeded onto a step template when the kind is picked.
|
||||||
"""
|
"""
|
||||||
_name = 'fp.step.kind'
|
_name = 'fp.step.kind'
|
||||||
_description = 'Fusion Plating — Step Kind'
|
_description = 'Fusion Plating - Step Kind'
|
||||||
_order = 'sequence, name'
|
_order = 'sequence, name'
|
||||||
|
|
||||||
code = fields.Char(
|
code = fields.Char(
|
||||||
@@ -34,7 +34,7 @@ class FpStepKind(models.Model):
|
|||||||
string='Icon',
|
string='Icon',
|
||||||
default='fa-cog',
|
default='fa-cog',
|
||||||
)
|
)
|
||||||
# 2026-05-24 — Shop Floor live-step fix.
|
# 2026-05-24 - Shop Floor live-step fix.
|
||||||
# Each kind self-declares which plant-view column its steps land in.
|
# Each kind self-declares which plant-view column its steps land in.
|
||||||
# Replaces the hardcoded _STEP_KIND_TO_AREA dict (removed from
|
# Replaces the hardcoded _STEP_KIND_TO_AREA dict (removed from
|
||||||
# fusion_plating_jobs/models/fp_job_step.py). Pre-migrate
|
# fusion_plating_jobs/models/fp_job_step.py). Pre-migrate
|
||||||
@@ -56,7 +56,7 @@ class FpStepKind(models.Model):
|
|||||||
index=True,
|
index=True,
|
||||||
help='Determines which column on the Shop Floor plant kanban shows '
|
help='Determines which column on the Shop Floor plant kanban shows '
|
||||||
'cards whose active step uses this kind. Step kinds drive '
|
'cards whose active step uses this kind. Step kinds drive '
|
||||||
'routing automatically — picking a kind tells the system both '
|
'routing automatically - picking a kind tells the system both '
|
||||||
'what gates fire AND where the card lives.',
|
'what gates fire AND where the card lives.',
|
||||||
)
|
)
|
||||||
company_id = fields.Many2one(
|
company_id = fields.Many2one(
|
||||||
@@ -79,7 +79,7 @@ class FpStepKind(models.Model):
|
|||||||
]
|
]
|
||||||
|
|
||||||
# Curated FontAwesome 4 icon catalog for the visual icon picker.
|
# Curated FontAwesome 4 icon catalog for the visual icon picker.
|
||||||
# Bigger than the 24 historical fp.process.node list — covers
|
# Bigger than the 24 historical fp.process.node list - covers
|
||||||
# manufacturing, lab, quality, shipping, safety, time, status etc.
|
# manufacturing, lab, quality, shipping, safety, time, status etc.
|
||||||
# FA4 ships with Odoo (no extra deps). Key = CSS class, Value = label.
|
# FA4 ships with Odoo (no extra deps). Key = CSS class, Value = label.
|
||||||
_ICON_SELECTION = [
|
_ICON_SELECTION = [
|
||||||
@@ -258,7 +258,7 @@ class FpStepKind(models.Model):
|
|||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
return {
|
return {
|
||||||
'type': 'ir.actions.act_window',
|
'type': 'ir.actions.act_window',
|
||||||
'name': _('Step Templates — %s') % self.name,
|
'name': _('Step Templates - %s') % self.name,
|
||||||
'res_model': 'fp.step.template',
|
'res_model': 'fp.step.template',
|
||||||
'view_mode': 'list,form',
|
'view_mode': 'list,form',
|
||||||
'domain': [('kind_id', '=', self.id)],
|
'domain': [('kind_id', '=', self.id)],
|
||||||
@@ -271,10 +271,10 @@ class FpStepKindDefaultInput(models.Model):
|
|||||||
|
|
||||||
When a recipe author picks a kind on a step template and clicks
|
When a recipe author picks a kind on a step template and clicks
|
||||||
'Seed Defaults', these get copied into the template's input list
|
'Seed Defaults', these get copied into the template's input list
|
||||||
(idempotent — skips by name).
|
(idempotent - skips by name).
|
||||||
"""
|
"""
|
||||||
_name = 'fp.step.kind.default.input'
|
_name = 'fp.step.kind.default.input'
|
||||||
_description = 'Fusion Plating — Step Kind Default Input'
|
_description = 'Fusion Plating - Step Kind Default Input'
|
||||||
_order = 'sequence, name'
|
_order = 'sequence, name'
|
||||||
|
|
||||||
name = fields.Char(string='Name', required=True, translate=True)
|
name = fields.Char(string='Name', required=True, translate=True)
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ class FpStepTemplate(models.Model):
|
|||||||
"""Reusable step template for the Simple Recipe Editor.
|
"""Reusable step template for the Simple Recipe Editor.
|
||||||
|
|
||||||
A library entry the recipe author can drag into a recipe. Snapshot-
|
A library entry the recipe author can drag into a recipe. Snapshot-
|
||||||
copied at drag time — editing the template later does NOT change
|
copied at drag time - editing the template later does NOT change
|
||||||
recipes already built. Carries the same shape fields as the runtime
|
recipes already built. Carries the same shape fields as the runtime
|
||||||
`fusion.plating.process.node` so a snapshot copy is a 1:1 field
|
`fusion.plating.process.node` so a snapshot copy is a 1:1 field
|
||||||
transfer.
|
transfer.
|
||||||
"""
|
"""
|
||||||
_name = 'fp.step.template'
|
_name = 'fp.step.template'
|
||||||
_description = 'Fusion Plating — Step Library Template'
|
_description = 'Fusion Plating - Step Library Template'
|
||||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||||
_order = 'sequence, name'
|
_order = 'sequence, name'
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ class FpStepTemplate(models.Model):
|
|||||||
requires_predecessor_done = fields.Boolean(
|
requires_predecessor_done = fields.Boolean(
|
||||||
string='Require Predecessor Done (legacy)',
|
string='Require Predecessor Done (legacy)',
|
||||||
help='Legacy per-step opt-in for predecessor enforcement. Recipes '
|
help='Legacy per-step opt-in for predecessor enforcement. Recipes '
|
||||||
'now default to Enforce Sequential Order — use Parallel '
|
'now default to Enforce Sequential Order - use Parallel '
|
||||||
'Start instead when you want a step to run alongside others.',
|
'Start instead when you want a step to run alongside others.',
|
||||||
)
|
)
|
||||||
parallel_start = fields.Boolean(
|
parallel_start = fields.Boolean(
|
||||||
@@ -79,7 +79,7 @@ class FpStepTemplate(models.Model):
|
|||||||
'earlier-sequence steps are still in progress (e.g. '
|
'earlier-sequence steps are still in progress (e.g. '
|
||||||
'paperwork that runs alongside production).',
|
'paperwork that runs alongside production).',
|
||||||
)
|
)
|
||||||
# Sub 14 — triggers_workflow_state_id is declared via _inherit in
|
# Sub 14 - triggers_workflow_state_id is declared via _inherit in
|
||||||
# fusion_plating_jobs/models/fp_job.py. It can't live here because
|
# fusion_plating_jobs/models/fp_job.py. It can't live here because
|
||||||
# the target model (fp.job.workflow.state) is defined in jobs, and
|
# the target model (fp.job.workflow.state) is defined in jobs, and
|
||||||
# core can't depend on jobs (cyclic dependency).
|
# core can't depend on jobs (cyclic dependency).
|
||||||
@@ -88,8 +88,8 @@ class FpStepTemplate(models.Model):
|
|||||||
requires_transition_form = fields.Boolean(string='Requires Transition Form',
|
requires_transition_form = fields.Boolean(string='Requires Transition Form',
|
||||||
help='Opens the transition form before Mark Done (Sub 12b).')
|
help='Opens the transition form before Mark Done (Sub 12b).')
|
||||||
|
|
||||||
# Sub 14b — User-extensible Step Kinds (was Selection of 24).
|
# Sub 14b - User-extensible Step Kinds (was Selection of 24).
|
||||||
# 2026-05-20: required — same rationale as on fusion.plating.process.node
|
# 2026-05-20: required - same rationale as on fusion.plating.process.node
|
||||||
# (kind drives every downstream gate / milestone / routing decision).
|
# (kind drives every downstream gate / milestone / routing decision).
|
||||||
kind_id = fields.Many2one(
|
kind_id = fields.Many2one(
|
||||||
'fp.step.kind', string='Step Kind', ondelete='restrict',
|
'fp.step.kind', string='Step Kind', ondelete='restrict',
|
||||||
@@ -102,7 +102,7 @@ class FpStepTemplate(models.Model):
|
|||||||
'milestones / routing when authors instantiate the template. '
|
'milestones / routing when authors instantiate the template. '
|
||||||
'Pick "Other" only when the step has no special behaviour.',
|
'Pick "Other" only when the step has no special behaviour.',
|
||||||
)
|
)
|
||||||
# Back-compat shim — every legacy `tpl.default_kind == "cleaning"`
|
# Back-compat shim - every legacy `tpl.default_kind == "cleaning"`
|
||||||
# call site keeps working without a refactor. Stored=True so existing
|
# call site keeps working without a refactor. Stored=True so existing
|
||||||
# search domains [('default_kind', '=', 'cleaning')] still hit an
|
# search domains [('default_kind', '=', 'cleaning')] still hit an
|
||||||
# indexed column.
|
# indexed column.
|
||||||
@@ -148,7 +148,7 @@ class FpStepTemplate(models.Model):
|
|||||||
return super().write(vals)
|
return super().write(vals)
|
||||||
|
|
||||||
# ----- Sane defaults seeding ---------------------------------------------
|
# ----- Sane defaults seeding ---------------------------------------------
|
||||||
# Sub 14b — moved from a Python dict into seeded fp.step.kind records
|
# Sub 14b - moved from a Python dict into seeded fp.step.kind records
|
||||||
# so users can add new kinds + their default inputs through the
|
# so users can add new kinds + their default inputs through the
|
||||||
# standard UI. The dict below is preserved as a fallback only for
|
# standard UI. The dict below is preserved as a fallback only for
|
||||||
# codes that don't have a matching kind_id record (legacy data after
|
# codes that don't have a matching kind_id record (legacy data after
|
||||||
@@ -205,7 +205,7 @@ class FpStepTemplate(models.Model):
|
|||||||
{'name': 'Rinse Type', 'input_type': 'selection', 'sequence': 10,
|
{'name': 'Rinse Type', 'input_type': 'selection', 'sequence': 10,
|
||||||
'selection_options': 'cascade,spray,DI,city'},
|
'selection_options': 'cascade,spray,DI,city'},
|
||||||
{'name': 'Conductivity', 'input_type': 'number', 'sequence': 20,
|
{'name': 'Conductivity', 'input_type': 'number', 'sequence': 20,
|
||||||
'hint': 'µS/cm — required for DI rinses'},
|
'hint': 'µS/cm - required for DI rinses'},
|
||||||
{'name': 'Actual Time', 'input_type': 'time_seconds',
|
{'name': 'Actual Time', 'input_type': 'time_seconds',
|
||||||
'target_unit': 's', 'sequence': 30},
|
'target_unit': 's', 'sequence': 30},
|
||||||
],
|
],
|
||||||
@@ -232,7 +232,7 @@ class FpStepTemplate(models.Model):
|
|||||||
{'name': 'Bath Concentration', 'input_type': 'number', 'sequence': 50,
|
{'name': 'Bath Concentration', 'input_type': 'number', 'sequence': 50,
|
||||||
'hint': 'g/L'},
|
'hint': 'g/L'},
|
||||||
{'name': 'Current Density', 'input_type': 'number', 'sequence': 60,
|
{'name': 'Current Density', 'input_type': 'number', 'sequence': 60,
|
||||||
'hint': 'ASF — electroplate only'},
|
'hint': 'ASF - electroplate only'},
|
||||||
{'name': 'Plating Thickness', 'input_type': 'multi_point_thickness',
|
{'name': 'Plating Thickness', 'input_type': 'multi_point_thickness',
|
||||||
'target_unit': 'in', 'sequence': 70},
|
'target_unit': 'in', 'sequence': 70},
|
||||||
],
|
],
|
||||||
@@ -414,7 +414,7 @@ class FpStepTemplate(models.Model):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
# Mapping from fp.step.kind.default.input fields → fp.step.template.input
|
# Mapping from fp.step.kind.default.input fields → fp.step.template.input
|
||||||
# spec dict. Keep narrow — copy only the columns both models share.
|
# spec dict. Keep narrow - copy only the columns both models share.
|
||||||
_KIND_DEFAULT_INPUT_FIELDS = (
|
_KIND_DEFAULT_INPUT_FIELDS = (
|
||||||
'name', 'input_type', 'target_unit', 'required',
|
'name', 'input_type', 'target_unit', 'required',
|
||||||
'hint', 'selection_options', 'sequence',
|
'hint', 'selection_options', 'sequence',
|
||||||
@@ -422,12 +422,12 @@ class FpStepTemplate(models.Model):
|
|||||||
|
|
||||||
def action_seed_default_inputs(self):
|
def action_seed_default_inputs(self):
|
||||||
"""Seed input_template_ids from kind_id.default_input_ids.
|
"""Seed input_template_ids from kind_id.default_input_ids.
|
||||||
Idempotent — only adds inputs whose names don't already exist on
|
Idempotent - only adds inputs whose names don't already exist on
|
||||||
this template.
|
this template.
|
||||||
|
|
||||||
Falls back to the legacy DEFAULT_INPUTS_BY_KIND dict if the
|
Falls back to the legacy DEFAULT_INPUTS_BY_KIND dict if the
|
||||||
template has no kind_id but still carries a default_kind code
|
template has no kind_id but still carries a default_kind code
|
||||||
(defensive — shouldn't happen post-migration).
|
(defensive - shouldn't happen post-migration).
|
||||||
|
|
||||||
Public method (Odoo 19 requires non-underscore-prefixed names
|
Public method (Odoo 19 requires non-underscore-prefixed names
|
||||||
for methods called from a view button).
|
for methods called from a view button).
|
||||||
@@ -441,7 +441,7 @@ class FpStepTemplate(models.Model):
|
|||||||
spec = {f: d[f] for f in self._KIND_DEFAULT_INPUT_FIELDS}
|
spec = {f: d[f] for f in self._KIND_DEFAULT_INPUT_FIELDS}
|
||||||
specs.append(spec)
|
specs.append(spec)
|
||||||
elif tpl.default_kind:
|
elif tpl.default_kind:
|
||||||
# Legacy fallback — kind_id never got linked.
|
# Legacy fallback - kind_id never got linked.
|
||||||
specs = self.DEFAULT_INPUTS_BY_KIND.get(tpl.default_kind, [])
|
specs = self.DEFAULT_INPUTS_BY_KIND.get(tpl.default_kind, [])
|
||||||
for spec in specs:
|
for spec in specs:
|
||||||
if spec['name'] in existing_names:
|
if spec['name'] in existing_names:
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class FpStepTemplateInput(models.Model):
|
|||||||
step.
|
step.
|
||||||
"""
|
"""
|
||||||
_name = 'fp.step.template.input'
|
_name = 'fp.step.template.input'
|
||||||
_description = 'Fusion Plating — Step Template Input'
|
_description = 'Fusion Plating - Step Template Input'
|
||||||
_order = 'sequence, name'
|
_order = 'sequence, name'
|
||||||
|
|
||||||
name = fields.Char(string='Name', required=True, translate=True)
|
name = fields.Char(string='Name', required=True, translate=True)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class FpStepTemplateTransitionInput(models.Model):
|
|||||||
into a recipe. Sub 12b uses these to render the Move Parts dialog.
|
into a recipe. Sub 12b uses these to render the Move Parts dialog.
|
||||||
"""
|
"""
|
||||||
_name = 'fp.step.template.transition.input'
|
_name = 'fp.step.template.transition.input'
|
||||||
_description = 'Fusion Plating — Step Template Transition Input'
|
_description = 'Fusion Plating - Step Template Transition Input'
|
||||||
_order = 'sequence, name'
|
_order = 'sequence, name'
|
||||||
|
|
||||||
name = fields.Char(string='Name', required=True, translate=True)
|
name = fields.Char(string='Name', required=True, translate=True)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class FpTank(models.Model):
|
|||||||
shop-floor station.
|
shop-floor station.
|
||||||
"""
|
"""
|
||||||
_name = 'fusion.plating.tank'
|
_name = 'fusion.plating.tank'
|
||||||
_description = 'Fusion Plating — Tank'
|
_description = 'Fusion Plating - Tank'
|
||||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||||
_order = 'facility_id, section_id, sequence, code'
|
_order = 'facility_id, section_id, sequence, code'
|
||||||
|
|
||||||
@@ -228,7 +228,7 @@ class FpTank(models.Model):
|
|||||||
@api.onchange('current_bath_process_id')
|
@api.onchange('current_bath_process_id')
|
||||||
def _onchange_seed_current_process(self):
|
def _onchange_seed_current_process(self):
|
||||||
"""Pre-fill the editable Current Process from the active bath when
|
"""Pre-fill the editable Current Process from the active bath when
|
||||||
the operator hasn't already set one — keeps the field useful out of
|
the operator hasn't already set one - keeps the field useful out of
|
||||||
the box while still allowing manual override."""
|
the box while still allowing manual override."""
|
||||||
for rec in self:
|
for rec in self:
|
||||||
if not rec.current_process_id and rec.current_bath_process_id:
|
if not rec.current_process_id and rec.current_bath_process_id:
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class FpTankComposition(models.Model):
|
|||||||
percentages. Changes to ingredients are also chatter-tracked.
|
percentages. Changes to ingredients are also chatter-tracked.
|
||||||
"""
|
"""
|
||||||
_name = 'fusion.plating.tank.composition'
|
_name = 'fusion.plating.tank.composition'
|
||||||
_description = 'Fusion Plating — Tank Composition'
|
_description = 'Fusion Plating - Tank Composition'
|
||||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||||
_order = 'tank_id, sequence, name'
|
_order = 'tank_id, sequence, name'
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ class FpTankComposition(models.Model):
|
|||||||
code = fields.Char(
|
code = fields.Char(
|
||||||
string='Code',
|
string='Code',
|
||||||
tracking=True,
|
tracking=True,
|
||||||
help='Short identifier — "A", "B", "C".',
|
help='Short identifier - "A", "B", "C".',
|
||||||
)
|
)
|
||||||
sequence = fields.Integer(
|
sequence = fields.Integer(
|
||||||
string='Sequence',
|
string='Sequence',
|
||||||
@@ -114,7 +114,7 @@ class FpTankCompositionIngredient(models.Model):
|
|||||||
composition; total % roll-up lives on the parent composition.
|
composition; total % roll-up lives on the parent composition.
|
||||||
"""
|
"""
|
||||||
_name = 'fusion.plating.tank.composition.ingredient'
|
_name = 'fusion.plating.tank.composition.ingredient'
|
||||||
_description = 'Fusion Plating — Tank Composition Ingredient'
|
_description = 'Fusion Plating - Tank Composition Ingredient'
|
||||||
_order = 'composition_id, sequence, id'
|
_order = 'composition_id, sequence, id'
|
||||||
|
|
||||||
composition_id = fields.Many2one(
|
composition_id = fields.Many2one(
|
||||||
@@ -166,7 +166,7 @@ class FpTankCompositionIngredient(models.Model):
|
|||||||
records = super().create(vals_list)
|
records = super().create(vals_list)
|
||||||
for rec in records:
|
for rec in records:
|
||||||
rec.composition_id.message_post(body=_(
|
rec.composition_id.message_post(body=_(
|
||||||
'Ingredient added: %(name)s — %(pct)s %(uom)s'
|
'Ingredient added: %(name)s - %(pct)s %(uom)s'
|
||||||
) % {
|
) % {
|
||||||
'name': rec.name,
|
'name': rec.name,
|
||||||
'pct': rec.percentage,
|
'pct': rec.percentage,
|
||||||
@@ -198,7 +198,7 @@ class FpTankCompositionIngredient(models.Model):
|
|||||||
changed.append(_('unit: %s → %s') % (before.get('uom'), rec.uom))
|
changed.append(_('unit: %s → %s') % (before.get('uom'), rec.uom))
|
||||||
if changed:
|
if changed:
|
||||||
rec.composition_id.message_post(body=_(
|
rec.composition_id.message_post(body=_(
|
||||||
'Ingredient %(name)s updated — %(changes)s'
|
'Ingredient %(name)s updated - %(changes)s'
|
||||||
) % {
|
) % {
|
||||||
'name': rec.name,
|
'name': rec.name,
|
||||||
'changes': '; '.join(changed),
|
'changes': '; '.join(changed),
|
||||||
@@ -208,7 +208,7 @@ class FpTankCompositionIngredient(models.Model):
|
|||||||
def unlink(self):
|
def unlink(self):
|
||||||
for rec in self:
|
for rec in self:
|
||||||
rec.composition_id.message_post(body=_(
|
rec.composition_id.message_post(body=_(
|
||||||
'Ingredient removed: %(name)s — %(pct)s'
|
'Ingredient removed: %(name)s - %(pct)s'
|
||||||
) % {
|
) % {
|
||||||
'name': rec.name,
|
'name': rec.name,
|
||||||
'pct': rec.percentage,
|
'pct': rec.percentage,
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ class FpTankSection(models.Model):
|
|||||||
"Specialty Line").
|
"Specialty Line").
|
||||||
|
|
||||||
Sections give the shop a familiar way to slice the tank list: every shop
|
Sections give the shop a familiar way to slice the tank list: every shop
|
||||||
organises its tanks differently — by metal, by chemistry family, by
|
organises its tanks differently - by metal, by chemistry family, by
|
||||||
physical aisle, or by customer programme — and a fixed taxonomy never
|
physical aisle, or by customer programme - and a fixed taxonomy never
|
||||||
fits. Sections are free-form, renameable, and per-facility.
|
fits. Sections are free-form, renameable, and per-facility.
|
||||||
"""
|
"""
|
||||||
_name = 'fusion.plating.tank.section'
|
_name = 'fusion.plating.tank.section'
|
||||||
_description = 'Fusion Plating — Tank Section'
|
_description = 'Fusion Plating - Tank Section'
|
||||||
_order = 'sequence, name'
|
_order = 'sequence, name'
|
||||||
|
|
||||||
name = fields.Char(
|
name = fields.Char(
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"""Timezone helpers for Fusion Plating.
|
"""Timezone helpers for Fusion Plating.
|
||||||
|
|
||||||
The Postgres database stores all datetimes naive-UTC. Anything that is
|
The Postgres database stores all datetimes naive-UTC. Anything that is
|
||||||
shown to a user — dashboards, PDFs, emails, OWL frontends — must be
|
shown to a user - dashboards, PDFs, emails, OWL frontends - must be
|
||||||
converted to a human's local timezone first.
|
converted to a human's local timezone first.
|
||||||
|
|
||||||
Resolution order for "what timezone does this user see":
|
Resolution order for "what timezone does this user see":
|
||||||
@@ -140,7 +140,7 @@ def detect_default_tz(env=None):
|
|||||||
if partner_tz:
|
if partner_tz:
|
||||||
return partner_tz
|
return partner_tz
|
||||||
|
|
||||||
# Server-side detection — works on most Linux hosts.
|
# Server-side detection - works on most Linux hosts.
|
||||||
try:
|
try:
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
local = datetime.now().astimezone()
|
local = datetime.now().astimezone()
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ from odoo import fields, models
|
|||||||
class FpWorkCenter(models.Model):
|
class FpWorkCenter(models.Model):
|
||||||
"""A physical production line inside a facility.
|
"""A physical production line inside a facility.
|
||||||
|
|
||||||
Examples: "Line 1 — EN", "Anodize Line", "Prep Bay", "Bake Station",
|
Examples: "Line 1 - EN", "Anodize Line", "Prep Bay", "Bake Station",
|
||||||
"Inspection Booth", "Shipping Dock". Production lines group tanks
|
"Inspection Booth", "Shipping Dock". Production lines group tanks
|
||||||
and provide daily-capacity scheduling. This is the SHOP-LAYOUT
|
and provide daily-capacity scheduling. This is the SHOP-LAYOUT
|
||||||
entity — distinct from `fp.work.centre` which is the per-job-step
|
entity - distinct from `fp.work.centre` which is the per-job-step
|
||||||
routing station with cost-per-hour rollup.
|
routing station with cost-per-hour rollup.
|
||||||
"""
|
"""
|
||||||
_name = 'fusion.plating.work.center'
|
_name = 'fusion.plating.work.center'
|
||||||
_description = 'Fusion Plating — Production Line'
|
_description = 'Fusion Plating - Production Line'
|
||||||
_order = 'facility_id, sequence, name'
|
_order = 'facility_id, sequence, name'
|
||||||
|
|
||||||
name = fields.Char(
|
name = fields.Char(
|
||||||
@@ -58,7 +58,7 @@ class FpWorkCenter(models.Model):
|
|||||||
)
|
)
|
||||||
capacity_per_day = fields.Float(
|
capacity_per_day = fields.Float(
|
||||||
string='Capacity / Day',
|
string='Capacity / Day',
|
||||||
help='Theoretical throughput (parts, jobs, or square metres per day) — unit depends on shop.',
|
help='Theoretical throughput (parts, jobs, or square metres per day) - unit depends on shop.',
|
||||||
)
|
)
|
||||||
|
|
||||||
_sql_constraints = [
|
_sql_constraints = [
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
# Copyright 2026 Nexa Systems Inc.
|
# Copyright 2026 Nexa Systems Inc.
|
||||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
#
|
#
|
||||||
# fp.work.centre — native plating work-centre model.
|
# fp.work.centre - native plating work-centre model.
|
||||||
#
|
#
|
||||||
# Replaces mrp.workcenter for the plating flow. Plating work centres
|
# Replaces mrp.workcenter for the plating flow. Plating work centres
|
||||||
# are domain-specific (a tank line, a bake oven, a rack station — not
|
# are domain-specific (a tank line, a bake oven, a rack station - not
|
||||||
# assembly cells). Each centre has a 'kind' that drives release-ready
|
# assembly cells). Each centre has a 'kind' that drives release-ready
|
||||||
# validation on fp.job.step (e.g. wet_line -> bath+tank required).
|
# validation on fp.job.step (e.g. wet_line -> bath+tank required).
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ from odoo import fields, models
|
|||||||
|
|
||||||
|
|
||||||
class FpWorkCentre(models.Model):
|
class FpWorkCentre(models.Model):
|
||||||
"""Routing station for a job step — replaces mrp.workcenter for
|
"""Routing station for a job step - replaces mrp.workcenter for
|
||||||
plating after the Sub 11 MRP cutout.
|
plating after the Sub 11 MRP cutout.
|
||||||
|
|
||||||
Each routing station has a `kind` (wet_line / bake / mask / rack /
|
Each routing station has a `kind` (wet_line / bake / mask / rack /
|
||||||
@@ -62,7 +62,7 @@ class FpWorkCentre(models.Model):
|
|||||||
],
|
],
|
||||||
string='Floor Column',
|
string='Floor Column',
|
||||||
help='Which Shop Floor column this work centre belongs to. '
|
help='Which Shop Floor column this work centre belongs to. '
|
||||||
'Drives the plant-view kanban grouping — any job whose '
|
'Drives the plant-view kanban grouping - any job whose '
|
||||||
'active step uses this work centre routes into this column. '
|
'active step uses this work centre routes into this column. '
|
||||||
'See docs/superpowers/specs/2026-05-23-shopfloor-plant-view-'
|
'See docs/superpowers/specs/2026-05-23-shopfloor-plant-view-'
|
||||||
'design.md §4.2 for the mapping rules.',
|
'design.md §4.2 for the mapping rules.',
|
||||||
@@ -78,21 +78,21 @@ class FpWorkCentre(models.Model):
|
|||||||
)
|
)
|
||||||
default_bath_id = fields.Many2one('fusion.plating.bath')
|
default_bath_id = fields.Many2one('fusion.plating.bath')
|
||||||
default_tank_id = fields.Many2one('fusion.plating.tank')
|
default_tank_id = fields.Many2one('fusion.plating.tank')
|
||||||
# NOTE: `default_oven_id` from the spec/plan is omitted here — the
|
# NOTE: `default_oven_id` from the spec/plan is omitted here - the
|
||||||
# `fusion.plating.bake.oven` model lives in fusion_plating_shopfloor,
|
# `fusion.plating.bake.oven` model lives in fusion_plating_shopfloor,
|
||||||
# which the core module cannot depend on. The bridge module that
|
# which the core module cannot depend on. The bridge module that
|
||||||
# introduces fp.job/fp.job.step (Task 1.x) can re-introduce this
|
# introduces fp.job/fp.job.step (Task 1.x) can re-introduce this
|
||||||
# field via _inherit if/when the bake-oven coupling is needed.
|
# field via _inherit if/when the bake-oven coupling is needed.
|
||||||
active = fields.Boolean(default=True)
|
active = fields.Boolean(default=True)
|
||||||
|
|
||||||
# Phase 4 tablet redesign — Manager At-Risk heatmap inputs.
|
# Phase 4 tablet redesign - Manager At-Risk heatmap inputs.
|
||||||
# Non-stored (recomputed on every read by /fp/manager/at_risk; the
|
# Non-stored (recomputed on every read by /fp/manager/at_risk; the
|
||||||
# endpoint caches the payload for 60s anyway so the cost is bounded).
|
# endpoint caches the payload for 60s anyway so the cost is bounded).
|
||||||
bottleneck_score = fields.Float(
|
bottleneck_score = fields.Float(
|
||||||
compute='_compute_bottleneck',
|
compute='_compute_bottleneck',
|
||||||
string='Bottleneck Score',
|
string='Bottleneck Score',
|
||||||
help='active_step_count * avg_wait_minutes (rolling 7-day). '
|
help='active_step_count * avg_wait_minutes (rolling 7-day). '
|
||||||
'Drives the Manager At-Risk heatmap — work centres with '
|
'Drives the Manager At-Risk heatmap - work centres with '
|
||||||
'high score have queue + wait pressure.',
|
'high score have queue + wait pressure.',
|
||||||
)
|
)
|
||||||
avg_wait_minutes = fields.Float(
|
avg_wait_minutes = fields.Float(
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
# Part of the Fusion Plating product family.
|
# Part of the Fusion Plating product family.
|
||||||
#
|
#
|
||||||
# Phase 1 (Sub 11) — relocated from fusion_plating_bridge_mrp. The model
|
# Phase 1 (Sub 11) - relocated from fusion_plating_bridge_mrp. The model
|
||||||
# never had MRP fields; the bridge module was just its initial home.
|
# never had MRP fields; the bridge module was just its initial home.
|
||||||
|
|
||||||
from odoo import api, fields, models
|
from odoo import api, fields, models
|
||||||
@@ -19,11 +19,11 @@ class FpWorkRole(models.Model):
|
|||||||
role each.
|
role each.
|
||||||
- Cross-trained workers: multiple roles per worker.
|
- Cross-trained workers: multiple roles per worker.
|
||||||
|
|
||||||
The model is intentionally flat — no hierarchy, no workflow. Roles
|
The model is intentionally flat - no hierarchy, no workflow. Roles
|
||||||
are just tags that the step auto-assignment compares.
|
are just tags that the step auto-assignment compares.
|
||||||
"""
|
"""
|
||||||
_name = 'fp.work.role'
|
_name = 'fp.work.role'
|
||||||
_description = 'Fusion Plating — Shop Work Role'
|
_description = 'Fusion Plating - Shop Work Role'
|
||||||
_order = 'sequence, code'
|
_order = 'sequence, code'
|
||||||
|
|
||||||
name = fields.Char(string='Role Name', required=True, translate=True)
|
name = fields.Char(string='Role Name', required=True, translate=True)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class HrEmployee(models.Model):
|
|||||||
of them. A small shop where the owner wears every hat just tags
|
of them. A small shop where the owner wears every hat just tags
|
||||||
themselves with every role.
|
themselves with every role.
|
||||||
|
|
||||||
Lead hands are a separate per-role list — they don't have to be
|
Lead hands are a separate per-role list - they don't have to be
|
||||||
primary owners of those roles, but they're authorised to step in
|
primary owners of those roles, but they're authorised to step in
|
||||||
when the regular owner is absent or behind. The Manager Desk
|
when the regular owner is absent or behind. The Manager Desk
|
||||||
promotes lead hands above other workers in its dropdown for any
|
promotes lead hands above other workers in its dropdown for any
|
||||||
@@ -53,9 +53,9 @@ class HrEmployee(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Attendance helpers — used by the Manager Desk to show who is
|
# Attendance helpers - used by the Manager Desk to show who is
|
||||||
# currently clocked in. Works with vanilla hr_attendance or the
|
# currently clocked in. Works with vanilla hr_attendance or the
|
||||||
# full fusion_clock module — both store an open record (no
|
# full fusion_clock module - both store an open record (no
|
||||||
# check_out) for as long as the employee is on shift.
|
# check_out) for as long as the employee is on shift.
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
x_fc_is_clocked_in = fields.Boolean(
|
x_fc_is_clocked_in = fields.Boolean(
|
||||||
@@ -70,7 +70,7 @@ class HrEmployee(models.Model):
|
|||||||
"""Compute attendance status from hr.attendance.
|
"""Compute attendance status from hr.attendance.
|
||||||
|
|
||||||
Batched so the manager dashboard doesn't issue one query per
|
Batched so the manager dashboard doesn't issue one query per
|
||||||
employee — important when the shop has dozens of operators.
|
employee - important when the shop has dozens of operators.
|
||||||
"""
|
"""
|
||||||
if not self:
|
if not self:
|
||||||
return
|
return
|
||||||
@@ -96,7 +96,7 @@ class HrEmployee(models.Model):
|
|||||||
1. Odoo 19 normalises ``('=', True)`` into
|
1. Odoo 19 normalises ``('=', True)`` into
|
||||||
``('in', OrderedSet([True]))`` before invoking the search
|
``('in', OrderedSet([True]))`` before invoking the search
|
||||||
method. The previous code only handled ``=`` / ``!=`` and
|
method. The previous code only handled ``=`` / ``!=`` and
|
||||||
fell through to ``return []`` for ``in`` / ``not in`` —
|
fell through to ``return []`` for ``in`` / ``not in`` -
|
||||||
which Odoo treats as "no constraint" and matches every
|
which Odoo treats as "no constraint" and matches every
|
||||||
row.
|
row.
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ class HrEmployee(models.Model):
|
|||||||
on the cached open-attendance employee ids. Variable signature
|
on the cached open-attendance employee ids. Variable signature
|
||||||
future-proofs against Odoo's compute-field API shifting again.
|
future-proofs against Odoo's compute-field API shifting again.
|
||||||
"""
|
"""
|
||||||
# Variable signature — Odoo 19 may pass (records, op, val).
|
# Variable signature - Odoo 19 may pass (records, op, val).
|
||||||
if len(args) == 3:
|
if len(args) == 3:
|
||||||
_records, operator, value = args
|
_records, operator, value = args
|
||||||
elif len(args) == 2:
|
elif len(args) == 2:
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ class ResCompany(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# =====================================================================
|
# =====================================================================
|
||||||
# Sub 12a — Default recipe editor
|
# Sub 12a - Default recipe editor
|
||||||
# =====================================================================
|
# =====================================================================
|
||||||
x_fc_default_recipe_editor = fields.Selection(
|
x_fc_default_recipe_editor = fields.Selection(
|
||||||
[('tree', 'Tree Editor'), ('simple', 'Simple Editor')],
|
[('tree', 'Tree Editor'), ('simple', 'Simple Editor')],
|
||||||
@@ -175,7 +175,7 @@ class ResCompany(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# =====================================================================
|
# =====================================================================
|
||||||
# Sub 12c+ — Default Certification Statement
|
# Sub 12c+ - Default Certification Statement
|
||||||
# =====================================================================
|
# =====================================================================
|
||||||
x_fc_default_cert_statement = fields.Text(
|
x_fc_default_cert_statement = fields.Text(
|
||||||
string='Default Cert Statement',
|
string='Default Cert Statement',
|
||||||
@@ -187,7 +187,7 @@ class ResCompany(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# =====================================================================
|
# =====================================================================
|
||||||
# Phase F — Plating Designated Officials
|
# Phase F - Plating Designated Officials
|
||||||
# =====================================================================
|
# =====================================================================
|
||||||
# These are SPECIFIC NAMED PEOPLE registered with regulatory bodies.
|
# These are SPECIFIC NAMED PEOPLE registered with regulatory bodies.
|
||||||
# Stored as Many2one to res.users so the link survives renames.
|
# Stored as Many2one to res.users so the link survives renames.
|
||||||
|
|||||||
@@ -56,14 +56,14 @@ class ResConfigSettings(models.TransientModel):
|
|||||||
readonly=False, string='Area Unit',
|
readonly=False, string='Area Unit',
|
||||||
)
|
)
|
||||||
|
|
||||||
# ----- Sub 12a — recipe editor default ------------------------------
|
# ----- Sub 12a - recipe editor default ------------------------------
|
||||||
x_fc_default_recipe_editor = fields.Selection(
|
x_fc_default_recipe_editor = fields.Selection(
|
||||||
related='company_id.x_fc_default_recipe_editor',
|
related='company_id.x_fc_default_recipe_editor',
|
||||||
readonly=False,
|
readonly=False,
|
||||||
string='Default Recipe Editor',
|
string='Default Recipe Editor',
|
||||||
)
|
)
|
||||||
|
|
||||||
# ----- Phase 1 — Plating landing page default -----------------------
|
# ----- Phase 1 - Plating landing page default -----------------------
|
||||||
# Comodel MUST match res.company.x_fc_default_landing_action_id, which
|
# Comodel MUST match res.company.x_fc_default_landing_action_id, which
|
||||||
# was widened to ir.actions.actions in the post-deploy fixes so the
|
# was widened to ir.actions.actions in the post-deploy fixes so the
|
||||||
# picker accepts both window AND client actions (Manager Desk, Plant
|
# picker accepts both window AND client actions (Manager Desk, Plant
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ _FP_PLATING_ROLE_TO_GROUP_XMLID = {
|
|||||||
'owner': 'fusion_plating.group_fp_owner',
|
'owner': 'fusion_plating.group_fp_owner',
|
||||||
}
|
}
|
||||||
|
|
||||||
# Highest precedence first — first match wins
|
# Highest precedence first - first match wins
|
||||||
_FP_ROLE_PRECEDENCE = (
|
_FP_ROLE_PRECEDENCE = (
|
||||||
'owner', 'quality_manager', 'manager', 'sales_manager',
|
'owner', 'quality_manager', 'manager', 'sales_manager',
|
||||||
'shop_manager', 'sales_rep', 'technician',
|
'shop_manager', 'sales_rep', 'technician',
|
||||||
@@ -35,7 +35,7 @@ class ResUsers(models.Model):
|
|||||||
|
|
||||||
# Allow non-admin users to write their OWN plating-related fields
|
# Allow non-admin users to write their OWN plating-related fields
|
||||||
# from the standard User Preferences dialog. SELF_WRITEABLE_FIELDS is
|
# from the standard User Preferences dialog. SELF_WRITEABLE_FIELDS is
|
||||||
# a @property in Odoo 19 (not a class attribute) — must override via
|
# a @property in Odoo 19 (not a class attribute) - must override via
|
||||||
# @property + super(). See CLAUDE.md rule 13k.
|
# @property + super(). See CLAUDE.md rule 13k.
|
||||||
@property
|
@property
|
||||||
def SELF_WRITEABLE_FIELDS(self):
|
def SELF_WRITEABLE_FIELDS(self):
|
||||||
@@ -99,7 +99,7 @@ class ResUsers(models.Model):
|
|||||||
role_to_group[role] = grp
|
role_to_group[role] = grp
|
||||||
all_role_ids.append(grp.id)
|
all_role_ids.append(grp.id)
|
||||||
|
|
||||||
# I4 fix — capture old roles BEFORE the cache mutates by reading
|
# I4 fix - capture old roles BEFORE the cache mutates by reading
|
||||||
# the stored x_fc_plating_role column directly from PostgreSQL.
|
# the stored x_fc_plating_role column directly from PostgreSQL.
|
||||||
# `user._origin.x_fc_plating_role` returns the IN-CACHE new value
|
# `user._origin.x_fc_plating_role` returns the IN-CACHE new value
|
||||||
# (the assignment that triggered the inverse), not the prior DB
|
# (the assignment that triggered the inverse), not the prior DB
|
||||||
@@ -115,7 +115,7 @@ class ResUsers(models.Model):
|
|||||||
old_role = old_role_by_id.get(user.id) or 'unset'
|
old_role = old_role_by_id.get(user.id) or 'unset'
|
||||||
new_role = user.x_fc_plating_role
|
new_role = user.x_fc_plating_role
|
||||||
if old_role == new_role:
|
if old_role == new_role:
|
||||||
# No actual change — skip both the writes and the audit so
|
# No actual change - skip both the writes and the audit so
|
||||||
# we don't spam chatter with "X -> X" rows.
|
# we don't spam chatter with "X -> X" rows.
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ then create an SO for ABC Manufacturing and confirm it so the
|
|||||||
operator-facing job is ready to run.
|
operator-facing job is ready to run.
|
||||||
|
|
||||||
After running, the user navigates to:
|
After running, the user navigates to:
|
||||||
- Recipe form (Process Recipes menu) — verify instructions present
|
- Recipe form (Process Recipes menu) - verify instructions present
|
||||||
- Simple Recipe Editor — verify per-step Instructions + Measurements
|
- Simple Recipe Editor - verify per-step Instructions + Measurements
|
||||||
- Sale Orders — verify the new SO with line referencing the recipe
|
- Sale Orders - verify the new SO with line referencing the recipe
|
||||||
- Plating Jobs — verify job created with all steps
|
- Plating Jobs - verify job created with all steps
|
||||||
- Click Mark Done on any step → verify operator wizard shows
|
- Click Mark Done on any step → verify operator wizard shows
|
||||||
instructions + measurement prompts
|
instructions + measurement prompts
|
||||||
"""
|
"""
|
||||||
@@ -25,7 +25,7 @@ print('\n========== Build Hard Anodize Type III Recipe ==========\n')
|
|||||||
# Clean up any prior run of this script (prior recipes + their variants + jobs + SOs)
|
# Clean up any prior run of this script (prior recipes + their variants + jobs + SOs)
|
||||||
prior_recipes = Node.search([
|
prior_recipes = Node.search([
|
||||||
'|', ('name', '=', 'Hard Anodize Type III + Dye + Seal'),
|
'|', ('name', '=', 'Hard Anodize Type III + Dye + Seal'),
|
||||||
('name', 'ilike', 'Hard Anodize Type III + Dye + Seal — '),
|
('name', 'ilike', 'Hard Anodize Type III + Dye + Seal - '),
|
||||||
])
|
])
|
||||||
if prior_recipes:
|
if prior_recipes:
|
||||||
print('Cleaning up %d prior recipe(s)...' % len(prior_recipes))
|
print('Cleaning up %d prior recipe(s)...' % len(prior_recipes))
|
||||||
@@ -53,7 +53,7 @@ recipe = Node.create({
|
|||||||
'is_template': True,
|
'is_template': True,
|
||||||
'description': '''<p><strong>Hard Anodize Type III per MIL-A-8625F</strong></p>
|
'description': '''<p><strong>Hard Anodize Type III per MIL-A-8625F</strong></p>
|
||||||
<p>Aluminum substrate. Black sulfuric dye. Hot nickel acetate seal.</p>
|
<p>Aluminum substrate. Black sulfuric dye. Hot nickel acetate seal.</p>
|
||||||
<p>Tolerances: 0.0015"–0.0020" coating thickness, 60kV breakdown
|
<p>Tolerances: 0.0015"-0.0020" coating thickness, 60kV breakdown
|
||||||
voltage. Hardness ≥350HV300.</p>''',
|
voltage. Hardness ≥350HV300.</p>''',
|
||||||
})
|
})
|
||||||
print('Created recipe:', recipe.id, recipe.name)
|
print('Created recipe:', recipe.id, recipe.name)
|
||||||
@@ -100,14 +100,14 @@ STEPS = [
|
|||||||
{
|
{
|
||||||
'name': '5. Alkaline Clean (Tank A-1)',
|
'name': '5. Alkaline Clean (Tank A-1)',
|
||||||
'kind': 'cleaning',
|
'kind': 'cleaning',
|
||||||
'description': '''<p><strong>Soak clean — Aluminum Etch Cleaner.</strong></p>
|
'description': '''<p><strong>Soak clean - Aluminum Etch Cleaner.</strong></p>
|
||||||
<ul><li>Tank: A-1, Bath: ALKCLEAN-1</li>
|
<ul><li>Tank: A-1, Bath: ALKCLEAN-1</li>
|
||||||
<li>Time: 4–6 minutes</li>
|
<li>Time: 4-6 minutes</li>
|
||||||
<li>Temperature: 140–160°F</li>
|
<li>Temperature: 140-160°F</li>
|
||||||
<li>Confirm titration done within last 24 hours</li></ul>''',
|
<li>Confirm titration done within last 24 hours</li></ul>''',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': '6. Rinse — Cascade DI (Tank A-2)',
|
'name': '6. Rinse - Cascade DI (Tank A-2)',
|
||||||
'kind': 'rinse',
|
'kind': 'rinse',
|
||||||
'description': '''<p><strong>Triple cascade DI rinse.</strong></p>
|
'description': '''<p><strong>Triple cascade DI rinse.</strong></p>
|
||||||
<ul><li>Tank A-2 (DI rinse, conductivity < 50 µS/cm)</li>
|
<ul><li>Tank A-2 (DI rinse, conductivity < 50 µS/cm)</li>
|
||||||
@@ -117,47 +117,47 @@ STEPS = [
|
|||||||
{
|
{
|
||||||
'name': '7. Etch (Tank A-3)',
|
'name': '7. Etch (Tank A-3)',
|
||||||
'kind': 'etch',
|
'kind': 'etch',
|
||||||
'description': '''<p><strong>Caustic etch — sodium hydroxide bath.</strong></p>
|
'description': '''<p><strong>Caustic etch - sodium hydroxide bath.</strong></p>
|
||||||
<ul><li>Tank: A-3, Bath: ETCH-1</li>
|
<ul><li>Tank: A-3, Bath: ETCH-1</li>
|
||||||
<li>Time: 30–90 seconds (per drawing — heavy etch removes 0.0005"/side)</li>
|
<li>Time: 30-90 seconds (per drawing - heavy etch removes 0.0005"/side)</li>
|
||||||
<li>Temperature: 130–150°F</li>
|
<li>Temperature: 130-150°F</li>
|
||||||
<li>Concentration: 4–6 oz/gal NaOH</li>
|
<li>Concentration: 4-6 oz/gal NaOH</li>
|
||||||
<li>HE-risk parts (high-strength) require post-bake — flag accordingly</li></ul>''',
|
<li>HE-risk parts (high-strength) require post-bake - flag accordingly</li></ul>''',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': '8. Rinse — Cascade DI (Tank A-4)',
|
'name': '8. Rinse - Cascade DI (Tank A-4)',
|
||||||
'kind': 'rinse',
|
'kind': 'rinse',
|
||||||
'description': '<p>Triple cascade DI rinse — Tank A-4. 30 sec agitate.</p>',
|
'description': '<p>Triple cascade DI rinse - Tank A-4. 30 sec agitate.</p>',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': '9. Desmut / Deoxidize (Tank A-5)',
|
'name': '9. Desmut / Deoxidize (Tank A-5)',
|
||||||
'kind': 'etch',
|
'kind': 'etch',
|
||||||
'description': '''<p><strong>Acid desmut to remove black smut from etch.</strong></p>
|
'description': '''<p><strong>Acid desmut to remove black smut from etch.</strong></p>
|
||||||
<ul><li>Tank: A-5, Bath: DEOX-1 (HNO3-based)</li>
|
<ul><li>Tank: A-5, Bath: DEOX-1 (HNO3-based)</li>
|
||||||
<li>Time: 30–60 seconds</li>
|
<li>Time: 30-60 seconds</li>
|
||||||
<li>Temperature: ambient</li>
|
<li>Temperature: ambient</li>
|
||||||
<li>Surface should be water-break-free after this step</li></ul>''',
|
<li>Surface should be water-break-free after this step</li></ul>''',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': '10. Rinse — Cascade DI (Tank A-6)',
|
'name': '10. Rinse - Cascade DI (Tank A-6)',
|
||||||
'kind': 'rinse',
|
'kind': 'rinse',
|
||||||
'description': '<p>Final pre-anodize rinse — Tank A-6. Conductivity must be < 50 µS/cm.</p>',
|
'description': '<p>Final pre-anodize rinse - Tank A-6. Conductivity must be < 50 µS/cm.</p>',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': '11. Hard Anodize Type III (Tank A-9)',
|
'name': '11. Hard Anodize Type III (Tank A-9)',
|
||||||
'kind': 'plate',
|
'kind': 'plate',
|
||||||
'description': '''<p><strong>HARD ANODIZE — the primary process step.</strong></p>
|
'description': '''<p><strong>HARD ANODIZE - the primary process step.</strong></p>
|
||||||
<ul><li>Tank: A-9, Bath: HARDANO-1 (15% sulfuric acid)</li>
|
<ul><li>Tank: A-9, Bath: HARDANO-1 (15% sulfuric acid)</li>
|
||||||
<li>Temperature: 28–32°F (chilled bath — confirm chiller is running)</li>
|
<li>Temperature: 28-32°F (chilled bath - confirm chiller is running)</li>
|
||||||
<li>Current density: 24–36 ASF</li>
|
<li>Current density: 24-36 ASF</li>
|
||||||
<li>Voltage ramp: 0–80V over first 5 minutes</li>
|
<li>Voltage ramp: 0-80V over first 5 minutes</li>
|
||||||
<li>Time at voltage: 60 minutes (gives ~0.002" coating)</li>
|
<li>Time at voltage: 60 minutes (gives ~0.002" coating)</li>
|
||||||
<li>Record amperage every 15 minutes</li>
|
<li>Record amperage every 15 minutes</li>
|
||||||
<li>Check thickness midway with Fischerscope on witness coupon</li>
|
<li>Check thickness midway with Fischerscope on witness coupon</li>
|
||||||
<li>If color reading off, halt and call supervisor</li></ul>''',
|
<li>If color reading off, halt and call supervisor</li></ul>''',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': '12. Rinse — Cold (Tank A-12)',
|
'name': '12. Rinse - Cold (Tank A-12)',
|
||||||
'kind': 'rinse',
|
'kind': 'rinse',
|
||||||
'description': '<p>Cold cascade rinse to remove sulfuric residue. Tank A-12.</p>',
|
'description': '<p>Cold cascade rinse to remove sulfuric residue. Tank A-12.</p>',
|
||||||
},
|
},
|
||||||
@@ -166,24 +166,24 @@ STEPS = [
|
|||||||
'kind': 'plate',
|
'kind': 'plate',
|
||||||
'description': '''<p><strong>Sulfo Black BL dye absorption.</strong></p>
|
'description': '''<p><strong>Sulfo Black BL dye absorption.</strong></p>
|
||||||
<ul><li>Tank: A-14, Bath: DYE-BL-1</li>
|
<ul><li>Tank: A-14, Bath: DYE-BL-1</li>
|
||||||
<li>Temperature: 130–150°F</li>
|
<li>Temperature: 130-150°F</li>
|
||||||
<li>Time: 12–18 minutes</li>
|
<li>Time: 12-18 minutes</li>
|
||||||
<li>Maintain pH 5.0–6.0</li>
|
<li>Maintain pH 5.0-6.0</li>
|
||||||
<li>Visually verify uniform black with no streaks before sealing</li></ul>''',
|
<li>Visually verify uniform black with no streaks before sealing</li></ul>''',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': '14. Rinse — Warm (Tank A-15)',
|
'name': '14. Rinse - Warm (Tank A-15)',
|
||||||
'kind': 'rinse',
|
'kind': 'rinse',
|
||||||
'description': '<p>Warm rinse before sealing. Tank A-15. ~110°F.</p>',
|
'description': '<p>Warm rinse before sealing. Tank A-15. ~110°F.</p>',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': '15. Hot Nickel Acetate Seal (Tank A-16)',
|
'name': '15. Hot Nickel Acetate Seal (Tank A-16)',
|
||||||
'kind': 'bake',
|
'kind': 'bake',
|
||||||
'description': '''<p><strong>Nickel acetate seal — locks in dye, improves corrosion resistance.</strong></p>
|
'description': '''<p><strong>Nickel acetate seal - locks in dye, improves corrosion resistance.</strong></p>
|
||||||
<ul><li>Tank: A-16, Bath: SEAL-NA-1</li>
|
<ul><li>Tank: A-16, Bath: SEAL-NA-1</li>
|
||||||
<li>Temperature: 195–205°F</li>
|
<li>Temperature: 195-205°F</li>
|
||||||
<li>Time: 18–22 minutes</li>
|
<li>Time: 18-22 minutes</li>
|
||||||
<li>pH: 5.5–6.0</li>
|
<li>pH: 5.5-6.0</li>
|
||||||
<li>Attach AMS-2759 chart-recorder file as photo before unloading</li>
|
<li>Attach AMS-2759 chart-recorder file as photo before unloading</li>
|
||||||
<li>Quality of seal verified post-process by dye absorption test</li></ul>''',
|
<li>Quality of seal verified post-process by dye absorption test</li></ul>''',
|
||||||
},
|
},
|
||||||
@@ -195,10 +195,10 @@ STEPS = [
|
|||||||
{
|
{
|
||||||
'name': '17. Drying (Hot Air Knife)',
|
'name': '17. Drying (Hot Air Knife)',
|
||||||
'kind': 'dry',
|
'kind': 'dry',
|
||||||
'description': '''<p><strong>Hot-air knife dry — leave parts on rack.</strong></p>
|
'description': '''<p><strong>Hot-air knife dry - leave parts on rack.</strong></p>
|
||||||
<ul><li>Hot air knife @ 180°F</li>
|
<ul><li>Hot air knife @ 180°F</li>
|
||||||
<li>Time: 5 minutes minimum</li>
|
<li>Time: 5 minutes minimum</li>
|
||||||
<li>Verify parts fully dry before unracking — water spotting is a defect</li></ul>''',
|
<li>Verify parts fully dry before unracking - water spotting is a defect</li></ul>''',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': '18. De-Racking',
|
'name': '18. De-Racking',
|
||||||
@@ -248,7 +248,7 @@ STEPS = [
|
|||||||
{
|
{
|
||||||
'name': '23. Shipping',
|
'name': '23. Shipping',
|
||||||
'kind': 'ship',
|
'kind': 'ship',
|
||||||
'description': '''<p><strong>Outbound — confirm carrier and BoL.</strong></p>
|
'description': '''<p><strong>Outbound - confirm carrier and BoL.</strong></p>
|
||||||
<ul><li>Carrier per SO (UPS / FedEx / customer pickup)</li>
|
<ul><li>Carrier per SO (UPS / FedEx / customer pickup)</li>
|
||||||
<li>Print BoL, attach to package</li>
|
<li>Print BoL, attach to package</li>
|
||||||
<li>Photograph sealed shipment for proof-of-shipment</li>
|
<li>Photograph sealed shipment for proof-of-shipment</li>
|
||||||
@@ -315,7 +315,7 @@ sol = env['sale.order.line'].create({
|
|||||||
})
|
})
|
||||||
print('Created SO:', so.name, 'line', sol.id)
|
print('Created SO:', so.name, 'line', sol.id)
|
||||||
|
|
||||||
# Confirm — triggers fp.job creation
|
# Confirm - triggers fp.job creation
|
||||||
so.action_confirm()
|
so.action_confirm()
|
||||||
print('Confirmed SO. State =', so.state)
|
print('Confirmed SO. State =', so.state)
|
||||||
env.cr.commit()
|
env.cr.commit()
|
||||||
@@ -337,7 +337,7 @@ for js in job_steps:
|
|||||||
prompts = len(rn.input_ids.filtered(lambda i: i.collect))
|
prompts = len(rn.input_ids.filtered(lambda i: i.collect))
|
||||||
instructions_visible += int(ins)
|
instructions_visible += int(ins)
|
||||||
prompts_visible += prompts
|
prompts_visible += prompts
|
||||||
print(' [%2d] %s — instructions=%s prompts=%d kind=%s' % (
|
print(' [%2d] %s - instructions=%s prompts=%d kind=%s' % (
|
||||||
js.sequence, js.name, '✓' if ins else '✗', prompts, rn.default_kind or '-'
|
js.sequence, js.name, '✓' if ins else '✗', prompts, rn.default_kind or '-'
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""End-to-end battle test — full Anodize job for ABC Manufacturing.
|
"""End-to-end battle test - full Anodize job for ABC Manufacturing.
|
||||||
|
|
||||||
Phases:
|
Phases:
|
||||||
A. Auto-infer default_kind on existing Anodize recipe steps + seed prompts
|
A. Auto-infer default_kind on existing Anodize recipe steps + seed prompts
|
||||||
@@ -25,7 +25,7 @@ NodeInput = env['fusion.plating.process.node.input']
|
|||||||
Template = env['fp.step.template']
|
Template = env['fp.step.template']
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Phase A — auto-seed prompts on the Anodize recipe
|
# Phase A - auto-seed prompts on the Anodize recipe
|
||||||
# ============================================================
|
# ============================================================
|
||||||
print('\n========== PHASE A: seed prompts on Anodize recipe ==========\n')
|
print('\n========== PHASE A: seed prompts on Anodize recipe ==========\n')
|
||||||
|
|
||||||
@@ -122,7 +122,7 @@ for s in all_steps[:5]:
|
|||||||
))
|
))
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Phase B — create SO line + confirm
|
# Phase B - create SO line + confirm
|
||||||
# ============================================================
|
# ============================================================
|
||||||
print('\n========== PHASE B: create SO + confirm ==========\n')
|
print('\n========== PHASE B: create SO + confirm ==========\n')
|
||||||
|
|
||||||
@@ -179,7 +179,7 @@ for js in job_steps[:5]:
|
|||||||
))
|
))
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Phase C — record measurements covering every input type
|
# Phase C - record measurements covering every input type
|
||||||
# ============================================================
|
# ============================================================
|
||||||
print('\n========== PHASE C: record measurements ==========\n')
|
print('\n========== PHASE C: record measurements ==========\n')
|
||||||
|
|
||||||
@@ -296,7 +296,7 @@ else:
|
|||||||
find('PASS', 'No value-creation errors')
|
find('PASS', 'No value-creation errors')
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Phase D — verify CoC chronological can render
|
# Phase D - verify CoC chronological can render
|
||||||
# ============================================================
|
# ============================================================
|
||||||
print('\n========== PHASE D: render CoC report ==========\n')
|
print('\n========== PHASE D: render CoC report ==========\n')
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""End-to-end battle test v2 — operates on existing job 1234.
|
"""End-to-end battle test v2 - operates on existing job 1234.
|
||||||
|
|
||||||
1. Seed prompts onto variant subtree (recipe id=1775)
|
1. Seed prompts onto variant subtree (recipe id=1775)
|
||||||
2. Re-link recipe nodes to job steps (recipe_node_id should point at variant nodes)
|
2. Re-link recipe nodes to job steps (recipe_node_id should point at variant nodes)
|
||||||
@@ -21,7 +21,7 @@ NodeInput = env['fusion.plating.process.node.input']
|
|||||||
Template = env['fp.step.template']
|
Template = env['fp.step.template']
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Phase A — seed prompts on the per-part variant (id=1775)
|
# Phase A - seed prompts on the per-part variant (id=1775)
|
||||||
# ============================================================
|
# ============================================================
|
||||||
print('\n========== PHASE A: seed prompts on variant 1775 ==========\n')
|
print('\n========== PHASE A: seed prompts on variant 1775 ==========\n')
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ KIND_KEYWORDS = [
|
|||||||
('adhesion_test', ['adhesion']),
|
('adhesion_test', ['adhesion']),
|
||||||
('salt_spray', ['salt spray', 'corrosion test']),
|
('salt_spray', ['salt spray', 'corrosion test']),
|
||||||
('replenishment', ['replenish', 'top-up']),
|
('replenishment', ['replenish', 'top-up']),
|
||||||
# Surface prep — fall back to cleaning since shops record similar fields
|
# Surface prep - fall back to cleaning since shops record similar fields
|
||||||
('cleaning', ['blast']),
|
('cleaning', ['blast']),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -111,7 +111,7 @@ find('INFO', 'Seeded %d prompts onto variant subtree' % seeded)
|
|||||||
env.cr.commit()
|
env.cr.commit()
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Phase B — verify job 1234 sees the prompts
|
# Phase B - verify job 1234 sees the prompts
|
||||||
# ============================================================
|
# ============================================================
|
||||||
print('\n========== PHASE B: job sees prompts ==========\n')
|
print('\n========== PHASE B: job sees prompts ==========\n')
|
||||||
|
|
||||||
@@ -126,12 +126,12 @@ find('INFO', 'Total prompts visible to job %d: %d across %d steps' % (
|
|||||||
job.id, prompt_total, len(job_steps)
|
job.id, prompt_total, len(job_steps)
|
||||||
))
|
))
|
||||||
if prompt_total == 0:
|
if prompt_total == 0:
|
||||||
find('FAIL', 'No prompts visible — variant cloning still broken')
|
find('FAIL', 'No prompts visible - variant cloning still broken')
|
||||||
else:
|
else:
|
||||||
find('PASS', 'Prompts now visible at runtime')
|
find('PASS', 'Prompts now visible at runtime')
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Phase C — record measurements covering every input type
|
# Phase C - record measurements covering every input type
|
||||||
# ============================================================
|
# ============================================================
|
||||||
print('\n========== PHASE C: record measurements ==========\n')
|
print('\n========== PHASE C: record measurements ==========\n')
|
||||||
|
|
||||||
@@ -255,7 +255,7 @@ if some_step:
|
|||||||
types_exercised.add('bath_chemistry_panel')
|
types_exercised.add('bath_chemistry_panel')
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Phase D — render CoC chronological body via QWeb
|
# Phase D - render CoC chronological body via QWeb
|
||||||
# ============================================================
|
# ============================================================
|
||||||
print('\n========== PHASE D: render CoC body ==========\n')
|
print('\n========== PHASE D: render CoC body ==========\n')
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""End-to-end battle test — Phase 1: reconnaissance."""
|
"""End-to-end battle test - Phase 1: reconnaissance."""
|
||||||
|
|
||||||
# Find ABC Manufacturing
|
# Find ABC Manufacturing
|
||||||
abc = env['res.partner'].search([('name', 'ilike', 'ABC Manufactor')], limit=1)
|
abc = env['res.partner'].search([('name', 'ilike', 'ABC Manufactor')], limit=1)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""Phase 2 recon — examine Anodize recipe tree + SO→job flow."""
|
"""Phase 2 recon - examine Anodize recipe tree + SO→job flow."""
|
||||||
|
|
||||||
Node = env['fusion.plating.process.node']
|
Node = env['fusion.plating.process.node']
|
||||||
|
|
||||||
@@ -31,14 +31,14 @@ def walk(n, depth=0):
|
|||||||
|
|
||||||
walk(anodize)
|
walk(anodize)
|
||||||
|
|
||||||
# Sample part for ABC — make sure we have one
|
# Sample part for ABC - make sure we have one
|
||||||
abc = env['res.partner'].browse(943)
|
abc = env['res.partner'].browse(943)
|
||||||
parts = env['fp.part.catalog'].search([('partner_id', '=', abc.id)], limit=3)
|
parts = env['fp.part.catalog'].search([('partner_id', '=', abc.id)], limit=3)
|
||||||
print('\n=== ABC parts ===')
|
print('\n=== ABC parts ===')
|
||||||
for p in parts:
|
for p in parts:
|
||||||
print(' id=%d num=%s rev=%s' % (p.id, p.part_number, p.revision or ''))
|
print(' id=%d num=%s rev=%s' % (p.id, p.part_number, p.revision or ''))
|
||||||
if not parts:
|
if not parts:
|
||||||
print(' (none — will need to create one)')
|
print(' (none - will need to create one)')
|
||||||
|
|
||||||
# How does an SO line reference a recipe? Look at sale.order.line fields
|
# How does an SO line reference a recipe? Look at sale.order.line fields
|
||||||
sol = env['sale.order.line']
|
sol = env['sale.order.line']
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""Phase 3 recon — SO→job→step trigger pipeline."""
|
"""Phase 3 recon - SO→job→step trigger pipeline."""
|
||||||
|
|
||||||
# Find recent confirmed SO with x_fc_process_variant_id set
|
# Find recent confirmed SO with x_fc_process_variant_id set
|
||||||
sol = env['sale.order.line'].search([
|
sol = env['sale.order.line'].search([
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""Phase 4 recon — diagnose recipe variant cloning."""
|
"""Phase 4 recon - diagnose recipe variant cloning."""
|
||||||
|
|
||||||
Node = env['fusion.plating.process.node']
|
Node = env['fusion.plating.process.node']
|
||||||
n = Node.browse(1777)
|
n = Node.browse(1777)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""Battle test — Step Library audit expansion (Sub 12d).
|
"""Battle test - Step Library audit expansion (Sub 12d).
|
||||||
|
|
||||||
Run via odoo-shell on entech:
|
Run via odoo-shell on entech:
|
||||||
|
|
||||||
@@ -132,7 +132,7 @@ check(12, 'wizard filter excludes collect=False',
|
|||||||
ni_off not in visible and ni_on in visible,
|
ni_off not in visible and ni_on in visible,
|
||||||
'%d/%d visible' % (len(visible), len(node.input_ids)))
|
'%d/%d visible' % (len(visible), len(node.input_ids)))
|
||||||
|
|
||||||
# 13. Master switch path — when False, filter returns empty
|
# 13. Master switch path - when False, filter returns empty
|
||||||
node.collect_measurements = False
|
node.collect_measurements = False
|
||||||
empty_path = (not node.collect_measurements)
|
empty_path = (not node.collect_measurements)
|
||||||
check(13, 'master collect_measurements=False short-circuits',
|
check(13, 'master collect_measurements=False short-circuits',
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
License OPL-1 (Odoo Proprietary License v1.0)
|
License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
Part of the Fusion Plating product family.
|
Part of the Fusion Plating product family.
|
||||||
|
|
||||||
2026-05-24 — Hide non-essential app menus from Technicians.
|
2026-05-24 - Hide non-essential app menus from Technicians.
|
||||||
|
|
||||||
Per user request: technicians should see ONLY the apps they actually
|
Per user request: technicians should see ONLY the apps they actually
|
||||||
need on the tablet — Discuss, To-do, Plating, AI, Maintenance, Time
|
need on the tablet - Discuss, To-do, Plating, AI, Maintenance, Time
|
||||||
Off. Every other top-level app menu is restricted to a new "office
|
Off. Every other top-level app menu is restricted to a new "office
|
||||||
user" group implied by every fp role ABOVE technician.
|
user" group implied by every fp role ABOVE technician.
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
<menuitem id="other_module.X" groups="..."/> overrides require the
|
<menuitem id="other_module.X" groups="..."/> overrides require the
|
||||||
other module in `depends`, which would lock us into hard
|
other module in `depends`, which would lock us into hard
|
||||||
dependencies on calendar/sale/hr/etc. The hook uses
|
dependencies on calendar/sale/hr/etc. The hook uses
|
||||||
env.ref(..., raise_if_not_found=False) — modules that aren't
|
env.ref(..., raise_if_not_found=False) - modules that aren't
|
||||||
installed are silently skipped.
|
installed are silently skipped.
|
||||||
|
|
||||||
Why a separate office_user group instead of !technician?
|
Why a separate office_user group instead of !technician?
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
<data>
|
<data>
|
||||||
|
|
||||||
<!-- ============================================================ -->
|
<!-- ============================================================ -->
|
||||||
<!-- New marker group: "Office User" — implied by every non- -->
|
<!-- New marker group: "Office User" - implied by every non- -->
|
||||||
<!-- technician fp role. -->
|
<!-- technician fp role. -->
|
||||||
<!-- ============================================================ -->
|
<!-- ============================================================ -->
|
||||||
<record id="group_fp_office_user" model="res.groups">
|
<record id="group_fp_office_user" model="res.groups">
|
||||||
|
|||||||
@@ -73,10 +73,10 @@
|
|||||||
</record>
|
</record>
|
||||||
|
|
||||||
<!-- ================================================================== -->
|
<!-- ================================================================== -->
|
||||||
<!-- RECORD RULE — Multi-company isolation on facilities -->
|
<!-- RECORD RULE - Multi-company isolation on facilities -->
|
||||||
<!-- ================================================================== -->
|
<!-- ================================================================== -->
|
||||||
<record id="fp_facility_company_rule" model="ir.rule">
|
<record id="fp_facility_company_rule" model="ir.rule">
|
||||||
<field name="name">Fusion Plating: Facility — multi-company</field>
|
<field name="name">Fusion Plating: Facility - multi-company</field>
|
||||||
<field name="model_id" ref="model_fusion_plating_facility"/>
|
<field name="model_id" ref="model_fusion_plating_facility"/>
|
||||||
<field name="global" eval="True"/>
|
<field name="global" eval="True"/>
|
||||||
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
|
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/** @odoo-module **/
|
/** @odoo-module **/
|
||||||
// Sub 14b — visual FontAwesome icon picker for fp.step.kind.icon and any
|
// Sub 14b - visual FontAwesome icon picker for fp.step.kind.icon and any
|
||||||
// other Selection field whose values are FA classes (e.g. 'fa-flask').
|
// other Selection field whose values are FA classes (e.g. 'fa-flask').
|
||||||
//
|
//
|
||||||
// Always-visible compact grid with a Search box. Glyph-only tiles
|
// Always-visible compact grid with a Search box. Glyph-only tiles
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/** @odoo-module **/
|
/** @odoo-module **/
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Fusion Plating — Recipe Tree Editor (OWL backend client action)
|
// Fusion Plating - Recipe Tree Editor (OWL backend client action)
|
||||||
// Copyright 2026 Nexa Systems Inc.
|
// Copyright 2026 Nexa Systems Inc.
|
||||||
// License OPL-1 (Odoo Proprietary License v1.0)
|
// License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
//
|
//
|
||||||
@@ -102,7 +102,7 @@ export class RecipeTreeEditor extends Component {
|
|||||||
this.state = useState({
|
this.state = useState({
|
||||||
recipe: null,
|
recipe: null,
|
||||||
tree: null,
|
tree: null,
|
||||||
workflowStates: [], // Sub 14 — populated by loadTree
|
workflowStates: [], // Sub 14 - populated by loadTree
|
||||||
loading: false,
|
loading: false,
|
||||||
saving: false,
|
saving: false,
|
||||||
selectedNodeId: null,
|
selectedNodeId: null,
|
||||||
@@ -158,13 +158,13 @@ export class RecipeTreeEditor extends Component {
|
|||||||
if (result && result.ok) {
|
if (result && result.ok) {
|
||||||
this.state.recipe = result.recipe;
|
this.state.recipe = result.recipe;
|
||||||
this.state.tree = result.tree;
|
this.state.tree = result.tree;
|
||||||
// Sub 14 — workflow states for the per-step trigger
|
// Sub 14 - workflow states for the per-step trigger
|
||||||
// dropdown in the properties panel.
|
// dropdown in the properties panel.
|
||||||
this.state.workflowStates = result.workflow_states || [];
|
this.state.workflowStates = result.workflow_states || [];
|
||||||
// Auto-expand every node on first load AND auto-expand
|
// Auto-expand every node on first load AND auto-expand
|
||||||
// any node we haven't seen before (e.g. freshly imported
|
// any node we haven't seen before (e.g. freshly imported
|
||||||
// nodes after a "Import from recipe" run). Nodes the
|
// nodes after a "Import from recipe" run). Nodes the
|
||||||
// user has explicitly collapsed stay collapsed — we only
|
// user has explicitly collapsed stay collapsed - we only
|
||||||
// touch nodes that are missing from expandedNodes.
|
// touch nodes that are missing from expandedNodes.
|
||||||
if (result.tree) {
|
if (result.tree) {
|
||||||
const applyDefault = (n) => {
|
const applyDefault = (n) => {
|
||||||
@@ -225,7 +225,7 @@ export class RecipeTreeEditor extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
collapseAll() {
|
collapseAll() {
|
||||||
// Collapse everything EXCEPT the recipe root itself — otherwise
|
// Collapse everything EXCEPT the recipe root itself - otherwise
|
||||||
// the canvas goes blank and the user has to click the root open
|
// the canvas goes blank and the user has to click the root open
|
||||||
// again to see anything.
|
// again to see anything.
|
||||||
const walk = (n, isRoot) => {
|
const walk = (n, isRoot) => {
|
||||||
@@ -272,10 +272,10 @@ export class RecipeTreeEditor extends Component {
|
|||||||
customer_visible: node.customer_visible,
|
customer_visible: node.customer_visible,
|
||||||
is_manual: node.is_manual,
|
is_manual: node.is_manual,
|
||||||
requires_signoff: node.requires_signoff,
|
requires_signoff: node.requires_signoff,
|
||||||
// Sub 13 — sequential enforcement
|
// Sub 13 - sequential enforcement
|
||||||
enforce_sequential: !!node.enforce_sequential,
|
enforce_sequential: !!node.enforce_sequential,
|
||||||
parallel_start: !!node.parallel_start,
|
parallel_start: !!node.parallel_start,
|
||||||
// Sub 14 — workflow milestone trigger
|
// Sub 14 - workflow milestone trigger
|
||||||
triggers_workflow_state_id: node.triggers_workflow_state_id || false,
|
triggers_workflow_state_id: node.triggers_workflow_state_id || false,
|
||||||
};
|
};
|
||||||
const result = await rpc("/fp/recipe/node/write", {
|
const result = await rpc("/fp/recipe/node/write", {
|
||||||
@@ -425,7 +425,7 @@ export class RecipeTreeEditor extends Component {
|
|||||||
const targetParentId = parentNode ? parentNode.id : null;
|
const targetParentId = parentNode ? parentNode.id : null;
|
||||||
|
|
||||||
if (dragged.parentId === targetParentId) {
|
if (dragged.parentId === targetParentId) {
|
||||||
// Reorder within same parent — swap positions
|
// Reorder within same parent - swap positions
|
||||||
const siblings = parentNode
|
const siblings = parentNode
|
||||||
? (parentNode.children || [])
|
? (parentNode.children || [])
|
||||||
: [this.state.tree];
|
: [this.state.tree];
|
||||||
@@ -556,7 +556,7 @@ export class RecipeTreeEditor extends Component {
|
|||||||
|
|
||||||
onBackToList() {
|
onBackToList() {
|
||||||
// Pop this editor off the action stack and restore the
|
// Pop this editor off the action stack and restore the
|
||||||
// previous controller — which is whatever opened the editor:
|
// previous controller - which is whatever opened the editor:
|
||||||
// * Recipes list → recipe form → editor ⇒ back to recipe form
|
// * Recipes list → recipe form → editor ⇒ back to recipe form
|
||||||
// * Part form → composer → editor ⇒ back to composer
|
// * Part form → composer → editor ⇒ back to composer
|
||||||
// * Part form → editor (direct link) ⇒ back to part form
|
// * Part form → editor (direct link) ⇒ back to part form
|
||||||
@@ -568,7 +568,7 @@ export class RecipeTreeEditor extends Component {
|
|||||||
this.action.restore();
|
this.action.restore();
|
||||||
return;
|
return;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// No prior controller — fall through to a sensible default.
|
// No prior controller - fall through to a sensible default.
|
||||||
}
|
}
|
||||||
// Fallback: when opened directly via URL with no prior crumb,
|
// Fallback: when opened directly via URL with no prior crumb,
|
||||||
// pick the most contextual landing page we have.
|
// pick the most contextual landing page we have.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/** @odoo-module */
|
/** @odoo-module */
|
||||||
/*
|
/*
|
||||||
* Sub 12a — Simple Recipe Editor (OWL client action).
|
* Sub 12a - Simple Recipe Editor (OWL client action).
|
||||||
*
|
*
|
||||||
* Flat drag-drop alternative to the tree editor. Library on the right,
|
* Flat drag-drop alternative to the tree editor. Library on the right,
|
||||||
* Selected (ordered steps) on the left. Drag from library → snapshot-
|
* Selected (ordered steps) on the left. Drag from library → snapshot-
|
||||||
@@ -38,25 +38,25 @@ export class FpSimpleRecipeEditor extends Component {
|
|||||||
dragOverIndex: null, // 0..N (insertion index)
|
dragOverIndex: null, // 0..N (insertion index)
|
||||||
dragPreviewLabel: "", // shown next to the indicator line
|
dragPreviewLabel: "", // shown next to the indicator line
|
||||||
dragPreviewIcon: "fa-cog",
|
dragPreviewIcon: "fa-cog",
|
||||||
// Inline edit panel — id of the step currently being edited
|
// Inline edit panel - id of the step currently being edited
|
||||||
// (null = no panel open). Mirrors live values so the textarea
|
// (null = no panel open). Mirrors live values so the textarea
|
||||||
// stays controlled without RPC roundtrip on every keystroke.
|
// stays controlled without RPC roundtrip on every keystroke.
|
||||||
editingStepId: null,
|
editingStepId: null,
|
||||||
editName: "",
|
editName: "",
|
||||||
editInstructions: "",
|
editInstructions: "",
|
||||||
// Sub 14 + Sub 13 — additional per-step settings that the
|
// Sub 14 + Sub 13 - additional per-step settings that the
|
||||||
// user can change inline without delete + re-add.
|
// user can change inline without delete + re-add.
|
||||||
editDefaultKind: "",
|
editDefaultKind: "",
|
||||||
editTriggersWorkflowStateId: false,
|
editTriggersWorkflowStateId: false,
|
||||||
editParallelStart: false,
|
editParallelStart: false,
|
||||||
editRequiresSignoff: false,
|
editRequiresSignoff: false,
|
||||||
// Inline library form — open when authoring or editing a
|
// Inline library form - open when authoring or editing a
|
||||||
// library template directly from the right pane. null =
|
// library template directly from the right pane. null =
|
||||||
// closed; otherwise carries the template payload.
|
// closed; otherwise carries the template payload.
|
||||||
libraryEditor: null,
|
libraryEditor: null,
|
||||||
libraryEditorBusy: false,
|
libraryEditorBusy: false,
|
||||||
tankSearchResults: [],
|
tankSearchResults: [],
|
||||||
// Sub 14 — workflow-state catalog cache for the inline
|
// Sub 14 - workflow-state catalog cache for the inline
|
||||||
// library form's "Triggers Workflow State" dropdown. Lazy-
|
// library form's "Triggers Workflow State" dropdown. Lazy-
|
||||||
// loaded the first time the user opens the library editor.
|
// loaded the first time the user opens the library editor.
|
||||||
workflowStates: [],
|
workflowStates: [],
|
||||||
@@ -87,7 +87,7 @@ export class FpSimpleRecipeEditor extends Component {
|
|||||||
|
|
||||||
async loadAll() {
|
async loadAll() {
|
||||||
// Preserve scroll position across the re-render. .o_fp_simple_editor
|
// Preserve scroll position across the re-render. .o_fp_simple_editor
|
||||||
// is the overflow:auto scroll container — when `state.steps` is
|
// is the overflow:auto scroll container - when `state.steps` is
|
||||||
// replaced with a fresh array, OWL tears down the t-foreach and
|
// replaced with a fresh array, OWL tears down the t-foreach and
|
||||||
// rebuilds every row, which snaps scrollTop back to 0. Operators
|
// rebuilds every row, which snaps scrollTop back to 0. Operators
|
||||||
// hate this: they save a step half-way down the recipe and the
|
// hate this: they save a step half-way down the recipe and the
|
||||||
@@ -153,7 +153,7 @@ export class FpSimpleRecipeEditor extends Component {
|
|||||||
}
|
}
|
||||||
// dragOverIndex is the insertion point in the ORIGINAL list. Once
|
// dragOverIndex is the insertion point in the ORIGINAL list. Once
|
||||||
// we splice the dragged item out, every position to the right of
|
// we splice the dragged item out, every position to the right of
|
||||||
// oldIndex shifts left by one — so an insertion at newIndex when
|
// oldIndex shifts left by one - so an insertion at newIndex when
|
||||||
// newIndex > oldIndex must be decremented. Without this, dropping
|
// newIndex > oldIndex must be decremented. Without this, dropping
|
||||||
// right after itself moves the row one slot down instead of
|
// right after itself moves the row one slot down instead of
|
||||||
// staying put.
|
// staying put.
|
||||||
@@ -285,13 +285,13 @@ export class FpSimpleRecipeEditor extends Component {
|
|||||||
* opened from the part-scoped Process Composer, return to that part
|
* opened from the part-scoped Process Composer, return to that part
|
||||||
* form; otherwise drop the user back on the Recipes list.
|
* form; otherwise drop the user back on the Recipes list.
|
||||||
*
|
*
|
||||||
* `clearBreadcrumbs: true` is critical — without it, every part →
|
* `clearBreadcrumbs: true` is critical - without it, every part →
|
||||||
* composer → editor → back leaves intermediate pages on the
|
* composer → editor → back leaves intermediate pages on the
|
||||||
* breadcrumb stack so a second visit shows nonsense.
|
* breadcrumb stack so a second visit shows nonsense.
|
||||||
*/
|
*/
|
||||||
onBackToList() {
|
onBackToList() {
|
||||||
// Pop this editor off the action stack and restore the
|
// Pop this editor off the action stack and restore the
|
||||||
// previous controller — preserves the full breadcrumb trail
|
// previous controller - preserves the full breadcrumb trail
|
||||||
// (Recipes > LGPS1104 > Editor → back keeps "Recipes"
|
// (Recipes > LGPS1104 > Editor → back keeps "Recipes"
|
||||||
// visible; Part > Composer > Editor → back returns to the
|
// visible; Part > Composer > Editor → back returns to the
|
||||||
// Composer with crumbs intact).
|
// Composer with crumbs intact).
|
||||||
@@ -299,7 +299,7 @@ export class FpSimpleRecipeEditor extends Component {
|
|||||||
this.action.restore();
|
this.action.restore();
|
||||||
return;
|
return;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// No prior controller — fall through to a sensible default.
|
// No prior controller - fall through to a sensible default.
|
||||||
}
|
}
|
||||||
if (this._partId) {
|
if (this._partId) {
|
||||||
this.action.doAction(
|
this.action.doAction(
|
||||||
@@ -337,8 +337,8 @@ export class FpSimpleRecipeEditor extends Component {
|
|||||||
description: "",
|
description: "",
|
||||||
requires_signoff: false,
|
requires_signoff: false,
|
||||||
requires_predecessor_done: false,
|
requires_predecessor_done: false,
|
||||||
parallel_start: false, // Sub 13 — per-step opt-out
|
parallel_start: false, // Sub 13 - per-step opt-out
|
||||||
triggers_workflow_state_id: false, // Sub 14 — workflow trigger
|
triggers_workflow_state_id: false, // Sub 14 - workflow trigger
|
||||||
triggers_workflow_state_name: "",
|
triggers_workflow_state_name: "",
|
||||||
requires_rack_assignment: false,
|
requires_rack_assignment: false,
|
||||||
requires_transition_form: false,
|
requires_transition_form: false,
|
||||||
@@ -349,7 +349,7 @@ export class FpSimpleRecipeEditor extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sub 14 — fetch the workflow-state catalog once per editor session,
|
* Sub 14 - fetch the workflow-state catalog once per editor session,
|
||||||
* cache on this.state.workflowStates. Used by both create + edit
|
* cache on this.state.workflowStates. Used by both create + edit
|
||||||
* flows to populate the "Triggers Workflow State" dropdown.
|
* flows to populate the "Triggers Workflow State" dropdown.
|
||||||
*/
|
*/
|
||||||
@@ -368,7 +368,7 @@ export class FpSimpleRecipeEditor extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sub 14b — fetch the user-extensible Step Kind catalog once per
|
* Sub 14b - fetch the user-extensible Step Kind catalog once per
|
||||||
* editor session, cache on this.state.kindOptions. Used by both
|
* editor session, cache on this.state.kindOptions. Used by both
|
||||||
* create + edit flows to populate the "Step Kind" dropdown so
|
* create + edit flows to populate the "Step Kind" dropdown so
|
||||||
* user-added kinds appear without a page reload.
|
* user-added kinds appear without a page reload.
|
||||||
@@ -386,7 +386,7 @@ export class FpSimpleRecipeEditor extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sub 14b — handler for Step Kind dropdown change. Special-cases
|
* Sub 14b - handler for Step Kind dropdown change. Special-cases
|
||||||
* the "+ Add a new kind…" sentinel: prompt the user for a name,
|
* the "+ Add a new kind…" sentinel: prompt the user for a name,
|
||||||
* round-trip to /kinds/create, refresh the cached options, then
|
* round-trip to /kinds/create, refresh the cached options, then
|
||||||
* select the newly-created kind.
|
* select the newly-created kind.
|
||||||
@@ -412,7 +412,7 @@ export class FpSimpleRecipeEditor extends Component {
|
|||||||
name: name.trim(),
|
name: name.trim(),
|
||||||
});
|
});
|
||||||
if (!data.ok) {
|
if (!data.ok) {
|
||||||
// 2026-05-20 — backend forbids non-managers from
|
// 2026-05-20 - backend forbids non-managers from
|
||||||
// creating kinds. Surface the explanatory message
|
// creating kinds. Surface the explanatory message
|
||||||
// instead of a generic error code.
|
// instead of a generic error code.
|
||||||
alert(data.message || data.error || "Could not create Step Kind.");
|
alert(data.message || data.error || "Could not create Step Kind.");
|
||||||
@@ -435,7 +435,7 @@ export class FpSimpleRecipeEditor extends Component {
|
|||||||
template_id: templateId,
|
template_id: templateId,
|
||||||
});
|
});
|
||||||
if (data.ok) {
|
if (data.ok) {
|
||||||
// Defensive copy — OWL useState wraps top-level fields, but
|
// Defensive copy - OWL useState wraps top-level fields, but
|
||||||
// we want to be able to mutate this.state.libraryEditor.* in
|
// we want to be able to mutate this.state.libraryEditor.* in
|
||||||
// place without triggering library list re-renders.
|
// place without triggering library list re-renders.
|
||||||
this.state.libraryEditor = JSON.parse(JSON.stringify(data.template));
|
this.state.libraryEditor = JSON.parse(JSON.stringify(data.template));
|
||||||
@@ -448,7 +448,7 @@ export class FpSimpleRecipeEditor extends Component {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.notification.add(
|
this.notification.add(
|
||||||
_t("Could not load library template — it may have been deleted."),
|
_t("Could not load library template - it may have been deleted."),
|
||||||
{ type: "warning" }
|
{ type: "warning" }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -477,7 +477,7 @@ export class FpSimpleRecipeEditor extends Component {
|
|||||||
requires_signoff: !!ed.requires_signoff,
|
requires_signoff: !!ed.requires_signoff,
|
||||||
requires_predecessor_done: !!ed.requires_predecessor_done,
|
requires_predecessor_done: !!ed.requires_predecessor_done,
|
||||||
parallel_start: !!ed.parallel_start,
|
parallel_start: !!ed.parallel_start,
|
||||||
// Sub 14 — workflow trigger (Many2one int or false)
|
// Sub 14 - workflow trigger (Many2one int or false)
|
||||||
triggers_workflow_state_id: ed.triggers_workflow_state_id || false,
|
triggers_workflow_state_id: ed.triggers_workflow_state_id || false,
|
||||||
requires_rack_assignment: !!ed.requires_rack_assignment,
|
requires_rack_assignment: !!ed.requires_rack_assignment,
|
||||||
requires_transition_form: !!ed.requires_transition_form,
|
requires_transition_form: !!ed.requires_transition_form,
|
||||||
@@ -645,7 +645,7 @@ export class FpSimpleRecipeEditor extends Component {
|
|||||||
this.state.dragOverIndex = before ? rowIndex : rowIndex + 1;
|
this.state.dragOverIndex = before ? rowIndex : rowIndex + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Trailing dropzone — always inserts at the end. */
|
/** Trailing dropzone - always inserts at the end. */
|
||||||
onTailDragOver(ev) {
|
onTailDragOver(ev) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.dataTransfer.dropEffect =
|
ev.dataTransfer.dropEffect =
|
||||||
@@ -657,7 +657,7 @@ export class FpSimpleRecipeEditor extends Component {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Panel-level dragover. Required so HTML5 `drop` actually fires
|
* Panel-level dragover. Required so HTML5 `drop` actually fires
|
||||||
* across the whole panel surface — including the gap between rows
|
* across the whole panel surface - including the gap between rows
|
||||||
* (.25rem margin) and the panel padding (1rem). Without this, drops
|
* (.25rem margin) and the panel padding (1rem). Without this, drops
|
||||||
* on those areas are silently rejected by the browser. Row-level
|
* on those areas are silently rejected by the browser. Row-level
|
||||||
* dragover handlers still run first and set the precise index;
|
* dragover handlers still run first and set the precise index;
|
||||||
@@ -695,7 +695,7 @@ export class FpSimpleRecipeEditor extends Component {
|
|||||||
|
|
||||||
onDragLeave(ev) {
|
onDragLeave(ev) {
|
||||||
// Only clear when leaving the panel entirely. Browser fires
|
// Only clear when leaving the panel entirely. Browser fires
|
||||||
// dragleave when crossing into a child element too — guard against
|
// dragleave when crossing into a child element too - guard against
|
||||||
// that by checking relatedTarget.
|
// that by checking relatedTarget.
|
||||||
if (!ev.currentTarget.contains(ev.relatedTarget)) {
|
if (!ev.currentTarget.contains(ev.relatedTarget)) {
|
||||||
this.state.dragOverIndex = null;
|
this.state.dragOverIndex = null;
|
||||||
@@ -716,7 +716,7 @@ export class FpSimpleRecipeEditor extends Component {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggle the inline edit panel for a step. Closing without explicit
|
* Toggle the inline edit panel for a step. Closing without explicit
|
||||||
* Save discards changes — operator-style "I clicked the wrong row"
|
* Save discards changes - operator-style "I clicked the wrong row"
|
||||||
* shouldn't write garbage to the recipe.
|
* shouldn't write garbage to the recipe.
|
||||||
*/
|
*/
|
||||||
async onToggleEdit(stepId) {
|
async onToggleEdit(stepId) {
|
||||||
@@ -726,10 +726,10 @@ export class FpSimpleRecipeEditor extends Component {
|
|||||||
}
|
}
|
||||||
const step = this.state.steps.find((s) => s.id === stepId);
|
const step = this.state.steps.find((s) => s.id === stepId);
|
||||||
if (!step) return;
|
if (!step) return;
|
||||||
// Sub 14 — make sure the workflow-state catalog is cached so
|
// Sub 14 - make sure the workflow-state catalog is cached so
|
||||||
// the dropdown in the inline form has options to render.
|
// the dropdown in the inline form has options to render.
|
||||||
await this._fpEnsureWorkflowStatesLoaded();
|
await this._fpEnsureWorkflowStatesLoaded();
|
||||||
// 2026-05-20 — Step Type dropdown is now driven by the
|
// 2026-05-20 - Step Type dropdown is now driven by the
|
||||||
// fp.step.kind catalog (curated to 12 active kinds). Cache the
|
// fp.step.kind catalog (curated to 12 active kinds). Cache the
|
||||||
// list before opening the panel so the select renders with
|
// list before opening the panel so the select renders with
|
||||||
// options instead of being empty.
|
// options instead of being empty.
|
||||||
@@ -738,7 +738,7 @@ export class FpSimpleRecipeEditor extends Component {
|
|||||||
this.state.editName = step.name || "";
|
this.state.editName = step.name || "";
|
||||||
this.state.editInstructions = this._htmlToText(step.description || "");
|
this.state.editInstructions = this._htmlToText(step.description || "");
|
||||||
// Settings the user can now change WITHOUT delete + re-add.
|
// Settings the user can now change WITHOUT delete + re-add.
|
||||||
// Default to 'other' when no kind is set — kind_id is required
|
// Default to 'other' when no kind is set - kind_id is required
|
||||||
// on the model so we never want a blank value to round-trip.
|
// on the model so we never want a blank value to round-trip.
|
||||||
this.state.editDefaultKind = step.default_kind || "other";
|
this.state.editDefaultKind = step.default_kind || "other";
|
||||||
this.state.editTriggersWorkflowStateId =
|
this.state.editTriggersWorkflowStateId =
|
||||||
@@ -763,7 +763,7 @@ export class FpSimpleRecipeEditor extends Component {
|
|||||||
const vals = {
|
const vals = {
|
||||||
name: this.state.editName || _t("Untitled Step"),
|
name: this.state.editName || _t("Untitled Step"),
|
||||||
description: this._textToHtml(this.state.editInstructions),
|
description: this._textToHtml(this.state.editInstructions),
|
||||||
// New per-step settings — user can flip these without
|
// New per-step settings - user can flip these without
|
||||||
// deleting and re-adding the step.
|
// deleting and re-adding the step.
|
||||||
default_kind: this.state.editDefaultKind || false,
|
default_kind: this.state.editDefaultKind || false,
|
||||||
triggers_workflow_state_id:
|
triggers_workflow_state_id:
|
||||||
@@ -798,7 +798,7 @@ export class FpSimpleRecipeEditor extends Component {
|
|||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
if (!file.type.startsWith("image/")) {
|
if (!file.type.startsWith("image/")) {
|
||||||
this.notification.add(
|
this.notification.add(
|
||||||
_t("%s isn't an image — skipped.").replace("%s", file.name),
|
_t("%s isn't an image - skipped.").replace("%s", file.name),
|
||||||
{ type: "warning" },
|
{ type: "warning" },
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
@@ -864,7 +864,7 @@ export class FpSimpleRecipeEditor extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- Sub 12d — measurements config --------------------
|
// -------------------- Sub 12d - measurements config --------------------
|
||||||
|
|
||||||
async onToggleStepCollect(stepId, collect) {
|
async onToggleStepCollect(stepId, collect) {
|
||||||
await rpc("/fp/simple_recipe/step/toggle_collect", {
|
await rpc("/fp/simple_recipe/step/toggle_collect", {
|
||||||
@@ -915,7 +915,7 @@ export class FpSimpleRecipeEditor extends Component {
|
|||||||
});
|
});
|
||||||
if (result.error === "library_sourced") {
|
if (result.error === "library_sourced") {
|
||||||
this.notification.add(
|
this.notification.add(
|
||||||
_t("Library prompts can't be deleted — toggle Collect off instead."),
|
_t("Library prompts can't be deleted - toggle Collect off instead."),
|
||||||
{ type: "warning" }
|
{ type: "warning" }
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@@ -936,7 +936,7 @@ export class FpSimpleRecipeEditor extends Component {
|
|||||||
}
|
}
|
||||||
await this.loadAll();
|
await this.loadAll();
|
||||||
this.notification.add(
|
this.notification.add(
|
||||||
_t("Reset to library defaults — custom prompts preserved"),
|
_t("Reset to library defaults - custom prompts preserved"),
|
||||||
{ type: "success" }
|
{ type: "success" }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -944,7 +944,7 @@ export class FpSimpleRecipeEditor extends Component {
|
|||||||
/**
|
/**
|
||||||
* Render stored HTML as plain text for the textarea. Strips tags,
|
* Render stored HTML as plain text for the textarea. Strips tags,
|
||||||
* collapses block elements to newlines. Good enough for the simple
|
* collapses block elements to newlines. Good enough for the simple
|
||||||
* editor — the tree editor handles full rich text.
|
* editor - the tree editor handles full rich text.
|
||||||
*/
|
*/
|
||||||
_htmlToText(html) {
|
_htmlToText(html) {
|
||||||
if (!html) return "";
|
if (!html) return "";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// =====================================================================
|
// =====================================================================
|
||||||
// Fusion Plating — Chatter dark-mode patch
|
// Fusion Plating - Chatter dark-mode patch
|
||||||
//
|
//
|
||||||
// In dark mode the floating message-action toolbar (reaction / reply /
|
// In dark mode the floating message-action toolbar (reaction / reply /
|
||||||
// star / link icons) renders white-on-white because Odoo sets the
|
// star / link icons) renders white-on-white because Odoo sets the
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Sub 14b — Visual icon picker for fp.step.kind.icon and similar
|
// Sub 14b - Visual icon picker for fp.step.kind.icon and similar
|
||||||
// Selection fields whose values are FontAwesome class names.
|
// Selection fields whose values are FontAwesome class names.
|
||||||
//
|
//
|
||||||
// Compact 12-column grid with Search filter. Glyph-only tiles
|
// Compact 12-column grid with Search filter. Glyph-only tiles
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Fusion Plating — backend styles
|
// Fusion Plating - backend styles
|
||||||
// Copyright 2026 Nexa Systems Inc.
|
// Copyright 2026 Nexa Systems Inc.
|
||||||
// License OPL-1 (Odoo Proprietary License v1.0)
|
// License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
//
|
//
|
||||||
@@ -18,10 +18,10 @@
|
|||||||
//
|
//
|
||||||
// Semantic status colours (green / amber / red) use `color-mix()` against the
|
// Semantic status colours (green / amber / red) use `color-mix()` against the
|
||||||
// Bootstrap theme token so a green badge is darker on light mode and brighter
|
// Bootstrap theme token so a green badge is darker on light mode and brighter
|
||||||
// on dark mode automatically — one rule, two looks.
|
// on dark mode automatically - one rule, two looks.
|
||||||
//
|
//
|
||||||
// We never target `.o_dark`, `html.dark`, or `@media (prefers-color-scheme)`
|
// We never target `.o_dark`, `html.dark`, or `@media (prefers-color-scheme)`
|
||||||
// to override colours. If you find yourself needing that, it's a smell — use
|
// to override colours. If you find yourself needing that, it's a smell - use
|
||||||
// a variable instead.
|
// a variable instead.
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
@@ -72,12 +72,12 @@
|
|||||||
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// Tank kanban — state badge theming
|
// Tank kanban - state badge theming
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
.o_fp_tank_kanban {
|
.o_fp_tank_kanban {
|
||||||
|
|
||||||
.o_fp_tank_card {
|
.o_fp_tank_card {
|
||||||
// Let the left-border carry the state — subtle, theme-aware.
|
// Let the left-border carry the state - subtle, theme-aware.
|
||||||
border-left-width: 4px;
|
border-left-width: 4px;
|
||||||
|
|
||||||
&[data-state="empty"],
|
&[data-state="empty"],
|
||||||
@@ -123,7 +123,7 @@
|
|||||||
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// Bath kanban — chemistry health dot
|
// Bath kanban - chemistry health dot
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
.o_fp_bath_kanban {
|
.o_fp_bath_kanban {
|
||||||
|
|
||||||
@@ -162,7 +162,7 @@
|
|||||||
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// Facility kanban — stat strip spacing
|
// Facility kanban - stat strip spacing
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
.o_fp_facility_kanban {
|
.o_fp_facility_kanban {
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Fusion Plating — Recipe Tree Editor (horizontal bracket-tree, v2, 2026-04)
|
// Fusion Plating - Recipe Tree Editor (horizontal bracket-tree, v2, 2026-04)
|
||||||
// Copyright 2026 Nexa Systems Inc. · License OPL-1
|
// Copyright 2026 Nexa Systems Inc. · License OPL-1
|
||||||
//
|
//
|
||||||
// Same Steelhead-style bracket layout as the read-only Process Tree, but
|
// Same Steelhead-style bracket layout as the read-only Process Tree, but
|
||||||
@@ -235,7 +235,7 @@ $re-line-w : 2px;
|
|||||||
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Card — dark Steelhead-style
|
// Card - dark Steelhead-style
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
.o_fp_re_card {
|
.o_fp_re_card {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -296,14 +296,14 @@ $re-line-w : 2px;
|
|||||||
// ---- MO-execution state palette (Sub 3) --------------------------
|
// ---- MO-execution state palette (Sub 3) --------------------------
|
||||||
// Applied when rendering the tree in an MO context where each
|
// Applied when rendering the tree in an MO context where each
|
||||||
// node can be mapped to a linked WO. Red is reserved for true
|
// node can be mapped to a linked WO. Red is reserved for true
|
||||||
// error / blocked states — previously the "active" highlight
|
// error / blocked states - previously the "active" highlight
|
||||||
// used red which confused operators into seeing it as an error.
|
// used red which confused operators into seeing it as an error.
|
||||||
// completed -> green (WO done)
|
// completed -> green (WO done)
|
||||||
// active -> blue (WO in progress)
|
// active -> blue (WO in progress)
|
||||||
// failed /
|
// failed /
|
||||||
// blocked -> red (WO cancel OR quality hold active)
|
// blocked -> red (WO cancel OR quality hold active)
|
||||||
// Pending nodes (no WO, or WO pending/ready) keep the default
|
// Pending nodes (no WO, or WO pending/ready) keep the default
|
||||||
// card styling — no modifier class applied.
|
// card styling - no modifier class applied.
|
||||||
&.o_fp_re_card_completed {
|
&.o_fp_re_card_completed {
|
||||||
background-color: color-mix(in srgb, var(--bs-success, #28a745) 10%, #{$re-card});
|
background-color: color-mix(in srgb, var(--bs-success, #28a745) 10%, #{$re-card});
|
||||||
border: 2px solid var(--bs-success, #28a745);
|
border: 2px solid var(--bs-success, #28a745);
|
||||||
@@ -371,7 +371,7 @@ $re-line-w : 2px;
|
|||||||
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Capability flags — small icon row inside the card
|
// Capability flags - small icon row inside the card
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
.o_fp_re_flags {
|
.o_fp_re_flags {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -467,7 +467,7 @@ $re-line-w : 2px;
|
|||||||
position: relative;
|
position: relative;
|
||||||
padding-left: $re-stub;
|
padding-left: $re-stub;
|
||||||
|
|
||||||
// horizontal stub — bus column → child card
|
// horizontal stub - bus column → child card
|
||||||
&::before {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// Sub 12a — Simple Recipe Editor styling.
|
// Sub 12a - Simple Recipe Editor styling.
|
||||||
//
|
//
|
||||||
// Tokens follow the existing fp_shopfloor pattern (CSS custom props
|
// Tokens follow the existing fp_shopfloor pattern (CSS custom props
|
||||||
// with hex fallbacks; dark-mode aware via $o-webclient-color-scheme
|
// with hex fallbacks; dark-mode aware via $o-webclient-color-scheme
|
||||||
// SCSS @if branch — see fusion_plating CLAUDE.md for the rule).
|
// SCSS @if branch - see fusion_plating CLAUDE.md for the rule).
|
||||||
|
|
||||||
$o-webclient-color-scheme: bright !default;
|
$o-webclient-color-scheme: bright !default;
|
||||||
|
|
||||||
@@ -118,7 +118,7 @@ $fp-se-drop: var(--fp-drop-bg, #{$_fp_se_drop_hex});
|
|||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
// align-items: start so the library panel can be shorter than
|
// align-items: start so the library panel can be shorter than
|
||||||
// the recipe-step column without stretching to match its height
|
// the recipe-step column without stretching to match its height
|
||||||
// — required for sticky positioning to behave.
|
// - required for sticky positioning to behave.
|
||||||
align-items: start;
|
align-items: start;
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
@@ -141,7 +141,7 @@ $fp-se-drop: var(--fp-drop-bg, #{$_fp_se_drop_hex});
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step Library — pin to the top of the scroll container so authors
|
// Step Library - pin to the top of the scroll container so authors
|
||||||
// can drag from it into the recipe without scrolling back up.
|
// can drag from it into the recipe without scrolling back up.
|
||||||
// Recipes can be 40+ steps long; before this, the library scrolled
|
// Recipes can be 40+ steps long; before this, the library scrolled
|
||||||
// off with the page and you had to scroll to the top, grab a step,
|
// off with the page and you had to scroll to the top, grab a step,
|
||||||
@@ -149,7 +149,7 @@ $fp-se-drop: var(--fp-drop-bg, #{$_fp_se_drop_hex});
|
|||||||
//
|
//
|
||||||
// Sticky inside the editor's overflow:auto container. max-height +
|
// Sticky inside the editor's overflow:auto container. max-height +
|
||||||
// internal overflow-y so the library's OWN content (could be 30+
|
// internal overflow-y so the library's OWN content (could be 30+
|
||||||
// entries) doesn't blow past the viewport — it grows a scrollbar
|
// entries) doesn't blow past the viewport - it grows a scrollbar
|
||||||
// instead.
|
// instead.
|
||||||
.o_fp_library_panel {
|
.o_fp_library_panel {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
@@ -161,7 +161,7 @@ $fp-se-drop: var(--fp-drop-bg, #{$_fp_se_drop_hex});
|
|||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
// Stacked layout — no sticky, behaves like a normal block.
|
// Stacked layout - no sticky, behaves like a normal block.
|
||||||
position: static;
|
position: static;
|
||||||
max-height: none;
|
max-height: none;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
@@ -271,7 +271,7 @@ $fp-se-drop: var(--fp-drop-bg, #{$_fp_se_drop_hex});
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step nodes inside an operation are rendered as indented sub-rows
|
// Step nodes inside an operation are rendered as indented sub-rows
|
||||||
// — same node model as operations, but they're sub-instructions
|
// - same node model as operations, but they're sub-instructions
|
||||||
// (the WO generator folds them into the operation's instruction
|
// (the WO generator folds them into the operation's instruction
|
||||||
// text). Visual treatment: smaller, indented, no drag handle, no
|
// text). Visual treatment: smaller, indented, no drag handle, no
|
||||||
// numeric position so the eye can tell them apart from operations.
|
// numeric position so the eye can tell them apart from operations.
|
||||||
@@ -615,7 +615,7 @@ $fp-se-drop: var(--fp-drop-bg, #{$_fp_se_drop_hex});
|
|||||||
|
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Instruction images gallery — recipe-author upload + thumbnail strip in
|
// Instruction images gallery - recipe-author upload + thumbnail strip in
|
||||||
// the Simple Editor's inline step edit panel. Mirrors what the Record
|
// the Simple Editor's inline step edit panel. Mirrors what the Record
|
||||||
// Inputs dialog renders at runtime so authors can preview the same way
|
// Inputs dialog renders at runtime so authors can preview the same way
|
||||||
// the operator will see it.
|
// the operator will see it.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
License OPL-1 (Odoo Proprietary License v1.0)
|
License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
Part of the Fusion Plating product family.
|
Part of the Fusion Plating product family.
|
||||||
|
|
||||||
Recipe Tree Editor — horizontal hierarchical layout (Steelhead-style).
|
Recipe Tree Editor - horizontal hierarchical layout (Steelhead-style).
|
||||||
Recursive template renders recipe → sub-process → operation → step
|
Recursive template renders recipe → sub-process → operation → step
|
||||||
cards left→right with bracket connectors. Each card carries hover-
|
cards left→right with bracket connectors. Each card carries hover-
|
||||||
revealed Add / Delete buttons; a side panel slides in for editing
|
revealed Add / Delete buttons; a side panel slides in for editing
|
||||||
@@ -212,7 +212,7 @@
|
|||||||
<label class="o_fp_re_import_label">Import children from:</label>
|
<label class="o_fp_re_import_label">Import children from:</label>
|
||||||
<select class="o_fp_re_import_select"
|
<select class="o_fp_re_import_select"
|
||||||
t-model="state.importRecipeId">
|
t-model="state.importRecipeId">
|
||||||
<option value="">— Pick a recipe —</option>
|
<option value="">- Pick a recipe -</option>
|
||||||
<t t-foreach="state.importRecipeOptions" t-as="r" t-key="r.id">
|
<t t-foreach="state.importRecipeOptions" t-as="r" t-key="r.id">
|
||||||
<option t-att-value="r.id" t-esc="r.name"/>
|
<option t-att-value="r.id" t-esc="r.name"/>
|
||||||
</t>
|
</t>
|
||||||
@@ -350,7 +350,7 @@
|
|||||||
t-on-change="(ev) => { state.selectedNode.customer_visible = ev.target.checked; }"/>
|
t-on-change="(ev) => { state.selectedNode.customer_visible = ev.target.checked; }"/>
|
||||||
<label class="form-check-label" for="fp_re_chk_visible">Customer visible</label>
|
<label class="form-check-label" for="fp_re_chk_visible">Customer visible</label>
|
||||||
</div>
|
</div>
|
||||||
<!-- Sub 13 — sequential enforcement (recipe root) -->
|
<!-- Sub 13 - sequential enforcement (recipe root) -->
|
||||||
<div class="form-check"
|
<div class="form-check"
|
||||||
t-if="state.selectedNode.node_type === 'recipe'">
|
t-if="state.selectedNode.node_type === 'recipe'">
|
||||||
<input type="checkbox" class="form-check-input" id="fp_re_chk_seq"
|
<input type="checkbox" class="form-check-input" id="fp_re_chk_seq"
|
||||||
@@ -361,7 +361,7 @@
|
|||||||
Enforce Sequential Order
|
Enforce Sequential Order
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<!-- Sub 13 — per-step opt-out (operation/step nodes) -->
|
<!-- Sub 13 - per-step opt-out (operation/step nodes) -->
|
||||||
<div class="form-check"
|
<div class="form-check"
|
||||||
t-if="state.selectedNode.node_type === 'operation' or state.selectedNode.node_type === 'step'">
|
t-if="state.selectedNode.node_type === 'operation' or state.selectedNode.node_type === 'step'">
|
||||||
<input type="checkbox" class="form-check-input" id="fp_re_chk_par"
|
<input type="checkbox" class="form-check-input" id="fp_re_chk_par"
|
||||||
@@ -374,7 +374,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Sub 14 — workflow milestone trigger (operation / step nodes) -->
|
<!-- Sub 14 - workflow milestone trigger (operation / step nodes) -->
|
||||||
<div class="o_fp_re_field"
|
<div class="o_fp_re_field"
|
||||||
t-if="(state.selectedNode.node_type === 'operation' or state.selectedNode.node_type === 'step') and state.workflowStates.length">
|
t-if="(state.selectedNode.node_type === 'operation' or state.selectedNode.node_type === 'step') and state.workflowStates.length">
|
||||||
<label for="fp_re_workflow_state">Triggers Workflow State</label>
|
<label for="fp_re_workflow_state">Triggers Workflow State</label>
|
||||||
@@ -383,7 +383,7 @@
|
|||||||
t-on-change="(ev) => { state.selectedNode.triggers_workflow_state_id = ev.target.value ? (+ev.target.value) : false; }">
|
t-on-change="(ev) => { state.selectedNode.triggers_workflow_state_id = ev.target.value ? (+ev.target.value) : false; }">
|
||||||
<option value=""
|
<option value=""
|
||||||
t-att-selected="!state.selectedNode.triggers_workflow_state_id">
|
t-att-selected="!state.selectedNode.triggers_workflow_state_id">
|
||||||
— None (use default-kind matching) —
|
- None (use default-kind matching) -
|
||||||
</option>
|
</option>
|
||||||
<t t-foreach="state.workflowStates" t-as="ws" t-key="ws.id">
|
<t t-foreach="state.workflowStates" t-as="ws" t-key="ws.id">
|
||||||
<option t-att-value="ws.id"
|
<option t-att-value="ws.id"
|
||||||
@@ -411,9 +411,9 @@
|
|||||||
t-att-selected="state.selectedNode.opt_in_out === 'opt_in'">Opt-In (excluded by default)</option>
|
t-att-selected="state.selectedNode.opt_in_out === 'opt_in'">Opt-In (excluded by default)</option>
|
||||||
</select>
|
</select>
|
||||||
<small class="text-muted d-block mt-1">
|
<small class="text-muted d-block mt-1">
|
||||||
<strong>Required</strong> — every job runs this step.
|
<strong>Required</strong> - every job runs this step.
|
||||||
<strong>Opt-Out</strong> — ships included, estimator can remove per job.
|
<strong>Opt-Out</strong> - ships included, estimator can remove per job.
|
||||||
<strong>Opt-In</strong> — ships excluded, estimator can add per job.
|
<strong>Opt-In</strong> - ships excluded, estimator can add per job.
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
<select id="fp_import_template_select"
|
<select id="fp_import_template_select"
|
||||||
class="form-select o_fp_import_select"
|
class="form-select o_fp_import_select"
|
||||||
t-model="state.selectedTemplate">
|
t-model="state.selectedTemplate">
|
||||||
<option value="">— Select template —</option>
|
<option value="">- Select template -</option>
|
||||||
<t t-foreach="state.templateOptions" t-as="tpl" t-key="tpl.id">
|
<t t-foreach="state.templateOptions" t-as="tpl" t-key="tpl.id">
|
||||||
<option t-att-value="tpl.id">
|
<option t-att-value="tpl.id">
|
||||||
<t t-esc="tpl.name"/> (<t t-esc="tpl.step_count"/> steps)
|
<t t-esc="tpl.name"/> (<t t-esc="tpl.step_count"/> steps)
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
<div class="o_fp_steps_list">
|
<div class="o_fp_steps_list">
|
||||||
|
|
||||||
<!-- Top drop indicator (insertion at index 0). Visible
|
<!-- Top drop indicator (insertion at index 0). Visible
|
||||||
only when dragOverIndex === 0 — i.e. cursor is
|
only when dragOverIndex === 0 - i.e. cursor is
|
||||||
hovering above the first row's midpoint. -->
|
hovering above the first row's midpoint. -->
|
||||||
<div class="o_fp_drop_indicator"
|
<div class="o_fp_drop_indicator"
|
||||||
t-att-class="state.dragOverIndex === 0 ? 'o_fp_drop_indicator_active' : ''">
|
t-att-class="state.dragOverIndex === 0 ? 'o_fp_drop_indicator_active' : ''">
|
||||||
@@ -145,13 +145,13 @@
|
|||||||
<textarea class="form-control"
|
<textarea class="form-control"
|
||||||
rows="5"
|
rows="5"
|
||||||
t-model="state.editInstructions"
|
t-model="state.editInstructions"
|
||||||
placeholder="What the operator/employee sees on the shop floor when running this step. Plain text — line breaks are preserved."/>
|
placeholder="What the operator/employee sees on the shop floor when running this step. Plain text - line breaks are preserved."/>
|
||||||
<p class="o_fp_edit_hint">
|
<p class="o_fp_edit_hint">
|
||||||
Shown to operators when running this step at the tank. Use line breaks for separate points.
|
Shown to operators when running this step at the tank. Use line breaks for separate points.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Sub 14 + Sub 13 — settings the user can change
|
<!-- Sub 14 + Sub 13 - settings the user can change
|
||||||
without delete + re-add. Step Type drives the
|
without delete + re-add. Step Type drives the
|
||||||
default-kind workflow trigger; the dropdown
|
default-kind workflow trigger; the dropdown
|
||||||
below it lets them override per-step. -->
|
below it lets them override per-step. -->
|
||||||
@@ -200,7 +200,7 @@
|
|||||||
<label>Triggers Workflow State</label>
|
<label>Triggers Workflow State</label>
|
||||||
<select class="form-select"
|
<select class="form-select"
|
||||||
t-on-change="(ev) => { state.editTriggersWorkflowStateId = ev.target.value ? (+ev.target.value) : false; }">
|
t-on-change="(ev) => { state.editTriggersWorkflowStateId = ev.target.value ? (+ev.target.value) : false; }">
|
||||||
<option value="" t-att-selected="!state.editTriggersWorkflowStateId">— None (use Step Type) —</option>
|
<option value="" t-att-selected="!state.editTriggersWorkflowStateId">- None (use Step Type) -</option>
|
||||||
<t t-foreach="state.workflowStates" t-as="ws" t-key="ws.id">
|
<t t-foreach="state.workflowStates" t-as="ws" t-key="ws.id">
|
||||||
<option t-att-value="ws.id"
|
<option t-att-value="ws.id"
|
||||||
t-att-selected="state.editTriggersWorkflowStateId === ws.id"
|
t-att-selected="state.editTriggersWorkflowStateId === ws.id"
|
||||||
@@ -234,7 +234,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Instruction images — recipe author drops
|
<!-- Instruction images - recipe author drops
|
||||||
photos / screenshots / diagrams here.
|
photos / screenshots / diagrams here.
|
||||||
Operators see the gallery at runtime in
|
Operators see the gallery at runtime in
|
||||||
the Record Inputs dialog and the step
|
the Record Inputs dialog and the step
|
||||||
@@ -283,7 +283,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Sub 12d — Measurements config -->
|
<!-- Sub 12d - Measurements config -->
|
||||||
<div class="o_fp_edit_field o_fp_measurements_config">
|
<div class="o_fp_edit_field o_fp_measurements_config">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
@@ -324,7 +324,7 @@
|
|||||||
t-on-blur="(ev) => this.onInputBlur(inp.id, 'name', ev)"/>
|
t-on-blur="(ev) => this.onInputBlur(inp.id, 'name', ev)"/>
|
||||||
<small t-if="inp.from_library"
|
<small t-if="inp.from_library"
|
||||||
class="text-muted"
|
class="text-muted"
|
||||||
title="From library template — toggle Collect off instead of deleting">
|
title="From library template - toggle Collect off instead of deleting">
|
||||||
from library
|
from library
|
||||||
</small>
|
</small>
|
||||||
</td>
|
</td>
|
||||||
@@ -509,7 +509,7 @@
|
|||||||
<t t-foreach="state.kindOptions || []" t-as="k" t-key="k.id">
|
<t t-foreach="state.kindOptions || []" t-as="k" t-key="k.id">
|
||||||
<option t-att-value="k.code" t-att-selected="k.code === state.libraryEditor.default_kind">
|
<option t-att-value="k.code" t-att-selected="k.code === state.libraryEditor.default_kind">
|
||||||
<t t-esc="k.name"/>
|
<t t-esc="k.name"/>
|
||||||
<t t-if="k.area_kind_label"> — <t t-esc="k.area_kind_label"/> column</t>
|
<t t-if="k.area_kind_label"> - <t t-esc="k.area_kind_label"/> column</t>
|
||||||
</option>
|
</option>
|
||||||
</t>
|
</t>
|
||||||
<!-- Manager-only inline create. The
|
<!-- Manager-only inline create. The
|
||||||
@@ -592,7 +592,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Sub 14 — workflow milestone trigger dropdown.
|
<!-- Sub 14 - workflow milestone trigger dropdown.
|
||||||
Hidden when no states exist (e.g. catalog
|
Hidden when no states exist (e.g. catalog
|
||||||
not seeded yet). -->
|
not seeded yet). -->
|
||||||
<div class="o_fp_le_field"
|
<div class="o_fp_le_field"
|
||||||
@@ -602,7 +602,7 @@
|
|||||||
t-on-change="(ev) => { state.libraryEditor.triggers_workflow_state_id = ev.target.value ? (+ev.target.value) : false; }">
|
t-on-change="(ev) => { state.libraryEditor.triggers_workflow_state_id = ev.target.value ? (+ev.target.value) : false; }">
|
||||||
<option value=""
|
<option value=""
|
||||||
t-att-selected="!state.libraryEditor.triggers_workflow_state_id">
|
t-att-selected="!state.libraryEditor.triggers_workflow_state_id">
|
||||||
— None (use default-kind matching) —
|
- None (use default-kind matching) -
|
||||||
</option>
|
</option>
|
||||||
<t t-foreach="state.workflowStates" t-as="ws" t-key="ws.id">
|
<t t-foreach="state.workflowStates" t-as="ws" t-key="ws.id">
|
||||||
<option t-att-value="ws.id"
|
<option t-att-value="ws.id"
|
||||||
@@ -702,7 +702,7 @@
|
|||||||
Save the step first, then add prompts.
|
Save the step first, then add prompts.
|
||||||
</t>
|
</t>
|
||||||
<t t-else="">
|
<t t-else="">
|
||||||
No prompts yet — operators will not be asked for measurements at runtime.
|
No prompts yet - operators will not be asked for measurements at runtime.
|
||||||
</t>
|
</t>
|
||||||
</p>
|
</p>
|
||||||
<div class="o_fp_le_prompt_actions"
|
<div class="o_fp_le_prompt_actions"
|
||||||
@@ -714,7 +714,7 @@
|
|||||||
<button class="btn btn-link btn-sm"
|
<button class="btn btn-link btn-sm"
|
||||||
t-if="state.libraryEditor.default_kind"
|
t-if="state.libraryEditor.default_kind"
|
||||||
t-on-click="onSeedLibraryDefaults"
|
t-on-click="onSeedLibraryDefaults"
|
||||||
title="Append the canonical prompts for this Step Kind. Idempotent — won't duplicate existing prompts.">
|
title="Append the canonical prompts for this Step Kind. Idempotent - won't duplicate existing prompts.">
|
||||||
<i class="fa fa-magic"/> Seed defaults from kind
|
<i class="fa fa-magic"/> Seed defaults from kind
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -46,11 +46,11 @@ class TestFpJobStateMachine(TransactionCase):
|
|||||||
job.action_confirm()
|
job.action_confirm()
|
||||||
|
|
||||||
def test_cannot_cancel_done(self):
|
def test_cannot_cancel_done(self):
|
||||||
# Done jobs cannot be cancelled — covers the UserError branch in
|
# Done jobs cannot be cancelled - covers the UserError branch in
|
||||||
# action_cancel.
|
# action_cancel.
|
||||||
job = self._make_job()
|
job = self._make_job()
|
||||||
job.action_confirm()
|
job.action_confirm()
|
||||||
# Force the state to 'done' for the test (no public action yet —
|
# Force the state to 'done' for the test (no public action yet -
|
||||||
# done is set by step-completion logic landing in Task 1.5+).
|
# done is set by step-completion logic landing in Task 1.5+).
|
||||||
job.state = 'done'
|
job.state = 'done'
|
||||||
with self.assertRaises(UserError):
|
with self.assertRaises(UserError):
|
||||||
@@ -71,7 +71,7 @@ class TestFpJobStateMachine(TransactionCase):
|
|||||||
job = self._make_job()
|
job = self._make_job()
|
||||||
# Force state to 'done' (no public action yet)
|
# Force state to 'done' (no public action yet)
|
||||||
job.state = 'done'
|
job.state = 'done'
|
||||||
# Recompute — Odoo's compute is auto on read
|
# Recompute - Odoo's compute is auto on read
|
||||||
self.assertEqual(job.current_location, 'Done')
|
self.assertEqual(job.current_location, 'Done')
|
||||||
|
|
||||||
def test_margin_zero_when_no_revenue(self):
|
def test_margin_zero_when_no_revenue(self):
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class TestFpJobStepStateMachine(TransactionCase):
|
|||||||
|
|
||||||
def test_button_start_requires_ready_or_paused(self):
|
def test_button_start_requires_ready_or_paused(self):
|
||||||
step = self._make_step()
|
step = self._make_step()
|
||||||
# state is 'pending' — should raise
|
# state is 'pending' - should raise
|
||||||
with self.assertRaises(UserError):
|
with self.assertRaises(UserError):
|
||||||
step.button_start()
|
step.button_start()
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class TestFpWorkCentre(TransactionCase):
|
|||||||
|
|
||||||
def test_facility_optional_at_create(self):
|
def test_facility_optional_at_create(self):
|
||||||
# Facility is soft-required (warning at confirm, not constraint
|
# Facility is soft-required (warning at confirm, not constraint
|
||||||
# at create) — verify a centre without facility still creates.
|
# at create) - verify a centre without facility still creates.
|
||||||
wc = self.env['fp.work.centre'].create({
|
wc = self.env['fp.work.centre'].create({
|
||||||
'name': 'Test',
|
'name': 'Test',
|
||||||
'code': 'T',
|
'code': 'T',
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# Copyright 2026 Nexa Systems Inc.
|
# Copyright 2026 Nexa Systems Inc.
|
||||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
# Part of the Fusion Plating product family.
|
# Part of the Fusion Plating product family.
|
||||||
"""Phase E (Plating permissions overhaul) — role-based landing dispatch.
|
"""Phase E (Plating permissions overhaul) - role-based landing dispatch.
|
||||||
|
|
||||||
Section 3 of the design spec covers per-role landing pages:
|
Section 3 of the design spec covers per-role landing pages:
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ class TestLandingResolver(TransactionCase):
|
|||||||
|
|
||||||
The resolver lives on `ir.actions.act_window` (helper method, not a
|
The resolver lives on `ir.actions.act_window` (helper method, not a
|
||||||
column). It can return an action dict for either an act_window or a
|
column). It can return an action dict for either an act_window or a
|
||||||
client action — both carry an `xml_id` key once we go through
|
client action - both carry an `xml_id` key once we go through
|
||||||
`_render_resolved`.
|
`_render_resolved`.
|
||||||
"""
|
"""
|
||||||
Window = self.env['ir.actions.act_window']
|
Window = self.env['ir.actions.act_window']
|
||||||
@@ -123,7 +123,7 @@ class TestLandingResolver(TransactionCase):
|
|||||||
"""The legacy 'fp_shopfloor_landing' component was retired
|
"""The legacy 'fp_shopfloor_landing' component was retired
|
||||||
2026-05-25. The ``fusion_plating_shopfloor.layout`` flag is now
|
2026-05-25. The ``fusion_plating_shopfloor.layout`` flag is now
|
||||||
orphaned (kept in res.config.settings for one release cycle) and
|
orphaned (kept in res.config.settings for one release cycle) and
|
||||||
flipping it must NOT change the landing — every technician lands
|
flipping it must NOT change the landing - every technician lands
|
||||||
on the plant kanban."""
|
on the plant kanban."""
|
||||||
self.env['ir.config_parameter'].sudo().set_param(
|
self.env['ir.config_parameter'].sudo().set_param(
|
||||||
'fusion_plating_shopfloor.layout', 'legacy')
|
'fusion_plating_shopfloor.layout', 'legacy')
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class TestMenuVisibility(TransactionCase):
|
|||||||
'email': f'menu_{name}@example.com',
|
'email': f'menu_{name}@example.com',
|
||||||
'group_ids': [(6, 0, [self.env.ref(xmlid).id])] if xmlid else [(6, 0, [])],
|
'group_ids': [(6, 0, [self.env.ref(xmlid).id])] if xmlid else [(6, 0, [])],
|
||||||
})
|
})
|
||||||
# "No" user has only base.group_user — no plating group
|
# "No" user has only base.group_user - no plating group
|
||||||
no_user = Users.create({
|
no_user = Users.create({
|
||||||
'login': 'menu_no', 'name': 'Menu Test no',
|
'login': 'menu_no', 'name': 'Menu Test no',
|
||||||
'email': 'menu_no@example.com',
|
'email': 'menu_no@example.com',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Phase 1 — rack-load core model tests.
|
# Phase 1 - rack-load core model tests.
|
||||||
from odoo.tests import TransactionCase, tagged
|
from odoo.tests import TransactionCase, tagged
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class TestRoleGroupsStructure(TransactionCase):
|
|||||||
|
|
||||||
def test_all_seven_groups_exist(self):
|
def test_all_seven_groups_exist(self):
|
||||||
"""The 7 new res.groups records must all be defined. (The 8th role 'No'
|
"""The 7 new res.groups records must all be defined. (The 8th role 'No'
|
||||||
is implicit — absence of any plating group.)"""
|
is implicit - absence of any plating group.)"""
|
||||||
xmlids = {
|
xmlids = {
|
||||||
'group_fp_technician', 'group_fp_sales_rep',
|
'group_fp_technician', 'group_fp_sales_rep',
|
||||||
'group_fp_shop_manager_v2', 'group_fp_sales_manager',
|
'group_fp_shop_manager_v2', 'group_fp_sales_manager',
|
||||||
@@ -33,7 +33,7 @@ class TestRoleGroupsStructure(TransactionCase):
|
|||||||
'Owner must transitively imply base.group_system')
|
'Owner must transitively imply base.group_system')
|
||||||
|
|
||||||
def test_manager_implies_both_branches(self):
|
def test_manager_implies_both_branches(self):
|
||||||
"""Manager is the diamond apex — must imply both Shop Manager and Sales Manager."""
|
"""Manager is the diamond apex - must imply both Shop Manager and Sales Manager."""
|
||||||
mgr = self.env.ref('fusion_plating.group_fp_manager')
|
mgr = self.env.ref('fusion_plating.group_fp_manager')
|
||||||
sm = self.env.ref('fusion_plating.group_fp_shop_manager_v2')
|
sm = self.env.ref('fusion_plating.group_fp_shop_manager_v2')
|
||||||
sales_mgr = self.env.ref('fusion_plating.group_fp_sales_manager')
|
sales_mgr = self.env.ref('fusion_plating.group_fp_sales_manager')
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user