Интегрирован wesp в сайт
CI / quality (push) Canceled after 0s

This commit is contained in:
влад
2026-07-17 12:57:18 +03:00
parent 5dfa06ddbe
commit 355c0ef9f1
883 changed files with 194576 additions and 177 deletions
@@ -0,0 +1,360 @@
"""Тестовые рецепты 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