from __future__ import annotations from app.lab.calc.nutrients import norm_diff from app.lab.indicators import label_to_indicator_key from app.lab.dto.ration import RationLineSnapshot, RationSnapshot from app.lab.models import LabAnimalProfile, LabRationLine, LabRecipeRation from app.lab.services.component_nutrients import nutrients_calc_dict from app.lab.services.profile_norms import resolve_norms_for_profile from app.lab.services.ration_calc_store import ( load_compound_results, load_params, load_ration_results, ) from app.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 ]