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,283 @@
import csv
from pathlib import Path
from django.contrib.auth import get_user_model
from django.core.management.base import BaseCommand, CommandError
from django.core.exceptions import ValidationError
from django.db import transaction
from modules.crm.models import Client
from modules.fleet.models import Vehicle
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 dizalice iz CSV datoteke i sprema ih u Vehicle model (asset_type=crane)."
expected_columns = [
"registration_number",
"crane_serial_number",
"make",
"model",
"year",
"vin",
"superstructure_working_hours",
"chassis_working_hours",
"current_mileage",
"service_interval_km",
"is_company_vehicle",
"client_id",
"assigned_servicer_email",
"is_active",
]
optional_client_columns = [
"client_name",
"client_type",
"client_tax_id",
"client_email",
"client_phone",
"client_address",
"client_city",
"client_postal_code",
]
def add_arguments(self, parser):
parser.add_argument(
"csv_path",
type=str,
help="Apsolutna ili relativna putanja do CSV datoteke.",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Validira CSV i ispisuje rezultat bez spremanja u bazu.",
)
parser.add_argument(
"--create-only",
action="store_true",
help="Kreira samo nove zapise; postojeće registracije se preskaču.",
)
def handle(self, *args, **options):
csv_path = Path(options["csv_path"]).expanduser().resolve()
dry_run = options["dry_run"]
create_only = options["create_only"]
if not csv_path.exists():
raise CommandError(f"CSV datoteka ne postoji: {csv_path}")
created_count = 0
updated_count = 0
skipped_count = 0
User = get_user_model()
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
registration_number = (row.get("registration_number") or "").strip()
crane_serial_number = (row.get("crane_serial_number") or "").strip()
if not registration_number:
raise CommandError(f"Red {row_number}: registration_number je obavezan.")
if not crane_serial_number:
raise CommandError(f"Red {row_number}: crane_serial_number je obavezan.")
vehicle = Vehicle.objects.filter(registration_number=registration_number).first()
if vehicle and create_only:
skipped_count += 1
self.stdout.write(
self.style.WARNING(
f"Red {row_number}: preskočeno (već postoji) {registration_number}"
)
)
continue
if vehicle is None:
vehicle = Vehicle(registration_number=registration_number)
was_created = True
else:
was_created = False
assigned_servicer = None
assigned_servicer_email = (row.get("assigned_servicer_email") or "").strip()
if assigned_servicer_email:
assigned_servicer = User.objects.filter(email=assigned_servicer_email).first()
if not assigned_servicer:
raise CommandError(
f"Red {row_number}: korisnik s emailom '{assigned_servicer_email}' ne postoji."
)
is_company_vehicle = _to_bool(row.get("is_company_vehicle"), "is_company_vehicle")
is_active = _to_bool(row.get("is_active"), "is_active")
if is_company_vehicle is None:
is_company_vehicle = True
client = self._resolve_or_create_client(
row=row,
row_number=row_number,
is_company_vehicle=is_company_vehicle,
)
vehicle.asset_type = "crane"
vehicle.crane_serial_number = crane_serial_number
vehicle.make = (row.get("make") or "").strip()
vehicle.model = (row.get("model") or "").strip()
vehicle.year = _to_int(row.get("year"), "year")
vehicle.vin = (row.get("vin") or "").strip() or None
vehicle.superstructure_working_hours = _to_int(
row.get("superstructure_working_hours"),
"superstructure_working_hours",
) or 0
vehicle.chassis_working_hours = _to_int(
row.get("chassis_working_hours"),
"chassis_working_hours",
) or 0
vehicle.current_mileage = _to_int(row.get("current_mileage"), "current_mileage") or 0
vehicle.service_interval_km = _to_int(
row.get("service_interval_km"),
"service_interval_km",
) or 15000
if is_company_vehicle is not None:
vehicle.is_company_vehicle = is_company_vehicle
vehicle.client = client
vehicle.assigned_servicer = assigned_servicer
if is_active is not None:
vehicle.is_active = is_active
try:
vehicle.full_clean()
except ValidationError as exc:
raise CommandError(f"Red {row_number}: validacija nije prošla -> {exc.message_dict}") from exc
if not dry_run:
vehicle.save()
if was_created:
created_count += 1
else:
updated_count += 1
self.stdout.write(
self.style.SUCCESS(
f"Red {row_number}: {'kreirano' if was_created else 'ažurirano'} {registration_number}"
)
)
if dry_run:
transaction.set_rollback(True)
summary = (
f"Import 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_or_create_client(self, *, row, row_number, is_company_vehicle):
client_id = _to_int(row.get("client_id"), "client_id")
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
client_tax_id = (row.get("client_tax_id") or "").strip()
client_email = (row.get("client_email") or "").strip().lower()
client_name = (row.get("client_name") or "").strip()
# Reuse pattern iz postojećih seed komandi: pokušaj pronaći postojeći zapis prije kreiranja.
if client_tax_id:
existing = Client.objects.filter(tax_id=client_tax_id).first()
if existing:
return existing
if client_email:
existing = Client.objects.filter(email__iexact=client_email).first()
if existing:
return existing
if client_name:
existing = Client.objects.filter(name=client_name).first()
if existing:
return existing
if is_company_vehicle:
return None
if not any([client_tax_id, client_email, client_name]):
raise CommandError(
f"Red {row_number}: za klijentsku dizalicu (is_company_vehicle=false) "
"morate navesti client_id ili client podatke za kreiranje kupca."
)
missing_for_create = []
create_fields = {
"name": client_name,
"client_type": (row.get("client_type") or "legal").strip() or "legal",
"tax_id": client_tax_id,
"email": client_email,
"phone": (row.get("client_phone") or "").strip() or None,
"address": (row.get("client_address") or "").strip(),
"city": (row.get("client_city") or "").strip(),
"postal_code": (row.get("client_postal_code") or "").strip(),
}
for required_key in ["name", "tax_id", "email", "address", "city", "postal_code"]:
if not create_fields[required_key]:
missing_for_create.append(required_key)
if missing_for_create:
raise CommandError(
f"Red {row_number}: nedostaju podaci za kreiranje kupca: {', '.join(missing_for_create)}"
)
if create_fields["client_type"] not in {"legal", "individual"}:
raise CommandError(
f"Red {row_number}: client_type mora biti 'legal' ili 'individual'."
)
client, was_created = Client.objects.get_or_create(
tax_id=create_fields["tax_id"],
defaults=create_fields,
)
if was_created:
self.stdout.write(
self.style.SUCCESS(
f"Red {row_number}: kreiran kupac '{client.name}' (OIB/VAT: {client.tax_id})"
)
)
return client

View File

@@ -0,0 +1,267 @@
import csv
from pathlib import Path
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from modules.fleet.models import Vehicle
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 vozila servisera iz CSV datoteke i sprema ih u Vehicle model "
"(asset_type=vehicle, is_company_vehicle=true)."
)
expected_columns = [
"registration_number",
"make",
"model",
"year",
"vin",
"current_mileage",
"service_interval_km",
"assigned_servicer_email",
"is_active",
]
users_template_columns = [
"email",
"is_serviser",
]
def add_arguments(self, parser):
parser.add_argument(
"csv_path",
type=str,
help="Apsolutna ili relativna putanja do CSV datoteke vozila.",
)
parser.add_argument(
"--users-csv-path",
type=str,
default="",
help=(
"Putanja do users_import_template.csv (fallback je "
"backend/core/users/import_templates/users_import_template.csv)."
),
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Validira CSV i ispisuje rezultat bez spremanja u bazu.",
)
parser.add_argument(
"--create-only",
action="store_true",
help="Kreira samo nove zapise; postojeće registracije se preskaču.",
)
def handle(self, *args, **options):
csv_path = self._resolve_input_csv_path(options["csv_path"])
users_csv_path = self._resolve_users_csv_path(options.get("users_csv_path") or "")
dry_run = options["dry_run"]
create_only = options["create_only"]
if not csv_path.exists():
raise CommandError(f"CSV datoteka ne postoji: {csv_path}")
if not users_csv_path.exists():
raise CommandError(f"users_import_template.csv ne postoji: {users_csv_path}")
allowed_servicer_emails = self._load_servicer_emails_from_template(users_csv_path)
if not allowed_servicer_emails:
raise CommandError(
"users_import_template.csv ne sadrži nijednog servisera (is_serviser=true)."
)
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
registration_number = (row.get("registration_number") or "").strip()
if not registration_number:
raise CommandError(f"Red {row_number}: registration_number je obavezan.")
assigned_servicer_email = (row.get("assigned_servicer_email") or "").strip().lower()
if not assigned_servicer_email:
raise CommandError(f"Red {row_number}: assigned_servicer_email je obavezan.")
if assigned_servicer_email not in allowed_servicer_emails:
raise CommandError(
f"Red {row_number}: '{assigned_servicer_email}' nije valjan serviser "
"u users_import_template.csv."
)
assigned_servicer = User.objects.filter(email__iexact=assigned_servicer_email).first()
if not assigned_servicer:
raise CommandError(
f"Red {row_number}: korisnik s emailom '{assigned_servicer_email}' ne postoji u bazi."
)
if not assigned_servicer.is_serviser:
raise CommandError(
f"Red {row_number}: korisnik '{assigned_servicer_email}' nije označen kao serviser."
)
vehicle = Vehicle.objects.filter(registration_number=registration_number).first()
if vehicle and create_only:
skipped_count += 1
self.stdout.write(
self.style.WARNING(
f"Red {row_number}: preskočeno (već postoji) {registration_number}"
)
)
continue
if vehicle is None:
vehicle = Vehicle(registration_number=registration_number)
was_created = True
else:
was_created = False
if vehicle.asset_type and vehicle.asset_type != "vehicle":
raise CommandError(
f"Red {row_number}: zapis {registration_number} već postoji kao "
f"asset_type='{vehicle.asset_type}', ne može se prepisati u 'vehicle'."
)
is_active = _to_bool(row.get("is_active"), "is_active")
vehicle.asset_type = "vehicle"
vehicle.make = (row.get("make") or "").strip()
vehicle.model = (row.get("model") or "").strip()
vehicle.year = _to_int(row.get("year"), "year")
vehicle.vin = (row.get("vin") or "").strip() or None
vehicle.current_mileage = _to_int(row.get("current_mileage"), "current_mileage") or 0
vehicle.service_interval_km = _to_int(
row.get("service_interval_km"),
"service_interval_km",
) or 15000
vehicle.is_company_vehicle = True
vehicle.client = None
vehicle.assigned_servicer = assigned_servicer
if is_active is not None:
vehicle.is_active = is_active
try:
vehicle.full_clean()
except ValidationError as exc:
raise CommandError(f"Red {row_number}: validacija nije prošla -> {exc.message_dict}") from exc
if not dry_run:
vehicle.save()
if was_created:
created_count += 1
else:
updated_count += 1
self.stdout.write(
self.style.SUCCESS(
f"Red {row_number}: {'kreirano' if was_created else 'ažurirano'} "
f"{registration_number} -> {assigned_servicer_email}"
)
)
if dry_run:
transaction.set_rollback(True)
summary = (
f"Import vozila servisera 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_input_csv_path(self, csv_path):
resolved = Path(csv_path).expanduser().resolve()
if resolved.exists():
return resolved
raw = str(csv_path).strip()
candidates = []
if "/backend/" in raw:
candidates.append(raw.replace("/backend/", "/", 1))
if "\\backend\\" in raw:
candidates.append(raw.replace("\\backend\\", "\\", 1))
if raw.startswith("backend/"):
candidates.append(raw.replace("backend/", "", 1))
if raw.startswith("backend\\"):
candidates.append(raw.replace("backend\\", "", 1))
for candidate in candidates:
candidate_path = Path(candidate).expanduser().resolve()
if candidate_path.exists():
self.stdout.write(
self.style.WARNING(
f"CSV nije pronađen na '{resolved}', koristim '{candidate_path}'."
)
)
return candidate_path
return resolved
def _resolve_users_csv_path(self, users_csv_path):
if users_csv_path:
return Path(users_csv_path).expanduser().resolve()
backend_root = Path(__file__).resolve().parents[4]
return backend_root / "core" / "users" / "import_templates" / "users_import_template.csv"
def _load_servicer_emails_from_template(self, users_csv_path):
with users_csv_path.open("r", encoding="utf-8-sig", newline="") as handle:
reader = csv.DictReader(handle)
if not reader.fieldnames:
raise CommandError("users_import_template.csv nema zaglavlje (header).")
normalized_headers = [str(h).strip() for h in reader.fieldnames]
missing = [column for column in self.users_template_columns if column not in normalized_headers]
if missing:
raise CommandError(
"users_import_template.csv nema obavezna polja: " + ", ".join(missing)
)
emails = set()
row_number = 1
for row in reader:
row_number += 1
if not any((value or "").strip() for value in row.values()):
continue
is_serviser = _to_bool(row.get("is_serviser"), f"is_serviser (users CSV, red {row_number})")
email = (row.get("email") or "").strip().lower()
if is_serviser and email:
emails.add(email)
return emails