@@ -0,0 +1,220 @@
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user