""" Management command: import_weapons_csv Reads weapon.csv (French RGA export, ';'-separated) and: 1. Creates Caliber instances for all unique caliber names (status=VERIFIED). 2. Creates Firearm instances for each row where the weapon type is mappable to a FirearmType (skips air guns, NL weapons, etc.). CSV column layout (1-indexed, matching the header): 1 referenceRGA 2 famille (EPAULE / POING) 3 typeArme (weapon type in French) 4 marque (brand) 5 modele (model name) 6 fabricant 7 paysFabricant 8 modeFonctionnement 9 systemeAlimentation 10 longueurArme 11 capaciteHorsChambre 12 capaciteChambre 13 calibreCanonUn ← primary caliber 14 modePercussionCanonUn 15 typeCanonUn 16 longueurCanonUn (barrel length mm) Usage: python manage.py import_weapons_csv /path/to/weapon.csv python manage.py import_weapons_csv /path/to/weapon.csv --dry-run """ import csv from django.core.management.base import BaseCommand from django.db import transaction from django.utils import timezone FIREARM_TYPE_MAP = { 'CARABINE': 'CARBINE', 'CARABINE A BARILLET': 'CARBINE', 'FUSIL': 'SHOTGUN', "FUSIL A POMPE": 'SHOTGUN', "FUSIL D'ASSAUT": 'RIFLE', "FUSIL (FAP MODIFIE 1 COUP)": 'SHOTGUN', "FUSIL SEMI-AUTOMATIQUE ET A POMPE": 'SHOTGUN', 'PISTOLET': 'PISTOL', 'REVOLVER': 'REVOLVER', } def _clean(val: str) -> str: """Strip surrounding whitespace and quotation marks.""" return val.strip().strip('"').strip("'").strip() class Command(BaseCommand): help = 'Import calibers and firearms from the French RGA weapon CSV.' def add_arguments(self, parser): parser.add_argument('csv_path', type=str, help='Path to weapon.csv') parser.add_argument( '--dry-run', action='store_true', help='Parse and count without writing to the database.', ) def handle(self, *args, **options): from apps.calibers.models import Caliber, CaliberStatus from apps.gears.models import Firearm, GearStatus csv_path = options['csv_path'] dry_run = options['dry_run'] if dry_run: self.stdout.write(self.style.WARNING('DRY RUN — nothing will be saved.')) # ── Pass 1: collect unique caliber names ────────────────────────────── caliber_names = set() rows = [] self.stdout.write('Reading CSV…') with open(csv_path, encoding='utf-8', errors='replace') as fh: reader = csv.reader(fh, delimiter=';') next(reader) # skip header for row in reader: if len(row) < 13: continue cal_raw = _clean(row[12]) type_raw = _clean(row[2]) if cal_raw and type_raw in FIREARM_TYPE_MAP: caliber_names.add(cal_raw) rows.append(row) self.stdout.write(f' {len(rows)} data rows, {len(caliber_names)} unique calibers to import.') if dry_run: fw_count = sum( 1 for r in rows if len(r) >= 5 and _clean(r[2]) in FIREARM_TYPE_MAP and _clean(r[4]) ) self.stdout.write(f' ~{fw_count} firearms would be created.') return # ── Pass 2: upsert Caliber instances ───────────────────────────────── self.stdout.write('Upserting calibers…') caliber_map = {} # name → Caliber pk existing = {c.name: c for c in Caliber.objects.filter(name__in=caliber_names)} caliber_map.update({name: cal.pk for name, cal in existing.items()}) new_calibers = [] now = timezone.now() for name in caliber_names: if name not in existing: new_calibers.append(Caliber( name=name, status=CaliberStatus.VERIFIED, reviewed_at=now, )) if new_calibers: created = Caliber.objects.bulk_create(new_calibers, batch_size=500) for c in created: caliber_map[c.name] = c.pk self.stdout.write(f' Created {len(created)} new calibers ({len(existing)} already existed).') else: self.stdout.write(f' All {len(existing)} calibers already existed.') # ── Pass 3: import firearms ─────────────────────────────────────────── self.stdout.write('Importing firearms…') # Build set of existing (brand, model_name) pairs to avoid duplicates existing_firearms = set( Firearm.objects.values_list('brand', 'model_name') ) to_create = [] skipped_type = 0 skipped_dup = 0 skipped_no_model = 0 for row in rows: if len(row) < 5: continue type_raw = _clean(row[2]) firearm_type = FIREARM_TYPE_MAP.get(type_raw) if not firearm_type: skipped_type += 1 continue brand = _clean(row[3]) model = _clean(row[4]) if not model: skipped_no_model += 1 continue if not brand: brand = '(unknown)' if (brand, model) in existing_firearms: skipped_dup += 1 continue cal_raw = _clean(row[12]) if len(row) > 12 else '' cal_pk = caliber_map.get(cal_raw) if cal_raw else None barrel_mm = None if len(row) > 15: try: barrel_mm = float(_clean(row[15]).replace(',', '.')) if barrel_mm <= 0: barrel_mm = None except (ValueError, AttributeError): barrel_mm = None cap_extra = None if len(row) > 10: try: cap_extra = int(_clean(row[10])) except (ValueError, AttributeError): pass to_create.append(Firearm( brand = brand, model_name = model, firearm_type = firearm_type, caliber_id = cal_pk, barrel_length_mm = barrel_mm, magazine_capacity = cap_extra if cap_extra else None, status = GearStatus.VERIFIED, reviewed_at = now, )) # Track to prevent within-batch duplicates existing_firearms.add((brand, model)) self.stdout.write( f' {len(to_create)} firearms to create ' f'({skipped_dup} duplicates, {skipped_type} unsupported types, ' f'{skipped_no_model} no model name).' ) with transaction.atomic(): # Firearm inherits from Gear (MTI) so bulk_create won't work directly. # Use chunked individual creates instead. chunk = 500 for i in range(0, len(to_create), chunk): batch = to_create[i:i + chunk] for fw in batch: fw.save() pct = min(i + chunk, len(to_create)) self.stdout.write(f' … {pct}/{len(to_create)}', ending='\r') self.stdout.flush() self.stdout.write('') self.stdout.write(self.style.SUCCESS( f'Done. {len(to_create)} firearms and {len(new_calibers)} calibers imported.' ))