@@ -13,7 +13,7 @@ from app.core.database import session_scope
|
||||
from app.modules.sync.engine import SyncEngine
|
||||
from app.modules.sync.schemas import ChangeEventIn
|
||||
from app.modules.zootech.catalog_apply import load_catalog_row
|
||||
from app.modules.zootech.catalog_models import ZootechUnloadingGroup
|
||||
from app.modules.zootech.catalog_models import ZootechFeedingPeriod, ZootechPeriodRecipe, ZootechUnloadingGroup
|
||||
from app.modules.zootech.models import ZootechComponent, ZootechIngredient, ZootechRecipe
|
||||
from app.modules.zootech.recipe_calculator import calculate_recipe
|
||||
from app.modules.zootech.sync_content_hash import compute_content_hash
|
||||
@@ -126,9 +126,10 @@ def _push_change(
|
||||
SyncEngine(enterprise_id, SyncEngine.ORCHESTRATOR_SITE_ID).process_push([event], origin="orchestrator")
|
||||
|
||||
|
||||
def _resolve_component(enterprise_id: str, ing: dict[str, Any]) -> ZootechComponent | None:
|
||||
def _resolve_component(enterprise_id: str, ing: dict[str, Any]) -> dict[str, Any] | None:
|
||||
comp_id = ing.get("component_id")
|
||||
with session_scope() as db:
|
||||
row: ZootechComponent | None = None
|
||||
if comp_id:
|
||||
row = db.scalar(
|
||||
select(ZootechComponent).where(
|
||||
@@ -137,18 +138,23 @@ def _resolve_component(enterprise_id: str, ing: dict[str, Any]) -> ZootechCompon
|
||||
ZootechComponent.is_deleted.is_(False),
|
||||
)
|
||||
)
|
||||
if row:
|
||||
return row
|
||||
name = (ing.get("name") or "").strip()
|
||||
if name:
|
||||
return db.scalar(
|
||||
select(ZootechComponent).where(
|
||||
ZootechComponent.enterprise_id == enterprise_id,
|
||||
ZootechComponent.name == name,
|
||||
ZootechComponent.is_deleted.is_(False),
|
||||
if row is None:
|
||||
name = (ing.get("name") or "").strip()
|
||||
if name:
|
||||
row = db.scalar(
|
||||
select(ZootechComponent).where(
|
||||
ZootechComponent.enterprise_id == enterprise_id,
|
||||
ZootechComponent.name == name,
|
||||
ZootechComponent.is_deleted.is_(False),
|
||||
)
|
||||
)
|
||||
)
|
||||
return None
|
||||
if row is None:
|
||||
return None
|
||||
return {
|
||||
"id": row.id,
|
||||
"name": row.name,
|
||||
"dry_matter": float(row.dry_matter or 0),
|
||||
}
|
||||
|
||||
|
||||
def update_recipe_wesp(enterprise_id: str, recipe_id: str, data: dict[str, Any]) -> dict[str, Any]:
|
||||
@@ -212,6 +218,9 @@ def update_recipe_wesp(enterprise_id: str, recipe_id: str, data: dict[str, Any])
|
||||
"target_component_id": data.get("target_component_id", recipe_target_component_id),
|
||||
"version": recipe_version,
|
||||
}
|
||||
dispenser_id = str(data.get("dispenser_id") or "").strip()
|
||||
if dispenser_id:
|
||||
recipe_payload["dispenser_id"] = dispenser_id
|
||||
_push_change(enterprise_id, "recipe", recipe_id, recipe_payload)
|
||||
|
||||
seen_ing_ids: set[str] = set()
|
||||
@@ -227,7 +236,7 @@ def update_recipe_wesp(enterprise_id: str, recipe_id: str, data: dict[str, Any])
|
||||
order_value = int(ing.get("order") or idx)
|
||||
wph = float(ing.get("weight_per_head") or ing.get("weightPerHead") or 0)
|
||||
amount = float(ing.get("amount") or 0)
|
||||
dm = float(ing.get("dry_matter") or component.dry_matter or 0)
|
||||
dm = float(ing.get("dry_matter") or component.get("dry_matter") or 0)
|
||||
dm_ph = ing.get("dry_matter_per_head")
|
||||
if dm_ph is None:
|
||||
dm_ph = ing.get("dryMatterPerHead")
|
||||
@@ -236,8 +245,8 @@ def update_recipe_wesp(enterprise_id: str, recipe_id: str, data: dict[str, Any])
|
||||
payload = {
|
||||
"id": ing_id,
|
||||
"recipe_id": recipe_id,
|
||||
"component_id": component.id,
|
||||
"name": component.name,
|
||||
"component_id": component["id"],
|
||||
"name": component["name"],
|
||||
"amount": amount,
|
||||
"weight_per_head": wph,
|
||||
"dry_matter": dm,
|
||||
@@ -348,5 +357,311 @@ def update_recipe_wesp(enterprise_id: str, recipe_id: str, data: dict[str, Any])
|
||||
}
|
||||
|
||||
|
||||
def create_recipe_wesp(
|
||||
enterprise_id: str,
|
||||
data: dict[str, Any],
|
||||
*,
|
||||
period_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
if not data:
|
||||
raise RecipeWriteError("Данные не предоставлены", 400)
|
||||
for key in ("name", "heads_count", "mixing_time"):
|
||||
if key not in data:
|
||||
raise RecipeWriteError(f'Отсутствует обязательное поле "{key}"', 400)
|
||||
|
||||
if period_id:
|
||||
with session_scope() as db:
|
||||
period = db.scalar(
|
||||
select(ZootechFeedingPeriod).where(
|
||||
ZootechFeedingPeriod.enterprise_id == enterprise_id,
|
||||
ZootechFeedingPeriod.id == period_id,
|
||||
ZootechFeedingPeriod.is_deleted.is_(False),
|
||||
)
|
||||
)
|
||||
if not period:
|
||||
raise RecipeWriteError("Период не найден", 404)
|
||||
|
||||
recipe_id = str(uuid4())
|
||||
recipe_payload = {
|
||||
"id": recipe_id,
|
||||
"name": str(data["name"]),
|
||||
"heads_per_trip": int(data["heads_count"]),
|
||||
"mixing_time": int(data["mixing_time"]),
|
||||
"trip_percent": float(data.get("trip_percent") or 100),
|
||||
"dry_matter_locked": bool(data.get("dry_matter_locked", False)),
|
||||
"unloading_link_broken": bool(data.get("unloading_link_broken", False)),
|
||||
"target_component_id": data.get("target_component_id"),
|
||||
"version": 1,
|
||||
}
|
||||
dispenser_id = str(data.get("dispenser_id") or "").strip()
|
||||
if dispenser_id:
|
||||
recipe_payload["dispenser_id"] = dispenser_id
|
||||
_push_change(enterprise_id, "recipe", recipe_id, recipe_payload)
|
||||
|
||||
if period_id:
|
||||
with session_scope() as db:
|
||||
links = list(
|
||||
db.scalars(
|
||||
select(ZootechPeriodRecipe).where(
|
||||
ZootechPeriodRecipe.enterprise_id == enterprise_id,
|
||||
ZootechPeriodRecipe.period_id == period_id,
|
||||
ZootechPeriodRecipe.is_deleted.is_(False),
|
||||
)
|
||||
)
|
||||
)
|
||||
order = max((int(link.order or 0) for link in links), default=0) + 1
|
||||
_push_change(
|
||||
enterprise_id,
|
||||
"period_recipes",
|
||||
f"{period_id}:{recipe_id}",
|
||||
{
|
||||
"period_id": period_id,
|
||||
"recipe_id": recipe_id,
|
||||
"order": order,
|
||||
"version": 1,
|
||||
},
|
||||
)
|
||||
|
||||
result = update_recipe_wesp(enterprise_id, recipe_id, data)
|
||||
if result.get("success"):
|
||||
result["message"] = "Рецепт создан"
|
||||
return result
|
||||
|
||||
|
||||
def unlink_recipe_from_period_wesp(
|
||||
enterprise_id: str,
|
||||
dispenser_id: str,
|
||||
period_id: str,
|
||||
recipe_id: str,
|
||||
) -> dict[str, Any]:
|
||||
with session_scope() as db:
|
||||
period = db.scalar(
|
||||
select(ZootechFeedingPeriod).where(
|
||||
ZootechFeedingPeriod.enterprise_id == enterprise_id,
|
||||
ZootechFeedingPeriod.id == period_id,
|
||||
ZootechFeedingPeriod.dispenser_id == dispenser_id,
|
||||
ZootechFeedingPeriod.is_deleted.is_(False),
|
||||
)
|
||||
)
|
||||
if not period:
|
||||
raise RecipeWriteError("Период не найден", 404)
|
||||
|
||||
link_id = f"{period_id}:{recipe_id}"
|
||||
existing = load_catalog_row(enterprise_id, "period_recipes", link_id)
|
||||
if not existing or existing.get("is_deleted"):
|
||||
raise RecipeWriteError("Рейс не найден в периоде", 404)
|
||||
|
||||
version = int(existing.get("version") or 0) + 1
|
||||
payload = {
|
||||
"period_id": period_id,
|
||||
"recipe_id": recipe_id,
|
||||
"order": int(existing.get("order") or 0),
|
||||
"version": version,
|
||||
"is_deleted": True,
|
||||
}
|
||||
_push_change(enterprise_id, "period_recipes", link_id, payload, action="delete")
|
||||
return {"success": True, "message": "Рейс удалён из периода"}
|
||||
|
||||
|
||||
def _period_recipe_ids_ordered(enterprise_id: str, period_id: str) -> list[str]:
|
||||
with session_scope() as db:
|
||||
links = list(
|
||||
db.scalars(
|
||||
select(ZootechPeriodRecipe)
|
||||
.where(
|
||||
ZootechPeriodRecipe.enterprise_id == enterprise_id,
|
||||
ZootechPeriodRecipe.period_id == period_id,
|
||||
ZootechPeriodRecipe.is_deleted.is_(False),
|
||||
)
|
||||
.order_by(ZootechPeriodRecipe.order.asc(), ZootechPeriodRecipe.recipe_id.asc())
|
||||
)
|
||||
)
|
||||
return [link.recipe_id for link in links]
|
||||
|
||||
|
||||
def _set_period_recipe_order(enterprise_id: str, period_id: str, recipe_ids: list[str]) -> None:
|
||||
for order, rid in enumerate(recipe_ids):
|
||||
link_id = f"{period_id}:{rid}"
|
||||
existing = load_catalog_row(enterprise_id, "period_recipes", link_id)
|
||||
if existing and not existing.get("is_deleted") and int(existing.get("order") or -1) == order:
|
||||
continue
|
||||
version = int(existing.get("version") or 0) + 1 if existing else 1
|
||||
_push_change(
|
||||
enterprise_id,
|
||||
"period_recipes",
|
||||
link_id,
|
||||
{
|
||||
"period_id": period_id,
|
||||
"recipe_id": rid,
|
||||
"order": order,
|
||||
"version": version,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _soft_delete_period_recipe_link(enterprise_id: str, period_id: str, recipe_id: str) -> None:
|
||||
link_id = f"{period_id}:{recipe_id}"
|
||||
existing = load_catalog_row(enterprise_id, "period_recipes", link_id)
|
||||
if not existing or existing.get("is_deleted"):
|
||||
return
|
||||
version = int(existing.get("version") or 0) + 1
|
||||
_push_change(
|
||||
enterprise_id,
|
||||
"period_recipes",
|
||||
link_id,
|
||||
{
|
||||
"period_id": period_id,
|
||||
"recipe_id": recipe_id,
|
||||
"order": int(existing.get("order") or 0),
|
||||
"version": version,
|
||||
"is_deleted": True,
|
||||
},
|
||||
action="delete",
|
||||
)
|
||||
|
||||
|
||||
def transfer_recipe_between_periods_wesp(
|
||||
enterprise_id: str,
|
||||
to_dispenser_id: str,
|
||||
to_period_id: str,
|
||||
recipe_id: str,
|
||||
*,
|
||||
from_dispenser_id: str,
|
||||
from_period_id: str,
|
||||
to_index: int,
|
||||
) -> dict[str, Any]:
|
||||
if not from_dispenser_id or not from_period_id:
|
||||
raise RecipeWriteError("Не указан исходный период", 400)
|
||||
try:
|
||||
insert_at = int(to_index)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise RecipeWriteError("Некорректная позиция вставки", 400) from exc
|
||||
if insert_at < 0:
|
||||
raise RecipeWriteError("Некорректная позиция вставки", 400)
|
||||
|
||||
with session_scope() as db:
|
||||
from_period = db.scalar(
|
||||
select(ZootechFeedingPeriod).where(
|
||||
ZootechFeedingPeriod.enterprise_id == enterprise_id,
|
||||
ZootechFeedingPeriod.id == from_period_id,
|
||||
ZootechFeedingPeriod.dispenser_id == from_dispenser_id,
|
||||
ZootechFeedingPeriod.is_deleted.is_(False),
|
||||
)
|
||||
)
|
||||
to_period = db.scalar(
|
||||
select(ZootechFeedingPeriod).where(
|
||||
ZootechFeedingPeriod.enterprise_id == enterprise_id,
|
||||
ZootechFeedingPeriod.id == to_period_id,
|
||||
ZootechFeedingPeriod.dispenser_id == to_dispenser_id,
|
||||
ZootechFeedingPeriod.is_deleted.is_(False),
|
||||
)
|
||||
)
|
||||
recipe = db.scalar(
|
||||
select(ZootechRecipe).where(
|
||||
ZootechRecipe.enterprise_id == enterprise_id,
|
||||
ZootechRecipe.id == recipe_id,
|
||||
ZootechRecipe.is_deleted.is_(False),
|
||||
)
|
||||
)
|
||||
|
||||
if not recipe:
|
||||
raise RecipeWriteError("Рецепт не найден", 404)
|
||||
if not from_period:
|
||||
raise RecipeWriteError("Исходный период не найден", 404)
|
||||
if not to_period:
|
||||
raise RecipeWriteError("Целевой период не найден", 404)
|
||||
if from_period_id == to_period_id:
|
||||
raise RecipeWriteError("Период назначения совпадает с исходным", 400)
|
||||
|
||||
src_link = load_catalog_row(enterprise_id, "period_recipes", f"{from_period_id}:{recipe_id}")
|
||||
if not src_link or src_link.get("is_deleted"):
|
||||
raise RecipeWriteError("Рейс не найден в исходном периоде", 404)
|
||||
|
||||
tgt_link = load_catalog_row(enterprise_id, "period_recipes", f"{to_period_id}:{recipe_id}")
|
||||
if tgt_link and not tgt_link.get("is_deleted"):
|
||||
raise RecipeWriteError("Рейс уже есть в целевом периоде", 409)
|
||||
|
||||
src_ids = _period_recipe_ids_ordered(enterprise_id, from_period_id)
|
||||
if recipe_id not in src_ids:
|
||||
raise RecipeWriteError("Рейс не найден в исходном периоде", 404)
|
||||
|
||||
tgt_ids = _period_recipe_ids_ordered(enterprise_id, to_period_id)
|
||||
insert_at = min(insert_at, len(tgt_ids))
|
||||
|
||||
_soft_delete_period_recipe_link(enterprise_id, from_period_id, recipe_id)
|
||||
_set_period_recipe_order(enterprise_id, from_period_id, [rid for rid in src_ids if rid != recipe_id])
|
||||
|
||||
tgt_ids.insert(insert_at, recipe_id)
|
||||
_set_period_recipe_order(enterprise_id, to_period_id, tgt_ids)
|
||||
|
||||
return {"success": True, "message": "Рейс перенесён"}
|
||||
|
||||
|
||||
def move_recipe_in_period_wesp(
|
||||
enterprise_id: str,
|
||||
recipe_id: str,
|
||||
*,
|
||||
period_id: str,
|
||||
from_index: int,
|
||||
to_index: int,
|
||||
) -> dict[str, Any]:
|
||||
if not period_id:
|
||||
raise RecipeWriteError("Период не указан", 400)
|
||||
try:
|
||||
src = int(from_index)
|
||||
dst = int(to_index)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise RecipeWriteError("Некорректные индексы", 400) from exc
|
||||
|
||||
with session_scope() as db:
|
||||
period = db.scalar(
|
||||
select(ZootechFeedingPeriod).where(
|
||||
ZootechFeedingPeriod.enterprise_id == enterprise_id,
|
||||
ZootechFeedingPeriod.id == period_id,
|
||||
ZootechFeedingPeriod.is_deleted.is_(False),
|
||||
)
|
||||
)
|
||||
recipe = db.scalar(
|
||||
select(ZootechRecipe).where(
|
||||
ZootechRecipe.enterprise_id == enterprise_id,
|
||||
ZootechRecipe.id == recipe_id,
|
||||
ZootechRecipe.is_deleted.is_(False),
|
||||
)
|
||||
)
|
||||
|
||||
if not period:
|
||||
raise RecipeWriteError("Период не найден", 404)
|
||||
if not recipe:
|
||||
raise RecipeWriteError("Рецепт не найден", 404)
|
||||
|
||||
ids = _period_recipe_ids_ordered(enterprise_id, period_id)
|
||||
if recipe_id not in ids:
|
||||
raise RecipeWriteError("Рейс не найден в периоде", 404)
|
||||
if dst < 0 or dst >= len(ids):
|
||||
raise RecipeWriteError("Некорректные индексы", 400)
|
||||
|
||||
actual_from = ids.index(recipe_id)
|
||||
if src != actual_from:
|
||||
src = actual_from
|
||||
if src == dst:
|
||||
return {"success": True, "message": "Порядок не изменился"}
|
||||
|
||||
ids.pop(src)
|
||||
ids.insert(dst, recipe_id)
|
||||
_set_period_recipe_order(enterprise_id, period_id, ids)
|
||||
return {"success": True, "message": "Порядок обновлён"}
|
||||
|
||||
|
||||
def delete_recipe_wesp(enterprise_id: str, recipe_id: str) -> dict[str, Any]:
|
||||
existing = load_catalog_row(enterprise_id, "recipe", recipe_id)
|
||||
if not existing or existing.get("is_deleted"):
|
||||
raise RecipeWriteError("Рецепт не найден", 404)
|
||||
|
||||
version = int(existing.get("version") or 0) + 1
|
||||
payload = {**existing, "version": version, "is_deleted": True}
|
||||
_push_change(enterprise_id, "recipe", recipe_id, payload, action="delete")
|
||||
return {"success": True, "message": "Рейс удален"}
|
||||
|
||||
|
||||
def recipe_write_http_error(exc: RecipeWriteError) -> HTTPException:
|
||||
return HTTPException(status_code=exc.status_code, detail={"message": exc.message, "error": True})
|
||||
|
||||
Reference in New Issue
Block a user