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("//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("//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