"""Тестовые рецепты LAB: полная матрица показателей + рацион «в нормах».""" from __future__ import annotations import logging from dataclasses import dataclass, field from pathlib import Path from typing import Any from app import db from app.lab.commands.recalculate import recalculate_ration from app.lab.models import LabAnimalProfile, LabRationLine, LabRecipeRation from app.lab.services.component_nutrients import save_component_nutrients from app.models import Component, Ingredient, Recipe from app.models.base import default_uuid _log = logging.getLogger(__name__) PROFILE_KEY = "lab_math_dairy_01" HEADS = 10 _MARKER = ".lab_math_ration_seeded" RECIPE_MATRIX = "LAB тест — полная матрица" RECIPE_IN_NORMS = "LAB тест — в нормах" # Реалистичные г/кг (и МДж для энергии) — покрывают расширенный каталог indicators.py _COMPONENTS: tuple[dict[str, Any], ...] = ( { "name": "LAB тест — сено", "type": "Грубые корма", "dry_matter": 85.0, "price": 12.0, "nutrients": { "СВ": 850, "Осн.Корм": 850, "Сыр. Протеин": 78, "уСП": 62, "ОЭ-КРС": 5.9, "ЧЭЛ- КРС": 3.4, "Сырая клетч": 328, "НДК": 328, "КДК": 210, "Структур клетч": 265, "Сырой жир": 22, "Ca": 3.5, "P": 2.1, "Mg": 1.8, "Na": 0.5, "K": 18, "DCAB Форм": 120, "Сахар и Крохм": 120, "Сахар": 40, "Крахмал": 15, "Нераств. Крохмал": 5, "Каротин": 25, "Сырая зола": 65, "БЕР": 95, "Переварим Протеин": 55, "КРС Протеин": 68, "Переварим сырая клетч": 195, "Fe": 40, "Zn": 22, "Cu": 6, "Mn": 35, "Se": 0.04, "J": 0.02, "Вит А": 12000, "Вит D": 800, "Вит Е": 30, "Вит В1": 2.5, "Вит В2": 4.0, "Вит В6": 3.0, "Вит В12": 0.0, "Лизин": 2.8, "Метаб лизин": 1.2, "% уСП/кг СВ": 7.3, "Нер. СП / кг СВ": 11, }, }, { "name": "LAB тест — комбикорм", "type": "Концентрированные", "dry_matter": 88.0, "price": 22.0, "nutrients": { "СВ": 880, "Осн.Корм": 0, "Сыр. Протеин": 170, "уСП": 142, "ОЭ-КРС": 12.5, "ЧЭЛ- КРС": 7.8, "Сырая клетч": 95, "НДК": 95, "КДК": 42, "Структур клетч": 28, "Сырой жир": 38, "Ca": 8.0, "P": 5.5, "Mg": 3.2, "Na": 1.2, "K": 6.5, "DCAB Форм": 45, "Сахар и Крохм": 420, "Сахар": 80, "Крахмал": 280, "Нераств. Крохмал": 35, "Каротин": 5, "Сырая зола": 55, "БЕР": 520, "Переварим Протеин": 125, "КРС Протеин": 155, "Fe": 85, "Zn": 55, "Cu": 12, "Mn": 28, "Se": 0.12, "Вит А": 2500, "Вит D": 400, "Вит Е": 45, "Вит В1": 5.5, "Вит В2": 6.0, "Лизин": 8.5, "Метаб лизин": 4.2, "% уСП/кг СВ": 16.1, "Нер. СП / кг СВ": 28, }, }, { "name": "LAB тест — силос", "type": "Сочные корма", "dry_matter": 34.0, "price": 4.0, "nutrients": { "СВ": 340, "Осн.Корм": 0, "Сыр. Протеин": 32, "уСП": 28, "ОЭ-КРС": 6.2, "ЧЭЛ- КРС": 3.8, "Сырая клетч": 220, "НДК": 220, "КДК": 125, "Структур клетч": 45, "Сырой жир": 28, "Ca": 4.5, "P": 2.8, "Mg": 1.5, "Na": 0.3, "K": 12, "DCAB Форм": 95, "Сахар и Крохм": 180, "Сахар": 60, "Крахмал": 45, "Нераств. Крохмал": 12, "Каротин": 18, "Сырая зола": 38, "БЕР": 145, "Переварим Протеин": 22, "КРС Протеин": 28, "Fe": 25, "Zn": 18, "Cu": 4, "Mn": 15, "Вит А": 3500, "Вит Е": 18, "Лизин": 1.1, "% уСП/кг СВ": 8.2, }, }, ) _RECIPE_SPECS: tuple[dict[str, Any], ...] = ( { "name": RECIPE_MATRIX, "profile_key": PROFILE_KEY, "description": "перегруз для проверки красных отклонений", "lines": ( ("LAB тест — сено", 5.0), ("LAB тест — комбикорм", 12.0), ("LAB тест — силос", 8.0), ), }, { "name": RECIPE_IN_NORMS, "profile_key": PROFILE_KEY, "description": "поддержание 300 кг — большинство норм в зелёной зоне", "lines": ( ("LAB тест — сено", 4.5), ("LAB тест — комбикорм", 1.0), ("LAB тест — силос", 1.8), ), }, ) @dataclass class LabMathRationStats: recipe_ids: list[str] = field(default_factory=list) created: int = 0 updated: int = 0 recalculated: int = 0 errors: list[str] = field(default_factory=list) @property def recipe_id(self) -> str | None: return self.recipe_ids[0] if self.recipe_ids else None def _marker_path() -> Path: from config import Config return Path(Config.DATA_DIR) / _MARKER def _get_or_create_component(spec: dict[str, Any]) -> Component: comp = Component.query.filter_by(name=spec["name"], is_deleted=False).first() if comp is None: comp = Component( id=default_uuid(), name=spec["name"], type=spec["type"], dry_matter=float(spec["dry_matter"]), protein=0.0, energy=0.0, price=float(spec.get("price", 0)), created_by="lab-math-ration", updated_by="lab-math-ration", ) db.session.add(comp) db.session.flush() else: comp.dry_matter = float(spec["dry_matter"]) comp.price = float(spec.get("price", comp.price or 0)) comp.updated_by = "lab-math-ration" nutrients = {k: float(v) for k, v in spec["nutrients"].items()} save_component_nutrients(comp.id, nutrients, user_id="lab-math-ration", pin=True) return comp def _seed_one_recipe( spec: dict[str, Any], *, by_name: dict[str, Component], profile: LabAnimalProfile, stats: LabMathRationStats, ) -> str: recipe = Recipe.query.filter_by(name=spec["name"], is_deleted=False).first() if recipe is None: recipe = Recipe( id=default_uuid(), name=spec["name"], heads_per_trip=HEADS, ration_type="DAIRY", mixing_time=0, trip_percent=100.0, created_by="lab-math-ration", updated_by="lab-math-ration", ) db.session.add(recipe) stats.created += 1 db.session.flush() else: recipe.heads_per_trip = HEADS recipe.ration_type = "DAIRY" stats.updated += 1 for ing in Ingredient.query.filter_by(recipe_id=recipe.id, is_deleted=False).all(): ing.soft_delete("lab-math-ration") for order, (cname, wph) in enumerate(spec["lines"]): comp = by_name[cname] db.session.add( Ingredient( id=default_uuid(), recipe_id=recipe.id, component_id=comp.id, name=comp.name, amount=wph * HEADS, weight_per_head=wph, dry_matter=comp.dry_matter, order=order, created_by="lab-math-ration", updated_by="lab-math-ration", ) ) header = LabRecipeRation.query.filter_by(recipe_id=recipe.id, is_deleted=False).first() if header is None: header = LabRecipeRation( recipe_id=recipe.id, created_by="lab-math-ration", updated_by="lab-math-ration", ) db.session.add(header) header.animal_profile_id = profile.id header.seed_source = "lab_math_test" for line in LabRationLine.query.filter_by(recipe_id=recipe.id, is_deleted=False).all(): line.soft_delete("lab-math-ration") for idx, (cname, wph) in enumerate(spec["lines"]): comp = by_name[cname] db.session.add( LabRationLine( id=default_uuid(), recipe_id=recipe.id, component_id=comp.id, ingredient_name=comp.name, row_index=idx, daily_kg=wph * HEADS, in_ration=True, in_compound=False, created_by="lab-math-ration", updated_by="lab-math-ration", ) ) return recipe.id def seed_lab_math_ration(*, force: bool = False, run_calc: bool = True) -> LabMathRationStats: stats = LabMathRationStats() if not force and _marker_path().is_file(): for spec in _RECIPE_SPECS: existing = Recipe.query.filter_by(name=spec["name"], is_deleted=False).first() if existing: stats.recipe_ids.append(existing.id) if stats.recipe_ids: stats.updated = len(stats.recipe_ids) return stats profile = LabAnimalProfile.query.filter_by(profile_key=PROFILE_KEY, is_deleted=False).first() if profile is None: stats.errors.append( f"Профиль {PROFILE_KEY} не найден — запустите scripts/seed_lab_math_profiles.py" ) return stats components = [_get_or_create_component(spec) for spec in _COMPONENTS] by_name = {c.name: c for c in components} for spec in _RECIPE_SPECS: rid = _seed_one_recipe(spec, by_name=by_name, profile=profile, stats=stats) stats.recipe_ids.append(rid) db.session.commit() if run_calc: for rid in stats.recipe_ids: try: recalculate_ration(rid, "lab-math-ration") stats.recalculated += 1 except Exception as exc: stats.errors.append(f"пересчёт {rid}: {exc}") _marker_path().parent.mkdir(parents=True, exist_ok=True) _marker_path().write_text("\n".join(stats.recipe_ids) + "\n", encoding="utf-8") _log.info( "lab math rations: ids=%s matrix+in_norms heads=%s", stats.recipe_ids, HEADS, ) return stats