173 lines
6.7 KiB
Python
173 lines
6.7 KiB
Python
from datetime import date, timedelta
|
|
|
|
from django.core.exceptions import ValidationError
|
|
from django.utils.dateparse import parse_date
|
|
from rest_framework import permissions, status, viewsets
|
|
from rest_framework.decorators import action
|
|
from rest_framework.response import Response
|
|
|
|
from apps.accounts.models import User
|
|
from apps.farms.models import Cycle, Flock, Kandang, Site
|
|
from apps.farms.serializers import CycleSerializer, FlockSerializer, KandangSerializer, SiteSerializer
|
|
from apps.operations.models import FeedSacks, ManualInput
|
|
from apps.operations.services.karung_web import KarungWebError, request_karung
|
|
from apps.operations.services.manual_input import recompute_manual_totals
|
|
|
|
|
|
class IsSuperAdminForWrite(permissions.BasePermission):
|
|
"""Reads for any authenticated user; mutations only for superadmin.
|
|
|
|
Staff accounts may read sites/kandangs (to pick one and create a cycle) but
|
|
cannot create, edit, or delete them — only superadmins manage structures.
|
|
"""
|
|
|
|
def has_permission(self, request, view):
|
|
if not request.user or not request.user.is_authenticated:
|
|
return False
|
|
if request.method in permissions.SAFE_METHODS:
|
|
return True
|
|
return bool(request.user.is_superuser or request.user.status == User.STATUS_SUPERADMIN)
|
|
|
|
|
|
class SiteViewSet(viewsets.ModelViewSet):
|
|
queryset = Site.objects.select_related("user").all()
|
|
serializer_class = SiteSerializer
|
|
permission_classes = [IsSuperAdminForWrite]
|
|
|
|
def get_queryset(self):
|
|
qs = super().get_queryset()
|
|
user_id = self.request.query_params.get("user_id")
|
|
if user_id:
|
|
qs = qs.filter(user_id=user_id)
|
|
return qs
|
|
|
|
|
|
class KandangViewSet(viewsets.ModelViewSet):
|
|
queryset = Kandang.objects.select_related("site").all()
|
|
serializer_class = KandangSerializer
|
|
permission_classes = [IsSuperAdminForWrite]
|
|
|
|
def get_queryset(self):
|
|
qs = super().get_queryset()
|
|
site_id = self.request.query_params.get("site_id")
|
|
if site_id:
|
|
qs = qs.filter(site_id=site_id)
|
|
return qs
|
|
|
|
|
|
class FlockViewSet(viewsets.ModelViewSet):
|
|
queryset = Flock.objects.all()
|
|
serializer_class = FlockSerializer
|
|
|
|
def get_queryset(self):
|
|
qs = super().get_queryset()
|
|
cycle_id = self.request.query_params.get("cycle_id")
|
|
if cycle_id:
|
|
qs = qs.filter(cycle_id=cycle_id)
|
|
return qs
|
|
|
|
|
|
class CycleViewSet(viewsets.ModelViewSet):
|
|
queryset = Cycle.objects.select_related("kandang").prefetch_related("flocks").all()
|
|
serializer_class = CycleSerializer
|
|
|
|
def get_queryset(self):
|
|
qs = super().get_queryset()
|
|
params = self.request.query_params
|
|
if params.get("kandang_id"):
|
|
qs = qs.filter(kandang_id=params["kandang_id"])
|
|
if params.get("status"):
|
|
qs = qs.filter(status=params["status"])
|
|
return qs
|
|
|
|
@action(detail=True, methods=["post"], url_path="initial-balance")
|
|
def set_initial_balance(self, request, pk=None):
|
|
"""Save the feed-in balance for the cycle (Saldo Awal).
|
|
|
|
Sets ``feed_initial_balance`` on the cycle (the only cycle field this
|
|
feature writes — never during cycle creation), records the manual feed-in
|
|
for the balance date, then pulls the IoT-detected "in" for that date from
|
|
the karung-web-admin feed and returns a manual vs IoT accuracy figure.
|
|
"""
|
|
cycle = self.get_object()
|
|
|
|
date_str = request.data.get("date")
|
|
if not date_str:
|
|
return Response({"detail": "date is required."}, status=status.HTTP_400_BAD_REQUEST)
|
|
balance_date = parse_date(str(date_str))
|
|
if balance_date is None:
|
|
return Response(
|
|
{"detail": "date must be in YYYY-MM-DD format."},
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
)
|
|
|
|
try:
|
|
feed_in_manual = int(request.data.get("feed_in_manual"))
|
|
except (TypeError, ValueError):
|
|
return Response(
|
|
{"detail": "feed_in_manual must be an integer."},
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
)
|
|
if feed_in_manual < 0:
|
|
return Response(
|
|
{"detail": "feed_in_manual cannot be negative."},
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
)
|
|
|
|
cycle.feed_initial_balance = feed_in_manual
|
|
cycle.save(update_fields=["feed_initial_balance", "updated_at"])
|
|
|
|
# Pull IoT "in" at the balance date (karung-web-admin syncs today/yesterday).
|
|
feed_sack = FeedSacks.objects.filter(cycle=cycle, date=balance_date).first()
|
|
sync_error = None
|
|
if feed_sack is None:
|
|
try:
|
|
today = date.today()
|
|
section = None
|
|
if balance_date == today:
|
|
section = "today"
|
|
elif balance_date == today - timedelta(days=1):
|
|
section = "yesterday_history"
|
|
if section:
|
|
request_karung(cycle, section=section)
|
|
feed_sack = FeedSacks.objects.filter(cycle=cycle, date=balance_date).first()
|
|
except (KarungWebError, ValidationError) as exc:
|
|
sync_error = str(exc)
|
|
iot_in = feed_sack.in_today if feed_sack is not None else None
|
|
|
|
# Record the manual feed-in for the balance date.
|
|
ManualInput.objects.update_or_create(
|
|
cycle=cycle,
|
|
date=balance_date,
|
|
defaults={"feed_in_manual": feed_in_manual},
|
|
)
|
|
recompute_manual_totals(cycle)
|
|
|
|
# FeedSacks in_total always includes the initial balance as its base.
|
|
running = cycle.feed_initial_balance
|
|
for fs_row in FeedSacks.objects.filter(cycle=cycle).order_by("date", "pk"):
|
|
running += fs_row.in_today
|
|
if fs_row.in_total != running:
|
|
fs_row.in_total = running
|
|
fs_row.save(update_fields=["in_total", "updated_at"])
|
|
|
|
accuracy = None
|
|
if feed_in_manual > 0 and iot_in is not None and iot_in > 0:
|
|
accuracy = round(min(feed_in_manual, iot_in) / max(feed_in_manual, iot_in) * 1000) / 10
|
|
elif feed_in_manual == 0 and iot_in == 0:
|
|
accuracy = 100.0
|
|
|
|
manual_row = ManualInput.objects.filter(cycle=cycle, date=balance_date).first()
|
|
return Response(
|
|
{
|
|
"cycle": cycle.pk,
|
|
"date": balance_date.isoformat(),
|
|
"feed_initial_balance": cycle.feed_initial_balance,
|
|
"manual": feed_in_manual,
|
|
"manual_total": manual_row.feed_in_manual_total if manual_row else feed_in_manual,
|
|
"iot": iot_in,
|
|
"accuracy": accuracy,
|
|
"sync_error": sync_error,
|
|
}
|
|
)
|