Files
site/WESP_REL/app/lab/commands/seed_lab_math_ration.py
2026-07-17 12:57:18 +03:00

361 lines
12 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Тестовые рецепты 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