Files
site/WESP_REL/app/routes/periods.py
T
2026-07-17 12:57:18 +03:00

221 lines
7.9 KiB
Python

import logging
from datetime import datetime
from flask import Blueprint, jsonify, request
from sqlalchemy import func, select
from app import db
from app.models import FeedingPeriod, Ingredient, PeriodRecipe, Recipe, UnloadingGroup
from app.routes.auth_decorators import require_auth, require_auth_or_paired_terminal
from app.services.period_recipes_query import recipes_for_period_ordered
from app.services.daily_plan.adjustments import get_adjusted_recipe_ids
from app.services.daily_plan.replacements import get_replaced_recipe_ids
from app.services.daily_plan.builder import recipe_total_weights_by_id
from app.services.daily_plan.skips import (
filter_recipes_for_list_view,
get_skipped_ingredient_ids,
get_skipped_unloading_group_ids,
is_kiosk_recipe_list_request,
is_zootech_recipe_list_view,
)
from app.routes.recipes import (
_group_distribution_type,
_ingredient_dry_matter_per_head,
_ingredient_weight_per_head,
_unloading_groups_from_payload,
)
bp = Blueprint("periods", __name__, url_prefix="/api/periods")
logger = logging.getLogger(__name__)
def _error(message: str, status_code: int = 400):
return jsonify({"error": True, "message": message}), status_code
def _serialize_period_recipe(
recipe: Recipe,
*,
total_weight: float | None = None,
skipped_today: bool = False,
skipped_ingredient_today: bool = False,
skipped_group_today: bool = False,
adjusted_ingredient_today: bool = False,
replaced_ingredient_today: bool = False,
):
payload = {
"id": recipe.id,
"name": recipe.name,
"heads_count": recipe.heads_per_trip,
"mixing_time": recipe.mixing_time,
"trip_percent": recipe.trip_percent,
}
if total_weight is not None:
payload["total_weight"] = total_weight
if is_zootech_recipe_list_view():
payload["skippedToday"] = skipped_today
payload["skippedIngredientToday"] = skipped_ingredient_today
payload["skippedGroupToday"] = skipped_group_today
payload["adjustedIngredientToday"] = adjusted_ingredient_today
payload["replacedIngredientToday"] = replaced_ingredient_today
return payload
@bp.get("/<string:period_id>/recipes")
@require_auth_or_paired_terminal
def period_recipes_list(period_id: str):
period = db.session.execute(
select(FeedingPeriod).where(
FeedingPeriod.id == period_id, FeedingPeriod.is_deleted.is_(False)
)
).unique().scalar_one_or_none()
if not period:
return _error("Период не найден", 404)
recipes = recipes_for_period_ordered(period_id)
visible, skipped_today = filter_recipes_for_list_view(recipes)
skip_ings = get_skipped_ingredient_ids()
skip_grps = get_skipped_unloading_group_ids()
adj_ids = get_adjusted_recipe_ids()
repl_ids = get_replaced_recipe_ids()
plan_weights: dict[str, float] = {}
if is_kiosk_recipe_list_request():
plan_weights = recipe_total_weights_by_id(dispenser_id=period.dispenser_id)
def _row(recipe: Recipe) -> dict:
return _serialize_period_recipe(
recipe,
total_weight=plan_weights.get(recipe.id),
skipped_today=recipe.id in skipped_today,
skipped_ingredient_today=bool(skip_ings.get(recipe.id)),
skipped_group_today=bool(skip_grps.get(recipe.id)),
adjusted_ingredient_today=recipe.id in adj_ids,
replaced_ingredient_today=recipe.id in repl_ids,
)
return jsonify([_row(r) for r in visible])
@bp.post("/<string:period_id>/recipes")
@require_auth
def period_recipes_add(period_id: str):
period = db.session.execute(
select(FeedingPeriod).where(
FeedingPeriod.id == period_id, FeedingPeriod.is_deleted.is_(False)
)
).unique().scalar_one_or_none()
if not period:
return _error("Период не найден", 404)
data = request.get_json() or {}
recipe_id = data.get("recipe_id")
logger.info(
"[RECIPES-DB] period_recipes_add period_id=%s dispenser_id=%s attach_recipe_id=%s "
"path=%s remote=%s",
period_id,
period.dispenser_id,
recipe_id or "(new)",
request.path,
request.remote_addr,
)
if recipe_id:
recipe = db.session.execute(
select(Recipe).where(Recipe.id == recipe_id, Recipe.is_deleted.is_(False))
).scalar_one_or_none()
if not recipe:
return _error("Рецепт не найден", 404)
else:
name = (data.get("name") or "").strip()
if not name:
return _error("Название рецепта обязательно", 400)
recipe = Recipe(
name=name,
heads_per_trip=int(data.get("heads_count", data.get("headsPerTrip", 1))),
mixing_time=int(data.get("mixing_time", data.get("mixingTime", 0))),
trip_percent=float(data.get("trip_percent", data.get("tripPercent", 100))),
dry_matter_locked=bool(
data.get("dry_matter_locked", data.get("dryMatterLocked", False))
),
unloading_link_broken=bool(data.get("unloading_link_broken", False)),
target_component_id=data.get("target_component_id"),
created_by="system",
updated_by="system",
)
db.session.add(recipe)
db.session.flush()
for idx, ing in enumerate(data.get("ingredients", []) or [], start=1):
ingredient = Ingredient(
name=ing.get("name", ""),
weight_per_head=_ingredient_weight_per_head(ing),
amount=float(ing.get("amount", 0) or 0),
dry_matter=float(ing.get("dry_matter", 0) or 0),
dry_matter_per_head=_ingredient_dry_matter_per_head(ing),
component_id=ing.get("component_id"),
order=idx,
recipe_id=recipe.id,
created_by="system",
updated_by="system",
)
db.session.add(ingredient)
for idx, group in enumerate(_unloading_groups_from_payload(data), start=1):
unloading_group = UnloadingGroup(
name=group.get("name", ""),
distribution_type=_group_distribution_type(group),
value=float(group.get("value", 0) or 0),
weight=float(group["weight"]) if group.get("weight") not in (None, "") else None,
order=int(group.get("order", idx)),
recipe_id=recipe.id,
created_by="system",
updated_by="system",
)
db.session.add(unloading_group)
existing = db.session.execute(
select(PeriodRecipe).where(
PeriodRecipe.period_id == period.id,
PeriodRecipe.recipe_id == recipe.id,
PeriodRecipe.is_deleted.is_(False),
)
).scalar_one_or_none()
if existing:
db.session.commit()
logger.info(
"[RECIPES-DB] period_recipes_add period_id=%s recipe_id=%s already_linked",
period.id,
recipe.id,
)
return jsonify({"success": True, "id": recipe.id, "message": "Рецепт уже в периоде"})
max_order = db.session.scalar(
select(func.max(PeriodRecipe.order)).where(
PeriodRecipe.period_id == period.id, PeriodRecipe.is_deleted.is_(False)
)
) or 0
link = PeriodRecipe(
period_id=period.id,
recipe_id=recipe.id,
order=max_order + 1,
created_at=datetime.now(),
created_by="system",
updated_by="system",
)
db.session.add(link)
db.session.commit()
logger.info(
"[RECIPES-DB] period_recipes_add period_id=%s dispenser_id=%s recipe_id=%s "
"period_recipe_order=%s new_recipe=%s",
period.id,
period.dispenser_id,
recipe.id,
link.order,
not bool(recipe_id),
)
return jsonify({"success": True, "id": recipe.id, "message": "Рецепт добавлен в период"}), 201