@@ -0,0 +1,163 @@
|
||||
"""Канонические поля zootech-показателей компонента (parity tab INGREDIENT_NUTRIENT_EDIT_ROWS)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
# column_name -> ключи в API/calc (первый — канон для UI)
|
||||
NUTRIENT_FIELD_SPECS: tuple[tuple[str, tuple[str, ...]], ...] = (
|
||||
("crude_protein", ("Сыр. Протеин",)),
|
||||
("usp", ("уСП",)),
|
||||
("rnb", ("RNB", "БРА", " БРА ")),
|
||||
("nel_cattle", ("ЧЭЛ- КРС", " ЧЭЛ- КРС", "ЧЭЛ - КРС Форм")),
|
||||
("oe_cattle", ("ОЭ-КРС", " ОЭ-КРС", "OЭ КРС форм")),
|
||||
("ndf", ("Сырая клетчатка", "Сырая клетч")),
|
||||
("structural_fiber", ("Структур. клетчатка", "Структур клетч")),
|
||||
("crude_fat", ("Сырой жир",)),
|
||||
)
|
||||
|
||||
|
||||
def _normalize_key(value: str) -> str:
|
||||
return " ".join((value or "").split()).strip().lower()
|
||||
|
||||
|
||||
def _parse_num(value: Any) -> float | None:
|
||||
if value is None or value == "":
|
||||
return None
|
||||
try:
|
||||
n = float(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
return None if n != n else n
|
||||
|
||||
|
||||
def dry_matter_g_per_kg(dry_matter_pct: float | None) -> float | None:
|
||||
"""СВ г/кг из канонического component.dry_matter (%): 88.7% → 887 г/кг."""
|
||||
dm = _parse_num(dry_matter_pct)
|
||||
if dm is None:
|
||||
return None
|
||||
if 0 < dm <= 100:
|
||||
return dm * 10.0
|
||||
if dm > 100:
|
||||
return dm
|
||||
return None
|
||||
|
||||
|
||||
def resolve_sv_g_per_kg(
|
||||
nutrients: dict[str, Any] | None,
|
||||
dry_matter_pct: float | None,
|
||||
) -> float | None:
|
||||
"""Deprecated alias: СВ только из component.dry_matter (%). nutrients игнорируется."""
|
||||
return dry_matter_g_per_kg(dry_matter_pct)
|
||||
|
||||
|
||||
def read_from_mapping(data: dict[str, Any] | None, keys: tuple[str, ...]) -> float | None:
|
||||
if not data:
|
||||
return None
|
||||
for search in keys:
|
||||
target = _normalize_key(search)
|
||||
for k, v in data.items():
|
||||
if _normalize_key(str(k)) != target:
|
||||
continue
|
||||
n = _parse_num(v)
|
||||
if n is not None:
|
||||
return n
|
||||
return None
|
||||
|
||||
|
||||
def api_dict_to_column_values(data: dict[str, Any] | None) -> dict[str, float | None]:
|
||||
payload = data or {}
|
||||
out: dict[str, float | None] = {}
|
||||
for col, keys in NUTRIENT_FIELD_SPECS:
|
||||
out[col] = read_from_mapping(payload, keys)
|
||||
return out
|
||||
|
||||
|
||||
def derived_to_column_values(data: dict[str, Any] | None) -> dict[str, float | None]:
|
||||
return api_dict_to_column_values(data)
|
||||
|
||||
|
||||
def column_values_to_mapping(values: dict[str, float | None]) -> dict[str, float]:
|
||||
out: dict[str, float] = {}
|
||||
for col, keys in NUTRIENT_FIELD_SPECS:
|
||||
val = values.get(col)
|
||||
n = _parse_num(val)
|
||||
if n is not None:
|
||||
out[keys[0]] = n
|
||||
return out
|
||||
|
||||
|
||||
def mapping_to_calc_dict(data: dict[str, Any] | None) -> dict[str, float]:
|
||||
if not data:
|
||||
return {}
|
||||
from app.lab.indicators import RATION_ALL_INDICATORS
|
||||
|
||||
result: dict[str, float] = {}
|
||||
for _col, keys in NUTRIENT_FIELD_SPECS:
|
||||
val = read_from_mapping(data, keys)
|
||||
if val is None:
|
||||
continue
|
||||
for key in keys:
|
||||
result[key] = val
|
||||
seen: set[str] = set()
|
||||
for defn in RATION_ALL_INDICATORS:
|
||||
for key in defn.get("nutrient_keys") or []:
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
val = read_from_mapping(data, (key,))
|
||||
if val is not None:
|
||||
result[key] = val
|
||||
return result
|
||||
|
||||
|
||||
def row_to_calc_dict(row: Any | None) -> dict[str, float]:
|
||||
if row is None:
|
||||
return {}
|
||||
result: dict[str, float] = {}
|
||||
for col, keys in NUTRIENT_FIELD_SPECS:
|
||||
val = getattr(row, col, None)
|
||||
if val is None:
|
||||
continue
|
||||
n = _parse_num(val)
|
||||
if n is None:
|
||||
continue
|
||||
for key in keys:
|
||||
result[key] = n
|
||||
return result
|
||||
|
||||
|
||||
def row_to_api_dict(row: Any | None) -> dict[str, float]:
|
||||
"""API nutrients — канонические ключи tab."""
|
||||
if row is None:
|
||||
return {}
|
||||
out: dict[str, float] = {}
|
||||
for col, keys in NUTRIENT_FIELD_SPECS:
|
||||
val = getattr(row, col, None)
|
||||
n = _parse_num(val)
|
||||
if n is not None:
|
||||
out[keys[0]] = n
|
||||
return out
|
||||
|
||||
|
||||
def row_is_empty(row: Any | None) -> bool:
|
||||
if row is None:
|
||||
return True
|
||||
for col, _keys in NUTRIENT_FIELD_SPECS:
|
||||
if _parse_num(getattr(row, col, None)) is not None:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def legacy_json_to_column_values(raw: str | None) -> dict[str, float | None]:
|
||||
if not raw or not str(raw).strip() or str(raw).strip() == "{}":
|
||||
return {col: None for col, _ in NUTRIENT_FIELD_SPECS}
|
||||
import json
|
||||
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
data = {}
|
||||
if not isinstance(data, dict):
|
||||
data = {}
|
||||
return api_dict_to_column_values(data)
|
||||
Reference in New Issue
Block a user