Files
003-fleet-management/001.BACKEND/operations/models.py
2026-05-20 21:24:00 +02:00

220 lines
7.7 KiB
Python

import os
from PIL import Image
import io
from django.db import models, transaction
from django.conf import settings
from django.utils import timezone
from django.core.files.base import ContentFile
from django.core.exceptions import ValidationError
# --- FUNKCIJE ZA DINAMIČKE PUTANJE ---
def putanja_slike_naloga(instance, filename):
# Kreira putanju: radni-nalozi/broj_naloga/originalni_naziv.ekstenzija
return f'radni-nalozi/{instance.broj_naloga}/{filename}'
def putanja_galerije_naloga(instance, filename):
# instance je objekt RadniNalogSlika koji ima ForeignKey na radni_nalog
# Sprema u isti folder gdje je i glavna slika kvara
return f'radni-nalozi/{instance.radni_nalog.broj_naloga}/galerija/{filename}'
class PutniNalog(models.Model):
class StatusChoices(models.TextChoices):
OTVOREN = 'OTVOREN', 'Otvoren'
ZAVRSEN = 'ZAVRSEN', 'Završen'
broj_naloga = models.CharField(max_length=50, unique=True)
datum_izdavanja = models.DateField(default=timezone.now)
vozilo = models.ForeignKey(
'fleet.Vozilo',
on_delete=models.PROTECT,
related_name='putni_nalozi',
verbose_name="Službeno vozilo"
)
korisnik = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
relacija = models.CharField(max_length=255)
mjesto_odredista = models.CharField(max_length=255)
pocetna_km = models.PositiveIntegerField(
editable=False,
help_text="Automatski preuzeto iz modela Vozilo"
)
zavrsna_km = models.PositiveIntegerField(null=True, blank=True)
vrijeme_polaska = models.DateTimeField(default=timezone.now)
vrijeme_povratka = models.DateTimeField(null=True, blank=True)
status = models.CharField(
max_length=15,
choices=StatusChoices.choices,
default=StatusChoices.OTVOREN
)
class Meta:
verbose_name = "Putni nalog"
verbose_name_plural = "Putni nalozi"
def clean(self):
super().clean()
if self.status == self.StatusChoices.ZAVRSEN:
if self.zavrsna_km is None:
raise ValidationError({'zavrsna_km': "Morate unijeti završne kilometre prije zatvaranja naloga."})
otvoreni_radni_nalozi = self.radni_nalozi.exclude(status='ZAVRSENO').exists()
if otvoreni_radni_nalozi:
raise ValidationError({
'status': "Putni nalog ne može biti završen jer postoje povezani radni nalozi koji još nisu zatvoreni."
})
def save(self, *args, **kwargs):
if not self.pk:
self.pocetna_km = self.vozilo.trenutni_kilometri
self.full_clean()
if self.status == self.StatusChoices.ZAVRSEN and self.zavrsna_km:
with transaction.atomic():
super().save(*args, **kwargs)
v = self.vozilo
v.trenutni_kilometri = self.zavrsna_km
v.pocetni_kilometri = self.zavrsna_km
v.save()
else:
super().save(*args, **kwargs)
def __str__(self):
return f"{self.broj_naloga} - {self.vozilo.registracija}"
class RadniNalog(models.Model):
class StatusRadaChoices(models.TextChoices):
PLANIRANO = 'PLANIRANO', 'Planirano'
U_RADU = 'U_RADU', 'U radu'
ZAVRSENO = 'ZAVRSENO', 'Završeno'
broj_naloga = models.CharField(max_length=20, unique=True, blank=True)
stroj = models.ForeignKey(
'fleet.Stroj',
on_delete=models.CASCADE,
related_name='radni_nalozi',
verbose_name="Stroj na popravku"
)
putni_nalog = models.ForeignKey(
'PutniNalog',
on_delete=models.SET_NULL,
null=True, blank=True,
related_name='radni_nalozi'
)
klijent = models.ForeignKey(
'kupci.Kupac',
on_delete=models.PROTECT,
related_name='radni_nalozi'
)
opis_kvara = models.TextField()
izvrsitelj = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True, blank=True,
related_name='dodijeljeni_nalozi'
)
status = models.CharField(
max_length=20,
choices=StatusRadaChoices.choices,
default=StatusRadaChoices.PLANIRANO
)
datum_kreiranja = models.DateTimeField(auto_now_add=True, verbose_name="Datum kreiranja")
datum_azuriranja = models.DateTimeField(auto_now=True, verbose_name="Zadnja izmjena")
slika_kvara = models.ImageField(upload_to=putanja_slike_naloga, null=True, blank=True)
potpis_klijenta = models.ImageField(upload_to='potpisi/%Y/%m/', null=True, blank=True)
def save(self, *args, **kwargs):
# 1. AUTOMATSKO GENERIRANJE BROJA NALOGA
if not self.broj_naloga:
godina = timezone.now().year
prefix = f"RN-{godina}-"
# Koristimo atomsku transakciju i select_for_update da spriječimo
# da dva korisnika dobiju isti broj u istoj milisekundi
with transaction.atomic():
zadnji_nalog = RadniNalog.objects.select_for_update().filter(
broj_naloga__startswith=prefix
).order_by('-broj_naloga').first()
if zadnji_nalog:
try:
# RN-2026-0001 -> uzimamo "0001"
zadnji_broj_str = zadnji_nalog.broj_naloga.split('-')[-1]
novi_broj = int(zadnji_broj_str) + 1
except (ValueError, IndexError):
novi_broj = 1
else:
novi_broj = 1
self.broj_naloga = f"{prefix}{novi_broj:04d}"
# 2. OBRADA SLIKE
if self.slika_kvara:
try:
self.slika_kvara = self.rescale_image(self.slika_kvara)
except Exception as e:
print(f"Greška pri obradi slike: {e}")
# 3. PRIMARNO SPREMANJE
super().save(*args, **kwargs)
# 4. LOGIKA ZA PUTNI NALOG
if self.putni_nalog and self.status == self.StatusRadaChoices.ZAVRSENO:
pn = self.putni_nalog
# Provjeravamo ima li nalog još uvijek otvorenih radnih naloga
ostali_otvoreni = pn.radni_nalozi.exclude(
id=self.id, # Isključujemo trenutni nalog jer je on upravo završen
status=self.StatusRadaChoices.ZAVRSENO
).exists()
if not ostali_otvoreni and pn.zavrsna_km:
pn.status = 'ZAVRSEN'
pn.save()
def rescale_image(self, image_field):
img = Image.open(image_field)
if img.height > 1200 or img.width > 1200:
output_size = (1200, 1200)
img.thumbnail(output_size)
img_io = io.BytesIO()
img_format = img.format if img.format else 'JPEG'
img.save(img_io, format=img_format, quality=90)
return ContentFile(img_io.getvalue(), name=image_field.name)
return image_field
class Meta:
verbose_name = "Radni nalog"
verbose_name_plural = "Radni nalozi"
def __str__(self):
return f"RN-{self.broj_naloga} | {self.stroj.naziv}"
class RadniNalogSlika(models.Model):
radni_nalog = models.ForeignKey(
RadniNalog,
related_name='slike',
on_delete=models.CASCADE
)
# POPRAVLJENO: uklonjeni navodnici oko funkcije
slika = models.ImageField(upload_to=putanja_galerije_naloga, verbose_name="Slika")
opis = models.CharField(max_length=100, blank=True, verbose_name="Kratki opis slike")
class Meta:
verbose_name = "Dodatna slika naloga"
verbose_name_plural = "Dodatne slike naloga"
def __str__(self):
return f"Slika za nalog {self.radni_nalog.broj_naloga}"