@@ -0,0 +1,622 @@
|
||||
"""Сборка плана на день из периодов, рейсов и ингредиентов."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from sqlalchemy import exists, select
|
||||
|
||||
from app import db
|
||||
from app.models import (
|
||||
Component,
|
||||
FeedDispenser,
|
||||
FeedingPeriod,
|
||||
Ingredient,
|
||||
PeriodRecipe,
|
||||
Recipe,
|
||||
UnloadingGroup,
|
||||
)
|
||||
from app.services.daily_plan.adjustments import (
|
||||
get_component_adjustment_map,
|
||||
list_component_norm_adjustments,
|
||||
)
|
||||
from app.services.daily_plan.ingredient_weights import resolve_plan_ingredient_weights
|
||||
from app.services.daily_plan.replacements import (
|
||||
get_ingredient_replacement_map,
|
||||
list_ingredient_replacements,
|
||||
)
|
||||
from app.services.daily_plan.skips import (
|
||||
get_skipped_ingredient_ids,
|
||||
get_skipped_recipe_ids,
|
||||
get_skipped_unloading_group_ids,
|
||||
list_ingredient_skips,
|
||||
list_skips,
|
||||
list_unloading_group_skips,
|
||||
)
|
||||
from app.services.period_recipes_query import recipes_for_period_ordered
|
||||
from app.services.recipe_calculator import calculate_ingredients, calculate_recipe
|
||||
|
||||
ALL_DISPENSERS_ID = "__all_dispensers__"
|
||||
ALL_MILLS_ID = "__all_mills__"
|
||||
|
||||
|
||||
def _parse_plan_date(value: Optional[str]) -> str:
|
||||
if value:
|
||||
try:
|
||||
return date.fromisoformat(str(value).strip()[:10]).isoformat()
|
||||
except ValueError:
|
||||
pass
|
||||
return date.today().isoformat()
|
||||
|
||||
|
||||
def _ingredient_display_name(ing: Ingredient, comp_names: Dict[str, str]) -> str:
|
||||
raw = (ing.name or "").strip()
|
||||
if raw:
|
||||
return raw
|
||||
if ing.component_id and ing.component_id in comp_names:
|
||||
return comp_names[ing.component_id]
|
||||
return "—"
|
||||
|
||||
|
||||
def _distribution_label(dist_type: Optional[str], value: float) -> str:
|
||||
if (dist_type or "percent") == "heads":
|
||||
rounded = int(value) if value == int(value) else value
|
||||
return f"{rounded} гол."
|
||||
rounded = int(value) if value == int(value) else value
|
||||
return f"{rounded}%"
|
||||
|
||||
|
||||
def _load_components_by_id(comp_ids: List[str]) -> Dict[str, Component]:
|
||||
if not comp_ids:
|
||||
return {}
|
||||
comps = db.session.execute(
|
||||
select(Component).where(
|
||||
Component.id.in_(set(comp_ids)),
|
||||
Component.is_deleted.is_(False),
|
||||
)
|
||||
).scalars().all()
|
||||
return {c.id: c for c in comps}
|
||||
|
||||
|
||||
def _serialize_trip(
|
||||
recipe: Recipe,
|
||||
*,
|
||||
order: int,
|
||||
skipped_ingredient_ids: Optional[set[str]] = None,
|
||||
skipped_group_ids: Optional[set[str]] = None,
|
||||
replacement_by_ingredient: Optional[Dict[str, str]] = None,
|
||||
component_adjustment_map: Optional[Dict[str, Dict[str, Optional[float]]]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
ingredients = db.session.execute(
|
||||
select(Ingredient)
|
||||
.where(Ingredient.recipe_id == recipe.id, Ingredient.is_deleted.is_(False))
|
||||
.order_by(Ingredient.order.asc())
|
||||
).scalars().all()
|
||||
comp_ids = [i.component_id for i in ingredients if i.component_id]
|
||||
repl_ids = list((replacement_by_ingredient or {}).values())
|
||||
comp_ids.extend(repl_ids)
|
||||
components_by_id = _load_components_by_id(comp_ids)
|
||||
comp_names = {cid: c.name for cid, c in components_by_id.items()}
|
||||
|
||||
heads = int(recipe.heads_per_trip or 0)
|
||||
skip_ings = skipped_ingredient_ids or set()
|
||||
skip_groups = skipped_group_ids or set()
|
||||
repl_map = replacement_by_ingredient or {}
|
||||
adj_map = component_adjustment_map or {}
|
||||
dm_map = {cid: float(c.dry_matter or 0) for cid, c in components_by_id.items()}
|
||||
|
||||
prepared: List[Dict[str, Any]] = []
|
||||
calc_inputs: List[Dict[str, Any]] = []
|
||||
calc_index_by_ing: Dict[str, int] = {}
|
||||
|
||||
for ing in ingredients:
|
||||
is_skipped = ing.id in skip_ings
|
||||
replacement_id = repl_map.get(ing.id) if not is_skipped else None
|
||||
component_adj = adj_map.get(str(ing.component_id)) if ing.component_id else None
|
||||
weights = resolve_plan_ingredient_weights(
|
||||
ing,
|
||||
recipe,
|
||||
heads=heads,
|
||||
components_by_id=components_by_id,
|
||||
replacement_component_id=replacement_id,
|
||||
component_adjustment=component_adj,
|
||||
)
|
||||
original_name = _ingredient_display_name(ing, comp_names)
|
||||
replaced_today = bool(replacement_id)
|
||||
adjusted_today = bool(weights.get("adjustedToday"))
|
||||
display_name = original_name
|
||||
replacement_name = None
|
||||
if replaced_today:
|
||||
replacement_name = comp_names.get(replacement_id, "—")
|
||||
display_name = f"{original_name} → {replacement_name}"
|
||||
|
||||
trip_kg = 0.0
|
||||
if not is_skipped:
|
||||
calc_index_by_ing[ing.id] = len(calc_inputs)
|
||||
calc_inputs.append(
|
||||
{
|
||||
"weightPerHead": weights["weightPerHead"],
|
||||
"dryMatter": weights["dryMatterPct"],
|
||||
"component_id": replacement_id or ing.component_id,
|
||||
}
|
||||
)
|
||||
|
||||
prepared.append(
|
||||
{
|
||||
"ing": ing,
|
||||
"is_skipped": is_skipped,
|
||||
"weights": weights,
|
||||
"original_name": original_name,
|
||||
"display_name": display_name,
|
||||
"replacement_id": replacement_id,
|
||||
"replacement_name": replacement_name,
|
||||
"replaced_today": replaced_today,
|
||||
"adjusted_today": adjusted_today,
|
||||
"trip_kg": trip_kg,
|
||||
}
|
||||
)
|
||||
|
||||
groups = db.session.execute(
|
||||
select(UnloadingGroup)
|
||||
.where(UnloadingGroup.recipe_id == recipe.id, UnloadingGroup.is_deleted.is_(False))
|
||||
.order_by(UnloadingGroup.order.asc())
|
||||
).scalars().all()
|
||||
|
||||
active_groups = [g for g in groups if g.id not in skip_groups]
|
||||
calc_groups_payload = [
|
||||
{
|
||||
"distributionType": g.distribution_type or "percent",
|
||||
"value": float(g.value or 0),
|
||||
}
|
||||
for g in active_groups
|
||||
]
|
||||
|
||||
calc_result = calculate_recipe(
|
||||
calc_inputs,
|
||||
heads_count=heads,
|
||||
trip_percent=float(recipe.trip_percent or 100),
|
||||
unloading_groups=calc_groups_payload,
|
||||
component_dry_matter_map=dm_map,
|
||||
)
|
||||
calc_ingredients = calc_result.get("ingredients") or []
|
||||
calc_unloading = calc_result.get("unloadingGroups") or []
|
||||
total_weight = float(calc_result.get("totals", {}).get("totalTripWeight") or 0)
|
||||
unloading_total = float(calc_result.get("unloadingTotals", {}).get("totalWeightKg") or 0)
|
||||
|
||||
baseline_inputs = [
|
||||
{
|
||||
"weightPerHead": item["weights"]["weightPerHead"],
|
||||
"dryMatter": item["weights"]["dryMatterPct"],
|
||||
"component_id": item["replacement_id"] or item["ing"].component_id,
|
||||
}
|
||||
for item in prepared
|
||||
]
|
||||
baseline_ingredients = calculate_ingredients(
|
||||
baseline_inputs,
|
||||
heads_count=heads,
|
||||
trip_percent=float(recipe.trip_percent or 100),
|
||||
component_dry_matter_map=dm_map,
|
||||
)
|
||||
|
||||
group_weight_by_id = {
|
||||
g.id: float(calc_unloading[idx].get("calculatedWeight") or 0)
|
||||
for idx, g in enumerate(active_groups)
|
||||
if idx < len(calc_unloading)
|
||||
}
|
||||
|
||||
ing_rows = []
|
||||
for idx, item in enumerate(prepared):
|
||||
ing = item["ing"]
|
||||
weights = item["weights"]
|
||||
if not item["is_skipped"]:
|
||||
calc_idx = calc_index_by_ing.get(ing.id)
|
||||
if calc_idx is not None and calc_idx < len(calc_ingredients):
|
||||
item["trip_kg"] = float(calc_ingredients[calc_idx].get("tripWeight") or 0)
|
||||
|
||||
row: Dict[str, Any] = {
|
||||
"id": ing.id,
|
||||
"name": item["display_name"],
|
||||
"originalName": item["original_name"],
|
||||
"weightPerHead": weights["weightPerHead"],
|
||||
"totalKg": item["trip_kg"],
|
||||
"dryMatterPct": weights["dryMatterPct"],
|
||||
"dryMatterPerHead": weights["dryMatterPerHead"],
|
||||
"componentId": ing.component_id,
|
||||
"originalComponentId": ing.component_id,
|
||||
"replacementComponentId": item["replacement_id"],
|
||||
"replacementName": item["replacement_name"],
|
||||
"skippedToday": item["is_skipped"],
|
||||
"replacedToday": item["replaced_today"],
|
||||
"adjustedToday": item["adjusted_today"],
|
||||
}
|
||||
if item["replaced_today"] or item["adjusted_today"]:
|
||||
row.update(
|
||||
{
|
||||
"originalWeightPerHead": weights.get("originalWeightPerHead"),
|
||||
"originalDryMatterPerHead": weights.get("originalDryMatterPerHead"),
|
||||
"originalDryMatterPct": weights.get("originalDryMatterPct"),
|
||||
}
|
||||
)
|
||||
if item["replaced_today"]:
|
||||
row["recalculationMode"] = weights.get("recalculationMode")
|
||||
if item["is_skipped"] and idx < len(baseline_ingredients):
|
||||
baseline = baseline_ingredients[idx]
|
||||
row["baselineWeightPerHead"] = float(baseline.get("weightPerHead") or 0)
|
||||
row["baselineTotalKg"] = float(baseline.get("tripWeight") or 0)
|
||||
|
||||
ing_rows.append(row)
|
||||
|
||||
return {
|
||||
"order": order,
|
||||
"recipeId": recipe.id,
|
||||
"recipeName": recipe.name,
|
||||
"headsPerTrip": heads,
|
||||
"mixingTimeSec": int(recipe.mixing_time or 0),
|
||||
"tripPercent": float(recipe.trip_percent or 100),
|
||||
"dryMatterLocked": bool(recipe.dry_matter_locked),
|
||||
"totalWeightKg": round(total_weight, 2),
|
||||
"unloadingTotalKg": round(unloading_total, 2),
|
||||
"ingredients": ing_rows,
|
||||
"unloadingGroups": [
|
||||
{
|
||||
"id": g.id,
|
||||
"name": g.name,
|
||||
"weightKg": 0.0
|
||||
if g.id in skip_groups
|
||||
else float(group_weight_by_id.get(g.id, 0)),
|
||||
"distributionType": g.distribution_type,
|
||||
"distributionLabel": _distribution_label(
|
||||
g.distribution_type, float(g.value or 0)
|
||||
),
|
||||
"value": float(g.value or 0),
|
||||
"skippedToday": g.id in skip_groups,
|
||||
}
|
||||
for g in groups
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _periods_for_dispenser(
|
||||
dispenser: FeedDispenser,
|
||||
*,
|
||||
skipped_ids: set[str],
|
||||
skipped_ingredients: Dict[str, set[str]],
|
||||
skipped_groups: Dict[str, set[str]],
|
||||
replacement_map: Dict[str, Dict[str, str]],
|
||||
component_adjustment_map: Dict[str, Dict[str, Optional[float]]],
|
||||
) -> List[Dict[str, Any]]:
|
||||
periods = db.session.execute(
|
||||
select(FeedingPeriod)
|
||||
.where(
|
||||
FeedingPeriod.dispenser_id == dispenser.id,
|
||||
FeedingPeriod.is_deleted.is_(False),
|
||||
FeedingPeriod.is_active.is_(True),
|
||||
)
|
||||
.order_by(FeedingPeriod.created_at.asc())
|
||||
).unique().scalars().all()
|
||||
|
||||
payload = []
|
||||
for period in periods:
|
||||
recipes = [
|
||||
r for r in recipes_for_period_ordered(period.id) if r.id not in skipped_ids
|
||||
]
|
||||
trips = [
|
||||
_serialize_trip(
|
||||
r,
|
||||
order=idx + 1,
|
||||
skipped_ingredient_ids=skipped_ingredients.get(r.id, set()),
|
||||
skipped_group_ids=skipped_groups.get(r.id, set()),
|
||||
replacement_by_ingredient=replacement_map.get(r.id, {}),
|
||||
component_adjustment_map=component_adjustment_map,
|
||||
)
|
||||
for idx, r in enumerate(recipes)
|
||||
]
|
||||
payload.append(
|
||||
{
|
||||
"id": period.id,
|
||||
"name": period.name,
|
||||
"trips": trips,
|
||||
}
|
||||
)
|
||||
return payload
|
||||
|
||||
|
||||
def _mill_trips_fallback(
|
||||
dispenser_id: str,
|
||||
*,
|
||||
skipped_ids: set[str],
|
||||
skipped_ingredients: Dict[str, set[str]],
|
||||
skipped_groups: Dict[str, set[str]],
|
||||
replacement_map: Dict[str, Dict[str, str]],
|
||||
component_adjustment_map: Dict[str, Dict[str, Optional[float]]],
|
||||
) -> List[Dict[str, Any]]:
|
||||
recipes = db.session.execute(
|
||||
select(Recipe)
|
||||
.where(
|
||||
Recipe.is_deleted.is_(False),
|
||||
~exists(
|
||||
select(1).where(
|
||||
PeriodRecipe.recipe_id == Recipe.id,
|
||||
PeriodRecipe.is_deleted.is_(False),
|
||||
)
|
||||
),
|
||||
)
|
||||
.order_by(Recipe.updated_at.desc())
|
||||
).scalars().all()
|
||||
recipes = [r for r in recipes if r.id not in skipped_ids]
|
||||
trips = [
|
||||
_serialize_trip(
|
||||
r,
|
||||
order=idx + 1,
|
||||
skipped_ingredient_ids=skipped_ingredients.get(r.id, set()),
|
||||
skipped_group_ids=skipped_groups.get(r.id, set()),
|
||||
replacement_by_ingredient=replacement_map.get(r.id, {}),
|
||||
)
|
||||
for idx, r in enumerate(recipes)
|
||||
]
|
||||
if not trips:
|
||||
return []
|
||||
return [{"id": None, "name": "Рейсы", "trips": trips}]
|
||||
|
||||
|
||||
def _aggregate_ingredient_totals(periods: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
totals: Dict[str, float] = {}
|
||||
grand_total = 0.0
|
||||
for period in periods:
|
||||
for trip in period.get("trips") or []:
|
||||
for ing in trip.get("ingredients") or []:
|
||||
if ing.get("skippedToday"):
|
||||
continue
|
||||
if ing.get("replacedToday") and ing.get("replacementName"):
|
||||
name = str(ing.get("replacementName") or "—")
|
||||
else:
|
||||
name = str(ing.get("originalName") or ing.get("name") or "—")
|
||||
kg = float(ing.get("totalKg") or 0)
|
||||
totals[name] = totals.get(name, 0.0) + kg
|
||||
grand_total += kg
|
||||
rows = [
|
||||
{"name": name, "totalKg": round(kg, 2)}
|
||||
for name, kg in sorted(totals.items(), key=lambda x: x[0].lower())
|
||||
]
|
||||
return {
|
||||
"rows": rows,
|
||||
"grandTotalKg": round(grand_total, 2),
|
||||
}
|
||||
|
||||
|
||||
def _periods_for_dispenser_named(
|
||||
dispenser: FeedDispenser,
|
||||
*,
|
||||
prefix_name: bool = False,
|
||||
skipped_ids: set[str],
|
||||
skipped_ingredients: Dict[str, set[str]],
|
||||
skipped_groups: Dict[str, set[str]],
|
||||
replacement_map: Dict[str, Dict[str, str]],
|
||||
component_adjustment_map: Dict[str, Dict[str, Optional[float]]],
|
||||
) -> List[Dict[str, Any]]:
|
||||
if dispenser.type == "mill":
|
||||
periods = _mill_trips_fallback(
|
||||
dispenser.id,
|
||||
skipped_ids=skipped_ids,
|
||||
skipped_ingredients=skipped_ingredients,
|
||||
skipped_groups=skipped_groups,
|
||||
replacement_map=replacement_map,
|
||||
component_adjustment_map=component_adjustment_map,
|
||||
)
|
||||
else:
|
||||
periods = _periods_for_dispenser(
|
||||
dispenser,
|
||||
skipped_ids=skipped_ids,
|
||||
skipped_ingredients=skipped_ingredients,
|
||||
skipped_groups=skipped_groups,
|
||||
replacement_map=replacement_map,
|
||||
component_adjustment_map=component_adjustment_map,
|
||||
)
|
||||
if not prefix_name:
|
||||
return periods
|
||||
prefixed: List[Dict[str, Any]] = []
|
||||
for period in periods:
|
||||
item = dict(period)
|
||||
item["name"] = f"{dispenser.name} · {period.get('name') or 'Период'}"
|
||||
item["dispenserId"] = dispenser.id
|
||||
prefixed.append(item)
|
||||
return prefixed
|
||||
|
||||
|
||||
def _plan_extras(
|
||||
periods: List[Dict[str, Any]],
|
||||
*,
|
||||
iso_date: str,
|
||||
) -> Dict[str, Any]:
|
||||
totals = _aggregate_ingredient_totals(periods)
|
||||
return {
|
||||
"ingredientTotals": totals["rows"],
|
||||
"ingredientGrandTotalKg": totals["grandTotalKg"],
|
||||
"ingredientReplacements": list_ingredient_replacements(iso_date),
|
||||
"componentNormAdjustments": list_component_norm_adjustments(iso_date),
|
||||
}
|
||||
|
||||
|
||||
def _plan_payload_base(plan_date: Optional[str]) -> tuple[str, set[str], List, List, List]:
|
||||
iso_date = _parse_plan_date(plan_date)
|
||||
skipped_ids = get_skipped_recipe_ids(iso_date)
|
||||
skipped_trips = list_skips(iso_date)
|
||||
skipped_ingredients = list_ingredient_skips(iso_date)
|
||||
skipped_groups = list_unloading_group_skips(iso_date)
|
||||
return iso_date, skipped_ids, skipped_trips, skipped_ingredients, skipped_groups
|
||||
|
||||
|
||||
def _part_skip_maps(plan_date: Optional[str]) -> tuple[Dict[str, set[str]], Dict[str, set[str]]]:
|
||||
iso_date = _parse_plan_date(plan_date)
|
||||
return get_skipped_ingredient_ids(iso_date), get_skipped_unloading_group_ids(iso_date)
|
||||
|
||||
|
||||
def _build_all_dispensers_plan(plan_date: Optional[str]) -> Dict[str, Any]:
|
||||
iso_date, skipped_ids, skipped_trips, skipped_ingredients, skipped_groups = _plan_payload_base(
|
||||
plan_date
|
||||
)
|
||||
skip_ing_map, skip_grp_map = _part_skip_maps(plan_date)
|
||||
replacement_map = get_ingredient_replacement_map(iso_date)
|
||||
component_adjustment_map = get_component_adjustment_map(iso_date)
|
||||
dispensers = db.session.execute(
|
||||
select(FeedDispenser)
|
||||
.where(
|
||||
FeedDispenser.is_deleted.is_(False),
|
||||
FeedDispenser.type == "dispenser",
|
||||
)
|
||||
.order_by(FeedDispenser.name.asc())
|
||||
).scalars().all()
|
||||
periods: List[Dict[str, Any]] = []
|
||||
for dispenser in dispensers:
|
||||
periods.extend(
|
||||
_periods_for_dispenser_named(
|
||||
dispenser,
|
||||
prefix_name=True,
|
||||
skipped_ids=skipped_ids,
|
||||
skipped_ingredients=skip_ing_map,
|
||||
skipped_groups=skip_grp_map,
|
||||
replacement_map=replacement_map,
|
||||
component_adjustment_map=component_adjustment_map,
|
||||
)
|
||||
)
|
||||
return {
|
||||
"date": iso_date,
|
||||
"generatedAt": datetime.now().isoformat(timespec="seconds"),
|
||||
"dispenserId": ALL_DISPENSERS_ID,
|
||||
"dispenserName": "Все кормораздатчики",
|
||||
"farm": "",
|
||||
"dispenserType": "dispenser",
|
||||
"periods": periods,
|
||||
**_plan_extras(periods, iso_date=iso_date),
|
||||
"skippedTrips": skipped_trips,
|
||||
"skippedIngredients": skipped_ingredients,
|
||||
"skippedUnloadingGroups": skipped_groups,
|
||||
}
|
||||
|
||||
|
||||
def _build_all_mills_plan(plan_date: Optional[str]) -> Dict[str, Any]:
|
||||
iso_date, skipped_ids, skipped_trips, skipped_ingredients, skipped_groups = _plan_payload_base(
|
||||
plan_date
|
||||
)
|
||||
skip_ing_map, skip_grp_map = _part_skip_maps(plan_date)
|
||||
replacement_map = get_ingredient_replacement_map(iso_date)
|
||||
component_adjustment_map = get_component_adjustment_map(iso_date)
|
||||
mills = db.session.execute(
|
||||
select(FeedDispenser)
|
||||
.where(
|
||||
FeedDispenser.is_deleted.is_(False),
|
||||
FeedDispenser.type == "mill",
|
||||
)
|
||||
.order_by(FeedDispenser.name.asc())
|
||||
).scalars().all()
|
||||
periods = _mill_trips_fallback(
|
||||
ALL_MILLS_ID,
|
||||
skipped_ids=skipped_ids,
|
||||
skipped_ingredients=skip_ing_map,
|
||||
skipped_groups=skip_grp_map,
|
||||
replacement_map=replacement_map,
|
||||
component_adjustment_map=component_adjustment_map,
|
||||
)
|
||||
if mills:
|
||||
mill_names = ", ".join(m.name for m in mills)
|
||||
farm = mills[0].farm if len(mills) == 1 else ""
|
||||
else:
|
||||
mill_names = ""
|
||||
farm = ""
|
||||
return {
|
||||
"date": iso_date,
|
||||
"generatedAt": datetime.now().isoformat(timespec="seconds"),
|
||||
"dispenserId": ALL_MILLS_ID,
|
||||
"dispenserName": "Все кормоцеха",
|
||||
"farm": farm,
|
||||
"dispenserType": "mill",
|
||||
"scopeNote": mill_names,
|
||||
"periods": periods,
|
||||
**_plan_extras(periods, iso_date=iso_date),
|
||||
"skippedTrips": skipped_trips,
|
||||
"skippedIngredients": skipped_ingredients,
|
||||
"skippedUnloadingGroups": skipped_groups,
|
||||
}
|
||||
|
||||
|
||||
def build_daily_plan(
|
||||
*,
|
||||
dispenser_id: str,
|
||||
plan_date: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""План на дату: текущая конфигурация периодов кормораздатчика."""
|
||||
if dispenser_id == ALL_DISPENSERS_ID:
|
||||
return _build_all_dispensers_plan(plan_date)
|
||||
if dispenser_id == ALL_MILLS_ID:
|
||||
return _build_all_mills_plan(plan_date)
|
||||
|
||||
dispenser = db.session.execute(
|
||||
select(FeedDispenser).where(
|
||||
FeedDispenser.id == dispenser_id,
|
||||
FeedDispenser.is_deleted.is_(False),
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
if dispenser is None:
|
||||
raise LookupError("Кормораздатчик не найден")
|
||||
|
||||
iso_date, skipped_ids, skipped_trips, skipped_ingredients, skipped_groups = _plan_payload_base(
|
||||
plan_date
|
||||
)
|
||||
skip_ing_map, skip_grp_map = _part_skip_maps(plan_date)
|
||||
replacement_map = get_ingredient_replacement_map(iso_date)
|
||||
component_adjustment_map = get_component_adjustment_map(iso_date)
|
||||
periods = _periods_for_dispenser_named(
|
||||
dispenser,
|
||||
skipped_ids=skipped_ids,
|
||||
skipped_ingredients=skip_ing_map,
|
||||
skipped_groups=skip_grp_map,
|
||||
replacement_map=replacement_map,
|
||||
component_adjustment_map=component_adjustment_map,
|
||||
)
|
||||
|
||||
return {
|
||||
"date": iso_date,
|
||||
"generatedAt": datetime.now().isoformat(timespec="seconds"),
|
||||
"dispenserId": dispenser.id,
|
||||
"dispenserName": dispenser.name,
|
||||
"farm": dispenser.farm,
|
||||
"dispenserType": dispenser.type,
|
||||
"periods": periods,
|
||||
**_plan_extras(periods, iso_date=iso_date),
|
||||
"skippedTrips": skipped_trips,
|
||||
"skippedIngredients": skipped_ingredients,
|
||||
"skippedUnloadingGroups": skipped_groups,
|
||||
}
|
||||
|
||||
|
||||
def recipe_total_weights_by_id(
|
||||
*,
|
||||
dispenser_id: str,
|
||||
plan_date: Optional[str] = None,
|
||||
) -> Dict[str, float]:
|
||||
"""totalWeightKg по recipeId из плана на день (список рейсов на терминале)."""
|
||||
plan = build_daily_plan(dispenser_id=dispenser_id, plan_date=plan_date)
|
||||
weights: Dict[str, float] = {}
|
||||
for period in plan.get("periods") or []:
|
||||
for trip in period.get("trips") or []:
|
||||
recipe_id = trip.get("recipeId")
|
||||
if recipe_id:
|
||||
weights[str(recipe_id)] = float(trip.get("totalWeightKg") or 0)
|
||||
return weights
|
||||
|
||||
|
||||
def trip_overlay_for_recipe(recipe: Recipe, plan_date: str) -> Dict[str, Any]:
|
||||
"""Рейс из плана на день для одного recipe — те же веса, что в build_daily_plan."""
|
||||
skipped_ing_by_recipe = get_skipped_ingredient_ids(plan_date)
|
||||
skipped_grp_by_recipe = get_skipped_unloading_group_ids(plan_date)
|
||||
replacement_map = get_ingredient_replacement_map(plan_date)
|
||||
component_adjustment_map = get_component_adjustment_map(plan_date)
|
||||
return _serialize_trip(
|
||||
recipe,
|
||||
order=1,
|
||||
skipped_ingredient_ids=skipped_ing_by_recipe.get(recipe.id, set()),
|
||||
skipped_group_ids=skipped_grp_by_recipe.get(recipe.id, set()),
|
||||
replacement_by_ingredient=replacement_map.get(recipe.id, {}),
|
||||
component_adjustment_map=component_adjustment_map,
|
||||
)
|
||||
Reference in New Issue
Block a user