kpi configuration (still prototype some data still use manual need to update later)
This commit is contained in:
1 parent
e4dbfd0252
commit
ed84d989b7
12 files changed
+547
-89
No files matched your search
@@ -0,0 +1,123 @@
|
||||
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)
|
||||
Reference in new issue
Block a user