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.services.feed_sacks import ( cleanup_opening_balance_feed_rows, recompute_feed_sack_in_totals, ) from apps.operations.services.karung_web import ( initial_balance_accuracy, pull_initial_balance_iot, ) 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.select_related("kandang").all() serializer_class = FlockSerializer permission_classes = [IsSuperAdminForWrite] def get_queryset(self): qs = super().get_queryset() kandang_id = self.request.query_params.get("kandang_id") if kandang_id: qs = qs.filter(kandang_id=kandang_id) return qs class CycleViewSet(viewsets.ModelViewSet): queryset = Cycle.objects.select_related("kandang").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=["get"], url_path="initial-balance-compare") def compare_initial_balance(self, request, pk=None): """Pull IoT masuk for a date and compare against the cycle saldo awal.""" cycle = self.get_object() date_str = request.query_params.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, ) manual_raw = request.query_params.get("manual") if manual_raw is None: feed_in_manual = cycle.feed_initial_balance else: try: feed_in_manual = int(manual_raw) except (TypeError, ValueError): return Response( {"detail": "manual must be an integer."}, status=status.HTTP_400_BAD_REQUEST, ) iot_in, sync_error = pull_initial_balance_iot(cycle, balance_date) return Response( { "cycle": cycle.pk, "date": balance_date.isoformat(), "feed_initial_balance": cycle.feed_initial_balance, "feed_initial_balance_date": ( cycle.feed_initial_balance_date.isoformat() if cycle.feed_initial_balance_date else None ), "manual": feed_in_manual, "manual_total": feed_in_manual, "iot": iot_in, "accuracy": initial_balance_accuracy(feed_in_manual, iot_in), "sync_error": sync_error, } ) @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). Opening balance is kept on the cycle for saldo only and is not written into daily FeedSacks rows. Pulls the IoT-detected "in" for that date from karung-web-admin ``/api/combined`` 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, ) previous_date = cycle.feed_initial_balance_date previous_balance = cycle.feed_initial_balance cycle.feed_initial_balance = feed_in_manual cycle.feed_initial_balance_date = balance_date cycle.save( update_fields=["feed_initial_balance", "feed_initial_balance_date", "updated_at"] ) # Pull IoT "in" at the balance date from karung-web-admin /api/combined. iot_in, sync_error = pull_initial_balance_iot(cycle, balance_date) cleanup_opening_balance_feed_rows( cycle, previous_date=previous_date, previous_balance=previous_balance, balance_date=balance_date, balance_amount=feed_in_manual, ) recompute_feed_sack_in_totals(cycle) accuracy = initial_balance_accuracy(feed_in_manual, iot_in) return Response( { "cycle": cycle.pk, "date": balance_date.isoformat(), "feed_initial_balance": cycle.feed_initial_balance, "feed_initial_balance_date": ( cycle.feed_initial_balance_date.isoformat() if cycle.feed_initial_balance_date else None ), "manual": feed_in_manual, "manual_total": feed_in_manual, "iot": iot_in, "accuracy": accuracy, "sync_error": sync_error, } )