419 lines
10 KiB
Python
419 lines
10 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright 2026 Nexa Systems Inc.
|
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
|
# Part of the Fusion Plating product family.
|
|
|
|
"""Shared Unit-of-Measure selection list for plating chemistry, physical
|
|
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
|
|
pick from a known list instead of typing.
|
|
|
|
Re-use:
|
|
from .._fp_uom_selection import FP_UOM_SELECTION, FP_UOM_LEGACY_MAP
|
|
|
|
uom = fields.Selection(FP_UOM_SELECTION, string='Unit')
|
|
|
|
Migration:
|
|
Use FP_UOM_LEGACY_MAP to translate pre-existing free-text values
|
|
into selection keys during post_init / migration. Anything not in
|
|
the map gets cleared (NULL) so the user is forced to pick.
|
|
"""
|
|
|
|
# Single source of truth — keep alphabetised within each section.
|
|
FP_UOM_SELECTION = [
|
|
# --- Concentration / chemistry ---------------------------------------
|
|
('g_l', 'g/L'),
|
|
('mg_l', 'mg/L'),
|
|
('ug_l', 'µg/L'),
|
|
('kg_l', 'kg/L'),
|
|
('oz_gal', 'oz/gal (US)'),
|
|
('oz_gal_imp', 'oz/Imp gal'),
|
|
('ml_l', 'mL/L'),
|
|
('mol_l', 'mol/L'),
|
|
('n', 'N (Normality)'),
|
|
('ppm', 'ppm'),
|
|
('ppb', 'ppb'),
|
|
('pct', '%'),
|
|
('pct_w', '% (w/w)'),
|
|
('pct_v', '% (v/v)'),
|
|
('pct_vw', '% (v/w)'),
|
|
|
|
# --- Temperature -----------------------------------------------------
|
|
('c', '°C'),
|
|
('f', '°F'),
|
|
('k', 'K'),
|
|
|
|
# --- Dimensionless / pH / specific units -----------------------------
|
|
('ph', 'pH'),
|
|
('su', 'SU (Standard Units)'),
|
|
('ratio', 'Ratio (e.g. 5:1)'),
|
|
('none', '— (none)'),
|
|
|
|
# --- Conductivity / turbidity ----------------------------------------
|
|
('us_cm', 'µS/cm'),
|
|
('ms_cm', 'mS/cm'),
|
|
('ntu', 'NTU'),
|
|
|
|
# --- Time ------------------------------------------------------------
|
|
('s', 's (seconds)'),
|
|
('min', 'min'),
|
|
('h', 'h'),
|
|
('day', 'day'),
|
|
|
|
# --- Mass ------------------------------------------------------------
|
|
('mg', 'mg'),
|
|
('g', 'g'),
|
|
('kg', 'kg'),
|
|
('t', 't (tonne)'),
|
|
('oz', 'oz'),
|
|
('lb', 'lb'),
|
|
|
|
# --- Volume ----------------------------------------------------------
|
|
('ml', 'mL'),
|
|
('l', 'L'),
|
|
('m3', 'm³'),
|
|
('gal_us', 'US gal'),
|
|
('gal_imp', 'Imp gal'),
|
|
('ft3', 'ft³'),
|
|
|
|
# --- Length / thickness ----------------------------------------------
|
|
('nm', 'nm'),
|
|
('um', 'µm'),
|
|
('mm', 'mm'),
|
|
('cm', 'cm'),
|
|
('m', 'm'),
|
|
('mil', 'mil (0.001 in)'),
|
|
('in', 'in'),
|
|
('ft', 'ft'),
|
|
|
|
# --- Area ------------------------------------------------------------
|
|
('cm2', 'cm²'),
|
|
('m2', 'm²'),
|
|
('in2', 'in²'),
|
|
('ft2', 'ft²'),
|
|
('dm2', 'dm²'),
|
|
|
|
# --- Electrical / current density ------------------------------------
|
|
('a', 'A'),
|
|
('ma', 'mA'),
|
|
('v', 'V'),
|
|
('asd_a_dm2', 'A/dm² (ASD)'),
|
|
('asd_a_ft2', 'A/ft² (ASF)'),
|
|
('dm2_l', 'dm²/L (load)'),
|
|
|
|
# --- Pressure --------------------------------------------------------
|
|
('pa', 'Pa'),
|
|
('kpa', 'kPa'),
|
|
('bar', 'bar'),
|
|
('psi', 'psi'),
|
|
('mmhg', 'mmHg'),
|
|
|
|
# --- Rate / flow / generation ----------------------------------------
|
|
('kg_day', 'kg/day'),
|
|
('l_day', 'L/day'),
|
|
('kg_month', 'kg/month'),
|
|
('l_min', 'L/min'),
|
|
('gpm', 'gpm'),
|
|
('cfm', 'cfm'),
|
|
|
|
# --- Exposure / occupational hygiene ---------------------------------
|
|
('mg_m3', 'mg/m³'),
|
|
('ug_m3', 'µg/m³'),
|
|
('dba', 'dBA'),
|
|
('lux', 'lux'),
|
|
|
|
# --- Plating-specific counts -----------------------------------------
|
|
('mto', 'MTO (metal turnover)'),
|
|
('cycles', 'cycles'),
|
|
('count', 'count'),
|
|
('each', 'each'),
|
|
('rpm', 'rpm'),
|
|
]
|
|
|
|
|
|
# Map free-text values produced before this list existed → selection keys.
|
|
# Keep keys lower-cased + stripped during lookup.
|
|
FP_UOM_LEGACY_MAP = {
|
|
# Concentration
|
|
'g/l': 'g_l',
|
|
'gpl': 'g_l',
|
|
'grams/l': 'g_l',
|
|
'g per l': 'g_l',
|
|
'mg/l': 'mg_l',
|
|
'ug/l': 'ug_l',
|
|
'µg/l': 'ug_l',
|
|
'kg/l': 'kg_l',
|
|
'oz/gal': 'oz_gal',
|
|
'oz/g': 'oz_gal',
|
|
'oz/gallon': 'oz_gal',
|
|
'oz/imp gal': 'oz_gal_imp',
|
|
'ml/l': 'ml_l',
|
|
'mol/l': 'mol_l',
|
|
'molar': 'mol_l',
|
|
'm': 'mol_l',
|
|
'n': 'n',
|
|
'normal': 'n',
|
|
'normality': 'n',
|
|
'ppm': 'ppm',
|
|
'ppb': 'ppb',
|
|
'%': 'pct',
|
|
'percent': 'pct',
|
|
'pct': 'pct',
|
|
'% w/w': 'pct_w',
|
|
'%(w/w)': 'pct_w',
|
|
'%w/w': 'pct_w',
|
|
'% v/v': 'pct_v',
|
|
'%v/v': 'pct_v',
|
|
'% v/w': 'pct_vw',
|
|
|
|
# Temperature
|
|
'c': 'c',
|
|
'°c': 'c',
|
|
'celsius': 'c',
|
|
'deg c': 'c',
|
|
'degc': 'c',
|
|
'f': 'f',
|
|
'°f': 'f',
|
|
'fahrenheit': 'f',
|
|
'deg f': 'f',
|
|
'degf': 'f',
|
|
'k': 'k',
|
|
'kelvin': 'k',
|
|
|
|
# Dimensionless
|
|
'ph': 'ph',
|
|
'su': 'su',
|
|
'standard units': 'su',
|
|
'ratio': 'ratio',
|
|
'-': 'none',
|
|
'none': 'none',
|
|
|
|
# Conductivity / turbidity
|
|
'us/cm': 'us_cm',
|
|
'µs/cm': 'us_cm',
|
|
'ms/cm': 'ms_cm',
|
|
'ntu': 'ntu',
|
|
|
|
# Time
|
|
'second': 's',
|
|
'seconds': 's',
|
|
'sec': 's',
|
|
'secs': 's',
|
|
's': 's',
|
|
'minute': 'min',
|
|
'minutes': 'min',
|
|
'min': 'min',
|
|
'mins': 'min',
|
|
'hour': 'h',
|
|
'hours': 'h',
|
|
'hr': 'h',
|
|
'hrs': 'h',
|
|
'h': 'h',
|
|
'day': 'day',
|
|
'days': 'day',
|
|
'd': 'day',
|
|
|
|
# Mass
|
|
'mg': 'mg',
|
|
'g': 'g',
|
|
'gr': 'g',
|
|
'gram': 'g',
|
|
'grams': 'g',
|
|
'kg': 'kg',
|
|
'kgs': 'kg',
|
|
'kilogram': 'kg',
|
|
'kilograms': 'kg',
|
|
't': 't',
|
|
'tonne': 't',
|
|
'tonnes': 't',
|
|
'metric ton': 't',
|
|
'oz': 'oz',
|
|
'ounce': 'oz',
|
|
'ounces': 'oz',
|
|
'lb': 'lb',
|
|
'lbs': 'lb',
|
|
'pound': 'lb',
|
|
'pounds': 'lb',
|
|
|
|
# Volume
|
|
'ml': 'ml',
|
|
'l': 'l',
|
|
'liter': 'l',
|
|
'liters': 'l',
|
|
'litre': 'l',
|
|
'litres': 'l',
|
|
'm3': 'm3',
|
|
'm³': 'm3',
|
|
'cubic meter': 'm3',
|
|
'gal': 'gal_us',
|
|
'gal_us': 'gal_us',
|
|
'us gal': 'gal_us',
|
|
'gallon': 'gal_us',
|
|
'gallons': 'gal_us',
|
|
'imp gal': 'gal_imp',
|
|
'imperial gallon': 'gal_imp',
|
|
'ft3': 'ft3',
|
|
'ft³': 'ft3',
|
|
'cubic feet': 'ft3',
|
|
'cu ft': 'ft3',
|
|
|
|
# Length
|
|
'nm': 'nm',
|
|
'um': 'um',
|
|
'µm': 'um',
|
|
'micron': 'um',
|
|
'mm': 'mm',
|
|
'cm': 'cm',
|
|
'mil': 'mil',
|
|
'in': 'in',
|
|
'inch': 'in',
|
|
'inches': 'in',
|
|
'"': 'in',
|
|
'ft': 'ft',
|
|
'feet': 'ft',
|
|
'foot': 'ft',
|
|
|
|
# Area
|
|
'cm2': 'cm2',
|
|
'cm²': 'cm2',
|
|
'm2': 'm2',
|
|
'm²': 'm2',
|
|
'in2': 'in2',
|
|
'in²': 'in2',
|
|
'sq in': 'in2',
|
|
'ft2': 'ft2',
|
|
'ft²': 'ft2',
|
|
'sq ft': 'ft2',
|
|
'dm2': 'dm2',
|
|
'dm²': 'dm2',
|
|
|
|
# Electrical
|
|
'a': 'a',
|
|
'amp': 'a',
|
|
'amps': 'a',
|
|
'ampere': 'a',
|
|
'amperes': 'a',
|
|
'ma': 'ma',
|
|
'milliamp': 'ma',
|
|
'milliamps': 'ma',
|
|
'v': 'v',
|
|
'volt': 'v',
|
|
'volts': 'v',
|
|
'a/dm2': 'asd_a_dm2',
|
|
'a/dm²': 'asd_a_dm2',
|
|
'asd': 'asd_a_dm2',
|
|
'a/ft2': 'asd_a_ft2',
|
|
'a/ft²': 'asd_a_ft2',
|
|
'asf': 'asd_a_ft2',
|
|
'dm2/l': 'dm2_l',
|
|
'dm²/l': 'dm2_l',
|
|
|
|
# Pressure
|
|
'pa': 'pa',
|
|
'kpa': 'kpa',
|
|
'bar': 'bar',
|
|
'psi': 'psi',
|
|
'mmhg': 'mmhg',
|
|
|
|
# Rate
|
|
'kg/day': 'kg_day',
|
|
'l/day': 'l_day',
|
|
'kg/month': 'kg_month',
|
|
'l/min': 'l_min',
|
|
'lpm': 'l_min',
|
|
'gpm': 'gpm',
|
|
'cfm': 'cfm',
|
|
|
|
# Exposure
|
|
'mg/m3': 'mg_m3',
|
|
'mg/m³': 'mg_m3',
|
|
'ug/m3': 'ug_m3',
|
|
'µg/m³': 'ug_m3',
|
|
'dba': 'dba',
|
|
'db': 'dba',
|
|
'lux': 'lux',
|
|
|
|
# Plating counts
|
|
'mto': 'mto',
|
|
'cycle': 'cycles',
|
|
'cycles': 'cycles',
|
|
'count': 'count',
|
|
'each': 'each',
|
|
'ea': 'each',
|
|
'pcs': 'each',
|
|
'pieces': 'each',
|
|
'rpm': 'rpm',
|
|
}
|
|
|
|
|
|
def fp_normalize_legacy_uom(raw_value):
|
|
"""Translate a legacy free-text UoM string to a selection key.
|
|
|
|
Returns the selection key, or None if no match (caller decides whether
|
|
to NULL the column or leave it).
|
|
"""
|
|
if raw_value is None:
|
|
return None
|
|
key = (raw_value or '').strip().lower()
|
|
if not key:
|
|
return None
|
|
return FP_UOM_LEGACY_MAP.get(key)
|
|
|
|
|
|
def fp_migrate_uom_column(env, table, column, label_for_log=None):
|
|
"""Walk a table's free-text uom column and rewrite values into the
|
|
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
|
|
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').
|
|
|
|
Args:
|
|
env: Odoo environment.
|
|
table: SQL table name (e.g. 'fusion_plating_bath_parameter').
|
|
column: SQL column name (e.g. 'uom').
|
|
label_for_log: human-readable name for the migration log line.
|
|
"""
|
|
cr = env.cr
|
|
cr.execute(
|
|
"SELECT 1 FROM information_schema.columns "
|
|
"WHERE table_name = %s AND column_name = %s",
|
|
(table, column),
|
|
)
|
|
if not cr.fetchone():
|
|
return 0, 0 # table/column not present (module not installed)
|
|
cr.execute(f'SELECT id, "{column}" FROM "{table}" WHERE "{column}" IS NOT NULL')
|
|
rows = cr.fetchall()
|
|
valid_keys = {k for k, _ in FP_UOM_SELECTION}
|
|
cleared = 0
|
|
rewritten = 0
|
|
for row_id, raw in rows:
|
|
if raw in valid_keys:
|
|
continue # already a selection key
|
|
new_key = fp_normalize_legacy_uom(raw)
|
|
if new_key:
|
|
cr.execute(
|
|
f'UPDATE "{table}" SET "{column}" = %s WHERE id = %s',
|
|
(new_key, row_id),
|
|
)
|
|
rewritten += 1
|
|
else:
|
|
cr.execute(
|
|
f'UPDATE "{table}" SET "{column}" = NULL WHERE id = %s',
|
|
(row_id,),
|
|
)
|
|
cleared += 1
|
|
import logging
|
|
_logger = logging.getLogger(__name__)
|
|
_logger.info(
|
|
'Fusion Plating UoM migration — %s.%s%s: %s rewritten, %s cleared',
|
|
table, column, f' ({label_for_log})' if label_for_log else '',
|
|
rewritten, cleared,
|
|
)
|
|
return rewritten, cleared
|