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}"