Files
dashboard-cpsp/backend/apps/operations/tests_kpi_rollups.py
T

124 lines
5.2 KiB
Python

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)