From 33ddec926cf4ad0095681cd51eb85b89ffa7b1a0 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Mon, 27 Apr 2026 20:36:55 -0400 Subject: [PATCH] =?UTF-8?q?feat(sub12a):=20post=5Finit=5Fhook=20=E2=80=94?= =?UTF-8?q?=20backfill=20kind=20+=20seed=20step=20library?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends the existing post_init_hook to also do (idempotent) Sub 12a work on first install / upgrade: 1. Backfill kind='step_input' on existing fusion_plating_process_node_input rows where kind IS NULL. 2. Seed fp.step.template from the ENP-ALUM-BASIC recipe's child nodes if the library is currently empty. Uses _STARTER_KIND_BY_NAME to map recipe-step names to default_kind values, then calls _seed_default_inputs() to populate the per-kind input rows. 3. Falls back to a 15-entry hard-coded minimal seed list if ENP-ALUM-BASIC doesn't exist on the target DB. All three operations no-op when the relevant state already exists. Co-Authored-By: Claude Opus 4.7 (1M context) --- fusion_plating/fusion_plating/__init__.py | 167 +++++++++++++++++++++- 1 file changed, 161 insertions(+), 6 deletions(-) diff --git a/fusion_plating/fusion_plating/__init__.py b/fusion_plating/fusion_plating/__init__.py index b237c4b8..bd8e7014 100644 --- a/fusion_plating/fusion_plating/__init__.py +++ b/fusion_plating/fusion_plating/__init__.py @@ -12,14 +12,23 @@ _logger = logging.getLogger(__name__) def post_init_hook(env): - """Auto-detect a sensible default timezone on first install. + """Run on first install / module upgrade. Idempotent. - Sets ``res.company.x_fc_default_tz`` to the admin user's timezone - (Odoo populates that from the browser on first login), falling back - to the host server's timezone, then to ``America/Toronto`` as a - last resort. Only writes when the field is still empty so re-installs - never clobber a user's choice. + 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 + fusion.plating.process.node.input rows that pre-date the + `kind` field. + 3. Sub 12a — seed fp.step.template with starter library entries + derived from ENP-ALUM-BASIC if the library is currently empty. """ + _seed_default_timezone(env) + _backfill_node_input_kind(env) + _seed_step_library_if_empty(env) + + +def _seed_default_timezone(env): from .models.fp_tz import detect_default_tz detected = detect_default_tz(env) @@ -30,3 +39,149 @@ def post_init_hook(env): 'Fusion Plating: set default timezone for company %s -> %s', company.name, detected, ) + + +def _backfill_node_input_kind(env): + """Sub 12a — set kind='step_input' on rows that have NULL kind.""" + cr = env.cr + cr.execute( + "UPDATE fusion_plating_process_node_input " + "SET kind = 'step_input' WHERE kind IS NULL" + ) + if cr.rowcount: + _logger.info( + "Fusion Plating: backfilled kind='step_input' on %s " + "fusion.plating.process.node.input rows", cr.rowcount, + ) + + +# Mapping of recipe-step name → default_kind. Drives sane-default +# input seeding on the starter library entries. +_STARTER_KIND_BY_NAME = { + 'soak clean': 'cleaning', + 'electroclean': 'cleaning', + 'solvent clean': 'cleaning', + 'rinse': 'rinse', + 'primary rinse': 'rinse', + 'secondary rinse': 'rinse', + 'hot rinse': 'rinse', + 'final rinse': 'rinse', + 'etch': 'etch', + 'desmut': 'etch', + 'zincate': 'etch', + 'strip zincate': 'etch', + 'acid dip': 'etch', + 'water break test': 'wbf_test', + 'issue panels': 'mask', + 'racking': 'racking', + 'rack': 'racking', + 'e-nickel plate': 'plate', + 'electroless nickel plate': 'plate', + 'electroless nickel plating': 'plate', + 'drying': 'dry', + 'dry': 'dry', + 'de-rack': 'derack', + 'de-racking': 'derack', + 'inspection': 'inspect', + 'final inspection': 'final_inspect', + 'shipping': 'ship', +} + + +def _seed_step_library_if_empty(env): + """Sub 12a — seed fp.step.template starter library. + + Source priority: + 1. ENP-ALUM-BASIC recipe's child nodes (best — reuses the + author-curated step set). + 2. Hard-coded minimal list (fallback for fresh DBs). + """ + Tpl = env['fp.step.template'] + if Tpl.search_count([]): + _logger.info( + 'Fusion Plating: step library already populated, skip seed', + ) + return + + Node = env['fusion.plating.process.node'] + src = Node.search([ + ('node_type', '=', 'recipe'), + '|', ('code', '=', 'ENP-ALUM-BASIC'), + ('name', 'ilike', 'ENP-ALUM-BASIC'), + ], limit=1) + + if not src: + _seed_minimal_library(env) + return + + seen = set() + for child in src.child_ids: + if child.node_type == 'step': + _create_template_from_node(env, child, seen) + else: + for grandchild in child.child_ids: + _create_template_from_node(env, grandchild, seen) + + _logger.info( + "Fusion Plating: seeded step library with %s entries from %s", + len(seen), src.name, + ) + + +def _create_template_from_node(env, node, seen): + if not node.name or node.name.lower() in seen: + return + seen.add(node.name.lower()) + + kind = _STARTER_KIND_BY_NAME.get(node.name.lower()) + vals = { + 'name': node.name, + 'description': node.description or False, + 'icon': node.icon or 'fa-cog', + 'process_type_id': node.process_type_id.id, + 'requires_signoff': node.requires_signoff, + 'requires_predecessor_done': node.requires_predecessor_done, + 'default_kind': kind, + } + # Snapshot tank_ids if the node has them (added by Sub 12a; + # existing nodes may not). + if 'tank_ids' in node._fields and node.tank_ids: + vals['tank_ids'] = [(6, 0, node.tank_ids.ids)] + # Snapshot any time/temp targets the node may already carry. + for f in ('time_min_target', 'time_max_target', 'time_unit', + 'temp_min_target', 'temp_max_target', 'temp_unit'): + if f in node._fields: + vals[f] = node[f] or vals.get(f) + + tpl = env['fp.step.template'].create(vals) + if kind: + tpl._seed_default_inputs() + + +def _seed_minimal_library(env): + """Hard-coded minimal seed when ENP-ALUM-BASIC isn't on the target DB.""" + Tpl = env['fp.step.template'] + minimal = [ + ('Soak Clean', 'cleaning'), + ('Electroclean', 'cleaning'), + ('Rinse', 'rinse'), + ('Etch', 'etch'), + ('Desmut', 'etch'), + ('Zincate', 'etch'), + ('Acid Dip', 'etch'), + ('Water Break Test', 'wbf_test'), + ('Racking', 'racking'), + ('De-Racking', 'derack'), + ('E-Nickel Plate', 'plate'), + ('Drying', 'dry'), + ('Inspection', 'inspect'), + ('Final Inspection', 'final_inspect'), + ('Shipping', 'ship'), + ] + for name, kind in minimal: + tpl = Tpl.create({'name': name, 'default_kind': kind}) + tpl._seed_default_inputs() + _logger.info( + 'Fusion Plating: seeded minimal step library (%s entries)', + len(minimal), + )