from datetime import date, timedelta from django.test import TestCase, override_settings from apps.accounts.models import User from apps.farms.models import Cycle, Kandang, Site from apps.operations.models import ChickenCounting, ChickenWeight, FeedSacks, KPI, ManualInput from apps.operations.services.kpi_rollups import refresh_kpi_source_fields from apps.operations.services.manual_input import recompute_manual_totals @override_settings(DASHBOARD_PUBLISH_HOUR=0) class KpiRollupServiceTests(TestCase): def setUp(self): user = User.objects.create_user(user_name="kpi-tester", password="secret") site = Site.objects.create(site_name="Site", user=user) kandang = Kandang.objects.create(kandang_name="K1", site=site) self.start = date(2026, 5, 22) self.cycle = Cycle.objects.create( kandang=kandang, total_days=40, start_date=self.start, end_date=self.start + timedelta(days=40), doc_in_weight=40, doc_in_count=25000, status=Cycle.STATUS_ACTIVE, ) def _sources(self, day: date, *, mort=10, harvest=0, harvest_wt_g=0.0, feed_use=15, weight=35.0): ChickenCounting.objects.create(cycle=self.cycle, date=day, total_count=20000, mortality_count=mort) ChickenWeight.objects.create( cycle=self.cycle, date=day, age=(day - self.start).days, doc_weight=40, average_weight=weight, chicken_count=100, uniformity=80, average_daily_gain=1, ) FeedSacks.objects.create( cycle=self.cycle, date=day, feed_use_today=feed_use, feed_use_total=feed_use, ) ManualInput.objects.create( cycle=self.cycle, date=day, age_manual=(day - self.start).days, mortality_manual=mort, harvest_manual=harvest, harvest_weight_manual=harvest_wt_g, ) recompute_manual_totals(self.cycle) def test_skips_day_when_weight_missing(self): day = self.start ChickenCounting.objects.create(cycle=self.cycle, date=day, total_count=1, mortality_count=0) FeedSacks.objects.create(cycle=self.cycle, date=day, feed_use_today=1, feed_use_total=1) ManualInput.objects.create(cycle=self.cycle, date=day, mortality_manual=5) recompute_manual_totals(self.cycle) written = refresh_kpi_source_fields(self.cycle, through=day) self.assertEqual(written, 0) self.assertFalse(KPI.objects.filter(cycle=self.cycle, date=day).exists()) def test_writes_kpi_with_chicken_life_and_fcr(self): day0 = self.start self._sources(day0, mort=21, feed_use=15, weight=35.0) written = refresh_kpi_source_fields(self.cycle, through=day0) self.assertEqual(written, 1) kpi = KPI.objects.get(cycle=self.cycle, date=day0) self.assertEqual(kpi.age, 0) self.assertEqual(kpi.chicken_life, 25000 - 21) self.assertAlmostEqual(kpi.chicken_life_percentage, (24979 / 25000) * 100, places=6) self.assertEqual(kpi.iot_weight, 35.0) self.assertEqual(kpi.feed_total, 15) self.assertAlmostEqual(kpi.fcr, (15 * 50 * 1000) / (24979 * 35), places=9) self.assertEqual(kpi.eef, 0.0) # age 0 → avg day blend is 0 def test_stock_akhir_used_in_fcr_after_harvest(self): day0 = self.start day1 = self.start + timedelta(days=1) self._sources(day0, mort=21, feed_use=15, weight=35.0) self._sources(day1, mort=30, harvest=100, harvest_wt_g=80_000.0, feed_use=20, weight=48.44) fs1 = FeedSacks.objects.get(cycle=self.cycle, date=day1) fs1.feed_use_total = 35 fs1.save(update_fields=["feed_use_total"]) recompute_manual_totals(self.cycle) refresh_kpi_source_fields(self.cycle, through=day1) kpi = KPI.objects.get(cycle=self.cycle, date=day1) mort_total = 51 harvest_total = 100 chicken_life = 25000 - mort_total stock = chicken_life - harvest_total self.assertEqual(kpi.chicken_life, chicken_life) self.assertEqual(kpi.harvest_total, harvest_total) expected_fcr = (35 * 50 * 1000) / (stock * 48.44 + 80_000.0) self.assertAlmostEqual(kpi.fcr, expected_fcr, places=9) self.assertGreater(kpi.eef, 0) def test_manual_save_refreshes_kpi_when_sources_complete(self): day = self.start ChickenCounting.objects.create(cycle=self.cycle, date=day, total_count=1, mortality_count=0) ChickenWeight.objects.create( cycle=self.cycle, date=day, age=0, doc_weight=40, average_weight=35.0, chicken_count=1, uniformity=80, average_daily_gain=0, ) FeedSacks.objects.create(cycle=self.cycle, date=day, feed_use_today=15, feed_use_total=15) ManualInput.objects.create(cycle=self.cycle, date=day, mortality_manual=21) recompute_manual_totals(self.cycle) refresh_kpi_source_fields(self.cycle, through=day) self.assertTrue(KPI.objects.filter(cycle=self.cycle, date=day).exists()) self.assertEqual(KPI.objects.get(cycle=self.cycle, date=day).mortality_total, 21)