patch oko teksta PDFa i UI tablica
Some checks failed
ERP CI/CD Pipeline / test (push) Has been cancelled
ERP CI/CD Pipeline / Deploy (server git pull + compose) (push) Has been cancelled

This commit is contained in:
mariomitte
2026-07-12 03:25:03 +02:00
parent 3b54499b17
commit 4e3bf6dab0
40 changed files with 2149 additions and 331 deletions

View File

@@ -0,0 +1,6 @@
email,first_name,last_name,password,is_serviser,is_kupac,is_team_member,is_staff,is_superuser,is_active,telefon,oib,occupation,residence,work_position,licence_number,is_verified,client_id,client_tax_id
serviser1@example.com,Ime1,Prezime1,password123,true,false,true,false,false,true,+38591111222,12345678901,Serviser,Zagreb,Serviser dizalice,LIC-001,true,,
serviser2@example.com,Ime2,Prezime2,password123,true,false,true,false,false,true,+38591111222,12345678901,Serviser,Zagreb,Serviser dizalice,LIC-001,true,,
serviser3@example.com,Ime3,Prezime3,password123,true,false,true,false,false,true,+38591111222,12345678901,Serviser,Zagreb,Serviser dizalice,LIC-001,true,,
admin1@example.com,Admin1,Prezime1,password123,false,false,true,true,true,true,+38591111222,12345678901,Admin,Zagreb,Direktor,LIC-001,true,,
admin2@example.com,Admin2,Prezime2,password123,false,false,true,true,true,true,+38591111222,12345678901,Admin,Zagreb,Direktor,LIC-001,true,,
1 email first_name last_name password is_serviser is_kupac is_team_member is_staff is_superuser is_active telefon oib occupation residence work_position licence_number is_verified client_id client_tax_id
2 serviser1@example.com Ime1 Prezime1 password123 true false true false false true +38591111222 12345678901 Serviser Zagreb Serviser dizalice LIC-001 true
3 serviser2@example.com Ime2 Prezime2 password123 true false true false false true +38591111222 12345678901 Serviser Zagreb Serviser dizalice LIC-001 true
4 serviser3@example.com Ime3 Prezime3 password123 true false true false false true +38591111222 12345678901 Serviser Zagreb Serviser dizalice LIC-001 true
5 admin1@example.com Admin1 Prezime1 password123 false false true true true true +38591111222 12345678901 Admin Zagreb Direktor LIC-001 true
6 admin2@example.com Admin2 Prezime2 password123 false false true true true true +38591111222 12345678901 Admin Zagreb Direktor LIC-001 true

View File

