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

345 lines
11 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.
"""Стартовые nutrients для компонентов без EAV (демо-шаблоны, не лабораторный анализ).
При старте и пересчёте рациона заполняет только пустые `lab_component_nutrient_value`.
Маркер в data/ — журнал последнего прогона, не блокирует новые компоненты.
"""
from __future__ import annotations
import logging
from pathlib import Path
from typing import Any
from app import db
from config import Config
from app.lab.nutrient_schema import read_from_mapping
from app.lab.services.component_nutrients import nutrients_full_dict, nutrients_is_empty, save_component_nutrients
from app.models.component import Component
_log = logging.getLogger(__name__)
_MARKER_NAME = ".lab_demo_nutrients_seeded"
# Доп. поля для полной матрицы рациона (демо; заменить лабораторными при наличии).
_MINERAL_SUPPLEMENT: dict[str, float] = {
"Ca": 8.0,
"P": 5.5,
"Mg": 3.2,
"Na": 1.2,
"K": 6.5,
"DCAB Форм": 45.0,
"Сырая зола": 55.0,
"Каротин": 5.0,
}
_CARB_SUPPLEMENT: dict[str, float] = {
"Сахар": 80.0,
"Крахмал": 280.0,
"Сахар и Крохм": 420.0,
"Нераств. Крохмал": 35.0,
}
_ROUGHAGE_TYPES = frozenset({"объемные корма", "грубые корма", "сочные корма"})
_SUPPLEMENT_KEYS: tuple[str, ...] = tuple({**_MINERAL_SUPPLEMENT, **_CARB_SUPPLEMENT}.keys())
_NAMED_SEEDS: dict[str, dict[str, Any]] = {
"комбикорм": {
"dry_matter": 88.0,
"nutrients": {
"Сыр. Протеин": 170,
"уСП": 142,
"БРА": 36,
"ОЭ-КРС": 12.5,
"ЧЭЛ- КРС": 7.8,
"Сырая клетчатка": 95,
"Структур. клетчатка": 28,
"Сырой жир": 38,
},
},
"жом свекловичный гранулы": {
"dry_matter": 90.0,
"nutrients": {
"Сыр. Протеин": 105,
"уСП": 88,
"ОЭ-КРС": 11.8,
"ЧЭЛ- КРС": 7.2,
"Сырая клетчатка": 220,
"Структур. клетчатка": 48,
"Сырой жир": 12,
},
},
"жом": {
"dry_matter": 90.0,
"nutrients": {
"Сыр. Протеин": 105,
"уСП": 88,
"ОЭ-КРС": 11.8,
"ЧЭЛ- КРС": 7.2,
"Сырая клетчатка": 220,
"Структур. клетчатка": 48,
"Сырой жир": 12,
},
},
"дробина": {
"dry_matter": 91.0,
"nutrients": {
"Сыр. Протеин": 100,
"уСП": 88,
"ОЭ-КРС": 11.5,
"ЧЭЛ- КРС": 7.0,
"Сырая клетчатка": 200,
"Структур. клетчатка": 40,
"Сырой жир": 12,
},
},
"добавки": {
"dry_matter": 99.0,
"nutrients": {
"Сыр. Протеин": 8,
"ОЭ-КРС": 2.0,
"ЧЭЛ- КРС": 1.2,
"Сырая клетчатка": 5,
"Сырой жир": 3,
},
},
"сено": {
"dry_matter": 85.0,
"nutrients": {
"Сыр. Протеин": 78,
"уСП": 62,
"ОЭ-КРС": 5.9,
"ЧЭЛ- КРС": 3.4,
"Сырая клетчатка": 328,
"Структур. клетчатка": 265,
"Сырой жир": 22,
},
},
"анионовые соли": {
"dry_matter": 95.0,
"nutrients": {
"Сыр. Протеин": 12,
"ОЭ-КРС": 1.2,
"ЧЭЛ- КРС": 0.8,
"Сырая клетчатка": 8,
"Сырой жир": 2,
},
},
}
_TYPE_DEFAULTS: dict[str, dict[str, Any]] = {
"зерновые": {
"dry_matter": 88.0,
"nutrients": {
"Сыр. Протеин": 120,
"уСП": 105,
"БРА": 28,
"ОЭ-КРС": 11.0,
"ЧЭЛ- КРС": 6.8,
"Сырая клетчатка": 85,
"Структур. клетчатка": 22,
"Сырой жир": 35,
},
},
"белковые": {
"dry_matter": 90.0,
"nutrients": {
"Сыр. Протеин": 380,
"уСП": 300,
"БРА": 80,
"ОЭ-КРС": 11.5,
"ЧЭЛ- КРС": 7.0,
"Сырая клетчатка": 120,
"Сырой жир": 45,
},
},
"минеральные": {
"dry_matter": 95.0,
"nutrients": {
"Сыр. Протеин": 12,
"ОЭ-КРС": 1.2,
"ЧЭЛ- КРС": 0.8,
"Сырая клетчатка": 8,
"Сырой жир": 2,
},
},
"витаминные": {
"dry_matter": 99.0,
"nutrients": {
"Сыр. Протеин": 8,
"ОЭ-КРС": 2.0,
"ЧЭЛ- КРС": 1.2,
"Сырая клетчатка": 5,
"Сырой жир": 3,
},
},
"энергетические": {
"dry_matter": 90.0,
"nutrients": {
"Сыр. Протеин": 100,
"уСП": 88,
"ОЭ-КРС": 11.5,
"ЧЭЛ- КРС": 7.0,
"Сырая клетчатка": 200,
"Структур. клетчатка": 40,
"Сырой жир": 12,
},
},
"объемные корма": {
"dry_matter": 85.0,
"nutrients": {
"Сыр. Протеин": 78,
"уСП": 62,
"ОЭ-КРС": 5.9,
"ЧЭЛ- КРС": 3.4,
"Сырая клетчатка": 328,
"Структур. клетчатка": 265,
"Сырой жир": 22,
},
},
}
_FALLBACK = {
"dry_matter": 88.0,
"nutrients": {
"Сыр. Протеин": 100,
"уСП": 85,
"ОЭ-КРС": 10.0,
"ЧЭЛ- КРС": 6.2,
"Сырая клетчатка": 150,
"Сырой жир": 25,
},
}
def _normalize_name(name: str | None) -> str:
return (name or "").strip().lower()
def _merge_supplement(nutrients: dict[str, Any], component: Component) -> dict[str, float]:
merged = dict(_MINERAL_SUPPLEMENT)
type_key = _normalize_name(component.type)
if type_key not in _ROUGHAGE_TYPES:
merged.update(_CARB_SUPPLEMENT)
merged.update(nutrients)
return {k: float(v) for k, v in merged.items() if v is not None}
def _payload_for_component(component: Component) -> dict[str, Any]:
name_key = _normalize_name(component.name)
if name_key in _NAMED_SEEDS:
payload = _NAMED_SEEDS[name_key]
else:
payload = None
for key in sorted(_NAMED_SEEDS, key=len, reverse=True):
if key in name_key:
payload = _NAMED_SEEDS[key]
break
if payload is None:
type_key = _normalize_name(component.type)
payload = _TYPE_DEFAULTS.get(type_key, _FALLBACK)
return {
"dry_matter": payload.get("dry_matter"),
"nutrients": _merge_supplement(payload.get("nutrients") or {}, component),
}
def _apply_demo_payload(component: Component, payload: dict[str, Any], *, user_id: str) -> None:
if payload.get("dry_matter") is not None:
dm = float(payload["dry_matter"])
current_dm = float(component.dry_matter or 0)
if dm > 0 and (current_dm <= 0 or current_dm > 100):
component.dry_matter = dm
save_component_nutrients(
component.id,
payload["nutrients"],
user_id=user_id,
)
component.protein = 0.0
component.energy = 0.0
component.updated_by = "demo-nutrients-seed"
def supplement_component_nutrients_if_sparse(
component_id: str | None,
*,
user_id: str = "system",
) -> bool:
"""Добавляет Ca/P/сахара и т.д., если EAV уже есть, но без минералов."""
if not component_id or nutrients_is_empty(component_id):
return False
component = Component.query.filter_by(id=component_id, is_deleted=False).first()
if component is None:
return False
full = nutrients_full_dict(component_id)
template = _payload_for_component(component)["nutrients"]
allowed = set(_MINERAL_SUPPLEMENT)
if _normalize_name(component.type) not in _ROUGHAGE_TYPES:
allowed.update(_CARB_SUPPLEMENT)
missing = {
key: float(template[key])
for key in allowed
if key in template and read_from_mapping(full, (key,)) is None
}
if not missing:
return False
save_component_nutrients(component_id, missing, user_id=user_id, pin=True)
return True
def ensure_component_nutrients_if_empty(
component_id: str | None,
*,
user_id: str = "system",
) -> bool:
"""Заполняет EAV демо-шаблоном, если у компонента нет nutrients. Возвращает True при записи."""
if not component_id or not nutrients_is_empty(component_id):
return False
component = Component.query.filter_by(id=component_id, is_deleted=False).first()
if component is None:
return False
_apply_demo_payload(component, _payload_for_component(component), user_id=user_id)
return True
def _marker_path() -> Path:
return Path(Config.DATA_DIR) / _MARKER_NAME
def _already_seeded() -> bool:
return _marker_path().is_file()
def _mark_seeded() -> None:
path = _marker_path()
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("1\n", encoding="utf-8")
def seed_demo_component_nutrients_once(*, user_id: str = "system", force: bool = False) -> dict:
"""Заполняет lab_component_nutrient_value у всех компонентов с пустой матрицей."""
updated = 0
skipped = 0
for component in Component.query.filter_by(is_deleted=False).all():
if not force and not nutrients_is_empty(component.id):
skipped += 1
continue
if force and not nutrients_is_empty(component.id):
skipped += 1
continue
_apply_demo_payload(component, _payload_for_component(component), user_id=user_id)
updated += 1
if updated:
db.session.commit()
_mark_seeded()
_log.info(
"demo component nutrients seed user=%s updated=%s skipped=%s",
user_id,
updated,
skipped,
)
return {"seeded": True, "updated": updated, "skipped": skipped}
return {"seeded": False, "reason": "nothing_to_update", "updated": 0, "skipped": skipped}