"""Временная правка СВ% компонентов в плане на день.""" from __future__ import annotations import unittest from app import create_app, db from app.models import Component, Ingredient, Recipe from app.services.daily_plan.adjustments import ( adjust_component_norm, get_component_adjustment_map, list_component_norms_for_plan, undo_component_norm_adjustment, ) from app.services.daily_plan.builder import build_daily_plan from app.services.daily_plan.replacements import replace_ingredient 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_DISP_RECIPE_2, seed_dispenser_period_recipes, ) from tests.helpers.zootech_test_helpers import ZootechTestConfig class DailyPlanAdjustmentsTests(unittest.TestCase): PLAN_DATE = "2026-06-07" def setUp(self) -> None: self.app = create_app(ZootechTestConfig) self.client = self.app.test_client() self.ctx = self.app.app_context() self.ctx.push() db.create_all() mark_setup_complete(self.app) seed_dispenser_period_recipes() alt = Component( id="e2e-disp-comp-alt", name="Alt Silo", type="silage", dry_matter=35.0, protein=3.0, energy=6.0, price=0.0, ) db.session.add(alt) db.session.commit() self.client.post( "/api/auth/login", json={"login": "zootech-test-admin", "password": "zootech-test-secret"}, ) def tearDown(self) -> None: db.session.remove() db.drop_all() self.ctx.pop() def test_adjustment_applies_to_all_trips_with_component(self) -> None: adjust_component_norm( E2E_DISP_COMP, self.PLAN_DATE, dry_matter=50.0, ) plan = build_daily_plan(dispenser_id=E2E_DISP_ID, plan_date=self.PLAN_DATE) trips = plan["periods"][0]["trips"] self.assertEqual(len(trips), 2) for trip in trips: ing = trip["ingredients"][0] self.assertTrue(ing["adjustedToday"]) self.assertEqual(ing["dryMatterPct"], 50.0) master_wph = 1.0 if trip["recipeId"] == E2E_DISP_RECIPE_1 else 2.0 self.assertEqual(ing["weightPerHead"], master_wph) self.assertAlmostEqual(ing["dryMatterPerHead"], master_wph * 0.5, places=4) self.assertEqual(ing["originalWeightPerHead"], master_wph) def test_master_ingredient_unchanged_after_adjustment(self) -> None: adjust_component_norm( E2E_DISP_COMP, self.PLAN_DATE, dry_matter=45.0, ) ing = db.session.get(Ingredient, "e2e-disp-ing-1") self.assertEqual(float(ing.weight_per_head), 1.0) master = self.client.get(f"/api/recipes/{E2E_DISP_RECIPE_1}").get_json() self.assertEqual(master["ingredients"][0]["weightPerHead"], 1.0) self.assertNotIn("adjustedToday", master["ingredients"][0]) overlay = self.client.get( f"/api/recipes/{E2E_DISP_RECIPE_1}?date={self.PLAN_DATE}" ).get_json() self.assertTrue(overlay["ingredients"][0]["adjustedToday"]) self.assertEqual(overlay["ingredients"][0]["weightPerHead"], 1.0) self.assertEqual(overlay["ingredients"][0]["dry_matter"], 45.0) self.assertAlmostEqual(overlay["ingredients"][0]["dry_matter_per_head"], 0.45, places=4) self.assertEqual(overlay["ingredients"][0]["originalWeightPerHead"], 1.0) def test_component_norms_api_and_undo(self) -> None: created = self.client.post( "/api/daily-plan/adjustments/components", json={ "componentId": E2E_DISP_COMP, "dryMatter": 40.0, "dryMatterLocked": True, "date": self.PLAN_DATE, "duration": "today", }, ) self.assertEqual(created.status_code, 200, created.get_data(as_text=True)) listed = self.client.get( f"/api/daily-plan/component-norms?dispenser_id={E2E_DISP_ID}&date={self.PLAN_DATE}" ).get_json() self.assertEqual(len(listed["items"]), 1) item = listed["items"][0] self.assertEqual(item["componentId"], E2E_DISP_COMP) self.assertTrue(item["adjustedToday"]) self.assertEqual(item["planDryMatterPct"], 40.0) self.assertTrue(item["planDryMatterLocked"]) self.assertEqual(item["usageCount"], 2) deleted = self.client.delete( f"/api/daily-plan/adjustments/components?component_id={E2E_DISP_COMP}&date={self.PLAN_DATE}" ) self.assertEqual(deleted.status_code, 200) self.assertEqual(get_component_adjustment_map(self.PLAN_DATE), {}) plan = build_daily_plan(dispenser_id=E2E_DISP_ID, plan_date=self.PLAN_DATE) ing = plan["periods"][0]["trips"][0]["ingredients"][0] self.assertFalse(ing["adjustedToday"]) def test_adjustment_before_replacement_on_same_ingredient(self) -> None: adjust_component_norm( E2E_DISP_COMP, self.PLAN_DATE, dry_matter=50.0, ) replace_ingredient( E2E_DISP_RECIPE_1, "e2e-disp-ing-1", "e2e-disp-comp-alt", self.PLAN_DATE, ) plan = build_daily_plan(dispenser_id=E2E_DISP_ID, plan_date=self.PLAN_DATE) ing = plan["periods"][0]["trips"][0]["ingredients"][0] self.assertTrue(ing["replacedToday"]) self.assertTrue(ing["adjustedToday"]) self.assertEqual(ing["weightPerHead"], 1.0) self.assertEqual(ing["dryMatterPct"], 35.0) def test_locked_mode_adjustment_keeps_dry_matter_per_head(self) -> None: recipe = db.session.get(Recipe, E2E_DISP_RECIPE_1) self.assertIsNotNone(recipe) recipe.dry_matter_locked = True db.session.commit() adjust_component_norm( E2E_DISP_COMP, self.PLAN_DATE, dry_matter=40.0, ) plan = build_daily_plan(dispenser_id=E2E_DISP_ID, plan_date=self.PLAN_DATE) ing = plan["periods"][0]["trips"][0]["ingredients"][0] self.assertAlmostEqual(ing["dryMatterPerHead"], 0.55, places=4) self.assertAlmostEqual(ing["weightPerHead"], 1.38, places=2) self.assertEqual(ing["dryMatterPct"], 40.0) def test_undo_service(self) -> None: adjust_component_norm(E2E_DISP_COMP, self.PLAN_DATE, dry_matter=42.0) self.assertTrue( undo_component_norm_adjustment(E2E_DISP_COMP, self.PLAN_DATE) ) self.assertEqual(get_component_adjustment_map(self.PLAN_DATE), {}) def test_list_component_norms_for_plan(self) -> None: rows = list_component_norms_for_plan(self.PLAN_DATE, dispenser_id=E2E_DISP_ID) self.assertEqual(len(rows), 1) self.assertEqual(rows[0]["usageCount"], 2) adjust_component_norm(E2E_DISP_COMP, self.PLAN_DATE, dry_matter=48.0) rows2 = list_component_norms_for_plan(self.PLAN_DATE, dispenser_id=E2E_DISP_ID) self.assertTrue(rows2[0]["adjustedToday"]) self.assertEqual(rows2[0]["planDryMatterPct"], 48.0) if __name__ == "__main__": unittest.main()