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:
gsinghpal
2026-06-05 00:16:19 -04:00
parent c9eb61ee0c
commit 8c76a16366
789 changed files with 4692 additions and 4692 deletions

View File

@@ -18,13 +18,13 @@ def post_init_hook(env):
Does several things, each guarded by an "is this already done?"
check so re-running the hook doesn't clobber state:
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
`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.
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
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
still holds an old plating-role group + notify Owners.
"""
_seed_default_timezone(env)
@@ -43,7 +43,7 @@ def post_init_hook(env):
# from uninstalled modules so this is safe across configurations.
# Kept visible to technicians (NOT in this list): Discuss, To-do,
# 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.
MENU_HIDE_FROM_TECHNICIANS = [
'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.
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).
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
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,
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,
ENP-ALUM-BASIC, etc.) lived in the manifest's ``data`` list. With
``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
unlink, Odoo on the next ``-u`` sees the xmlid as missing and
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
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
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.
"""
from odoo.tools import convert
@@ -197,7 +197,7 @@ def _seed_starter_recipes_once(env):
module_name, name = xmlid.split('.', 1)
if IMD.search_count([('module', '=', module_name), ('name', '=', name)]):
# 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.
continue
# 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):
"""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
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):
"""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.execute(
"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
# input seeding on the starter library entries.
_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_*
# hooks in fusion_plating_jobs detect the kind=='contract_review' and
# 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 final inspection': '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
# resolver didn't know that turned up in the entech recipes audit.
# Blasting variants
@@ -413,13 +413,13 @@ def fp_resolve_step_kind(name):
key = name.strip().lower()
if key in _STARTER_KIND_BY_NAME:
return _STARTER_KIND_BY_NAME[key]
# Parenthetical strip "Masking (If Required)" → "masking",
# Parenthetical strip - "Masking (If Required)" → "masking",
# "Incoming Inspection (Standard)" → "incoming inspection",
# "Trivalent Chromate Conversion (A-14 / A)" → "trivalent chromate conversion".
bare = re.sub(r'\s*\([^)]*\)\s*', ' ', key).strip()
if bare and bare != key and bare in _STARTER_KIND_BY_NAME:
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.
if key.startswith('ready for ') or key.startswith('ready '):
return 'gating'
@@ -429,7 +429,7 @@ def fp_resolve_step_kind(name):
# Translates resolver kind output to the active fp.step.kind.code values.
# The resolver still returns the OLD vocabulary (cleaning, electroclean,
# 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
# fusion.plating.process.node + the recipe-cleanup migration
# (fusion_plating_jobs 19.0.10.26.0).
@@ -446,7 +446,7 @@ RESOLVER_KIND_TO_ACTIVE_KIND = {
# for "Strip Process - AL", "Chemical
# Conversion", "Trivalent Chromate
# Conversion" maps DIRECTLY to
# 'wet_process' this passthrough
# 'wet_process' - this passthrough
# entry lets those land correctly.
# 1:1 mappings (kind exists and is active)
'contract_review': 'contract_review',
@@ -465,10 +465,10 @@ RESOLVER_KIND_TO_ACTIVE_KIND = {
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:
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).
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):
"""Sub 12b seed 4 starter rack tags."""
"""Sub 12b - seed 4 starter rack tags."""
Tag = env['fp.rack.tag']
if Tag.search_count([]):
return

View File

@@ -9,7 +9,7 @@
'category': 'Manufacturing/Plating',
'summary': 'Core plating / metal finishing ERP: facilities, processes, tanks, baths, jobs, operators.',
'description': """
Fusion Plating Core
Fusion Plating - Core
=====================
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.
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_chrome Chrome coating (hex or trivalent)
* fusion_plating_process_anodize Aluminum anodizing (Type II, III)
* fusion_plating_process_black_oxide Black oxidizing
* fusion_plating_quality QMS (NCR, CAPA, calibration, CoC, doc control)
* fusion_plating_compliance Generic compliance framework
* fusion_plating_compliance_on Ontario regulatory pack
* fusion_plating_compliance_tor Toronto Ch. 681 municipal pack
* fusion_plating_safety SDS, WHMIS/TDG training, JHSC, exposure
* fusion_plating_shopfloor Tablet operator stations, QR scanning
* fusion_plating_portal Customer portal
* fusion_plating_aerospace AS9100 + Nadcap AC7108 pack
* fusion_plating_nuclear CSA N299, CNSC, NQA-1 pack
* fusion_plating_cgp Controlled Goods Program pack
* fusion_plating_logistics Pickup & delivery
* fusion_plating_culture Values / fundamentals framework
* fusion_plating_process_en - Electroless nickel plating
* fusion_plating_process_chrome - Chrome coating (hex or trivalent)
* fusion_plating_process_anodize - Aluminum anodizing (Type II, III)
* fusion_plating_process_black_oxide - Black oxidizing
* fusion_plating_quality - QMS (NCR, CAPA, calibration, CoC, doc control)
* fusion_plating_compliance - Generic compliance framework
* fusion_plating_compliance_on - Ontario regulatory pack
* fusion_plating_compliance_tor - Toronto Ch. 681 municipal pack
* fusion_plating_safety - SDS, WHMIS/TDG training, JHSC, exposure
* fusion_plating_shopfloor - Tablet operator stations, QR scanning
* fusion_plating_portal - Customer portal
* fusion_plating_aerospace - AS9100 + Nadcap AC7108 pack
* fusion_plating_nuclear - CSA N299, CNSC, NQA-1 pack
* fusion_plating_cgp - Controlled Goods Program pack
* fusion_plating_logistics - Pickup & delivery
* fusion_plating_culture - Values / fundamentals framework
Core concepts
-------------
* Facility a physical site with its own tanks, operators, compliance profile
* Process Type extensible taxonomy of finishing processes
* Work Center production line or station within a facility
* Tank physical vessel with QR code and state
* Bath the chemistry currently in a tank, with its own lifecycle
* Bath Log daily chemistry readings with pass/fail vs target
* KPI configurable headline metrics per shop
* Delegation Inbox single pane of "things waiting for someone"
* Facility - a physical site with its own tanks, operators, compliance profile
* Process Type - extensible taxonomy of finishing processes
* Work Center - production line or station within a facility
* Tank - physical vessel with QR code and state
* Bath - the chemistry currently in a tank, with its own lifecycle
* Bath Log - daily chemistry readings with pass/fail vs target
* KPI - configurable headline metrics per shop
* Delegation Inbox - single pane of "things waiting for someone"
Design principles
-----------------
@@ -82,7 +82,7 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
'security/fp_security.xml',
'security/fp_security_v2.xml',
'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
# implied_ids. Loads after fp_menu.xml in spirit BUT references
# 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_rack_load_sequence.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
# buckets. Every other view file (in this module and downstream)
# 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_bath_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
# kind_id) AND before fp_step_template_views.xml (the form
# 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/res_config_settings_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
# role groups, all loaded earlier (fp_menu.xml + fp_security_v2.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
# them back here would re-create deleted nodes on every
# 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").
# 'data/fp_recipe_enp_alum_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_chem_conversion.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
# daily 30-day expiry purge. Both reference model_fp_migration_preview
# 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/fp_chatter_dark.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/xml/recipe_tree_editor.xml',
'fusion_plating/static/src/xml/simple_recipe_editor.xml',

View File

@@ -15,7 +15,7 @@ class FpRecipeController(http.Controller):
"""JSON-RPC endpoints for the process recipe tree editor."""
# ------------------------------------------------------------------
# Read full tree
# Read - full tree
# ------------------------------------------------------------------
@http.route('/fp/recipe/tree', type='jsonrpc', auth='user')
def get_tree(self, recipe_id):
@@ -27,7 +27,7 @@ class FpRecipeController(http.Controller):
recipe = Node.browse(int(recipe_id))
if not recipe.exists():
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
# fusion_plating_jobs (where the model lives).
workflow_states = []
@@ -103,9 +103,9 @@ class FpRecipeController(http.Controller):
'estimated_duration',
'auto_complete', 'customer_visible', 'is_manual',
'requires_signoff', 'opt_in_out', 'sequence', 'version',
# Sub 13 sequential enforcement
# Sub 13 - sequential enforcement
'enforce_sequential', 'parallel_start',
# Sub 14 workflow milestone trigger
# Sub 14 - workflow milestone trigger
'triggers_workflow_state_id',
}
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):
"""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).
"""
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')
def move_sibling(self, node_id, direction):
@@ -212,7 +212,7 @@ class FpRecipeController(http.Controller):
# Swap the two sequence values
a_seq, b_seq = node.sequence, other.sequence
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):
s.sequence = i * 10
a_seq, b_seq = node.sequence, other.sequence
@@ -240,7 +240,7 @@ class FpRecipeController(http.Controller):
* 0 → insert at the start.
* <positive int> → insert right before that child id.
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.
Returns: {ok, imported_count, skipped_count}
@@ -251,7 +251,7 @@ class FpRecipeController(http.Controller):
if not source.exists() or not target.exists():
return {'ok': False, 'error': 'Source or target not found.'}
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()
if dedupe_by_name:
@@ -282,7 +282,7 @@ class FpRecipeController(http.Controller):
_copy_subtree(child, new_node, i * 10)
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
# reordering below.
new_top_level_ids = []
@@ -298,7 +298,7 @@ class FpRecipeController(http.Controller):
if 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
# instead of always appearing after every existing child.
target.invalidate_recordset(['child_ids'])

View File

@@ -14,7 +14,7 @@ from odoo.http import request
# 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).
_SNAPSHOT_FIELDS = [
'name', 'code', 'description', 'icon',
@@ -24,9 +24,9 @@ _SNAPSHOT_FIELDS = [
'voltage_target', 'viscosity_target',
'requires_signoff', 'requires_predecessor_done',
'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',
'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
@@ -40,7 +40,7 @@ _INPUT_SNAPSHOT_FIELDS = [
def _copy_snapshot_fields(source, fields):
"""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``
because the SQL adapter doesn't know how to serialize a recordset.
Scalar fields pass through untouched.
@@ -66,7 +66,7 @@ class SimpleRecipeController(http.Controller):
# Tree-Editor-authored recipes carry FOUR node levels:
# recipe → sub_process → operation → step
# 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
# nodes), authors saw 10 rows out of 43. Work-order generation
# 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):
"""Legacy helper returns ONLY operations.
"""Legacy helper - returns ONLY operations.
Kept for back-compat with callers and tests that asked for the
operations-only view. Most paths should now use
@@ -144,7 +144,7 @@ class SimpleRecipeController(http.Controller):
for child in node.child_ids.sorted('sequence'):
_walk(child, sub_path)
# `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.
_walk(recipe, '')
@@ -161,7 +161,7 @@ class SimpleRecipeController(http.Controller):
[recipe.process_type_id.id, recipe.process_type_id.name]
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).
'user_is_manager': request.env.user.has_group(
'fusion_plating.group_fusion_plating_manager'
@@ -169,7 +169,7 @@ class SimpleRecipeController(http.Controller):
}
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
# renders them in author order.
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,
'source_template_id': step.source_template_id.id or False,
'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),
# Sub 14 workflow milestone trigger override
# Sub 14 - workflow milestone trigger override
'triggers_workflow_state_id': (
step.triggers_workflow_state_id.id
if 'triggers_workflow_state_id' in step._fields
@@ -222,7 +222,7 @@ class SimpleRecipeController(http.Controller):
'measurements_badge_class': badge_class,
# Reference images attached to the step. Operators see
# 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': [
{
'id': att.id,
@@ -328,7 +328,7 @@ class SimpleRecipeController(http.Controller):
'requires_signoff': tpl.requires_signoff,
'requires_predecessor_done': tpl.requires_predecessor_done,
'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': (
tpl.triggers_workflow_state_id.id
if tpl.triggers_workflow_state_id else False
@@ -367,9 +367,9 @@ class SimpleRecipeController(http.Controller):
refresh in one round-trip.
"""
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
# 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.
allowed = {
'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')
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.
"""
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',
type='jsonrpc', auth='user')
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.
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
"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.
"""
Kind = request.env['fp.step.kind']
@@ -517,7 +517,7 @@ class SimpleRecipeController(http.Controller):
Auto-derives a code from the name if blank.
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
the curated catalog; adding a new kind requires manager
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 '
'catalog is curated because each kind drives gates, '
'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.'
),
}
@@ -561,12 +561,12 @@ class SimpleRecipeController(http.Controller):
@http.route('/fp/simple_recipe/workflow_states/list',
type='jsonrpc', auth='user')
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
renders left-to-right matching the status bar.
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.
"""
WS = request.env.get('fp.job.workflow.state')
@@ -594,7 +594,7 @@ class SimpleRecipeController(http.Controller):
new_vals = {
'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
# of a recipe were silently skipped pre-19.0.18.8.0.
'node_type': 'operation',
@@ -666,7 +666,7 @@ class SimpleRecipeController(http.Controller):
"""Update fields on an existing recipe step (operation node).
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))
if not node.exists():
@@ -674,7 +674,7 @@ class SimpleRecipeController(http.Controller):
node.check_access('write')
allowed = {
'name', 'description', 'icon',
'kind_id', # Sub 14b replaces default_kind
'kind_id', # Sub 14b - replaces default_kind
'requires_signoff', 'requires_predecessor_done',
'parallel_start', # Sub 13
'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
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.
New version: group node ids by their parent_id, then renumber
within each parent. Substeps stay sequenced under their
operation; operations stay sequenced under the recipe / sub-
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.
"""
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
that operation. Otherwise it falls under the operation
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 = Node.browse(int(node_id))
@@ -792,7 +792,7 @@ class SimpleRecipeController(http.Controller):
if node.node_type != 'operation':
return {'ok': False, 'error': 'not_an_operation',
'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).
if node.child_ids:
return {'ok': False, 'error': 'has_children',
@@ -864,7 +864,7 @@ class SimpleRecipeController(http.Controller):
Node = request.env['fusion.plating.process.node']
new_vals = {
'parent_id': target_recipe.id,
# See _SNAPSHOT_FIELDS comment operation, not step.
# See _SNAPSHOT_FIELDS comment - operation, not step.
'node_type': 'operation',
'sequence': src_node.sequence,
'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')
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.check_access('write')
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')
def step_remove_input(self, input_id):
"""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']
rec = Input.browse(int(input_id))
if not rec.exists():
@@ -961,7 +961,7 @@ class SimpleRecipeController(http.Controller):
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
# edit panel. Operators see them on the Record Inputs dialog and
# the step quick-look modal at runtime.

View File

@@ -1,6 +1,6 @@
<?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.
-->
<odoo noupdate="1">
@@ -35,13 +35,13 @@
<!-- ========== FACILITIES ========== -->
<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="sequence">10</field>
</record>
<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="sequence">20</field>
</record>
@@ -85,7 +85,7 @@
<!-- ========== TANKS ========== -->
<!-- EN Line -->
<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="facility_id" ref="demo_facility_main"/>
<field name="work_center_id" ref="demo_wc_en_line"/>
@@ -99,7 +99,7 @@
</record>
<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="facility_id" ref="demo_facility_main"/>
<field name="work_center_id" ref="demo_wc_en_line"/>
@@ -212,7 +212,7 @@
</record>
<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="facility_id" ref="demo_facility_main"/>
<field name="work_center_id" ref="demo_wc_anodize_line"/>

View File

@@ -2,14 +2,14 @@
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Demo recipe: Electroless Nickel Plating Steel Line
Demo recipe: Electroless Nickel Plating - Steel Line
-->
<odoo>
<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">
<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="node_type">recipe</field>
<field name="icon">fa-flask</field>

View File

@@ -1,5 +1,5 @@
<?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
on every module update. Matches the convention in fp_sequence_data.xml. -->
<odoo noupdate="1">

View File

@@ -4,7 +4,7 @@
License OPL-1 (Odoo Proprietary License v1.0)
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
on click. It resolves which window action to open in this priority
@@ -20,7 +20,7 @@
<odoo noupdate="0">
<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="state">code</field>
<field name="code"><![CDATA[

View File

@@ -5,7 +5,7 @@
Part of the Fusion Plating product family.
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.).
-->
<odoo noupdate="1">
@@ -42,7 +42,7 @@
<field name="name">Preparation</field>
<field name="code">prep</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 id="pcat_strip" model="fusion.plating.process.category">

View File

@@ -2,7 +2,7 @@
<!--
Copyright 2026 Nexa Systems Inc.
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).
Anodize is an umbrella workflow covering pre-treatment, the wet

View File

@@ -2,7 +2,7 @@
<!--
Copyright 2026 Nexa Systems Inc.
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
-->
<odoo>

View File

@@ -8,7 +8,7 @@
Notes:
- Steelhead allows a node to appear at multiple positions in the
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.
- The Electroless Nickel Plating sub-process holds the wet line;
everything inside it is a separate plating step (cleaner →
@@ -16,7 +16,7 @@
Tree:
ENP-SP (recipe)
├── Oven baking (op, signoff, auto) first oven cycle
├── Oven baking (op, signoff, auto) - first oven cycle
│ ├── Ready for bake
│ └── Bake (customer-visible)
├── Adhesion Test Coupon (op, opt-out)
@@ -43,7 +43,7 @@
│ ├── E-Nickel Plate (Hi-Phos) (SP-8) (opt-out)
│ │ └── Rinse (SP-11)
│ └── Drying
├── Oven baking (op, signoff, auto) second oven cycle (#2)
├── Oven baking (op, signoff, auto) - second oven cycle (#2)
│ ├── Ready for bake
│ └── Bake (customer-visible)
├── De-racking (op, auto)
@@ -310,7 +310,7 @@
<field name="customer_visible">True</field>
</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">
<field name="name">E-Nickel Plate (Hi-Phos) (SP-8)</field>
<field name="node_type">operation</field>
@@ -343,7 +343,7 @@
<!-- ========================= 8. Post-plate Bake (H2 Embrittlement Relief) =========================
Drives out hydrogen absorbed during plating. Must START within
~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">
<field name="name">Post-plate Bake (H2 Embrittlement Relief)</field>
<field name="node_type">operation</field>

View File

@@ -248,7 +248,7 @@
<field name="customer_visible">True</field>
</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">
<field name="name">E-Nickel Plate (Mid Phos)(S-9)</field>
<field name="node_type">operation</field>
@@ -276,7 +276,7 @@
<field name="customer_visible">True</field>
</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">
<field name="name">E-Nickel Plate (S-10)</field>
<field name="node_type">operation</field>
@@ -328,7 +328,7 @@
<!-- ========================= 6. Post-plate Bake (H2 Embrittlement Relief) =========================
Drives out hydrogen absorbed during plating. Must START within
~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">
<field name="name">Post-plate Bake (H2 Embrittlement Relief)</field>
<field name="node_type">operation</field>

View File

@@ -2,7 +2,7 @@
<!--
Copyright 2026 Nexa Systems Inc.
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).
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.
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
- "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
- Treatment Groups / "Use Price Builders" hook into pricing

View File

@@ -22,9 +22,9 @@
<field name="company_id" eval="False"/>
</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">
<field name="name">FP Move Log</field>
<field name="name">FP - Move Log</field>
<field name="code">fp.job.step.move</field>
<field name="prefix">FP/MOVE/%(year)s/</field>
<field name="padding">5</field>

View File

@@ -12,15 +12,15 @@
adhesion_test, salt_spray, packaging, gating) are kept
in this XML for history but flipped active=False by 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.
- New: `other` (catch-all, default) and `wet_process`
(covers all bath-based steps).
- `mask` covers Masking + De-Masking, `racking` covers
Racking + De-Racking operators differentiate by the
Racking + De-Racking - operators differentiate by the
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
column routing. Every record below carries an
area_kind. New `blast` kind for the Blasting column.
@@ -29,7 +29,7 @@
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">
@@ -224,7 +224,7 @@
</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 -->
@@ -407,7 +407,7 @@
<field name="kind_id" ref="step_kind_rinse"/>
<field name="name">Conductivity</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>
</record>
<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="name">Current Density</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>
</record>
<record id="step_kind_input_plate_thick" model="fp.step.kind.default.input">

View File

@@ -103,7 +103,7 @@
]]></field>
</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">
<field name="name">Hot Water Porosity Test (A-15)</field>

View File

@@ -30,7 +30,7 @@
<field name="code">plating_op</field>
<field name="sequence">30</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 id="work_role_demask" model="fp.work.role">

View File

@@ -3,12 +3,12 @@
# License OPL-1 (Odoo Proprietary License v1.0)
# 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.
Runs after fusion_plating's tables have been re-described (so the
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.
"""
@@ -32,7 +32,7 @@ def migrate(cr, version):
('fusion_plating_process_node_input', 'uom', 'process node input'),
('fusion_plating_process_node_input', 'target_unit', 'process node 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)
('fusion_plating_discharge_limit', 'uom', 'discharge limit'),
('fusion_plating_discharge_sample_line', 'uom', 'discharge sample line'),
@@ -50,7 +50,7 @@ def migrate(cr, version):
total_cleared += cleared
_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).',
total_rewritten, total_cleared, len(targets),
)

View File

@@ -3,7 +3,7 @@
# License OPL-1 (Odoo Proprietary License v1.0)
# 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
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,
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
from scratch. Without them, an empty recipe has no obvious starting
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.
"""

View File

@@ -3,7 +3,7 @@
# License OPL-1 (Odoo Proprietary License v1.0)
# 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
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'
placeholder and re-derives `kind` from `recipe_node_id.default_kind`
(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.
"""
@@ -33,7 +33,7 @@ from odoo.addons.fusion_plating import fp_resolve_step_kind
_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 = {
'cleaning': 'wet',
'etch': 'wet',

View File

@@ -3,13 +3,13 @@
# License OPL-1 (Odoo Proprietary License v1.0)
# 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:
The legacy per-step `requires_predecessor_done` opt-in defaulted to
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
WH/JOB/00339 Incoming Inspection ran while Contract Review was
WH/JOB/00339 - Incoming Inspection ran while Contract Review was
still in progress).
This migration:
@@ -19,7 +19,7 @@ This migration:
and continue to work for any recipe whose author opts back into
free-flow mode (sets enforce_sequential = False).
Idempotent safe to re-run.
Idempotent - safe to re-run.
"""
import logging
@@ -29,14 +29,14 @@ _logger = logging.getLogger(__name__)
def migrate(cr, version):
if not version:
return # Brand new install defaults already correct.
return # Brand new install - defaults already correct.
_logger.info(
'[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
# already set True (or set by a manual install of a newer version)
# are skipped so the operation is clean to re-run.

View File

@@ -2,19 +2,19 @@
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# 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
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
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`
column and points kind_id at the seeded fp.step.kind record whose code
matches.
Idempotent running it twice is a no-op.
Idempotent - running it twice is a no-op.
"""
import logging
@@ -29,7 +29,7 @@ def migrate(cr, version):
code_to_id = {code: kid for kid, code in cr.fetchall()}
if not code_to_id:
_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.',
)
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
`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.
cr.execute("""
SELECT column_name FROM information_schema.columns
@@ -89,7 +89,7 @@ def _backfill(cr, table, text_col, m2o_col, code_to_id):
return
# 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.
cr.execute(f"""
SELECT id, {text_col} FROM {table}
@@ -120,7 +120,7 @@ def _backfill(cr, table, text_col, m2o_col, code_to_id):
updated += len(ids)
_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)',
table, m2o_col, updated, skipped,
)

View File

@@ -2,10 +2,10 @@
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# 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.
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
stored related Char (`related='kind_id.code', store=True`). On first
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
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).
"""

View File

@@ -1,12 +1,12 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# 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.
2. Default `collect_measurements=True` on all existing recipe steps.
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).
4. Backfill template_input_id by name-matching against the linked
library template (best-effort).
@@ -55,7 +55,7 @@ def migrate(cr, version):
)
_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.
# Note: fusion_plating_process_node_input.name is plain varchar;
# fp_step_template_input.name is translatable JSONB (use ->>'en_US').

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# 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
----------
@@ -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)
only creates fp.job.step rows for nodes whose node_type is 'operation'.
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.
Fix
---
Promote every `step` node whose direct parent is a `recipe` to
`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'`.
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>Step nodes that were direct children of this recipe (Simple '
'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>'
'<p>If this recipe was authored via the Tree Editor with explicit '
'sub-process / operation hierarchy, this migration was a no-op '
@@ -73,7 +73,7 @@ def migrate(cr, version):
)
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
# (parent.node_type='operation') are untouched.
cr.execute("""

View File

@@ -2,7 +2,7 @@
# Copyright 2026 Nexa Systems Inc.
# 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
# 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
# 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

View File

@@ -2,7 +2,7 @@
# Copyright 2026 Nexa Systems Inc.
# 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
# required=True without choking on existing NULL rows. Three jobs:
@@ -37,7 +37,7 @@ import logging
_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
# wet-bath specialisations roll up into wet_process.
_REMAP = {
@@ -60,7 +60,7 @@ _REMAP = {
# -- Name-match heuristic for NULL backfill --------------------------------
# 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 = [
# Most specific
('qa-005', 'contract_review'),
@@ -111,7 +111,7 @@ _NAME_HEURISTIC = [
('dry', 'wet_process'),
('water break', 'wet_process'),
('wbf', 'wet_process'),
# Gating / ready / wait soft sequencers, no behaviour
# Gating / ready / wait - soft sequencers, no behaviour
('ready for', 'other'),
('ready to', 'other'),
]
@@ -127,19 +127,19 @@ def migrate(cr, version):
cr.execute("SELECT id, code FROM fp_step_kind")
by_code = {code: kid for kid, code in cr.fetchall()}
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
other_id = by_code['other']
# 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
# write both columns together.
for retired_code, new_code in _REMAP.items():
retired_id = by_code.get(retired_code)
new_id = by_code.get(new_code) or other_id
if not retired_id:
continue # not in this DB nothing to remap
continue # not in this DB - nothing to remap
cr.execute("""
UPDATE fp_step_template
SET kind_id = %s, default_kind = %s
@@ -197,7 +197,7 @@ def migrate(cr, version):
(kid, by_id.get(kid, 'other'), ids),
)
_logger.info(
'Step Kind curation: backfilled %d %s row(s) '
'Step Kind curation: backfilled %d %s row(s) - '
'distribution: %s',
len(rows), table,
{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
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,))
row = cr.fetchone()

View File

@@ -2,7 +2,7 @@
# Copyright 2026 Nexa Systems Inc.
# 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
# every routing station has a defined Floor Column on day 1. Admins can
# override afterwards via Configuration → Shop Setup → Routing Stations.

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""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),
`-u fusion_plating` after this branch lands would otherwise leave 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'
)
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(
'Failed to run role-migration preview check (non-fatal): %s', e
)

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# 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
NOT NULL constraint on the new field hits the schema. Also activates
@@ -50,14 +50,14 @@ KIND_TO_AREA = {
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.
cr.execute(
"ALTER TABLE fp_step_kind "
"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.
seeded = 0
for code, area in KIND_TO_AREA.items():
@@ -73,7 +73,7 @@ def migrate(cr, version):
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.
cr.execute(
"UPDATE fp_step_kind SET area_kind = 'plating' "
@@ -85,7 +85,7 @@ def migrate(cr, version):
cr.rowcount,
)
# Phase 4 Activate kinds we need for full coverage.
# Phase 4 - Activate kinds we need for full coverage.
activated = 0
for code in ('derack', 'demask', 'gating'):
cr.execute(

View File

@@ -1,12 +1,12 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# 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).
This script runs the same helper on every -u so existing installs
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.
"""
import logging

View File

@@ -1,14 +1,14 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# 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
fusion.plating.process.node (requires_coc, requires_thickness_report,
requires_nadcap_cert, requires_mill_test, requires_customer_specific).
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).
Idempotent: safe to re-run.

View File

@@ -2,7 +2,7 @@
# Copyright 2026 Nexa Systems Inc.
# 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
# into fusion_plating core. Re-key all related ir.model.data so the
# new module owner picks up the existing records cleanly.
@@ -14,7 +14,7 @@ _logger = logging.getLogger(__name__)
def migrate(cr, version):
if not version:
return # Fresh install nothing to migrate
return # Fresh install - nothing to migrate
patterns = [
'model_fp_work_role',

View File

@@ -28,7 +28,7 @@ from . import res_company
from . import res_users
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_cgp, etc.) that touch hr.employee can see the
# shop-roles fields without a transitive dep on jobs.
@@ -37,20 +37,20 @@ from . import fp_proficiency
from . import hr_employee
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_template
from . import fp_step_template_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_job_step_move
# Phase 1 Plating landing-page resolver
# Phase 1 - Plating landing-page resolver
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
# imports the predicate chain + xmlid maps from the former).
from . import fp_role_constants

View File

@@ -8,8 +8,8 @@ quantities, and process inputs.
Free-text unit fields invite typos ("kgs", "Kg", "kilo", "KG") that
break filters, reports, and trend graphs. Every UoM in the plating
domain chemistry, mass, volume, length, area, electrical, time,
pressure, dimensionless lives here as a curated selection so users
domain - chemistry, mass, volume, length, area, electrical, time,
pressure, dimensionless - lives here as a curated selection so users
pick from a known list instead of typing.
Re-use:
@@ -23,7 +23,7 @@ Migration:
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 = [
# --- Concentration / chemistry ---------------------------------------
('g_l', 'g/L'),
@@ -51,7 +51,7 @@ FP_UOM_SELECTION = [
('ph', 'pH'),
('su', 'SU (Standard Units)'),
('ratio', 'Ratio (e.g. 5:1)'),
('none', ' (none)'),
('none', '- (none)'),
# --- Conductivity / turbidity ----------------------------------------
('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
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
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
_logger = logging.getLogger(__name__)
_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 '',
rewritten, cleared,
)

View File

@@ -24,7 +24,7 @@ class FpBath(models.Model):
without touching the generic bath model.
"""
_name = 'fusion.plating.bath'
_description = 'Fusion Plating Bath'
_description = 'Fusion Plating - Bath'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'state, makeup_date desc, id desc'
_rec_name = 'display_name'
@@ -190,7 +190,7 @@ class FpBath(models.Model):
@api.depends('state', 'last_log_status')
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
control the final rendering.
@@ -237,7 +237,7 @@ class FpBath(models.Model):
class FpBathTarget(models.Model):
"""Per-bath target range for a chemistry parameter."""
_name = 'fusion.plating.bath.target'
_description = 'Fusion Plating Bath Target'
_description = 'Fusion Plating - Bath Target'
_order = 'bath_id, sequence, parameter_id'
bath_id = fields.Many2one(

View File

@@ -15,12 +15,12 @@ class FpBathLog(models.Model):
Each log has one or more lines (one per parameter).
Overall log status is rolled up from the lines:
* ok every line is within target
* warning at least one line is within warning tolerance
* out_of_spec at least one line is outside target
* ok - every line is within target
* warning - at least one line is within warning tolerance
* out_of_spec - at least one line is outside target
"""
_name = 'fusion.plating.bath.log'
_description = 'Fusion Plating Bath Chemistry Log'
_description = 'Fusion Plating - Bath Chemistry Log'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'log_date desc, id desc'
_rec_name = 'display_name'
@@ -118,7 +118,7 @@ class FpBathLog(models.Model):
@api.constrains('line_ids')
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
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)
if 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')
def _compute_status(self):

View File

@@ -16,7 +16,7 @@ class FpBathLogLine(models.Model):
up to the parent log.
"""
_name = 'fusion.plating.bath.log.line'
_description = 'Fusion Plating Bath Log Reading'
_description = 'Fusion Plating - Bath Log Reading'
_order = 'log_id, sequence, id'
log_id = fields.Many2one(
@@ -87,7 +87,7 @@ class FpBathLogLine(models.Model):
This means the operator (or backend user) hits "add reading", picks
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.
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'
# ------------------------------------------------------------------
# T1.2 Auto-suggest replenishment on every log line
# T1.2 - Auto-suggest replenishment on every log line
# ------------------------------------------------------------------
@api.model_create_multi
def create(self, vals_list):

View File

@@ -18,7 +18,7 @@ class FpBathParameter(models.Model):
or on the bath recipe.
"""
_name = 'fusion.plating.bath.parameter'
_description = 'Fusion Plating Bath Parameter'
_description = 'Fusion Plating - Bath Parameter'
_order = 'sequence, name'
name = fields.Char(
@@ -62,7 +62,7 @@ class FpBathParameter(models.Model):
string='Unit (display)',
compute='_compute_uom_display',
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(
string='Default Target Min',
@@ -79,7 +79,7 @@ class FpBathParameter(models.Model):
target_value = fields.Float(
string='Default Setpoint / Optimum',
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 '
'Target Max. Per-sensor override via '
'fp.tank.sensor.target_value_override.',

View File

@@ -19,7 +19,7 @@ class FpBathReplenishmentRule(models.Model):
Shops wanting non-linear or piecewise rules can extend this model.
"""
_name = 'fusion.plating.bath.replenishment.rule'
_description = 'Fusion Plating Replenishment Rule'
_description = 'Fusion Plating - Replenishment Rule'
_order = 'process_type_id, parameter_id'
name = fields.Char(string='Rule Name', required=True)
@@ -42,7 +42,7 @@ class FpBathReplenishmentRule(models.Model):
)
product_name = fields.Char(
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.product', string='Product (Inventory)',
@@ -111,7 +111,7 @@ class FpBathReplenishmentSuggestion(models.Model):
"""One suggestion generated from a bath-log reading. Operators mark
them applied or dismissed once the dose has been added."""
_name = 'fusion.plating.bath.replenishment.suggestion'
_description = 'Fusion Plating Replenishment Suggestion'
_description = 'Fusion Plating - Replenishment Suggestion'
_inherit = ['mail.thread']
_order = 'create_date desc, id desc'

View File

@@ -18,7 +18,7 @@ class FpFacility(models.Model):
model with jurisdiction-specific fields via inheritance.
"""
_name = 'fusion.plating.facility'
_description = 'Fusion Plating Facility'
_description = 'Fusion Plating - Facility'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'sequence, name'

View File

@@ -2,11 +2,11 @@
# Copyright 2026 Nexa Systems Inc.
# 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.
# 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.
#
# State machine:
@@ -52,7 +52,7 @@ class FpJob(models.Model):
return dt.astimezone(tz).strftime(fmt)
_description = 'Work Order'
_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),
# then high-priority first within each state, then nearest deadline.
# state_priority is a small stored compute below.
@@ -96,7 +96,7 @@ class FpJob(models.Model):
tracking=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
# SQL ORDER BY hits an index and doesn't recompute per row.
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
# 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
# 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
# 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()
# ------------------------------------------------------------------
# Steps One2many to fp.job.step (Task 1.5)
# Steps - One2many to fp.job.step (Task 1.5)
# ------------------------------------------------------------------
step_ids = fields.One2many(
'fp.job.step',
@@ -258,7 +258,7 @@ class FpJob(models.Model):
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."
# / "Rework" columns (screens 16-18). Sub 12c's traveller report pulls
# these into the printed header.
@@ -285,13 +285,13 @@ class FpJob(models.Model):
string='Rush Order',
tracking=True,
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.',
)
# ---- Scheduling targets mirrored from sale.order -----------------
# 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.
x_fc_internal_deadline = fields.Date(
string='Internal Deadline',
@@ -342,7 +342,7 @@ class FpJob(models.Model):
'job_id',
string='Active Timers',
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.',
)
move_ids = fields.One2many(
@@ -350,7 +350,7 @@ class FpJob(models.Model):
string='Move Log',
)
# 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
# non-stored fields share a compute method, so they get distinct
# methods below.
@@ -424,7 +424,7 @@ class FpJob(models.Model):
continue # caller set an explicit name (e.g. bulk SO confirm)
if not rec._fp_assign_parent_name():
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
# fallback path consistent across all child models.
self.env.cr.execute(
@@ -435,10 +435,10 @@ class FpJob(models.Model):
return records
# ------------------------------------------------------------------
# State machine actions
# State machine - actions
# ------------------------------------------------------------------
# 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
# cancel paths are the only enforced transitions; everything else is
# an explicit `state` write by privileged code.
@@ -450,7 +450,7 @@ class FpJob(models.Model):
) % (job.name, job.state))
job.state = 'confirmed'
# 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.
return True
@@ -458,7 +458,7 @@ class FpJob(models.Model):
for job in self:
if job.state == 'done':
raise UserError(_(
"Job %s is done cannot cancel."
"Job %s is done - cannot cancel."
) % job.name)
if job.state == 'cancelled':
raise UserError(_(

View File

@@ -2,13 +2,13 @@
# Copyright 2026 Nexa Systems Inc.
# 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
# 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
# to display hierarchy. See spec §5.2 (Option A operations only).
# to display hierarchy. See spec §5.2 (Option A - operations only).
#
# State machine:
# pending → ready → in_progress → done
@@ -83,9 +83,9 @@ class FpJobStep(models.Model):
# ------------------------------------------------------------------
# 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.
# 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
# the masking model lands (likely in fusion_plating_process_en
# or a future fusion_plating_masking module).
@@ -109,7 +109,7 @@ class FpJobStep(models.Model):
default='um',
)
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
# adjacent `bake_setpoint_temp_uom_display` compute. Hardcoding °C
# 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 '
'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
# hatches: enforce_sequential=False on the recipe (free-flow), or
# parallel_start=True on this specific step (explicit parallelism).
@@ -175,8 +175,8 @@ class FpJobStep(models.Model):
'parent recipe has enforce_sequential=True.',
)
# ===== Sub 12b chain-of-custody + rack awareness =====================
# Note: rack_id (line 95 above) already exists reused as the "current
# ===== Sub 12b - chain-of-custody + rack awareness =====================
# 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.
requires_rack_assignment = fields.Boolean(
related='recipe_node_id.requires_rack_assignment',
@@ -199,12 +199,12 @@ class FpJobStep(models.Model):
)
is_racked = fields.Boolean(
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).',
)
qty_at_step_start = fields.Integer(string='Qty at Step Start')
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
# first-step seed: the lowest-sequence step on a confirmed job
# 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):
for rec in self:
# 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.
if rec.state in ('done', 'cancelled', 'skipped'):
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
# ------------------------------------------------------------------
# State machine actions
# State machine - actions
# ------------------------------------------------------------------
# Implemented: button_start (ready/paused → in_progress),
# button_finish (in_progress → done).
@@ -308,7 +308,7 @@ class FpJobStep(models.Model):
for step in self:
if step.state != 'in_progress':
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))
now = fields.Datetime.now()
open_log = step.time_log_ids.filtered(lambda l: not l.date_finished)
@@ -318,25 +318,25 @@ class FpJobStep(models.Model):
return True
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
the state-machine logic."""
for step in self:
if step.state != 'paused':
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))
return self.button_start()
def button_skip(self):
"""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).
"""
for step in self:
if step.state not in ('pending', 'ready'):
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.state = 'skipped'
step.message_post(body=_('Step skipped by %s') % self.env.user.name)
@@ -351,7 +351,7 @@ class FpJobStep(models.Model):
for step in self:
if step.state in ('done', 'cancelled'):
raise UserError(_(
"Step '%s' is in state '%s' cannot cancel."
"Step '%s' is in state '%s' - cannot cancel."
) % (step.name, step.state))
now = fields.Datetime.now()
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:
if step.state not in ('ready', 'paused'):
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))
now = fields.Datetime.now()
step.state = 'in_progress'
@@ -372,7 +372,7 @@ class FpJobStep(models.Model):
if not step.date_started:
step.date_started = now
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
# first log share a single instant.
self.env['fp.job.step.timelog'].create({
@@ -387,17 +387,17 @@ class FpJobStep(models.Model):
for step in self:
if step.state != 'in_progress':
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))
# Quantity gate: refuses if parts still parked AND there's
# 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).
#
# Seed-only exemption: the first-step seed in
# _compute_qty_at_step gives the earliest non-terminal step
# 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.
# If the step has no REAL incoming moves, skip the gate.
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:
raise UserError(_(
"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' "
"or 'Move…' button."
) % {'name': step.name, 'n': step.qty_at_step})
@@ -453,7 +453,7 @@ class FpJobStep(models.Model):
) % step.name)
prev_state = step.state
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.
open_log = step.time_log_ids.filtered(
lambda l: not l.date_finished
@@ -484,7 +484,7 @@ class FpJobStep(models.Model):
next button_finish writes fresh first-finish stamps instead
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
computed from the sum of timelogs, not (finish - start), so the
elapsed math remains correct."""
@@ -502,7 +502,7 @@ class FpJobStep(models.Model):
prev_state = step.state
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).
open_log = step.time_log_ids.filtered(
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
# 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.
if step.state == 'done':
vals['date_finished'] = False
@@ -538,7 +538,7 @@ class FpJobStep(models.Model):
) % self.name)
if self.qty_at_step < 1:
raise UserError(_(
"No parts parked at step '%s' nothing to complete."
"No parts parked at step '%s' - nothing to complete."
) % self.name)
next_step = self.job_id.step_ids.filtered(
lambda s: s.sequence > self.sequence
@@ -546,7 +546,7 @@ class FpJobStep(models.Model):
).sorted('sequence')[:1]
if not next_step:
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 "
"instead (it will close out the job)."
) % self.name)

View File

@@ -10,14 +10,14 @@ from odoo.exceptions import UserError
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
rack moves, one-per-batch atomic) row here. Sub 12c walks these in
chronological order to render the customer CoC PDF.
"""
_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']
_order = 'move_datetime desc, id desc'
@@ -99,12 +99,12 @@ class FpJobStepMove(models.Model):
return moves
# ------------------------------------------------------------------
# S23 required transition-input gate
# S23 - required transition-input gate
# ------------------------------------------------------------------
# When the destination step has requires_transition_form=True, the
# recipe author wants chain-of-custody attestations captured on the
# 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
# _fp_check_transition_inputs_complete() after writing values to
# transition_input_value_ids.
@@ -117,7 +117,7 @@ class FpJobStepMove(models.Model):
def _fp_missing_required_transition_inputs(self):
"""Return the recordset of required transition_input prompts on
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."""
self.ensure_one()
Prompt = self.env['fusion.plating.process.node.input']
@@ -145,7 +145,7 @@ class FpJobStepMove(models.Model):
def _fp_check_transition_inputs_complete(self):
"""Raise UserError when the destination step has
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
._fp_check_signoff_complete (S22).
@@ -160,7 +160,7 @@ class FpJobStepMove(models.Model):
continue
move.message_post(body=Markup(_(
'Transition-form gate bypassed by %s. '
'Documented deviation required prompts not '
'Documented deviation - required prompts not '
'recorded on this move.'
)) % self.env.user.name)
return
@@ -172,7 +172,7 @@ class FpJobStepMove(models.Model):
'"%s"' % (p.name or '').strip() for p in missing
)
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: '
'%(names)s. Fill them in the Move dialog before '
'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.
"""
_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'
move_id = fields.Many2one('fp.job.step.move', string='Move',

View File

@@ -2,7 +2,7 @@
# Copyright 2026 Nexa Systems Inc.
# 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
# step.button_finish() (or button_pause once added) closes the open
@@ -55,7 +55,7 @@ class FpJobStepTimeLog(models.Model):
rec_bits.append(mins)
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
# field + reconciliation columns. Default state='running' → existing
# 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.
Clears date_finished, last_paused_at, total_paused_seconds so accrued
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(
'fusion_plating.group_fusion_plating_manager'):
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
# leaves the step's "Actual Min" column showing stale data.
# ------------------------------------------------------------------

View File

@@ -2,28 +2,28 @@
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
"""Phase 1 + Phase E Plating landing-page resolver.
"""Phase 1 + Phase E - Plating landing-page resolver.
Layers:
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,
Quotations, Manager Desk, Plant Kanban, Quality Dashboard, etc.) so
the landing-page dropdown only offers sensible options, not all 200+
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
``ir.actions.act_window`` (only act_window actions can be selected
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
the user's actually-accessible actions (Technician can't pick
"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
spec. Returns an action dict suitable for the
``action_fp_resolve_plating_landing`` server action.
@@ -58,7 +58,7 @@ class IrActionsActions(models.Model):
)
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
and client actions resolve correctly."""
self.ensure_one()
@@ -66,7 +66,7 @@ class IrActionsActions(models.Model):
return self.env['ir.actions.client'].browse(self.id)._render_resolved()
if self.type == 'ir.actions.act_window':
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.pop('id', 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'
# ------------------------------------------------------------------
# Resolver role-based dispatch (Phase E)
# Resolver - role-based dispatch (Phase E)
# ------------------------------------------------------------------
@api.model
def _fp_resolve_landing_for_current_user(self):
@@ -108,7 +108,7 @@ class IrActionsActWindow(models.Model):
and company.x_fc_default_landing_action_id:
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(
'fusion_plating_configurator.action_fp_sale_orders',
raise_if_not_found=False,
@@ -152,7 +152,7 @@ class IrActionsActWindow(models.Model):
Returns ``action_fp_plant_kanban`` (the 2026-05-23 plant view).
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
survives orphaned for one release cycle so we can ship a
settings-UI cleanup separately; flipping it has no effect.
@@ -179,7 +179,7 @@ class IrActionsActWindow(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
actions, not act_window records.
@@ -189,7 +189,7 @@ class IrActionsClient(models.Model):
"""
_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.
def _render_resolved(self):

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
"""Phase H dry-run + Owner-approval migration workflow."""
"""Phase H - dry-run + Owner-approval migration workflow."""
import json
import logging
from datetime import timedelta
@@ -96,7 +96,7 @@ class FpMigrationPreview(models.Model):
def _fp_notify_owners(self):
"""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()
owner_grp = self.env.ref('fusion_plating.group_fp_owner', raise_if_not_found=False)
if not owner_grp:
@@ -214,7 +214,7 @@ class FpMigrationPreview(models.Model):
# Clear snapshots (no more rollback possible)
for preview in expired:
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)
old_group_ids = []
for xmlid in _FP_OLD_GROUP_XMLIDS:
@@ -222,7 +222,7 @@ class FpMigrationPreview(models.Model):
if g:
old_group_ids.append(g.id)
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
# we'd cascade-strip them silently from their permissions.
safe_to_unlink = []

View File

@@ -15,7 +15,7 @@ class FpOperatorCertification(models.Model):
for that process.
"""
_name = 'fp.operator.certification'
_description = 'Fusion Plating Operator Certification'
_description = 'Fusion Plating - Operator Certification'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'employee_id, process_type_id'
@@ -53,7 +53,7 @@ class FpOperatorCertification(models.Model):
('revoked', 'Revoked')],
string='Status',
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.
)
@@ -85,7 +85,7 @@ class FpOperatorCertification(models.Model):
continue
if rec.expires_date and rec.expires_date < today:
continue
# This record is active look for another active sibling
# This record is active - look for another active sibling
dupes = self.search_count([
('id', '!=', rec.id),
('employee_id', '=', rec.employee_id.id),
@@ -108,7 +108,7 @@ class FpOperatorCertification(models.Model):
@api.model
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
`state` column, so even a certification that expired yesterday

View File

@@ -84,21 +84,21 @@ class FpParentNumberedMixin(models.AbstractModel):
# downstream modules (e.g. fusion_plating_receiving) inherit the
# mixin but don't depend on jobs, so so.x_fc_parent_number can
# 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.
if not so or 'x_fc_parent_number' not in so._fields:
return False
if not so.x_fc_parent_number:
return False
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
# subclasses return a literal; this guard exists so a future
# subclass that reads the field name from context / Selection /
# user input can't smuggle a SQL fragment in.
if not _FP_COUNTER_FIELD_RE.match(counter_field or ''):
raise UserError(_(
'Invalid parent-counter field name %r must match '
'Invalid parent-counter field name %r - must match '
'pattern x_fc_pn_*_count.'
) % counter_field)
# 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
# keeps the issued number tied to its cancellation reason. Hard
# 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):
for rec in self:
# 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
# name is something other than 'New' / '/', the record
# has been issued and is permanent.

View File

@@ -14,7 +14,7 @@ class FpProcessCategory(models.Model):
load specific process types.
"""
_name = 'fusion.plating.process.category'
_description = 'Fusion Plating Process Category'
_description = 'Fusion Plating - Process Category'
_order = 'sequence, name'
name = fields.Char(

View File

@@ -19,15 +19,15 @@ class FpProcessNode(models.Model):
Node types
----------
* recipe top-level root (e.g. "Electroless Nickel Steel Line")
* sub_process a group of operations (e.g. "Steel Line", "Cleaner")
* operation a single production step (e.g. "Acid Dip", "Nickel Strike")
* step a sub-step within an operation (e.g. "Ready for Blast", "Blast")
* recipe - top-level root (e.g. "Electroless Nickel - Steel Line")
* sub_process - a group of operations (e.g. "Steel Line", "Cleaner")
* operation - a single production step (e.g. "Acid Dip", "Nickel Strike")
* step - a sub-step within an operation (e.g. "Ready for Blast", "Blast")
Hierarchy uses Odoo's _parent_store for efficient tree queries.
"""
_name = 'fusion.plating.process.node'
_description = 'Fusion Plating Process Node'
_description = 'Fusion Plating - Process Node'
_inherit = ['mail.thread', 'mail.activity.mixin']
_parent_store = True
_parent_name = 'parent_id'
@@ -108,7 +108,7 @@ class FpProcessNode(models.Model):
string='Description',
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).
collect_measurements = fields.Boolean(
string='Collect Measurements at Runtime',
@@ -178,7 +178,7 @@ class FpProcessNode(models.Model):
# Recipe authors attach photos and screenshots here so operators see
# them on the shop floor when running the step. Anything from a
# 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(
'ir.attachment',
'fp_node_instruction_attachment_rel',
@@ -187,7 +187,7 @@ class FpProcessNode(models.Model):
domain=[('mimetype', 'ilike', 'image/')],
help='Reference photos and screenshots that operators see at '
'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.',
)
instruction_attachment_count = fields.Integer(
@@ -227,7 +227,7 @@ class FpProcessNode(models.Model):
requires_signoff = fields.Boolean(
string='Requires Sign-Off',
default=False,
help='Quality hold point requires operator sign-off.',
help='Quality hold point - requires operator sign-off.',
)
requires_predecessor_done = fields.Boolean(
string='Requires Predecessor Done (legacy)',
@@ -235,16 +235,16 @@ class FpProcessNode(models.Model):
help='LEGACY per-step opt-in for predecessor enforcement. As of '
'19.0.X, recipes default to enforce_sequential=True so every '
'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 '
'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
# 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.
# 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
# (e.g. paperwork that doesn't need previous step done).
enforce_sequential = fields.Boolean(
@@ -286,10 +286,10 @@ class FpProcessNode(models.Model):
default='disabled',
help='Controls whether this step can be skipped or added on a '
'per-job basis:\n'
' * Required every job runs this step. Cannot be removed.\n'
' * Opt-Out included by default; an estimator can remove '
' * Required - every job runs this step. Cannot be removed.\n'
' * Opt-Out - included by default; an estimator can remove '
'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.',
tracking=True,
)
@@ -314,7 +314,7 @@ class FpProcessNode(models.Model):
# ---- Part ownership & provenance (Sub 3) --------------------------------
# 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
# module). Adding them here would create a circular dependency.
# 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
# (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
# 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.
phosphorus_level = fields.Selection(
[('low_phos', 'Low Phosphorus (2-5%)'),
@@ -375,12 +375,12 @@ class FpProcessNode(models.Model):
[('mils', 'mils'), ('microns', 'microns'), ('inches', 'inches')],
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.
# "0.0005-0.0008 mils") that auto-fills from last-used per
# (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(
string='Requires Bake Relief',
help='Hydrogen embrittlement relief bake required (high-strength '
@@ -454,7 +454,7 @@ class FpProcessNode(models.Model):
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
# 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',
ondelete='set null',
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 '
'this node (snapshot semantics).',
)
@@ -499,14 +499,14 @@ class FpProcessNode(models.Model):
viscosity_target = fields.Float(string='Viscosity Target')
requires_rack_assignment = fields.Boolean(
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(
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).
# Default True for all five so existing recipes keep producing the
# same cert set they produce today. A recipe author flips OFF only
@@ -532,7 +532,7 @@ class FpProcessNode(models.Model):
default=True,
help='When False, this recipe never produces a thickness report. '
'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.',
)
requires_nadcap_cert = fields.Boolean(
@@ -555,8 +555,8 @@ class FpProcessNode(models.Model):
'cert.',
)
# Sub 14b User-extensible Step Kinds (was Selection of 24).
# 2026-05-20: required + ondelete='restrict' kind drives gates,
# Sub 14b - User-extensible Step Kinds (was Selection of 24).
# 2026-05-20: required + ondelete='restrict' - kind drives gates,
# workflow milestones, and operator routing. Optional was a foot-gun
# (operators silently picked Generic / nothing). Pre-migrate
# 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
# 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
# avoid an infinite write loop.
_FP_NON_VERSIONED_FIELDS = {
@@ -656,7 +656,7 @@ class FpProcessNode(models.Model):
the current recordset."""
roots = self.mapped('recipe_root_id')
# _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:
if not rec.recipe_root_id and rec.node_type == 'recipe':
roots |= rec
@@ -676,7 +676,7 @@ class FpProcessNode(models.Model):
@api.model_create_multi
def create(self, 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.
descendants = records.filtered(lambda r: r.node_type != 'recipe')
if descendants:
@@ -684,7 +684,7 @@ class FpProcessNode(models.Model):
return records
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
# so the lock can be toggled off.
if (self
@@ -759,11 +759,11 @@ class FpProcessNode(models.Model):
'customer_visible': self.customer_visible,
'is_manual': self.is_manual,
'requires_signoff': self.requires_signoff,
# Sub 13 sequential enforcement
# Sub 13 - sequential enforcement
'enforce_sequential': self.enforce_sequential,
'parallel_start': self.parallel_start,
'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': (
self.triggers_workflow_state_id.id
if 'triggers_workflow_state_id' in self._fields
@@ -817,7 +817,7 @@ class FpProcessNode(models.Model):
return {
'type': 'ir.actions.client',
'tag': 'fp_recipe_tree_editor',
'name': f'Recipe {root.name}',
'name': f'Recipe - {root.name}',
'context': {'recipe_id': root.id},
}
@@ -828,7 +828,7 @@ class FpProcessNode(models.Model):
return {
'type': 'ir.actions.client',
'tag': 'fp_simple_recipe_editor',
'name': f'Recipe {root.name}',
'name': f'Recipe - {root.name}',
'context': {'recipe_id': root.id},
}
@@ -846,7 +846,7 @@ class FpProcessNode(models.Model):
def action_open_recipe_with_preferred_editor(self):
"""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
preference without forcing a tree-loving engineer to pick
between two buttons every time.
@@ -930,10 +930,10 @@ class FpProcessNodeInput(models.Model):
"""An operator input definition attached to a process node.
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'
_description = 'Fusion Plating Process Node Input'
_description = 'Fusion Plating - Process Node Input'
_order = 'sequence, id'
name = fields.Char(
@@ -954,7 +954,7 @@ class FpProcessNodeInput(models.Model):
('boolean', 'Yes / No'),
('selection', 'Selection'),
('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_seconds', 'Time (seconds)'),
('temperature', 'Temperature'),
@@ -992,11 +992,11 @@ class FpProcessNodeInput(models.Model):
uom = fields.Selection(
FP_UOM_SELECTION,
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).',
)
# ===== Sub 12a kind + target ranges + compliance tag ==================
# ===== Sub 12a - kind + target ranges + compliance tag ==================
kind = fields.Selection(
[
('step_input', 'Step Measurement'),
@@ -1036,7 +1036,7 @@ class FpProcessNodeInput(models.Model):
string='Compliance Tag', default='none',
)
# ===== Sub 12d per-recipe configurability =============================
# ===== Sub 12d - per-recipe configurability =============================
collect = fields.Boolean(
string='Collect This Measurement',
default=True,
@@ -1049,7 +1049,7 @@ class FpProcessNodeInput(models.Model):
string='Source Library Prompt',
ondelete='set null',
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 '
'the reset.',
)

View File

@@ -19,14 +19,14 @@ class FpProcessType(models.Model):
and linked here via parameter_ids.
"""
_name = 'fusion.plating.process.type'
_description = 'Fusion Plating Process Type'
_description = 'Fusion Plating - Process Type'
_order = 'sequence, name'
name = fields.Char(
string='Process',
required=True,
translate=True,
help='Display name (e.g. "Electroless Nickel Mid Phosphorus").',
help='Display name (e.g. "Electroless Nickel - Mid Phosphorus").',
)
code = fields.Char(
string='Code',

View File

@@ -3,7 +3,7 @@
# License OPL-1 (Odoo Proprietary License v1.0)
# 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.
from markupsafe import Markup
@@ -12,7 +12,7 @@ from odoo import _, api, fields, models
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
role's mastery threshold is crossed.
@@ -23,7 +23,7 @@ class FpOperatorProficiency(models.Model):
in a form; their growing skill set just unlocks itself.
"""
_name = 'fp.operator.proficiency'
_description = 'Fusion Plating Operator Task Proficiency'
_description = 'Fusion Plating - Operator Task Proficiency'
_rec_name = 'display_name'
_order = 'employee_id, role_id'
@@ -55,7 +55,7 @@ class FpOperatorProficiency(models.Model):
index=True,
help='True once the role has been added to the operator\'s Shop '
'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.',
)
promoted_at = fields.Datetime(
@@ -83,7 +83,7 @@ class FpOperatorProficiency(models.Model):
def _compute_display_name(self):
for rec in self:
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')
@@ -99,7 +99,7 @@ class FpOperatorProficiency(models.Model):
def _record_completion(self, employee, role):
"""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
have write access to their own profile.
"""
@@ -151,7 +151,7 @@ class FpOperatorProficiency(models.Model):
})
employee.message_post(
body=Markup(_(
'<b>%(name)s promoted</b> qualified for '
'<b>%(name)s promoted</b> - qualified for '
'<b>%(role)s</b> after %(count)s successful '
'completions.'
)) % {

View File

@@ -15,7 +15,7 @@ class FpRack(models.Model):
on parts.
"""
_name = 'fusion.plating.rack'
_description = 'Fusion Plating Rack / Fixture'
_description = 'Fusion Plating - Rack / Fixture'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'facility_id, rack_type, name'
@@ -79,7 +79,7 @@ class FpRack(models.Model):
def _compute_state(self):
for rec in self:
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:
rec.state = 'needs_strip'
elif rec.state != 'active':
@@ -116,7 +116,7 @@ class FpRack(models.Model):
for rec in self:
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(
[
('empty', 'Empty'),
@@ -136,8 +136,8 @@ class FpRack(models.Model):
string='Tags',
)
capacity_count = fields.Integer(
string='Capacity (parts) soft warn',
help='Soft warning threshold runtime informs operator when '
string='Capacity (parts) - soft warn',
help='Soft warning threshold - runtime informs operator when '
'rack is loaded beyond this. Not enforced. Distinct from '
'`capacity` field (planning capacity).',
)

View File

@@ -3,7 +3,7 @@
# License OPL-1 (Odoo Proprietary License v1.0)
# 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
# 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)
# ------------------------------------------------------------------
# Pure division math (no DB) verifiable in isolation.
# Pure division math (no DB) - verifiable in isolation.
# ------------------------------------------------------------------
@api.model
def _fp_equal_split(self, total, n):

View File

@@ -9,12 +9,12 @@ from odoo import fields, models
class FpRackTag(models.Model):
"""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
plant-overview rack rows. M2M; one rack can carry many tags.
"""
_name = 'fp.rack.tag'
_description = 'Fusion Plating Rack Tag'
_description = 'Fusion Plating - Rack Tag'
_order = 'sequence, name'
name = fields.Char(string='Tag', required=True, translate=True)

View File

@@ -39,7 +39,7 @@ _NEW_ROLE_XMLID = {
# Predicate is a callable taking a res.users record; returns bool.
_FP_ROLE_MAPPING_RULES = [
# 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.
# If admin matched first, the DO field would never get populated for shops
# where the admin is also the registered PSPC Designated Official.

View File

@@ -16,7 +16,7 @@ class FpStepKind(models.Model):
inputs that get seeded onto a step template when the kind is picked.
"""
_name = 'fp.step.kind'
_description = 'Fusion Plating Step Kind'
_description = 'Fusion Plating - Step Kind'
_order = 'sequence, name'
code = fields.Char(
@@ -34,7 +34,7 @@ class FpStepKind(models.Model):
string='Icon',
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.
# Replaces the hardcoded _STEP_KIND_TO_AREA dict (removed from
# fusion_plating_jobs/models/fp_job_step.py). Pre-migrate
@@ -56,7 +56,7 @@ class FpStepKind(models.Model):
index=True,
help='Determines which column on the Shop Floor plant kanban shows '
'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.',
)
company_id = fields.Many2one(
@@ -79,7 +79,7 @@ class FpStepKind(models.Model):
]
# 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.
# FA4 ships with Odoo (no extra deps). Key = CSS class, Value = label.
_ICON_SELECTION = [
@@ -258,7 +258,7 @@ class FpStepKind(models.Model):
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': _('Step Templates %s') % self.name,
'name': _('Step Templates - %s') % self.name,
'res_model': 'fp.step.template',
'view_mode': 'list,form',
'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
'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'
_description = 'Fusion Plating Step Kind Default Input'
_description = 'Fusion Plating - Step Kind Default Input'
_order = 'sequence, name'
name = fields.Char(string='Name', required=True, translate=True)

View File

@@ -10,13 +10,13 @@ class FpStepTemplate(models.Model):
"""Reusable step template for the Simple Recipe Editor.
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
`fusion.plating.process.node` so a snapshot copy is a 1:1 field
transfer.
"""
_name = 'fp.step.template'
_description = 'Fusion Plating Step Library Template'
_description = 'Fusion Plating - Step Library Template'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'sequence, name'
@@ -69,7 +69,7 @@ class FpStepTemplate(models.Model):
requires_predecessor_done = fields.Boolean(
string='Require Predecessor Done (legacy)',
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.',
)
parallel_start = fields.Boolean(
@@ -79,7 +79,7 @@ class FpStepTemplate(models.Model):
'earlier-sequence steps are still in progress (e.g. '
'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
# the target model (fp.job.workflow.state) is defined in jobs, and
# 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',
help='Opens the transition form before Mark Done (Sub 12b).')
# Sub 14b User-extensible Step Kinds (was Selection of 24).
# 2026-05-20: required same rationale as on fusion.plating.process.node
# Sub 14b - User-extensible Step Kinds (was Selection of 24).
# 2026-05-20: required - same rationale as on fusion.plating.process.node
# (kind drives every downstream gate / milestone / routing decision).
kind_id = fields.Many2one(
'fp.step.kind', string='Step Kind', ondelete='restrict',
@@ -102,7 +102,7 @@ class FpStepTemplate(models.Model):
'milestones / routing when authors instantiate the template. '
'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
# search domains [('default_kind', '=', 'cleaning')] still hit an
# indexed column.
@@ -148,7 +148,7 @@ class FpStepTemplate(models.Model):
return super().write(vals)
# ----- 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
# 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
@@ -205,7 +205,7 @@ class FpStepTemplate(models.Model):
{'name': 'Rinse Type', 'input_type': 'selection', 'sequence': 10,
'selection_options': 'cascade,spray,DI,city'},
{'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',
'target_unit': 's', 'sequence': 30},
],
@@ -232,7 +232,7 @@ class FpStepTemplate(models.Model):
{'name': 'Bath Concentration', 'input_type': 'number', 'sequence': 50,
'hint': 'g/L'},
{'name': 'Current Density', 'input_type': 'number', 'sequence': 60,
'hint': 'ASF electroplate only'},
'hint': 'ASF - electroplate only'},
{'name': 'Plating Thickness', 'input_type': 'multi_point_thickness',
'target_unit': 'in', 'sequence': 70},
],
@@ -414,7 +414,7 @@ class FpStepTemplate(models.Model):
return True
# 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 = (
'name', 'input_type', 'target_unit', 'required',
'hint', 'selection_options', 'sequence',
@@ -422,12 +422,12 @@ class FpStepTemplate(models.Model):
def action_seed_default_inputs(self):
"""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.
Falls back to the legacy DEFAULT_INPUTS_BY_KIND dict if the
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
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}
specs.append(spec)
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, [])
for spec in specs:
if spec['name'] in existing_names:

View File

@@ -16,7 +16,7 @@ class FpStepTemplateInput(models.Model):
step.
"""
_name = 'fp.step.template.input'
_description = 'Fusion Plating Step Template Input'
_description = 'Fusion Plating - Step Template Input'
_order = 'sequence, name'
name = fields.Char(string='Name', required=True, translate=True)

View File

@@ -15,7 +15,7 @@ class FpStepTemplateTransitionInput(models.Model):
into a recipe. Sub 12b uses these to render the Move Parts dialog.
"""
_name = 'fp.step.template.transition.input'
_description = 'Fusion Plating Step Template Transition Input'
_description = 'Fusion Plating - Step Template Transition Input'
_order = 'sequence, name'
name = fields.Char(string='Name', required=True, translate=True)

View File

@@ -17,7 +17,7 @@ class FpTank(models.Model):
shop-floor station.
"""
_name = 'fusion.plating.tank'
_description = 'Fusion Plating Tank'
_description = 'Fusion Plating - Tank'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'facility_id, section_id, sequence, code'
@@ -228,7 +228,7 @@ class FpTank(models.Model):
@api.onchange('current_bath_process_id')
def _onchange_seed_current_process(self):
"""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."""
for rec in self:
if not rec.current_process_id and rec.current_bath_process_id:

View File

@@ -19,7 +19,7 @@ class FpTankComposition(models.Model):
percentages. Changes to ingredients are also chatter-tracked.
"""
_name = 'fusion.plating.tank.composition'
_description = 'Fusion Plating Tank Composition'
_description = 'Fusion Plating - Tank Composition'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'tank_id, sequence, name'
@@ -31,7 +31,7 @@ class FpTankComposition(models.Model):
code = fields.Char(
string='Code',
tracking=True,
help='Short identifier "A", "B", "C".',
help='Short identifier - "A", "B", "C".',
)
sequence = fields.Integer(
string='Sequence',
@@ -114,7 +114,7 @@ class FpTankCompositionIngredient(models.Model):
composition; total % roll-up lives on the parent composition.
"""
_name = 'fusion.plating.tank.composition.ingredient'
_description = 'Fusion Plating Tank Composition Ingredient'
_description = 'Fusion Plating - Tank Composition Ingredient'
_order = 'composition_id, sequence, id'
composition_id = fields.Many2one(
@@ -166,7 +166,7 @@ class FpTankCompositionIngredient(models.Model):
records = super().create(vals_list)
for rec in records:
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,
'pct': rec.percentage,
@@ -198,7 +198,7 @@ class FpTankCompositionIngredient(models.Model):
changed.append(_('unit: %s%s') % (before.get('uom'), rec.uom))
if changed:
rec.composition_id.message_post(body=_(
'Ingredient %(name)s updated %(changes)s'
'Ingredient %(name)s updated - %(changes)s'
) % {
'name': rec.name,
'changes': '; '.join(changed),
@@ -208,7 +208,7 @@ class FpTankCompositionIngredient(models.Model):
def unlink(self):
for rec in self:
rec.composition_id.message_post(body=_(
'Ingredient removed: %(name)s %(pct)s'
'Ingredient removed: %(name)s - %(pct)s'
) % {
'name': rec.name,
'pct': rec.percentage,

View File

@@ -11,12 +11,12 @@ class FpTankSection(models.Model):
"Specialty Line").
Sections give the shop a familiar way to slice the tank list: every shop
organises its tanks differently by metal, by chemistry family, by
physical aisle, or by customer programme and a fixed taxonomy never
organises its tanks differently - by metal, by chemistry family, by
physical aisle, or by customer programme - and a fixed taxonomy never
fits. Sections are free-form, renameable, and per-facility.
"""
_name = 'fusion.plating.tank.section'
_description = 'Fusion Plating Tank Section'
_description = 'Fusion Plating - Tank Section'
_order = 'sequence, name'
name = fields.Char(

View File

@@ -5,7 +5,7 @@
"""Timezone helpers for Fusion Plating.
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.
Resolution order for "what timezone does this user see":
@@ -140,7 +140,7 @@ def detect_default_tz(env=None):
if partner_tz:
return partner_tz
# Server-side detection works on most Linux hosts.
# Server-side detection - works on most Linux hosts.
try:
from datetime import datetime
local = datetime.now().astimezone()

View File

@@ -9,14 +9,14 @@ from odoo import fields, models
class FpWorkCenter(models.Model):
"""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
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.
"""
_name = 'fusion.plating.work.center'
_description = 'Fusion Plating Production Line'
_description = 'Fusion Plating - Production Line'
_order = 'facility_id, sequence, name'
name = fields.Char(
@@ -58,7 +58,7 @@ class FpWorkCenter(models.Model):
)
capacity_per_day = fields.Float(
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 = [

View File

@@ -2,10 +2,10 @@
# Copyright 2026 Nexa Systems Inc.
# 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
# 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
# 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):
"""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.
Each routing station has a `kind` (wet_line / bake / mask / rack /
@@ -62,7 +62,7 @@ class FpWorkCentre(models.Model):
],
string='Floor Column',
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. '
'See docs/superpowers/specs/2026-05-23-shopfloor-plant-view-'
'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_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,
# which the core module cannot depend on. The bridge module that
# introduces fp.job/fp.job.step (Task 1.x) can re-introduce this
# field via _inherit if/when the bake-oven coupling is needed.
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
# endpoint caches the payload for 60s anyway so the cost is bounded).
bottleneck_score = fields.Float(
compute='_compute_bottleneck',
string='Bottleneck Score',
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.',
)
avg_wait_minutes = fields.Float(

View File

@@ -3,7 +3,7 @@
# License OPL-1 (Odoo Proprietary License v1.0)
# 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.
from odoo import api, fields, models
@@ -19,11 +19,11 @@ class FpWorkRole(models.Model):
role each.
- 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.
"""
_name = 'fp.work.role'
_description = 'Fusion Plating Shop Work Role'
_description = 'Fusion Plating - Shop Work Role'
_order = 'sequence, code'
name = fields.Char(string='Role Name', required=True, translate=True)

View File

@@ -14,7 +14,7 @@ class HrEmployee(models.Model):
of them. A small shop where the owner wears every hat just tags
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
when the regular owner is absent or behind. The Manager Desk
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
# 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.
# ------------------------------------------------------------------
x_fc_is_clocked_in = fields.Boolean(
@@ -70,7 +70,7 @@ class HrEmployee(models.Model):
"""Compute attendance status from hr.attendance.
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:
return
@@ -96,7 +96,7 @@ class HrEmployee(models.Model):
1. Odoo 19 normalises ``('=', True)`` into
``('in', OrderedSet([True]))`` before invoking the search
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
row.
@@ -110,7 +110,7 @@ class HrEmployee(models.Model):
on the cached open-attendance employee ids. Variable signature
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:
_records, operator, value = args
elif len(args) == 2:

View File

@@ -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(
[('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(
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.
# Stored as Many2one to res.users so the link survives renames.

View File

@@ -56,14 +56,14 @@ class ResConfigSettings(models.TransientModel):
readonly=False, string='Area Unit',
)
# ----- Sub 12a recipe editor default ------------------------------
# ----- Sub 12a - recipe editor default ------------------------------
x_fc_default_recipe_editor = fields.Selection(
related='company_id.x_fc_default_recipe_editor',
readonly=False,
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
# was widened to ir.actions.actions in the post-deploy fixes so the
# picker accepts both window AND client actions (Manager Desk, Plant

View File

@@ -23,7 +23,7 @@ _FP_PLATING_ROLE_TO_GROUP_XMLID = {
'owner': 'fusion_plating.group_fp_owner',
}
# Highest precedence first first match wins
# Highest precedence first - first match wins
_FP_ROLE_PRECEDENCE = (
'owner', 'quality_manager', 'manager', 'sales_manager',
'shop_manager', 'sales_rep', 'technician',
@@ -35,7 +35,7 @@ class ResUsers(models.Model):
# Allow non-admin users to write their OWN plating-related fields
# 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
def SELF_WRITEABLE_FIELDS(self):
@@ -99,7 +99,7 @@ class ResUsers(models.Model):
role_to_group[role] = grp
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.
# `user._origin.x_fc_plating_role` returns the IN-CACHE new value
# (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'
new_role = user.x_fc_plating_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.
continue

View File

@@ -5,10 +5,10 @@ then create an SO for ABC Manufacturing and confirm it so the
operator-facing job is ready to run.
After running, the user navigates to:
- Recipe form (Process Recipes menu) verify instructions present
- Simple Recipe Editor verify per-step Instructions + Measurements
- Sale Orders verify the new SO with line referencing the recipe
- Plating Jobs verify job created with all steps
- Recipe form (Process Recipes menu) - verify instructions present
- Simple Recipe Editor - verify per-step Instructions + Measurements
- Sale Orders - verify the new SO with line referencing the recipe
- Plating Jobs - verify job created with all steps
- Click Mark Done on any step → verify operator wizard shows
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)
prior_recipes = Node.search([
'|', ('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:
print('Cleaning up %d prior recipe(s)...' % len(prior_recipes))
@@ -53,7 +53,7 @@ recipe = Node.create({
'is_template': True,
'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>Tolerances: 0.0015"0.0020" coating thickness, 60kV breakdown
<p>Tolerances: 0.0015"-0.0020" coating thickness, 60kV breakdown
voltage. Hardness ≥350HV300.</p>''',
})
print('Created recipe:', recipe.id, recipe.name)
@@ -100,14 +100,14 @@ STEPS = [
{
'name': '5. Alkaline Clean (Tank A-1)',
'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>
<li>Time: 46 minutes</li>
<li>Temperature: 140160°F</li>
<li>Time: 4-6 minutes</li>
<li>Temperature: 140-160°F</li>
<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',
'description': '''<p><strong>Triple cascade DI rinse.</strong></p>
<ul><li>Tank A-2 (DI rinse, conductivity &lt; 50 µS/cm)</li>
@@ -117,47 +117,47 @@ STEPS = [
{
'name': '7. Etch (Tank A-3)',
'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>
<li>Time: 3090 seconds (per drawing heavy etch removes 0.0005"/side)</li>
<li>Temperature: 130150°F</li>
<li>Concentration: 46 oz/gal NaOH</li>
<li>HE-risk parts (high-strength) require post-bake flag accordingly</li></ul>''',
<li>Time: 30-90 seconds (per drawing - heavy etch removes 0.0005"/side)</li>
<li>Temperature: 130-150°F</li>
<li>Concentration: 4-6 oz/gal NaOH</li>
<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',
'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)',
'kind': 'etch',
'description': '''<p><strong>Acid desmut to remove black smut from etch.</strong></p>
<ul><li>Tank: A-5, Bath: DEOX-1 (HNO3-based)</li>
<li>Time: 3060 seconds</li>
<li>Time: 30-60 seconds</li>
<li>Temperature: ambient</li>
<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',
'description': '<p>Final pre-anodize rinse Tank A-6. Conductivity must be &lt; 50 µS/cm.</p>',
'description': '<p>Final pre-anodize rinse - Tank A-6. Conductivity must be &lt; 50 µS/cm.</p>',
},
{
'name': '11. Hard Anodize Type III (Tank A-9)',
'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>
<li>Temperature: 2832°F (chilled bath confirm chiller is running)</li>
<li>Current density: 2436 ASF</li>
<li>Voltage ramp: 080V over first 5 minutes</li>
<li>Temperature: 28-32°F (chilled bath - confirm chiller is running)</li>
<li>Current density: 24-36 ASF</li>
<li>Voltage ramp: 0-80V over first 5 minutes</li>
<li>Time at voltage: 60 minutes (gives ~0.002" coating)</li>
<li>Record amperage every 15 minutes</li>
<li>Check thickness midway with Fischerscope on witness coupon</li>
<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',
'description': '<p>Cold cascade rinse to remove sulfuric residue. Tank A-12.</p>',
},
@@ -166,24 +166,24 @@ STEPS = [
'kind': 'plate',
'description': '''<p><strong>Sulfo Black BL dye absorption.</strong></p>
<ul><li>Tank: A-14, Bath: DYE-BL-1</li>
<li>Temperature: 130150°F</li>
<li>Time: 1218 minutes</li>
<li>Maintain pH 5.06.0</li>
<li>Temperature: 130-150°F</li>
<li>Time: 12-18 minutes</li>
<li>Maintain pH 5.0-6.0</li>
<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',
'description': '<p>Warm rinse before sealing. Tank A-15. ~110°F.</p>',
},
{
'name': '15. Hot Nickel Acetate Seal (Tank A-16)',
'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>
<li>Temperature: 195205°F</li>
<li>Time: 1822 minutes</li>
<li>pH: 5.56.0</li>
<li>Temperature: 195-205°F</li>
<li>Time: 18-22 minutes</li>
<li>pH: 5.5-6.0</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>''',
},
@@ -195,10 +195,10 @@ STEPS = [
{
'name': '17. Drying (Hot Air Knife)',
'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>
<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',
@@ -248,7 +248,7 @@ STEPS = [
{
'name': '23. Shipping',
'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>
<li>Print BoL, attach to package</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)
# Confirm triggers fp.job creation
# Confirm - triggers fp.job creation
so.action_confirm()
print('Confirmed SO. State =', so.state)
env.cr.commit()
@@ -337,7 +337,7 @@ for js in job_steps:
prompts = len(rn.input_ids.filtered(lambda i: i.collect))
instructions_visible += int(ins)
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 '-'
))

View File

@@ -1,5 +1,5 @@
# -*- 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:
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']
# ============================================================
# 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')
@@ -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')
@@ -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')
@@ -296,7 +296,7 @@ else:
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')

View File

@@ -1,5 +1,5 @@
# -*- 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)
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']
# ============================================================
# 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')
@@ -49,7 +49,7 @@ KIND_KEYWORDS = [
('adhesion_test', ['adhesion']),
('salt_spray', ['salt spray', 'corrosion test']),
('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']),
]
@@ -111,7 +111,7 @@ find('INFO', 'Seeded %d prompts onto variant subtree' % seeded)
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')
@@ -126,12 +126,12 @@ find('INFO', 'Total prompts visible to job %d: %d across %d steps' % (
job.id, prompt_total, len(job_steps)
))
if prompt_total == 0:
find('FAIL', 'No prompts visible variant cloning still broken')
find('FAIL', 'No prompts visible - variant cloning still broken')
else:
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')
@@ -255,7 +255,7 @@ if some_step:
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')

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
"""End-to-end battle test Phase 1: reconnaissance."""
"""End-to-end battle test - Phase 1: reconnaissance."""
# Find ABC Manufacturing
abc = env['res.partner'].search([('name', 'ilike', 'ABC Manufactor')], limit=1)

View File

@@ -1,5 +1,5 @@
# -*- 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']
@@ -31,14 +31,14 @@ def walk(n, depth=0):
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)
parts = env['fp.part.catalog'].search([('partner_id', '=', abc.id)], limit=3)
print('\n=== ABC parts ===')
for p in parts:
print(' id=%d num=%s rev=%s' % (p.id, p.part_number, p.revision or ''))
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
sol = env['sale.order.line']

View File

@@ -1,5 +1,5 @@
# -*- 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
sol = env['sale.order.line'].search([

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
"""Phase 4 recon diagnose recipe variant cloning."""
"""Phase 4 recon - diagnose recipe variant cloning."""
Node = env['fusion.plating.process.node']
n = Node.browse(1777)

View File

@@ -1,5 +1,5 @@
# -*- 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:
@@ -132,7 +132,7 @@ check(12, 'wizard filter excludes collect=False',
ni_off not in visible and ni_on in visible,
'%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
empty_path = (not node.collect_measurements)
check(13, 'master collect_measurements=False short-circuits',

View File

@@ -4,10 +4,10 @@
License OPL-1 (Odoo Proprietary License v1.0)
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
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
user" group implied by every fp role ABOVE technician.
@@ -18,7 +18,7 @@
<menuitem id="other_module.X" groups="..."/> overrides require the
other module in `depends`, which would lock us into hard
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.
Why a separate office_user group instead of !technician?
@@ -32,7 +32,7 @@
<data>
<!-- ============================================================ -->
<!-- New marker group: "Office User" implied by every non- -->
<!-- New marker group: "Office User" - implied by every non- -->
<!-- technician fp role. -->
<!-- ============================================================ -->
<record id="group_fp_office_user" model="res.groups">

View File

@@ -73,10 +73,10 @@
</record>
<!-- ================================================================== -->
<!-- RECORD RULE Multi-company isolation on facilities -->
<!-- RECORD RULE - Multi-company isolation on facilities -->
<!-- ================================================================== -->
<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="global" eval="True"/>
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>

View File

@@ -1,5 +1,5 @@
/** @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').
//
// Always-visible compact grid with a Search box. Glyph-only tiles

View File

@@ -1,6 +1,6 @@
/** @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.
// License OPL-1 (Odoo Proprietary License v1.0)
//
@@ -102,7 +102,7 @@ export class RecipeTreeEditor extends Component {
this.state = useState({
recipe: null,
tree: null,
workflowStates: [], // Sub 14 populated by loadTree
workflowStates: [], // Sub 14 - populated by loadTree
loading: false,
saving: false,
selectedNodeId: null,
@@ -158,13 +158,13 @@ export class RecipeTreeEditor extends Component {
if (result && result.ok) {
this.state.recipe = result.recipe;
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.
this.state.workflowStates = result.workflow_states || [];
// Auto-expand every node on first load AND auto-expand
// any node we haven't seen before (e.g. freshly imported
// 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.
if (result.tree) {
const applyDefault = (n) => {
@@ -225,7 +225,7 @@ export class RecipeTreeEditor extends Component {
}
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
// again to see anything.
const walk = (n, isRoot) => {
@@ -272,10 +272,10 @@ export class RecipeTreeEditor extends Component {
customer_visible: node.customer_visible,
is_manual: node.is_manual,
requires_signoff: node.requires_signoff,
// Sub 13 sequential enforcement
// Sub 13 - sequential enforcement
enforce_sequential: !!node.enforce_sequential,
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,
};
const result = await rpc("/fp/recipe/node/write", {
@@ -425,7 +425,7 @@ export class RecipeTreeEditor extends Component {
const targetParentId = parentNode ? parentNode.id : null;
if (dragged.parentId === targetParentId) {
// Reorder within same parent swap positions
// Reorder within same parent - swap positions
const siblings = parentNode
? (parentNode.children || [])
: [this.state.tree];
@@ -556,7 +556,7 @@ export class RecipeTreeEditor extends Component {
onBackToList() {
// 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
// * Part form → composer → editor ⇒ back to composer
// * Part form → editor (direct link) ⇒ back to part form
@@ -568,7 +568,7 @@ export class RecipeTreeEditor extends Component {
this.action.restore();
return;
} 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,
// pick the most contextual landing page we have.

View File

@@ -1,6 +1,6 @@
/** @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,
* 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)
dragPreviewLabel: "", // shown next to the indicator line
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
// stays controlled without RPC roundtrip on every keystroke.
editingStepId: null,
editName: "",
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.
editDefaultKind: "",
editTriggersWorkflowStateId: false,
editParallelStart: 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 =
// closed; otherwise carries the template payload.
libraryEditor: null,
libraryEditorBusy: false,
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-
// loaded the first time the user opens the library editor.
workflowStates: [],
@@ -87,7 +87,7 @@ export class FpSimpleRecipeEditor extends Component {
async loadAll() {
// 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
// rebuilds every row, which snaps scrollTop back to 0. Operators
// 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
// 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
// right after itself moves the row one slot down instead of
// staying put.
@@ -285,13 +285,13 @@ export class FpSimpleRecipeEditor extends Component {
* opened from the part-scoped Process Composer, return to that part
* 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
* breadcrumb stack so a second visit shows nonsense.
*/
onBackToList() {
// 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"
// visible; Part > Composer > Editor → back returns to the
// Composer with crumbs intact).
@@ -299,7 +299,7 @@ export class FpSimpleRecipeEditor extends Component {
this.action.restore();
return;
} catch (e) {
// No prior controller fall through to a sensible default.
// No prior controller - fall through to a sensible default.
}
if (this._partId) {
this.action.doAction(
@@ -337,8 +337,8 @@ export class FpSimpleRecipeEditor extends Component {
description: "",
requires_signoff: false,
requires_predecessor_done: false,
parallel_start: false, // Sub 13 per-step opt-out
triggers_workflow_state_id: false, // Sub 14 workflow trigger
parallel_start: false, // Sub 13 - per-step opt-out
triggers_workflow_state_id: false, // Sub 14 - workflow trigger
triggers_workflow_state_name: "",
requires_rack_assignment: 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
* 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
* create + edit flows to populate the "Step Kind" dropdown so
* 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,
* round-trip to /kinds/create, refresh the cached options, then
* select the newly-created kind.
@@ -412,7 +412,7 @@ export class FpSimpleRecipeEditor extends Component {
name: name.trim(),
});
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
// instead of a generic error code.
alert(data.message || data.error || "Could not create Step Kind.");
@@ -435,7 +435,7 @@ export class FpSimpleRecipeEditor extends Component {
template_id: templateId,
});
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
// place without triggering library list re-renders.
this.state.libraryEditor = JSON.parse(JSON.stringify(data.template));
@@ -448,7 +448,7 @@ export class FpSimpleRecipeEditor extends Component {
);
} else {
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" }
);
}
@@ -477,7 +477,7 @@ export class FpSimpleRecipeEditor extends Component {
requires_signoff: !!ed.requires_signoff,
requires_predecessor_done: !!ed.requires_predecessor_done,
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,
requires_rack_assignment: !!ed.requires_rack_assignment,
requires_transition_form: !!ed.requires_transition_form,
@@ -645,7 +645,7 @@ export class FpSimpleRecipeEditor extends Component {
this.state.dragOverIndex = before ? rowIndex : rowIndex + 1;
}
/** Trailing dropzone always inserts at the end. */
/** Trailing dropzone - always inserts at the end. */
onTailDragOver(ev) {
ev.preventDefault();
ev.dataTransfer.dropEffect =
@@ -657,7 +657,7 @@ export class FpSimpleRecipeEditor extends Component {
/**
* 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
* on those areas are silently rejected by the browser. Row-level
* dragover handlers still run first and set the precise index;
@@ -695,7 +695,7 @@ export class FpSimpleRecipeEditor extends Component {
onDragLeave(ev) {
// 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.
if (!ev.currentTarget.contains(ev.relatedTarget)) {
this.state.dragOverIndex = null;
@@ -716,7 +716,7 @@ export class FpSimpleRecipeEditor extends Component {
/**
* 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.
*/
async onToggleEdit(stepId) {
@@ -726,10 +726,10 @@ export class FpSimpleRecipeEditor extends Component {
}
const step = this.state.steps.find((s) => s.id === stepId);
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.
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
// list before opening the panel so the select renders with
// options instead of being empty.
@@ -738,7 +738,7 @@ export class FpSimpleRecipeEditor extends Component {
this.state.editName = step.name || "";
this.state.editInstructions = this._htmlToText(step.description || "");
// 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.
this.state.editDefaultKind = step.default_kind || "other";
this.state.editTriggersWorkflowStateId =
@@ -763,7 +763,7 @@ export class FpSimpleRecipeEditor extends Component {
const vals = {
name: this.state.editName || _t("Untitled Step"),
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.
default_kind: this.state.editDefaultKind || false,
triggers_workflow_state_id:
@@ -798,7 +798,7 @@ export class FpSimpleRecipeEditor extends Component {
for (const file of files) {
if (!file.type.startsWith("image/")) {
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" },
);
continue;
@@ -864,7 +864,7 @@ export class FpSimpleRecipeEditor extends Component {
}
}
// -------------------- Sub 12d measurements config --------------------
// -------------------- Sub 12d - measurements config --------------------
async onToggleStepCollect(stepId, collect) {
await rpc("/fp/simple_recipe/step/toggle_collect", {
@@ -915,7 +915,7 @@ export class FpSimpleRecipeEditor extends Component {
});
if (result.error === "library_sourced") {
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" }
);
return;
@@ -936,7 +936,7 @@ export class FpSimpleRecipeEditor extends Component {
}
await this.loadAll();
this.notification.add(
_t("Reset to library defaults custom prompts preserved"),
_t("Reset to library defaults - custom prompts preserved"),
{ type: "success" }
);
}
@@ -944,7 +944,7 @@ export class FpSimpleRecipeEditor extends Component {
/**
* Render stored HTML as plain text for the textarea. Strips tags,
* 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) {
if (!html) return "";

View File

@@ -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 /
// star / link icons) renders white-on-white because Odoo sets the

View File

@@ -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.
//
// Compact 12-column grid with Search filter. Glyph-only tiles

View File

@@ -1,5 +1,5 @@
// =============================================================================
// Fusion Plating backend styles
// Fusion Plating - backend styles
// Copyright 2026 Nexa Systems Inc.
// License OPL-1 (Odoo Proprietary License v1.0)
//
@@ -18,10 +18,10 @@
//
// 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
// 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)`
// 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.
// =============================================================================
@@ -72,12 +72,12 @@
// -----------------------------------------------------------------------------
// Tank kanban state badge theming
// Tank kanban - state badge theming
// -----------------------------------------------------------------------------
.o_fp_tank_kanban {
.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;
&[data-state="empty"],
@@ -123,7 +123,7 @@
// -----------------------------------------------------------------------------
// Bath kanban chemistry health dot
// Bath kanban - chemistry health dot
// -----------------------------------------------------------------------------
.o_fp_bath_kanban {
@@ -162,7 +162,7 @@
// -----------------------------------------------------------------------------
// Facility kanban stat strip spacing
// Facility kanban - stat strip spacing
// -----------------------------------------------------------------------------
.o_fp_facility_kanban {

View File

@@ -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
//
// 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 {
display: inline-flex;
@@ -296,14 +296,14 @@ $re-line-w : 2px;
// ---- MO-execution state palette (Sub 3) --------------------------
// Applied when rendering the tree in an MO context where each
// 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.
// completed -> green (WO done)
// active -> blue (WO in progress)
// failed /
// blocked -> red (WO cancel OR quality hold active)
// 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 {
background-color: color-mix(in srgb, var(--bs-success, #28a745) 10%, #{$re-card});
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 {
display: inline-flex;
@@ -467,7 +467,7 @@ $re-line-w : 2px;
position: relative;
padding-left: $re-stub;
// horizontal stub bus column → child card
// horizontal stub - bus column → child card
&::before {
content: "";
position: absolute;

View File

@@ -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
// 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;
@@ -118,7 +118,7 @@ $fp-se-drop: var(--fp-drop-bg, #{$_fp_se_drop_hex});
gap: 1rem;
// align-items: start so the library panel can be shorter than
// 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;
@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.
// 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,
@@ -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 +
// 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.
.o_fp_library_panel {
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);
@media (max-width: 900px) {
// Stacked layout no sticky, behaves like a normal block.
// Stacked layout - no sticky, behaves like a normal block.
position: static;
max-height: 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
// 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
// text). Visual treatment: smaller, indented, no drag handle, no
// 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
// Inputs dialog renders at runtime so authors can preview the same way
// the operator will see it.

View File

@@ -4,7 +4,7 @@
License OPL-1 (Odoo Proprietary License v1.0)
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
cards left→right with bracket connectors. Each card carries hover-
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>
<select class="o_fp_re_import_select"
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">
<option t-att-value="r.id" t-esc="r.name"/>
</t>
@@ -350,7 +350,7 @@
t-on-change="(ev) => { state.selectedNode.customer_visible = ev.target.checked; }"/>
<label class="form-check-label" for="fp_re_chk_visible">Customer visible</label>
</div>
<!-- Sub 13 sequential enforcement (recipe root) -->
<!-- Sub 13 - sequential enforcement (recipe root) -->
<div class="form-check"
t-if="state.selectedNode.node_type === 'recipe'">
<input type="checkbox" class="form-check-input" id="fp_re_chk_seq"
@@ -361,7 +361,7 @@
Enforce Sequential Order
</label>
</div>
<!-- Sub 13 per-step opt-out (operation/step nodes) -->
<!-- Sub 13 - per-step opt-out (operation/step nodes) -->
<div class="form-check"
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"
@@ -374,7 +374,7 @@
</div>
</div>
<!-- Sub 14 workflow milestone trigger (operation / step nodes) -->
<!-- Sub 14 - workflow milestone trigger (operation / step nodes) -->
<div class="o_fp_re_field"
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>
@@ -383,7 +383,7 @@
t-on-change="(ev) => { state.selectedNode.triggers_workflow_state_id = ev.target.value ? (+ev.target.value) : false; }">
<option value=""
t-att-selected="!state.selectedNode.triggers_workflow_state_id">
None (use default-kind matching)
- None (use default-kind matching) -
</option>
<t t-foreach="state.workflowStates" t-as="ws" t-key="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>
</select>
<small class="text-muted d-block mt-1">
<strong>Required</strong> every job runs this step.
<strong>Opt-Out</strong> ships included, estimator can remove per job.
<strong>Opt-In</strong> ships excluded, estimator can add per job.
<strong>Required</strong> - every job runs this step.
<strong>Opt-Out</strong> - ships included, estimator can remove per job.
<strong>Opt-In</strong> - ships excluded, estimator can add per job.
</small>
</div>

View File

@@ -30,7 +30,7 @@
<select id="fp_import_template_select"
class="form-select o_fp_import_select"
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">
<option t-att-value="tpl.id">
<t t-esc="tpl.name"/> (<t t-esc="tpl.step_count"/> steps)
@@ -56,7 +56,7 @@
<div class="o_fp_steps_list">
<!-- 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. -->
<div class="o_fp_drop_indicator"
t-att-class="state.dragOverIndex === 0 ? 'o_fp_drop_indicator_active' : ''">
@@ -145,13 +145,13 @@
<textarea class="form-control"
rows="5"
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">
Shown to operators when running this step at the tank. Use line breaks for separate points.
</p>
</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
default-kind workflow trigger; the dropdown
below it lets them override per-step. -->
@@ -200,7 +200,7 @@
<label>Triggers Workflow State</label>
<select class="form-select"
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">
<option t-att-value="ws.id"
t-att-selected="state.editTriggersWorkflowStateId === ws.id"
@@ -234,7 +234,7 @@
</label>
</div>
<!-- Instruction images recipe author drops
<!-- Instruction images - recipe author drops
photos / screenshots / diagrams here.
Operators see the gallery at runtime in
the Record Inputs dialog and the step
@@ -283,7 +283,7 @@
</label>
</div>
<!-- Sub 12d Measurements config -->
<!-- Sub 12d - Measurements config -->
<div class="o_fp_edit_field o_fp_measurements_config">
<label>
<input type="checkbox"
@@ -324,7 +324,7 @@
t-on-blur="(ev) => this.onInputBlur(inp.id, 'name', ev)"/>
<small t-if="inp.from_library"
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
</small>
</td>
@@ -509,7 +509,7 @@
<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">
<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>
</t>
<!-- Manager-only inline create. The
@@ -592,7 +592,7 @@
</label>
</div>
<!-- Sub 14 workflow milestone trigger dropdown.
<!-- Sub 14 - workflow milestone trigger dropdown.
Hidden when no states exist (e.g. catalog
not seeded yet). -->
<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; }">
<option value=""
t-att-selected="!state.libraryEditor.triggers_workflow_state_id">
None (use default-kind matching)
- None (use default-kind matching) -
</option>
<t t-foreach="state.workflowStates" t-as="ws" t-key="ws.id">
<option t-att-value="ws.id"
@@ -702,7 +702,7 @@
Save the step first, then add prompts.
</t>
<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>
</p>
<div class="o_fp_le_prompt_actions"
@@ -714,7 +714,7 @@
<button class="btn btn-link btn-sm"
t-if="state.libraryEditor.default_kind"
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
</button>
</div>

View File

@@ -46,11 +46,11 @@ class TestFpJobStateMachine(TransactionCase):
job.action_confirm()
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.
job = self._make_job()
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+).
job.state = 'done'
with self.assertRaises(UserError):
@@ -71,7 +71,7 @@ class TestFpJobStateMachine(TransactionCase):
job = self._make_job()
# Force state to 'done' (no public action yet)
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')
def test_margin_zero_when_no_revenue(self):

View File

@@ -33,7 +33,7 @@ class TestFpJobStepStateMachine(TransactionCase):
def test_button_start_requires_ready_or_paused(self):
step = self._make_step()
# state is 'pending' should raise
# state is 'pending' - should raise
with self.assertRaises(UserError):
step.button_start()

View File

@@ -15,7 +15,7 @@ class TestFpWorkCentre(TransactionCase):
def test_facility_optional_at_create(self):
# 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({
'name': 'Test',
'code': 'T',

View File

@@ -2,7 +2,7 @@
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# 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:
@@ -57,7 +57,7 @@ class TestLandingResolver(TransactionCase):
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
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`.
"""
Window = self.env['ir.actions.act_window']
@@ -123,7 +123,7 @@ class TestLandingResolver(TransactionCase):
"""The legacy 'fp_shopfloor_landing' component was retired
2026-05-25. The ``fusion_plating_shopfloor.layout`` flag is now
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."""
self.env['ir.config_parameter'].sudo().set_param(
'fusion_plating_shopfloor.layout', 'legacy')

View File

@@ -14,7 +14,7 @@ class TestMenuVisibility(TransactionCase):
'email': f'menu_{name}@example.com',
'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({
'login': 'menu_no', 'name': 'Menu Test no',
'email': 'menu_no@example.com',

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Phase 1 rack-load core model tests.
# Phase 1 - rack-load core model tests.
from odoo.tests import TransactionCase, tagged

View File

@@ -11,7 +11,7 @@ class TestRoleGroupsStructure(TransactionCase):
def test_all_seven_groups_exist(self):
"""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 = {
'group_fp_technician', 'group_fp_sales_rep',
'group_fp_shop_manager_v2', 'group_fp_sales_manager',
@@ -33,7 +33,7 @@ class TestRoleGroupsStructure(TransactionCase):
'Owner must transitively imply base.group_system')
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')
sm = self.env.ref('fusion_plating.group_fp_shop_manager_v2')
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