221 lines
7.9 KiB
Python
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
|
|
|