from rest_framework import serializers import re from django.urls import reverse from .models import ( Crane, Vehicle, VehicleNotification, WorkOrder, WorkOrderPhoto, WorkOrderInvoice, VehicleServiceRecord, VehicleServicePhoto, VehicleServiceAttachment, ) from . import services class VehicleSerializer(serializers.ModelSerializer): # Čitljivo ime klijenta — read-only, za prikaz u frontendu i adminu client_name = serializers.SerializerMethodField() class Meta: model = Vehicle fields = [ 'id', 'asset_type', 'registration_number', 'make', 'model', 'year', 'vin', 'crane_serial_number', 'superstructure_working_hours', 'chassis_working_hours', 'current_mileage', 'service_interval_km', 'is_company_vehicle', 'client', 'client_name', 'assigned_servicer', 'is_active', ] read_only_fields = ['id', 'current_mileage', 'client_name'] def get_client_name(self, obj): if obj.client_id: return obj.client.name return None def create(self, validated_data): validated_data.setdefault('asset_type', 'vehicle') return services.VehicleService.create_vehicle(validated_data) def update(self, instance, validated_data): validated_data.pop('asset_type', None) return services.VehicleService.update_vehicle(instance, validated_data) def validate_crane_serial_number(self, value): if value in (None, ''): return None normalized = str(value).strip() if not re.fullmatch(r"[0-9]{6}", normalized): raise serializers.ValidationError('Serijski broj dizalice mora sadržavati točno 6 znamenki.') return normalized def validate(self, attrs): asset_type = attrs.get('asset_type', getattr(self.instance, 'asset_type', None)) if asset_type is None and getattr(self.Meta, 'model', None) is Crane: asset_type = 'crane' serial = attrs.get('crane_serial_number', getattr(self.instance, 'crane_serial_number', None)) is_create = self.instance is None if asset_type == 'crane' and not serial and is_create: raise serializers.ValidationError({ 'crane_serial_number': 'Serijski broj dizalice je obavezan za dizalice.' }) return attrs class CraneSerializer(VehicleSerializer): display_name = serializers.SerializerMethodField() class Meta(VehicleSerializer.Meta): model = Crane fields = VehicleSerializer.Meta.fields + ['display_name'] read_only_fields = VehicleSerializer.Meta.read_only_fields + ['display_name'] def get_display_name(self, obj): details = " ".join(part for part in [obj.make, obj.model] if part) return f"{obj.registration_number} {details}".strip() def create(self, validated_data): validated_data['asset_type'] = 'crane' return services.VehicleService.create_vehicle(validated_data) def update(self, instance, validated_data): validated_data.pop('asset_type', None) return services.VehicleService.update_vehicle(instance, validated_data) class VehicleNotificationSerializer(serializers.ModelSerializer): class Meta: model = VehicleNotification fields = ['id', 'title', 'message', 'metadata', 'level', 'is_sent', 'is_read', 'created_at'] read_only_fields = ['id', 'title', 'message', 'metadata', 'level', 'is_sent', 'created_at'] # is_read is writable via the "mark_read" action, but normally read-only in list class WorkOrderSerializer(serializers.ModelSerializer): crane = serializers.SerializerMethodField() crane_label = serializers.SerializerMethodField() class Meta: model = WorkOrder fields = [ 'id', 'vehicle', 'crane', 'crane_label', 'creator', 'date', 'start_mileage', 'end_mileage', 'servicer_vehicle_registration', 'servicer_vehicle_make_model', 'servicer_vehicle_start_mileage', 'servicer_vehicle_end_mileage', 'servicer_vehicle_fuel_cost', 'location', 'travel_start_at', 'travel_end_at', 'purpose', 'notes', 'status' ] read_only_fields = ['id', 'date'] extra_kwargs = { 'vehicle': {'required': False}, 'creator': {'required': False}, 'start_mileage': {'required': False, 'allow_null': True}, 'end_mileage': {'required': False, 'allow_null': True}, 'servicer_vehicle_registration': {'required': False, 'allow_blank': True}, 'servicer_vehicle_make_model': {'required': False, 'allow_blank': True}, 'servicer_vehicle_start_mileage': {'required': False, 'allow_null': True}, 'servicer_vehicle_end_mileage': {'required': False, 'allow_null': True}, 'servicer_vehicle_fuel_cost': {'required': False, 'allow_null': True}, 'location': {'required': False, 'allow_blank': True}, 'travel_start_at': {'required': False, 'allow_null': True}, 'travel_end_at': {'required': False, 'allow_null': True}, } def get_crane(self, obj): return obj.vehicle_id def get_crane_label(self, obj): if not obj.vehicle_id: return None details = " ".join(part for part in [obj.vehicle.registration_number, obj.vehicle.make, obj.vehicle.model] if part) return details.strip() def create(self, validated_data): request = self.context.get('request') user = getattr(request, 'user', None) if not validated_data.get('vehicle'): raise serializers.ValidationError({ 'vehicle': 'Odaberite dizalicu za putni nalog iz servisnog konteksta.' }) if getattr(validated_data['vehicle'], 'asset_type', None) != 'crane': raise serializers.ValidationError({ 'vehicle': 'Za putni nalog morate odabrati dizalicu (asset_type=crane).' }) if user and 'creator' not in validated_data: validated_data['creator'] = user return services.VehicleService.create_work_order(validated_data) def update(self, instance, validated_data): return services.VehicleService.update_work_order(instance, validated_data) def validate(self, attrs): current = getattr(self, 'instance', None) travel_start_at = attrs.get('travel_start_at', getattr(current, 'travel_start_at', None)) travel_end_at = attrs.get('travel_end_at', getattr(current, 'travel_end_at', None)) location = attrs.get('location', getattr(current, 'location', None)) start_mileage = attrs.get('start_mileage', getattr(current, 'start_mileage', None)) end_mileage = attrs.get('end_mileage', getattr(current, 'end_mileage', None)) servicer_start_mileage = attrs.get('servicer_vehicle_start_mileage', getattr(current, 'servicer_vehicle_start_mileage', None)) servicer_end_mileage = attrs.get('servicer_vehicle_end_mileage', getattr(current, 'servicer_vehicle_end_mileage', None)) servicer_fuel_cost = attrs.get('servicer_vehicle_fuel_cost', getattr(current, 'servicer_vehicle_fuel_cost', None)) if (travel_start_at is None) ^ (travel_end_at is None): raise serializers.ValidationError({'travel_end_at': 'Unesite i početak i kraj puta, ili ostavite oba prazna.'}) if travel_start_at is not None and not str(location or '').strip(): raise serializers.ValidationError({'location': 'Lokacija je obavezna kada je definiran putni nalog.'}) if travel_start_at and travel_end_at and travel_end_at < travel_start_at: raise serializers.ValidationError({'travel_end_at': 'Kraj puta ne može biti prije početka puta.'}) if end_mileage is not None and start_mileage is None: raise serializers.ValidationError({'start_mileage': 'Početna kilometraža je obavezna ako je upisana završna.'}) if start_mileage is not None and end_mileage is not None and end_mileage < start_mileage: raise serializers.ValidationError({'end_mileage': 'Završna kilometraža mora biti veća ili jednaka početnoj.'}) if servicer_end_mileage is not None and servicer_start_mileage is None: raise serializers.ValidationError({'servicer_vehicle_start_mileage': 'Početna kilometraža vozila servisera je obavezna ako je upisana završna.'}) if servicer_start_mileage is not None and servicer_end_mileage is not None and servicer_end_mileage <= servicer_start_mileage: raise serializers.ValidationError({'servicer_vehicle_end_mileage': 'Završna kilometraža vozila servisera mora biti veća od početne.'}) if servicer_fuel_cost is not None and servicer_fuel_cost < 0: raise serializers.ValidationError({'servicer_vehicle_fuel_cost': 'Trošak goriva ne može biti negativan.'}) return attrs class WorkOrderInvoiceSerializer(serializers.ModelSerializer): image_url = serializers.SerializerMethodField() class Meta: model = WorkOrderInvoice fields = [ 'id', 'work_order', 'naziv_racuna', 'lokacija', 'datum', 'opis', 'image', 'image_url', 'created_by', 'created_at', ] read_only_fields = ['id', 'created_by', 'created_at'] def get_image_url(self, obj): request = self.context.get('request') if not obj.image: return None if request is None: return None return request.build_absolute_uri( reverse('work-order-invoice-image', kwargs={'pk': obj.pk}) ) class WorkOrderPhotoSerializer(serializers.ModelSerializer): class Meta: model = WorkOrderPhoto fields = ['id', 'work_order', 'image', 'description', 'uploaded_by', 'created_at'] read_only_fields = ['id', 'uploaded_by', 'created_at'] extra_kwargs = { 'work_order': {'required': False}, } class VehicleServiceRecordSerializer(serializers.ModelSerializer): crane = serializers.SerializerMethodField() crane_registration = serializers.SerializerMethodField() photos_count = serializers.SerializerMethodField() attachments_count = serializers.SerializerMethodField() files_count = serializers.SerializerMethodField() performed_by_name = serializers.SerializerMethodField() vehicle_registration = serializers.SerializerMethodField() task_label = serializers.SerializerMethodField() crane_serial_number = serializers.SerializerMethodField() class Meta: model = VehicleServiceRecord fields = [ 'id', 'vehicle', 'crane', 'vehicle_registration', 'crane_registration', 'task', 'task_label', 'crane_serial_number', 'performed_by', 'performed_by_name', 'service_title', 'description', 'parts', 'cost', 'service_date', 'mileage', 'next_service_due_at', 'photos_count', 'attachments_count', 'files_count', ] read_only_fields = [ 'id', 'service_date', 'next_service_due_at', 'photos_count', 'attachments_count', 'files_count', 'performed_by_name', 'vehicle_registration', 'crane', 'crane_registration', 'task_label', ] extra_kwargs = { 'performed_by': {'required': False}, 'vehicle': {'required': False}, 'mileage': {'required': False, 'allow_null': True}, } def get_photos_count(self, obj): # prefetch_related('photos') u viewsetu sprječava N+1 return obj.photos.count() def get_crane(self, obj): return obj.vehicle_id def get_attachments_count(self, obj): return obj.attachments.count() def get_files_count(self, obj): return self.get_photos_count(obj) + self.get_attachments_count(obj) def get_performed_by_name(self, obj): if obj.performed_by_id: return obj.performed_by.get_full_name() or obj.performed_by.username return None def get_vehicle_registration(self, obj): if obj.vehicle_id: return obj.vehicle.registration_number return None def get_crane_registration(self, obj): return self.get_vehicle_registration(obj) def get_task_label(self, obj): if not obj.task_id: return None return obj.task.title def get_crane_serial_number(self, obj): # Provjeri je li povezano vozilo/dizalica postoji i vrati serijski broj if hasattr(obj, 'vehicle') and obj.vehicle: return getattr(obj.vehicle, 'crane_serial_number', None) return None def create(self, validated_data): request = self.context.get('request') user = getattr(request, 'user', None) task = validated_data.get('task') if not task: raise serializers.ValidationError({ 'task': 'Odaberite radni zadatak prije kreiranja servisnog zapisa.' }) if not task.vehicle_id: raise serializers.ValidationError({ 'task': 'Odabrani radni zadatak nema dodijeljenu dizalicu.' }) validated_data['vehicle'] = task.vehicle if user and not getattr(user, 'is_staff', False): validated_data['performed_by'] = user elif user and 'performed_by' not in validated_data: validated_data['performed_by'] = user return services.VehicleService.create_service_record(validated_data) class VehicleServicePhotoSerializer(serializers.ModelSerializer): image = serializers.ImageField(required=True) class Meta: model = VehicleServicePhoto fields = ['id', 'service_record', 'image', 'description', 'uploaded_by', 'created_at'] read_only_fields = ['id', 'created_at', 'uploaded_by'] def create(self, validated_data): # Ako postoji request user, postavi uploaded_by request = self.context.get('request') if self.context else None user = getattr(request, 'user', None) if user and 'uploaded_by' not in validated_data: validated_data['uploaded_by'] = user # Kreiranje direktno kroz model je ok; može se delegirati u services ako želiš dodatnu logiku return VehicleServicePhoto.objects.create(**validated_data) class VehicleServiceAttachmentSerializer(serializers.ModelSerializer): file = serializers.FileField(required=True) class Meta: model = VehicleServiceAttachment fields = ['id', 'service_record', 'file', 'description', 'uploaded_by', 'created_at'] read_only_fields = ['id', 'created_at', 'uploaded_by'] def create(self, validated_data): request = self.context.get('request') if self.context else None user = getattr(request, 'user', None) if user and 'uploaded_by' not in validated_data: validated_data['uploaded_by'] = user return VehicleServiceAttachment.objects.create(**validated_data)