You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

397 lines
15 KiB
Python

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, generate_doc_number
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 IsAdminUser(IsAuthenticated):
def has_permission(self, request, view):
return bool(request.user and request.user.is_authenticated and request.user.is_admin)
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: Soft-delete a user (sets is_active=False)
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']
def get_permissions(self):
if self.action in ('me', 'change_password'):
return [IsAuthenticated()]
return [IsAuthenticated(), IsAdminUser()]
def destroy(self, request, *args, **kwargs):
instance = self.get_object()
instance.is_active = False
instance.save()
return Response(status=status.HTTP_204_NO_CONTENT)
@action(detail=False, methods=['get'])
def me(self, request):
"""Get current authenticated user details"""
serializer = UserDetailSerializer(request.user)
return Response(serializer.data)
@action(detail=False, methods=['post'],
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))
def _maybe_finalize(vehicle, user):
"""If both tare and gross are set and net hasn't been calculated yet, compute net and assign doc_number."""
if vehicle.tare is not None and vehicle.gross is not None and vehicle.net is None:
vehicle.net = vehicle.gross - vehicle.tare
vehicle.net_date = timezone.now()
vehicle.net_user = user
vehicle.doc_number = generate_doc_number(timezone.now().year)
vehicle.save()
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()
_maybe_finalize(vehicle, request.user)
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()
_maybe_finalize(vehicle, request.user)
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()