""" Commande Django pour importer des interventions préventives depuis un fichier Excel. Format du fichier Excel : - B1 : code de la thématique (ex: 'structures') - B2 : code du contrat (ex: 'CONT-2024-001') - B3 : code de la commande/order (ex: 'ORD-2025-001') - Ligne 5 : en-têtes de colonnes = codes de localisations (B5, C5, D5, …) - Colonne A à partir de la ligne 6 : codes de templates d'opérations (A6, A7, …) - Cellules B6 et au-delà : 'x', '1' ou 'v' (insensible à la casse) = créer une intervention Usage : python manage.py import_preventive_interventions --file scripts/data/preventif_tunnels.xlsx python manage.py import_preventive_interventions --file scripts/data/preventif_tunnels.xlsx --dry-run python manage.py import_preventive_interventions --file scripts/data/preventif_tunnels.xlsx \\ --user admin --begin 2026-01-01 --end 2026-12-31 --status in_preparation """ import os from datetime import datetime from django.core.management.base import BaseCommand, CommandError from django.contrib.auth import get_user_model from django.utils import timezone try: import openpyxl except ImportError: openpyxl = None from interventions.views.preventive_import import process_worksheet User = get_user_model() class Command(BaseCommand): help = ( "Importe des interventions préventives depuis un fichier Excel croisé " "(lignes = templates, colonnes = localisations). " "Crée également les opérations génériques et les opérations équipement " "pour les assets enfants des localisations." ) def add_arguments(self, parser): parser.add_argument( "--file", required=True, metavar="CHEMIN_FICHIER", help="Chemin vers le fichier Excel (.xlsx). Ex: scripts/data/preventif_tunnels.xlsx", ) parser.add_argument( "--dry-run", action="store_true", default=False, help="Simule l'import sans écrire en base de données.", ) parser.add_argument( "--user", default=None, metavar="USERNAME", help="Nom d'utilisateur (created_by) pour les interventions créées.", ) parser.add_argument( "--begin", default=None, metavar="YYYY-MM-DD", help="Date de début prévisionnelle des interventions (ex: 2026-01-01).", ) parser.add_argument( "--end", default=None, metavar="YYYY-MM-DD", help="Date de fin prévisionnelle des interventions (ex: 2026-12-31).", ) parser.add_argument( "--status", default="in_preparation", metavar="STATUS", help=( "Statut des interventions créées. " "Valeurs : in_preparation, to_be_planned, to_be_processed, … " "(défaut : in_preparation)" ), ) parser.add_argument( "--sheet", default=0, metavar="SHEET", help="Nom ou index (0-basé) de la feuille Excel à lire (défaut : 0 = première feuille).", ) def handle(self, *args, **options): if openpyxl is None: raise CommandError( "Le module 'openpyxl' est requis. Installez-le avec : pip install openpyxl" ) file_path = options["file"] dry_run = options["dry_run"] username = options["user"] status = options["status"] sheet_ref = options["sheet"] # --- Dates --- expected_begin = self._parse_date(options["begin"], "--begin") expected_end = self._parse_date(options["end"], "--end") if (expected_begin is None) != (expected_end is None): raise CommandError("--begin et --end doivent être fournis ensemble ou pas du tout.") # --- Fichier --- if not os.path.isabs(file_path): file_path = os.path.join(os.getcwd(), file_path) if not os.path.exists(file_path): raise CommandError(f"Fichier introuvable : {file_path}") # --- Utilisateur --- user = None if username: try: user = User.objects.get(username=username) except User.DoesNotExist: raise CommandError(f"Utilisateur introuvable : '{username}'") # --- Lecture du fichier Excel --- self.stdout.write(f"Lecture du fichier : {file_path}") wb = openpyxl.load_workbook(file_path, data_only=True) # --- Sélection de la feuille --- try: sheet_idx = int(sheet_ref) ws = wb.worksheets[sheet_idx] except (ValueError, TypeError): if sheet_ref not in wb.sheetnames: raise CommandError( f"Feuille '{sheet_ref}' introuvable. Feuilles disponibles : {wb.sheetnames}" ) ws = wb[sheet_ref] except IndexError: raise CommandError( f"Index de feuille {sheet_ref} invalide. " f"Le fichier contient {len(wb.worksheets)} feuille(s)." ) self.stdout.write(f"Feuille : '{ws.title}'") # --- Délégation au service --- result = process_worksheet( ws=ws, user=user, expected_begin=expected_begin, expected_end=expected_end, status=status, dry_run=dry_run, log=self.stdout.write, ) # --- Affichage des avertissements --- for w in result.warnings: self.stderr.write(self.style.WARNING(w)) for e in result.errors: self.stderr.write(self.style.ERROR(e)) # --- Résumé --- self.stdout.write("") if dry_run: self.stdout.write( self.style.WARNING( f"[DRY-RUN] {result.created} intervention(s) auraient été créées. " "Aucune donnée écrite en base." ) ) else: self.stdout.write( self.style.SUCCESS(f"{result.created} intervention(s) créée(s) avec succès.") ) if result.skipped: self.stdout.write( f"{result.skipped} cellule(s) ignorée(s) " "(vides ou localisation/template manquant)." ) @staticmethod def _parse_date(value, arg_name: str): if not value: return None try: d = datetime.strptime(value, "%Y-%m-%d") return timezone.make_aware(d) except ValueError: raise CommandError( f"Format de date invalide pour {arg_name} : '{value}'. Attendu : YYYY-MM-DD" )