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

View File

@@ -0,0 +1,90 @@
from django.contrib import admin
from django.urls import reverse
from django.utils.safestring import mark_safe
from .models import Stroj, Vozilo
from operations.models import RadniNalog, PutniNalog # Importamo modele iz druge aplikacije
# --- INLINE KLASE ---
class PutniNalogInline(admin.TabularInline):
"""Prikazuje putne naloge unutar pregleda vozila."""
model = PutniNalog
extra = 0
fields = ('broj_naloga', 'datum_izdavanja', 'korisnik', 'pocetna_km', 'zavrsna_km', 'status')
readonly_fields = ('broj_naloga', 'datum_izdavanja', 'korisnik', 'pocetna_km', 'zavrsna_km', 'status')
can_delete = False
verbose_name = "Povijest putovanja"
verbose_name_plural = "Povijest putovanja za ovo vozilo"
class RadniNalogInline(admin.TabularInline):
model = RadniNalog
extra = 0
# Umjesto običnog polja 'broj_naloga', koristimo metodu 'link_na_nalog'
fields = ('link_na_nalog', 'datum_kreiranja', 'klijent', 'izvrsitelj', 'status')
readonly_fields = ('link_na_nalog', 'datum_kreiranja', 'klijent', 'izvrsitelj', 'status')
can_delete = False
def link_na_nalog(self, obj):
if obj.id:
# Generiramo URL za admin promjenu Radnog Naloga
url = reverse('admin:operations_radninalog_change', args=[obj.id])
return mark_safe(f'<a href="{url}" style="font-weight:bold;">RN-{obj.broj_naloga}</a>')
return "-"
link_na_nalog.short_description = 'Broj naloga'
# --- GLAVNE ADMIN KLASE ---
@admin.register(Vozilo)
class VoziloAdmin(admin.ModelAdmin):
list_display = ('naziv', 'registracija', 'status', 'pocetni_kilometri', 'trenutni_kilometri')
search_fields = ('naziv', 'registracija')
list_filter = ('status',)
ordering = ('naziv',)
# Dodajemo inline za putne naloge
inlines = [PutniNalogInline]
@admin.register(Stroj)
class StrojAdmin(admin.ModelAdmin):
list_display = (
'serijski_broj',
'prikaz_naziva',
'vlasnik',
'tip',
'radni_sati',
'registracija',
'datum_zadnjeg_atesta'
)
search_fields = (
'serijski_broj',
'naziv',
'marka',
'model_stroja',
'vlasnik__naziv',
'vlasnik__oib'
)
list_filter = ('tip', 'marka', 'vlasnik')
autocomplete_fields = ['vlasnik']
# Dodajemo inline za radne naloge
inlines = [RadniNalogInline]
fieldsets = (
('Osnovne informacije', {
'fields': ('vlasnik', 'naziv', 'tip', 'serijski_broj')
}),
('Detalji o modelu', {
'fields': (('marka', 'model_stroja'), 'godina_proizvodnje')
}),
('Tehnički podaci i održavanje', {
'fields': ('radni_sati', 'registracija', 'datum_zadnjeg_atesta'),
'classes': ('collapse',)
}),
)
def prikaz_naziva(self, obj):
return f"{obj.marka} {obj.model_stroja}".strip() or obj.naziv
prikaz_naziva.short_description = 'Marka i Model'

View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class FleetConfig(AppConfig):
name = 'fleet'

View File

