Интегрирован 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,344 @@
"""Стартовые 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}