Files
004.erp.generic/backend/modules/fleet/serializers.py
mariomitte 1ab2bfb3c3
Some checks failed
ERP CI/CD Pipeline / test (push) Has been cancelled
ERP CI/CD Pipeline / Deploy (server git pull + compose) (push) Has been cancelled
patch prikaži stranicu servisnih zapisa sa SN dizalice
2026-07-14 23:09:00 +02:00

321 lines
14 KiB
Python

from rest_framework import serializers
import re
from .models import (
Crane,
Vehicle,
VehicleNotification,
WorkOrder,
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):
class Meta:
model = WorkOrderInvoice
fields = [
'id',
'work_order',
'naziv_racuna',
'lokacija',
'datum',
'opis',
'image',
'created_by',
'created_at',
]
read_only_fields = ['id', 'created_by', 'created_at']
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)