This commit is contained in:
412
backend/modules/fleet/services.py
Normal file
412
backend/modules/fleet/services.py
Normal file
@@ -0,0 +1,412 @@
|
||||
from django.db import transaction
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from modules.fleet.tasks import send_email_task
|
||||
from modules.fleet.models import Vehicle, WorkOrder, VehicleServiceRecord, VehicleNotification, VehicleServicePhoto
|
||||
from infrastructure.pusher_service import PusherService
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
class NotificationService:
|
||||
@staticmethod
|
||||
def create_notification(recipient=None, title="", message="", level="info", send_email=False, metadata=None):
|
||||
# 1. Kreiraj notifikaciju u DB-u
|
||||
notif = VehicleNotification.objects.create(
|
||||
recipient=recipient,
|
||||
title=title[:255],
|
||||
message=message,
|
||||
metadata=metadata or {},
|
||||
level=level,
|
||||
is_sent=False
|
||||
)
|
||||
|
||||
# 2. Broadcast preko WebSocket (Pusher) tek nakon uspješnog commit-a
|
||||
if recipient:
|
||||
transaction.on_commit(lambda: PusherService.broadcast_notification(
|
||||
user_id=recipient.id,
|
||||
title=title,
|
||||
message=message,
|
||||
level=level,
|
||||
notification_id=notif.id,
|
||||
metadata=metadata
|
||||
))
|
||||
|
||||
# 3. Slanje emaila ako je traženo
|
||||
if send_email and recipient and getattr(recipient, 'email', None):
|
||||
email_args = {
|
||||
'recipient_email': recipient.email,
|
||||
'subject': title,
|
||||
'message': message,
|
||||
'notif_id': notif.id
|
||||
}
|
||||
|
||||
if settings.DEBUG or getattr(settings, 'CELERY_TASK_ALWAYS_EAGER', False):
|
||||
send_email_task(**email_args)
|
||||
else:
|
||||
transaction.on_commit(lambda: send_email_task.delay(**email_args))
|
||||
|
||||
return notif
|
||||
class VehicleService:
|
||||
@staticmethod
|
||||
def create_vehicle(data: dict) -> Vehicle:
|
||||
vehicle = Vehicle(**data)
|
||||
try:
|
||||
vehicle.full_clean()
|
||||
except Exception as e:
|
||||
raise ValidationError({"detail": str(e)})
|
||||
|
||||
vehicle.save()
|
||||
return vehicle
|
||||
|
||||
@staticmethod
|
||||
def update_vehicle(instance: Vehicle, data: dict) -> Vehicle:
|
||||
for k, v in data.items():
|
||||
setattr(instance, k, v)
|
||||
try:
|
||||
instance.full_clean()
|
||||
except Exception as e:
|
||||
raise ValidationError({"detail": str(e)})
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
@staticmethod
|
||||
def create_work_order(data: dict) -> WorkOrder:
|
||||
# expecting creator in data
|
||||
wo = WorkOrder(**data)
|
||||
try:
|
||||
wo.full_clean()
|
||||
except Exception as e:
|
||||
raise ValidationError({"detail": str(e)})
|
||||
|
||||
with transaction.atomic():
|
||||
wo.save()
|
||||
|
||||
# obavijesti creatora i serviser-a vozila
|
||||
try:
|
||||
# Notify the creator
|
||||
NotificationService.create_notification(
|
||||
recipient=wo.creator,
|
||||
title=f"Putni nalog kreiran: WO-{wo.pk}",
|
||||
message=f"Putni nalog za dizalicu {wo.vehicle.registration_number} je kreiran.",
|
||||
metadata={
|
||||
"entity_type": "work_order",
|
||||
"work_order_id": str(wo.pk),
|
||||
"vehicle_id": str(wo.vehicle_id),
|
||||
"crane_id": str(wo.vehicle_id),
|
||||
"section": "work-orders",
|
||||
},
|
||||
level="info",
|
||||
send_email=False
|
||||
)
|
||||
# Notify assigned servicer if present
|
||||
servicer = wo.vehicle.assigned_servicer
|
||||
if servicer:
|
||||
NotificationService.create_notification(
|
||||
recipient=servicer,
|
||||
title=f"Novi putni nalog za {wo.vehicle.registration_number}",
|
||||
message=f"Imate novi putni nalog (WO-{wo.pk}) za dizalicu {wo.vehicle.registration_number}.",
|
||||
metadata={
|
||||
"entity_type": "work_order",
|
||||
"work_order_id": str(wo.pk),
|
||||
"vehicle_id": str(wo.vehicle_id),
|
||||
"crane_id": str(wo.vehicle_id),
|
||||
"section": "work-orders",
|
||||
},
|
||||
level="info",
|
||||
send_email=False
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Greška pri slanju notifikacija za create_work_order")
|
||||
|
||||
return wo
|
||||
|
||||
@staticmethod
|
||||
def update_work_order(instance: WorkOrder, data: dict) -> WorkOrder:
|
||||
for k, v in data.items():
|
||||
setattr(instance, k, v)
|
||||
try:
|
||||
instance.full_clean()
|
||||
except Exception as e:
|
||||
raise ValidationError({"detail": str(e)})
|
||||
with transaction.atomic():
|
||||
instance.save()
|
||||
# Ako je nalog zatvoren, obavijestimo kreatora/servisera
|
||||
if instance.status == 'closed':
|
||||
try:
|
||||
NotificationService.create_notification(
|
||||
recipient=instance.creator,
|
||||
title=f"Putni nalog zatvoren: WO-{instance.pk}",
|
||||
message=f"Putni nalog za dizalicu {instance.vehicle.registration_number} je zatvoren.",
|
||||
metadata={
|
||||
"entity_type": "work_order",
|
||||
"work_order_id": str(instance.pk),
|
||||
"vehicle_id": str(instance.vehicle_id),
|
||||
"crane_id": str(instance.vehicle_id),
|
||||
"section": "work-orders",
|
||||
},
|
||||
level="info",
|
||||
send_email=False
|
||||
)
|
||||
servicer = instance.vehicle.assigned_servicer
|
||||
if servicer:
|
||||
NotificationService.create_notification(
|
||||
recipient=servicer,
|
||||
title=f"Putni nalog zatvoren za {instance.vehicle.registration_number}",
|
||||
message=f"Putni nalog (WO-{instance.pk}) je zatvoren.",
|
||||
metadata={
|
||||
"entity_type": "work_order",
|
||||
"work_order_id": str(instance.pk),
|
||||
"vehicle_id": str(instance.vehicle_id),
|
||||
"crane_id": str(instance.vehicle_id),
|
||||
"section": "work-orders",
|
||||
},
|
||||
level="info",
|
||||
send_email=False
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Greška pri slanju notifikacija za update_work_order")
|
||||
return instance
|
||||
|
||||
@staticmethod
|
||||
def delete_work_order(instance: WorkOrder, user=None) -> WorkOrder:
|
||||
"""
|
||||
Soft-delete putnog naloga. Samo creator ili staff može obrisati.
|
||||
Vraća deaktivirani instance.
|
||||
"""
|
||||
# Provjera prava: samo creator ili staff
|
||||
if not user:
|
||||
raise ValidationError("Korisnik nije naveden za operaciju brisanja.")
|
||||
if not (user.is_staff or instance.creator == user):
|
||||
raise ValidationError("Nemate dozvolu za brisanje ovog putnog naloga.")
|
||||
|
||||
instance.is_active = False
|
||||
instance.save(update_fields=['is_active'])
|
||||
|
||||
# Notifikacija o brisanju
|
||||
try:
|
||||
NotificationService.create_notification(
|
||||
recipient=instance.creator,
|
||||
title=f"Putni nalog obrisan: WO-{instance.pk}",
|
||||
message=f"Putni nalog za dizalicu {instance.vehicle.registration_number} je obrisan od strane {user}.",
|
||||
metadata={
|
||||
"entity_type": "work_order",
|
||||
"work_order_id": str(instance.pk),
|
||||
"vehicle_id": str(instance.vehicle_id),
|
||||
"crane_id": str(instance.vehicle_id),
|
||||
"section": "work-orders",
|
||||
"deleted": True,
|
||||
},
|
||||
level="warning",
|
||||
send_email=False
|
||||
)
|
||||
servicer = instance.vehicle.assigned_servicer
|
||||
if servicer:
|
||||
NotificationService.create_notification(
|
||||
recipient=servicer,
|
||||
title=f"Putni nalog obrisan za {instance.vehicle.registration_number}",
|
||||
message=f"Putni nalog (WO-{instance.pk}) je obrisan.",
|
||||
metadata={
|
||||
"entity_type": "work_order",
|
||||
"work_order_id": str(instance.pk),
|
||||
"vehicle_id": str(instance.vehicle_id),
|
||||
"crane_id": str(instance.vehicle_id),
|
||||
"section": "work-orders",
|
||||
"deleted": True,
|
||||
},
|
||||
level="warning",
|
||||
send_email=False
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Greška pri slanju notifikacija za delete_work_order")
|
||||
|
||||
logger.info(f"WorkOrder {instance.pk} soft-deleted by {user}")
|
||||
return instance
|
||||
|
||||
@staticmethod
|
||||
def create_service_record(data: dict) -> VehicleServiceRecord:
|
||||
rec = VehicleServiceRecord(**data)
|
||||
try:
|
||||
rec.full_clean()
|
||||
except Exception as e:
|
||||
raise ValidationError({"detail": str(e)})
|
||||
with transaction.atomic():
|
||||
rec.save()
|
||||
# update vehicle mileage if needed
|
||||
v = rec.vehicle
|
||||
if rec.mileage and rec.mileage > (v.current_mileage or 0):
|
||||
v.current_mileage = rec.mileage
|
||||
v.save()
|
||||
|
||||
# notify assigned servicer / creator
|
||||
try:
|
||||
# Notify performed_by user
|
||||
NotificationService.create_notification(
|
||||
recipient=rec.performed_by,
|
||||
title=f"Servis zabilježen: {rec.vehicle.registration_number}",
|
||||
message=(
|
||||
f"Zabilježen servis na dizalici {rec.vehicle.registration_number}"
|
||||
+ (f" pri kilometraži {rec.mileage}." if rec.mileage is not None else ".")
|
||||
),
|
||||
metadata={
|
||||
"entity_type": "service_record",
|
||||
"service_record_id": str(rec.pk),
|
||||
"vehicle_id": str(rec.vehicle_id),
|
||||
"crane_id": str(rec.vehicle_id),
|
||||
"section": "service-records",
|
||||
},
|
||||
level="info",
|
||||
send_email=False
|
||||
)
|
||||
# Notify vehicle assigned servicer if different
|
||||
servicer = rec.vehicle.assigned_servicer
|
||||
if servicer and servicer != rec.performed_by:
|
||||
NotificationService.create_notification(
|
||||
recipient=servicer,
|
||||
title=f"Servis na vašem vozilu {rec.vehicle.registration_number}",
|
||||
message=f"Servis je izvršen na dizalici {rec.vehicle.registration_number}.",
|
||||
metadata={
|
||||
"entity_type": "service_record",
|
||||
"service_record_id": str(rec.pk),
|
||||
"vehicle_id": str(rec.vehicle_id),
|
||||
"crane_id": str(rec.vehicle_id),
|
||||
"section": "service-records",
|
||||
},
|
||||
level="info",
|
||||
send_email=False
|
||||
)
|
||||
# If next service is soon (e.g., within 1000 km), create a warning notification
|
||||
next_due = rec.next_service_due_at
|
||||
if next_due is None and rec.mileage is not None:
|
||||
next_due = rec.mileage + rec.vehicle.service_interval_km
|
||||
if next_due is not None and rec.mileage is not None and (next_due - rec.mileage) <= 1000:
|
||||
if servicer:
|
||||
NotificationService.create_notification(
|
||||
recipient=servicer,
|
||||
title=f"Uskoro servis za {rec.vehicle.registration_number}",
|
||||
message=f"Sljedeći servis za dizalicu {rec.vehicle.registration_number} očekuje se na ~{next_due} km.",
|
||||
metadata={
|
||||
"entity_type": "service_record",
|
||||
"service_record_id": str(rec.pk),
|
||||
"vehicle_id": str(rec.vehicle_id),
|
||||
"crane_id": str(rec.vehicle_id),
|
||||
"section": "service-records",
|
||||
"next_service_due_at": next_due,
|
||||
},
|
||||
level="warning",
|
||||
send_email=False
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Greška pri slanju notifikacija za create_service_record")
|
||||
return rec
|
||||
|
||||
class VehicleServicePhotoService:
|
||||
"""
|
||||
Servisna klasa za upravljanje VehicleServicePhoto zapisima.
|
||||
Delegira upload, ažuriranje i brisanje s validacijom i sigurnosnim provjerama.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def upload_photo(data: dict, uploaded_by=None) -> VehicleServicePhoto:
|
||||
"""
|
||||
Kreiraj VehicleServicePhoto s file upload-om.
|
||||
data treba sadržavati: service_record (ID ili instancu), image (UploadedFile), description (opcionalno).
|
||||
"""
|
||||
if not uploaded_by:
|
||||
raise ValidationError("Korisnik je obavezan za upload fotografije.")
|
||||
|
||||
# Ako je service_record ID ili UUID, dohvati instancu
|
||||
service_record = data.get('service_record')
|
||||
|
||||
# Provjeri je li već VehicleServiceRecord instanca
|
||||
if not isinstance(service_record, VehicleServiceRecord):
|
||||
# Pokušaj dohvatiti po ID-u (int, str, UUID)
|
||||
try:
|
||||
service_record = VehicleServiceRecord.objects.get(pk=service_record)
|
||||
except (VehicleServiceRecord.DoesNotExist, ValueError, TypeError):
|
||||
raise ValidationError("Servisni zapis ne postoji ili ID nije valjan.")
|
||||
|
||||
# Kreiraj novi dict s istim podacima ali sigurno postavljenim service_record
|
||||
photo_data = {
|
||||
'service_record': service_record,
|
||||
'image': data.get('image'),
|
||||
'description': data.get('description', ''),
|
||||
'uploaded_by': uploaded_by
|
||||
}
|
||||
|
||||
photo = VehicleServicePhoto(**photo_data)
|
||||
try:
|
||||
photo.full_clean()
|
||||
except Exception as e:
|
||||
raise ValidationError({"detail": str(e)})
|
||||
|
||||
photo.save()
|
||||
logger.info(f"Fotografija uploadana od strane {uploaded_by} za servisni zapis {service_record.id}")
|
||||
return photo
|
||||
|
||||
@staticmethod
|
||||
def update_photo(instance: VehicleServicePhoto, data: dict, user=None) -> VehicleServicePhoto:
|
||||
"""
|
||||
Ažuriraj VehicleServicePhoto. Samo uploader ili staff mogu ažurirati.
|
||||
"""
|
||||
# Sigurnosna provjera: samo uploader ili staff mogu ažurirati
|
||||
if not (user and (user.is_staff or instance.uploaded_by == user)):
|
||||
raise ValidationError("Nemate dozvolu za ažuriranje ove fotografije.")
|
||||
|
||||
for attr, value in data.items():
|
||||
if attr != 'image': # ne dozvoli ponovno upload slike (opciono)
|
||||
setattr(instance, attr, value)
|
||||
|
||||
try:
|
||||
instance.full_clean()
|
||||
except Exception as e:
|
||||
raise ValidationError({"detail": str(e)})
|
||||
|
||||
instance.save()
|
||||
logger.info(f"Fotografija {instance.id} ažurirana od strane {user}")
|
||||
return instance
|
||||
|
||||
@staticmethod
|
||||
def delete_photo(instance: VehicleServicePhoto, user=None) -> VehicleServicePhoto:
|
||||
"""
|
||||
Soft-delete VehicleServicePhoto. Samo uploader ili staff mogu obrisati.
|
||||
"""
|
||||
if not (user and (user.is_staff or instance.uploaded_by == user)):
|
||||
raise ValidationError("Nemate dozvolu za brisanje ove fotografije.")
|
||||
|
||||
instance.is_active = False
|
||||
instance.save(update_fields=['is_active'])
|
||||
logger.info(f"Fotografija {instance.id} soft-deleted od strane {user}")
|
||||
return instance
|
||||
|
||||
class FleetService:
|
||||
@staticmethod
|
||||
def bulk_close_work_orders(work_orders_qs):
|
||||
updated_vehicles = {}
|
||||
|
||||
with transaction.atomic():
|
||||
for wo in work_orders_qs:
|
||||
# 1. Preskoči ako je već zatvoren
|
||||
if wo.status == 'closed':
|
||||
continue
|
||||
|
||||
# 2. NOVO: Preskoči ako nalog nije kompletan (nema kilometražu)
|
||||
if wo.end_mileage is None:
|
||||
continue
|
||||
|
||||
# 3. Zatvori nalog
|
||||
wo.status = 'closed'
|
||||
wo.save()
|
||||
|
||||
# 4. Ažuriraj vozilo
|
||||
vehicle = wo.vehicle
|
||||
current_max = updated_vehicles.get(vehicle.pk, vehicle.current_mileage)
|
||||
if wo.end_mileage > current_max:
|
||||
updated_vehicles[vehicle.pk] = wo.end_mileage
|
||||
|
||||
# Masovno ažuriranje vozila
|
||||
for v_id, new_mileage in updated_vehicles.items():
|
||||
Vehicle.objects.filter(pk=v_id).update(current_mileage=new_mileage)
|
||||
Reference in New Issue
Block a user