loko/streetup/interventions/management/commands/import_preventive_interventions.py
2026-07-22 14:48:40 +02:00

192 lines
6.7 KiB
Python

"""
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"
)