@@ -0,0 +1,86 @@
import random
from datetime import date, timedelta
from django.core.management.base import BaseCommand
from faker import Faker
from fleet.models import Stroj
from kupci.models import Kupac
class Command(BaseCommand):
help = 'Popunjava bazu podataka isključivo Liebherr strojevima koristeći Faker'
def add_arguments(self, parser):
parser.add_argument(
'total',
type=int,
help='Broj Liebherr strojeva koje želiš kreirati',
default=10
)
def handle(self, *args, **kwargs):
total = kwargs['total']
fake = Faker(['hr_HR']) # Koristimo hrvatski lokalitet
kupci = list(Kupac.objects.all())
if not kupci:
self.stdout.write(self.style.ERROR("Greška: Nema kupaca u bazi. Prvo pokreni populate_kupci!"))
return
# Katalog usklađen s tvojim TIP_STROJA choices
liebherr_katalog = {
'dizalica_toranj': [
'132 EC-H 8 Litronic',
'280 EC-H 12',
'81 K.1',
'172 EC-B 8'
],
'dizalica_auto': [
'LTM 1030-2.1',
'LTM 1050-3.1',
'LTM 1060-3.1',
'LTM 1120-4.1',
'LTC 1050-3.1'
]
}
self.stdout.write(self.style.MIGRATE_HEADING(f'🚀 Generiram {total} Liebherr strojeva...'))
created_count = 0
for _ in range(total):
# Odabir tipa (usklađeno s tvojim modelom)
tip = random.choice(['dizalica_toranj', 'dizalica_auto'])
model_naziv = random.choice(liebherr_katalog[tip])
# Faker za generiranje serijskog broja i registracije
serijski = f"LE-{fake.bothify(text='######')}" # npr. LE-482931
# Registracija samo za autodizalice (Faker format)
reg = None
if tip == 'dizalica_auto':
# Generira format sličan ZG-1234-LH
reg = f"ZG-{fake.numerify('####')}-LH"
# Radni sati
sati = random.uniform(500.0, 12000.0) if tip == 'dizalica_toranj' else random.uniform(100.0, 5000.0)
# Faker za datume (unutar zadnjih godinu dana)
datum_atesta = fake.date_between(start_date='-1y', end_date='today')
try:
Stroj.objects.create(
vlasnik=random.choice(kupci),
naziv=f"Liebherr {model_naziv}",
serijski_broj=serijski,
marka='Liebherr',
model_stroja=model_naziv,
godina_proizvodnje=random.randint(2015, 2024),
tip=tip,
radni_sati=round(sati, 2),
registracija=reg,
datum_zadnjeg_atesta=datum_atesta
)
created_count += 1
except Exception as e:
self.stderr.write(f"Greška kod S/N {serijski}: {e}")
continue
self.stdout.write(self.style.SUCCESS(f'✅ Uspješno dodano {created_count} Liebherr strojeva!'))

View File

@@ -0,0 +1,71 @@
import random
from django.core.management.base import BaseCommand
from django.core.exceptions import ValidationError
from faker import Faker
from fleet.models import Vozilo
class Command(BaseCommand):
help = 'Popunjava bazu podataka s gospodarskim vozilima koristeći Faker'
def add_arguments(self, parser):
parser.add_argument(
'total',
type=int,
help='Broj vozila koje želiš kreirati',
default=5
)
def handle(self, *args, **kwargs):
total = kwargs['total']
fake = Faker(['hr_HR'])
# Modeli koji se pojavljuju u tvom radnom nalogu (CSV)
modeli_podaci = [
'VW Transporter T6',
'Mercedes-Benz Vito',
'Citroen Jumper',
'Ford Transit Custom',
'Renault Kangoo Maxi',
'Peugeot Boxer',
'Opel Vivaro',
]
self.stdout.write(self.style.MIGRATE_HEADING(f'🚀 Kreiram {total} vozila u flotu...'))
brojac = 0
while brojac < total:
model_naziv = random.choice(modeli_podaci)
# Generiranje registracije koristeći Faker numerify/bothify
# Format: ZG 1234 AB
gradovi = ['ZG', 'ST', 'RI', 'OS', 'VZ', 'PU', 'KA', 'KC']
grad = random.choice(gradovi)
reg_oznaka = fake.numerify(text='####') # npr. 6732
reg_slova = fake.bothify(text='??', letters='ABCDEFGHIJKLMNOPQRSTUVWXYZ') # npr. II
registracija = f"{grad}{reg_oznaka}{reg_slova}"
# Kilometri: Koristimo tvoju logiku da trenutni budu veći od početnih
pocetni = random.randint(0, 150000)
dodatni = random.randint(500, 20000)
trenutni = pocetni + dodatni
try:
# Koristimo registraciju u nazivu radi lakšeg prepoznavanja u Astro frontendu
vozilo = Vozilo.objects.create(
naziv=f"{model_naziv} ({grad})",
registracija=registracija,
pocetni_kilometri=pocetni,
trenutni_kilometri=trenutni,
status=random.choice(['aktivan', 'aktivan', 'servis', 'neaktivan'])
)
brojac += 1
self.stdout.write(f" ✅ Kreirano: {vozilo.naziv} [{vozilo.registracija}]")
except ValidationError:
# Ako Faker pogodi postojeću registraciju, samo nastavi dalje
continue
except Exception as e:
self.stderr.write(f" ❌ Greška: {e}")
break
self.stdout.write(self.style.SUCCESS(f'\nGotovo! Uspješno dodano {brojac} vozila u flotu.'))

View File

