added vehicles and vehicles extra, dynamic tables for entities, dynamic dropdowns with overlay for selecting and manage data
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
# Generated by Django 4.2.8 on 2026-02-05 23:24
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("api", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Report",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=255)),
|
||||
("page_width", models.IntegerField(default=80)),
|
||||
("page_height", models.IntegerField(default=66)),
|
||||
(
|
||||
"api_endpoint",
|
||||
models.CharField(blank=True, default="", max_length=500),
|
||||
),
|
||||
("elements", models.JSONField(default=list)),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"created_by",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="reports",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"ordering": ["-updated_at"],
|
||||
},
|
||||
),
|
||||
]
|
||||
+23
-2
@@ -26,13 +26,34 @@ class ComPortReading(models.Model):
|
||||
data = models.TextField()
|
||||
timestamp = models.DateTimeField(auto_now_add=True)
|
||||
source_ip = models.GenericIPAddressField(null=True, blank=True)
|
||||
|
||||
|
||||
class Meta:
|
||||
ordering = ['-timestamp']
|
||||
indexes = [
|
||||
models.Index(fields=['-timestamp']),
|
||||
models.Index(fields=['port', '-timestamp']),
|
||||
]
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.port} - {self.timestamp}"
|
||||
|
||||
|
||||
class Report(models.Model):
|
||||
"""Model to store report templates for the report editor"""
|
||||
name = models.CharField(max_length=255)
|
||||
page_width = models.IntegerField(default=80)
|
||||
page_height = models.IntegerField(default=66)
|
||||
api_endpoint = models.CharField(max_length=500, blank=True, default='')
|
||||
elements = models.JSONField(default=list)
|
||||
created_by = models.ForeignKey(
|
||||
User, on_delete=models.SET_NULL, null=True, blank=True,
|
||||
related_name='reports'
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-updated_at']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
+160
-21
@@ -1,8 +1,8 @@
|
||||
from rest_framework import serializers
|
||||
from django.contrib.auth.password_validation import validate_password
|
||||
from .models import ComPortReading, User
|
||||
from .models import ComPortReading, User, Report
|
||||
from vehicles.models import Vehicle, VehicleExtra
|
||||
from nomenclatures.models import Nomenclature, NomenclatureField
|
||||
from nomenclatures.models import Nomenclature, NomenclatureField, NomenclatureEntry
|
||||
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
@@ -80,14 +80,58 @@ class VehicleExtraSerializer(serializers.ModelSerializer):
|
||||
fields = ['id', 'data']
|
||||
read_only_fields = ['id']
|
||||
|
||||
def validate_data(self, value):
|
||||
if not value:
|
||||
return value
|
||||
|
||||
nomenclatures = {
|
||||
n.code: n for n in
|
||||
Nomenclature.objects.filter(applies_to='vehicle').prefetch_related('fields', 'entries')
|
||||
}
|
||||
|
||||
errors = {}
|
||||
for key, val in value.items():
|
||||
if key not in nomenclatures:
|
||||
errors[key] = f"Unknown nomenclature code '{key}'."
|
||||
continue
|
||||
|
||||
nom = nomenclatures[key]
|
||||
|
||||
if nom.kind == Nomenclature.LOOKUP:
|
||||
if not isinstance(val, int):
|
||||
errors[key] = "Must be a nomenclature entry ID (integer)."
|
||||
elif not nom.entries.filter(id=val, is_active=True).exists():
|
||||
errors[key] = f"Entry ID {val} not found in '{nom.code}'."
|
||||
|
||||
elif nom.kind == Nomenclature.FIELD:
|
||||
field_def = nom.fields.first()
|
||||
if field_def:
|
||||
if field_def.field_type == NomenclatureField.TEXT and not isinstance(val, str):
|
||||
errors[key] = "Must be a string."
|
||||
elif field_def.field_type == NomenclatureField.NUMBER and not isinstance(val, (int, float)):
|
||||
errors[key] = "Must be a number."
|
||||
elif field_def.field_type == NomenclatureField.BOOL and not isinstance(val, bool):
|
||||
errors[key] = "Must be a boolean."
|
||||
elif field_def.field_type == NomenclatureField.CHOICE and val not in (field_def.choices or []):
|
||||
errors[key] = f"Must be one of {field_def.choices}."
|
||||
|
||||
if errors:
|
||||
raise serializers.ValidationError(errors)
|
||||
|
||||
return value
|
||||
|
||||
|
||||
class VehicleSerializer(serializers.ModelSerializer):
|
||||
extra = VehicleExtraSerializer(required=False, allow_null=True)
|
||||
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)
|
||||
|
||||
class Meta:
|
||||
model = Vehicle
|
||||
fields = ['id', 'vehicle_number', 'extra']
|
||||
read_only_fields = ['id']
|
||||
fields = ['id', 'vehicle_number', 'tare', 'tare_date', 'tare_user', 'tare_user_name',
|
||||
'gross', 'gross_date', 'gross_user', 'gross_user_name', 'extra']
|
||||
read_only_fields = ['id', 'tare_date', 'tare_user', 'tare_user_name',
|
||||
'gross_date', 'gross_user', 'gross_user_name']
|
||||
|
||||
def create(self, validated_data):
|
||||
extra_data = validated_data.pop('extra', None)
|
||||
@@ -101,19 +145,17 @@ class VehicleSerializer(serializers.ModelSerializer):
|
||||
def update(self, instance, validated_data):
|
||||
extra_data = validated_data.pop('extra', None)
|
||||
|
||||
# Update Vehicle fields
|
||||
instance.vehicle_number = validated_data.get('vehicle_number', instance.vehicle_number)
|
||||
for attr, value in validated_data.items():
|
||||
setattr(instance, attr, value)
|
||||
instance.save()
|
||||
|
||||
# Handle VehicleExtra update/creation
|
||||
if extra_data is not None:
|
||||
if hasattr(instance, 'extra'):
|
||||
# Update existing VehicleExtra
|
||||
for attr, value in extra_data.items():
|
||||
setattr(instance.extra, attr, value)
|
||||
instance.extra.save()
|
||||
else:
|
||||
# Create new VehicleExtra
|
||||
VehicleExtra.objects.create(vehicle=instance, **extra_data)
|
||||
|
||||
return instance
|
||||
@@ -127,7 +169,7 @@ class VehicleSerializer(serializers.ModelSerializer):
|
||||
class NomenclatureFieldSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = NomenclatureField
|
||||
fields = ['id', 'key', 'field_type']
|
||||
fields = ['id', 'key', 'label', 'field_type', 'required', 'choices', 'sort_order']
|
||||
read_only_fields = ['id']
|
||||
|
||||
def validate_key(self, value):
|
||||
@@ -143,17 +185,80 @@ class NomenclatureFieldSerializer(serializers.ModelSerializer):
|
||||
)
|
||||
return value
|
||||
|
||||
def validate(self, data):
|
||||
if data.get('field_type') == NomenclatureField.CHOICE:
|
||||
choices = data.get('choices')
|
||||
if not choices or not isinstance(choices, list) or len(choices) == 0:
|
||||
raise serializers.ValidationError({
|
||||
'choices': 'Choices must be a non-empty list for choice fields.'
|
||||
})
|
||||
elif data.get('choices'):
|
||||
raise serializers.ValidationError({
|
||||
'choices': 'Only choice-type fields can have choices.'
|
||||
})
|
||||
return data
|
||||
|
||||
|
||||
class NomenclatureEntrySerializer(serializers.ModelSerializer):
|
||||
display_value = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = NomenclatureEntry
|
||||
fields = ['id', 'data', 'is_active', 'display_value']
|
||||
read_only_fields = ['id']
|
||||
|
||||
def get_display_value(self, obj):
|
||||
display_key = obj.nomenclature.display_field or "name"
|
||||
return obj.data.get(display_key, f"Entry #{obj.pk}")
|
||||
|
||||
def validate_data(self, value):
|
||||
nomenclature = self.context.get('nomenclature')
|
||||
if not nomenclature:
|
||||
return value
|
||||
|
||||
field_defs = {f.key: f for f in nomenclature.fields.all()}
|
||||
|
||||
for key in value.keys():
|
||||
if key not in field_defs:
|
||||
raise serializers.ValidationError(
|
||||
f"Unknown field '{key}'. Valid fields: {list(field_defs.keys())}"
|
||||
)
|
||||
|
||||
for key, field_def in field_defs.items():
|
||||
if field_def.required and key not in value:
|
||||
raise serializers.ValidationError(
|
||||
f"Required field '{key}' is missing."
|
||||
)
|
||||
|
||||
for key, val in value.items():
|
||||
if key not in field_defs:
|
||||
continue
|
||||
field_def = field_defs[key]
|
||||
|
||||
if field_def.field_type == NomenclatureField.TEXT and not isinstance(val, str):
|
||||
raise serializers.ValidationError(f"Field '{key}' must be a string.")
|
||||
elif field_def.field_type == NomenclatureField.NUMBER and not isinstance(val, (int, float)):
|
||||
raise serializers.ValidationError(f"Field '{key}' must be a number.")
|
||||
elif field_def.field_type == NomenclatureField.BOOL and not isinstance(val, bool):
|
||||
raise serializers.ValidationError(f"Field '{key}' must be a boolean.")
|
||||
elif field_def.field_type == NomenclatureField.CHOICE and val not in (field_def.choices or []):
|
||||
raise serializers.ValidationError(f"Field '{key}' must be one of {field_def.choices}.")
|
||||
|
||||
return value
|
||||
|
||||
|
||||
class NomenclatureSerializer(serializers.ModelSerializer):
|
||||
fields = NomenclatureFieldSerializer(many=True, required=False, source='nomenclaturefield_set')
|
||||
fields = NomenclatureFieldSerializer(many=True, required=False)
|
||||
entry_count = serializers.IntegerField(source='entries.count', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Nomenclature
|
||||
fields = ['id', 'code', 'name', 'applies_to', 'fields']
|
||||
fields = ['id', 'code', 'name', 'applies_to', 'kind', 'display_field',
|
||||
'sort_order', 'fields', 'entry_count']
|
||||
read_only_fields = ['id']
|
||||
|
||||
def create(self, validated_data):
|
||||
fields_data = validated_data.pop('nomenclaturefield_set', [])
|
||||
fields_data = validated_data.pop('fields', [])
|
||||
nomenclature = Nomenclature.objects.create(**validated_data)
|
||||
|
||||
for field_data in fields_data:
|
||||
@@ -162,19 +267,14 @@ class NomenclatureSerializer(serializers.ModelSerializer):
|
||||
return nomenclature
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
fields_data = validated_data.pop('nomenclaturefield_set', None)
|
||||
fields_data = validated_data.pop('fields', None)
|
||||
|
||||
# Update Nomenclature fields
|
||||
instance.code = validated_data.get('code', instance.code)
|
||||
instance.name = validated_data.get('name', instance.name)
|
||||
instance.applies_to = validated_data.get('applies_to', instance.applies_to)
|
||||
for attr, value in validated_data.items():
|
||||
setattr(instance, attr, value)
|
||||
instance.save()
|
||||
|
||||
# Handle fields update - replace all fields
|
||||
if fields_data is not None:
|
||||
# Delete existing fields
|
||||
instance.nomenclaturefield_set.all().delete()
|
||||
# Create new fields
|
||||
instance.fields.all().delete()
|
||||
for field_data in fields_data:
|
||||
NomenclatureField.objects.create(nomenclature=instance, **field_data)
|
||||
|
||||
@@ -192,3 +292,42 @@ class NomenclatureSerializer(serializers.ModelSerializer):
|
||||
f"Invalid applies_to value. Must be one of: {', '.join(valid_choices)}"
|
||||
)
|
||||
return value
|
||||
|
||||
def validate(self, data):
|
||||
kind = data.get('kind', getattr(self.instance, 'kind', Nomenclature.LOOKUP))
|
||||
fields_data = data.get('fields', [])
|
||||
|
||||
if kind == Nomenclature.FIELD and fields_data and len(fields_data) != 1:
|
||||
raise serializers.ValidationError(
|
||||
"A 'field' nomenclature must have exactly one field definition."
|
||||
)
|
||||
|
||||
if kind == Nomenclature.LOOKUP:
|
||||
display_field = data.get('display_field', getattr(self.instance, 'display_field', 'name'))
|
||||
field_keys = [f['key'] for f in fields_data] if fields_data else []
|
||||
if field_keys and display_field not in field_keys:
|
||||
raise serializers.ValidationError(
|
||||
f"display_field '{display_field}' must match one of the defined field keys."
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class ReportSerializer(serializers.ModelSerializer):
|
||||
created_by_name = serializers.CharField(source='created_by.username', read_only=True, default=None)
|
||||
|
||||
class Meta:
|
||||
model = Report
|
||||
fields = ['id', 'name', 'page_width', 'page_height', 'api_endpoint',
|
||||
'elements', 'created_by', 'created_by_name', 'created_at', 'updated_at']
|
||||
read_only_fields = ['id', 'created_by', 'created_by_name', 'created_at', 'updated_at']
|
||||
|
||||
def validate_name(self, value):
|
||||
if not value or not value.strip():
|
||||
raise serializers.ValidationError("Report name cannot be empty")
|
||||
return value.strip()
|
||||
|
||||
def validate_elements(self, value):
|
||||
if not isinstance(value, list):
|
||||
raise serializers.ValidationError("Elements must be a list")
|
||||
return value
|
||||
|
||||
@@ -11,6 +11,8 @@ router.register(r'users', views.UserViewSet, basename='user')
|
||||
router.register(r'readings', views.ComPortReadingViewSet, basename='reading')
|
||||
router.register(r'vehicles', views.VehicleViewSet, basename='vehicle')
|
||||
router.register(r'nomenclatures', views.NomenclatureViewSet, basename='nomenclature')
|
||||
router.register(r'nomenclature-entries', views.NomenclatureEntryViewSet, basename='nomenclature-entry')
|
||||
router.register(r'reports', views.ReportViewSet, basename='report')
|
||||
|
||||
urlpatterns = [
|
||||
# JWT token endpoints
|
||||
|
||||
+168
-11
@@ -2,10 +2,16 @@ 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
|
||||
from .serializers import ComPortReadingSerializer, UserSerializer, UserDetailSerializer, ChangePasswordSerializer, VehicleSerializer, NomenclatureSerializer
|
||||
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
|
||||
from nomenclatures.models import Nomenclature, NomenclatureEntry
|
||||
from scalesapp.sse import sse_broadcast_update
|
||||
|
||||
|
||||
class UserViewSet(viewsets.ModelViewSet):
|
||||
@@ -77,7 +83,7 @@ class ComPortReadingViewSet(viewsets.ModelViewSet):
|
||||
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"""
|
||||
@@ -90,7 +96,7 @@ class ComPortReadingViewSet(viewsets.ModelViewSet):
|
||||
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')
|
||||
@@ -99,7 +105,7 @@ class ComPortReadingViewSet(viewsets.ModelViewSet):
|
||||
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))
|
||||
@@ -116,14 +122,31 @@ class VehicleViewSet(viewsets.ModelViewSet):
|
||||
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').all()
|
||||
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"""
|
||||
@@ -144,6 +167,48 @@ class VehicleViewSet(viewsets.ModelViewSet):
|
||||
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):
|
||||
"""
|
||||
@@ -154,16 +219,30 @@ class NomenclatureViewSet(viewsets.ModelViewSet):
|
||||
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)
|
||||
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('nomenclaturefield_set').all()
|
||||
queryset = Nomenclature.objects.prefetch_related('fields').all()
|
||||
serializer_class = NomenclatureSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
filterset_fields = ['applies_to', 'code']
|
||||
filterset_fields = ['applies_to', 'code', 'kind']
|
||||
search_fields = ['code', 'name']
|
||||
ordering = ['code']
|
||||
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):
|
||||
@@ -209,3 +288,81 @@ class NomenclatureViewSet(viewsets.ModelViewSet):
|
||||
|
||||
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()
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
from django.contrib import admin
|
||||
from .models import Nomenclature, NomenclatureField, NomenclatureEntry
|
||||
|
||||
# Register your models here.
|
||||
|
||||
class NomenclatureFieldInline(admin.TabularInline):
|
||||
model = NomenclatureField
|
||||
extra = 1
|
||||
|
||||
|
||||
@admin.register(Nomenclature)
|
||||
class NomenclatureAdmin(admin.ModelAdmin):
|
||||
list_display = ['code', 'name', 'applies_to', 'kind', 'sort_order']
|
||||
list_filter = ['applies_to', 'kind']
|
||||
search_fields = ['code', 'name']
|
||||
inlines = [NomenclatureFieldInline]
|
||||
|
||||
|
||||
@admin.register(NomenclatureEntry)
|
||||
class NomenclatureEntryAdmin(admin.ModelAdmin):
|
||||
list_display = ['id', '__str__', 'nomenclature', 'is_active']
|
||||
list_filter = ['nomenclature', 'is_active']
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
# Generated by Django 4.2.8 on 2026-02-05 15:29
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("nomenclatures", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="nomenclature",
|
||||
options={"ordering": ["sort_order", "code"]},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="nomenclaturefield",
|
||||
options={"ordering": ["sort_order", "key"]},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="nomenclature",
|
||||
name="display_field",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
default="name",
|
||||
help_text="Key of the NomenclatureField to use as dropdown label (for lookup kind)",
|
||||
max_length=50,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="nomenclature",
|
||||
name="kind",
|
||||
field=models.CharField(
|
||||
choices=[("lookup", "Lookup Table"), ("field", "Custom Field")],
|
||||
default="lookup",
|
||||
max_length=10,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="nomenclature",
|
||||
name="sort_order",
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="nomenclaturefield",
|
||||
name="choices",
|
||||
field=models.JSONField(
|
||||
blank=True,
|
||||
help_text='List of allowed values for choice type, e.g. ["Red","Green","Blue"]',
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="nomenclaturefield",
|
||||
name="label",
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="nomenclaturefield",
|
||||
name="required",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="nomenclaturefield",
|
||||
name="sort_order",
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="nomenclaturefield",
|
||||
name="nomenclature",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="fields",
|
||||
to="nomenclatures.nomenclature",
|
||||
),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name="nomenclaturefield",
|
||||
unique_together={("nomenclature", "key")},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="NomenclatureEntry",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("data", models.JSONField(default=dict)),
|
||||
("is_active", models.BooleanField(default=True)),
|
||||
(
|
||||
"nomenclature",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="entries",
|
||||
to="nomenclatures.nomenclature",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name_plural": "Nomenclature entries",
|
||||
"ordering": ["id"],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,13 +1,39 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
|
||||
class Nomenclature(models.Model):
|
||||
VEHICLE = "vehicle"
|
||||
CONTAINER = "container"
|
||||
APPLIES_TO_CHOICES = [
|
||||
(VEHICLE, "Vehicle"),
|
||||
(CONTAINER, "Container"),
|
||||
]
|
||||
|
||||
LOOKUP = "lookup"
|
||||
FIELD = "field"
|
||||
KIND_CHOICES = [
|
||||
(LOOKUP, "Lookup Table"),
|
||||
(FIELD, "Custom Field"),
|
||||
]
|
||||
|
||||
code = models.CharField(max_length=50, unique=True)
|
||||
name = models.CharField(max_length=255)
|
||||
applies_to = models.CharField(
|
||||
applies_to = models.CharField(max_length=50, choices=APPLIES_TO_CHOICES)
|
||||
kind = models.CharField(max_length=10, choices=KIND_CHOICES, default=LOOKUP)
|
||||
display_field = models.CharField(
|
||||
max_length=50,
|
||||
choices=[("vehicle", "Vehicle"), ("container", "Container")]
|
||||
default="name",
|
||||
blank=True,
|
||||
help_text="Key of the NomenclatureField to use as dropdown label (for lookup kind)",
|
||||
)
|
||||
sort_order = models.IntegerField(default=0)
|
||||
|
||||
class Meta:
|
||||
ordering = ["sort_order", "code"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.code})"
|
||||
|
||||
|
||||
class NomenclatureField(models.Model):
|
||||
TEXT = "text"
|
||||
@@ -23,7 +49,42 @@ class NomenclatureField(models.Model):
|
||||
]
|
||||
|
||||
nomenclature = models.ForeignKey(
|
||||
Nomenclature, on_delete=models.CASCADE
|
||||
Nomenclature,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="fields",
|
||||
)
|
||||
key = models.CharField(max_length=50)
|
||||
field_type = models.CharField(max_length=20, choices=FIELD_TYPES)
|
||||
label = models.CharField(max_length=100, blank=True)
|
||||
field_type = models.CharField(max_length=20, choices=FIELD_TYPES)
|
||||
required = models.BooleanField(default=False)
|
||||
choices = models.JSONField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text='List of allowed values for choice type, e.g. ["Red","Green","Blue"]',
|
||||
)
|
||||
sort_order = models.IntegerField(default=0)
|
||||
|
||||
class Meta:
|
||||
unique_together = [("nomenclature", "key")]
|
||||
ordering = ["sort_order", "key"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.nomenclature.code}.{self.key} ({self.field_type})"
|
||||
|
||||
|
||||
class NomenclatureEntry(models.Model):
|
||||
nomenclature = models.ForeignKey(
|
||||
Nomenclature,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="entries",
|
||||
)
|
||||
data = models.JSONField(default=dict)
|
||||
is_active = models.BooleanField(default=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = "Nomenclature entries"
|
||||
ordering = ["id"]
|
||||
|
||||
def __str__(self):
|
||||
display_key = self.nomenclature.display_field or "name"
|
||||
return str(self.data.get(display_key, f"Entry #{self.pk}"))
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
# Generated by Django 4.2.8 on 2026-01-29 13:08
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
("vehicles", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="vehicle",
|
||||
name="gross",
|
||||
field=models.IntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="vehicle",
|
||||
name="gross_date",
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="vehicle",
|
||||
name="gross_user",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="user_vehicle_gross",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="vehicle",
|
||||
name="tare",
|
||||
field=models.IntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="vehicle",
|
||||
name="tare_date",
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="vehicle",
|
||||
name="tare_user",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="user_vehicle_tare",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
]
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
# Generated by Django 4.2.8 on 2026-02-10 15:19
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
(
|
||||
"vehicles",
|
||||
"0002_vehicle_gross_vehicle_gross_date_vehicle_gross_user_and_more",
|
||||
),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="vehicle",
|
||||
name="driver_pid",
|
||||
field=models.CharField(blank=True, max_length=50, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="vehicle",
|
||||
name="trailer1_number",
|
||||
field=models.CharField(blank=True, max_length=15, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="vehicle",
|
||||
name="trailer2_number",
|
||||
field=models.CharField(blank=True, max_length=15, null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,8 +1,23 @@
|
||||
from typing import Required
|
||||
from django.db import models
|
||||
|
||||
from api.models import User
|
||||
|
||||
# Create your models here.
|
||||
class Vehicle(models.Model):
|
||||
vehicle_number = models.CharField(max_length=15, unique=True)
|
||||
trailer1_number = models.CharField(max_length=15, null=True, blank=True)
|
||||
trailer2_number = models.CharField(max_length=15, null=True, blank=True)
|
||||
driver_pid = models.CharField(max_length=50, null=True, blank=True)
|
||||
|
||||
tare = models.IntegerField(null=True, blank=True)
|
||||
tare_date = models.DateTimeField(null=True, blank=True)
|
||||
tare_user = models.ForeignKey(User, null=True, blank=True, related_name='user_vehicle_tare', on_delete=models.SET_NULL)
|
||||
gross = models.IntegerField(null=True, blank=True)
|
||||
gross_date = models.DateTimeField(null=True, blank=True)
|
||||
gross_user = models.ForeignKey(User, null=True, blank=True, related_name='user_vehicle_gross', on_delete=models.SET_NULL)
|
||||
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.vehicle_number}"
|
||||
|
||||
Reference in New Issue
Block a user