Files
mariomitte 181cab1f45
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
fix: sync multi-day work hours accounting
Aggregate task work-hour rows into travel-cost calculations and monthly servicer reporting, and keep the frontend calendar aligned with the task work-hours source.
2026-08-08 01:32:17 +02:00

222 lines
7.9 KiB
Python

# 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()
work_hours_table = 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_hours_table',
'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
def get_work_hours_table(self, obj):
table = getattr(obj, 'work_hours_table', None)
if not table:
return None
return TaskWorkHoursTableSerializer(table).data