@@ -0,0 +1,88 @@
from django.db import models
from django.core.exceptions import ValidationError
class Vozilo(models.Model):
STATUS_CHOICES = [
('aktivan', 'Aktivan'),
('servis', 'Na Servisu'),
('neaktivan', 'Izvan Pogona'),
]
naziv = models.CharField(max_length=50, verbose_name="Interni naziv")
registracija = models.CharField(max_length=15, unique=True, db_index=True)
# Kilometri
pocetni_kilometri = models.PositiveIntegerField(default=0)
trenutni_kilometri = models.PositiveIntegerField(default=0)
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default='aktivan'
)
class Meta:
verbose_name = "Vozilo"
verbose_name_plural = "Vozila"
ordering = ['naziv']
def clean(self):
# Osnovna logika: trenutni kilometri ne smiju biti manji od početnih
if self.trenutni_kilometri < self.pocetni_kilometri:
raise ValidationError({
'trenutni_kilometri': "Trenutni kilometri ne mogu biti manji od početnih."
})
def save(self, *args, **kwargs):
# Logika za nove zapise
if not self.pk:
self.trenutni_kilometri = self.pocetni_kilometri
self.registracija = self.registracija.upper().replace(" ", "")
# Pozivamo clean() prije spremanja (Django to ne radi automatski u save())
self.full_clean()
super().save(*args, **kwargs)
def __str__(self):
return f"{self.naziv} ({self.registracija})"
class Stroj(models.Model):
TIP_STROJA = [
('dizalica_toranj', 'Toranjska dizalica'),
('dizalica_auto', 'Autodizalica'),
('vilicar', 'Viličar'),
('platforma', 'Radna platforma'),
]
# Poveznica s kupcem: Jedan kupac može imati mnogo strojeva
# --- RJEŠENJE ZA LOOP: Koristimo string 'kupci.Kupac' ---
vlasnik = models.ForeignKey(
'kupci.Kupac',
on_delete=models.CASCADE,
related_name='strojevi',
verbose_name="Vlasnik/Kupac"
)
naziv = models.CharField(max_length=100, help_text="Npr. Liebherr LTM 1030")
serijski_broj = models.CharField(max_length=50, unique=True)
marka = models.CharField(max_length=50, blank=True)
model_stroja = models.CharField(max_length=50, blank=True)
godina_proizvodnje = models.PositiveIntegerField(null=True, blank=True)
tip = models.CharField(max_length=30, choices=TIP_STROJA)
# Podaci za održavanje
radni_sati = models.DecimalField(max_digits=12, decimal_places=2, default=0)
registracija = models.CharField(max_length=20, blank=True, null=True, help_text="Za autodizalice i vozila")
datum_zadnjeg_atesta = models.DateField(null=True, blank=True)
class Meta:
verbose_name = "Stroj"
verbose_name_plural = "Strojevi"
ordering = ['marka', 'model_stroja']
def __str__(self):
# Mala nadogradnja da ne vraća prazan string ako marka/model nedostaju
oznaka = f"{self.marka} {self.model_stroja}".strip() or self.naziv
return f"{oznaka} (S/N: {self.serijski_broj})"

View File

@@ -0,0 +1,115 @@
from rest_framework import serializers
from .models import Stroj, Vozilo
class StrojShortSerializer(serializers.ModelSerializer):
"""
Kraća verzija serijalizatora za brze liste ili dropdown izbornike.
"""
class Meta:
model = Stroj
fields = ['id', 'naziv', 'serijski_broj', 'registracija']
class StrojSerializer(serializers.ModelSerializer):
"""
Glavni serializer za strojeve/dizalice.
"""
# Prikazuje naziv vlasnika (kupca) umjesto samo ID-a
# (source='vlasnik.naziv' jer je u modelu ForeignKey nazvan 'vlasnik')
vlasnik_naziv = serializers.ReadOnlyField(source='vlasnik.naziv')
# get_tip_display() pretvara 'dizalica_auto' u 'Autodizalica' (čitljivije za UI)
tip_human_readable = serializers.CharField(source='get_tip_display', read_only=True)
radni_nalozi = serializers.SerializerMethodField()
class Meta:
model = Stroj
fields = [
'id',
'vlasnik',
'vlasnik_naziv',
'naziv',
'marka',
'model_stroja',
'serijski_broj',
'tip',
'tip_human_readable',
'godina_proizvodnje',
'radni_sati',
'registracija',
'datum_zadnjeg_atesta',
'radni_nalozi', # Novo polje
]
def get_radni_nalozi(self, obj):
# Lokalni import rješava kružni import error s operations aplikacijom
from operations.serializers import RadniNalogSerializer
# Dohvaćamo radne naloge i sortiramo ih tako da najnoviji budu prvi
queryset = obj.radni_nalozi.all().order_by('-datum_kreiranja')
# Vraćamo serijalizirane podatke
return RadniNalogSerializer(queryset, many=True, context=self.context).data
class VoziloSerializer(serializers.ModelSerializer):
"""
Osnovni serializer koji se koristi za Create/Update operacije.
"""
class Meta:
model = Vozilo
fields = '__all__'
class VoziloListaSerializer(serializers.ModelSerializer):
"""
Serializer za brzi prikaz u tablicama.
"""
# Ljudski čitljiv status (npr. 'Na Servisu' umjesto 'servis')
status_prikaz = serializers.CharField(source='get_status_display', read_only=True)
class Meta:
model = Vozilo
fields = ['id', 'naziv', 'registracija', 'trenutni_kilometri', 'status', 'status_prikaz']
class VoziloListaSerializer(serializers.ModelSerializer):
"""
Serializer za brzi prikaz u tablicama.
"""
# Ljudski čitljiv status (npr. 'Na Servisu' umjesto 'servis')
status_prikaz = serializers.CharField(source='get_status_display', read_only=True)
class Meta:
model = Vozilo
fields = ['id', 'naziv', 'registracija', 'trenutni_kilometri', 'status', 'status_prikaz']
class VoziloDetaljiSerializer(serializers.ModelSerializer):
"""
Serializer s dodatnom statistikom za detaljni prikaz vozila.
"""
ukupno_predeno = serializers.SerializerMethodField()
status_prikaz = serializers.CharField(source='get_status_display', read_only=True)
putni_nalozi = serializers.SerializerMethodField()
class Meta:
model = Vozilo
fields = [
'id',
'naziv',
'registracija',
'pocetni_kilometri',
'trenutni_kilometri',
'ukupno_predeno',
'status',
'status_prikaz',
'putni_nalozi',
]
def get_ukupno_predeno(self, obj):
"""Izračunava razliku između trenutnih i početnih kilometara"""
return obj.trenutni_kilometri - obj.pocetni_kilometri
def get_putni_nalozi(self, obj):
from operations.serializers import PutniNalogSerializer
queryset = obj.putni_nalozi.all()
return PutniNalogSerializer(queryset, many=True, context=self.context).data

