loko/loko/interventions/tests/test_pending_analysis.py

240 lines
9 KiB
Python

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