"""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