Files
site/WESP_REL/app/lab/services/profile_norms.py
T
2026-07-17 12:57:18 +03:00

227 lines
7.9 KiB
Python

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