# backend/modules/task_management/serializers.py from rest_framework import serializers from .models import Task, TaskTemplate, TaskTemplateEntry, TaskWorkEntry, TaskWorkHoursTable WORK_HOURS_ROW_FIELDS = [ 'day', 'date', 'work_time_from', 'work_time_to', 'travel_time_from', 'travel_time_to', 'break_hours', 'work_hours', 'travel_hours', 'departure_place', 'arrival_place', 'vehicle_km', ] class TaskWorkHoursTableSerializer(serializers.ModelSerializer): class Meta: model = TaskWorkHoursTable fields = ['id', 'task', 'data', 'created_at', 'updated_at'] read_only_fields = ['id', 'created_at', 'updated_at'] def _normalize_row(self, row): normalized = {} for field in WORK_HOURS_ROW_FIELDS: value = row.get(field, '') normalized[field] = '' if value is None else str(value).strip() return normalized def validate_data(self, value): if value is None: return None if not isinstance(value, dict): raise serializers.ValidationError("Podaci tablice moraju biti JSON objekt ili null.") 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[:30]: 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} class TaskWorkEntrySerializer(serializers.ModelSerializer): class Meta: model = TaskWorkEntry fields = [ 'id', 'task', 'template_entry', 'sort_order', 'title', 'work_description', 'active_faults', 'explanation_hr', 'created_at', ] read_only_fields = ['id', 'created_at'] class TaskTemplateEntrySerializer(serializers.ModelSerializer): class Meta: model = TaskTemplateEntry fields = [ 'id', 'sort_order', 'title', 'work_description', 'active_faults', 'explanation_hr', ] read_only_fields = ['id'] class TaskTemplateSerializer(serializers.ModelSerializer): entries = TaskTemplateEntrySerializer(many=True, read_only=True) class Meta: model = TaskTemplate fields = ['id', 'code', 'title', 'description', 'status', 'entries'] read_only_fields = ['id'] class TaskSerializer(serializers.ModelSerializer): assigned_to_name = serializers.SerializerMethodField() work_order_label = serializers.SerializerMethodField() vehicle_registration = serializers.SerializerMethodField() vehicle_make = serializers.SerializerMethodField() vehicle_model = serializers.SerializerMethodField() vehicle_owner_name = serializers.SerializerMethodField() vehicle_asset_type = serializers.SerializerMethodField() crane_serial_number = serializers.SerializerMethodField() template_id = serializers.UUIDField(write_only=True, required=False, allow_null=True) auto_close_work_order = serializers.BooleanField(write_only=True, required=False) class Meta: model = Task fields = [ 'id', 'title', 'description', 'status', 'assigned_to', 'assigned_to_name', 'vehicle', 'vehicle_registration', 'vehicle_make', 'vehicle_model', 'vehicle_owner_name', 'vehicle_asset_type', 'crane_serial_number', 'work_order', 'work_order_label', 'scheduled_date', 'service_report_note', 'template_id', 'auto_close_work_order', 'created_at', ] read_only_fields = ['id', 'created_at'] extra_kwargs = { 'title': {'required': False, 'allow_blank': True}, 'description': {'required': False, 'allow_blank': True}, 'status': {'required': False}, 'work_order': {'required': False, 'allow_null': True}, 'vehicle': {'required': False, 'allow_null': True}, 'scheduled_date': {'required': False, 'allow_null': True}, 'service_report_note': {'required': False, 'allow_blank': True}, } def validate_title(self, value): if not value: return value if len(value) < 3: raise serializers.ValidationError("Naslov zadatka mora imati barem 3 znaka.") return value def validate(self, attrs): current = getattr(self, 'instance', None) template_id = attrs.get('template_id') status_value = attrs.get('status', getattr(current, 'status', 'aktivan')) work_order = attrs.get('work_order', getattr(current, 'work_order', None)) vehicle = attrs.get('vehicle', getattr(current, 'vehicle', None)) title = (attrs.get('title', getattr(current, 'title', '')) or '').strip() if template_id: template = TaskTemplate.objects.filter(id=template_id, is_active=True).first() if not template: raise serializers.ValidationError({'template_id': 'Odabrani template ne postoji ili nije aktivan.'}) if current is None and not vehicle: raise serializers.ValidationError({ 'vehicle': "Dizalica je obavezna pri kreiranju taska." }) if current is None and not template_id and not title: raise serializers.ValidationError({ 'title': "Naslov zadatka je obavezan ako se task ne kreira iz templatea." }) # If task has no crane yet, auto-derive it from the work order's crane (backward compat). # Cross-crane assignment is intentionally allowed: a servicer can share one work order # across tasks for multiple cranes on the same day. if work_order and not vehicle: vehicle = work_order.vehicle attrs['vehicle'] = vehicle if status_value in {'neaktivan', 'zavrsen'} and not work_order: raise serializers.ValidationError({ 'work_order': "Putni nalog je obavezan prije zatvaranja zadatka." }) return attrs 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() if display_code: return display_code return None def get_assigned_to_name(self, obj): if not obj.assigned_to_id: return None full_name = obj.assigned_to.get_full_name() if full_name: return full_name email = getattr(obj.assigned_to, 'email', None) if email: return email username = getattr(obj.assigned_to, 'username', None) if username: return username return str(obj.assigned_to_id) def get_vehicle_registration(self, obj): if not obj.vehicle_id: return None return obj.vehicle.registration_number def get_vehicle_make(self, obj): if not obj.vehicle_id: return None return obj.vehicle.make or None def get_vehicle_model(self, obj): if not obj.vehicle_id: return None return obj.vehicle.model or None def get_vehicle_owner_name(self, obj): if not obj.vehicle_id or not obj.vehicle.client_id: return None return obj.vehicle.client.name def get_vehicle_asset_type(self, obj): if not obj.vehicle_id: return None return obj.vehicle.asset_type def get_crane_serial_number(self, obj): if not obj.vehicle_id: return None return obj.vehicle.crane_serial_number or None