# -*- 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