240 lines
9 KiB
Python
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)
|