This commit is contained in:
gsinghpal
2026-05-04 02:14:34 -04:00
parent 3cc393454d
commit 586f05d567
43 changed files with 3656 additions and 112 deletions

View File

@@ -36,6 +36,7 @@ from . import hr_employee
from . import fp_process_node_inherit
# 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

View File

@@ -373,34 +373,16 @@ class FpProcessNode(models.Model):
string='Requires Transition Form',
help='Sub 12b — opens the transition form before Mark Done.',
)
default_kind = fields.Selection(
[
('receiving', 'Receiving / Incoming Inspection'),
('contract_review', 'Contract Review (QA-005)'),
('racking', 'Racking'),
('mask', 'Masking'),
('cleaning', 'Cleaning'),
('electroclean', 'Electroclean'),
('etch', 'Etch / Activation'),
('rinse', 'Rinse'),
('strike', 'Strike (Wood\'s Nickel / Activation)'),
('plate', 'Plating'),
('replenishment', 'Tank Replenishment'),
('wbf_test', 'Water Break Free Test'),
('dry', 'Drying'),
('bake', 'Bake (HE Relief / Stress Relief)'),
('demask', 'De-Masking'),
('derack', 'De-Racking'),
('inspect', 'Inspection'),
('hardness_test', 'Hardness Test (HV / HK / HRC)'),
('adhesion_test', 'Adhesion Test'),
('salt_spray', 'Salt Spray / Corrosion Test'),
('final_inspect', 'Final Inspection'),
('packaging', 'Packaging / Pre-Ship'),
('ship', 'Shipping'),
('gating', 'Gating'),
],
string='Step Kind',
# Sub 14b — User-extensible Step Kinds (was Selection of 24).
kind_id = fields.Many2one(
'fp.step.kind', string='Step Kind', ondelete='set null', index=True,
help='Pick from the catalog or create a new kind.',
)
# Back-compat: code-string accessor that all legacy
# `node.default_kind == "cleaning"` comparisons keep using.
default_kind = fields.Char(
related='kind_id.code', store=True, readonly=True, index=True,
string='Step Kind Code',
)
preferred_editor = fields.Selection(
[

View File

@@ -0,0 +1,282 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
from odoo import _, api, fields, models
from ._fp_uom_selection import FP_UOM_SELECTION
class FpStepKind(models.Model):
"""User-extensible Step Kind catalog.
Replaces the hardcoded `default_kind` Selection on fp.step.template
and fusion.plating.process.node. Each kind carries a list of default
inputs that get seeded onto a step template when the kind is picked.
"""
_name = 'fp.step.kind'
_description = 'Fusion Plating — Step Kind'
_order = 'sequence, name'
code = fields.Char(
string='Code', required=True, index=True,
help='Stable lowercase technical key. Used by automation rules and '
'workflow-state triggers (e.g. "cleaning", "plate"). Lower-case'
' enforced; underscores allowed.',
)
name = fields.Char(string='Name', required=True, translate=True)
sequence = fields.Integer(string='Sequence', default=10)
active = fields.Boolean(string='Active', default=True)
description = fields.Html(string='Description')
icon = fields.Selection(
selection='_get_icon_selection',
string='Icon',
default='fa-cog',
)
company_id = fields.Many2one(
'res.company', string='Company',
default=lambda self: self.env.company,
)
default_input_ids = fields.One2many(
'fp.step.kind.default.input', 'kind_id',
string='Default Inputs', copy=True,
help='Auto-seeded onto a step template when this kind is picked '
'(via the "Seed Defaults" action).',
)
template_count = fields.Integer(
string='Templates', compute='_compute_template_count')
_sql_constraints = [
('fp_step_kind_code_company_uniq',
'unique(code, company_id)',
'Step kind code must be unique within a company.'),
]
# Curated FontAwesome 4 icon catalog for the visual icon picker.
# 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 = [
# Process / chemistry
('fa-flask', 'Flask / Chemistry'),
('fa-tint', 'Drop / Liquid'),
('fa-tachometer', 'Gauge / Measure'),
('fa-thermometer-half', 'Temp / Heat'),
('fa-fire', 'Fire / Bake'),
('fa-snowflake-o', 'Cold / Freeze'),
('fa-bolt', 'Bolt / Electric'),
('fa-plug', 'Plug / Power'),
('fa-magnet', 'Magnet'),
('fa-bullseye', 'Target / Blast'),
('fa-shower', 'Shower / Clean'),
('fa-bathtub', 'Bathtub / Soak'),
('fa-tachometer', 'Tachometer'),
# Equipment / shop
('fa-industry', 'Industry / Line'),
('fa-wrench', 'Wrench / Operation'),
('fa-cog', 'Gear / General'),
('fa-cogs', 'Gears / System'),
('fa-sitemap', 'Sitemap / Process'),
('fa-cubes', 'Cubes / Batch'),
('fa-cube', 'Cube / Part'),
('fa-th', 'Grid / Racking'),
('fa-th-large', 'Large Grid'),
('fa-server', 'Server / Rack'),
('fa-database', 'Database / Tank'),
('fa-archive', 'Archive / Box'),
('fa-recycle', 'Recycle / Reuse'),
('fa-balance-scale', 'Scale / Balance'),
# Masking / surface
('fa-paint-brush', 'Paint / Masking'),
('fa-eraser', 'Eraser / De-Masking'),
('fa-shield', 'Shield / Protect'),
('fa-diamond', 'Diamond / Plating'),
('fa-circle-o-notch', 'Coating Layer'),
# Inspection / quality
('fa-search', 'Search / Inspect'),
('fa-search-plus', 'Search Plus'),
('fa-eye', 'Eye / Visual'),
('fa-eye-slash', 'Eye Slash / Hidden'),
('fa-camera', 'Camera / Photo'),
('fa-check', 'Check'),
('fa-check-circle', 'Check / Approve'),
('fa-check-square-o', 'Checkbox'),
('fa-times', 'Reject / Cancel'),
('fa-times-circle', 'Reject Circle'),
('fa-exclamation-triangle', 'Warning'),
('fa-exclamation-circle', 'Alert'),
('fa-info-circle', 'Info'),
('fa-question-circle', 'Question'),
('fa-bug', 'Bug / Defect'),
('fa-flag', 'Flag'),
('fa-flag-checkered', 'Finish / Done'),
('fa-trophy', 'Trophy / Pass'),
('fa-thumbs-up', 'Thumbs Up'),
('fa-thumbs-down', 'Thumbs Down'),
('fa-star', 'Star'),
('fa-bookmark', 'Bookmark'),
('fa-certificate', 'Certificate'),
# Time
('fa-clock-o', 'Clock / Wait'),
('fa-hourglass-half', 'Hourglass'),
('fa-hourglass-end', 'Hourglass End'),
('fa-calendar', 'Calendar'),
('fa-calendar-check-o', 'Scheduled'),
('fa-history', 'History'),
# Safety / handling
('fa-hand-paper-o', 'Hand / Manual'),
('fa-hand-stop-o', 'Stop Hand'),
('fa-life-ring', 'Safety / Life Ring'),
('fa-medkit', 'First Aid'),
('fa-user-md', 'Inspector'),
('fa-lock', 'Lock / Hold'),
('fa-unlock', 'Unlock / Release'),
('fa-key', 'Key'),
# Documentation / certs
('fa-file-text-o', 'Document'),
('fa-file-pdf-o', 'PDF'),
('fa-file-image-o', 'Image File'),
('fa-clipboard', 'Clipboard'),
('fa-list-alt', 'Checklist'),
('fa-list-ul', 'List'),
('fa-tags', 'Tags'),
('fa-tag', 'Tag'),
('fa-barcode', 'Barcode'),
('fa-qrcode', 'QR Code'),
('fa-pencil', 'Pencil'),
('fa-edit', 'Edit'),
('fa-print', 'Print'),
('fa-paperclip', 'Attach'),
# Shipping / logistics
('fa-truck', 'Truck / Receiving'),
('fa-paper-plane', 'Ship / Send'),
('fa-plane', 'Plane / Airfreight'),
('fa-ship', 'Ship'),
('fa-shopping-cart', 'Cart'),
('fa-shopping-bag', 'Bag / Pack'),
('fa-gift', 'Gift / Package'),
('fa-suitcase', 'Suitcase'),
('fa-globe', 'Global'),
('fa-map-marker', 'Location'),
('fa-road', 'In Transit'),
# Status / process flow
('fa-play-circle', 'Start'),
('fa-pause-circle', 'Pause / Hold'),
('fa-stop-circle', 'Stop'),
('fa-step-forward', 'Step Forward'),
('fa-fast-forward', 'Fast Forward'),
('fa-refresh', 'Refresh / Repeat'),
('fa-undo', 'Undo / Rework'),
('fa-share', 'Hand-off'),
('fa-arrow-right', 'Arrow Right'),
('fa-arrow-down', 'Arrow Down'),
('fa-long-arrow-right', 'Long Arrow Right'),
('fa-random', 'Random / Mix'),
('fa-exchange', 'Exchange'),
('fa-sort-amount-asc', 'Sort Asc'),
('fa-sort-amount-desc', 'Sort Desc'),
('fa-tasks', 'Tasks'),
# Misc useful
('fa-sun-o', 'Sun / Dry'),
('fa-moon-o', 'Moon / Night'),
('fa-cloud', 'Cloud'),
('fa-leaf', 'Leaf / Eco'),
('fa-tree', 'Tree'),
('fa-bell', 'Bell / Alert'),
('fa-bullhorn', 'Announce'),
('fa-trash', 'Trash / Discard'),
('fa-plus-circle', 'Add'),
('fa-minus-circle', 'Remove'),
('fa-circle', 'Circle'),
('fa-square', 'Square'),
('fa-asterisk', 'Asterisk'),
('fa-cutlery', 'Cutlery / Bend'),
('fa-link', 'Link / Adhesion'),
('fa-chain-broken', 'Broken Chain'),
('fa-anchor', 'Anchor'),
('fa-ban', 'Ban / Forbidden'),
]
@api.model
def _get_icon_selection(self):
return self._ICON_SELECTION
@api.depends()
def _compute_template_count(self):
Tpl = self.env['fp.step.template']
for k in self:
k.template_count = Tpl.search_count([('kind_id', '=', k.id)])
@api.model_create_multi
def create(self, vals_list):
for v in vals_list:
if v.get('code'):
v['code'] = v['code'].lower().strip().replace(' ', '_')
return super().create(vals_list)
def write(self, vals):
if vals.get('code'):
vals['code'] = vals['code'].lower().strip().replace(' ', '_')
return super().write(vals)
def action_open_templates(self):
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': _('Step Templates — %s') % self.name,
'res_model': 'fp.step.template',
'view_mode': 'list,form',
'domain': [('kind_id', '=', self.id)],
'context': {'default_kind_id': self.id},
}
class FpStepKindDefaultInput(models.Model):
"""Default input prototype attached to a step kind.
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).
"""
_name = 'fp.step.kind.default.input'
_description = 'Fusion Plating — Step Kind Default Input'
_order = 'sequence, name'
name = fields.Char(string='Name', required=True, translate=True)
kind_id = fields.Many2one(
'fp.step.kind', string='Kind',
required=True, ondelete='cascade', index=True,
)
input_type = fields.Selection([
('text', 'Text'),
('number', 'Number'),
('boolean', 'Yes/No'),
('selection', 'Selection'),
('date', 'Date / Time'),
('signature', 'Signature'),
('time_hms', 'Time (HH:MM:SS)'),
('time_seconds', 'Time (seconds)'),
('temperature', 'Temperature'),
('thickness', 'Thickness'),
('pass_fail', 'Pass / Fail'),
('photo', 'Photo'),
('multi_point_thickness', 'Multi-Point Thickness (avg)'),
('bath_chemistry_panel', 'Bath Chemistry Panel'),
('ph', 'pH'),
], string='Input Type', required=True, default='text')
target_unit = fields.Selection(FP_UOM_SELECTION, string='Target Unit')
required = fields.Boolean(string='Required', default=False)
hint = fields.Char(string='Hint')
selection_options = fields.Text(string='Selection Options',
help='Comma-separated when input_type is "selection".')
sequence = fields.Integer(string='Sequence', default=10)

View File

@@ -88,32 +88,21 @@ class FpStepTemplate(models.Model):
requires_transition_form = fields.Boolean(string='Requires Transition Form',
help='Opens the transition form before Mark Done (Sub 12b).')
default_kind = fields.Selection([
('receiving', 'Receiving / Incoming Inspection'),
('contract_review', 'Contract Review (QA-005)'),
('racking', 'Racking'),
('mask', 'Masking'),
('cleaning', 'Cleaning'),
('electroclean', 'Electroclean'),
('etch', 'Etch / Activation'),
('rinse', 'Rinse'),
('strike', 'Strike (Wood\'s Nickel / Activation)'),
('plate', 'Plating'),
('replenishment', 'Tank Replenishment'),
('wbf_test', 'Water Break Free Test'),
('dry', 'Drying'),
('bake', 'Bake (HE Relief / Stress Relief)'),
('demask', 'De-Masking'),
('derack', 'De-Racking'),
('inspect', 'Inspection'),
('hardness_test', 'Hardness Test (HV / HK / HRC)'),
('adhesion_test', 'Adhesion Test'),
('salt_spray', 'Salt Spray / Corrosion Test'),
('final_inspect', 'Final Inspection'),
('packaging', 'Packaging / Pre-Ship'),
('ship', 'Shipping'),
('gating', 'Gating'),
], string='Step Kind', help='Drives sane-default input seeding.')
# Sub 14b — User-extensible Step Kinds (was Selection of 24).
kind_id = fields.Many2one(
'fp.step.kind', string='Step Kind', ondelete='restrict',
index=True, tracking=True,
help='Pick from the catalog or create a new kind. Drives sane-'
'default input seeding.',
)
# 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.
default_kind = fields.Char(
related='kind_id.code', store=True, readonly=True, index=True,
string='Step Kind Code',
)
input_template_ids = fields.One2many(
'fp.step.template.input', 'template_id',
@@ -152,13 +141,11 @@ class FpStepTemplate(models.Model):
return super().write(vals)
# ----- Sane defaults seeding ---------------------------------------------
# NB target_unit must be a valid FP_UOM_SELECTION key — it became a
# Selection in 19.0.12.1.0 (uom cleanup). Free-text values like
# 'HH:MM', '°F', 'sec', 'in', 'each' raise ValueError on create.
# Mapping cheatsheet: sec → 's', °F → 'f', °C → 'c', in → 'in',
# each → 'each', min → 'min'. Format-only strings ('HH:MM') get
# left blank since they're not units.
# 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
# migration). It will be removed in a future version.
DEFAULT_INPUTS_BY_KIND = {
'receiving': [
{'name': 'Qty Received', 'input_type': 'number',
@@ -419,19 +406,37 @@ 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.
_KIND_DEFAULT_INPUT_FIELDS = (
'name', 'input_type', 'target_unit', 'required',
'hint', 'selection_options', 'sequence',
)
def action_seed_default_inputs(self):
"""Seed input_template_ids based on default_kind. Idempotent —
only adds inputs whose names don't already exist on this template.
"""Seed input_template_ids from kind_id.default_input_ids.
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).
Public method (Odoo 19 requires non-underscore-prefixed names
for methods called from a view button).
"""
Input = self.env['fp.step.template.input']
for tpl in self:
if not tpl.default_kind:
continue
existing_names = set(tpl.input_template_ids.mapped('name'))
for spec in self.DEFAULT_INPUTS_BY_KIND.get(tpl.default_kind, []):
specs = []
if tpl.kind_id:
for d in tpl.kind_id.default_input_ids:
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.
specs = self.DEFAULT_INPUTS_BY_KIND.get(tpl.default_kind, [])
for spec in specs:
if spec['name'] in existing_names:
continue
Input.create({