- Novo polje Task.scheduled_date (DateField, nullable, db_index) za planiranje - Migracija 0009_task_scheduled_date - Celery task notify_upcoming_tasks: dnevno u 08:00 šalje Pusher + email za sutrašnje zadatke - TaskCalendarWidget: react-spring slide panel s lijeve strane, grid prikaz mjeseca, dot indikatori, klik otvara TaskServiceRecordsModal - FleetDashboardShell: import + render kalendara, Planirano stupac u tablici zadataka - TaskCreateModal + TaskServiceRecordsModal: scheduled_date input i prikaz - Admin + serializer + views: scheduled_date filter i prikaz - 47 backend testova prolaze (uključuje 8 novih za notify_upcoming_tasks) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
538 lines
21 KiB
Python
538 lines
21 KiB
Python
import uuid
|
|
import re
|
|
from django.db import models
|
|
from django.conf import settings
|
|
from django.core.exceptions import ValidationError
|
|
from django.utils.translation import gettext_lazy as _
|
|
from django.utils import timezone
|
|
from core.base_models import BaseModel
|
|
from decimal import Decimal
|
|
|
|
def _service_photo_upload_to(instance, filename):
|
|
"""
|
|
Putanja za upload:
|
|
fleet/{registration_number}/service_photos/{uuid4}_{original_filename}
|
|
Koristimo uuid jer instance.service_record.pk možda nije dostupna prije prvog save-a.
|
|
"""
|
|
vehicle_reg = 'unknown'
|
|
try:
|
|
vehicle_reg = instance.service_record.vehicle.registration_number
|
|
except Exception:
|
|
# fallback
|
|
vehicle_reg = 'unknown'
|
|
unique_name = f"{uuid.uuid4().hex}_{filename}"
|
|
return f"fleet/{vehicle_reg}/service_photos/{unique_name}"
|
|
|
|
def _service_attachment_upload_to(instance, filename):
|
|
"""
|
|
Putanja za upload:
|
|
fleet/{registration_number}/service_attachments/{uuid4}_{original_filename}
|
|
"""
|
|
vehicle_reg = 'unknown'
|
|
try:
|
|
vehicle_reg = instance.service_record.vehicle.registration_number
|
|
except Exception:
|
|
vehicle_reg = 'unknown'
|
|
unique_name = f"{uuid.uuid4().hex}_{filename}"
|
|
return f"fleet/{vehicle_reg}/service_attachments/{unique_name}"
|
|
|
|
def _work_order_invoice_upload_to(instance, filename):
|
|
vehicle_reg = 'unknown'
|
|
try:
|
|
vehicle_reg = instance.work_order.vehicle.registration_number
|
|
except Exception:
|
|
vehicle_reg = 'unknown'
|
|
unique_name = f"{uuid.uuid4().hex}_{filename}"
|
|
return f"fleet/{vehicle_reg}/work_order_invoices/{unique_name}"
|
|
|
|
def _work_order_photo_upload_to(instance, filename):
|
|
vehicle_reg = 'unknown'
|
|
try:
|
|
vehicle_reg = instance.work_order.vehicle.registration_number
|
|
except Exception:
|
|
vehicle_reg = 'unknown'
|
|
unique_name = f"{uuid.uuid4().hex}_{filename}"
|
|
return f"fleet/{vehicle_reg}/work_order_photos/{unique_name}"
|
|
|
|
def _generated_work_order_pdf_upload_to(instance, filename):
|
|
vehicle_reg = 'unknown'
|
|
try:
|
|
vehicle_reg = instance.work_order.vehicle.registration_number
|
|
except Exception:
|
|
vehicle_reg = 'unknown'
|
|
unique_name = f"{uuid.uuid4().hex}_{filename}"
|
|
return f"fleet/{vehicle_reg}/generated_pdfs/{unique_name}"
|
|
|
|
|
|
class VehicleServicePhoto(BaseModel):
|
|
"""
|
|
Fotografije kvarova / radova povezane s VehicleServiceRecord.
|
|
Pohranjuju se slike i opcionalni opis te tko ih je upload-ao.
|
|
"""
|
|
service_record = models.ForeignKey(
|
|
'fleet.VehicleServiceRecord',
|
|
on_delete=models.CASCADE,
|
|
related_name='photos',
|
|
verbose_name=_("Servisni zapis")
|
|
)
|
|
image = models.ImageField(
|
|
upload_to=_service_photo_upload_to,
|
|
verbose_name=_("Fotografija")
|
|
)
|
|
description = models.CharField(max_length=255, blank=True, verbose_name=_("Opis"))
|
|
uploaded_by = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
null=True,
|
|
blank=True,
|
|
on_delete=models.SET_NULL,
|
|
related_name='uploaded_service_photos',
|
|
verbose_name=_("Postavio")
|
|
)
|
|
is_active = models.BooleanField(default=True, verbose_name=_("Aktivan"))
|
|
|
|
class Meta:
|
|
verbose_name = _("Fotografija servisa")
|
|
verbose_name_plural = _("Fotografije servisa")
|
|
ordering = ['-created_at']
|
|
|
|
def __str__(self):
|
|
return f"Photo {self.pk} for {self.service_record} ({self.image.name if self.image else 'no-image'})"
|
|
|
|
class VehicleServiceAttachment(BaseModel):
|
|
"""
|
|
Datoteke (PDF, DOC, XLS...) povezane sa servisnim zapisom.
|
|
"""
|
|
service_record = models.ForeignKey(
|
|
'fleet.VehicleServiceRecord',
|
|
on_delete=models.CASCADE,
|
|
related_name='attachments',
|
|
verbose_name=_("Servisni zapis")
|
|
)
|
|
file = models.FileField(
|
|
upload_to=_service_attachment_upload_to,
|
|
verbose_name=_("Datoteka")
|
|
)
|
|
description = models.CharField(max_length=255, blank=True, verbose_name=_("Opis"))
|
|
uploaded_by = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
null=True,
|
|
blank=True,
|
|
on_delete=models.SET_NULL,
|
|
related_name='uploaded_service_attachments',
|
|
verbose_name=_("Postavio")
|
|
)
|
|
is_active = models.BooleanField(default=True, verbose_name=_("Aktivan"))
|
|
|
|
class Meta:
|
|
verbose_name = _("Prilog servisa")
|
|
verbose_name_plural = _("Prilozi servisa")
|
|
ordering = ['-created_at']
|
|
|
|
def __str__(self):
|
|
return f"Attachment {self.pk} for {self.service_record} ({self.file.name if self.file else 'no-file'})"
|
|
|
|
class Vehicle(BaseModel):
|
|
ASSET_TYPE_CHOICES = [
|
|
('vehicle', _("Vozilo")),
|
|
('crane', _("Dizalica")),
|
|
]
|
|
|
|
asset_type = models.CharField(
|
|
max_length=20,
|
|
choices=ASSET_TYPE_CHOICES,
|
|
default='crane',
|
|
db_index=True,
|
|
verbose_name=_("Tip sredstva"),
|
|
)
|
|
registration_number = models.CharField(max_length=20, unique=True, verbose_name=_("Registracija"))
|
|
make = models.CharField(max_length=100, blank=True, verbose_name=_("Proizvođač"))
|
|
model = models.CharField(max_length=100, blank=True, verbose_name=_("Model"))
|
|
year = models.PositiveSmallIntegerField(null=True, blank=True, verbose_name=_("Godina"))
|
|
vin = models.CharField(max_length=50, blank=True, null=True, unique=True, verbose_name=_("VIN"))
|
|
crane_serial_number = models.CharField(
|
|
max_length=6,
|
|
null=True,
|
|
blank=True,
|
|
db_index=True,
|
|
verbose_name=_("Serijski broj dizalice"),
|
|
help_text=_("Serijski broj dizalice mora imati točno 6 znamenki."),
|
|
)
|
|
superstructure_working_hours = models.PositiveIntegerField(
|
|
default=0,
|
|
verbose_name=_("Radni sati nadogradnje"),
|
|
)
|
|
chassis_working_hours = models.PositiveIntegerField(
|
|
default=0,
|
|
verbose_name=_("Radni sati podvozja"),
|
|
)
|
|
current_mileage = models.PositiveIntegerField(default=0, verbose_name=_("Trenutna kilometraža (km)"))
|
|
service_interval_km = models.PositiveIntegerField(default=15000, verbose_name=_("Interval servisa (km)"))
|
|
assigned_servicer = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.PROTECT,
|
|
related_name='assigned_vehicles',
|
|
null=True,
|
|
blank=True,
|
|
verbose_name=_("Serviser")
|
|
)
|
|
# Označava je li vozilo u vlasništvu naše tvrtke (fleet) ili kupca
|
|
is_company_vehicle = models.BooleanField(
|
|
default=True,
|
|
verbose_name=_("Vozilo tvrtke"),
|
|
help_text=_("Označite ako vozilo pripada našoj tvrtki. Poništite za vozila kupaca.")
|
|
)
|
|
# Kupac kojemu pripada vozilo (samo za vozila kupaca)
|
|
client = models.ForeignKey(
|
|
'crm.Client',
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='vehicles',
|
|
verbose_name=_("Kupac"),
|
|
help_text=_("Popunite samo za vozila kupaca (is_company_vehicle = Ne).")
|
|
)
|
|
is_active = models.BooleanField(default=True, verbose_name=_("Aktivno"))
|
|
|
|
class Meta:
|
|
verbose_name = _("Vozilo")
|
|
verbose_name_plural = _("Vozila")
|
|
ordering = ['registration_number']
|
|
|
|
def __str__(self):
|
|
return f"{self.registration_number} - {self.make} {self.model}"
|
|
|
|
def clean(self):
|
|
super().clean()
|
|
if self.asset_type == 'crane':
|
|
if not self.crane_serial_number and self._state.adding:
|
|
raise ValidationError({'crane_serial_number': _("Serijski broj dizalice je obavezan za dizalice.")})
|
|
if self.crane_serial_number and not re.fullmatch(r"[0-9]{6}", str(self.crane_serial_number or '')):
|
|
raise ValidationError({'crane_serial_number': _("Serijski broj dizalice mora sadržavati točno 6 znamenki.")})
|
|
elif self.crane_serial_number:
|
|
if not re.fullmatch(r"[0-9]{6}", str(self.crane_serial_number or '')):
|
|
raise ValidationError({'crane_serial_number': _("Serijski broj dizalice mora sadržavati točno 6 znamenki.")})
|
|
|
|
def next_service_at(self) -> int:
|
|
"""
|
|
Vrati kilometražu pri kojoj sljedeći servis pada, bazirano na trenutnoj kilometraži i intervalu.
|
|
Npr. current=12000, interval=15000 -> next_service_at = 15000
|
|
Npr. current=32000, interval=15000 -> next_service_at = 45000
|
|
"""
|
|
if self.service_interval_km <= 0:
|
|
return 0
|
|
blocks = (self.current_mileage // self.service_interval_km) + 1
|
|
return blocks * self.service_interval_km
|
|
|
|
|
|
class Crane(Vehicle):
|
|
class Meta:
|
|
proxy = True
|
|
verbose_name = _("Dizalica")
|
|
verbose_name_plural = _("Dizalice")
|
|
ordering = ['registration_number']
|
|
|
|
def __str__(self):
|
|
details = " ".join(part for part in [self.make, self.model] if part)
|
|
if details:
|
|
return f"{self.registration_number} - {details}"
|
|
return self.registration_number
|
|
|
|
class WorkOrder(BaseModel):
|
|
"""
|
|
Putni nalog / radni nalog koji prati kilometražu vozila tijekom zadatka na terenu.
|
|
Kad se postavi end_mileage, model ažurira vehicle.current_mileage ako je end_mileage veći.
|
|
"""
|
|
STATUS_CHOICES = [
|
|
('open', 'Otvoren'),
|
|
('closed', 'Zatvoren'),
|
|
]
|
|
PURPOSE_CHOICES = [
|
|
('defektaza', _("Defektaža")),
|
|
('kontrola', _("Kontrola")),
|
|
('redovni_pregled', _("Redovni pregled")),
|
|
('popravak', _("Popravak")),
|
|
]
|
|
|
|
vehicle = models.ForeignKey(Vehicle, on_delete=models.PROTECT, related_name='work_orders', verbose_name=_("Vozilo"))
|
|
creator = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='created_work_orders', verbose_name=_("Kreirao"))
|
|
date = models.DateField(auto_now_add=True, verbose_name=_("Datum"))
|
|
start_mileage = models.PositiveIntegerField(null=True, blank=True, verbose_name=_("Početna kilometraža"))
|
|
end_mileage = models.PositiveIntegerField(null=True, blank=True, verbose_name=_("Završna kilometraža"))
|
|
servicer_vehicle_registration = models.CharField(
|
|
max_length=20,
|
|
blank=True,
|
|
verbose_name=_("Vozilo servisera - registracija"),
|
|
)
|
|
servicer_vehicle_make_model = models.CharField(
|
|
max_length=120,
|
|
blank=True,
|
|
verbose_name=_("Vozilo servisera - marka/model"),
|
|
)
|
|
servicer_vehicle_start_mileage = models.PositiveIntegerField(
|
|
null=True,
|
|
blank=True,
|
|
verbose_name=_("Vozilo servisera - početna kilometraža"),
|
|
)
|
|
servicer_vehicle_end_mileage = models.PositiveIntegerField(
|
|
null=True,
|
|
blank=True,
|
|
verbose_name=_("Vozilo servisera - završna kilometraža"),
|
|
)
|
|
servicer_vehicle_fuel_cost = models.DecimalField(
|
|
max_digits=10,
|
|
decimal_places=2,
|
|
null=True,
|
|
blank=True,
|
|
verbose_name=_("Vozilo servisera - trošak goriva (EUR)"),
|
|
)
|
|
location = models.CharField(max_length=255, blank=True, verbose_name=_("Lokacija odredišta"))
|
|
travel_start_at = models.DateTimeField(null=True, blank=True, verbose_name=_("Početak puta"))
|
|
travel_end_at = models.DateTimeField(null=True, blank=True, verbose_name=_("Kraj puta"))
|
|
display_code = models.CharField(
|
|
max_length=8,
|
|
blank=True,
|
|
db_index=True,
|
|
verbose_name=_("Prikaz putnog naloga"),
|
|
help_text=_("Format: MT070626 (2 slova + 6 znamenki)."),
|
|
)
|
|
purpose = models.CharField(max_length=50, choices=PURPOSE_CHOICES, blank=True, verbose_name=_("Svrha"))
|
|
notes = models.TextField(blank=True, verbose_name=_("Bilješke"))
|
|
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='open', verbose_name=_("Status"))
|
|
|
|
class Meta:
|
|
verbose_name = _("Putni nalog")
|
|
verbose_name_plural = _("Putni nalozi")
|
|
ordering = ['-created_at']
|
|
|
|
def __str__(self):
|
|
return f"WO-{self.pk} {self.vehicle.registration_number} ({self.status})"
|
|
|
|
@property
|
|
def distance(self):
|
|
if self.end_mileage is None or self.start_mileage is None:
|
|
return None
|
|
return max(0, self.end_mileage - self.start_mileage)
|
|
|
|
def save(self, *args, **kwargs):
|
|
self.full_clean() # Provjera validnosti prije svega
|
|
updating_vehicle = False
|
|
if self.end_mileage is not None:
|
|
# Ako je end_mileage veći od trenutne vehicle.current_mileage, ažuriraj vozilo.
|
|
if self.end_mileage > (self.vehicle.current_mileage or 0):
|
|
self.vehicle.current_mileage = self.end_mileage
|
|
updating_vehicle = True
|
|
|
|
super().save(*args, **kwargs)
|
|
|
|
if updating_vehicle:
|
|
# Spremi vozilo (nakon što nalog ima PK)
|
|
self.vehicle.save()
|
|
|
|
def clean(self):
|
|
super().clean()
|
|
if self.display_code:
|
|
self.display_code = str(self.display_code).strip().upper()
|
|
if not re.fullmatch(r"[A-Z]{2}[0-9]{6}", self.display_code):
|
|
raise ValidationError({
|
|
"display_code": _("Prikaz putnog naloga mora biti u formatu MT070626 (2 slova + 6 znamenki).")
|
|
})
|
|
if self.travel_start_at and self.travel_end_at and self.travel_end_at < self.travel_start_at:
|
|
raise ValidationError({"travel_end_at": _("Kraj puta ne može biti prije početka puta.")})
|
|
|
|
|
|
class WorkOrderInvoice(BaseModel):
|
|
work_order = models.ForeignKey(
|
|
WorkOrder,
|
|
on_delete=models.CASCADE,
|
|
related_name='invoices',
|
|
verbose_name=_("Putni nalog"),
|
|
)
|
|
naziv_racuna = models.CharField(max_length=255, verbose_name=_("Naziv računa"))
|
|
lokacija = models.CharField(max_length=255, blank=True, verbose_name=_("Lokacija"))
|
|
datum = models.DateField(default=timezone.now, verbose_name=_("Datum"))
|
|
opis = models.TextField(blank=True, verbose_name=_("Opis"))
|
|
image = models.ImageField(
|
|
upload_to=_work_order_invoice_upload_to,
|
|
null=True,
|
|
blank=True,
|
|
verbose_name=_("Slika računa"),
|
|
)
|
|
created_by = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
null=True,
|
|
blank=True,
|
|
on_delete=models.SET_NULL,
|
|
related_name='created_work_order_invoices',
|
|
verbose_name=_("Kreirao"),
|
|
)
|
|
is_active = models.BooleanField(default=True, verbose_name=_("Aktivan"))
|
|
|
|
class Meta:
|
|
verbose_name = _("Račun putnog naloga")
|
|
verbose_name_plural = _("Računi putnog naloga")
|
|
ordering = ['-datum', '-created_at']
|
|
|
|
def __str__(self):
|
|
return f"{self.naziv_racuna} ({self.work_order_id})"
|
|
|
|
|
|
class WorkOrderPhoto(BaseModel):
|
|
work_order = models.ForeignKey(
|
|
WorkOrder,
|
|
on_delete=models.CASCADE,
|
|
related_name='photos',
|
|
verbose_name=_("Putni nalog"),
|
|
)
|
|
image = models.ImageField(
|
|
upload_to=_work_order_photo_upload_to,
|
|
verbose_name=_("Fotografija putnog naloga"),
|
|
)
|
|
description = models.CharField(max_length=255, blank=True, verbose_name=_("Opis"))
|
|
uploaded_by = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
null=True,
|
|
blank=True,
|
|
on_delete=models.SET_NULL,
|
|
related_name='uploaded_work_order_photos',
|
|
verbose_name=_("Postavio"),
|
|
)
|
|
is_active = models.BooleanField(default=True, verbose_name=_("Aktivan"))
|
|
|
|
class Meta:
|
|
verbose_name = _("Fotografija putnog naloga")
|
|
verbose_name_plural = _("Fotografije putnog naloga")
|
|
ordering = ['-created_at']
|
|
|
|
def __str__(self):
|
|
return f"WorkOrderPhoto {self.pk} ({self.work_order_id})"
|
|
|
|
|
|
class GeneratedWorkOrderPdf(BaseModel):
|
|
PDF_TYPE_CHOICES = [
|
|
('work_order', _("Putni nalog")),
|
|
('service_records', _("Servisni zapisi")),
|
|
('invoices', _("Računi")),
|
|
]
|
|
STATUS_CHOICES = [
|
|
('pending', _("U obradi")),
|
|
('ready', _("Spremno")),
|
|
('failed', _("Neuspješno")),
|
|
]
|
|
|
|
work_order = models.ForeignKey(
|
|
WorkOrder,
|
|
on_delete=models.CASCADE,
|
|
related_name='generated_pdfs',
|
|
verbose_name=_("Putni nalog"),
|
|
)
|
|
requested_by = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
null=True,
|
|
blank=True,
|
|
on_delete=models.SET_NULL,
|
|
related_name='requested_generated_work_order_pdfs',
|
|
verbose_name=_("Zatražio"),
|
|
)
|
|
pdf_type = models.CharField(max_length=32, choices=PDF_TYPE_CHOICES, db_index=True, verbose_name=_("Tip PDF-a"))
|
|
status = models.CharField(max_length=16, choices=STATUS_CHOICES, default='pending', db_index=True, verbose_name=_("Status"))
|
|
file = models.FileField(upload_to=_generated_work_order_pdf_upload_to, null=True, blank=True, verbose_name=_("PDF datoteka"))
|
|
filename = models.CharField(max_length=255, blank=True, verbose_name=_("Naziv datoteke"))
|
|
expires_at = models.DateTimeField(null=True, blank=True, db_index=True, verbose_name=_("Vrijedi do"))
|
|
error_message = models.TextField(blank=True, verbose_name=_("Poruka greške"))
|
|
|
|
class Meta:
|
|
verbose_name = _("Generirani PDF putnog naloga")
|
|
verbose_name_plural = _("Generirani PDF-ovi putnih naloga")
|
|
ordering = ['-created_at']
|
|
|
|
def __str__(self):
|
|
return f"{self.pdf_type}:{self.work_order_id}:{self.status}"
|
|
|
|
|
|
class VehicleServiceRecord(BaseModel):
|
|
"""
|
|
Zapisi o servisima/popravcima na vozilu.
|
|
"""
|
|
vehicle = models.ForeignKey(Vehicle, on_delete=models.PROTECT, related_name='service_records', verbose_name=_("Vozilo"))
|
|
task = models.ForeignKey(
|
|
'task_management.Task',
|
|
on_delete=models.SET_NULL,
|
|
related_name='service_records',
|
|
null=True,
|
|
blank=True,
|
|
verbose_name=_("Radni zadatak"),
|
|
)
|
|
performed_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='performed_vehicle_services', verbose_name=_("Izvršio"))
|
|
service_title = models.CharField(max_length=255, blank=True, default='', verbose_name=_("Naziv servisnog zapisa"))
|
|
description = models.TextField(verbose_name=_("Opis posla"))
|
|
parts = models.TextField(blank=True, verbose_name=_("Korišteni dijelovi"))
|
|
cost = models.DecimalField(max_digits=10, decimal_places=2, default=Decimal('0.00'), verbose_name=_("Trošak"))
|
|
service_date = models.DateField(auto_now_add=True, verbose_name=_("Datum servisa"))
|
|
mileage = models.PositiveIntegerField(null=True, blank=True, verbose_name=_("Kilometraža pri servisu"))
|
|
next_service_due_at = models.PositiveIntegerField(null=True, blank=True, verbose_name=_("Kilometraža sljedećeg servisa"))
|
|
|
|
class Meta:
|
|
verbose_name = _("Servisni zapis")
|
|
verbose_name_plural = _("Servisni zapisi")
|
|
ordering = ['-service_date']
|
|
|
|
def __str__(self):
|
|
mileage_display = self.mileage if self.mileage is not None else "-"
|
|
title = (self.service_title or '').strip()
|
|
if title:
|
|
return f"{title} ({self.vehicle.registration_number} @ {mileage_display})"
|
|
return f"Service {self.vehicle.registration_number} @ {mileage_display}"
|
|
|
|
def clean(self):
|
|
super().clean()
|
|
if self.task_id and self.vehicle_id:
|
|
task = self.task
|
|
if task.vehicle_id and task.vehicle_id != self.vehicle_id:
|
|
raise ValidationError({'task': _("Radni zadatak mora biti povezan s istom dizalicom kao servisni zapis.")})
|
|
|
|
def save(self, *args, **kwargs):
|
|
self.full_clean() # Provjera validnosti prije svega
|
|
# Ako nije zadan next_service_due_at, postavi prema vehicle.service_interval_km
|
|
if self.next_service_due_at is None and self.mileage is not None and self.vehicle and self.vehicle.service_interval_km:
|
|
self.next_service_due_at = self.mileage + self.vehicle.service_interval_km
|
|
|
|
# VAŽNO: ažuriraj vehicle.current_mileage PRIJE save-a kako bi refresh-om nakon toga korisnik vidio svježe podatke
|
|
if self.mileage is not None and self.mileage > (self.vehicle.current_mileage or 0):
|
|
self.vehicle.current_mileage = self.mileage
|
|
self.vehicle.save(update_fields=['current_mileage'])
|
|
|
|
super().save(*args, **kwargs)
|
|
|
|
class VehicleNotification(BaseModel):
|
|
"""
|
|
Jednostavni zapis notifikacije vezane uz fleet objekte.
|
|
Može se koristiti za in-app obavijesti ili kao log koji je poslan.
|
|
"""
|
|
LEVEL_CHOICES = [
|
|
('info', 'Info'),
|
|
('warning', 'Upozorenje'),
|
|
('critical', 'Kritično'),
|
|
]
|
|
|
|
recipient = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
null=True,
|
|
blank=True,
|
|
on_delete=models.SET_NULL,
|
|
related_name='vehicle_notifications',
|
|
verbose_name=_("Primatelj")
|
|
)
|
|
title = models.CharField(max_length=255, verbose_name=_("Naslov"))
|
|
message = models.TextField(verbose_name=_("Poruka"))
|
|
metadata = models.JSONField(default=dict, blank=True, verbose_name=_("Metapodaci"))
|
|
level = models.CharField(max_length=20, choices=LEVEL_CHOICES, default='info', verbose_name=_("Razina"))
|
|
is_sent = models.BooleanField(default=False, verbose_name=_("Poslano (e-mail)"))
|
|
is_read = models.BooleanField(default=False, verbose_name=_("Pročitano"))
|
|
|
|
class Meta:
|
|
verbose_name = _("Notifikacija za vozilo")
|
|
verbose_name_plural = _("Notifikacije za vozila")
|
|
ordering = ['-created_at']
|
|
|
|
def __str__(self):
|
|
return f"{self.title} -> {self.recipient} ({self.level})" |