Dodaje status 'spreman_za_zavrsetak' i backend provjeru koja blokira zatvaranje taska kada je povezani putni nalog otvoren, uz 409 odgovor i opcionalni auto-close flow.\n\nPri zatvaranju putnog naloga taskovi u statusu 'servis' automatski prelaze u 'spreman_za_zavrsetak'. Frontend modal sada podržava potvrdu i retry s auto_close_work_order=true.\n\nUključeni su i ciljani testovi za novi behavior, te ažuriran prikaz statusa u dashboardu.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
150 lines
6.6 KiB
Python
150 lines
6.6 KiB
Python
# backend/modules/task_management/services.py
|
|
|
|
from django.db import transaction
|
|
from rest_framework.exceptions import ValidationError
|
|
from django.core.exceptions import ValidationError as DjangoValidationError
|
|
import logging
|
|
|
|
from .models import Task, TaskTemplate, TaskWorkEntry
|
|
from .exceptions import OpenWorkOrderBlockingTaskClose
|
|
from modules.fleet.models import VehicleServiceRecord
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class TaskService:
|
|
@staticmethod
|
|
def create_task_entry(data: dict, user=None) -> Task:
|
|
"""
|
|
Univerzalna metoda: prihvaća podatke i opcionalnog korisnika.
|
|
"""
|
|
payload = dict(data)
|
|
template_id = payload.pop('template_id', None)
|
|
|
|
if payload.get('status') == 'DONE' and (user and not user.is_staff):
|
|
raise ValidationError({"status": "Samo admini mogu odmah označiti zadatak kao gotov."})
|
|
|
|
template = None
|
|
if template_id:
|
|
template = TaskTemplate.objects.filter(id=template_id, is_active=True).prefetch_related('entries').first()
|
|
if not template:
|
|
raise ValidationError({"template_id": "Odabrani template nije pronađen."})
|
|
|
|
payload['title'] = template.title
|
|
payload['description'] = template.description
|
|
payload['status'] = 'aktivan'
|
|
payload.setdefault('work_order', None)
|
|
|
|
title = (payload.get('title') or '').strip()
|
|
if title and len(title) < 3:
|
|
raise ValidationError({"title": "Naslov zadatka mora imati barem 3 znaka."})
|
|
if not title:
|
|
raise ValidationError({"title": "Naslov zadatka je obavezan."})
|
|
|
|
if user and 'assigned_to' not in payload:
|
|
payload['assigned_to'] = user
|
|
|
|
entry = Task(**payload)
|
|
try:
|
|
entry.full_clean()
|
|
except DjangoValidationError as exc:
|
|
logger.error(f"Neuspješna validacija zadatka: {exc}")
|
|
raise ValidationError(exc.message_dict if hasattr(exc, 'message_dict') else exc.messages)
|
|
|
|
with transaction.atomic():
|
|
entry.save()
|
|
if template:
|
|
TaskService._create_work_entries_from_template(task=entry, template=template)
|
|
TaskService._create_service_records_from_template(task=entry, template=template, user=user)
|
|
|
|
logger.info(f"Zadatak uspješno kreiran: {entry.id}")
|
|
return entry
|
|
|
|
@staticmethod
|
|
def _create_work_entries_from_template(*, task: Task, template: TaskTemplate):
|
|
template_entries = list(template.entries.filter(is_active=True).order_by('sort_order', 'created_at')[:10])
|
|
for idx, source in enumerate(template_entries, start=1):
|
|
TaskWorkEntry.objects.create(
|
|
task=task,
|
|
template_entry=source,
|
|
sort_order=idx,
|
|
title=source.title,
|
|
work_description=source.work_description,
|
|
active_faults=source.active_faults,
|
|
explanation_hr=source.explanation_hr,
|
|
)
|
|
|
|
@staticmethod
|
|
def _create_service_records_from_template(*, task: Task, template: TaskTemplate, user):
|
|
if not user:
|
|
raise ValidationError({
|
|
"template_id": "Kreiranje taska iz templatea zahtijeva prijavljenog korisnika za servisne zapise."
|
|
})
|
|
if not task.vehicle_id:
|
|
raise ValidationError({
|
|
"vehicle": "Task iz templatea mora imati dodijeljenu dizalicu za generiranje servisnih zapisa."
|
|
})
|
|
|
|
template_entries = list(template.entries.filter(is_active=True).order_by('sort_order', 'created_at')[:10])
|
|
for source in template_entries:
|
|
description_parts = []
|
|
if source.work_description:
|
|
description_parts.append(source.work_description.strip())
|
|
if source.explanation_hr:
|
|
description_parts.append(f"Obrazloženje (HR): {source.explanation_hr.strip()}")
|
|
if source.active_faults:
|
|
description_parts.append(f"Aktivne greške: {source.active_faults.strip()}")
|
|
description = "\n\n".join([part for part in description_parts if part]).strip() or source.title
|
|
|
|
VehicleServiceRecord.objects.create(
|
|
vehicle=task.vehicle,
|
|
task=task,
|
|
performed_by=user,
|
|
service_title=source.title,
|
|
description=description,
|
|
parts=(source.active_faults or '').strip(),
|
|
cost=0,
|
|
)
|
|
|
|
@staticmethod
|
|
def update_task_entry(instance, data, user, auto_close_work_order=False):
|
|
if not user.is_staff and instance.assigned_to != user:
|
|
raise ValidationError("Nemate dozvolu za izmjenu ovog zadatka.")
|
|
|
|
target_status = data.get('status', instance.status)
|
|
target_work_order = data.get('work_order', instance.work_order)
|
|
closing_statuses = {'neaktivan', 'zavrsen'}
|
|
|
|
if target_status in closing_statuses:
|
|
if not target_work_order:
|
|
raise ValidationError({"work_order": "Putni nalog je obavezan prije zatvaranja zadatka."})
|
|
if target_work_order.status == 'open':
|
|
if not auto_close_work_order:
|
|
work_order_label = f"WO-{str(target_work_order.id).split('-')[0].upper()}"
|
|
raise OpenWorkOrderBlockingTaskClose(detail={
|
|
"code": "OPEN_WORK_ORDER_BLOCKING_TASK_CLOSE",
|
|
"message": f"Putni nalog {work_order_label} je još otvoren. Želite li ga automatski zatvoriti?",
|
|
"work_order_id": str(target_work_order.id),
|
|
"work_order_label": work_order_label,
|
|
})
|
|
if target_work_order.end_mileage is None:
|
|
raise ValidationError({
|
|
"work_order": "Automatsko zatvaranje putnog naloga nije moguće bez završne kilometraže."
|
|
})
|
|
from modules.fleet.services import VehicleService
|
|
target_work_order = VehicleService.update_work_order(target_work_order, {"status": "closed"})
|
|
data['work_order'] = target_work_order
|
|
|
|
for attr, value in data.items():
|
|
if attr == 'template_id':
|
|
continue
|
|
setattr(instance, attr, value)
|
|
if 'title' in data and len(str(data.get('title') or '').strip()) < 3:
|
|
raise ValidationError({"title": "Naslov zadatka mora imati barem 3 znaka."})
|
|
try:
|
|
instance.full_clean()
|
|
except DjangoValidationError as exc:
|
|
raise ValidationError(exc.message_dict if hasattr(exc, 'message_dict') else exc.messages)
|
|
instance.save()
|
|
return instance
|