feat(sub12a): post_init_hook — backfill kind + seed step library

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) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-27 20:36:55 -04:00
parent 0862e55de6
commit 33ddec926c

View File

@@ -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),
)