from rest_framework import viewsets, status from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated, AllowAny from .models import ComPortReading, User, Report from .serializers import ( ComPortReadingSerializer, UserSerializer, UserDetailSerializer, ChangePasswordSerializer, VehicleSerializer, NomenclatureSerializer, NomenclatureEntrySerializer, ReportSerializer, ) from django.utils import timezone from vehicles.models import Vehicle from nomenclatures.models import Nomenclature, NomenclatureEntry from scalesapp.sse import sse_broadcast_update class UserViewSet(viewsets.ModelViewSet): """ API endpoint for user management. list: Get all users create: Create a new user retrieve: Get a specific user update: Update a user destroy: Delete a user me: Get current authenticated user change_password: Change password for current user """ queryset = User.objects.all() serializer_class = UserSerializer filterset_fields = ['role', 'is_admin', 'is_active'] ordering = ['username'] @action(detail=False, methods=['get'], permission_classes=[IsAuthenticated]) def me(self, request): """Get current authenticated user details""" serializer = UserDetailSerializer(request.user) return Response(serializer.data) @action(detail=False, methods=['post'], permission_classes=[IsAuthenticated], url_path='change-password', url_name='change_password') def change_password(self, request): """Change password for current user""" serializer = ChangePasswordSerializer( data=request.data, context={'request': request} ) if serializer.is_valid(): serializer.save() return Response({ 'detail': 'Password updated successfully' }, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) class ComPortReadingViewSet(viewsets.ModelViewSet): """ API endpoint for COM port readings. list: Get all readings with optional filtering create: Create a new reading (allows unauthenticated for serial bridge) retrieve: Get a specific reading destroy: Delete a reading latest: Get the latest reading by_port: Get readings for a specific port """ queryset = ComPortReading.objects.all() serializer_class = ComPortReadingSerializer filterset_fields = ['port'] ordering = ['-timestamp'] def get_permissions(self): """Allow unauthenticated POST for serial bridge""" if self.action == 'create': return [AllowAny()] return [IsAuthenticated()] @action(detail=False, methods=['get']) def latest(self, request): """Get the latest reading""" reading = ComPortReading.objects.first() if reading: serializer = self.get_serializer(reading) return Response(serializer.data) return Response({'detail': 'No readings yet'}, status=status.HTTP_404_NOT_FOUND) @action(detail=False, methods=['get']) def by_port(self, request): """Get readings for a specific port""" port = request.query_params.get('port', None) if not port: return Response( {'detail': 'port parameter is required'}, status=status.HTTP_400_BAD_REQUEST ) readings = ComPortReading.objects.filter(port=port) serializer = self.get_serializer(readings, many=True) return Response(serializer.data) def get_client_ip(self, request): """Get client IP address""" x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') if x_forwarded_for: ip = x_forwarded_for.split(',')[0] else: ip = request.META.get('REMOTE_ADDR') return ip def perform_create(self, serializer): """Save the reading with client IP""" serializer.save(source_ip=self.get_client_ip(self.request)) class VehicleViewSet(viewsets.ModelViewSet): """ API endpoint for vehicle management. list: Get all vehicles with pagination create: Create a new vehicle with optional extra data retrieve: Get a specific vehicle by ID update: Full update of vehicle and extra data partial_update: Partial update of vehicle destroy: Delete a vehicle (cascades to VehicleExtra) by_number: Get vehicle by vehicle_number set_tare: Set tare weight from scale reading set_gross: Set gross weight from scale reading """ queryset = Vehicle.objects.select_related('extra', 'tare_user', 'gross_user').all() serializer_class = VehicleSerializer permission_classes = [IsAuthenticated] filterset_fields = ['vehicle_number'] search_fields = ['vehicle_number'] ordering = ['vehicle_number'] def perform_create(self, serializer): instance = serializer.save() data = VehicleSerializer(instance).data sse_broadcast_update('vehicle', 'created', data) def perform_update(self, serializer): instance = serializer.save() data = VehicleSerializer(instance).data sse_broadcast_update('vehicle', 'updated', data) def perform_destroy(self, instance): data = VehicleSerializer(instance).data instance.delete() sse_broadcast_update('vehicle', 'deleted', data) @action(detail=False, methods=['get'], url_path='by-number') def by_number(self, request): """Get vehicle by vehicle_number query parameter""" vehicle_number = request.query_params.get('vehicle_number', None) if not vehicle_number: return Response( {'detail': 'vehicle_number parameter is required'}, status=status.HTTP_400_BAD_REQUEST ) try: vehicle = Vehicle.objects.get(vehicle_number=vehicle_number) serializer = self.get_serializer(vehicle) return Response(serializer.data) except Vehicle.DoesNotExist: return Response( {'detail': 'Vehicle not found'}, status=status.HTTP_404_NOT_FOUND ) @action(detail=True, methods=['post'], url_path='set-tare') def set_tare(self, request, pk=None): """Set tare weight from scale reading""" vehicle = self.get_object() value = request.data.get('value') if value is None: return Response({'detail': 'value is required'}, status=status.HTTP_400_BAD_REQUEST) try: value = int(value) except (ValueError, TypeError): return Response({'detail': 'value must be a number'}, status=status.HTTP_400_BAD_REQUEST) vehicle.tare = value vehicle.tare_date = timezone.now() vehicle.tare_user = request.user vehicle.save() serializer = self.get_serializer(vehicle) sse_broadcast_update('vehicle', 'updated', serializer.data) return Response(serializer.data) @action(detail=True, methods=['post'], url_path='set-gross') def set_gross(self, request, pk=None): """Set gross weight from scale reading""" vehicle = self.get_object() value = request.data.get('value') if value is None: return Response({'detail': 'value is required'}, status=status.HTTP_400_BAD_REQUEST) try: value = int(value) except (ValueError, TypeError): return Response({'detail': 'value must be a number'}, status=status.HTTP_400_BAD_REQUEST) vehicle.gross = value vehicle.gross_date = timezone.now() vehicle.gross_user = request.user vehicle.save() serializer = self.get_serializer(vehicle) sse_broadcast_update('vehicle', 'updated', serializer.data) return Response(serializer.data) class NomenclatureViewSet(viewsets.ModelViewSet): """ API endpoint for nomenclature management. list: Get all nomenclatures with pagination create: Create a new nomenclature with fields retrieve: Get a specific nomenclature by ID update: Full update of nomenclature and fields partial_update: Partial update of nomenclature destroy: Delete a nomenclature (cascades to fields and entries) by_code: Get nomenclature by code by_applies_to: Filter nomenclatures by applies_to type entries: List or create entries for a specific nomenclature """ queryset = Nomenclature.objects.prefetch_related('fields').all() serializer_class = NomenclatureSerializer permission_classes = [IsAuthenticated] filterset_fields = ['applies_to', 'code', 'kind'] search_fields = ['code', 'name'] ordering = ['sort_order', 'code'] def perform_create(self, serializer): instance = serializer.save() sse_broadcast_update('nomenclature', 'insert', NomenclatureSerializer(instance).data) def perform_update(self, serializer): instance = serializer.save() sse_broadcast_update('nomenclature', 'update', NomenclatureSerializer(instance).data) def perform_destroy(self, instance): data = NomenclatureSerializer(instance).data instance.delete() sse_broadcast_update('nomenclature', 'delete', data) @action(detail=False, methods=['get'], url_path='by-code') def by_code(self, request): """Get nomenclature by code query parameter""" code = request.query_params.get('code', None) if not code: return Response( {'detail': 'code parameter is required'}, status=status.HTTP_400_BAD_REQUEST ) try: nomenclature = Nomenclature.objects.get(code=code) serializer = self.get_serializer(nomenclature) return Response(serializer.data) except Nomenclature.DoesNotExist: return Response( {'detail': 'Nomenclature not found'}, status=status.HTTP_404_NOT_FOUND ) @action(detail=False, methods=['get'], url_path='by-applies-to') def by_applies_to(self, request): """Get nomenclatures filtered by applies_to query parameter""" applies_to = request.query_params.get('applies_to', None) if not applies_to: return Response( {'detail': 'applies_to parameter is required'}, status=status.HTTP_400_BAD_REQUEST ) if applies_to not in ['vehicle', 'container']: return Response( {'detail': 'applies_to must be either vehicle or container'}, status=status.HTTP_400_BAD_REQUEST ) nomenclatures = Nomenclature.objects.filter(applies_to=applies_to) page = self.paginate_queryset(nomenclatures) if page is not None: serializer = self.get_serializer(page, many=True) return self.get_paginated_response(serializer.data) serializer = self.get_serializer(nomenclatures, many=True) return Response(serializer.data) @action(detail=True, methods=['get', 'post'], url_path='entries') def entries(self, request, pk=None): """List or create entries for a specific nomenclature.""" nomenclature = self.get_object() if request.method == 'GET': entries = nomenclature.entries.all() is_active = request.query_params.get('is_active') if is_active is not None: entries = entries.filter(is_active=is_active.lower() == 'true') serializer = NomenclatureEntrySerializer(entries, many=True) return Response(serializer.data) serializer = NomenclatureEntrySerializer( data=request.data, context={'nomenclature': nomenclature}, ) serializer.is_valid(raise_exception=True) serializer.save(nomenclature=nomenclature) entry_data = {**serializer.data, 'nomenclature_code': nomenclature.code} sse_broadcast_update('nomenclature_entry', 'insert', entry_data) return Response(serializer.data, status=status.HTTP_201_CREATED) class NomenclatureEntryViewSet(viewsets.ModelViewSet): """ API endpoint for individual nomenclature entry management. For list/create use the nested endpoint: /api/nomenclatures/{id}/entries/ This viewset handles retrieve/update/delete of individual entries. """ queryset = NomenclatureEntry.objects.select_related('nomenclature').all() serializer_class = NomenclatureEntrySerializer permission_classes = [IsAuthenticated] def get_serializer_context(self): context = super().get_serializer_context() if self.kwargs.get('pk'): try: entry = NomenclatureEntry.objects.select_related('nomenclature').get(pk=self.kwargs['pk']) context['nomenclature'] = entry.nomenclature except NomenclatureEntry.DoesNotExist: pass return context def perform_update(self, serializer): instance = serializer.save() entry_data = {**NomenclatureEntrySerializer(instance).data, 'nomenclature_code': instance.nomenclature.code} sse_broadcast_update('nomenclature_entry', 'update', entry_data) def perform_destroy(self, instance): entry_data = {**NomenclatureEntrySerializer(instance).data, 'nomenclature_code': instance.nomenclature.code} instance.delete() sse_broadcast_update('nomenclature_entry', 'delete', entry_data) class ReportViewSet(viewsets.ModelViewSet): """ API endpoint for report template management. list: Get all reports create: Create a new report retrieve: Get a specific report by ID update: Update a report destroy: Delete a report """ queryset = Report.objects.select_related('created_by').all() serializer_class = ReportSerializer permission_classes = [IsAuthenticated] filterset_fields = ['name'] search_fields = ['name'] ordering = ['-updated_at'] def perform_create(self, serializer): serializer.save(created_by=self.request.user) def perform_update(self, serializer): serializer.save()