add from v002

This commit is contained in:
2026-05-20 21:24:00 +02:00
parent 1a6ba86d0b
commit 83fb8c69bb
98 changed files with 10878 additions and 0 deletions

View File

@@ -0,0 +1,220 @@
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}"