""" Tests unitaires pour le service et la commande d'analyse des encours non terminés. """ import os import tempfile from datetime import timedelta from unittest import mock from django.contrib.auth import get_user_model from django.core.management import call_command from django.test import TestCase from django.utils import timezone import openpyxl from common.models import UserConfig, Role, Thematic from contracts.models import Company, Contract from assets.models import AssetCategory from interventions.models import Intervention, InterventionTimeLine, Symptom from observations.models import Observation from interventions.services import get_pending_analysis_data, generate_pending_analysis_excel class PendingAnalysisTests(TestCase): def setUp(self): User = get_user_model() self.user = User.objects.create_user(username='test_user', password='password123') self.user_config = UserConfig.objects.create(user=self.user, is_intern=True) self.thematic = Thematic.objects.create(code='lighting', name_fr='Éclairage public', name_nl='Openbare verlichting') self.category = AssetCategory.objects.create(name_fr='Candélabre', name_nl='Lantaarnpaal', thematic=self.thematic) self.company = Company.objects.create(name='Prestataire Lumis') self.contract = Contract.objects.create( contract_number='CTR-2025-01', company=self.company, start_date=timezone.now().date() - timedelta(days=365), end_date=timezone.now().date() + timedelta(days=365), is_active=True ) self.symptom = Symptom.objects.create( name_fr='Ampoule grillée', name_nl='Lamp kapot', thematic=self.thematic, contract=self.contract, provider=self.company, auto_create_intervention=True, ) self.symptom_manual = Symptom.objects.create( name_fr='Dégradation sans auto-création', name_nl='Schade zonder auto', thematic=self.thematic, auto_create_intervention=False, ) now = timezone.now() # 1. Intervention active en cours self.itv1 = Intervention.objects.create( title="Réparation luminaire A", thematic=self.thematic, asset_category=self.category, symptom=self.symptom, status='in_progress', contract=self.contract, assigned_provider=self.company, created_by=self.user, expected_end_time=now + timedelta(days=2), priority='3', ) InterventionTimeLine.objects.create( intervention=self.itv1, event_user=self.user, event_time=now - timedelta(days=3), event_type='creation', to_status='in_progress' ) # 2. Intervention en pause self.itv2 = Intervention.objects.create( title="Réparation luminaire B (En pause)", thematic=self.thematic, asset_category=self.category, symptom=self.symptom, status='on_pause', pause_reason='order_material', contract=self.contract, assigned_provider=self.company, created_by=self.user, priority='2', ) InterventionTimeLine.objects.create( intervention=self.itv2, event_user=self.user, event_time=now - timedelta(days=10), event_type='status_change', to_status='on_pause' ) # 3. Intervention en retard self.itv3 = Intervention.objects.create( title="Réparation urgente C (En retard)", thematic=self.thematic, asset_category=self.category, symptom=self.symptom, status='to_be_processed', contract=self.contract, assigned_provider=self.company, created_by=self.user, expected_end_time=now - timedelta(days=5), priority='1', ) # 4. Intervention terminée (ne doit PAS être incluse dans l'analyse des encours) self.itv_finished = Intervention.objects.create( title="Réparation terminée D", thematic=self.thematic, status='finished', created_by=self.user, ) # 5. Observation orpheline (symptôme sans auto-création) self.obs1 = Observation.objects.create( description="Poteau tordu", thematic=self.thematic, category=self.category, symptom=self.symptom_manual, status='to_process', latitude=50.8503, longitude=4.3517, created_by=self.user, ) # 6. Observation en cours liée à itv1 self.obs2 = Observation.objects.create( description="Lampe clignotante", thematic=self.thematic, category=self.category, symptom=self.symptom, status='in_progress', intervention=self.itv1, latitude=50.8503, longitude=4.3517, created_by=self.user, ) # 7. Observation désynchronisée (intervention terminée) self.obs3 = Observation.objects.create( description="Lampe éteinte", thematic=self.thematic, category=self.category, status='in_progress', intervention=self.itv_finished, latitude=50.8503, longitude=4.3517, created_by=self.user, ) # 8. Observation clôturée (ne doit PAS être incluse) self.obs_closed = Observation.objects.create( description="Déjà résolu", status='closed', latitude=50.8503, longitude=4.3517, created_by=self.user, ) def test_get_pending_analysis_data(self): data = get_pending_analysis_data(months=24) # Vérification des volumes self.assertEqual(len(data['interventions']), 3) # itv1, itv2, itv3 (itv_finished exclue) self.assertEqual(len(data['observations']), 3) # obs1, obs2, obs3 (obs_closed exclue) stats = data['stats'] self.assertEqual(stats['total_interventions'], 3) self.assertEqual(stats['total_observations'], 3) self.assertEqual(stats['paused_itvs_count'], 1) self.assertEqual(stats['delayed_itvs_count'], 1) self.assertEqual(stats['orphan_obs_count'], 1) self.assertEqual(stats['desynced_obs_count'], 1) # Vérification de l'observation désynchronisée obs3_data = next(o for o in data['observations'] if o['id'] == self.obs3.id) self.assertTrue(obs3_data['is_desynced']) self.assertIn("Désynchronisée", obs3_data['diagnostic']) # Vérification de l'intervention en retard itv3_data = next(i for i in data['interventions'] if i['id'] == self.itv3.id) self.assertTrue(itv3_data['is_delayed']) self.assertIn("En retard", itv3_data['sla_status_label']) def test_generate_pending_analysis_excel(self): wb = generate_pending_analysis_excel(months=24, output_format='workbook') # Vérification des 7 feuilles expected_sheets = [ "01_Synthese_KPI", "02_Analyse_Thematiques", "03_Diagnostics_Blocages", "04_Prestataires_Contrats", "05_Detail_Observations", "06_Detail_Interventions", "07_Definitions_Guide", ] self.assertEqual(wb.sheetnames, expected_sheets) # Vérification du contenu de la feuille de synthèse ws1 = wb["01_Synthese_KPI"] self.assertIn("LOKO — AUDIT", ws1["A1"].value) self.assertEqual(ws1["A5"].value, "INTERVENTIONS EN COURS") # Vérification de l'onglet d'analyse thématique ws_th = wb["02_Analyse_Thematiques"] self.assertIn("TABLEAUX CROISÉS DYNAMIQUES", ws_th["A1"].value) # Vérification du détail des observations (feuille 5) ws4 = wb["05_Detail_Observations"] self.assertEqual(ws4["A1"].value, "Code_Obs") # 3 lignes de données + 1 en-tête = 4 lignes self.assertEqual(ws4.max_row, 4) self.assertIn("Table_Observations", ws4.tables) # Vérification du détail des interventions (feuille 6) ws5 = wb["06_Detail_Interventions"] self.assertEqual(ws5["A1"].value, "Code_Itv") self.assertEqual(ws5.max_row, 4) self.assertIn("Table_Interventions", ws5.tables) def test_analyze_pending_items_command(self): with tempfile.TemporaryDirectory() as tmp_dir: out_file = os.path.join(tmp_dir, "test_pending_report.xlsx") call_command("analyze_pending_items", months=24, output=out_file) self.assertTrue(os.path.exists(out_file)) self.assertGreater(os.path.getsize(out_file), 5000) # Recharger le fichier sauvegardé pour vérifier son intégrité wb = openpyxl.load_workbook(out_file) self.assertEqual(len(wb.sheetnames), 7)