@@ -0,0 +1,202 @@
import csv
from pathlib import Path
from django.contrib.auth import get_user_model
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from modules.crm.models import Client
def _to_bool(value, field_name):
if value is None or value == "":
return None
normalized = str(value).strip().lower()
if normalized in {"1", "true", "t", "yes", "y", "da"}:
return True
if normalized in {"0", "false", "f", "no", "n", "ne"}:
return False
raise CommandError(f"Neispravna boolean vrijednost za '{field_name}': {value}")
def _to_int(value, field_name):
if value is None or value == "":
return None
try:
return int(str(value).strip())
except (TypeError, ValueError) as exc:
raise CommandError(f"Neispravna brojčana vrijednost za '{field_name}': {value}") from exc
class Command(BaseCommand):
help = "Uvozi korisnike (radnike) iz CSV datoteke u CustomUser model."
expected_columns = [
"email",
"first_name",
"last_name",
"password",
"is_serviser",
"is_kupac",
"is_team_member",
"is_staff",
"is_superuser",
"is_active",
"telefon",
"oib",
"occupation",
"residence",
"work_position",
"licence_number",
"is_verified",
"client_id",
"client_tax_id",
]
def add_arguments(self, parser):
parser.add_argument("csv_path", type=str, help="Putanja do CSV datoteke.")
parser.add_argument(
"--dry-run",
action="store_true",
help="Validira i simulira import bez spremanja u bazu.",
)
parser.add_argument(
"--create-only",
action="store_true",
help="Kreira samo nove korisnike; postojeće preskače.",
)
parser.add_argument(
"--default-password",
type=str,
default="",
help="Fallback lozinka za nove korisnike ako polje 'password' nije popunjeno.",
)
def handle(self, *args, **options):
csv_path = Path(options["csv_path"]).expanduser().resolve()
dry_run = options["dry_run"]
create_only = options["create_only"]
default_password = options["default_password"]
if not csv_path.exists():
raise CommandError(f"CSV datoteka ne postoji: {csv_path}")
User = get_user_model()
created_count = 0
updated_count = 0
skipped_count = 0
with csv_path.open("r", encoding="utf-8-sig", newline="") as handle:
reader = csv.DictReader(handle)
if not reader.fieldnames:
raise CommandError("CSV nema zaglavlje (header).")
normalized_headers = [str(h).strip() for h in reader.fieldnames]
missing = [column for column in self.expected_columns if column not in normalized_headers]
if missing:
raise CommandError("CSV nema obavezna polja: " + ", ".join(missing))
row_number = 1
with transaction.atomic():
for row in reader:
row_number += 1
if not any((value or "").strip() for value in row.values()):
continue
email = (row.get("email") or "").strip().lower()
if not email:
raise CommandError(f"Red {row_number}: email je obavezan.")
user = User.objects.filter(email__iexact=email).first()
if user and create_only:
skipped_count += 1
self.stdout.write(
self.style.WARNING(
f"Red {row_number}: preskočeno (korisnik već postoji) {email}"
)
)
continue
is_create = user is None
if is_create:
password = (row.get("password") or "").strip() or default_password
if not password:
raise CommandError(
f"Red {row_number}: password je obavezan za nove korisnike "
"(ili koristite --default-password)."
)
user = User(email=email)
user.set_password(password)
else:
password = (row.get("password") or "").strip()
if password:
user.set_password(password)
client = self._resolve_client(row=row, row_number=row_number)
user.first_name = (row.get("first_name") or "").strip()
user.last_name = (row.get("last_name") or "").strip()
user.telefon = (row.get("telefon") or "").strip()
user.oib = (row.get("oib") or "").strip() or None
user.occupation = (row.get("occupation") or "").strip()
user.residence = (row.get("residence") or "").strip()
user.work_position = (row.get("work_position") or "").strip()
user.licence_number = (row.get("licence_number") or "").strip() or None
for bool_field in [
"is_serviser",
"is_kupac",
"is_team_member",
"is_staff",
"is_superuser",
"is_active",
"is_verified",
]:
parsed = _to_bool(row.get(bool_field), bool_field)
if parsed is not None:
setattr(user, bool_field, parsed)
user.client_profile = client
user.username = None
user.full_clean()
if not dry_run:
user.save()
if is_create:
created_count += 1
else:
updated_count += 1
self.stdout.write(
self.style.SUCCESS(
f"Red {row_number}: {'kreiran' if is_create else 'ažuriran'} korisnik {email}"
)
)
if dry_run:
transaction.set_rollback(True)
summary = (
f"Import korisnika završen. created={created_count}, updated={updated_count}, "
f"skipped={skipped_count}, dry_run={dry_run}"
)
self.stdout.write(self.style.SUCCESS(summary))
def _resolve_client(self, *, row, row_number):
client_id = _to_int(row.get("client_id"), "client_id")
client_tax_id = (row.get("client_tax_id") or "").strip()
if client_id:
client = Client.objects.filter(id=client_id).first()
if not client:
raise CommandError(f"Red {row_number}: klijent s ID={client_id} ne postoji.")
return client
if client_tax_id:
client = Client.objects.filter(tax_id=client_tax_id).first()
if not client:
raise CommandError(f"Red {row_number}: klijent s tax_id={client_tax_id} ne postoji.")
return client
return None

View File

@@ -1,5 +1,4 @@
from rest_framework import serializers
from django.core.exceptions import ObjectDoesNotExist
from .models import CustomUser
class UserSerializer(serializers.ModelSerializer):
@@ -14,11 +13,8 @@ class UserSerializer(serializers.ModelSerializer):
'first_name': {'required': True, 'allow_blank': False}
}
def _get_assigned_asset(self, obj):
try:
return obj.assigned_vehicles
except ObjectDoesNotExist:
return None
def _get_assigned_assets(self, obj):
return obj.assigned_vehicles.all()
def _serialize_asset(self, vehicle):
if not vehicle:
@@ -34,10 +30,14 @@ class UserSerializer(serializers.ModelSerializer):
}
def get_assigned_vehicle(self, obj):
return self._serialize_asset(self._get_assigned_asset(obj))
assigned = self._get_assigned_assets(obj)
vehicle = assigned.filter(asset_type='vehicle').first() or assigned.first()
return self._serialize_asset(vehicle)
def get_assigned_crane(self, obj):
return self._serialize_asset(self._get_assigned_asset(obj))
assigned = self._get_assigned_assets(obj)
crane = assigned.filter(asset_type='crane').first() or assigned.first()
return self._serialize_asset(crane)
def validate(self, data):
"""

View File

@@ -1,4 +1,5 @@
import pytest
from django.contrib.auth.models import Group
from django.contrib.auth import get_user_model
User = get_user_model()
@@ -22,4 +23,20 @@ def test_create_superuser():
password="password123"
)
assert admin.is_superuser is True
assert admin.is_staff is True
assert admin.is_staff is True
@pytest.mark.django_db
def test_user_groups_many_to_many_relation():
user = User.objects.create_user(
email="m2m@example.com",
password="password123",
)
group_a = Group.objects.create(name="Serviseri")
group_b = Group.objects.create(name="Dispeceri")
user.groups.add(group_a, group_b)
assert user.groups.count() == 2
assert group_a.user_set.filter(id=user.id).exists()
assert group_b.user_set.filter(id=user.id).exists()