119 lines
4.2 KiB
Python
119 lines
4.2 KiB
Python
"""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
|