361 lines
12 KiB
Python
361 lines
12 KiB
Python
"""Тестовые рецепты 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
|