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 class BrojacSekvence(models.Model): """ Tablica koja pamti isključivo zadnji iskorišteni broj za bilo koji entitet. Broj se ovdje samo povećava i nikada se ne briše. """ naziv_sekvence = models.CharField(max_length=50, unique=True) # npr. 'radni_nalog_2026' zadnji_broj = models.PositiveIntegerField(default=0) class Meta: verbose_name = "Brojač sekvence" verbose_name_plural = "Brojači sekvenci" def __str__(self): return f"{self.naziv_sekvence}: {self.zadnji_broj}" # --- FUNKCIJE ZA DINAMIČKE PUTANJE --- def putanja_slike_naloga(instance, filename): # Čistimo broj naloga od duplih oznaka za putanju direktorija broj = instance.broj_naloga.replace('RN-', '') if instance.broj_naloga else 'nepoznato' return f'radni-nalozi/RN-{broj}/{filename}' def putanja_galerije_naloga(instance, filename): broj = instance.radni_nalog.broj_naloga.replace('RN-', '') if instance.radni_nalog.broj_naloga else 'nepoznato' return f'radni-nalozi/RN-{broj}/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šen' 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): if self.broj_naloga == "": self.broj_naloga = None # 1. AUTOMATSKO GENERIRANJE BROJA NALOGA KROZ SEKVENCIJSKU TABLICU if not self.broj_naloga: godina = timezone.now().year kljuc_sekvence = f"radni_nalog_{godina}" prefix = f"RN-{godina}-" # Atomska transakcija zaključava samo brojač, sprječavajući dupliranje with transaction.atomic(): # Dohvati ili kreiraj brojač za tekuću godinu i zaključaj redak za ažuriranje brojac, created = BrojacSekvence.objects.select_for_update().get_or_create( naziv_sekvence=kljuc_sekvence ) # Ako je brojač tek kreiran, a u bazi već imaš povijest (npr. stigao si do 25), # radimo automatsku sinkronizaciju da ne krene od 1 ako tablica nije prazna if created: zadnji_u_bazi = RadniNalog.objects.filter(broj_naloga__startswith=prefix).order_by('-broj_naloga').first() if zadnji_u_bazi: try: brojac.zadnji_broj = int(zadnji_u_bazi.broj_naloga.split('-')[-1]) except (ValueError, IndexError): brojac.zadnji_broj = 0 # Uvećaj sekvencu za 1 (Čak i ako se nalozi obrišu, ovaj broj samo raste!) brojac.zadnji_broj += 1 brojac.save() # Dodijeli savršeno formatiran broj self.broj_naloga = f"{prefix}{brojac.zadnji_broj:04d}" else: if self.broj_naloga.startswith("RN-RN-"): self.broj_naloga = self.broj_naloga.replace("RN-RN-", "RN-") # 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 ostali_otvoreni = pn.radni_nalozi.exclude( id=self.id, 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): # POPRAVLJENO: Budući da polje broj_naloga već u bazi sadrži "RN-2026-0004", # ovdje izbacujemo fiksni prefiks "RN-" kako se ne bi duplirao pri pretvaranju objekta u string! return f"{self.broj_naloga} | {self.stroj.naziv}" class RadniNalogSlika(models.Model): radni_nalog = models.ForeignKey( RadniNalog, related_name='slike', on_delete=models.CASCADE ) 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}"