124 lines
5.2 KiB
Python
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)
|