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