227 lines
7.9 KiB
Python
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
|