added print, download as pdf. no puppeteer, we'll see

This commit is contained in:
2026-09-22 22:06:06 +03:00
parent 122480cd46
commit 6f614270e7
37 changed files with 3955 additions and 473 deletions
+11 -3
View File
@@ -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
View File
@@ -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,