diff --git a/fusion_plating/fusion_plating/__init__.py b/fusion_plating/fusion_plating/__init__.py index ff872e85..d306cacc 100644 --- a/fusion_plating/fusion_plating/__init__.py +++ b/fusion_plating/fusion_plating/__init__.py @@ -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 diff --git a/fusion_plating/fusion_plating/__manifest__.py b/fusion_plating/fusion_plating/__manifest__.py index 17f92b45..68dfb1df 100644 --- a/fusion_plating/fusion_plating/__manifest__.py +++ b/fusion_plating/fusion_plating/__manifest__.py @@ -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', diff --git a/fusion_plating/fusion_plating/controllers/recipe_controller.py b/fusion_plating/fusion_plating/controllers/recipe_controller.py index 63f97ac0..86fb5742 100644 --- a/fusion_plating/fusion_plating/controllers/recipe_controller.py +++ b/fusion_plating/fusion_plating/controllers/recipe_controller.py @@ -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. * → 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']) diff --git a/fusion_plating/fusion_plating/controllers/simple_recipe_controller.py b/fusion_plating/fusion_plating/controllers/simple_recipe_controller.py index e929d41d..1e5e76f9 100644 --- a/fusion_plating/fusion_plating/controllers/simple_recipe_controller.py +++ b/fusion_plating/fusion_plating/controllers/simple_recipe_controller.py @@ -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. diff --git a/fusion_plating/fusion_plating/data/fp_demo_data.xml b/fusion_plating/fusion_plating/data/fp_demo_data.xml index 7ce77c33..6202d9f4 100644 --- a/fusion_plating/fusion_plating/data/fp_demo_data.xml +++ b/fusion_plating/fusion_plating/data/fp_demo_data.xml @@ -1,6 +1,6 @@ @@ -35,13 +35,13 @@ - Fusion Plating — Main Plant + Fusion Plating - Main Plant FP-MAIN 10 - Fusion Plating — East Annex + Fusion Plating - East Annex FP-EAST 20 @@ -85,7 +85,7 @@ - EN Tank 1 — Mid-Phos + EN Tank 1 - Mid-Phos T-EN-01 @@ -99,7 +99,7 @@ - EN Tank 2 — High-Phos + EN Tank 2 - High-Phos T-EN-02 @@ -212,7 +212,7 @@ - Dye Immersion Tank — Black + Dye Immersion Tank - Black T-AN-DYE diff --git a/fusion_plating/fusion_plating/data/fp_demo_recipe_data.xml b/fusion_plating/fusion_plating/data/fp_demo_recipe_data.xml index 44c6fb60..38a55228 100644 --- a/fusion_plating/fusion_plating/data/fp_demo_recipe_data.xml +++ b/fusion_plating/fusion_plating/data/fp_demo_recipe_data.xml @@ -2,14 +2,14 @@ - + - Electroless Nickel Plating — Steel Line + Electroless Nickel Plating - Steel Line EN_STEEL recipe fa-flask diff --git a/fusion_plating/fusion_plating/data/fp_job_sequences.xml b/fusion_plating/fusion_plating/data/fp_job_sequences.xml index ae13baaf..1e84e7c7 100644 --- a/fusion_plating/fusion_plating/data/fp_job_sequences.xml +++ b/fusion_plating/fusion_plating/data/fp_job_sequences.xml @@ -1,5 +1,5 @@ - diff --git a/fusion_plating/fusion_plating/data/fp_landing_data.xml b/fusion_plating/fusion_plating/data/fp_landing_data.xml index d52afae4..c694debe 100644 --- a/fusion_plating/fusion_plating/data/fp_landing_data.xml +++ b/fusion_plating/fusion_plating/data/fp_landing_data.xml @@ -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 @@ - Plating — Open Landing Page + Plating - Open Landing Page code @@ -42,7 +42,7 @@ Preparation prep 50 - Cleaning, degreasing, etching, activation — surface prep before the main finishing step. + Cleaning, degreasing, etching, activation - surface prep before the main finishing step. diff --git a/fusion_plating/fusion_plating/data/fp_recipe_anodize.xml b/fusion_plating/fusion_plating/data/fp_recipe_anodize.xml index e30bf4fb..2f0d3355 100644 --- a/fusion_plating/fusion_plating/data/fp_recipe_anodize.xml +++ b/fusion_plating/fusion_plating/data/fp_recipe_anodize.xml @@ -2,7 +2,7 @@ diff --git a/fusion_plating/fusion_plating/data/fp_recipe_enp_sp.xml b/fusion_plating/fusion_plating/data/fp_recipe_enp_sp.xml index b7fb4412..ef46ec95 100644 --- a/fusion_plating/fusion_plating/data/fp_recipe_enp_sp.xml +++ b/fusion_plating/fusion_plating/data/fp_recipe_enp_sp.xml @@ -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 @@ True - + E-Nickel Plate (Hi-Phos) (SP-8) operation @@ -343,7 +343,7 @@ + rack - de-rack happens after this bake. --> Post-plate Bake (H2 Embrittlement Relief) operation diff --git a/fusion_plating/fusion_plating/data/fp_recipe_enp_steel_basic.xml b/fusion_plating/fusion_plating/data/fp_recipe_enp_steel_basic.xml index 9c86768a..1bb2bf9d 100644 --- a/fusion_plating/fusion_plating/data/fp_recipe_enp_steel_basic.xml +++ b/fusion_plating/fusion_plating/data/fp_recipe_enp_steel_basic.xml @@ -248,7 +248,7 @@ True - + E-Nickel Plate (Mid Phos)(S-9) operation @@ -276,7 +276,7 @@ True - + E-Nickel Plate (S-10) operation @@ -328,7 +328,7 @@ + rack - de-rack happens after this bake. --> Post-plate Bake (H2 Embrittlement Relief) operation diff --git a/fusion_plating/fusion_plating/data/fp_recipe_general_processing.xml b/fusion_plating/fusion_plating/data/fp_recipe_general_processing.xml index 0f3b451a..a5f24ce2 100644 --- a/fusion_plating/fusion_plating/data/fp_recipe_general_processing.xml +++ b/fusion_plating/fusion_plating/data/fp_recipe_general_processing.xml @@ -2,7 +2,7 @@ + - FP — Move Log + FP - Move Log fp.job.step.move FP/MOVE/%(year)s/ 5 diff --git a/fusion_plating/fusion_plating/data/fp_step_kind_data.xml b/fusion_plating/fusion_plating/data/fp_step_kind_data.xml index b09b9c77..f34d8a70 100644 --- a/fusion_plating/fusion_plating/data/fp_step_kind_data.xml +++ b/fusion_plating/fusion_plating/data/fp_step_kind_data.xml @@ -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). --> - + @@ -224,7 +224,7 @@ @@ -407,7 +407,7 @@ Conductivity number - µS/cm — required for DI rinses + µS/cm - required for DI rinses 20 @@ -499,7 +499,7 @@ Current Density number - ASF — electroplate only + ASF - electroplate only 60 diff --git a/fusion_plating/fusion_plating/data/fp_step_template_data.xml b/fusion_plating/fusion_plating/data/fp_step_template_data.xml index 40e5f047..913b33b3 100644 --- a/fusion_plating/fusion_plating/data/fp_step_template_data.xml +++ b/fusion_plating/fusion_plating/data/fp_step_template_data.xml @@ -103,7 +103,7 @@ ]]> - + Hot Water Porosity Test (A-15) diff --git a/fusion_plating/fusion_plating/data/fp_work_role_data.xml b/fusion_plating/fusion_plating/data/fp_work_role_data.xml index b50c5ca8..d358f7d7 100644 --- a/fusion_plating/fusion_plating/data/fp_work_role_data.xml +++ b/fusion_plating/fusion_plating/data/fp_work_role_data.xml @@ -30,7 +30,7 @@ plating_op 30 fa-flask - Runs the plating line — chemistry checks, dwell, thickness. + Runs the plating line - chemistry checks, dwell, thickness. diff --git a/fusion_plating/fusion_plating/migrations/19.0.12.1.0/post-migrate.py b/fusion_plating/fusion_plating/migrations/19.0.12.1.0/post-migrate.py index 4ee8cf49..8060cb61 100644 --- a/fusion_plating/fusion_plating/migrations/19.0.12.1.0/post-migrate.py +++ b/fusion_plating/fusion_plating/migrations/19.0.12.1.0/post-migrate.py @@ -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), ) diff --git a/fusion_plating/fusion_plating/migrations/19.0.12.4.1/post-migrate.py b/fusion_plating/fusion_plating/migrations/19.0.12.4.1/post-migrate.py index caafe113..1a1ce367 100644 --- a/fusion_plating/fusion_plating/migrations/19.0.12.4.1/post-migrate.py +++ b/fusion_plating/fusion_plating/migrations/19.0.12.4.1/post-migrate.py @@ -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. """ diff --git a/fusion_plating/fusion_plating/migrations/19.0.12.5.0/post-migrate.py b/fusion_plating/fusion_plating/migrations/19.0.12.5.0/post-migrate.py index db4181e4..10cbf4ed 100644 --- a/fusion_plating/fusion_plating/migrations/19.0.12.5.0/post-migrate.py +++ b/fusion_plating/fusion_plating/migrations/19.0.12.5.0/post-migrate.py @@ -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', diff --git a/fusion_plating/fusion_plating/migrations/19.0.18.12.0/post-migrate.py b/fusion_plating/fusion_plating/migrations/19.0.18.12.0/post-migrate.py index 1657004f..695810af 100644 --- a/fusion_plating/fusion_plating/migrations/19.0.18.12.0/post-migrate.py +++ b/fusion_plating/fusion_plating/migrations/19.0.18.12.0/post-migrate.py @@ -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. diff --git a/fusion_plating/fusion_plating/migrations/19.0.18.13.0/post-migrate.py b/fusion_plating/fusion_plating/migrations/19.0.18.13.0/post-migrate.py index b260034d..3bfadd05 100644 --- a/fusion_plating/fusion_plating/migrations/19.0.18.13.0/post-migrate.py +++ b/fusion_plating/fusion_plating/migrations/19.0.18.13.0/post-migrate.py @@ -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, ) diff --git a/fusion_plating/fusion_plating/migrations/19.0.18.13.0/pre-migrate.py b/fusion_plating/fusion_plating/migrations/19.0.18.13.0/pre-migrate.py index e27e5407..be2f4f48 100644 --- a/fusion_plating/fusion_plating/migrations/19.0.18.13.0/pre-migrate.py +++ b/fusion_plating/fusion_plating/migrations/19.0.18.13.0/pre-migrate.py @@ -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). """ diff --git a/fusion_plating/fusion_plating/migrations/19.0.18.7.0/post-migrate.py b/fusion_plating/fusion_plating/migrations/19.0.18.7.0/post-migrate.py index 3aa97d0a..80028137 100644 --- a/fusion_plating/fusion_plating/migrations/19.0.18.7.0/post-migrate.py +++ b/fusion_plating/fusion_plating/migrations/19.0.18.7.0/post-migrate.py @@ -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'). diff --git a/fusion_plating/fusion_plating/migrations/19.0.18.8.0/post-migrate.py b/fusion_plating/fusion_plating/migrations/19.0.18.8.0/post-migrate.py index 07214187..90ad613a 100644 --- a/fusion_plating/fusion_plating/migrations/19.0.18.8.0/post-migrate.py +++ b/fusion_plating/fusion_plating/migrations/19.0.18.8.0/post-migrate.py @@ -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( '

