@@ -3,12 +3,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import date as date_cls
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.core.database import session_scope
|
||||
from app.db.base import Base
|
||||
from app.modules.sync.tenant import TenantContext, require_enterprise_zootech
|
||||
from app.modules.zootech.catalog_apply import load_catalog_row
|
||||
from app.modules.zootech.catalog_models import (
|
||||
@@ -17,18 +19,87 @@ from app.modules.zootech.catalog_models import (
|
||||
ZootechPeriodRecipe,
|
||||
ZootechUnloadingGroup,
|
||||
)
|
||||
from app.modules.zootech import service as zootech_service
|
||||
from app.modules.zootech.service import ZootechServiceError
|
||||
from app.modules.zootech.models import ZootechComponent, ZootechIngredient, ZootechRecipe
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
_LAB_NUTRIENT_MODEL = None
|
||||
|
||||
|
||||
def _lab_nutrient_model():
|
||||
global _LAB_NUTRIENT_MODEL
|
||||
if _LAB_NUTRIENT_MODEL is not False and _LAB_NUTRIENT_MODEL is None:
|
||||
try:
|
||||
from sqlalchemy import Float, String, inspect
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
class ZootechLabComponentNutrientValue(Base):
|
||||
__tablename__ = "lab_component_nutrient_value"
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True)
|
||||
component_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
|
||||
nutrient_key: Mapped[str] = mapped_column(String(80), nullable=False, index=True)
|
||||
value: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
|
||||
with session_scope() as db:
|
||||
if "lab_component_nutrient_value" not in inspect(db.bind).get_table_names():
|
||||
_LAB_NUTRIENT_MODEL = False
|
||||
else:
|
||||
_LAB_NUTRIENT_MODEL = ZootechLabComponentNutrientValue
|
||||
except Exception:
|
||||
_LAB_NUTRIENT_MODEL = False
|
||||
return _LAB_NUTRIENT_MODEL if _LAB_NUTRIENT_MODEL not in (None, False) else None
|
||||
|
||||
|
||||
def _component_nutrients_dict(db, component_id: str) -> dict[str, float]:
|
||||
model = _lab_nutrient_model()
|
||||
if model is None:
|
||||
return {}
|
||||
rows = list(
|
||||
db.scalars(select(model).where(model.component_id == component_id))
|
||||
)
|
||||
return {
|
||||
row.nutrient_key: float(row.value)
|
||||
for row in rows
|
||||
if row.value is not None and row.nutrient_key != "СВ"
|
||||
}
|
||||
|
||||
|
||||
def _save_component_nutrients(db, component_id: str, nutrients: dict) -> None:
|
||||
from uuid import uuid4
|
||||
|
||||
model = _lab_nutrient_model()
|
||||
if model is None or not isinstance(nutrients, dict):
|
||||
return
|
||||
existing = {
|
||||
row.nutrient_key: row
|
||||
for row in db.scalars(select(model).where(model.component_id == component_id))
|
||||
}
|
||||
for key, value in nutrients.items():
|
||||
if value is None or key == "СВ":
|
||||
continue
|
||||
row = existing.get(key)
|
||||
if row is None:
|
||||
db.add(
|
||||
model(
|
||||
id=str(uuid4()),
|
||||
component_id=component_id,
|
||||
nutrient_key=str(key),
|
||||
value=float(value),
|
||||
)
|
||||
)
|
||||
else:
|
||||
row.value = float(value)
|
||||
|
||||
|
||||
def _require_ent(enterprise_id: str, tenant: TenantContext) -> None:
|
||||
if tenant.enterprise_id != enterprise_id:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="ENTERPRISE_FORBIDDEN")
|
||||
|
||||
|
||||
def _component_wesp(row: ZootechComponent) -> dict[str, Any]:
|
||||
return {
|
||||
def _component_wesp(row: ZootechComponent, db=None) -> dict[str, Any]:
|
||||
payload = {
|
||||
"id": row.id,
|
||||
"name": row.name,
|
||||
"type": row.type,
|
||||
@@ -41,6 +112,20 @@ def _component_wesp(row: ZootechComponent) -> dict[str, Any]:
|
||||
"version": row.version,
|
||||
"content_hash": row.content_hash,
|
||||
}
|
||||
if db is not None:
|
||||
nutrients = _component_nutrients_dict(db, row.id)
|
||||
if nutrients:
|
||||
payload["nutrients"] = nutrients
|
||||
return payload
|
||||
|
||||
|
||||
def _component_body_from_wesp(body: dict[str, Any]) -> dict[str, Any]:
|
||||
normalized = dict(body)
|
||||
if "externalNo" in body and "external_no" not in normalized:
|
||||
normalized["external_no"] = body["externalNo"]
|
||||
if "dryMatter" in body and "dry_matter" not in normalized:
|
||||
normalized["dry_matter"] = body["dryMatter"]
|
||||
return normalized
|
||||
|
||||
|
||||
def _recipe_short(row: ZootechRecipe) -> dict[str, Any]:
|
||||
@@ -53,6 +138,50 @@ def _recipe_short(row: ZootechRecipe) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def _payload_meta(row) -> dict[str, Any]:
|
||||
if not row.payload_json:
|
||||
return {}
|
||||
try:
|
||||
parsed = json.loads(row.payload_json)
|
||||
except json.JSONDecodeError:
|
||||
return {}
|
||||
return parsed if isinstance(parsed, dict) else {}
|
||||
|
||||
|
||||
def _mill_recipes_for_dispenser(
|
||||
db,
|
||||
enterprise_id: str,
|
||||
dispenser_id: str,
|
||||
exclude_ids: set[str],
|
||||
) -> list[dict]:
|
||||
recipes = list(
|
||||
db.scalars(
|
||||
select(ZootechRecipe).where(
|
||||
ZootechRecipe.enterprise_id == enterprise_id,
|
||||
ZootechRecipe.is_deleted.is_(False),
|
||||
)
|
||||
)
|
||||
)
|
||||
out: list[dict] = []
|
||||
for recipe in recipes:
|
||||
if recipe.id in exclude_ids:
|
||||
continue
|
||||
meta = _payload_meta(recipe)
|
||||
if str(meta.get("dispenser_id") or "") == str(dispenser_id):
|
||||
out.append(_recipe_short(recipe))
|
||||
return out
|
||||
|
||||
|
||||
def _period_wesp(db, enterprise_id: str, period: ZootechFeedingPeriod, *, has_skip_today: bool = False) -> dict[str, Any]:
|
||||
return {
|
||||
"id": period.id,
|
||||
"name": period.name,
|
||||
"dispenser_id": period.dispenser_id,
|
||||
"recipes": _recipes_for_period(db, enterprise_id, period.id),
|
||||
"hasSkipToday": has_skip_today,
|
||||
}
|
||||
|
||||
|
||||
def _ingredient_wesp(row: ZootechIngredient, comp_name: str | None = None) -> dict[str, Any]:
|
||||
wph = float(row.weight_per_head or 0)
|
||||
dm_pct = float(row.dry_matter or 0)
|
||||
@@ -158,7 +287,7 @@ def _serialize_recipe_wesp(db, enterprise_id: str, recipe: ZootechRecipe) -> dic
|
||||
}
|
||||
|
||||
|
||||
def _dispenser_wesp(row: ZootechFeedDispenser, periods: list[dict] | None = None) -> dict[str, Any]:
|
||||
def _dispenser_wesp(row: ZootechFeedDispenser, periods: list[dict] | None = None, *, has_skip_today: bool = False) -> dict[str, Any]:
|
||||
extra: dict[str, Any] = {}
|
||||
if row.payload_json:
|
||||
try:
|
||||
@@ -171,7 +300,7 @@ def _dispenser_wesp(row: ZootechFeedDispenser, periods: list[dict] | None = None
|
||||
"version": row.version,
|
||||
"content_hash": row.content_hash,
|
||||
"periods": periods or [],
|
||||
"hasSkipToday": False,
|
||||
"hasSkipToday": has_skip_today,
|
||||
**{k: v for k, v in extra.items() if k not in ("id", "name")},
|
||||
}
|
||||
|
||||
@@ -202,7 +331,46 @@ def wesp_list_components(
|
||||
.limit(limit)
|
||||
)
|
||||
)
|
||||
return [_component_wesp(r) for r in rows]
|
||||
return [_component_wesp(r, db) for r in rows]
|
||||
|
||||
|
||||
@router.post("/components")
|
||||
def wesp_create_component(
|
||||
body: dict,
|
||||
enterprise_id: str = Query(...),
|
||||
tenant: TenantContext = Depends(require_enterprise_zootech),
|
||||
):
|
||||
_require_ent(enterprise_id, tenant)
|
||||
row = zootech_service.upsert_component(enterprise_id, None, _component_body_from_wesp(body))
|
||||
with session_scope() as db:
|
||||
component = db.scalar(
|
||||
select(ZootechComponent).where(
|
||||
ZootechComponent.enterprise_id == enterprise_id,
|
||||
ZootechComponent.id == row["id"],
|
||||
)
|
||||
)
|
||||
if not component:
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="CREATE_FAILED")
|
||||
payload = _component_wesp(component, db)
|
||||
payload["success"] = True
|
||||
return payload
|
||||
|
||||
|
||||
_FEED_TYPE_OPTIONS = [
|
||||
{"value": "Грубые корма", "groupLabel": "Корма"},
|
||||
{"value": "Сочные корма", "groupLabel": "Корма"},
|
||||
{"value": "Концентрированные", "groupLabel": "Корма"},
|
||||
{"value": "Добавки", "groupLabel": "Корма"},
|
||||
]
|
||||
|
||||
|
||||
@router.get("/components/feed-types")
|
||||
def wesp_component_feed_types(
|
||||
enterprise_id: str = Query(...),
|
||||
tenant: TenantContext = Depends(require_enterprise_zootech),
|
||||
):
|
||||
_require_ent(enterprise_id, tenant)
|
||||
return {"types": _FEED_TYPE_OPTIONS}
|
||||
|
||||
|
||||
@router.get("/components/{component_id}")
|
||||
@@ -212,22 +380,50 @@ def wesp_get_component(
|
||||
tenant: TenantContext = Depends(require_enterprise_zootech),
|
||||
):
|
||||
_require_ent(enterprise_id, tenant)
|
||||
row = load_catalog_row(enterprise_id, "component", component_id)
|
||||
if not row:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="NOT_FOUND")
|
||||
return {
|
||||
"id": row["id"],
|
||||
"name": row["name"],
|
||||
"type": row.get("type", ""),
|
||||
"is_active": row.get("is_active", True),
|
||||
"dryMatter": row.get("dry_matter", 0),
|
||||
"protein": row.get("protein", 0),
|
||||
"energy": row.get("energy", 0),
|
||||
"price": row.get("price", 0),
|
||||
"externalNo": row.get("external_no"),
|
||||
"version": row.get("version", 1),
|
||||
"content_hash": row.get("content_hash", ""),
|
||||
}
|
||||
with session_scope() as db:
|
||||
component = db.scalar(
|
||||
select(ZootechComponent).where(
|
||||
ZootechComponent.enterprise_id == enterprise_id,
|
||||
ZootechComponent.id == component_id,
|
||||
ZootechComponent.is_deleted.is_(False),
|
||||
)
|
||||
)
|
||||
if not component:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="NOT_FOUND")
|
||||
return _component_wesp(component, db)
|
||||
|
||||
|
||||
@router.put("/components/{component_id}")
|
||||
def wesp_update_component(
|
||||
component_id: str,
|
||||
body: dict,
|
||||
enterprise_id: str = Query(...),
|
||||
tenant: TenantContext = Depends(require_enterprise_zootech),
|
||||
):
|
||||
_require_ent(enterprise_id, tenant)
|
||||
nutrients = body.get("nutrients")
|
||||
try:
|
||||
zootech_service.patch_component(enterprise_id, component_id, _component_body_from_wesp(body))
|
||||
except ZootechServiceError:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="NOT_FOUND") from None
|
||||
if isinstance(nutrients, dict):
|
||||
with session_scope() as db:
|
||||
_save_component_nutrients(db, component_id, nutrients)
|
||||
return {"success": True, "message": "Компонент обновлён"}
|
||||
|
||||
|
||||
@router.delete("/components/{component_id}")
|
||||
def wesp_delete_component(
|
||||
component_id: str,
|
||||
enterprise_id: str = Query(...),
|
||||
tenant: TenantContext = Depends(require_enterprise_zootech),
|
||||
):
|
||||
_require_ent(enterprise_id, tenant)
|
||||
try:
|
||||
zootech_service.delete_component(enterprise_id, component_id)
|
||||
except ZootechServiceError:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="NOT_FOUND") from None
|
||||
return {"success": True, "message": "Компонент удалён"}
|
||||
|
||||
|
||||
@router.get("/feed_dispensers/ping")
|
||||
@@ -269,6 +465,10 @@ def list_feed_dispensers(
|
||||
offset: int = Query(0),
|
||||
):
|
||||
_require_ent(enterprise_id, tenant)
|
||||
from app.modules.zootech import wesp_daily_plan_service as dp_service
|
||||
|
||||
today_iso = date_cls.today().isoformat()
|
||||
skip_today_ids = dp_service.get_recipe_ids_with_any_skip(enterprise_id, today_iso)
|
||||
with session_scope() as db:
|
||||
dispensers = list(
|
||||
db.scalars(
|
||||
@@ -294,10 +494,111 @@ def list_feed_dispensers(
|
||||
)
|
||||
)
|
||||
period_payload = [{"id": p.id, "name": p.name} for p in periods]
|
||||
result.append(_dispenser_wesp(d, period_payload))
|
||||
dispenser_has_skip = False
|
||||
for period in periods:
|
||||
for recipe in _recipes_for_period(db, enterprise_id, period.id):
|
||||
if recipe["id"] in skip_today_ids:
|
||||
dispenser_has_skip = True
|
||||
break
|
||||
if dispenser_has_skip:
|
||||
break
|
||||
result.append(_dispenser_wesp(d, period_payload, has_skip_today=dispenser_has_skip))
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/feed_dispensers")
|
||||
def create_feed_dispenser(
|
||||
body: dict,
|
||||
enterprise_id: str = Query(...),
|
||||
tenant: TenantContext = Depends(require_enterprise_zootech),
|
||||
):
|
||||
_require_ent(enterprise_id, tenant)
|
||||
row = zootech_service.upsert_feed_dispenser(enterprise_id, None, body)
|
||||
with session_scope() as db:
|
||||
dispenser = db.scalar(
|
||||
select(ZootechFeedDispenser).where(
|
||||
ZootechFeedDispenser.enterprise_id == enterprise_id,
|
||||
ZootechFeedDispenser.id == row["id"],
|
||||
)
|
||||
)
|
||||
if not dispenser:
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="CREATE_FAILED")
|
||||
return _dispenser_wesp(dispenser, [])
|
||||
|
||||
|
||||
@router.put("/feed_dispensers/{dispenser_id}")
|
||||
def update_feed_dispenser(
|
||||
dispenser_id: str,
|
||||
body: dict,
|
||||
enterprise_id: str = Query(...),
|
||||
tenant: TenantContext = Depends(require_enterprise_zootech),
|
||||
):
|
||||
_require_ent(enterprise_id, tenant)
|
||||
try:
|
||||
zootech_service.patch_feed_dispenser(enterprise_id, dispenser_id, body)
|
||||
except ZootechServiceError:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="NOT_FOUND") from None
|
||||
return {"message": "Кормораздатчик обновлён"}
|
||||
|
||||
|
||||
@router.delete("/feed_dispensers/{dispenser_id}")
|
||||
def delete_feed_dispenser_route(
|
||||
dispenser_id: str,
|
||||
enterprise_id: str = Query(...),
|
||||
tenant: TenantContext = Depends(require_enterprise_zootech),
|
||||
):
|
||||
_require_ent(enterprise_id, tenant)
|
||||
try:
|
||||
zootech_service.delete_feed_dispenser(enterprise_id, dispenser_id)
|
||||
except ZootechServiceError:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="NOT_FOUND") from None
|
||||
return {"message": "Кормораздатчик удалён"}
|
||||
|
||||
|
||||
@router.post("/feed_dispensers/{dispenser_id}/periods")
|
||||
def create_dispenser_period(
|
||||
dispenser_id: str,
|
||||
body: dict,
|
||||
enterprise_id: str = Query(...),
|
||||
tenant: TenantContext = Depends(require_enterprise_zootech),
|
||||
):
|
||||
_require_ent(enterprise_id, tenant)
|
||||
payload = {"name": body.get("name", ""), "dispenser_id": dispenser_id}
|
||||
row = zootech_service.upsert_feeding_period(enterprise_id, None, payload)
|
||||
return {"id": row["id"], "name": row.get("name", ""), "dispenser_id": dispenser_id}
|
||||
|
||||
|
||||
@router.put("/feed_dispensers/{dispenser_id}/periods/{period_id}")
|
||||
def update_dispenser_period(
|
||||
dispenser_id: str,
|
||||
period_id: str,
|
||||
body: dict,
|
||||
enterprise_id: str = Query(...),
|
||||
tenant: TenantContext = Depends(require_enterprise_zootech),
|
||||
):
|
||||
_require_ent(enterprise_id, tenant)
|
||||
try:
|
||||
row = zootech_service.patch_feeding_period(enterprise_id, period_id, {"name": body.get("name", ""), "dispenser_id": dispenser_id})
|
||||
except ZootechServiceError:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="NOT_FOUND") from None
|
||||
return {"id": row["id"], "name": row.get("name", ""), "dispenser_id": dispenser_id, "success": True}
|
||||
|
||||
|
||||
@router.delete("/feed_dispensers/{dispenser_id}/periods/{period_id}")
|
||||
def delete_dispenser_period(
|
||||
dispenser_id: str,
|
||||
period_id: str,
|
||||
enterprise_id: str = Query(...),
|
||||
tenant: TenantContext = Depends(require_enterprise_zootech),
|
||||
):
|
||||
_require_ent(enterprise_id, tenant)
|
||||
try:
|
||||
zootech_service.delete_feeding_period(enterprise_id, period_id)
|
||||
except ZootechServiceError:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="NOT_FOUND") from None
|
||||
return {"message": "Период удалён"}
|
||||
|
||||
|
||||
@router.get("/feed_dispensers/{dispenser_id}/periods")
|
||||
def dispenser_periods(
|
||||
dispenser_id: str,
|
||||
@@ -305,6 +606,10 @@ def dispenser_periods(
|
||||
tenant: TenantContext = Depends(require_enterprise_zootech),
|
||||
):
|
||||
_require_ent(enterprise_id, tenant)
|
||||
from app.modules.zootech import wesp_daily_plan_service as dp_service
|
||||
|
||||
today_iso = date_cls.today().isoformat()
|
||||
skip_today_ids = dp_service.get_recipe_ids_with_any_skip(enterprise_id, today_iso)
|
||||
with session_scope() as db:
|
||||
periods = list(
|
||||
db.scalars(
|
||||
@@ -315,7 +620,12 @@ def dispenser_periods(
|
||||
)
|
||||
)
|
||||
)
|
||||
return [{"id": p.id, "name": p.name, "dispenser_id": p.dispenser_id} for p in periods]
|
||||
result = []
|
||||
for p in periods:
|
||||
recipes = _recipes_for_period(db, enterprise_id, p.id)
|
||||
period_has_skip = any(r["id"] in skip_today_ids for r in recipes)
|
||||
result.append(_period_wesp(db, enterprise_id, p, has_skip_today=period_has_skip))
|
||||
return result
|
||||
|
||||
|
||||
def _recipes_for_period(db, enterprise_id: str, period_id: str) -> list[dict]:
|
||||
@@ -352,6 +662,16 @@ def dispenser_recipes(
|
||||
):
|
||||
_require_ent(enterprise_id, tenant)
|
||||
with session_scope() as db:
|
||||
dispenser = db.scalar(
|
||||
select(ZootechFeedDispenser).where(
|
||||
ZootechFeedDispenser.enterprise_id == enterprise_id,
|
||||
ZootechFeedDispenser.id == dispenser_id,
|
||||
ZootechFeedDispenser.is_deleted.is_(False),
|
||||
)
|
||||
)
|
||||
if not dispenser:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="NOT_FOUND")
|
||||
|
||||
periods = list(
|
||||
db.scalars(
|
||||
select(ZootechFeedingPeriod).where(
|
||||
@@ -365,6 +685,14 @@ def dispenser_recipes(
|
||||
for p in periods:
|
||||
for r in _recipes_for_period(db, enterprise_id, p.id):
|
||||
merged[r["id"]] = r
|
||||
|
||||
is_mill = _payload_meta(dispenser).get("type") == "mill"
|
||||
mill_recipes: list[dict] = []
|
||||
if is_mill:
|
||||
mill_recipes = _mill_recipes_for_dispenser(db, enterprise_id, dispenser_id, set(merged.keys()))
|
||||
for r in mill_recipes:
|
||||
merged[r["id"]] = r
|
||||
|
||||
return list(merged.values())
|
||||
|
||||
|
||||
@@ -388,6 +716,80 @@ def period_recipes(
|
||||
return _recipes_for_period(db, enterprise_id, period_id)
|
||||
|
||||
|
||||
@router.post("/periods/{period_id}/recipes")
|
||||
def wesp_create_period_recipe(
|
||||
period_id: str,
|
||||
body: dict,
|
||||
enterprise_id: str = Query(...),
|
||||
tenant: TenantContext = Depends(require_enterprise_zootech),
|
||||
):
|
||||
from app.modules.zootech.wesp_recipe_write import RecipeWriteError, create_recipe_wesp, recipe_write_http_error
|
||||
|
||||
_require_ent(enterprise_id, tenant)
|
||||
try:
|
||||
return create_recipe_wesp(enterprise_id, body, period_id=period_id)
|
||||
except RecipeWriteError as exc:
|
||||
raise recipe_write_http_error(exc) from exc
|
||||
|
||||
|
||||
@router.post("/recipes")
|
||||
def wesp_create_recipe(
|
||||
body: dict,
|
||||
enterprise_id: str = Query(...),
|
||||
tenant: TenantContext = Depends(require_enterprise_zootech),
|
||||
):
|
||||
from app.modules.zootech.wesp_recipe_write import RecipeWriteError, create_recipe_wesp, recipe_write_http_error
|
||||
|
||||
_require_ent(enterprise_id, tenant)
|
||||
try:
|
||||
return create_recipe_wesp(enterprise_id, body)
|
||||
except RecipeWriteError as exc:
|
||||
raise recipe_write_http_error(exc) from exc
|
||||
|
||||
|
||||
def _apply_trip_overlay(payload: dict[str, Any], overlay: dict[str, Any]) -> dict[str, Any]:
|
||||
result = dict(payload)
|
||||
overlay_ings = {row["id"]: row for row in overlay.get("ingredients") or []}
|
||||
overlay_groups = {row["id"]: row for row in overlay.get("unloadingGroups") or []}
|
||||
ingredients = []
|
||||
for ing in payload.get("ingredients") or []:
|
||||
row = dict(ing)
|
||||
trip_row = overlay_ings.get(row.get("id"))
|
||||
if trip_row:
|
||||
row["name"] = trip_row.get("name") or row.get("name")
|
||||
row["weightPerHead"] = trip_row.get("weightPerHead", row.get("weightPerHead"))
|
||||
row["weight_per_head"] = row["weightPerHead"]
|
||||
row["amount"] = trip_row.get("totalKg", row.get("amount"))
|
||||
if trip_row.get("componentId"):
|
||||
row["component_id"] = trip_row["componentId"]
|
||||
if trip_row.get("skippedToday"):
|
||||
row["skippedToday"] = True
|
||||
if trip_row.get("replacedToday"):
|
||||
row["replacedToday"] = True
|
||||
if trip_row.get("adjustedToday"):
|
||||
row["adjustedToday"] = True
|
||||
ingredients.append(row)
|
||||
groups = []
|
||||
for grp in payload.get("unloadingGroups") or []:
|
||||
row = dict(grp)
|
||||
trip_grp = overlay_groups.get(row.get("id"))
|
||||
if trip_grp:
|
||||
if trip_grp.get("weightKg") is not None:
|
||||
row["weight"] = trip_grp["weightKg"]
|
||||
if trip_grp.get("skippedToday"):
|
||||
row["skippedToday"] = True
|
||||
groups.append(row)
|
||||
result["ingredients"] = ingredients
|
||||
result["unloadingGroups"] = groups
|
||||
result["unloading_groups"] = groups
|
||||
if overlay.get("totalWeightKg") is not None:
|
||||
result["totalWeightKg"] = overlay["totalWeightKg"]
|
||||
result["total_weight"] = overlay["totalWeightKg"]
|
||||
if overlay.get("unloadingTotalKg") is not None:
|
||||
result["unloadingTotalKg"] = overlay["unloadingTotalKg"]
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/recipes/{recipe_id}")
|
||||
def wesp_get_recipe(
|
||||
recipe_id: str,
|
||||
@@ -406,7 +808,31 @@ def wesp_get_recipe(
|
||||
)
|
||||
if not recipe:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="NOT_FOUND")
|
||||
return _serialize_recipe_wesp(db, enterprise_id, recipe)
|
||||
payload = _serialize_recipe_wesp(db, enterprise_id, recipe)
|
||||
if date:
|
||||
from app.modules.zootech import wesp_daily_plan_service as dp_service
|
||||
|
||||
overlay = dp_service.trip_overlay_for_recipe(enterprise_id, recipe, date)
|
||||
payload = _apply_trip_overlay(payload, overlay)
|
||||
return payload
|
||||
|
||||
|
||||
@router.get("/recipes/{recipe_id}/open-context")
|
||||
def wesp_recipe_open_context(
|
||||
recipe_id: str,
|
||||
enterprise_id: str = Query(...),
|
||||
tenant: TenantContext = Depends(require_enterprise_zootech),
|
||||
):
|
||||
_require_ent(enterprise_id, tenant)
|
||||
from app.modules.zootech import wesp_daily_plan_service as dp_service
|
||||
|
||||
try:
|
||||
return dp_service.recipe_open_context(enterprise_id, recipe_id)
|
||||
except LookupError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail={"error": True, "message": str(exc)},
|
||||
) from exc
|
||||
|
||||
|
||||
@router.post("/recipes/calculate")
|
||||
@@ -419,6 +845,32 @@ def wesp_calculate_recipe(body: dict):
|
||||
raise recipe_write_http_error(exc) from exc
|
||||
|
||||
|
||||
@router.put("/recipes/{recipe_id}/move")
|
||||
def wesp_move_recipe_in_period(
|
||||
recipe_id: str,
|
||||
body: dict,
|
||||
enterprise_id: str = Query(...),
|
||||
tenant: TenantContext = Depends(require_enterprise_zootech),
|
||||
):
|
||||
from app.modules.zootech.wesp_recipe_write import (
|
||||
RecipeWriteError,
|
||||
move_recipe_in_period_wesp,
|
||||
recipe_write_http_error,
|
||||
)
|
||||
|
||||
_require_ent(enterprise_id, tenant)
|
||||
try:
|
||||
return move_recipe_in_period_wesp(
|
||||
enterprise_id,
|
||||
recipe_id,
|
||||
period_id=str(body.get("period_id") or ""),
|
||||
from_index=body.get("from_index", 0),
|
||||
to_index=body.get("to_index", 0),
|
||||
)
|
||||
except RecipeWriteError as exc:
|
||||
raise recipe_write_http_error(exc) from exc
|
||||
|
||||
|
||||
@router.put("/recipes/{recipe_id}")
|
||||
def wesp_update_recipe(
|
||||
recipe_id: str,
|
||||
@@ -433,3 +885,65 @@ def wesp_update_recipe(
|
||||
return update_recipe_wesp(enterprise_id, recipe_id, body)
|
||||
except RecipeWriteError as exc:
|
||||
raise recipe_write_http_error(exc) from exc
|
||||
|
||||
|
||||
@router.post("/feed_dispensers/{dispenser_id}/periods/{period_id}/recipes/{recipe_id}/transfer")
|
||||
def wesp_transfer_period_recipe(
|
||||
dispenser_id: str,
|
||||
period_id: str,
|
||||
recipe_id: str,
|
||||
body: dict,
|
||||
enterprise_id: str = Query(...),
|
||||
tenant: TenantContext = Depends(require_enterprise_zootech),
|
||||
):
|
||||
from app.modules.zootech.wesp_recipe_write import (
|
||||
RecipeWriteError,
|
||||
recipe_write_http_error,
|
||||
transfer_recipe_between_periods_wesp,
|
||||
)
|
||||
|
||||
_require_ent(enterprise_id, tenant)
|
||||
try:
|
||||
return transfer_recipe_between_periods_wesp(
|
||||
enterprise_id,
|
||||
dispenser_id,
|
||||
period_id,
|
||||
recipe_id,
|
||||
from_dispenser_id=str(body.get("from_dispenser_id") or ""),
|
||||
from_period_id=str(body.get("from_period_id") or ""),
|
||||
to_index=body.get("to_index", 0),
|
||||
)
|
||||
except RecipeWriteError as exc:
|
||||
raise recipe_write_http_error(exc) from exc
|
||||
|
||||
|
||||
@router.delete("/feed_dispensers/{dispenser_id}/periods/{period_id}/recipes/{recipe_id}")
|
||||
def wesp_unlink_period_recipe(
|
||||
dispenser_id: str,
|
||||
period_id: str,
|
||||
recipe_id: str,
|
||||
enterprise_id: str = Query(...),
|
||||
tenant: TenantContext = Depends(require_enterprise_zootech),
|
||||
):
|
||||
from app.modules.zootech.wesp_recipe_write import RecipeWriteError, recipe_write_http_error, unlink_recipe_from_period_wesp
|
||||
|
||||
_require_ent(enterprise_id, tenant)
|
||||
try:
|
||||
return unlink_recipe_from_period_wesp(enterprise_id, dispenser_id, period_id, recipe_id)
|
||||
except RecipeWriteError as exc:
|
||||
raise recipe_write_http_error(exc) from exc
|
||||
|
||||
|
||||
@router.delete("/recipes/{recipe_id}")
|
||||
def wesp_delete_recipe(
|
||||
recipe_id: str,
|
||||
enterprise_id: str = Query(...),
|
||||
tenant: TenantContext = Depends(require_enterprise_zootech),
|
||||
):
|
||||
from app.modules.zootech.wesp_recipe_write import RecipeWriteError, delete_recipe_wesp, recipe_write_http_error
|
||||
|
||||
_require_ent(enterprise_id, tenant)
|
||||
try:
|
||||
return delete_recipe_wesp(enterprise_id, recipe_id)
|
||||
except RecipeWriteError as exc:
|
||||
raise recipe_write_http_error(exc) from exc
|
||||
|
||||
Reference in New Issue
Block a user