Интегрирован 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
+202
View File
@@ -0,0 +1,202 @@
"""Аудит данных lab-модуля (профили, нормы, рационы) — без компонентов."""
from __future__ import annotations
import logging
from dataclasses import dataclass, field
from app import db
from app.lab.constants import RATION_QUALITY_INDICATORS
from app.lab.models import (
LabAnimalProfile,
LabProfileNorm,
LabRationCalcIndicator,
LabRationLine,
LabRecipeRation,
)
from app.lab.calc.feed_groups import classify_feed_group
from app.lab.nutrient_schema import read_from_mapping
from app.lab.services.component_nutrients import nutrients_full_dict
from app.models import Component, Recipe
_OMD_KEYS = ("ВРХ Орг Вещ", "КРС Орг Вещ")
_log = logging.getLogger(__name__)
CALC_NORM_KEYS = tuple(d["key"] for d in RATION_QUALITY_INDICATORS)
@dataclass
class AuditIssue:
level: str # error | warn | info
area: str
message: str
@dataclass
class LabDataAuditReport:
issues: list[AuditIssue] = field(default_factory=list)
profiles: int = 0
profiles_with_norms: int = 0
profile_norms_beef: int = 0
profile_norms_dairy: int = 0
recipe_rations: int = 0
ration_lines: int = 0
calc_indicators: int = 0
components: int = 0
components_omd_missing: int = 0
def add(self, level: str, area: str, message: str) -> None:
self.issues.append(AuditIssue(level, area, message))
def ok(self) -> bool:
return not any(i.level == "error" for i in self.issues)
def _audit_components(report: LabDataAuditReport) -> None:
components = Component.query.filter_by(is_deleted=False).all()
report.components = len(components)
for comp in components:
full = nutrients_full_dict(comp.id)
if not full:
continue
feed_group = classify_feed_group(comp)
omd = read_from_mapping(full, _OMD_KEYS)
if feed_group in ("rough", "succulent") and omd is None:
report.components_omd_missing += 1
report.add(
"warn",
"component",
f"Компонент {comp.name!r} ({feed_group}): нет ВРХ Орг Вещ",
)
oe = read_from_mapping(full, ("ОЭ-КРС", " ОЭ-КРС", "OЭ КРС форм"))
nel = read_from_mapping(full, ("ЧЭЛ- КРС", " ЧЭЛ- КРС", "ЧЭЛ - КРС Форм"))
if (oe is not None and oe < 0) or (nel is not None and nel < 0):
report.add(
"warn",
"component",
f"Компонент {comp.name!r}: отрицательная энергия (ОЭ={oe}, ЧЭЛ={nel})",
)
def audit_lab_data(*, include_components: bool = False) -> LabDataAuditReport:
report = LabDataAuditReport()
profiles = LabAnimalProfile.query.filter_by(is_deleted=False).all()
report.profiles = len(profiles)
for profile in profiles:
norms = LabProfileNorm.query.filter_by(profile_id=profile.id).all()
norm_keys = {n.indicator_key for n in norms}
if norms:
report.profiles_with_norms += 1
else:
report.add("warn", "profile", f"Профиль {profile.profile_key!r} без норм (lab_profile_norm пуст)")
if profile.mass_kg is None:
report.add("warn", "profile", f"Профиль {profile.profile_key!r}: mass_kg не задан")
missing = [k for k in CALC_NORM_KEYS if k not in norm_keys]
if norms and missing:
report.add(
"info",
"profile",
f"Профиль {profile.profile_key!r}: нет ключей {', '.join(missing[:5])}"
+ ("" if len(missing) > 5 else ""),
)
for norm in norms:
if norm.min_value is None and norm.max_value is None:
report.add(
"warn",
"profile",
f"Профиль {profile.profile_key!r}: {norm.indicator_key} без min и max",
)
for ration_type in ("BEEF", "DAIRY"):
count = (
db.session.query(LabProfileNorm)
.join(LabAnimalProfile, LabAnimalProfile.id == LabProfileNorm.profile_id)
.filter(
LabAnimalProfile.ration_type == ration_type,
LabAnimalProfile.is_deleted.is_(False),
)
.count()
)
if ration_type == "BEEF":
report.profile_norms_beef = count
else:
report.profile_norms_dairy = count
if report.profile_norms_beef == 0 and report.profile_norms_dairy == 0:
report.add(
"warn",
"profile_norm",
"lab_profile_norm пуст — нормы не импортированы (scripts/import_seed.py --norms)",
)
headers = (
LabRecipeRation.query.filter_by(is_deleted=False)
.join(Recipe, Recipe.id == LabRecipeRation.recipe_id)
.filter(Recipe.is_deleted.is_(False))
.all()
)
report.recipe_rations = len(headers)
for header in headers:
recipe = Recipe.query.get(header.recipe_id)
name = recipe.name if recipe else header.recipe_id
if not header.animal_profile_id:
report.add("warn", "ration", f"Рецепт {name!r}: нет animal_profile_id")
elif LabAnimalProfile.query.get(header.animal_profile_id) is None:
report.add("error", "ration", f"Рецепт {name!r}: профиль {header.animal_profile_id} не найден")
lines = LabRationLine.query.filter_by(
recipe_id=header.recipe_id, is_deleted=False
).all()
active = [ln for ln in lines if ln.in_ration and (ln.daily_kg or 0) > 0]
if not active:
report.add("info", "ration", f"Рецепт {name!r}: нет активных строк рациона")
for line in active:
if not line.component_id:
report.add("warn", "ration_line", f"Рецепт {name!r}, строка {line.row_index}: нет component_id")
if line.daily_kg is None:
report.add("warn", "ration_line", f"Рецепт {name!r}, строка {line.row_index}: daily_kg пуст")
if header.calculated_at and not LabRationCalcIndicator.query.filter_by(
recipe_id=header.recipe_id
).first():
report.add("warn", "calc", f"Рецепт {name!r}: calculated_at есть, lab_ration_calc_indicator пуст")
report.ration_lines = LabRationLine.query.filter_by(is_deleted=False).count()
report.calc_indicators = LabRationCalcIndicator.query.count()
if report.profiles == 0:
report.add("warn", "profile", "Нет профилей стада (lab_animal_profile)")
if include_components:
_audit_components(report)
_log.info(
"lab audit: profiles=%s with_norms=%s rations=%s issues=%s",
report.profiles,
report.profiles_with_norms,
report.recipe_rations,
len(report.issues),
)
return report
def format_audit_report(report: LabDataAuditReport) -> str:
lines = [
"=== Lab data audit ===",
f"Профили: {report.profiles} (с нормами: {report.profiles_with_norms})",
f"lab_profile_norm: BEEF={report.profile_norms_beef}, DAIRY={report.profile_norms_dairy}",
f"lab_recipe_ration: {report.recipe_rations}, строк: {report.ration_lines}",
f"lab_ration_calc_indicator: {report.calc_indicators}",
f"components: {report.components} (без ВРХ rough/succulent: {report.components_omd_missing})",
f"Замечаний: {len(report.issues)}",
]
for issue in report.issues:
lines.append(f" [{issue.level}] {issue.area}: {issue.message}")
return "\n".join(lines)