prije gemini provjere
This commit is contained in:
0
001.BACKEND/kupci/__init__.py
Normal file
0
001.BACKEND/kupci/__init__.py
Normal file
41
001.BACKEND/kupci/admin.py
Normal file
41
001.BACKEND/kupci/admin.py
Normal file
@@ -0,0 +1,41 @@
|
||||
from django.contrib import admin
|
||||
from .models import Kupac
|
||||
|
||||
@admin.register(Kupac)
|
||||
class KupacAdmin(admin.ModelAdmin):
|
||||
# Polja koja se vide u glavnoj tablici
|
||||
list_display = ('naziv', 'oib', 'grad', 'telefon', 'prikaz_broja_strojeva', 'aktivno')
|
||||
|
||||
# Brzo uređivanje "aktivno" statusa direktno iz tablice
|
||||
list_editable = ('aktivno',)
|
||||
|
||||
# Filtriranje s desne strane
|
||||
list_filter = ('tip', 'aktivno', 'grad')
|
||||
|
||||
# Pretraga po ključnim poljima
|
||||
# OBAVEZNO dodaj search_fields kako bi autocomplete radio u fleet i nalozi
|
||||
search_fields = ('naziv', 'oib', 'email', 'grad')
|
||||
|
||||
# Organizacija forme za unos
|
||||
fieldsets = (
|
||||
('Osnovni podaci', {
|
||||
'fields': (('naziv', 'tip'), 'oib')
|
||||
}),
|
||||
('Kontakt i lokacija', {
|
||||
'fields': ('email', 'telefon', 'adresa', ('grad', 'postanski_broj'))
|
||||
}),
|
||||
('Dodatno', {
|
||||
'fields': ('napomena', 'aktivno'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
# Čitanje sistemskih podataka bez mogućnosti izmjene
|
||||
readonly_fields = ('datum_kreiranja',)
|
||||
|
||||
def prikaz_broja_strojeva(self, obj):
|
||||
"""Prikazuje broj strojeva direktno u listi kupaca"""
|
||||
# Koristimo property koji si već definirao u modelu
|
||||
return obj.broj_strojeva
|
||||
|
||||
prikaz_broja_strojeva.short_description = 'Strojevi'
|
||||
5
001.BACKEND/kupci/apps.py
Normal file
5
001.BACKEND/kupci/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class KupciConfig(AppConfig):
|
||||
name = 'kupci'
|
||||
0
001.BACKEND/kupci/management/__init__.py
Normal file
0
001.BACKEND/kupci/management/__init__.py
Normal file
0
001.BACKEND/kupci/management/commands/__init__.py
Normal file
0
001.BACKEND/kupci/management/commands/__init__.py
Normal file
8
001.BACKEND/kupci/management/commands/hello_world.py
Normal file
8
001.BACKEND/kupci/management/commands/hello_world.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from typing import Any
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
||||
def handle(self, *args: Any, **options: Any):
|
||||
print("hello world")
|
||||
75
001.BACKEND/kupci/management/commands/populate_kupci.py
Normal file
75
001.BACKEND/kupci/management/commands/populate_kupci.py
Normal file
@@ -0,0 +1,75 @@
|
||||
import random
|
||||
from django.core.management.base import BaseCommand
|
||||
from faker import Faker
|
||||
from kupci.models import Kupac
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Popunjava bazu podataka s testnim kupcima spremnim za logistički modul'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'total',
|
||||
type=int,
|
||||
help='Broj kupaca koje želiš kreirati',
|
||||
default=10
|
||||
)
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
total = kwargs['total']
|
||||
fake = Faker(['hr_HR'])
|
||||
|
||||
self.stdout.write(self.style.MIGRATE_HEADING(f'🚀 Kreiram {total} testnih kupaca...'))
|
||||
|
||||
# Kontrolirani skup gradova u nominativu za savršen ispis relacija i odredišta u logistici
|
||||
hr_gradovi = [
|
||||
{'grad': 'Zagreb', 'pbr': '10000'},
|
||||
{'grad': 'Karlovac', 'pbr': '47000'},
|
||||
{'grad': 'Ozalj', 'pbr': '47280'},
|
||||
{'grad': 'Split', 'pbr': '21000'},
|
||||
{'grad': 'Rijeka', 'pbr': '51000'},
|
||||
{'grad': 'Osijek', 'pbr': '31000'},
|
||||
{'grad': 'Varaždin', 'pbr': '42000'},
|
||||
{'grad': 'Zadar', 'pbr': '23000'},
|
||||
{'grad': 'Velika Gorica', 'pbr': '10410'},
|
||||
{'grad': 'Sisak', 'pbr': '44000'}
|
||||
]
|
||||
|
||||
created_count = 0
|
||||
for _ in range(total):
|
||||
tip = random.choice(['pravno', 'fizicko'])
|
||||
|
||||
if tip == 'pravno':
|
||||
naziv = fake.company()
|
||||
else:
|
||||
naziv = fake.name()
|
||||
|
||||
# Generiramo OIB (točno 11 znamenki u string formatu)
|
||||
oib = "".join([str(random.randint(0, 9)) for _ in range(11)])
|
||||
|
||||
# Odabiremo grad i poštanski broj u paru kako bi podaci imali smisla
|
||||
lokacija = random.choice(hr_gradovi)
|
||||
|
||||
# Generiramo telefonski broj koji je čist i siguran za tel: linkove u Astru
|
||||
pozivni = random.randint(1, 9)
|
||||
telefon = f"+385 {pozivni} {random.randint(100, 999)}-{random.randint(1000, 9999)}"
|
||||
|
||||
try:
|
||||
# Upotrebljavamo create, a jedinstvenost OIB-a hvatamo u iznimci
|
||||
Kupac.objects.create(
|
||||
naziv=naziv,
|
||||
oib=oib,
|
||||
email=fake.email(),
|
||||
telefon=telefon,
|
||||
adresa=fake.street_address(),
|
||||
grad=lokacija['grad'], # Usklađeno: čisti nominativ (npr. 'Karlovac')
|
||||
postanski_broj=lokacija['pbr'], # Usklađeno: odgovarajući poštanski broj
|
||||
tip=tip,
|
||||
napomena="Automatski generiran testni kupac usklađen s modulom operativne logistike.",
|
||||
aktivno=True
|
||||
)
|
||||
created_count += 1
|
||||
except Exception as e:
|
||||
# Ako OIB slučajno kolidira (unutar unique=True), preskoči i nastavi generirati sljedećeg
|
||||
continue
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(f'✅ Uspješno dodano {created_count} novih kupaca u bazu!'))
|
||||
36
001.BACKEND/kupci/migrations/0001_initial.py
Normal file
36
001.BACKEND/kupci/migrations/0001_initial.py
Normal file
@@ -0,0 +1,36 @@
|
||||
# Generated by Django 6.0.5 on 2026-05-22 03:52
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Kupac',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('naziv', models.CharField(help_text='Ime i prezime ili puni naziv tvrtke', max_length=255)),
|
||||
('oib', models.CharField(blank=True, max_length=11, null=True, unique=True, verbose_name='OIB')),
|
||||
('email', models.EmailField(blank=True, max_length=254)),
|
||||
('telefon', models.CharField(blank=True, max_length=50)),
|
||||
('adresa', models.CharField(blank=True, max_length=255)),
|
||||
('grad', models.CharField(blank=True, max_length=100)),
|
||||
('postanski_broj', models.CharField(blank=True, max_length=10)),
|
||||
('tip', models.CharField(choices=[('pravno', 'Pravna osoba (Tvrtka)'), ('fizicko', 'Fizička osoba')], default='pravno', max_length=10)),
|
||||
('napomena', models.TextField(blank=True, help_text='Interni podaci o kupcu')),
|
||||
('datum_kreiranja', models.DateTimeField(auto_now_add=True)),
|
||||
('aktivno', models.BooleanField(default=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Kupac',
|
||||
'verbose_name_plural': 'Kupci',
|
||||
'ordering': ['naziv'],
|
||||
},
|
||||
),
|
||||
]
|
||||
0
001.BACKEND/kupci/migrations/__init__.py
Normal file
0
001.BACKEND/kupci/migrations/__init__.py
Normal file
49
001.BACKEND/kupci/models.py
Normal file
49
001.BACKEND/kupci/models.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from django.db import models
|
||||
|
||||
class Kupac(models.Model):
|
||||
TIP_KUPCA = [
|
||||
('pravno', 'Pravna osoba (Tvrtka)'),
|
||||
('fizicko', 'Fizička osoba'),
|
||||
]
|
||||
|
||||
naziv = models.CharField(
|
||||
max_length=255,
|
||||
help_text="Ime i prezime ili puni naziv tvrtke"
|
||||
)
|
||||
oib = models.CharField(
|
||||
max_length=11,
|
||||
unique=True,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="OIB"
|
||||
)
|
||||
email = models.EmailField(blank=True)
|
||||
telefon = models.CharField(max_length=50, blank=True)
|
||||
adresa = models.CharField(max_length=255, blank=True)
|
||||
grad = models.CharField(max_length=100, blank=True)
|
||||
postanski_broj = models.CharField(max_length=10, blank=True)
|
||||
|
||||
tip = models.CharField(
|
||||
max_length=10,
|
||||
choices=TIP_KUPCA,
|
||||
default='pravno'
|
||||
)
|
||||
|
||||
napomena = models.TextField(blank=True, help_text="Interni podaci o kupcu")
|
||||
|
||||
# Sistemski podaci
|
||||
datum_kreiranja = models.DateTimeField(auto_now_add=True)
|
||||
aktivno = models.BooleanField(default=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Kupac"
|
||||
verbose_name_plural = "Kupci"
|
||||
ordering = ['naziv']
|
||||
|
||||
def __str__(self):
|
||||
return self.naziv
|
||||
|
||||
@property
|
||||
def broj_strojeva(self):
|
||||
"""Vraća broj strojeva povezanih s ovim kupcem"""
|
||||
return self.strojevi.count()
|
||||
73
001.BACKEND/kupci/serializers.py
Normal file
73
001.BACKEND/kupci/serializers.py
Normal file
@@ -0,0 +1,73 @@
|
||||
from rest_framework import serializers
|
||||
from .models import Kupac
|
||||
from fleet.models import Stroj
|
||||
|
||||
from fleet.serializers import StrojSerializer
|
||||
|
||||
class KupacSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serializer za listu kupaca.
|
||||
Fokus je na brzini i osnovnim kontaktnim podacima.
|
||||
"""
|
||||
# Izračunato polje iz modela (@property broj_strojeva)
|
||||
broj_strojeva = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Kupac
|
||||
fields = [
|
||||
'id',
|
||||
'naziv',
|
||||
'grad',
|
||||
'telefon',
|
||||
'email',
|
||||
'tip',
|
||||
'aktivno',
|
||||
'broj_strojeva' # Korisno za prikaz u tablici (npr. "5 strojeva")
|
||||
]
|
||||
|
||||
class KupacDetaljiSerializer(serializers.ModelSerializer):
|
||||
# Ugniježđeni strojevi - prikazujemo puni objekt, ne samo ID
|
||||
strojevi = StrojSerializer(many=True, read_only=True)
|
||||
|
||||
# Dodatna polja koja nisu u bazi, ali su korisna za Astro frontend
|
||||
ukupan_broj_strojeva = serializers.IntegerField(source='broj_strojeva', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Kupac
|
||||
fields = [
|
||||
'id',
|
||||
'naziv',
|
||||
'oib',
|
||||
'email',
|
||||
'telefon',
|
||||
'adresa',
|
||||
'grad',
|
||||
'postanski_broj',
|
||||
'tip',
|
||||
'napomena',
|
||||
'aktivno',
|
||||
'datum_kreiranja',
|
||||
'ukupan_broj_strojeva',
|
||||
'strojevi' # Lista svih strojeva s punim detaljima
|
||||
]
|
||||
|
||||
class KupacListaSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Optimizirani serializer za prikaz u tablici (listi).
|
||||
Vraća samo osnovne podatke potrebne za pregled svih klijenata.
|
||||
"""
|
||||
# Dohvaćamo property iz modela koji broji strojeve
|
||||
broj_strojeva = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Kupac
|
||||
fields = [
|
||||
'id',
|
||||
'naziv',
|
||||
'grad',
|
||||
'oib',
|
||||
'telefon',
|
||||
'tip',
|
||||
'aktivno',
|
||||
'broj_strojeva'
|
||||
]
|
||||
3
001.BACKEND/kupci/tests.py
Normal file
3
001.BACKEND/kupci/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
14
001.BACKEND/kupci/urls.py
Normal file
14
001.BACKEND/kupci/urls.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from .views import KupacViewSet
|
||||
|
||||
# Kreiramo router i registriramo naš ViewSet
|
||||
# 'r' ispred stringa označava raw string (dobra praksa za regex/putanje)
|
||||
router = DefaultRouter()
|
||||
router.register(r'svi', KupacViewSet, basename='kupac')
|
||||
|
||||
# Definiramo URL obrasce
|
||||
urlpatterns = [
|
||||
# Sve rute koje router generira (npr. /api/kupci/ i /api/kupci/{id}/)
|
||||
path('', include(router.urls)),
|
||||
]
|
||||
35
001.BACKEND/kupci/views.py
Normal file
35
001.BACKEND/kupci/views.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from rest_framework import viewsets, filters
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from .models import Kupac
|
||||
from .serializers import KupacSerializer, KupacDetaljiSerializer, KupacListaSerializer
|
||||
|
||||
class KupacViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
API endpoint koji omogućuje pregled, pretragu i uređivanje kupaca.
|
||||
"""
|
||||
# Optimizacija: prefetch_related smanjuje broj upita jer odmah vuče strojeve
|
||||
queryset = Kupac.objects.all().prefetch_related('strojevi')
|
||||
serializer_class = KupacSerializer
|
||||
|
||||
# Dodajemo filter backende
|
||||
filter_backends = [
|
||||
DjangoFilterBackend,
|
||||
filters.SearchFilter,
|
||||
filters.OrderingFilter
|
||||
]
|
||||
|
||||
# 1. Točno filtriranje (npr. ?tip=pravno)
|
||||
filterset_fields = ['tip', 'aktivno', 'grad']
|
||||
|
||||
# 2. Pretraga (npr. ?search=gradnja)
|
||||
# Pretražuje naziv ili OIB
|
||||
search_fields = ['naziv', 'oib']
|
||||
|
||||
# 3. Sortiranje (npr. ?ordering=-datum_kreiranja)
|
||||
ordering_fields = ['naziv', 'datum_kreiranja']
|
||||
ordering = ['naziv'] # Defaultno sortiranje
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == 'retrieve': # Ako se gleda pojedinačni kupac
|
||||
return KupacDetaljiSerializer # Onaj s ugniježđenim strojevima
|
||||
return KupacListaSerializer # Obični bez strojeva
|
||||
Reference in New Issue
Block a user