Recipe migrated to v19.0.18.8.0 step layout.

' '

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 ' 'node_type changed.

' '

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(""" diff --git a/fusion_plating/fusion_plating/migrations/19.0.20.6.0/post-migrate.py b/fusion_plating/fusion_plating/migrations/19.0.20.6.0/post-migrate.py index 3f327dfe..3a7dd173 100644 --- a/fusion_plating/fusion_plating/migrations/19.0.20.6.0/post-migrate.py +++ b/fusion_plating/fusion_plating/migrations/19.0.20.6.0/post-migrate.py @@ -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 diff --git a/fusion_plating/fusion_plating/migrations/19.0.20.6.0/pre-migrate.py b/fusion_plating/fusion_plating/migrations/19.0.20.6.0/pre-migrate.py index a9eb65a5..58183fb2 100644 --- a/fusion_plating/fusion_plating/migrations/19.0.20.6.0/pre-migrate.py +++ b/fusion_plating/fusion_plating/migrations/19.0.20.6.0/pre-migrate.py @@ -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() diff --git a/fusion_plating/fusion_plating/migrations/19.0.21.0.0/post-migrate.py b/fusion_plating/fusion_plating/migrations/19.0.21.0.0/post-migrate.py index 24304625..8cae2b91 100644 --- a/fusion_plating/fusion_plating/migrations/19.0.21.0.0/post-migrate.py +++ b/fusion_plating/fusion_plating/migrations/19.0.21.0.0/post-migrate.py @@ -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. diff --git a/fusion_plating/fusion_plating/migrations/19.0.21.1.0/post-migrate.py b/fusion_plating/fusion_plating/migrations/19.0.21.1.0/post-migrate.py index fbe83312..257a6055 100644 --- a/fusion_plating/fusion_plating/migrations/19.0.21.1.0/post-migrate.py +++ b/fusion_plating/fusion_plating/migrations/19.0.21.1.0/post-migrate.py @@ -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 ) diff --git a/fusion_plating/fusion_plating/migrations/19.0.21.2.0/pre-migrate.py b/fusion_plating/fusion_plating/migrations/19.0.21.2.0/pre-migrate.py index 8b7feb94..495aaf20 100644 --- a/fusion_plating/fusion_plating/migrations/19.0.21.2.0/pre-migrate.py +++ b/fusion_plating/fusion_plating/migrations/19.0.21.2.0/pre-migrate.py @@ -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( diff --git a/fusion_plating/fusion_plating/migrations/19.0.21.4.0/post-migrate.py b/fusion_plating/fusion_plating/migrations/19.0.21.4.0/post-migrate.py index 56b9b8a9..64d52569 100644 --- a/fusion_plating/fusion_plating/migrations/19.0.21.4.0/post-migrate.py +++ b/fusion_plating/fusion_plating/migrations/19.0.21.4.0/post-migrate.py @@ -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 diff --git a/fusion_plating/fusion_plating/migrations/19.0.22.0.0/post-migrate.py b/fusion_plating/fusion_plating/migrations/19.0.22.0.0/post-migrate.py index de7bf275..b2fd29e9 100644 --- a/fusion_plating/fusion_plating/migrations/19.0.22.0.0/post-migrate.py +++ b/fusion_plating/fusion_plating/migrations/19.0.22.0.0/post-migrate.py @@ -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. diff --git a/fusion_plating/fusion_plating/migrations/19.0.9.0.0/pre-migration.py b/fusion_plating/fusion_plating/migrations/19.0.9.0.0/pre-migration.py index 64e8c234..7c2f4147 100644 --- a/fusion_plating/fusion_plating/migrations/19.0.9.0.0/pre-migration.py +++ b/fusion_plating/fusion_plating/migrations/19.0.9.0.0/pre-migration.py @@ -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', diff --git a/fusion_plating/fusion_plating/models/__init__.py b/fusion_plating/fusion_plating/models/__init__.py index a7681251..3d9b6ec8 100644 --- a/fusion_plating/fusion_plating/models/__init__.py +++ b/fusion_plating/fusion_plating/models/__init__.py @@ -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 diff --git a/fusion_plating/fusion_plating/models/_fp_uom_selection.py b/fusion_plating/fusion_plating/models/_fp_uom_selection.py index fcc4e37f..0129722b 100644 --- a/fusion_plating/fusion_plating/models/_fp_uom_selection.py +++ b/fusion_plating/fusion_plating/models/_fp_uom_selection.py @@ -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, ) diff --git a/fusion_plating/fusion_plating/models/fp_bath.py b/fusion_plating/fusion_plating/models/fp_bath.py index c62f24c1..a105ef3f 100644 --- a/fusion_plating/fusion_plating/models/fp_bath.py +++ b/fusion_plating/fusion_plating/models/fp_bath.py @@ -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( diff --git a/fusion_plating/fusion_plating/models/fp_bath_log.py b/fusion_plating/fusion_plating/models/fp_bath_log.py index 9d073a12..de6acd73 100644 --- a/fusion_plating/fusion_plating/models/fp_bath_log.py +++ b/fusion_plating/fusion_plating/models/fp_bath_log.py @@ -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): diff --git a/fusion_plating/fusion_plating/models/fp_bath_log_line.py b/fusion_plating/fusion_plating/models/fp_bath_log_line.py index 90380137..f37b4581 100644 --- a/fusion_plating/fusion_plating/models/fp_bath_log_line.py +++ b/fusion_plating/fusion_plating/models/fp_bath_log_line.py @@ -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): diff --git a/fusion_plating/fusion_plating/models/fp_bath_parameter.py b/fusion_plating/fusion_plating/models/fp_bath_parameter.py index 8ac41f19..9ef5eb7e 100644 --- a/fusion_plating/fusion_plating/models/fp_bath_parameter.py +++ b/fusion_plating/fusion_plating/models/fp_bath_parameter.py @@ -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.', diff --git a/fusion_plating/fusion_plating/models/fp_bath_replenishment_rule.py b/fusion_plating/fusion_plating/models/fp_bath_replenishment_rule.py index 25c44b1c..bb6b5196 100644 --- a/fusion_plating/fusion_plating/models/fp_bath_replenishment_rule.py +++ b/fusion_plating/fusion_plating/models/fp_bath_replenishment_rule.py @@ -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' diff --git a/fusion_plating/fusion_plating/models/fp_facility.py b/fusion_plating/fusion_plating/models/fp_facility.py index 1bfc6686..56d59c13 100644 --- a/fusion_plating/fusion_plating/models/fp_facility.py +++ b/fusion_plating/fusion_plating/models/fp_facility.py @@ -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' diff --git a/fusion_plating/fusion_plating/models/fp_job.py b/fusion_plating/fusion_plating/models/fp_job.py index 4298bafe..57e8be68 100644 --- a/fusion_plating/fusion_plating/models/fp_job.py +++ b/fusion_plating/fusion_plating/models/fp_job.py @@ -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(_( diff --git a/fusion_plating/fusion_plating/models/fp_job_step.py b/fusion_plating/fusion_plating/models/fp_job_step.py index 8ad5308c..33138cd4 100644 --- a/fusion_plating/fusion_plating/models/fp_job_step.py +++ b/fusion_plating/fusion_plating/models/fp_job_step.py @@ -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) diff --git a/fusion_plating/fusion_plating/models/fp_job_step_move.py b/fusion_plating/fusion_plating/models/fp_job_step_move.py index 5e1db95a..537b68ea 100644 --- a/fusion_plating/fusion_plating/models/fp_job_step_move.py +++ b/fusion_plating/fusion_plating/models/fp_job_step_move.py @@ -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', diff --git a/fusion_plating/fusion_plating/models/fp_job_step_timelog.py b/fusion_plating/fusion_plating/models/fp_job_step_timelog.py index 55ab9865..4d9b58d0 100644 --- a/fusion_plating/fusion_plating/models/fp_job_step_timelog.py +++ b/fusion_plating/fusion_plating/models/fp_job_step_timelog.py @@ -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. # ------------------------------------------------------------------ diff --git a/fusion_plating/fusion_plating/models/fp_landing.py b/fusion_plating/fusion_plating/models/fp_landing.py index db9df423..56f7241e 100644 --- a/fusion_plating/fusion_plating/models/fp_landing.py +++ b/fusion_plating/fusion_plating/models/fp_landing.py @@ -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): diff --git a/fusion_plating/fusion_plating/models/fp_migration.py b/fusion_plating/fusion_plating/models/fp_migration.py index 9552a7be..f749b4f6 100644 --- a/fusion_plating/fusion_plating/models/fp_migration.py +++ b/fusion_plating/fusion_plating/models/fp_migration.py @@ -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 = [] diff --git a/fusion_plating/fusion_plating/models/fp_operator_certification.py b/fusion_plating/fusion_plating/models/fp_operator_certification.py index e575053b..7b3030e8 100644 --- a/fusion_plating/fusion_plating/models/fp_operator_certification.py +++ b/fusion_plating/fusion_plating/models/fp_operator_certification.py @@ -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 diff --git a/fusion_plating/fusion_plating/models/fp_parent_numbered_mixin.py b/fusion_plating/fusion_plating/models/fp_parent_numbered_mixin.py index d3e5a75e..ad797e35 100644 --- a/fusion_plating/fusion_plating/models/fp_parent_numbered_mixin.py +++ b/fusion_plating/fusion_plating/models/fp_parent_numbered_mixin.py @@ -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. diff --git a/fusion_plating/fusion_plating/models/fp_process_category.py b/fusion_plating/fusion_plating/models/fp_process_category.py index a0cf7514..5a7b22e1 100644 --- a/fusion_plating/fusion_plating/models/fp_process_category.py +++ b/fusion_plating/fusion_plating/models/fp_process_category.py @@ -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( diff --git a/fusion_plating/fusion_plating/models/fp_process_node.py b/fusion_plating/fusion_plating/models/fp_process_node.py index 37cc3ad7..346533eb 100644 --- a/fusion_plating/fusion_plating/models/fp_process_node.py +++ b/fusion_plating/fusion_plating/models/fp_process_node.py @@ -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.', ) diff --git a/fusion_plating/fusion_plating/models/fp_process_type.py b/fusion_plating/fusion_plating/models/fp_process_type.py index a5537e00..7faad3a3 100644 --- a/fusion_plating/fusion_plating/models/fp_process_type.py +++ b/fusion_plating/fusion_plating/models/fp_process_type.py @@ -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', diff --git a/fusion_plating/fusion_plating/models/fp_proficiency.py b/fusion_plating/fusion_plating/models/fp_proficiency.py index 3a47b11b..beadbef7 100644 --- a/fusion_plating/fusion_plating/models/fp_proficiency.py +++ b/fusion_plating/fusion_plating/models/fp_proficiency.py @@ -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(_( - '%(name)s promoted — qualified for ' + '%(name)s promoted - qualified for ' '%(role)s after %(count)s successful ' 'completions.' )) % { diff --git a/fusion_plating/fusion_plating/models/fp_rack.py b/fusion_plating/fusion_plating/models/fp_rack.py index 57831149..2ef5ec2f 100644 --- a/fusion_plating/fusion_plating/models/fp_rack.py +++ b/fusion_plating/fusion_plating/models/fp_rack.py @@ -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).', ) diff --git a/fusion_plating/fusion_plating/models/fp_rack_load.py b/fusion_plating/fusion_plating/models/fp_rack_load.py index 93b34655..cd3c9360 100644 --- a/fusion_plating/fusion_plating/models/fp_rack_load.py +++ b/fusion_plating/fusion_plating/models/fp_rack_load.py @@ -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): diff --git a/fusion_plating/fusion_plating/models/fp_rack_tag.py b/fusion_plating/fusion_plating/models/fp_rack_tag.py index 0eeb3c4e..cf28c0b7 100644 --- a/fusion_plating/fusion_plating/models/fp_rack_tag.py +++ b/fusion_plating/fusion_plating/models/fp_rack_tag.py @@ -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) diff --git a/fusion_plating/fusion_plating/models/fp_role_constants.py b/fusion_plating/fusion_plating/models/fp_role_constants.py index 05afcea9..54e3ed8b 100644 --- a/fusion_plating/fusion_plating/models/fp_role_constants.py +++ b/fusion_plating/fusion_plating/models/fp_role_constants.py @@ -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. diff --git a/fusion_plating/fusion_plating/models/fp_step_kind.py b/fusion_plating/fusion_plating/models/fp_step_kind.py index 49aaaed7..1d9b7100 100644 --- a/fusion_plating/fusion_plating/models/fp_step_kind.py +++ b/fusion_plating/fusion_plating/models/fp_step_kind.py @@ -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) diff --git a/fusion_plating/fusion_plating/models/fp_step_template.py b/fusion_plating/fusion_plating/models/fp_step_template.py index 5823812b..106da49e 100644 --- a/fusion_plating/fusion_plating/models/fp_step_template.py +++ b/fusion_plating/fusion_plating/models/fp_step_template.py @@ -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: diff --git a/fusion_plating/fusion_plating/models/fp_step_template_input.py b/fusion_plating/fusion_plating/models/fp_step_template_input.py index 25cb2c09..b7604417 100644 --- a/fusion_plating/fusion_plating/models/fp_step_template_input.py +++ b/fusion_plating/fusion_plating/models/fp_step_template_input.py @@ -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) diff --git a/fusion_plating/fusion_plating/models/fp_step_template_transition_input.py b/fusion_plating/fusion_plating/models/fp_step_template_transition_input.py index 47d9aa09..3dec758c 100644 --- a/fusion_plating/fusion_plating/models/fp_step_template_transition_input.py +++ b/fusion_plating/fusion_plating/models/fp_step_template_transition_input.py @@ -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) diff --git a/fusion_plating/fusion_plating/models/fp_tank.py b/fusion_plating/fusion_plating/models/fp_tank.py index ed98582b..fada7693 100644 --- a/fusion_plating/fusion_plating/models/fp_tank.py +++ b/fusion_plating/fusion_plating/models/fp_tank.py @@ -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: diff --git a/fusion_plating/fusion_plating/models/fp_tank_composition.py b/fusion_plating/fusion_plating/models/fp_tank_composition.py index a2cbb958..8aed4626 100644 --- a/fusion_plating/fusion_plating/models/fp_tank_composition.py +++ b/fusion_plating/fusion_plating/models/fp_tank_composition.py @@ -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, diff --git a/fusion_plating/fusion_plating/models/fp_tank_section.py b/fusion_plating/fusion_plating/models/fp_tank_section.py index 840c9840..9df7ba16 100644 --- a/fusion_plating/fusion_plating/models/fp_tank_section.py +++ b/fusion_plating/fusion_plating/models/fp_tank_section.py @@ -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( diff --git a/fusion_plating/fusion_plating/models/fp_tz.py b/fusion_plating/fusion_plating/models/fp_tz.py index b9e1e4c8..c777e267 100644 --- a/fusion_plating/fusion_plating/models/fp_tz.py +++ b/fusion_plating/fusion_plating/models/fp_tz.py @@ -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() diff --git a/fusion_plating/fusion_plating/models/fp_work_center.py b/fusion_plating/fusion_plating/models/fp_work_center.py index e8cfc709..cb899372 100644 --- a/fusion_plating/fusion_plating/models/fp_work_center.py +++ b/fusion_plating/fusion_plating/models/fp_work_center.py @@ -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 = [ diff --git a/fusion_plating/fusion_plating/models/fp_work_centre.py b/fusion_plating/fusion_plating/models/fp_work_centre.py index 280b9710..6f992690 100644 --- a/fusion_plating/fusion_plating/models/fp_work_centre.py +++ b/fusion_plating/fusion_plating/models/fp_work_centre.py @@ -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( diff --git a/fusion_plating/fusion_plating/models/fp_work_role.py b/fusion_plating/fusion_plating/models/fp_work_role.py index c9960f92..ed98b14c 100644 --- a/fusion_plating/fusion_plating/models/fp_work_role.py +++ b/fusion_plating/fusion_plating/models/fp_work_role.py @@ -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) diff --git a/fusion_plating/fusion_plating/models/hr_employee.py b/fusion_plating/fusion_plating/models/hr_employee.py index bea5d80a..f2ac81d3 100644 --- a/fusion_plating/fusion_plating/models/hr_employee.py +++ b/fusion_plating/fusion_plating/models/hr_employee.py @@ -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: diff --git a/fusion_plating/fusion_plating/models/res_company.py b/fusion_plating/fusion_plating/models/res_company.py index 3d004837..484dc6da 100644 --- a/fusion_plating/fusion_plating/models/res_company.py +++ b/fusion_plating/fusion_plating/models/res_company.py @@ -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. diff --git a/fusion_plating/fusion_plating/models/res_config_settings.py b/fusion_plating/fusion_plating/models/res_config_settings.py index 1116ed9a..a8d1b692 100644 --- a/fusion_plating/fusion_plating/models/res_config_settings.py +++ b/fusion_plating/fusion_plating/models/res_config_settings.py @@ -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 diff --git a/fusion_plating/fusion_plating/models/res_users.py b/fusion_plating/fusion_plating/models/res_users.py index d5e146ce..dce91c97 100644 --- a/fusion_plating/fusion_plating/models/res_users.py +++ b/fusion_plating/fusion_plating/models/res_users.py @@ -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 diff --git a/fusion_plating/fusion_plating/scripts/bt_create_hard_anodize.py b/fusion_plating/fusion_plating/scripts/bt_create_hard_anodize.py index 5dc6b0d1..69e3a940 100644 --- a/fusion_plating/fusion_plating/scripts/bt_create_hard_anodize.py +++ b/fusion_plating/fusion_plating/scripts/bt_create_hard_anodize.py @@ -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': '''

Hard Anodize Type III per MIL-A-8625F

Aluminum substrate. Black sulfuric dye. Hot nickel acetate seal.

-

Tolerances: 0.0015"–0.0020" coating thickness, 60kV breakdown +

Tolerances: 0.0015"-0.0020" coating thickness, 60kV breakdown voltage. Hardness ≥350HV300.

''', }) print('Created recipe:', recipe.id, recipe.name) @@ -100,14 +100,14 @@ STEPS = [ { 'name': '5. Alkaline Clean (Tank A-1)', 'kind': 'cleaning', - 'description': '''

Soak clean — Aluminum Etch Cleaner.

+ 'description': '''

Soak clean - Aluminum Etch Cleaner.

  • Tank: A-1, Bath: ALKCLEAN-1
  • -
  • Time: 4–6 minutes
  • -
  • Temperature: 140–160°F
  • +
  • Time: 4-6 minutes
  • +
  • Temperature: 140-160°F
  • Confirm titration done within last 24 hours
''', }, { - 'name': '6. Rinse — Cascade DI (Tank A-2)', + 'name': '6. Rinse - Cascade DI (Tank A-2)', 'kind': 'rinse', 'description': '''

Triple cascade DI rinse.

  • Tank A-2 (DI rinse, conductivity < 50 µS/cm)
  • @@ -117,47 +117,47 @@ STEPS = [ { 'name': '7. Etch (Tank A-3)', 'kind': 'etch', - 'description': '''

    Caustic etch — sodium hydroxide bath.

    + 'description': '''

    Caustic etch - sodium hydroxide bath.

    • Tank: A-3, Bath: ETCH-1
    • -
    • Time: 30–90 seconds (per drawing — heavy etch removes 0.0005"/side)
    • -
    • Temperature: 130–150°F
    • -
    • Concentration: 4–6 oz/gal NaOH
    • -
    • HE-risk parts (high-strength) require post-bake — flag accordingly
    ''', +
  • Time: 30-90 seconds (per drawing - heavy etch removes 0.0005"/side)
  • +
  • Temperature: 130-150°F
  • +
  • Concentration: 4-6 oz/gal NaOH
  • +
  • HE-risk parts (high-strength) require post-bake - flag accordingly
''', }, { - 'name': '8. Rinse — Cascade DI (Tank A-4)', + 'name': '8. Rinse - Cascade DI (Tank A-4)', 'kind': 'rinse', - 'description': '

Triple cascade DI rinse — Tank A-4. 30 sec agitate.

', + 'description': '

Triple cascade DI rinse - Tank A-4. 30 sec agitate.

', }, { 'name': '9. Desmut / Deoxidize (Tank A-5)', 'kind': 'etch', 'description': '''

Acid desmut to remove black smut from etch.

  • Tank: A-5, Bath: DEOX-1 (HNO3-based)
  • -
  • Time: 30–60 seconds
  • +
  • Time: 30-60 seconds
  • Temperature: ambient
  • Surface should be water-break-free after this step
''', }, { - 'name': '10. Rinse — Cascade DI (Tank A-6)', + 'name': '10. Rinse - Cascade DI (Tank A-6)', 'kind': 'rinse', - 'description': '

Final pre-anodize rinse — Tank A-6. Conductivity must be < 50 µS/cm.

', + 'description': '

Final pre-anodize rinse - Tank A-6. Conductivity must be < 50 µS/cm.

', }, { 'name': '11. Hard Anodize Type III (Tank A-9)', 'kind': 'plate', - 'description': '''

HARD ANODIZE — the primary process step.

+ 'description': '''

HARD ANODIZE - the primary process step.

  • Tank: A-9, Bath: HARDANO-1 (15% sulfuric acid)
  • -
  • Temperature: 28–32°F (chilled bath — confirm chiller is running)
  • -
  • Current density: 24–36 ASF
  • -
  • Voltage ramp: 0–80V over first 5 minutes
  • +
  • Temperature: 28-32°F (chilled bath - confirm chiller is running)
  • +
  • Current density: 24-36 ASF
  • +
  • Voltage ramp: 0-80V over first 5 minutes
  • Time at voltage: 60 minutes (gives ~0.002" coating)
  • Record amperage every 15 minutes
  • Check thickness midway with Fischerscope on witness coupon
  • If color reading off, halt and call supervisor
''', }, { - 'name': '12. Rinse — Cold (Tank A-12)', + 'name': '12. Rinse - Cold (Tank A-12)', 'kind': 'rinse', 'description': '

Cold cascade rinse to remove sulfuric residue. Tank A-12.

', }, @@ -166,24 +166,24 @@ STEPS = [ 'kind': 'plate', 'description': '''

Sulfo Black BL dye absorption.

  • Tank: A-14, Bath: DYE-BL-1
  • -
  • Temperature: 130–150°F
  • -
  • Time: 12–18 minutes
  • -
  • Maintain pH 5.0–6.0
  • +
  • Temperature: 130-150°F
  • +
  • Time: 12-18 minutes
  • +
  • Maintain pH 5.0-6.0
  • Visually verify uniform black with no streaks before sealing
''', }, { - 'name': '14. Rinse — Warm (Tank A-15)', + 'name': '14. Rinse - Warm (Tank A-15)', 'kind': 'rinse', 'description': '

Warm rinse before sealing. Tank A-15. ~110°F.

', }, { 'name': '15. Hot Nickel Acetate Seal (Tank A-16)', 'kind': 'bake', - 'description': '''

Nickel acetate seal — locks in dye, improves corrosion resistance.

+ 'description': '''

Nickel acetate seal - locks in dye, improves corrosion resistance.

  • Tank: A-16, Bath: SEAL-NA-1
  • -
  • Temperature: 195–205°F
  • -
  • Time: 18–22 minutes
  • -
  • pH: 5.5–6.0
  • +
  • Temperature: 195-205°F
  • +
  • Time: 18-22 minutes
  • +
  • pH: 5.5-6.0
  • Attach AMS-2759 chart-recorder file as photo before unloading
  • Quality of seal verified post-process by dye absorption test
''', }, @@ -195,10 +195,10 @@ STEPS = [ { 'name': '17. Drying (Hot Air Knife)', 'kind': 'dry', - 'description': '''

Hot-air knife dry — leave parts on rack.

+ 'description': '''

Hot-air knife dry - leave parts on rack.

  • Hot air knife @ 180°F
  • Time: 5 minutes minimum
  • -
  • Verify parts fully dry before unracking — water spotting is a defect
''', +
  • Verify parts fully dry before unracking - water spotting is a defect
  • ''', }, { 'name': '18. De-Racking', @@ -248,7 +248,7 @@ STEPS = [ { 'name': '23. Shipping', 'kind': 'ship', - 'description': '''

    Outbound — confirm carrier and BoL.

    + 'description': '''

    Outbound - confirm carrier and BoL.

    • Carrier per SO (UPS / FedEx / customer pickup)
    • Print BoL, attach to package
    • Photograph sealed shipment for proof-of-shipment
    • @@ -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 '-' )) diff --git a/fusion_plating/fusion_plating/scripts/bt_e2e_anodize.py b/fusion_plating/fusion_plating/scripts/bt_e2e_anodize.py index 643cc323..ce34d606 100644 --- a/fusion_plating/fusion_plating/scripts/bt_e2e_anodize.py +++ b/fusion_plating/fusion_plating/scripts/bt_e2e_anodize.py @@ -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') diff --git a/fusion_plating/fusion_plating/scripts/bt_e2e_anodize_v2.py b/fusion_plating/fusion_plating/scripts/bt_e2e_anodize_v2.py index 93f58166..29b0336c 100644 --- a/fusion_plating/fusion_plating/scripts/bt_e2e_anodize_v2.py +++ b/fusion_plating/fusion_plating/scripts/bt_e2e_anodize_v2.py @@ -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') diff --git a/fusion_plating/fusion_plating/scripts/bt_e2e_recon.py b/fusion_plating/fusion_plating/scripts/bt_e2e_recon.py index 953398ba..5d990709 100644 --- a/fusion_plating/fusion_plating/scripts/bt_e2e_recon.py +++ b/fusion_plating/fusion_plating/scripts/bt_e2e_recon.py @@ -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) diff --git a/fusion_plating/fusion_plating/scripts/bt_e2e_recon2.py b/fusion_plating/fusion_plating/scripts/bt_e2e_recon2.py index 2b3923ab..cbf4702e 100644 --- a/fusion_plating/fusion_plating/scripts/bt_e2e_recon2.py +++ b/fusion_plating/fusion_plating/scripts/bt_e2e_recon2.py @@ -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'] diff --git a/fusion_plating/fusion_plating/scripts/bt_e2e_recon3.py b/fusion_plating/fusion_plating/scripts/bt_e2e_recon3.py index 11d1fe77..4a64ff7e 100644 --- a/fusion_plating/fusion_plating/scripts/bt_e2e_recon3.py +++ b/fusion_plating/fusion_plating/scripts/bt_e2e_recon3.py @@ -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([ diff --git a/fusion_plating/fusion_plating/scripts/bt_e2e_recon4.py b/fusion_plating/fusion_plating/scripts/bt_e2e_recon4.py index e40d4a42..04ed014c 100644 --- a/fusion_plating/fusion_plating/scripts/bt_e2e_recon4.py +++ b/fusion_plating/fusion_plating/scripts/bt_e2e_recon4.py @@ -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) diff --git a/fusion_plating/fusion_plating/scripts/bt_step_library_audit.py b/fusion_plating/fusion_plating/scripts/bt_step_library_audit.py index affd4f3f..29fdf1f0 100644 --- a/fusion_plating/fusion_plating/scripts/bt_step_library_audit.py +++ b/fusion_plating/fusion_plating/scripts/bt_step_library_audit.py @@ -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', diff --git a/fusion_plating/fusion_plating/security/fp_menu_visibility.xml b/fusion_plating/fusion_plating/security/fp_menu_visibility.xml index b3d57e32..bcc7115e 100644 --- a/fusion_plating/fusion_plating/security/fp_menu_visibility.xml +++ b/fusion_plating/fusion_plating/security/fp_menu_visibility.xml @@ -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 @@ 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 @@ - + diff --git a/fusion_plating/fusion_plating/security/fp_security.xml b/fusion_plating/fusion_plating/security/fp_security.xml index d95db8df..c3a5b20b 100644 --- a/fusion_plating/fusion_plating/security/fp_security.xml +++ b/fusion_plating/fusion_plating/security/fp_security.xml @@ -73,10 +73,10 @@ - + - Fusion Plating: Facility — multi-company + Fusion Plating: Facility - multi-company ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)] diff --git a/fusion_plating/fusion_plating/static/src/js/fp_icon_picker.js b/fusion_plating/fusion_plating/static/src/js/fp_icon_picker.js index b6fd3344..e9083cfc 100644 --- a/fusion_plating/fusion_plating/static/src/js/fp_icon_picker.js +++ b/fusion_plating/fusion_plating/static/src/js/fp_icon_picker.js @@ -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 diff --git a/fusion_plating/fusion_plating/static/src/js/recipe_tree_editor.js b/fusion_plating/fusion_plating/static/src/js/recipe_tree_editor.js index 3dd85f9f..9949034b 100644 --- a/fusion_plating/fusion_plating/static/src/js/recipe_tree_editor.js +++ b/fusion_plating/fusion_plating/static/src/js/recipe_tree_editor.js @@ -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. diff --git a/fusion_plating/fusion_plating/static/src/js/simple_recipe_editor.js b/fusion_plating/fusion_plating/static/src/js/simple_recipe_editor.js index 9ff158d3..04b08909 100644 --- a/fusion_plating/fusion_plating/static/src/js/simple_recipe_editor.js +++ b/fusion_plating/fusion_plating/static/src/js/simple_recipe_editor.js @@ -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 ""; diff --git a/fusion_plating/fusion_plating/static/src/scss/fp_chatter_dark.scss b/fusion_plating/fusion_plating/static/src/scss/fp_chatter_dark.scss index d972bdae..8133fb0e 100644 --- a/fusion_plating/fusion_plating/static/src/scss/fp_chatter_dark.scss +++ b/fusion_plating/fusion_plating/static/src/scss/fp_chatter_dark.scss @@ -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 diff --git a/fusion_plating/fusion_plating/static/src/scss/fp_icon_picker.scss b/fusion_plating/fusion_plating/static/src/scss/fp_icon_picker.scss index 819b916c..8212e3cb 100644 --- a/fusion_plating/fusion_plating/static/src/scss/fp_icon_picker.scss +++ b/fusion_plating/fusion_plating/static/src/scss/fp_icon_picker.scss @@ -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 diff --git a/fusion_plating/fusion_plating/static/src/scss/fusion_plating.scss b/fusion_plating/fusion_plating/static/src/scss/fusion_plating.scss index df4c38b8..b85fad39 100644 --- a/fusion_plating/fusion_plating/static/src/scss/fusion_plating.scss +++ b/fusion_plating/fusion_plating/static/src/scss/fusion_plating.scss @@ -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 { diff --git a/fusion_plating/fusion_plating/static/src/scss/recipe_tree_editor.scss b/fusion_plating/fusion_plating/static/src/scss/recipe_tree_editor.scss index d33dad35..943a1852 100644 --- a/fusion_plating/fusion_plating/static/src/scss/recipe_tree_editor.scss +++ b/fusion_plating/fusion_plating/static/src/scss/recipe_tree_editor.scss @@ -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; diff --git a/fusion_plating/fusion_plating/static/src/scss/simple_recipe_editor.scss b/fusion_plating/fusion_plating/static/src/scss/simple_recipe_editor.scss index 81d4014e..0c9d5c8d 100644 --- a/fusion_plating/fusion_plating/static/src/scss/simple_recipe_editor.scss +++ b/fusion_plating/fusion_plating/static/src/scss/simple_recipe_editor.scss @@ -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. diff --git a/fusion_plating/fusion_plating/static/src/xml/recipe_tree_editor.xml b/fusion_plating/fusion_plating/static/src/xml/recipe_tree_editor.xml index 854adf1d..4aba681c 100644 --- a/fusion_plating/fusion_plating/static/src/xml/recipe_tree_editor.xml +++ b/fusion_plating/fusion_plating/static/src/xml/recipe_tree_editor.xml @@ -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 @@ - +
      - +
      @@ -383,7 +383,7 @@ t-on-change="(ev) => { state.selectedNode.triggers_workflow_state_id = ev.target.value ? (+ev.target.value) : false; }"> - Required — every job runs this step. - Opt-Out — ships included, estimator can remove per job. - Opt-In — ships excluded, estimator can add per job. + Required - every job runs this step. + Opt-Out - ships included, estimator can remove per job. + Opt-In - ships excluded, estimator can add per job.
      diff --git a/fusion_plating/fusion_plating/static/src/xml/simple_recipe_editor.xml b/fusion_plating/fusion_plating/static/src/xml/simple_recipe_editor.xml index 147a10b9..66a73c66 100644 --- a/fusion_plating/fusion_plating/static/src/xml/simple_recipe_editor.xml +++ b/fusion_plating/fusion_plating/static/src/xml/simple_recipe_editor.xml @@ -30,7 +30,7 @@