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.
381 lines
14 KiB
Python
381 lines
14 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 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))
|
|
|
|
|
|
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()
|