updating cycle close request feature to get approval from national dashboard instead auto from system

This commit is contained in:
Alberto-Audrix committed 2026-09-09 14:18:39 +07:00
1 parent 3a868cf1db
commit e4dbfd0252
31 files changed
+1322 -182

No files matched your search

@@ -12,7 +12,7 @@ from django.db import transaction
from django.utils import timezone
from apps.farms.models import Cycle
from apps.operations.models import ChickenCounting
from apps.operations.models import ChickenCounting, ChickenWeight
class ChickenCountingEdgeError(Exception):
@@ -87,10 +87,47 @@ def aggregate_mortality_by_date(
return by_date
def aggregate_weight_by_date(
rows: list[dict[str, Any]] | None,
*,
kandang_name: str,
) -> dict[date, dict[str, float | int]]:
"""Map date → fused weight metrics for the matching coop.
Prefer ``camera_id=FUSED``. When both floors (e.g. K1-L1 / K1-L2) carry the
same fused prediction, average weight/uniformity and take max sample count
so duplicates are not double-counted.
"""
expected = coop_for_kandang(kandang_name).casefold()
floors_by_date: dict[date, list[dict[str, Any]]] = {}
for row in rows or []:
if str(row.get("camera_id") or "").strip().upper() != "FUSED":
continue
day = _parse_history_date(row.get("date"))
if day is None:
continue
coop = str(row.get("coop") or "").strip()
if coop.casefold() != expected:
continue
floors_by_date.setdefault(day, []).append(row)
by_date: dict[date, dict[str, float | int]] = {}
for day, floors in floors_by_date.items():
weights = [float(f.get("predicted_weight_g") or 0) for f in floors]
unifs = [float(f.get("uniformity") or 0) for f in floors]
samples = [int(f.get("n_valid_samples") or 0) for f in floors]
by_date[day] = {
"average_weight": sum(weights) / len(weights),
"uniformity": sum(unifs) / len(unifs),
"chicken_count": max(samples) if samples else 0,
}
return by_date
def cycle_history_window(cycle: Cycle, *, today: date | None = None) -> tuple[date, date]:
if today is None:
today = timezone.localdate()
end = min(today, cycle.end_date)
end = cycle.operational_end_date(today=today)
return cycle.start_date, end
@@ -99,6 +136,7 @@ class ChickenCountingEdgeClient:
DB_HISTORY_PATH = "/api/db/history"
MORTALITY_HISTORY_PATH = "/api/mortality/history"
WEIGHT_PREDICTIONS_PATH = "/api/weight/predictions"
def __init__(self, base_url: str | None = None, timeout: float | None = None):
self.base_url = (
@@ -136,6 +174,12 @@ class ChickenCountingEdgeClient:
return payload
return list(payload.get("results") or payload.get("history") or [])
def fetch_weight_predictions(self) -> list[dict[str, Any]]:
payload = self._get_json(self.WEIGHT_PREDICTIONS_PATH)
if isinstance(payload, list):
return payload
return list(payload.get("predictions") or payload.get("results") or [])
@transaction.atomic
def sync_chicken_counting_for_cycle(
@@ -212,3 +256,77 @@ def sync_chicken_counting_for_cycle(
upserted.append(day)
return {"dates": upserted, "upserted": len(upserted), "skipped": False}
@transaction.atomic
def sync_chicken_weight_for_cycle(
cycle: Cycle,
*,
predictions: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
"""Upsert ChickenWeight from edge weight predictions within the cycle window.
Day 0 (cycle start) always uses DOC weight and 0% uniformity — edge values
for that date are ignored.
"""
if not bool(getattr(settings, "CHICKEN_COUNTING_EDGE_WEIGHT_SYNC_ENABLED", True)):
return {"dates": [], "upserted": 0, "skipped": True}
client = ChickenCountingEdgeClient()
if predictions is None:
predictions = client.fetch_weight_predictions()
kandang_name = cycle.kandang.kandang_name
weight_by_date = aggregate_weight_by_date(predictions, kandang_name=kandang_name)
start, end = cycle_history_window(cycle)
if start > end:
return {"dates": [], "upserted": 0, "skipped": False}
doc_weight = int(cycle.doc_in_weight)
dates = sorted({d for d in weight_by_date if start <= d <= end} | {start})
prev_weight = float(doc_weight)
# Seed previous-day weight from any row before the first upsert date.
existing_before = (
ChickenWeight.objects.filter(cycle=cycle, date__lt=dates[0])
.order_by("-date")
.first()
)
if existing_before is not None:
prev_weight = float(existing_before.average_weight)
upserted: list[date] = []
for day in dates:
age = (day - start).days
if age == 0:
average_weight = float(doc_weight)
uniformity = 0.0
chicken_count = int(cycle.doc_in_count)
adg = 0.0
else:
metrics = weight_by_date.get(day)
if metrics is None:
continue
average_weight = float(metrics["average_weight"])
uniformity = float(metrics["uniformity"])
chicken_count = int(metrics["chicken_count"])
adg = round(average_weight - prev_weight, 2)
defaults = {
"age": age,
"doc_weight": doc_weight,
"average_weight": average_weight,
"chicken_count": chicken_count,
"uniformity": uniformity,
"average_daily_gain": adg,
}
ChickenWeight.objects.update_or_create(
cycle=cycle,
date=day,
defaults=defaults,
)
upserted.append(day)
prev_weight = average_weight
return {"dates": upserted, "upserted": len(upserted), "skipped": False}