220 lines
7.7 KiB
Python
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}" |