@@ -0,0 +1,201 @@
|
||||
"""Unit-тесты daily_plan/builder.py."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from app import create_app, db
|
||||
from app.models import Component, FeedDispenser, Ingredient, Recipe
|
||||
from app.services.daily_plan.builder import (
|
||||
ALL_DISPENSERS_ID,
|
||||
ALL_MILLS_ID,
|
||||
build_daily_plan,
|
||||
)
|
||||
from app.services.setup_state import mark_setup_complete
|
||||
from tests.helpers.dispenser_recipe_fixtures import (
|
||||
E2E_DISP_COMP,
|
||||
E2E_DISP_ID,
|
||||
E2E_DISP_RECIPE_1,
|
||||
E2E_PERIOD_A,
|
||||
seed_dispenser_period_recipes,
|
||||
)
|
||||
from tests.helpers.zootech_test_helpers import ZootechTestConfig
|
||||
|
||||
|
||||
class DailyPlanBuilderTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.app = create_app(ZootechTestConfig)
|
||||
self.ctx = self.app.app_context()
|
||||
self.ctx.push()
|
||||
db.create_all()
|
||||
mark_setup_complete(self.app)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
db.session.remove()
|
||||
db.drop_all()
|
||||
self.ctx.pop()
|
||||
|
||||
def test_build_aggregates_periods_trips_and_totals(self) -> None:
|
||||
seed_dispenser_period_recipes()
|
||||
plan = build_daily_plan(dispenser_id=E2E_DISP_ID, plan_date="2026-06-07")
|
||||
|
||||
self.assertEqual(plan["date"], "2026-06-07")
|
||||
self.assertEqual(plan["dispenserId"], E2E_DISP_ID)
|
||||
self.assertEqual(plan["dispenserName"], "E2E Раздатчик")
|
||||
self.assertEqual(plan["dispenserType"], "dispenser")
|
||||
self.assertIn("skippedTrips", plan)
|
||||
self.assertEqual(plan["skippedTrips"], [])
|
||||
|
||||
periods = plan["periods"]
|
||||
self.assertEqual(len(periods), 2)
|
||||
morning = next(p for p in periods if p["id"] == E2E_PERIOD_A)
|
||||
self.assertEqual(morning["name"], "E2E Утро")
|
||||
self.assertEqual(len(morning["trips"]), 2)
|
||||
|
||||
trip1 = morning["trips"][0]
|
||||
self.assertEqual(trip1["recipeId"], E2E_DISP_RECIPE_1)
|
||||
self.assertEqual(trip1["headsPerTrip"], 20)
|
||||
self.assertEqual(trip1["mixingTimeSec"], 3)
|
||||
self.assertEqual(len(trip1["ingredients"]), 1)
|
||||
self.assertEqual(trip1["ingredients"][0]["name"], "Ing 1")
|
||||
self.assertEqual(trip1["ingredients"][0]["totalKg"], 20.0)
|
||||
self.assertEqual(len(trip1["unloadingGroups"]), 1)
|
||||
self.assertEqual(trip1["unloadingGroups"][0]["name"], "Г1")
|
||||
self.assertEqual(trip1["unloadingGroups"][0]["weightKg"], 20.0)
|
||||
self.assertEqual(trip1["totalWeightKg"], 20.0)
|
||||
self.assertEqual(trip1["unloadingTotalKg"], 20.0)
|
||||
|
||||
totals = {row["name"]: row["totalKg"] for row in plan["ingredientTotals"]}
|
||||
self.assertEqual(totals["Ing 1"], 20.0)
|
||||
self.assertEqual(totals["Ing 2"], 30.0)
|
||||
|
||||
def test_all_dispensers_aggregates_with_prefixed_periods(self) -> None:
|
||||
seed_dispenser_period_recipes()
|
||||
plan = build_daily_plan(dispenser_id=ALL_DISPENSERS_ID, plan_date="2026-06-07")
|
||||
self.assertEqual(plan["dispenserName"], "Все кормораздатчики")
|
||||
self.assertTrue(plan["periods"])
|
||||
self.assertTrue(any("E2E Раздатчик" in p["name"] for p in plan["periods"]))
|
||||
self.assertTrue(plan["ingredientTotals"])
|
||||
|
||||
def test_all_mills_returns_orphan_recipes(self) -> None:
|
||||
plan = build_daily_plan(dispenser_id=ALL_MILLS_ID)
|
||||
self.assertEqual(plan["dispenserName"], "Все кормоцеха")
|
||||
self.assertEqual(plan["dispenserType"], "mill")
|
||||
|
||||
def test_unloading_group_has_distribution_label(self) -> None:
|
||||
seed_dispenser_period_recipes()
|
||||
plan = build_daily_plan(dispenser_id=E2E_DISP_ID)
|
||||
group = plan["periods"][0]["trips"][0]["unloadingGroups"][0]
|
||||
self.assertEqual(group["distributionType"], "percent")
|
||||
self.assertIn("%", group["distributionLabel"])
|
||||
|
||||
def test_missing_dispenser_raises(self) -> None:
|
||||
with self.assertRaises(LookupError):
|
||||
build_daily_plan(dispenser_id="missing-disp")
|
||||
|
||||
def test_mill_fallback_uses_orphan_recipes(self) -> None:
|
||||
mill_id = "mill-disp-1"
|
||||
recipe_id = "mill-recipe-1"
|
||||
comp_id = "mill-comp-1"
|
||||
db.session.add_all(
|
||||
[
|
||||
FeedDispenser(
|
||||
id=mill_id,
|
||||
name="Мельница",
|
||||
farm="Ферма",
|
||||
operator="Тест",
|
||||
type="mill",
|
||||
content_hash="",
|
||||
),
|
||||
Component(
|
||||
id=comp_id,
|
||||
name="Компонент",
|
||||
type="grain",
|
||||
dry_matter=55.0,
|
||||
protein=0.0,
|
||||
energy=0.0,
|
||||
price=0.0,
|
||||
),
|
||||
Recipe(
|
||||
id=recipe_id,
|
||||
name="Мельничный рейс",
|
||||
heads_per_trip=10,
|
||||
mixing_time=5,
|
||||
trip_percent=100.0,
|
||||
target_component_id=comp_id,
|
||||
content_hash="",
|
||||
),
|
||||
]
|
||||
)
|
||||
db.session.flush()
|
||||
db.session.add(
|
||||
Ingredient(
|
||||
id="mill-ing-1",
|
||||
name="Зерно",
|
||||
weight_per_head=2.5,
|
||||
amount=0.0,
|
||||
dry_matter=55.0,
|
||||
component_id=comp_id,
|
||||
order=1,
|
||||
recipe_id=recipe_id,
|
||||
created_by="system",
|
||||
updated_by="system",
|
||||
)
|
||||
)
|
||||
db.session.commit()
|
||||
|
||||
plan = build_daily_plan(dispenser_id=mill_id)
|
||||
self.assertEqual(plan["dispenserType"], "mill")
|
||||
self.assertEqual(len(plan["periods"]), 1)
|
||||
self.assertEqual(plan["periods"][0]["name"], "Рейсы")
|
||||
self.assertEqual(len(plan["periods"][0]["trips"]), 1)
|
||||
self.assertEqual(plan["periods"][0]["trips"][0]["recipeName"], "Мельничный рейс")
|
||||
self.assertEqual(plan["ingredientTotals"][0]["totalKg"], 25.0)
|
||||
|
||||
def test_unloading_weights_follow_trip_total_after_ingredient_skip(self) -> None:
|
||||
from app.services.daily_plan.skips import skip_ingredient
|
||||
|
||||
seed_dispenser_period_recipes()
|
||||
db.session.add(
|
||||
Ingredient(
|
||||
id="e2e-disp-ing-extra",
|
||||
name="Extra",
|
||||
weight_per_head=5.0,
|
||||
amount=0.0,
|
||||
dry_matter=55.0,
|
||||
component_id=E2E_DISP_COMP,
|
||||
order=2,
|
||||
recipe_id=E2E_DISP_RECIPE_1,
|
||||
created_by="system",
|
||||
updated_by="system",
|
||||
)
|
||||
)
|
||||
db.session.commit()
|
||||
|
||||
plan_before = build_daily_plan(dispenser_id=E2E_DISP_ID, plan_date="2026-06-07")
|
||||
trip_before = plan_before["periods"][0]["trips"][0]
|
||||
self.assertEqual(trip_before["totalWeightKg"], 120.0)
|
||||
self.assertEqual(trip_before["unloadingGroups"][0]["weightKg"], 120.0)
|
||||
|
||||
skip_ingredient(E2E_DISP_RECIPE_1, "e2e-disp-ing-extra", "2026-06-07")
|
||||
plan_after = build_daily_plan(dispenser_id=E2E_DISP_ID, plan_date="2026-06-07")
|
||||
trip_after = plan_after["periods"][0]["trips"][0]
|
||||
self.assertEqual(trip_after["totalWeightKg"], 20.0)
|
||||
self.assertEqual(trip_after["unloadingGroups"][0]["weightKg"], 20.0)
|
||||
self.assertEqual(trip_after["unloadingTotalKg"], 20.0)
|
||||
|
||||
def test_skipped_ingredient_includes_baseline_weights(self) -> None:
|
||||
from app.services.daily_plan.skips import skip_ingredient
|
||||
|
||||
seed_dispenser_period_recipes()
|
||||
skip_ingredient(E2E_DISP_RECIPE_1, "e2e-disp-ing-1", "2026-06-07")
|
||||
plan = build_daily_plan(dispenser_id=E2E_DISP_ID, plan_date="2026-06-07")
|
||||
skipped = next(
|
||||
ing
|
||||
for trip in plan["periods"][0]["trips"]
|
||||
for ing in trip["ingredients"]
|
||||
if ing.get("skippedToday")
|
||||
)
|
||||
self.assertEqual(skipped["totalKg"], 0.0)
|
||||
self.assertEqual(skipped["baselineWeightPerHead"], 1.0)
|
||||
self.assertEqual(skipped["baselineTotalKg"], 20.0)
|
||||
Reference in New Issue
Block a user