Widget Bilješke sada omogućuje odabir više članova tima (ne samo servisera), a backend validacija i testovi su usklađeni. Dodatno, nakon realtime service_note notifikacije sada se osvježava service-notes store, te create flow više ne briše lokalne bilješke kada odgovor vrati praznu listu. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
634 lines
27 KiB
Python
634 lines
27 KiB
Python
from rest_framework import serializers
|
|
import re
|
|
from pathlib import Path
|
|
from decimal import Decimal, InvalidOperation
|
|
from django.contrib.auth import get_user_model
|
|
from django.urls import reverse
|
|
from django.utils import timezone
|
|
from modules.task_management.models import Task
|
|
from .models import (
|
|
Crane,
|
|
Vehicle,
|
|
ServiceContextNote,
|
|
MonthlyServicerDayEntry,
|
|
VehicleNotification,
|
|
WorkOrder,
|
|
WorkOrderAdditionalCostsTable,
|
|
WorkOrderPhoto,
|
|
WorkOrderInvoice,
|
|
VehicleServiceRecord,
|
|
VehicleServicePhoto,
|
|
VehicleServiceAttachment,
|
|
)
|
|
from . import services
|
|
|
|
User = get_user_model()
|
|
|
|
|
|
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', '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 ServiceContextNoteSerializer(serializers.ModelSerializer):
|
|
created_by_name = serializers.SerializerMethodField()
|
|
recipient_name = serializers.SerializerMethodField()
|
|
work_order_label = serializers.SerializerMethodField()
|
|
task_title = serializers.SerializerMethodField()
|
|
|
|
class Meta:
|
|
model = ServiceContextNote
|
|
fields = [
|
|
'id',
|
|
'created_by',
|
|
'created_by_name',
|
|
'recipient',
|
|
'recipient_name',
|
|
'audience_source',
|
|
'work_order',
|
|
'work_order_label',
|
|
'task',
|
|
'task_title',
|
|
'note',
|
|
'note_date',
|
|
'is_closed',
|
|
'closed_at',
|
|
'created_at',
|
|
'updated_at',
|
|
]
|
|
read_only_fields = fields
|
|
|
|
def get_created_by_name(self, obj):
|
|
return obj.created_by.get_full_name() or obj.created_by.email
|
|
|
|
def get_recipient_name(self, obj):
|
|
return obj.recipient.get_full_name() or obj.recipient.email
|
|
|
|
def get_work_order_label(self, obj):
|
|
if not obj.work_order_id:
|
|
return None
|
|
display_code = str(getattr(obj.work_order, 'display_code', '') or '').strip().upper()
|
|
return display_code or f"WO-{obj.work_order_id}"
|
|
|
|
def get_task_title(self, obj):
|
|
if not obj.task_id:
|
|
return None
|
|
return obj.task.title
|
|
|
|
|
|
class ServiceContextNoteCreateSerializer(serializers.Serializer):
|
|
AUDIENCE_CHOICES = ['self', 'member', 'team']
|
|
|
|
note = serializers.CharField(required=True, allow_blank=False, trim_whitespace=True)
|
|
note_date = serializers.DateField(required=False, allow_null=True)
|
|
audience = serializers.ChoiceField(choices=AUDIENCE_CHOICES, default='self')
|
|
target_user = serializers.PrimaryKeyRelatedField(
|
|
queryset=User.objects.filter(is_active=True, is_team_member=True),
|
|
required=False,
|
|
allow_null=True,
|
|
)
|
|
target_users = serializers.ListField(
|
|
child=serializers.PrimaryKeyRelatedField(queryset=User.objects.filter(is_active=True, is_team_member=True)),
|
|
required=False,
|
|
allow_empty=False,
|
|
)
|
|
work_order = serializers.PrimaryKeyRelatedField(
|
|
queryset=WorkOrder.objects.filter(is_active=True),
|
|
required=False,
|
|
allow_null=True,
|
|
)
|
|
task = serializers.PrimaryKeyRelatedField(
|
|
queryset=Task.objects.filter(is_active=True),
|
|
required=False,
|
|
allow_null=True,
|
|
)
|
|
|
|
def validate(self, attrs):
|
|
request = self.context.get('request')
|
|
user = getattr(request, 'user', None)
|
|
if user is None:
|
|
raise serializers.ValidationError('Autentikacija je obavezna.')
|
|
|
|
audience = attrs.get('audience', 'self')
|
|
target_user = attrs.get('target_user')
|
|
target_users = attrs.get('target_users') or []
|
|
work_order = attrs.get('work_order')
|
|
task = attrs.get('task')
|
|
note = str(attrs.get('note', '') or '').strip()
|
|
can_manage_recipients = bool(
|
|
getattr(user, 'is_serviser', False)
|
|
or getattr(user, 'is_staff', False)
|
|
or (getattr(user, 'is_team_member', False) and not getattr(user, 'is_serviser', False))
|
|
)
|
|
|
|
if not note:
|
|
raise serializers.ValidationError({'note': 'Bilješka ne može biti prazna.'})
|
|
|
|
if not can_manage_recipients and audience != 'self':
|
|
raise serializers.ValidationError({'audience': 'Samo supervisor ili serviser može slati bilješke drugim članovima tima.'})
|
|
if audience == 'member' and not target_user and len(target_users) == 0:
|
|
raise serializers.ValidationError({'target_users': 'Odaberite barem jednog servisera.'})
|
|
if audience in {'self', 'team'} and target_user:
|
|
raise serializers.ValidationError({'target_user': 'Odabrani tip publike ne koristi ciljano polje člana tima.'})
|
|
if audience in {'self', 'team'} and len(target_users) > 0:
|
|
raise serializers.ValidationError({'target_users': 'Odabrani tip publike ne koristi listu članova tima.'})
|
|
if target_user and not getattr(target_user, 'is_team_member', False):
|
|
raise serializers.ValidationError({'target_user': 'Bilješku je moguće poslati samo članu tima.'})
|
|
for selected_user in target_users:
|
|
if not getattr(selected_user, 'is_team_member', False):
|
|
raise serializers.ValidationError({'target_users': 'Bilješku je moguće poslati samo članovima tima.'})
|
|
|
|
if work_order:
|
|
if not work_order.is_active or str(work_order.status or '').lower() != 'open':
|
|
raise serializers.ValidationError({'work_order': 'Moguće je povezati samo aktivni otvoreni putni nalog.'})
|
|
|
|
if task:
|
|
status = str(task.status or '').lower()
|
|
if status in {'zavrsen', 'neaktivan'}:
|
|
raise serializers.ValidationError({'task': 'Moguće je povezati samo aktivni radni zadatak.'})
|
|
|
|
if task and work_order and task.work_order_id and str(task.work_order_id) != str(work_order.id):
|
|
raise serializers.ValidationError({'task': 'Radni zadatak ne pripada odabranom putnom nalogu.'})
|
|
|
|
attrs['note'] = note
|
|
if task and not work_order and task.work_order_id:
|
|
attrs['work_order'] = task.work_order
|
|
if audience == 'member' and len(target_users) == 0 and target_user:
|
|
attrs['target_users'] = [target_user]
|
|
return attrs
|
|
|
|
|
|
class MonthlyServicerDayEntrySerializer(serializers.ModelSerializer):
|
|
class Meta:
|
|
model = MonthlyServicerDayEntry
|
|
fields = [
|
|
'id',
|
|
'entry_date',
|
|
'entry_type',
|
|
'description',
|
|
'location',
|
|
'start_time',
|
|
'end_time',
|
|
'regular_hours',
|
|
'overtime_hours',
|
|
'created_at',
|
|
'updated_at',
|
|
]
|
|
read_only_fields = [
|
|
'id',
|
|
'description',
|
|
'location',
|
|
'regular_hours',
|
|
'overtime_hours',
|
|
'created_at',
|
|
'updated_at',
|
|
]
|
|
|
|
class WorkOrderSerializer(serializers.ModelSerializer):
|
|
crane = serializers.SerializerMethodField()
|
|
crane_label = serializers.SerializerMethodField()
|
|
|
|
class Meta:
|
|
model = WorkOrder
|
|
fields = [
|
|
'id', 'display_code', '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',
|
|
'origin_location', '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},
|
|
'display_code': {'required': False, 'allow_blank': False},
|
|
'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},
|
|
'origin_location': {'required': False, 'allow_blank': 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
|
|
if not validated_data.get('display_code'):
|
|
creator = validated_data.get('creator')
|
|
generated_display_code = self._generate_display_code(creator, timezone.localdate())
|
|
if not generated_display_code:
|
|
raise serializers.ValidationError({
|
|
'display_code': 'Serviser mora imati ime i prezime za automatsko generiranje oznake. Unesite oznaku ručno u formatu MT070626.'
|
|
})
|
|
validated_data['display_code'] = generated_display_code
|
|
|
|
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
|
|
|
|
def validate_display_code(self, value):
|
|
normalized = str(value or '').strip().upper()
|
|
if not re.fullmatch(r"[A-Z]{2}[0-9]{6}", normalized):
|
|
raise serializers.ValidationError('Prikaz putnog naloga mora biti u formatu MT070626 (2 slova + 6 znamenki).')
|
|
return normalized
|
|
|
|
@staticmethod
|
|
def _generate_display_code(user, target_date):
|
|
if user is None:
|
|
return None
|
|
first_name = str(getattr(user, 'first_name', '') or '').strip()
|
|
last_name = str(getattr(user, 'last_name', '') or '').strip()
|
|
if not first_name or not last_name:
|
|
return None
|
|
return f"{first_name[0].upper()}{last_name[0].upper()}{target_date.strftime('%d%m%y')}"
|
|
|
|
|
|
class WorkOrderInvoiceSerializer(serializers.ModelSerializer):
|
|
image_url = serializers.SerializerMethodField()
|
|
image_content_type = serializers.SerializerMethodField()
|
|
work_order_display_code = serializers.SerializerMethodField()
|
|
|
|
class Meta:
|
|
model = WorkOrderInvoice
|
|
fields = [
|
|
'id',
|
|
'work_order',
|
|
'work_order_display_code',
|
|
'naziv_racuna',
|
|
'lokacija',
|
|
'datum',
|
|
'opis',
|
|
'image',
|
|
'image_url',
|
|
'image_content_type',
|
|
'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
|
|
base = request.build_absolute_uri(
|
|
reverse('work-order-invoice-image', kwargs={'pk': obj.pk})
|
|
)
|
|
suffix = Path(obj.image.name or '').suffix.lower()
|
|
if suffix == '.pdf':
|
|
return base
|
|
return f"{base}?w=1280&q=80&fmt=jpeg"
|
|
|
|
def get_image_content_type(self, obj):
|
|
if not obj.image:
|
|
return None
|
|
suffix = Path(obj.image.name or '').suffix.lower()
|
|
if suffix == '.pdf':
|
|
return 'application/pdf'
|
|
if suffix in {'.jpg', '.jpeg'}:
|
|
return 'image/jpeg'
|
|
if suffix == '.png':
|
|
return 'image/png'
|
|
if suffix == '.webp':
|
|
return 'image/webp'
|
|
if suffix == '.gif':
|
|
return 'image/gif'
|
|
return 'application/octet-stream'
|
|
|
|
def get_work_order_display_code(self, obj):
|
|
work_order = getattr(obj, 'work_order', None)
|
|
if work_order is None:
|
|
return None
|
|
display_code = str(getattr(work_order, 'display_code', '') or '').strip()
|
|
if display_code:
|
|
return display_code
|
|
return str(getattr(work_order, 'id', '') or '').strip() or None
|
|
|
|
def validate_image(self, value):
|
|
if not value:
|
|
return value
|
|
suffix = Path(getattr(value, 'name', '')).suffix.lower()
|
|
allowed_suffixes = {'.jpg', '.jpeg', '.png', '.webp', '.gif', '.pdf'}
|
|
if suffix not in allowed_suffixes:
|
|
raise serializers.ValidationError('Datoteka računa mora biti slika (.jpg, .jpeg, .png, .webp, .gif) ili PDF (.pdf).')
|
|
return value
|
|
|
|
|
|
class WorkOrderAdditionalCostsTableSerializer(serializers.ModelSerializer):
|
|
class Meta:
|
|
model = WorkOrderAdditionalCostsTable
|
|
fields = ['id', 'work_order', 'data', 'total_for_payout', 'created_at', 'updated_at']
|
|
read_only_fields = ['id', 'total_for_payout', 'created_at', 'updated_at']
|
|
|
|
@staticmethod
|
|
def _parse_decimal(value):
|
|
if value in (None, ''):
|
|
return Decimal('0.00')
|
|
try:
|
|
normalized = str(value).strip().replace('€', '').replace(' ', '').replace(',', '.')
|
|
return Decimal(normalized)
|
|
except (InvalidOperation, ValueError, TypeError):
|
|
return Decimal('0.00')
|
|
|
|
def _normalize_row(self, row):
|
|
return {
|
|
'naziv': str(row.get('naziv', '') or '').strip(),
|
|
'broj_racuna': str(row.get('broj_racuna', '') or '').strip(),
|
|
'ukupan_iznos': str(row.get('ukupan_iznos', '') or '').strip(),
|
|
'prilog': str(row.get('prilog', '') or '').strip(),
|
|
'source_invoice_id': str(row.get('source_invoice_id', '') or '').strip(),
|
|
}
|
|
|
|
def validate_data(self, value):
|
|
if value is None:
|
|
return {'rows': []}
|
|
if not isinstance(value, dict):
|
|
raise serializers.ValidationError("Podaci tablice moraju biti JSON objekt.")
|
|
rows = value.get('rows', [])
|
|
if not isinstance(rows, list):
|
|
raise serializers.ValidationError({"rows": "Polje 'rows' mora biti lista redaka."})
|
|
normalized_rows = []
|
|
for row in rows[:100]:
|
|
if not isinstance(row, dict):
|
|
raise serializers.ValidationError({"rows": "Svaki redak mora biti JSON objekt."})
|
|
normalized_rows.append(self._normalize_row(row))
|
|
return {'rows': normalized_rows}
|
|
|
|
def _calculate_total_for_payout(self, data):
|
|
rows = data.get('rows', []) if isinstance(data, dict) else []
|
|
total = sum((self._parse_decimal(row.get('ukupan_iznos')) for row in rows), Decimal('0.00'))
|
|
return total.quantize(Decimal('0.01'))
|
|
|
|
def create(self, validated_data):
|
|
validated_data['total_for_payout'] = self._calculate_total_for_payout(validated_data.get('data'))
|
|
return super().create(validated_data)
|
|
|
|
def update(self, instance, validated_data):
|
|
data = validated_data.get('data', instance.data or {'rows': []})
|
|
validated_data['total_for_payout'] = self._calculate_total_for_payout(data)
|
|
return super().update(instance, validated_data)
|
|
|
|
|
|
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) |