View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

11
001.BACKEND/fleet/urls.py Normal file
View File

@@ -0,0 +1,11 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import StrojViewSet, VoziloViewSet
router = DefaultRouter()
router.register(r'strojevi', StrojViewSet, basename='stroj')
router.register(r'vozila', VoziloViewSet, basename='vozilo')
urlpatterns = [
path('', include(router.urls)),
]

View File

@@ -0,0 +1,72 @@
from rest_framework import viewsets, filters
from django_filters.rest_framework import DjangoFilterBackend
from .models import Stroj, Vozilo
from .serializers import (
VoziloSerializer,
VoziloListaSerializer,
VoziloDetaljiSerializer,
StrojSerializer
)
class StrojViewSet(viewsets.ModelViewSet):
"""
ViewSet za pregled, kreiranje i uređivanje strojeva i dizalica.
"""
# select_related radi SQL JOIN i povlači podatke o kupcu odmah
queryset = Stroj.objects.all().select_related('vlasnik').prefetch_related('radni_nalozi')
serializer_class = StrojSerializer
# Dodajemo mogućnosti filtriranja, pretrage i sortiranja
filter_backends = [
DjangoFilterBackend,
filters.SearchFilter,
filters.OrderingFilter
]
# Omogućuje filtriranje strojeva po vlasniku ili tipu
# npr. /api/strojevi/?vlasnik=1 ili /api/strojevi/?tip=dizalica_auto
filterset_fields = ['vlasnik', 'tip']
# Omogućuje pretragu po nazivu, marki ili serijskom broju
search_fields = ['naziv', 'marka', 'serijski_broj', 'registracija']
# Omogućuje sortiranje po radnim satima ili godini proizvodnje
ordering_fields = ['radni_sati', 'godina_proizvodnje', 'datum_zadnjeg_atesta']
ordering = ['-datum_zadnjeg_atesta'] # Defaultno prikazuje one koji su zadnji atestirani
class VoziloViewSet(viewsets.ModelViewSet):
"""
ViewSet za pregled, kreiranje i uređivanje voznog parka.
Usklađen sa StrojViewSet arhitekturom.
"""
queryset = Vozilo.objects.all()
# Postavljanje filter backenda (isto kao u StrojViewSet)
filter_backends = [
DjangoFilterBackend,
filters.SearchFilter,
filters.OrderingFilter
]
# Omogućuje filtriranje vozila po statusu
# npr. /api/fleet/vozila/?status=u_radu
filterset_fields = ['status']
# Omogućuje pretragu po nazivu i registraciji
search_fields = ['naziv', 'registracija']
# Omogućuje sortiranje po kilometrima ili statusu
ordering_fields = ['trenutni_kilometri', 'status']
ordering = ['naziv'] # Defaultno sortiranje po abecedi naziva
def get_serializer_class(self):
"""
Dinamički odabir serializera za optimalne performanse.
"""
if self.action == 'list':
return VoziloListaSerializer
if self.action == 'retrieve':
return VoziloDetaljiSerializer
# Default za POST/PUT/PATCH
return VoziloSerializer