patch oko teksta PDFa i UI tablica
This commit is contained in:
283
backend/modules/fleet/management/commands/import_cranes_csv.py
Normal file
283
backend/modules/fleet/management/commands/import_cranes_csv.py
Normal 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
|
||||
Reference in New Issue
Block a user