from __future__ import annotations from app import db from app.lab.commands.write_audit import write_audit from app.lab.db_guard import retry_locked from app.lab.loaders.execution_loader import load_execution from app.lab.loaders.ration_loader import load_ration from app.models import Component, Ingredient, Recipe from app.models.base import default_uuid from app.services.recipe_update_service import _enqueue_recipe_children_sync @retry_locked def apply_from_master(recipe_id: str, user_id: str = "system") -> dict: snapshot = load_ration(recipe_id) if not snapshot.exists: raise LookupError("Мастер рациона не найден") recipe = Recipe.query.filter_by(id=recipe_id, is_deleted=False).first() if recipe is None: raise LookupError("Рецепт не найден") heads = int(recipe.heads_per_trip or 0) if heads <= 0: raise ValueError("Количество голов должно быть > 0") execution = load_execution(recipe_id) by_component = {str(l.component_id): l for l in execution.lines if l.component_id} master_by_component = { str(l.component_id): l for l in snapshot.lines if l.component_id and l.in_ration } touched_ingredient_ids: list[str] = [] for comp_id, master_line in master_by_component.items(): daily_kg = float(master_line.daily_kg or 0) weight_per_head = daily_kg / heads ing = by_component.get(comp_id) comp = Component.query.get(comp_id) dm_pct = float(comp.dry_matter or 0) if comp else float(master_line.dry_matter or 0) if ing: row = Ingredient.query.get(ing.ingredient_id) if row is None: continue row.weight_per_head = weight_per_head row.amount = weight_per_head row.dry_matter = dm_pct row.updated_by = user_id touched_ingredient_ids.append(row.id) else: comp_name = master_line.ingredient_name or comp_id new_ing = Ingredient( id=default_uuid(), recipe_id=recipe_id, component_id=comp_id, name=comp_name[:100], amount=weight_per_head, weight_per_head=weight_per_head, dry_matter=dm_pct, order=len(touched_ingredient_ids), created_by=user_id, updated_by=user_id, ) db.session.add(new_ing) touched_ingredient_ids.append(new_ing.id) master_ids = set(master_by_component) for line in execution.lines: if line.component_id and str(line.component_id) not in master_ids: row = Ingredient.query.get(line.ingredient_id) if row and not row.is_deleted: row.soft_delete(user_id) _enqueue_recipe_children_sync(recipe_id) write_audit("APPLY_FROM_MASTER", "lab_recipe_ration", recipe_id, user_id) db.session.commit() return {"recipeId": recipe_id, "ingredientsUpdated": len(touched_ingredient_ids)}