from rest_framework import serializers from django.contrib.auth.password_validation import validate_password from .models import ComPortReading, User, Report from vehicles.models import Vehicle, VehicleExtra from nomenclatures.models import Nomenclature, NomenclatureField, NomenclatureEntry class UserSerializer(serializers.ModelSerializer): password = serializers.CharField(write_only=True, required=False) class Meta: model = User fields = ['id', 'username', 'email', 'first_name', 'last_name', 'role', 'is_admin', 'is_active', 'date_joined', 'password'] read_only_fields = ['id', 'date_joined'] extra_kwargs = { 'password': {'write_only': True} } def create(self, validated_data): password = validated_data.pop('password', None) user = User(**validated_data) if password: user.set_password(password) user.save() return user def update(self, instance, validated_data): password = validated_data.pop('password', None) for attr, value in validated_data.items(): setattr(instance, attr, value) if password: instance.set_password(password) instance.save() return instance class UserDetailSerializer(serializers.ModelSerializer): """Serializer for current user details (excludes password)""" class Meta: model = User fields = ['id', 'username', 'email', 'first_name', 'last_name', 'role', 'is_admin', 'is_active', 'date_joined'] read_only_fields = ['id', 'date_joined'] class ChangePasswordSerializer(serializers.Serializer): """Serializer for password change endpoint""" old_password = serializers.CharField(required=True, write_only=True) new_password = serializers.CharField(required=True, write_only=True) def validate_old_password(self, value): user = self.context['request'].user if not user.check_password(value): raise serializers.ValidationError("Old password is incorrect") return value def validate_new_password(self, value): # Use Django's password validators validate_password(value, self.context['request'].user) return value def save(self, **kwargs): user = self.context['request'].user user.set_password(self.validated_data['new_password']) user.save() return user class ComPortReadingSerializer(serializers.ModelSerializer): class Meta: model = ComPortReading fields = ['id', 'port', 'data', 'timestamp', 'source_ip'] read_only_fields = ['id', 'timestamp'] class VehicleExtraSerializer(serializers.ModelSerializer): class Meta: model = VehicleExtra 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', '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) vehicle = Vehicle.objects.create(**validated_data) if extra_data: VehicleExtra.objects.create(vehicle=vehicle, **extra_data) return vehicle def update(self, instance, validated_data): extra_data = validated_data.pop('extra', None) 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'): for attr, value in extra_data.items(): setattr(instance.extra, attr, value) instance.extra.save() else: VehicleExtra.objects.create(vehicle=instance, **extra_data) return instance def validate_vehicle_number(self, value): if not value or not value.strip(): raise serializers.ValidationError("Vehicle number cannot be empty") return value.strip() class NomenclatureFieldSerializer(serializers.ModelSerializer): class Meta: model = NomenclatureField fields = ['id', 'key', 'label', 'field_type', 'required', 'choices', 'sort_order'] read_only_fields = ['id'] def validate_key(self, value): if not value or not value.strip(): raise serializers.ValidationError("Field key cannot be empty") return value.strip() def validate_field_type(self, value): valid_types = [choice[0] for choice in NomenclatureField.FIELD_TYPES] if value not in valid_types: raise serializers.ValidationError( f"Invalid field type. Must be one of: {', '.join(valid_types)}" ) 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) entry_count = serializers.IntegerField(source='entries.count', read_only=True) class Meta: model = Nomenclature 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('fields', []) nomenclature = Nomenclature.objects.create(**validated_data) for field_data in fields_data: NomenclatureField.objects.create(nomenclature=nomenclature, **field_data) return nomenclature def update(self, instance, validated_data): fields_data = validated_data.pop('fields', None) for attr, value in validated_data.items(): setattr(instance, attr, value) instance.save() if fields_data is not None: instance.fields.all().delete() for field_data in fields_data: NomenclatureField.objects.create(nomenclature=instance, **field_data) return instance def validate_code(self, value): if not value or not value.strip(): raise serializers.ValidationError("Code cannot be empty") return value.strip() def validate_applies_to(self, value): valid_choices = ['vehicle', 'container'] if value not in valid_choices: raise serializers.ValidationError( 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