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
@@ -1,58 +1,17 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db.models import Sum
|
||||
|
||||
from apps.farms.models import Cycle
|
||||
from apps.operations.models import ChickenCounting, FeedSacks, KPI
|
||||
from apps.operations.services.visibility import visible_through_date
|
||||
from apps.operations.services.kpi_rollups import refresh_kpi_source_fields
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Recompute simple KPI rollups from ops tables for active cycles"
|
||||
help = "Recompute KPI source fields and FCR/EEF for active cycles"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
cutoff = visible_through_date()
|
||||
count = 0
|
||||
for cycle in Cycle.objects.filter(status=Cycle.STATUS_ACTIVE):
|
||||
def visible(model):
|
||||
return model.objects.filter(cycle=cycle, date__lte=cutoff)
|
||||
|
||||
counting = visible(ChickenCounting).aggregate(
|
||||
mortality=Sum("mortality_count"),
|
||||
life=Sum("total_count"),
|
||||
)
|
||||
feed_sack = visible(FeedSacks).order_by("-date", "-pk").first()
|
||||
|
||||
latest_cc = visible(ChickenCounting).order_by("-date").first()
|
||||
latest_cw = None
|
||||
latest_fs = feed_sack
|
||||
latest_mi = None
|
||||
|
||||
from apps.operations.models import ChickenWeight, ManualInput
|
||||
latest_cw = visible(ChickenWeight).order_by("-date").first()
|
||||
latest_mi = visible(ManualInput).order_by("-date").first()
|
||||
|
||||
if not all([latest_cc, latest_cw, latest_fs, latest_mi]):
|
||||
self.stdout.write(f"cycle={cycle.pk} skipped (missing source records)")
|
||||
continue
|
||||
|
||||
kpi, _ = KPI.objects.update_or_create(
|
||||
cycle=cycle,
|
||||
date=latest_cc.date,
|
||||
defaults={
|
||||
"cc": latest_cc,
|
||||
"cw": latest_cw,
|
||||
"fs": latest_fs,
|
||||
"manual": latest_mi,
|
||||
"mortality_total": counting["mortality"] or 0,
|
||||
"chicken_life": counting["life"] or cycle.doc_in_count,
|
||||
"feed_total": feed_sack.feed_use_total if feed_sack else 0,
|
||||
"chicken_life_percentage": (
|
||||
((counting["life"] or cycle.doc_in_count) / cycle.doc_in_count * 100.0)
|
||||
if cycle.doc_in_count
|
||||
else 0.0
|
||||
),
|
||||
},
|
||||
)
|
||||
count += 1
|
||||
self.stdout.write(f"KPI {kpi.pk} for cycle {cycle.pk}")
|
||||
cycles = Cycle.objects.filter(status=Cycle.STATUS_ACTIVE)
|
||||
for cycle in cycles:
|
||||
written = refresh_kpi_source_fields(cycle)
|
||||
count += written
|
||||
self.stdout.write(f"cycle={cycle.pk} wrote {written} KPI row(s)")
|
||||
self.stdout.write(self.style.SUCCESS(f"Recomputed {count} KPI row(s)"))
|
||||
@@ -11,6 +11,7 @@ from apps.operations.models import (
|
||||
ManualInput,
|
||||
)
|
||||
from apps.accounts.serializers import PkAsIdMixin
|
||||
from apps.operations.services.kpi_rollups import refresh_kpi_source_fields
|
||||
from apps.operations.services.manual_input import recompute_manual_totals
|
||||
|
||||
|
||||
@@ -93,6 +94,7 @@ class ManualInputSerializer(PkAsIdMixin, CycleContextMixin, serializers.ModelSer
|
||||
defaults=defaults,
|
||||
)
|
||||
recompute_manual_totals(cycle)
|
||||
refresh_kpi_source_fields(cycle)
|
||||
row.refresh_from_db()
|
||||
return row
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
"""Pure KPI / FCR / EEF formula helpers (Excel-correct)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
def compute_chicken_life(doc_in_count: int, mortality_total: int) -> int:
|
||||
return max(int(doc_in_count) - int(mortality_total), 0)
|
||||
|
||||
|
||||
def compute_chicken_life_percentage(doc_in_count: int, chicken_life: int) -> float:
|
||||
if not doc_in_count:
|
||||
return 0.0
|
||||
return (chicken_life / doc_in_count) * 100.0
|
||||
|
||||
|
||||
def compute_stock_akhir(chicken_life: int, harvest_total: int) -> int:
|
||||
return max(int(chicken_life) - int(harvest_total), 0)
|
||||
|
||||
|
||||
def compute_fcr(
|
||||
feed_total: int | float,
|
||||
stock_akhir: int,
|
||||
iot_weight: float,
|
||||
harvest_weight_total_g: float,
|
||||
) -> float:
|
||||
"""FCR = (feed_total * 50 * 1000) / (stock_akhir * iot_weight + harvest_weight_total_g)."""
|
||||
denom = (stock_akhir * iot_weight) + harvest_weight_total_g
|
||||
if denom <= 0:
|
||||
return 0.0
|
||||
return (feed_total * 50 * 1000) / denom
|
||||
|
||||
|
||||
def compute_average_harvest_weight(
|
||||
harvest_weight_total_g: float,
|
||||
stock_akhir: int,
|
||||
iot_weight: float,
|
||||
harvest_total: int,
|
||||
) -> float:
|
||||
"""Excel live-blend: (hwt_g + stock*iot) / (harvest_total + stock)."""
|
||||
denom = harvest_total + stock_akhir
|
||||
if denom <= 0:
|
||||
return 0.0
|
||||
return (harvest_weight_total_g + stock_akhir * iot_weight) / denom
|
||||
|
||||
|
||||
def compute_average_harvest_day(
|
||||
harvest_age_sum: float,
|
||||
age: int,
|
||||
stock_akhir: int,
|
||||
harvest_total: int,
|
||||
) -> float:
|
||||
"""Excel live-blend: (Σ(age*harvest) + age*stock) / (harvest_total + stock)."""
|
||||
denom = harvest_total + stock_akhir
|
||||
if denom <= 0:
|
||||
return 0.0
|
||||
return (harvest_age_sum + age * stock_akhir) / denom
|
||||
|
||||
|
||||
def compute_eef(
|
||||
life_pct: float,
|
||||
average_harvest_weight_g: float,
|
||||
average_harvest_day: float,
|
||||
fcr: float,
|
||||
) -> float:
|
||||
"""EEF = life_pct * (avg_bw_g/1000) / (avg_day * fcr) * 100."""
|
||||
if average_harvest_day <= 0 or fcr <= 0:
|
||||
return 0.0
|
||||
return life_pct * (average_harvest_weight_g / 1000.0) / (average_harvest_day * fcr) * 100.0
|
||||
@@ -0,0 +1,118 @@
|
||||
"""Refresh daily KPI rows from ops sources and compute FCR / EEF."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date
|
||||
|
||||
from apps.farms.models import Cycle
|
||||
from apps.operations.models import ChickenCounting, ChickenWeight, FeedSacks, KPI, ManualInput
|
||||
from apps.operations.services.kpi_formulas import (
|
||||
compute_average_harvest_day,
|
||||
compute_average_harvest_weight,
|
||||
compute_chicken_life,
|
||||
compute_chicken_life_percentage,
|
||||
compute_eef,
|
||||
compute_fcr,
|
||||
compute_stock_akhir,
|
||||
)
|
||||
from apps.operations.services.visibility import visible_through_date
|
||||
|
||||
|
||||
def refresh_kpi_source_fields(cycle: Cycle, *, through: date | None = None) -> int:
|
||||
"""
|
||||
Upsert one KPI row per day that has cc + cw + fs + manual.
|
||||
|
||||
Returns the number of KPI rows written.
|
||||
"""
|
||||
cutoff = through if through is not None else visible_through_date()
|
||||
start = cycle.start_date
|
||||
if cycle.end_date and cycle.end_date < cutoff:
|
||||
cutoff = cycle.end_date
|
||||
|
||||
manuals = {
|
||||
row.date: row
|
||||
for row in ManualInput.objects.filter(cycle=cycle, date__gte=start, date__lte=cutoff).order_by(
|
||||
"date", "pk"
|
||||
)
|
||||
}
|
||||
countings = {
|
||||
row.date: row
|
||||
for row in ChickenCounting.objects.filter(cycle=cycle, date__gte=start, date__lte=cutoff)
|
||||
}
|
||||
weights = {
|
||||
row.date: row
|
||||
for row in ChickenWeight.objects.filter(cycle=cycle, date__gte=start, date__lte=cutoff)
|
||||
}
|
||||
feeds = {
|
||||
row.date: row
|
||||
for row in FeedSacks.objects.filter(cycle=cycle, date__gte=start, date__lte=cutoff)
|
||||
}
|
||||
|
||||
written = 0
|
||||
harvest_age_sum = 0.0
|
||||
|
||||
for day in sorted(set(manuals) | set(countings) | set(weights) | set(feeds)):
|
||||
if day < start or day > cutoff:
|
||||
continue
|
||||
manual = manuals.get(day)
|
||||
cc = countings.get(day)
|
||||
cw = weights.get(day)
|
||||
fs = feeds.get(day)
|
||||
if not all([manual, cc, cw, fs]):
|
||||
# Still accumulate harvest age sum from manual-only days so later
|
||||
# complete days keep a correct running Σ(age×harvest).
|
||||
if manual is not None:
|
||||
age = (day - start).days
|
||||
harvest_age_sum += age * (manual.harvest_manual or 0)
|
||||
continue
|
||||
|
||||
age = (day - start).days
|
||||
harvest_today = manual.harvest_manual or 0
|
||||
harvest_age_sum += age * harvest_today
|
||||
|
||||
mortality_total = manual.mortality_manual_total or 0
|
||||
harvest_total = manual.harvest_manual_total or 0
|
||||
harvest_weight_total_g = float(manual.harvest_weight_manual_total or 0.0)
|
||||
feed_total = fs.feed_use_total or 0
|
||||
iot_weight = float(cw.average_weight or 0.0)
|
||||
|
||||
chicken_life = compute_chicken_life(cycle.doc_in_count, mortality_total)
|
||||
life_pct = compute_chicken_life_percentage(cycle.doc_in_count, chicken_life)
|
||||
stock_akhir = compute_stock_akhir(chicken_life, harvest_total)
|
||||
|
||||
avg_bw = compute_average_harvest_weight(
|
||||
harvest_weight_total_g, stock_akhir, iot_weight, harvest_total
|
||||
)
|
||||
avg_day = compute_average_harvest_day(harvest_age_sum, age, stock_akhir, harvest_total)
|
||||
fcr = compute_fcr(feed_total, stock_akhir, iot_weight, harvest_weight_total_g)
|
||||
eef = compute_eef(life_pct, avg_bw, avg_day, fcr)
|
||||
|
||||
KPI.objects.update_or_create(
|
||||
cycle=cycle,
|
||||
date=day,
|
||||
defaults={
|
||||
"age": age,
|
||||
"mortality": manual.mortality_manual or 0,
|
||||
"mortality_total": mortality_total,
|
||||
"feed": fs.feed_use_today or 0,
|
||||
"feed_total": feed_total,
|
||||
"harvest": harvest_today,
|
||||
"harvest_total": harvest_total,
|
||||
"harvest_weight": float(manual.harvest_weight_manual or 0.0),
|
||||
"harvest_weight_total": harvest_weight_total_g,
|
||||
"chicken_life": chicken_life,
|
||||
"chicken_life_percentage": life_pct,
|
||||
"iot_weight": iot_weight,
|
||||
"average_harvest_day": avg_day,
|
||||
"average_harvest_weight": avg_bw,
|
||||
"fcr": fcr,
|
||||
"eef": eef,
|
||||
"cc": cc,
|
||||
"cw": cw,
|
||||
"fs": fs,
|
||||
"manual": manual,
|
||||
},
|
||||
)
|
||||
written += 1
|
||||
|
||||
return written
|
||||
@@ -0,0 +1,163 @@
|
||||
"""Excel-verified FCR/EEF formula tests (Sukawarna Kandang Atas days 30–35)."""
|
||||
|
||||
from django.test import SimpleTestCase
|
||||
|
||||
from apps.operations.services.kpi_formulas import (
|
||||
compute_average_harvest_day,
|
||||
compute_average_harvest_weight,
|
||||
compute_chicken_life,
|
||||
compute_chicken_life_percentage,
|
||||
compute_eef,
|
||||
compute_fcr,
|
||||
compute_stock_akhir,
|
||||
)
|
||||
|
||||
# From Laporan_Akhir-Siklus_Sukawarna_Kandang-Atas … FCR + EEF sheets.
|
||||
# harvest_total / harvest_age_sum reconstructed so live-blend avgs match Excel.
|
||||
EXCEL_DAYS = [
|
||||
{
|
||||
"age": 30,
|
||||
"feed_total": 928,
|
||||
"iot_weight": 1480.51,
|
||||
"stock_akhir": 17658,
|
||||
"harvest_weight_total_g": 6272.1 * 1000,
|
||||
"harvest_total": 6400,
|
||||
"life_pct": 96.232,
|
||||
"excel_fcr": 1.4314384667247064,
|
||||
"excel_avg_bw": 1347.3665965583173,
|
||||
"excel_avg_day": 29.112145648017293,
|
||||
"excel_eef": 311.14182093425177,
|
||||
},
|
||||
{
|
||||
"age": 31,
|
||||
"feed_total": 970,
|
||||
"iot_weight": 1524.39,
|
||||
"stock_akhir": 16419,
|
||||
"harvest_weight_total_g": 7665.3 * 1000,
|
||||
"harvest_total": 7600,
|
||||
"life_pct": 96.076,
|
||||
"excel_fcr": 1.4834408509392811,
|
||||
"excel_avg_bw": 1361.183205379075,
|
||||
"excel_avg_day": 29.844248303426454,
|
||||
"excel_eef": 295.39328356931856,
|
||||
},
|
||||
{
|
||||
"age": 32,
|
||||
"feed_total": 1013,
|
||||
"iot_weight": 1578.85,
|
||||
"stock_akhir": 15929,
|
||||
"harvest_weight_total_g": 8229.1 * 1000,
|
||||
"harvest_total": 8045,
|
||||
"life_pct": 95.896,
|
||||
"excel_fcr": 1.5174392423955845,
|
||||
"excel_avg_bw": 1392.2833757403853,
|
||||
"excel_avg_day": 30.52506882455994,
|
||||
"excel_eef": 288.2439276697344,
|
||||
},
|
||||
{
|
||||
"age": 33,
|
||||
"feed_total": 1031,
|
||||
"iot_weight": 1609.98,
|
||||
"stock_akhir": 10458,
|
||||
"harvest_weight_total_g": 16476 * 1000,
|
||||
"harvest_total": 13421,
|
||||
"life_pct": 95.516,
|
||||
"excel_fcr": 1.5474360050440639,
|
||||
"excel_avg_bw": 1395.0823250554881,
|
||||
"excel_avg_day": 31.18229406591566,
|
||||
"excel_eef": 276.15645742250786,
|
||||
},
|
||||
{
|
||||
"age": 34,
|
||||
"feed_total": 1052,
|
||||
"iot_weight": 1720.44,
|
||||
"stock_akhir": 10347,
|
||||
"harvest_weight_total_g": 16476 * 1000,
|
||||
"harvest_total": 13421,
|
||||
"life_pct": 95.072,
|
||||
"excel_fcr": 1.5345391200273755,
|
||||
"excel_avg_bw": 1442.1656294177044,
|
||||
"excel_avg_day": 31.60913833726018,
|
||||
"excel_eef": 282.668327683184,
|
||||
},
|
||||
{
|
||||
"age": 35,
|
||||
"feed_total": 1059,
|
||||
"iot_weight": 1561.1,
|
||||
"stock_akhir": 5198,
|
||||
"harvest_weight_total_g": 25032.8 * 1000,
|
||||
"harvest_total": 18413,
|
||||
"life_pct": 94.444,
|
||||
"excel_fcr": 1.5974104609804394,
|
||||
"excel_avg_bw": 1403.8963957477447,
|
||||
"excel_avg_day": 32.02481894032442,
|
||||
"excel_eef": 259.1828280867179,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def _harvest_age_sum(row: dict) -> float:
|
||||
"""Invert Excel avg_day blend: (sum + age*stock) / (h + stock) = avg_day."""
|
||||
age = row["age"]
|
||||
stock = row["stock_akhir"]
|
||||
h = row["harvest_total"]
|
||||
return row["excel_avg_day"] * (h + stock) - age * stock
|
||||
|
||||
|
||||
class KpiFormulaTests(SimpleTestCase):
|
||||
def test_chicken_life_excludes_harvest(self):
|
||||
self.assertEqual(compute_chicken_life(25000, 942), 24058)
|
||||
self.assertEqual(compute_chicken_life_percentage(25000, 24058), 96.232)
|
||||
self.assertEqual(compute_stock_akhir(24058, 6400), 17658)
|
||||
|
||||
def test_fcr_matches_excel_days_30_to_35(self):
|
||||
for row in EXCEL_DAYS:
|
||||
with self.subTest(age=row["age"]):
|
||||
got = compute_fcr(
|
||||
row["feed_total"],
|
||||
row["stock_akhir"],
|
||||
row["iot_weight"],
|
||||
row["harvest_weight_total_g"],
|
||||
)
|
||||
self.assertAlmostEqual(got, row["excel_fcr"], places=9)
|
||||
|
||||
def test_average_harvest_blend_matches_excel_days_30_to_35(self):
|
||||
for row in EXCEL_DAYS:
|
||||
with self.subTest(age=row["age"]):
|
||||
avg_bw = compute_average_harvest_weight(
|
||||
row["harvest_weight_total_g"],
|
||||
row["stock_akhir"],
|
||||
row["iot_weight"],
|
||||
row["harvest_total"],
|
||||
)
|
||||
avg_day = compute_average_harvest_day(
|
||||
_harvest_age_sum(row),
|
||||
row["age"],
|
||||
row["stock_akhir"],
|
||||
row["harvest_total"],
|
||||
)
|
||||
self.assertAlmostEqual(avg_bw, row["excel_avg_bw"], places=6)
|
||||
self.assertAlmostEqual(avg_day, row["excel_avg_day"], places=6)
|
||||
|
||||
def test_eef_matches_excel_days_30_to_35(self):
|
||||
# Use Excel avg columns + computed FCR (same path as verified against the sheet).
|
||||
for row in EXCEL_DAYS:
|
||||
with self.subTest(age=row["age"]):
|
||||
fcr = compute_fcr(
|
||||
row["feed_total"],
|
||||
row["stock_akhir"],
|
||||
row["iot_weight"],
|
||||
row["harvest_weight_total_g"],
|
||||
)
|
||||
got = compute_eef(
|
||||
row["life_pct"],
|
||||
row["excel_avg_bw"],
|
||||
row["excel_avg_day"],
|
||||
fcr,
|
||||
)
|
||||
self.assertAlmostEqual(got, row["excel_eef"], places=9)
|
||||
|
||||
def test_fcr_and_eef_zero_when_denom_invalid(self):
|
||||
self.assertEqual(compute_fcr(100, 0, 0, 0), 0.0)
|
||||
self.assertEqual(compute_eef(99.0, 1000.0, 0, 1.2), 0.0)
|
||||
self.assertEqual(compute_eef(99.0, 1000.0, 10, 0), 0.0)
|
||||
@@ -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