Интегрирован 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,249 @@
from __future__ import annotations
import uuid
from typing import Any
from app.modules.zootech.wesp_bridge_db import db
from app.modules.zootech.lab.calc.feed_groups import classify_feed_group
from app.modules.zootech.lab.calc.gfe_policies import DeriveContext
from app.modules.zootech.lab.calc.ingredient_catalog import DERIVED_HEADERS
from app.modules.zootech.lab.calc.ingredient_derive import derive_ingredient_nutrients
from app.modules.zootech.lab.models import LabComponentNutrientValue
from app.modules.zootech.lab.nutrient_keys import augment_with_indicator_keys, canonicalize_for_storage
from app.modules.zootech.lab.nutrient_schema import (
dry_matter_g_per_kg,
mapping_to_calc_dict,
read_from_mapping,
)
from app.modules.zootech.wesp_bridge_models import Component
_DERIVE_INPUT_KEYS = ("СВ", "Сыр. Протеин", "Сырая клетч", "Сырой жир")
_EAV_SKIP_KEYS = frozenset({"СВ"})
_MAIN_FEED_KEYS = ("Осн.Корм", "СВ Основной корм")
def derive_context_for_component(
component_id: str | None,
merged: dict[str, Any] | None = None,
) -> DeriveContext:
"""Контекст derive: тип корма + признак основного корма."""
main_feed = read_from_mapping(merged or {}, _MAIN_FEED_KEYS)
is_main = main_feed is not None and float(main_feed) > 0
feed_group: str = "unknown"
if component_id:
comp = Component.query.filter_by(id=component_id, is_deleted=False).first()
if comp is not None:
feed_group = classify_feed_group(comp)
return DeriveContext(feed_group=feed_group, is_main_feed=is_main) # type: ignore[arg-type]
def _component_dry_matter_pct(component_id: str | None) -> float | None:
if not component_id:
return None
comp = Component.query.get(component_id)
return comp.dry_matter if comp else None
def _inject_sv_for_derive(merged: dict[str, Any], dry_matter_pct: float | None) -> dict[str, Any]:
out = dict(merged)
sv = dry_matter_g_per_kg(dry_matter_pct)
if sv is not None:
out["СВ"] = sv
return out
def _should_derive(merged: dict[str, Any]) -> bool:
"""Derive при полном базовом вводе (СВ из component.dry_matter + 3 показателя)."""
return all(read_from_mapping(merged, (k,)) is not None for k in _DERIVE_INPUT_KEYS)
def nutrients_full_dict(component_id: str | None) -> dict[str, float]:
if not component_id:
return {}
rows = LabComponentNutrientValue.query.filter_by(component_id=component_id).all()
return {
row.nutrient_key: float(row.value)
for row in rows
if row.value is not None and row.nutrient_key not in _EAV_SKIP_KEYS
}
def nutrients_api_dict(component_id: str | None) -> dict[str, float]:
return nutrients_full_dict(component_id)
def _fill_missing_derived(
merged: dict[str, float],
*,
component_id: str | None,
) -> dict[str, float]:
"""Добавляет derived-поля (пОВ, переваримые фракции), не перезаписывая введённые лаб. значения."""
if not _should_derive(merged):
return merged
ctx = derive_context_for_component(component_id, merged)
derived = derive_ingredient_nutrients(merged, context=ctx)
out = dict(merged)
for header in DERIVED_HEADERS:
if read_from_mapping(out, (header,)) is not None:
continue
val = read_from_mapping(derived, (header,))
if val is not None:
out[header] = float(val)
return out
def _repair_for_ration_calc(
full: dict[str, float],
dry_matter_pct: float | None,
component_id: str | None = None,
) -> dict[str, float]:
merged = _inject_sv_for_derive(full, dry_matter_pct)
oe = read_from_mapping(merged, ("ОЭ-КРС", " ОЭ-КРС"))
nel = read_from_mapping(merged, ("ЧЭЛ- КРС", " ЧЭЛ- КРС"))
energy_bad = (oe is not None and oe < 0) or (nel is not None and nel < 0)
if energy_bad:
minimal = {key: read_from_mapping(merged, (key,)) for key in _DERIVE_INPUT_KEYS}
if all(v is not None for v in minimal.values()):
ctx = derive_context_for_component(component_id, merged)
merged.update(derive_ingredient_nutrients(minimal, context=ctx))
else:
merged = _fill_missing_derived(merged, component_id=component_id)
return merged
def _calc_dict_from_full(
full: dict[str, float],
dry_matter_pct: float | None,
component_id: str | None,
) -> dict[str, float]:
repaired = _repair_for_ration_calc(full, dry_matter_pct, component_id)
return mapping_to_calc_dict(augment_with_indicator_keys(repaired))
def nutrients_calc_dict(component_id: str | None) -> dict[str, float]:
full = nutrients_full_dict(component_id)
dry_matter_pct = _component_dry_matter_pct(component_id)
return _calc_dict_from_full(full, dry_matter_pct, component_id)
def nutrients_calc_dict_batch(component_ids: list[str]) -> dict[str, dict[str, float]]:
"""Batch-load EAV + dry_matter for formulate (one SQL round-trip per table)."""
unique = list(dict.fromkeys(cid for cid in component_ids if cid))
if not unique:
return {}
eav_by_id: dict[str, dict[str, float]] = {cid: {} for cid in unique}
rows = LabComponentNutrientValue.query.filter(
LabComponentNutrientValue.component_id.in_(unique),
).all()
for row in rows:
if row.value is None or row.nutrient_key in _EAV_SKIP_KEYS:
continue
eav_by_id.setdefault(row.component_id, {})[row.nutrient_key] = float(row.value)
dm_by_id: dict[str, float | None] = {cid: None for cid in unique}
for comp in Component.query.filter(
Component.id.in_(unique),
Component.is_deleted.is_(False),
).all():
dm_by_id[comp.id] = comp.dry_matter
return {
cid: _calc_dict_from_full(eav_by_id.get(cid, {}), dm_by_id.get(cid), cid)
for cid in unique
}
def nutrients_is_empty(component_id: str | None) -> bool:
if not component_id:
return True
return (
LabComponentNutrientValue.query.filter(
LabComponentNutrientValue.component_id == component_id,
LabComponentNutrientValue.nutrient_key.notin_(_EAV_SKIP_KEYS),
).first()
is None
)
def _upsert_eav(component_id: str, nutrients: dict[str, float]) -> None:
existing = {
row.nutrient_key: row
for row in LabComponentNutrientValue.query.filter_by(component_id=component_id).all()
}
for key in list(existing):
if key in _EAV_SKIP_KEYS:
db.session.delete(existing[key])
existing = {k: v for k, v in existing.items() if k not in _EAV_SKIP_KEYS}
for key, value in nutrients.items():
if value is None or key in _EAV_SKIP_KEYS:
continue
row = existing.get(key)
if row is None:
row = LabComponentNutrientValue(
id=str(uuid.uuid4()),
component_id=component_id,
nutrient_key=key,
value=float(value),
)
db.session.add(row)
existing[key] = row
else:
row.value = float(value)
def _prepare_payload(
component_id: str,
nutrients: dict[str, Any] | None,
*,
pin: bool = False,
) -> dict[str, float]:
dry_matter_pct = _component_dry_matter_pct(component_id)
merged = _inject_sv_for_derive(
{**nutrients_full_dict(component_id), **(nutrients or {})},
dry_matter_pct,
)
if pin:
payload = merged
elif _should_derive(merged):
ctx = derive_context_for_component(component_id, merged)
derived = derive_ingredient_nutrients(merged, context=ctx)
payload = {**merged, **derived}
else:
payload = merged
stored = canonicalize_for_storage(payload)
return {k: v for k, v in stored.items() if k not in _EAV_SKIP_KEYS}
def save_component_nutrients(
component_id: str,
nutrients: dict[str, Any] | None,
*,
user_id: str = "system",
pin: bool = False,
) -> dict[str, Any]:
"""Единственная точка записи EAV + derive. Возвращает stored dict и affectedRecipeIds."""
stored = _prepare_payload(component_id, nutrients, pin=pin)
_upsert_eav(component_id, stored)
from app.modules.zootech.lab.services.ration_recalc import find_rations_by_component
affected = find_rations_by_component(component_id)
return {"stored": stored, "affectedRecipeIds": affected, "userId": user_id}
# Backward-compatible aliases for tests and gradual migration
def upsert_from_api_dict(component_id: str, nutrients: dict[str, Any] | None) -> dict[str, float]:
result = save_component_nutrients(component_id, nutrients)
return result["stored"]
def pin_nutrient_values(component_id: str, values: dict[str, float]) -> None:
save_component_nutrients(component_id, values, pin=True)
def upsert_from_column_values(component_id: str, values: dict[str, float | None]) -> dict[str, float]:
from app.modules.zootech.lab.nutrient_schema import column_values_to_mapping
merged = nutrients_full_dict(component_id)
merged.update(column_values_to_mapping(values))
return upsert_from_api_dict(component_id, merged)