added print, download as pdf. no puppeteer, we'll see
This commit is contained in:
@@ -159,22 +159,25 @@ class VehicleSerializer(serializers.ModelSerializer):
|
||||
tare_user_name = serializers.CharField(source='tare_user.username', read_only=True, default=None)
|
||||
gross_user_name = serializers.CharField(source='gross_user.username', read_only=True, default=None)
|
||||
net_user_name = serializers.CharField(source='net_user.username', read_only=True, default=None)
|
||||
void_user_name = serializers.CharField(source='void_user.username', read_only=True, default=None)
|
||||
|
||||
class Meta:
|
||||
model = Vehicle
|
||||
fields = ['id', 'vehicle_number', 'trailer1_number', 'trailer2_number', 'driver_pid',
|
||||
fields = ['id', 'vehicle_number', 'trailer1_number', 'trailer2_number', 'driver_pid', 'driver_name',
|
||||
'tare', 'tare_date', 'tare_user', 'tare_user_name',
|
||||
'tare_manual', 'tare_manual_user', 'tare_manual_date', 'tare_manual_note',
|
||||
'gross', 'gross_date', 'gross_user', 'gross_user_name',
|
||||
'gross_manual', 'gross_manual_user', 'gross_manual_date', 'gross_manual_note',
|
||||
'net', 'net_date', 'net_user', 'net_user_name',
|
||||
'doc_number', 'extra']
|
||||
'doc_number', 'extra',
|
||||
'is_void', 'void_date', 'void_user', 'void_user_name', 'void_reason']
|
||||
read_only_fields = ['id', 'tare_date', 'tare_user', 'tare_user_name',
|
||||
'tare_manual', 'tare_manual_user', 'tare_manual_date', 'tare_manual_note',
|
||||
'gross_date', 'gross_user', 'gross_user_name',
|
||||
'gross_manual', 'gross_manual_user', 'gross_manual_date', 'gross_manual_note',
|
||||
'net', 'net_date', 'net_user', 'net_user_name',
|
||||
'doc_number']
|
||||
'doc_number',
|
||||
'is_void', 'void_date', 'void_user', 'void_user_name', 'void_reason']
|
||||
|
||||
def create(self, validated_data):
|
||||
extra_data = validated_data.pop('extra', None)
|
||||
@@ -208,6 +211,11 @@ class VehicleSerializer(serializers.ModelSerializer):
|
||||
raise serializers.ValidationError("Vehicle number cannot be empty")
|
||||
return value.strip()
|
||||
|
||||
def validate(self, attrs):
|
||||
if self.instance is None and not attrs.get('driver_name'):
|
||||
raise serializers.ValidationError({'driver_name': 'This field is required.'})
|
||||
return attrs
|
||||
|
||||
|
||||
class ExtraCombinationSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
|
||||
+65
-6
@@ -228,14 +228,19 @@ class VehicleViewSet(viewsets.ModelViewSet):
|
||||
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)
|
||||
destroy: Disabled - vehicles are never deleted, see void() instead
|
||||
by_number: Get vehicle by vehicle_number
|
||||
measure: Record a weighing (app assigns tare vs gross by weight)
|
||||
void: Flag a completed vehicle as voided (corrective reversal, not a delete)
|
||||
mark_notice_printed: Signal that a completed vehicle's notice was printed/downloaded
|
||||
"""
|
||||
queryset = Vehicle.objects.select_related('extra', 'tare_user', 'gross_user').all()
|
||||
serializer_class = VehicleSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
filterset_fields = ['vehicle_number']
|
||||
filterset_fields = {
|
||||
'vehicle_number': ['exact'],
|
||||
'doc_number': ['isnull'],
|
||||
}
|
||||
search_fields = ['vehicle_number']
|
||||
ordering = ['vehicle_number']
|
||||
|
||||
@@ -251,10 +256,17 @@ class VehicleViewSet(viewsets.ModelViewSet):
|
||||
sse_broadcast_update('vehicle', 'updated', data)
|
||||
_record_extra_combination(instance.extra.data if hasattr(instance, 'extra') else None)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
data = VehicleSerializer(instance).data
|
||||
instance.delete()
|
||||
sse_broadcast_update('vehicle', 'deleted', data)
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
# Vehicles are never deleted - a completed weighing is corrected with
|
||||
# /void/ instead, which keeps the record (and its doc_number) intact
|
||||
# and visible in History. Overriding destroy(), not perform_destroy():
|
||||
# ModelViewSet.destroy() calls perform_destroy() then always returns
|
||||
# 204, so neutering only perform_destroy would silently no-op instead
|
||||
# of telling the caller this action doesn't exist.
|
||||
return Response(
|
||||
{'detail': 'Vehicles cannot be deleted. Use /void/ on a completed vehicle instead.'},
|
||||
status=status.HTTP_405_METHOD_NOT_ALLOWED,
|
||||
)
|
||||
|
||||
@action(detail=False, methods=['get'], url_path='by-number')
|
||||
def by_number(self, request):
|
||||
@@ -321,6 +333,53 @@ class VehicleViewSet(viewsets.ModelViewSet):
|
||||
sse_broadcast_update('vehicle', 'updated', serializer.data)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=True, methods=['post'], url_path='void')
|
||||
def void(self, request, pk=None):
|
||||
"""Flag a completed vehicle as voided. Does not touch doc_number/net/tare/gross."""
|
||||
if not request.user.can_edit_documents:
|
||||
return Response({'detail': 'You do not have permission to void a document.'},
|
||||
status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
vehicle = self.get_object()
|
||||
if not vehicle.doc_number:
|
||||
return Response({'detail': 'Only a completed vehicle can be voided.'},
|
||||
status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
reason = (request.data.get('reason') or '').strip()
|
||||
if not reason:
|
||||
return Response({'detail': 'A reason is required to void a document.'},
|
||||
status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
vehicle.is_void = True
|
||||
vehicle.void_date = timezone.now()
|
||||
vehicle.void_user = request.user
|
||||
vehicle.void_reason = reason
|
||||
vehicle.save()
|
||||
|
||||
serializer = self.get_serializer(vehicle)
|
||||
sse_broadcast_update('vehicle', 'voided', serializer.data)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=True, methods=['post'], url_path='mark-notice-printed')
|
||||
def mark_notice_printed(self, request, pk=None):
|
||||
"""Signal that the operator has acted on a completed vehicle's notice (print/download).
|
||||
|
||||
Makes no DB change - doc_number already marks the vehicle as finalized.
|
||||
This only broadcasts the SSE event that removes it from the active list.
|
||||
"""
|
||||
if not request.user.can_measure:
|
||||
return Response({'detail': 'You do not have permission to do this.'},
|
||||
status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
vehicle = self.get_object()
|
||||
if not vehicle.doc_number:
|
||||
return Response({'detail': 'Only a completed vehicle can be archived.'},
|
||||
status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
serializer = self.get_serializer(vehicle)
|
||||
sse_broadcast_update('vehicle', 'archived', serializer.data)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class ExtraCombinationViewSet(mixins.ListModelMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
|
||||
Reference in New Issue
Block a user