updating cycle close request feature to get approval from national dashboard instead auto from system
This commit is contained in:
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}
|
||||
Reference in new issue
Block a user