@@ -0,0 +1,5 @@
|
||||
from .execution_loader import load_execution
|
||||
from .profile_loader import list_animal_profiles, load_animal_profile
|
||||
from .ration_loader import load_ration
|
||||
|
||||
__all__ = ["load_ration", "load_execution", "list_animal_profiles", "load_animal_profile"]
|
||||
@@ -0,0 +1,17 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
|
||||
def parse_json_text(value: Any, default: Any = None) -> Any:
|
||||
if default is None:
|
||||
default = {}
|
||||
if value is None or value == "":
|
||||
return default
|
||||
if isinstance(value, dict):
|
||||
return value
|
||||
try:
|
||||
return json.loads(value)
|
||||
except (TypeError, json.JSONDecodeError):
|
||||
return default
|
||||
@@ -0,0 +1,29 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from app.modules.zootech.lab.dto.ration import ExecutionLineSnapshot, ExecutionSnapshot
|
||||
from app.modules.zootech.wesp_bridge_models import Ingredient, Recipe
|
||||
|
||||
|
||||
def load_execution(recipe_id: str) -> ExecutionSnapshot:
|
||||
recipe = Recipe.query.filter_by(id=recipe_id, is_deleted=False).first()
|
||||
if recipe is None:
|
||||
raise LookupError("Рецепт не найден")
|
||||
heads = int(recipe.heads_per_trip or 1)
|
||||
ingredients = (
|
||||
Ingredient.query.filter_by(recipe_id=recipe_id, is_deleted=False)
|
||||
.order_by(Ingredient.order)
|
||||
.all()
|
||||
)
|
||||
lines = []
|
||||
for ing in ingredients:
|
||||
wph = float(ing.weight_per_head or 0)
|
||||
lines.append(
|
||||
ExecutionLineSnapshot(
|
||||
ingredient_id=ing.id,
|
||||
component_id=ing.component_id,
|
||||
name=ing.name,
|
||||
weight_per_head=wph,
|
||||
daily_kg_total=wph * heads,
|
||||
)
|
||||
)
|
||||
return ExecutionSnapshot(recipe_id=recipe_id, heads_per_trip=heads, lines=tuple(lines))
|
||||
@@ -0,0 +1,58 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from app.modules.zootech.lab.models import LabAnimalProfile
|
||||
from app.modules.zootech.lab.services.profile_norms import load_norms_api
|
||||
|
||||
|
||||
def list_animal_profiles(ration_type: str | None = None) -> list[dict]:
|
||||
q = LabAnimalProfile.query.filter_by(is_deleted=False)
|
||||
if ration_type:
|
||||
q = q.filter_by(ration_type=ration_type.upper())
|
||||
rows = q.order_by(LabAnimalProfile.profile_key).all()
|
||||
return [_profile_dict(p, include_resolved=False) for p in rows]
|
||||
|
||||
|
||||
def load_animal_profile(profile_id: str) -> dict:
|
||||
profile = LabAnimalProfile.query.filter_by(id=profile_id, is_deleted=False).first()
|
||||
if profile is None:
|
||||
raise LookupError("Профиль не найден")
|
||||
return _profile_dict(profile, resolve_norms=True, include_resolved=True)
|
||||
|
||||
|
||||
def _profile_dict(
|
||||
profile: LabAnimalProfile,
|
||||
*,
|
||||
resolve_norms: bool = False,
|
||||
include_resolved: bool = False,
|
||||
) -> dict:
|
||||
norms_api = load_norms_api(profile, resolve_dynamic=include_resolved) if resolve_norms else {}
|
||||
if not resolve_norms:
|
||||
from app.modules.zootech.lab.services.profile_norms import load_norms_dict
|
||||
|
||||
indicators = load_norms_dict(profile.id)
|
||||
norms_api = {"indicators": indicators}
|
||||
from app.modules.zootech.lab.services.norms_params import norms_params_api
|
||||
|
||||
out: dict = {
|
||||
"id": profile.id,
|
||||
"profileKey": profile.profile_key,
|
||||
"label": profile.label,
|
||||
"rationType": profile.ration_type,
|
||||
"massKg": profile.mass_kg,
|
||||
"milkYieldKg": profile.milk_yield_kg,
|
||||
"externalNo": profile.external_no,
|
||||
"normsMethod": profile.norms_method or "wesp",
|
||||
"normsParams": norms_params_api(profile),
|
||||
"normsData": norms_api,
|
||||
"norms": norms_api.get("indicators") or {},
|
||||
"normsProfileKey": profile.profile_key,
|
||||
"legacyNormsRemapped": False,
|
||||
}
|
||||
if include_resolved:
|
||||
out["resolvedNorms"] = norms_api.get("resolvedIndicators") or {}
|
||||
out["dynamicNorms"] = norms_api.get("dynamicNorms") or {}
|
||||
if norms_api.get("coverage"):
|
||||
out["coverage"] = norms_api["coverage"]
|
||||
if norms_api.get("normsMeta"):
|
||||
out["normsMeta"] = norms_api["normsMeta"]
|
||||
return out
|
||||
@@ -0,0 +1,125 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from app.modules.zootech.lab.calc.nutrients import norm_diff
|
||||
from app.modules.zootech.lab.indicators import label_to_indicator_key
|
||||
from app.modules.zootech.lab.dto.ration import RationLineSnapshot, RationSnapshot
|
||||
from app.modules.zootech.lab.models import LabAnimalProfile, LabRationLine, LabRecipeRation
|
||||
from app.modules.zootech.lab.services.component_nutrients import nutrients_calc_dict
|
||||
from app.modules.zootech.lab.services.profile_norms import resolve_norms_for_profile
|
||||
from app.modules.zootech.lab.services.ration_calc_store import (
|
||||
load_compound_results,
|
||||
load_params,
|
||||
load_ration_results,
|
||||
)
|
||||
from app.modules.zootech.wesp_bridge_models import Component, Recipe
|
||||
|
||||
|
||||
def _refresh_indicator_norms(
|
||||
indicators: list[dict],
|
||||
norms: dict[str, dict[str, float | None]],
|
||||
) -> list[dict]:
|
||||
"""min/max/diff в сохранённом расчёте — по текущему профилю норм."""
|
||||
if not indicators or not norms:
|
||||
return indicators
|
||||
out: list[dict] = []
|
||||
for row in indicators:
|
||||
item = dict(row)
|
||||
key = item.get("key") or label_to_indicator_key(str(item.get("label") or ""))
|
||||
if key and key in norms:
|
||||
bounds = norms[key]
|
||||
min_v = bounds.get("min")
|
||||
max_v = bounds.get("max")
|
||||
item["min"] = min_v
|
||||
item["max"] = max_v
|
||||
item["diff"] = norm_diff(item.get("content"), min_v, max_v)
|
||||
out.append(item)
|
||||
return out
|
||||
|
||||
|
||||
def load_ration(recipe_id: str) -> RationSnapshot:
|
||||
recipe = Recipe.query.filter_by(id=recipe_id, is_deleted=False).first()
|
||||
if recipe is None:
|
||||
raise LookupError("Рецепт не найден")
|
||||
header = LabRecipeRation.query.filter_by(recipe_id=recipe_id, is_deleted=False).first()
|
||||
ration_type = recipe.ration_type or "BEEF"
|
||||
norms: dict = {}
|
||||
params: dict = {}
|
||||
ration_results: dict = {}
|
||||
compound_results: dict = {}
|
||||
calculated_at = None
|
||||
animal_profile_id = None
|
||||
norms_profile_key = None
|
||||
if header:
|
||||
animal_profile_id = header.animal_profile_id
|
||||
params = load_params(recipe_id, header)
|
||||
ration_results = load_ration_results(recipe_id, header)
|
||||
compound_results = load_compound_results(recipe_id)
|
||||
calculated_at = header.calculated_at.isoformat() if header.calculated_at else None
|
||||
if header.animal_profile_id:
|
||||
profile = LabAnimalProfile.query.filter_by(
|
||||
id=header.animal_profile_id, is_deleted=False
|
||||
).first()
|
||||
if profile:
|
||||
norms_profile_key = profile.profile_key
|
||||
norms = resolve_norms_for_profile(profile)
|
||||
if ration_results.get("indicators") and norms:
|
||||
ration_results = {
|
||||
**ration_results,
|
||||
"indicators": _refresh_indicator_norms(ration_results["indicators"], norms),
|
||||
}
|
||||
lines_q = (
|
||||
LabRationLine.query.filter_by(recipe_id=recipe_id, is_deleted=False)
|
||||
.order_by(LabRationLine.row_index)
|
||||
.all()
|
||||
)
|
||||
line_snaps = []
|
||||
for line in lines_q:
|
||||
comp = Component.query.get(line.component_id) if line.component_id else None
|
||||
nutrients = nutrients_calc_dict(line.component_id) if line.component_id else {}
|
||||
dry_matter_pct = comp.dry_matter if comp else None
|
||||
line_snaps.append(
|
||||
RationLineSnapshot(
|
||||
id=line.id,
|
||||
component_id=line.component_id,
|
||||
ingredient_name=line.ingredient_name or (comp.name if comp else None),
|
||||
row_index=line.row_index,
|
||||
daily_kg=line.daily_kg,
|
||||
in_ration=bool(line.in_ration),
|
||||
in_compound=bool(line.in_compound),
|
||||
dry_matter=dry_matter_pct,
|
||||
price_per_kg=comp.price if comp else None,
|
||||
nutrients=nutrients,
|
||||
)
|
||||
)
|
||||
return RationSnapshot(
|
||||
recipe_id=recipe_id,
|
||||
recipe_name=recipe.name,
|
||||
ration_type=ration_type,
|
||||
heads_per_trip=int(recipe.heads_per_trip or 1),
|
||||
animal_profile_id=animal_profile_id,
|
||||
params=params,
|
||||
lines=tuple(line_snaps),
|
||||
norms=norms,
|
||||
ration_results=ration_results,
|
||||
compound_results=compound_results,
|
||||
calculated_at=calculated_at,
|
||||
exists=header is not None,
|
||||
norms_profile_key=norms_profile_key,
|
||||
legacy_norms_remapped=False,
|
||||
)
|
||||
|
||||
|
||||
def ration_to_calc_lines(snapshot: RationSnapshot) -> list[dict]:
|
||||
return [
|
||||
{
|
||||
"daily_kg": line.daily_kg,
|
||||
"in_ration": line.in_ration,
|
||||
"in_compound": line.in_compound,
|
||||
"ingredient_name": line.ingredient_name,
|
||||
"price_per_kg": line.price_per_kg,
|
||||
"dry_matter": line.dry_matter,
|
||||
"nutrients": line.nutrients,
|
||||
"component_id": line.component_id,
|
||||
}
|
||||
for line in snapshot.lines
|
||||
]
|
||||
Reference in New Issue
Block a user