from __future__ import annotations import json from typing import Any from app import db from app.lab.calc.norms_resolver import NormsResolveRequest, normalize_norms_method, resolve_norms from app.lab.services.norms_params import load_norms_params from app.lab.calc.norms import merge_norms_from_profile from app.lab.calc.nutrients import parse_num from app.lab.models import LabAnimalProfile, LabProfileNorm from app.models.base import default_uuid def _parse_profile_payload(data: Any) -> dict[str, Any]: if data is None: return {} if isinstance(data, str): try: data = json.loads(data or "{}") except (json.JSONDecodeError, TypeError): return {} return data if isinstance(data, dict) else {} def load_norms_dict(profile_id: str | None) -> dict[str, dict[str, float | None]]: if not profile_id: return {} rows = ( LabProfileNorm.query.filter_by(profile_id=profile_id) .order_by(LabProfileNorm.indicator_key) .all() ) return { row.indicator_key: { "min": parse_num(row.min_value), "max": parse_num(row.max_value), } for row in rows } def resolve_norms_for_profile( profile: LabAnimalProfile, *, method: str | None = None, params_override: dict | None = None, force_dynamic: bool = False, ) -> dict[str, dict[str, float | None]]: """Нормы для расчёта рациона по выбранной методике.""" stored = load_norms_dict(profile.id) norms_method = normalize_norms_method(method or profile.norms_method) params = load_norms_params(profile) if params_override: from app.lab.calc.norms_resolver import NormsParams base = { "milkFatPct": params.milk_fat_pct, "lactationNo": params.lactation_no, "lactationStage": params.lactation_stage, "bodyCondition": params.body_condition, "housingSystem": params.housing_system, "koncOeSv": params.konc_oe_sv, } base.update(params_override) params = NormsParams.from_dict(base) resolved, _ = resolve_norms( NormsResolveRequest( method=norms_method, stored=stored, mass_kg=profile.mass_kg, milk_yield_kg=profile.milk_yield_kg, ration_type=profile.ration_type, force_dynamic=force_dynamic, params=params, ) ) return resolved def load_norms_api(profile: LabAnimalProfile, *, resolve_dynamic: bool = True) -> dict[str, Any]: stored = load_norms_dict(profile.id) payload: dict[str, Any] = {} if profile.mass_kg is not None: payload["massKg"] = profile.mass_kg if profile.milk_yield_kg is not None: payload["milkYieldKg"] = profile.milk_yield_kg if profile.external_no is not None: payload["externalNo"] = profile.external_no if stored: payload["indicators"] = stored if resolve_dynamic: resolved, meta = resolve_norms( NormsResolveRequest( method=normalize_norms_method(profile.norms_method), stored=stored, mass_kg=profile.mass_kg, milk_yield_kg=profile.milk_yield_kg, ration_type=profile.ration_type, params=load_norms_params(profile), ) ) payload["resolvedIndicators"] = resolved payload["normsMethod"] = meta.get("normsMethod", profile.norms_method or "wesp") dynamic = meta.get("dynamicNorms") or meta.get("dynamic") or {} if dynamic: payload["dynamicNorms"] = dynamic if meta.get("meta"): payload["normsMeta"] = meta["meta"] if meta.get("coverage"): payload["coverage"] = meta["coverage"] return payload def clear_profile_norms(profile_id: str) -> None: LabProfileNorm.query.filter_by(profile_id=profile_id).delete(synchronize_session=False) def save_norms_from_payload(profile: LabAnimalProfile, data: Any) -> None: payload = _parse_profile_payload(data) profile.mass_kg = parse_num(payload.get("massKg", payload.get("mass_kg"))) if "milkYieldKg" in payload or "milk_yield_kg" in payload: profile.milk_yield_kg = parse_num(payload.get("milkYieldKg", payload.get("milk_yield_kg"))) ext = payload.get("externalNo", payload.get("external_no")) profile.external_no = int(ext) if ext is not None and str(ext).strip() != "" else None merged = merge_norms_from_profile(payload, profile.ration_type) skip_keys = {"massKg", "mass_kg", "externalNo", "external_no", "indicators"} for key, bounds in payload.items(): if key in skip_keys or not isinstance(bounds, dict): continue if "min" in bounds or "max" in bounds: merged[str(key)] = { "min": parse_num(bounds.get("min")), "max": parse_num(bounds.get("max")), } indicators = payload.get("indicators") if isinstance(indicators, dict): for key, bounds in indicators.items(): if not isinstance(bounds, dict): continue merged[str(key)] = { "min": parse_num(bounds.get("min")), "max": parse_num(bounds.get("max")), } clear_profile_norms(profile.id) for indicator_key, bounds in merged.items(): min_v = bounds.get("min") max_v = bounds.get("max") if min_v is None and max_v is None: continue db.session.add( LabProfileNorm( id=default_uuid(), profile_id=profile.id, indicator_key=indicator_key, min_value=min_v, max_value=max_v, ) ) def import_norms_from_legacy_text(profile: LabAnimalProfile, raw: str | None) -> None: save_norms_from_payload(profile, raw) def _can_sync_racion(profile: LabAnimalProfile) -> bool: method = normalize_norms_method(profile.norms_method) if method not in ("racion_moscow", "racion_piter"): return False if not profile.mass_kg or profile.mass_kg <= 0: return False if not profile.milk_yield_kg or profile.milk_yield_kg <= 0: return False if method == "racion_piter": from app.lab.services.norms_params import load_norms_params params = load_norms_params(profile) if params.konc_oe_sv is None or params.konc_oe_sv <= 0: return False return True def sync_racion_norms_to_profile(profile: LabAnimalProfile) -> int: """Вычислить RACION-нормы и upsert min в lab_profile_norm (max сохраняется).""" if not _can_sync_racion(profile): return 0 stored = load_norms_dict(profile.id) resolved, _ = resolve_norms( NormsResolveRequest( method=normalize_norms_method(profile.norms_method), stored=stored, mass_kg=profile.mass_kg, milk_yield_kg=profile.milk_yield_kg, ration_type=profile.ration_type, params=load_norms_params(profile), ) ) existing = { row.indicator_key: row for row in LabProfileNorm.query.filter_by(profile_id=profile.id).all() } updated = 0 for key, bounds in resolved.items(): min_v = bounds.get("min") if min_v is None: continue row = existing.get(key) if row is None: db.session.add( LabProfileNorm( id=default_uuid(), profile_id=profile.id, indicator_key=key, min_value=min_v, max_value=bounds.get("max"), ) ) updated += 1 elif row.min_value != min_v: row.min_value = min_v if bounds.get("max") is not None: row.max_value = bounds.get("max") updated += 1 return updated