feat: enhance pending analysis report with thematic cross-tabulation and TCD-ready data structures
This commit is contained in:
parent
f72fe1bfd7
commit
cfdcc41929
2 changed files with 318 additions and 103 deletions
|
|
@ -3,7 +3,8 @@ Service pour l'analyse et le diagnostic des observations et interventions non te
|
||||||
|
|
||||||
Ce module permet d'extraire, de diagnostiquer et de générer un rapport Excel (.xlsx)
|
Ce module permet d'extraire, de diagnostiquer et de générer un rapport Excel (.xlsx)
|
||||||
multi-onglets complet pour identifier les causes de blocage et les goulots d'étranglement
|
multi-onglets complet pour identifier les causes de blocage et les goulots d'étranglement
|
||||||
sur les 24 derniers mois (ou une période personnalisée).
|
sur les 24 derniers mois (ou une période personnalisée), avec matrices croisées par thématique
|
||||||
|
et tables de données exploitables en Tableaux Croisés Dynamiques (TCD).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import io
|
import io
|
||||||
|
|
@ -20,6 +21,7 @@ from django.utils.translation import gettext_lazy as _
|
||||||
import openpyxl
|
import openpyxl
|
||||||
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
|
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
|
||||||
from openpyxl.utils import get_column_letter
|
from openpyxl.utils import get_column_letter
|
||||||
|
from openpyxl.worksheet.table import Table, TableStyleInfo
|
||||||
|
|
||||||
# Regex pour éliminer les caractères illégaux pour openpyxl / XML
|
# Regex pour éliminer les caractères illégaux pour openpyxl / XML
|
||||||
_ILLEGAL_CHARS_RE = re.compile(r'[\x00-\x08\x0b\x0c\x0e-\x1f]')
|
_ILLEGAL_CHARS_RE = re.compile(r'[\x00-\x08\x0b\x0c\x0e-\x1f]')
|
||||||
|
|
@ -268,7 +270,7 @@ def get_pending_analysis_data(months=24, thematic_code=None, contract_number=Non
|
||||||
'id': itv.id,
|
'id': itv.id,
|
||||||
'code': itv.code,
|
'code': itv.code,
|
||||||
'title': itv.title,
|
'title': itv.title,
|
||||||
'thematic_code': itv.thematic.code if itv.thematic else "",
|
'thematic_code': itv.thematic.code if itv.thematic else "sans_thematique",
|
||||||
'thematic_name': itv.thematic.name_fr if itv.thematic else "Sans thématique",
|
'thematic_name': itv.thematic.name_fr if itv.thematic else "Sans thématique",
|
||||||
'asset_category': itv.asset_category.name_fr if itv.asset_category else "",
|
'asset_category': itv.asset_category.name_fr if itv.asset_category else "",
|
||||||
'symptom_code': itv.symptom.code if itv.symptom else "",
|
'symptom_code': itv.symptom.code if itv.symptom else "",
|
||||||
|
|
@ -382,7 +384,7 @@ def get_pending_analysis_data(months=24, thematic_code=None, contract_number=Non
|
||||||
enriched_observations.append({
|
enriched_observations.append({
|
||||||
'id': obs.id,
|
'id': obs.id,
|
||||||
'code': obs.code or f"OBS{obs.id:05d}",
|
'code': obs.code or f"OBS{obs.id:05d}",
|
||||||
'thematic_code': obs.thematic.code if obs.thematic else "",
|
'thematic_code': obs.thematic.code if obs.thematic else "sans_thematique",
|
||||||
'thematic_name': obs.thematic.name_fr if obs.thematic else "Sans thématique",
|
'thematic_name': obs.thematic.name_fr if obs.thematic else "Sans thématique",
|
||||||
'category_name': obs.category.name_fr if obs.category else "",
|
'category_name': obs.category.name_fr if obs.category else "",
|
||||||
'symptom_name': obs.symptom.name_fr if obs.symptom else "",
|
'symptom_name': obs.symptom.name_fr if obs.symptom else "",
|
||||||
|
|
@ -499,6 +501,54 @@ def get_pending_analysis_data(months=24, thematic_code=None, contract_number=Non
|
||||||
data['delayed_pct'] = round((data['delayed'] / data['total'] * 100), 1) if data['total'] else 0
|
data['delayed_pct'] = round((data['delayed'] / data['total'] * 100), 1) if data['total'] else 0
|
||||||
data['avg_age'] = round(sum(data['ages']) / len(data['ages']), 1) if data['ages'] else 0
|
data['avg_age'] = round(sum(data['ages']) / len(data['ages']), 1) if data['ages'] else 0
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# 4. Matrices croisées complètes par Thématique (pour TCD / Tableaux croisés)
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
all_thematics = set([i['thematic_name'] for i in enriched_interventions] + [o['thematic_name'] for o in enriched_observations])
|
||||||
|
thematic_matrix = {}
|
||||||
|
|
||||||
|
for th_name in sorted(all_thematics):
|
||||||
|
th_itvs = [i for i in enriched_interventions if i['thematic_name'] == th_name]
|
||||||
|
th_obs = [o for o in enriched_observations if o['thematic_name'] == th_name]
|
||||||
|
th_itv_ages = [i['age_days'] for i in th_itvs]
|
||||||
|
th_obs_ages = [o['age_days'] for o in th_obs]
|
||||||
|
|
||||||
|
# Répartition des statuts itv
|
||||||
|
itv_st_counts = {st: sum(1 for i in th_itvs if i['status'] == st) for st in PENDING_INTERVENTION_STATUSES}
|
||||||
|
delayed_cnt = sum(1 for i in th_itvs if i['is_delayed'])
|
||||||
|
|
||||||
|
# Répartition des statuts obs
|
||||||
|
obs_st_counts = {st: sum(1 for o in th_obs if o['status'] == st) for st in PENDING_OBSERVATION_STATUSES}
|
||||||
|
orphan_cnt = sum(1 for o in th_obs if o['is_orphan'])
|
||||||
|
desynced_cnt = sum(1 for o in th_obs if o['is_desynced'])
|
||||||
|
|
||||||
|
# Répartition des priorités
|
||||||
|
prio_counts = {p: sum(1 for i in th_itvs if i['priority'] == p) for p in ['1', '2', '3', '4']}
|
||||||
|
|
||||||
|
# Répartition des pauses
|
||||||
|
pause_counts = {pr: sum(1 for i in th_itvs if i['status'] == 'on_pause' and i['pause_reason'] == pr) for pr in PAUSE_REASON_LABELS.keys()}
|
||||||
|
pause_counts['other_or_none'] = sum(1 for i in th_itvs if i['status'] == 'on_pause' and (not i['pause_reason'] or i['pause_reason'] not in PAUSE_REASON_LABELS))
|
||||||
|
|
||||||
|
# Tranches d'âges itv
|
||||||
|
itv_aging = _calc_aging_buckets(th_itv_ages)
|
||||||
|
|
||||||
|
thematic_matrix[th_name] = {
|
||||||
|
'thematic_name': th_name,
|
||||||
|
'itv_total': len(th_itvs),
|
||||||
|
'itv_st_counts': itv_st_counts,
|
||||||
|
'itv_delayed_count': delayed_cnt,
|
||||||
|
'itv_delayed_pct': round(delayed_cnt / len(th_itvs) * 100, 1) if th_itvs else 0,
|
||||||
|
'itv_avg_age': round(sum(th_itv_ages) / len(th_itv_ages), 1) if th_itvs else 0,
|
||||||
|
'itv_aging': itv_aging,
|
||||||
|
'itv_prio_counts': prio_counts,
|
||||||
|
'itv_pause_counts': pause_counts,
|
||||||
|
'obs_total': len(th_obs),
|
||||||
|
'obs_st_counts': obs_st_counts,
|
||||||
|
'obs_orphan_count': orphan_cnt,
|
||||||
|
'obs_desynced_count': desynced_cnt,
|
||||||
|
'obs_avg_age': round(sum(th_obs_ages) / len(th_obs_ages), 1) if th_obs else 0,
|
||||||
|
}
|
||||||
|
|
||||||
stats = {
|
stats = {
|
||||||
'total_interventions': total_itvs,
|
'total_interventions': total_itvs,
|
||||||
'total_observations': total_obs,
|
'total_observations': total_obs,
|
||||||
|
|
@ -517,6 +567,7 @@ def get_pending_analysis_data(months=24, thematic_code=None, contract_number=Non
|
||||||
'obs_by_status': obs_by_status,
|
'obs_by_status': obs_by_status,
|
||||||
'pause_reasons_stats': pause_reasons_stats,
|
'pause_reasons_stats': pause_reasons_stats,
|
||||||
'provider_contract_stats': list(provider_contract_stats.values()),
|
'provider_contract_stats': list(provider_contract_stats.values()),
|
||||||
|
'thematic_matrix': thematic_matrix,
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -531,17 +582,7 @@ def get_pending_analysis_data(months=24, thematic_code=None, contract_number=Non
|
||||||
|
|
||||||
def generate_pending_analysis_excel(months=24, thematic_code=None, contract_number=None, provider_id=None, output_format='workbook'):
|
def generate_pending_analysis_excel(months=24, thematic_code=None, contract_number=None, provider_id=None, output_format='workbook'):
|
||||||
"""
|
"""
|
||||||
Génère le classeur Excel multi-onglets structuré et stylisé.
|
Génère le classeur Excel multi-onglets structuré et stylisé avec Tableaux Croisés Dynamiques.
|
||||||
|
|
||||||
Args:
|
|
||||||
months (int): Horizon d'antériorité en mois (défaut: 24)
|
|
||||||
thematic_code (str, optional): Code thématique pour filtrer
|
|
||||||
contract_number (str, optional): Numéro de contrat pour filtrer
|
|
||||||
provider_id (int, optional): ID du prestataire pour filtrer
|
|
||||||
output_format (str): 'workbook' pour l'objet Workbook, 'file' pour BytesIO
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
openpyxl.Workbook ou io.BytesIO
|
|
||||||
"""
|
"""
|
||||||
data = get_pending_analysis_data(
|
data = get_pending_analysis_data(
|
||||||
months=months,
|
months=months,
|
||||||
|
|
@ -551,7 +592,6 @@ def generate_pending_analysis_excel(months=24, thematic_code=None, contract_numb
|
||||||
)
|
)
|
||||||
|
|
||||||
wb = openpyxl.Workbook()
|
wb = openpyxl.Workbook()
|
||||||
# Supprimer la feuille par défaut
|
|
||||||
if "Sheet" in wb.sheetnames:
|
if "Sheet" in wb.sheetnames:
|
||||||
wb.remove(wb["Sheet"])
|
wb.remove(wb["Sheet"])
|
||||||
|
|
||||||
|
|
@ -569,8 +609,6 @@ def generate_pending_analysis_excel(months=24, thematic_code=None, contract_numb
|
||||||
font_body = Font(name="Segoe UI", size=9, color="2D3748")
|
font_body = Font(name="Segoe UI", size=9, color="2D3748")
|
||||||
font_body_bold = Font(name="Segoe UI", size=9, bold=True, color="1A365D")
|
font_body_bold = Font(name="Segoe UI", size=9, bold=True, color="1A365D")
|
||||||
font_alert_red = Font(name="Segoe UI", size=9, bold=True, color="9B2C2C")
|
font_alert_red = Font(name="Segoe UI", size=9, bold=True, color="9B2C2C")
|
||||||
font_alert_orange = Font(name="Segoe UI", size=9, bold=True, color="9C4221")
|
|
||||||
font_alert_green = Font(name="Segoe UI", size=9, bold=True, color="22543D")
|
|
||||||
|
|
||||||
fill_title = PatternFill(start_color="1A365D", end_color="1A365D", fill_type="solid")
|
fill_title = PatternFill(start_color="1A365D", end_color="1A365D", fill_type="solid")
|
||||||
fill_header = PatternFill(start_color="2B6CB0", end_color="2B6CB0", fill_type="solid")
|
fill_header = PatternFill(start_color="2B6CB0", end_color="2B6CB0", fill_type="solid")
|
||||||
|
|
@ -578,7 +616,6 @@ def generate_pending_analysis_excel(months=24, thematic_code=None, contract_numb
|
||||||
fill_card = PatternFill(start_color="F7FAFC", end_color="F7FAFC", fill_type="solid")
|
fill_card = PatternFill(start_color="F7FAFC", end_color="F7FAFC", fill_type="solid")
|
||||||
fill_card_alert = PatternFill(start_color="FFF5F5", end_color="FFF5F5", fill_type="solid")
|
fill_card_alert = PatternFill(start_color="FFF5F5", end_color="FFF5F5", fill_type="solid")
|
||||||
fill_zebra = PatternFill(start_color="F8FAFC", end_color="F8FAFC", fill_type="solid")
|
fill_zebra = PatternFill(start_color="F8FAFC", end_color="F8FAFC", fill_type="solid")
|
||||||
fill_white = PatternFill(start_color="FFFFFF", end_color="FFFFFF", fill_type="solid")
|
|
||||||
|
|
||||||
fill_red_pastel = PatternFill(start_color="FED7D7", end_color="FED7D7", fill_type="solid")
|
fill_red_pastel = PatternFill(start_color="FED7D7", end_color="FED7D7", fill_type="solid")
|
||||||
fill_orange_pastel = PatternFill(start_color="FEEBC8", end_color="FEEBC8", fill_type="solid")
|
fill_orange_pastel = PatternFill(start_color="FEEBC8", end_color="FEEBC8", fill_type="solid")
|
||||||
|
|
@ -618,7 +655,6 @@ def generate_pending_analysis_excel(months=24, thematic_code=None, contract_numb
|
||||||
ws1["A3"].fill = fill_title
|
ws1["A3"].fill = fill_title
|
||||||
ws1["A3"].alignment = Alignment(horizontal="left", vertical="center", indent=1)
|
ws1["A3"].alignment = Alignment(horizontal="left", vertical="center", indent=1)
|
||||||
|
|
||||||
# Ligne 5-7 : CARTES KPI
|
|
||||||
def _create_kpi_card(ws, start_col, title, value, sub="", is_alert=False):
|
def _create_kpi_card(ws, start_col, title, value, sub="", is_alert=False):
|
||||||
c1, c2 = start_col, chr(ord(start_col) + 1)
|
c1, c2 = start_col, chr(ord(start_col) + 1)
|
||||||
ws.merge_cells(f"{c1}5:{c2}5")
|
ws.merge_cells(f"{c1}5:{c2}5")
|
||||||
|
|
@ -648,7 +684,7 @@ def generate_pending_analysis_excel(months=24, thematic_code=None, contract_numb
|
||||||
_create_kpi_card(ws1, 'E', "INTERVENTIONS EN RETARD", f"{stats['delayed_itvs_count']:,} ({stats['delayed_itvs_pct']}%)", "Échéance ou SLA dépassé", is_alert=(stats['delayed_itvs_count'] > 0))
|
_create_kpi_card(ws1, 'E', "INTERVENTIONS EN RETARD", f"{stats['delayed_itvs_count']:,} ({stats['delayed_itvs_pct']}%)", "Échéance ou SLA dépassé", is_alert=(stats['delayed_itvs_count'] > 0))
|
||||||
_create_kpi_card(ws1, 'G', "EN PAUSE / ORPHELINES", f"{stats['paused_itvs_count']} itv / {stats['orphan_obs_count']} obs", f"{stats['desynced_obs_count']} obs désynchronisées", is_alert=(stats['orphan_obs_count'] > 0 or stats['desynced_obs_count'] > 0))
|
_create_kpi_card(ws1, 'G', "EN PAUSE / ORPHELINES", f"{stats['paused_itvs_count']} itv / {stats['orphan_obs_count']} obs", f"{stats['desynced_obs_count']} obs désynchronisées", is_alert=(stats['orphan_obs_count'] > 0 or stats['desynced_obs_count'] > 0))
|
||||||
|
|
||||||
# Ligne 9 : Section Pyramide des âges
|
# Pyramide des âges
|
||||||
ws1["A9"] = "1. Pyramide des âges de l'encours"
|
ws1["A9"] = "1. Pyramide des âges de l'encours"
|
||||||
ws1["A9"].font = font_section
|
ws1["A9"].font = font_section
|
||||||
|
|
||||||
|
|
@ -689,7 +725,7 @@ def generate_pending_analysis_excel(months=24, thematic_code=None, contract_numb
|
||||||
cell.fill = fill_zebra
|
cell.fill = fill_zebra
|
||||||
r_idx += 1
|
r_idx += 1
|
||||||
|
|
||||||
# Ligne Total Aging
|
# Total Aging
|
||||||
ws1.cell(row=r_idx, column=1, value="Total général").font = font_body_bold
|
ws1.cell(row=r_idx, column=1, value="Total général").font = font_body_bold
|
||||||
ws1.cell(row=r_idx, column=2, value=stats['total_interventions']).font = font_body_bold
|
ws1.cell(row=r_idx, column=2, value=stats['total_interventions']).font = font_body_bold
|
||||||
ws1.cell(row=r_idx, column=3, value=1.0).number_format = "0.0%"
|
ws1.cell(row=r_idx, column=3, value=1.0).number_format = "0.0%"
|
||||||
|
|
@ -700,7 +736,7 @@ def generate_pending_analysis_excel(months=24, thematic_code=None, contract_numb
|
||||||
c.border = border_cell
|
c.border = border_cell
|
||||||
c.fill = fill_card
|
c.fill = fill_card
|
||||||
|
|
||||||
# Section 2 : Répartition par Statut Interventions
|
# Statuts Interventions
|
||||||
r_idx += 3
|
r_idx += 3
|
||||||
ws1.cell(row=r_idx, column=1, value="2. Répartition des Interventions par Statut").font = font_section
|
ws1.cell(row=r_idx, column=1, value="2. Répartition des Interventions par Statut").font = font_section
|
||||||
r_idx += 1
|
r_idx += 1
|
||||||
|
|
@ -729,35 +765,6 @@ def generate_pending_analysis_excel(months=24, thematic_code=None, contract_numb
|
||||||
cell.fill = fill_zebra
|
cell.fill = fill_zebra
|
||||||
r_idx += 1
|
r_idx += 1
|
||||||
|
|
||||||
# Section 3 : Répartition par Statut Observations
|
|
||||||
r_idx += 2
|
|
||||||
ws1.cell(row=r_idx, column=1, value="3. Répartition des Observations par Statut").font = font_section
|
|
||||||
r_idx += 1
|
|
||||||
|
|
||||||
headers_obs_st = ["Statut d'observation", "Nombre", "% du Total", "Âge moyen (jours)"]
|
|
||||||
for col_idx, h in enumerate(headers_obs_st, 1):
|
|
||||||
c = ws1.cell(row=r_idx, column=col_idx, value=h)
|
|
||||||
c.font = font_header
|
|
||||||
c.fill = fill_header
|
|
||||||
c.alignment = align_header
|
|
||||||
c.border = border_cell
|
|
||||||
|
|
||||||
r_idx += 1
|
|
||||||
for st, row_data in stats['obs_by_status'].items():
|
|
||||||
ws1.cell(row=r_idx, column=1, value=row_data['label']).alignment = align_left
|
|
||||||
ws1.cell(row=r_idx, column=2, value=row_data['count']).alignment = align_right
|
|
||||||
ws1.cell(row=r_idx, column=3, value=row_data['pct'] / 100.0).number_format = "0.0%"
|
|
||||||
ws1.cell(row=r_idx, column=4, value=row_data['avg_age']).alignment = align_right
|
|
||||||
|
|
||||||
for col_idx in range(1, 5):
|
|
||||||
cell = ws1.cell(row=r_idx, column=col_idx)
|
|
||||||
cell.font = font_body
|
|
||||||
cell.border = border_cell
|
|
||||||
if r_idx % 2 == 0:
|
|
||||||
cell.fill = fill_zebra
|
|
||||||
r_idx += 1
|
|
||||||
|
|
||||||
# Auto-width ws1
|
|
||||||
for col in ws1.columns:
|
for col in ws1.columns:
|
||||||
max_len = max(len(str(cell.value or '')) for cell in col)
|
max_len = max(len(str(cell.value or '')) for cell in col)
|
||||||
col_letter = get_column_letter(col[0].column)
|
col_letter = get_column_letter(col[0].column)
|
||||||
|
|
@ -765,9 +772,207 @@ def generate_pending_analysis_excel(months=24, thematic_code=None, contract_numb
|
||||||
ws1.column_dimensions['A'].width = 38
|
ws1.column_dimensions['A'].width = 38
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
# ONGLET 2 : DIAGNOSTICS & BLOCAGES
|
# ONGLET 2 : ANALYSES CROISÉES PAR THÉMATIQUE (TCD PRÉ-CALCULÉS & FORMULES)
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
ws2 = wb.create_sheet(title="02_Diagnostics_Blocages")
|
ws_th = wb.create_sheet(title="02_Analyse_Thematiques")
|
||||||
|
ws_th.views.sheetView[0].showGridLines = True
|
||||||
|
|
||||||
|
ws_th.merge_cells("A1:N2")
|
||||||
|
ws_th["A1"] = "TABLEAUX CROISÉS DYNAMIQUES — ANALYSE DÉTAILLÉE PAR THÉMATIQUE"
|
||||||
|
ws_th["A1"].font = font_title
|
||||||
|
ws_th["A1"].fill = fill_title
|
||||||
|
ws_th["A1"].alignment = Alignment(horizontal="left", vertical="center", indent=1)
|
||||||
|
|
||||||
|
ws_th.merge_cells("A3:N3")
|
||||||
|
ws_th["A3"] = "Matrices croisées des interventions et observations par thématique métier (statuts, retards, pauses, âges et priorités)."
|
||||||
|
ws_th["A3"].font = font_subtitle
|
||||||
|
ws_th["A3"].fill = fill_title
|
||||||
|
ws_th["A3"].alignment = Alignment(horizontal="left", vertical="center", indent=1)
|
||||||
|
|
||||||
|
th_matrix = stats['thematic_matrix']
|
||||||
|
|
||||||
|
# 1. Matrice Thématique × Statuts Interventions
|
||||||
|
ws_th["A5"] = "1. Matrice Croisée : Thématiques × Statuts des Interventions en cours"
|
||||||
|
ws_th["A5"].font = font_section
|
||||||
|
|
||||||
|
headers_th_itv = [
|
||||||
|
"Thématique", "En prép.", "À approuver", "À planifier", "À traiter",
|
||||||
|
"Pris en charge", "Démarré", "En pause", "À corriger",
|
||||||
|
"Total en cours", "Nb en retard", "% en retard", "Âge moyen (j)"
|
||||||
|
]
|
||||||
|
for col_idx, h in enumerate(headers_th_itv, 1):
|
||||||
|
c = ws_th.cell(row=6, column=col_idx, value=h)
|
||||||
|
c.font = font_header
|
||||||
|
c.fill = fill_header
|
||||||
|
c.alignment = align_header
|
||||||
|
c.border = border_cell
|
||||||
|
|
||||||
|
r_idx = 7
|
||||||
|
for th_name, th_d in th_matrix.items():
|
||||||
|
ws_th.cell(row=r_idx, column=1, value=th_name).alignment = align_left
|
||||||
|
st_c = th_d['itv_st_counts']
|
||||||
|
ws_th.cell(row=r_idx, column=2, value=st_c.get('in_preparation', 0)).alignment = align_right
|
||||||
|
ws_th.cell(row=r_idx, column=3, value=st_c.get('to_be_approved', 0)).alignment = align_right
|
||||||
|
ws_th.cell(row=r_idx, column=4, value=st_c.get('to_be_planned', 0)).alignment = align_right
|
||||||
|
ws_th.cell(row=r_idx, column=5, value=st_c.get('to_be_processed', 0)).alignment = align_right
|
||||||
|
ws_th.cell(row=r_idx, column=6, value=st_c.get('assigned', 0)).alignment = align_right
|
||||||
|
ws_th.cell(row=r_idx, column=7, value=st_c.get('in_progress', 0)).alignment = align_right
|
||||||
|
ws_th.cell(row=r_idx, column=8, value=st_c.get('on_pause', 0)).alignment = align_right
|
||||||
|
ws_th.cell(row=r_idx, column=9, value=st_c.get('to_be_corrected', 0)).alignment = align_right
|
||||||
|
|
||||||
|
ws_th.cell(row=r_idx, column=10, value=th_d['itv_total']).alignment = align_right
|
||||||
|
|
||||||
|
c_del = ws_th.cell(row=r_idx, column=11, value=th_d['itv_delayed_count'])
|
||||||
|
c_del.alignment = align_right
|
||||||
|
if th_d['itv_delayed_count'] > 0:
|
||||||
|
c_del.font = font_alert_red
|
||||||
|
|
||||||
|
c_del_pct = ws_th.cell(row=r_idx, column=12, value=th_d['itv_delayed_pct'] / 100.0)
|
||||||
|
c_del_pct.number_format = "0.0%"
|
||||||
|
c_del_pct.alignment = align_right
|
||||||
|
if th_d['itv_delayed_pct'] > 20:
|
||||||
|
c_del_pct.font = font_alert_red
|
||||||
|
|
||||||
|
ws_th.cell(row=r_idx, column=13, value=th_d['itv_avg_age']).alignment = align_right
|
||||||
|
|
||||||
|
for col_idx in range(1, 14):
|
||||||
|
cell = ws_th.cell(row=r_idx, column=col_idx)
|
||||||
|
cell.border = border_cell
|
||||||
|
if col_idx not in [11, 12]:
|
||||||
|
cell.font = font_body
|
||||||
|
if r_idx % 2 == 1:
|
||||||
|
cell.fill = fill_zebra
|
||||||
|
r_idx += 1
|
||||||
|
|
||||||
|
# Total ligne Interventions
|
||||||
|
ws_th.cell(row=r_idx, column=1, value="Total général").font = font_body_bold
|
||||||
|
for c_i in range(2, 10):
|
||||||
|
c_letter = get_column_letter(c_i)
|
||||||
|
ws_th.cell(row=r_idx, column=c_i, value=f"=SUM({c_letter}7:{c_letter}{r_idx-1})").font = font_body_bold
|
||||||
|
ws_th.cell(row=r_idx, column=10, value=f"=SUM(J7:J{r_idx-1})").font = font_body_bold
|
||||||
|
ws_th.cell(row=r_idx, column=11, value=f"=SUM(K7:K{r_idx-1})").font = font_body_bold
|
||||||
|
for col_idx in range(1, 14):
|
||||||
|
c = ws_th.cell(row=r_idx, column=col_idx)
|
||||||
|
c.border = border_cell
|
||||||
|
c.fill = fill_card
|
||||||
|
|
||||||
|
# 2. Matrice Thématique × Statuts Observations
|
||||||
|
r_idx += 3
|
||||||
|
ws_th.cell(row=r_idx, column=1, value="2. Matrice Croisée : Thématiques × Statuts des Observations ouvertes").font = font_section
|
||||||
|
r_idx += 1
|
||||||
|
|
||||||
|
headers_th_obs = [
|
||||||
|
"Thématique", "En préparation", "À traiter", "En cours",
|
||||||
|
"Total ouvertes", "Orphelines (sans itv)", "Désynchronisées", "Âge moyen (j)"
|
||||||
|
]
|
||||||
|
for col_idx, h in enumerate(headers_th_obs, 1):
|
||||||
|
c = ws_th.cell(row=r_idx, column=col_idx, value=h)
|
||||||
|
c.font = font_header
|
||||||
|
c.fill = fill_header
|
||||||
|
c.alignment = align_header
|
||||||
|
c.border = border_cell
|
||||||
|
|
||||||
|
start_obs_r = r_idx + 1
|
||||||
|
r_idx += 1
|
||||||
|
for th_name, th_d in th_matrix.items():
|
||||||
|
ws_th.cell(row=r_idx, column=1, value=th_name).alignment = align_left
|
||||||
|
obs_st_c = th_d['obs_st_counts']
|
||||||
|
ws_th.cell(row=r_idx, column=2, value=obs_st_c.get('in_preparation', 0)).alignment = align_right
|
||||||
|
ws_th.cell(row=r_idx, column=3, value=obs_st_c.get('to_process', 0)).alignment = align_right
|
||||||
|
ws_th.cell(row=r_idx, column=4, value=obs_st_c.get('in_progress', 0)).alignment = align_right
|
||||||
|
ws_th.cell(row=r_idx, column=5, value=th_d['obs_total']).alignment = align_right
|
||||||
|
|
||||||
|
c_orph = ws_th.cell(row=r_idx, column=6, value=th_d['obs_orphan_count'])
|
||||||
|
c_orph.alignment = align_right
|
||||||
|
if th_d['obs_orphan_count'] > 0:
|
||||||
|
c_orph.font = font_alert_red
|
||||||
|
|
||||||
|
c_desync = ws_th.cell(row=r_idx, column=7, value=th_d['obs_desynced_count'])
|
||||||
|
c_desync.alignment = align_right
|
||||||
|
if th_d['obs_desynced_count'] > 0:
|
||||||
|
c_desync.font = font_alert_red
|
||||||
|
|
||||||
|
ws_th.cell(row=r_idx, column=8, value=th_d['obs_avg_age']).alignment = align_right
|
||||||
|
|
||||||
|
for col_idx in range(1, 9):
|
||||||
|
cell = ws_th.cell(row=r_idx, column=col_idx)
|
||||||
|
cell.border = border_cell
|
||||||
|
if col_idx not in [6, 7]:
|
||||||
|
cell.font = font_body
|
||||||
|
if r_idx % 2 == 1:
|
||||||
|
cell.fill = fill_zebra
|
||||||
|
r_idx += 1
|
||||||
|
|
||||||
|
# Total ligne Observations
|
||||||
|
ws_th.cell(row=r_idx, column=1, value="Total général").font = font_body_bold
|
||||||
|
for c_i in range(2, 6):
|
||||||
|
c_letter = get_column_letter(c_i)
|
||||||
|
ws_th.cell(row=r_idx, column=c_i, value=f"=SUM({c_letter}{start_obs_r}:{c_letter}{r_idx-1})").font = font_body_bold
|
||||||
|
ws_th.cell(row=r_idx, column=6, value=f"=SUM(F{start_obs_r}:F{r_idx-1})").font = font_body_bold
|
||||||
|
ws_th.cell(row=r_idx, column=7, value=f"=SUM(G{start_obs_r}:G{r_idx-1})").font = font_body_bold
|
||||||
|
for col_idx in range(1, 9):
|
||||||
|
c = ws_th.cell(row=r_idx, column=col_idx)
|
||||||
|
c.border = border_cell
|
||||||
|
c.fill = fill_card
|
||||||
|
|
||||||
|
# 3. Matrice Thématique × Motifs de Pause
|
||||||
|
r_idx += 3
|
||||||
|
ws_th.cell(row=r_idx, column=1, value="3. Matrice Croisée : Thématiques × Motifs de Pause (Interventions en pause)").font = font_section
|
||||||
|
r_idx += 1
|
||||||
|
|
||||||
|
headers_th_pause = [
|
||||||
|
"Thématique", "Matériel à commander", "Attente sous-traitant", "Attente livraison",
|
||||||
|
"Offre prestataire", "Attente validation", "À replanifier", "Garantie / Autre", "Total en Pause"
|
||||||
|
]
|
||||||
|
for col_idx, h in enumerate(headers_th_pause, 1):
|
||||||
|
c = ws_th.cell(row=r_idx, column=col_idx, value=h)
|
||||||
|
c.font = font_header
|
||||||
|
c.fill = fill_header
|
||||||
|
c.alignment = align_header
|
||||||
|
c.border = border_cell
|
||||||
|
|
||||||
|
start_p_r = r_idx + 1
|
||||||
|
r_idx += 1
|
||||||
|
for th_name, th_d in th_matrix.items():
|
||||||
|
ws_th.cell(row=r_idx, column=1, value=th_name).alignment = align_left
|
||||||
|
p_c = th_d['itv_pause_counts']
|
||||||
|
ws_th.cell(row=r_idx, column=2, value=p_c.get('order_material', 0)).alignment = align_right
|
||||||
|
ws_th.cell(row=r_idx, column=3, value=p_c.get('waiting_for_supplier', 0)).alignment = align_right
|
||||||
|
ws_th.cell(row=r_idx, column=4, value=p_c.get('waiting_for_material_delivery', 0)).alignment = align_right
|
||||||
|
ws_th.cell(row=r_idx, column=5, value=p_c.get('offer_by_provider', 0)).alignment = align_right
|
||||||
|
ws_th.cell(row=r_idx, column=6, value=p_c.get('waiting_for_validation', 0)).alignment = align_right
|
||||||
|
ws_th.cell(row=r_idx, column=7, value=p_c.get('reschedule', 0)).alignment = align_right
|
||||||
|
ws_th.cell(row=r_idx, column=8, value=p_c.get('under_framework_contract', 0) + p_c.get('other', 0) + p_c.get('other_or_none', 0)).alignment = align_right
|
||||||
|
ws_th.cell(row=r_idx, column=9, value=th_d['itv_st_counts'].get('on_pause', 0)).alignment = align_right
|
||||||
|
|
||||||
|
for col_idx in range(1, 10):
|
||||||
|
cell = ws_th.cell(row=r_idx, column=col_idx)
|
||||||
|
cell.border = border_cell
|
||||||
|
cell.font = font_body
|
||||||
|
if r_idx % 2 == 1:
|
||||||
|
cell.fill = fill_zebra
|
||||||
|
r_idx += 1
|
||||||
|
|
||||||
|
# Total ligne Pauses
|
||||||
|
ws_th.cell(row=r_idx, column=1, value="Total général").font = font_body_bold
|
||||||
|
for c_i in range(2, 10):
|
||||||
|
c_letter = get_column_letter(c_i)
|
||||||
|
ws_th.cell(row=r_idx, column=c_i, value=f"=SUM({c_letter}{start_p_r}:{c_letter}{r_idx-1})").font = font_body_bold
|
||||||
|
for col_idx in range(1, 10):
|
||||||
|
c = ws_th.cell(row=r_idx, column=col_idx)
|
||||||
|
c.border = border_cell
|
||||||
|
c.fill = fill_card
|
||||||
|
|
||||||
|
for col in ws_th.columns:
|
||||||
|
max_len = max(len(str(cell.value or '')) for cell in col)
|
||||||
|
col_letter = get_column_letter(col[0].column)
|
||||||
|
ws_th.column_dimensions[col_letter].width = max(max_len + 3, 12)
|
||||||
|
ws_th.column_dimensions['A'].width = 28
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# ONGLET 3 : DIAGNOSTICS & BLOCAGES
|
||||||
|
# =========================================================================
|
||||||
|
ws2 = wb.create_sheet(title="03_Diagnostics_Blocages")
|
||||||
ws2.views.sheetView[0].showGridLines = True
|
ws2.views.sheetView[0].showGridLines = True
|
||||||
|
|
||||||
ws2.merge_cells("A1:G2")
|
ws2.merge_cells("A1:G2")
|
||||||
|
|
@ -776,7 +981,7 @@ def generate_pending_analysis_excel(months=24, thematic_code=None, contract_numb
|
||||||
ws2["A1"].fill = fill_title
|
ws2["A1"].fill = fill_title
|
||||||
ws2["A1"].alignment = Alignment(horizontal="left", vertical="center", indent=1)
|
ws2["A1"].alignment = Alignment(horizontal="left", vertical="center", indent=1)
|
||||||
|
|
||||||
# Section 1 : Motifs de pause
|
# Motifs de pause
|
||||||
ws2["A4"] = "1. Répartition des Interventions en Pause (on_pause)"
|
ws2["A4"] = "1. Répartition des Interventions en Pause (on_pause)"
|
||||||
ws2["A4"].font = font_section
|
ws2["A4"].font = font_section
|
||||||
|
|
||||||
|
|
@ -808,7 +1013,7 @@ def generate_pending_analysis_excel(months=24, thematic_code=None, contract_numb
|
||||||
ws2.cell(row=r_idx, column=1, value="Aucune intervention actuellement en pause.").font = font_body
|
ws2.cell(row=r_idx, column=1, value="Aucune intervention actuellement en pause.").font = font_body
|
||||||
r_idx += 1
|
r_idx += 1
|
||||||
|
|
||||||
# Section 2 : Analyse des Observations en Souffrance
|
# Diagnostics Observations
|
||||||
r_idx += 2
|
r_idx += 2
|
||||||
ws2.cell(row=r_idx, column=1, value="2. Diagnostic des Observations en Souffrance").font = font_section
|
ws2.cell(row=r_idx, column=1, value="2. Diagnostic des Observations en Souffrance").font = font_section
|
||||||
r_idx += 1
|
r_idx += 1
|
||||||
|
|
@ -869,7 +1074,7 @@ def generate_pending_analysis_excel(months=24, thematic_code=None, contract_numb
|
||||||
cell.fill = fill_zebra
|
cell.fill = fill_zebra
|
||||||
r_idx += 1
|
r_idx += 1
|
||||||
|
|
||||||
# Section 3 : Interventions Silencieuses (> 30 jours sans timeline)
|
# Interventions Silencieuses
|
||||||
r_idx += 2
|
r_idx += 2
|
||||||
ws2.cell(row=r_idx, column=1, value="3. Interventions Silencieuses (> 30 jours sans événement Timeline)").font = font_section
|
ws2.cell(row=r_idx, column=1, value="3. Interventions Silencieuses (> 30 jours sans événement Timeline)").font = font_section
|
||||||
r_idx += 1
|
r_idx += 1
|
||||||
|
|
@ -884,7 +1089,7 @@ def generate_pending_analysis_excel(months=24, thematic_code=None, contract_numb
|
||||||
|
|
||||||
r_idx += 1
|
r_idx += 1
|
||||||
silent_itvs = [i for i in data['interventions'] if i['days_since_activity'] > 30 and i['status'] != 'on_pause']
|
silent_itvs = [i for i in data['interventions'] if i['days_since_activity'] > 30 and i['status'] != 'on_pause']
|
||||||
for sitv in silent_itvs[:50]: # Top 50 silencieuses
|
for sitv in silent_itvs[:50]:
|
||||||
ws2.cell(row=r_idx, column=1, value=sitv['code']).alignment = align_center
|
ws2.cell(row=r_idx, column=1, value=sitv['code']).alignment = align_center
|
||||||
ws2.cell(row=r_idx, column=2, value=_sanitize_cell_value(sitv['title'])).alignment = align_left
|
ws2.cell(row=r_idx, column=2, value=_sanitize_cell_value(sitv['title'])).alignment = align_left
|
||||||
ws2.cell(row=r_idx, column=3, value=sitv['thematic_name']).alignment = align_left
|
ws2.cell(row=r_idx, column=3, value=sitv['thematic_name']).alignment = align_left
|
||||||
|
|
@ -904,16 +1109,15 @@ def generate_pending_analysis_excel(months=24, thematic_code=None, contract_numb
|
||||||
if not silent_itvs:
|
if not silent_itvs:
|
||||||
ws2.cell(row=r_idx, column=1, value="Aucune intervention silencieuse détectée.").font = font_body
|
ws2.cell(row=r_idx, column=1, value="Aucune intervention silencieuse détectée.").font = font_body
|
||||||
|
|
||||||
# Auto-width ws2
|
|
||||||
for col in ws2.columns:
|
for col in ws2.columns:
|
||||||
max_len = max(len(str(cell.value or '')) for cell in col)
|
max_len = max(len(str(cell.value or '')) for cell in col)
|
||||||
col_letter = get_column_letter(col[0].column)
|
col_letter = get_column_letter(col[0].column)
|
||||||
ws2.column_dimensions[col_letter].width = min(max(max_len + 4, 12), 60)
|
ws2.column_dimensions[col_letter].width = min(max(max_len + 4, 12), 60)
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
# ONGLET 3 : ANALYSE PRESTATAIRES & CONTRATS
|
# ONGLET 4 : ANALYSE PRESTATAIRES & CONTRATS
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
ws3 = wb.create_sheet(title="03_Prestataires_Contrats")
|
ws3 = wb.create_sheet(title="04_Prestataires_Contrats")
|
||||||
ws3.views.sheetView[0].showGridLines = True
|
ws3.views.sheetView[0].showGridLines = True
|
||||||
|
|
||||||
ws3.merge_cells("A1:K2")
|
ws3.merge_cells("A1:K2")
|
||||||
|
|
@ -948,7 +1152,6 @@ def generate_pending_analysis_excel(months=24, thematic_code=None, contract_numb
|
||||||
ws3.cell(row=r_idx, column=3, value=row['manager'] or "—").alignment = align_left
|
ws3.cell(row=r_idx, column=3, value=row['manager'] or "—").alignment = align_left
|
||||||
ws3.cell(row=r_idx, column=4, value=row['total']).alignment = align_right
|
ws3.cell(row=r_idx, column=4, value=row['total']).alignment = align_right
|
||||||
|
|
||||||
# Statuts
|
|
||||||
by_st = row['by_status']
|
by_st = row['by_status']
|
||||||
ws3.cell(row=r_idx, column=5, value=by_st.get('in_preparation', 0)).alignment = align_right
|
ws3.cell(row=r_idx, column=5, value=by_st.get('in_preparation', 0)).alignment = align_right
|
||||||
ws3.cell(row=r_idx, column=6, value=by_st.get('to_be_approved', 0)).alignment = align_right
|
ws3.cell(row=r_idx, column=6, value=by_st.get('to_be_approved', 0)).alignment = align_right
|
||||||
|
|
@ -959,7 +1162,6 @@ def generate_pending_analysis_excel(months=24, thematic_code=None, contract_numb
|
||||||
ws3.cell(row=r_idx, column=11, value=by_st.get('on_pause', 0)).alignment = align_right
|
ws3.cell(row=r_idx, column=11, value=by_st.get('on_pause', 0)).alignment = align_right
|
||||||
ws3.cell(row=r_idx, column=12, value=by_st.get('to_be_corrected', 0)).alignment = align_right
|
ws3.cell(row=r_idx, column=12, value=by_st.get('to_be_corrected', 0)).alignment = align_right
|
||||||
|
|
||||||
# Retard & Age
|
|
||||||
c_retard = ws3.cell(row=r_idx, column=13, value=row['delayed'])
|
c_retard = ws3.cell(row=r_idx, column=13, value=row['delayed'])
|
||||||
c_retard.alignment = align_right
|
c_retard.alignment = align_right
|
||||||
if row['delayed'] > 0:
|
if row['delayed'] > 0:
|
||||||
|
|
@ -982,7 +1184,6 @@ def generate_pending_analysis_excel(months=24, thematic_code=None, contract_numb
|
||||||
cell.fill = fill_zebra
|
cell.fill = fill_zebra
|
||||||
r_idx += 1
|
r_idx += 1
|
||||||
|
|
||||||
# Total ligne
|
|
||||||
ws3.cell(row=r_idx, column=1, value="Total général").font = font_body_bold
|
ws3.cell(row=r_idx, column=1, value="Total général").font = font_body_bold
|
||||||
ws3.cell(row=r_idx, column=4, value=f"=SUM(D6:D{r_idx-1})").font = font_body_bold
|
ws3.cell(row=r_idx, column=4, value=f"=SUM(D6:D{r_idx-1})").font = font_body_bold
|
||||||
ws3.cell(row=r_idx, column=13, value=f"=SUM(M6:M{r_idx-1})").font = font_body_bold
|
ws3.cell(row=r_idx, column=13, value=f"=SUM(M6:M{r_idx-1})").font = font_body_bold
|
||||||
|
|
@ -991,7 +1192,6 @@ def generate_pending_analysis_excel(months=24, thematic_code=None, contract_numb
|
||||||
c.border = border_cell
|
c.border = border_cell
|
||||||
c.fill = fill_card
|
c.fill = fill_card
|
||||||
|
|
||||||
# Auto-width ws3
|
|
||||||
for col in ws3.columns:
|
for col in ws3.columns:
|
||||||
max_len = max(len(str(cell.value or '')) for cell in col)
|
max_len = max(len(str(cell.value or '')) for cell in col)
|
||||||
col_letter = get_column_letter(col[0].column)
|
col_letter = get_column_letter(col[0].column)
|
||||||
|
|
@ -1000,16 +1200,16 @@ def generate_pending_analysis_excel(months=24, thematic_code=None, contract_numb
|
||||||
ws3.column_dimensions['B'].width = 18
|
ws3.column_dimensions['B'].width = 18
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
# ONGLET 4 : DÉTAIL DES OBSERVATIONS
|
# ONGLET 5 : DÉTAIL DES OBSERVATIONS (TABLE EXCEL OFFICIELLE)
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
ws4 = wb.create_sheet(title="04_Detail_Observations")
|
ws4 = wb.create_sheet(title="05_Detail_Observations")
|
||||||
ws4.views.sheetView[0].showGridLines = True
|
ws4.views.sheetView[0].showGridLines = True
|
||||||
|
|
||||||
headers_obs_detail = [
|
headers_obs_detail = [
|
||||||
"Code Obs.", "Thématique", "Catégorie Asset", "Symptôme", "Statut",
|
"Code_Obs", "Thematique", "Categorie_Asset", "Symptome", "Statut",
|
||||||
"Date Création", "Âge (jours)", "Créé par", "Type Créateur", "Adresse",
|
"Date_Creation", "Age_Jours", "Cree_Par", "Type_Createur", "Adresse",
|
||||||
"Latitude", "Longitude", "Nb Photos", "Itv Liée (Code)", "Statut Itv Liée",
|
"Latitude", "Longitude", "Nb_Photos", "Itv_Liee_Code", "Statut_Itv_Liee",
|
||||||
"Diagnostic Cause de Blocage", "Description"
|
"Diagnostic_Cause", "Description"
|
||||||
]
|
]
|
||||||
for col_idx, h in enumerate(headers_obs_detail, 1):
|
for col_idx, h in enumerate(headers_obs_detail, 1):
|
||||||
c = ws4.cell(row=1, column=col_idx, value=h)
|
c = ws4.cell(row=1, column=col_idx, value=h)
|
||||||
|
|
@ -1061,9 +1261,12 @@ def generate_pending_analysis_excel(months=24, thematic_code=None, contract_numb
|
||||||
cell.fill = fill_zebra
|
cell.fill = fill_zebra
|
||||||
r_idx += 1
|
r_idx += 1
|
||||||
|
|
||||||
ws4.auto_filter.ref = f"A1:Q{max(r_idx-1, 1)}"
|
max_obs_row = max(r_idx - 1, 2)
|
||||||
|
# Déclaration de la Table Excel officielle
|
||||||
|
table_obs = Table(displayName="Table_Observations", ref=f"A1:Q{max_obs_row}")
|
||||||
|
table_obs.tableStyleInfo = TableStyleInfo(name="TableStyleLight1", showRowStripes=True)
|
||||||
|
ws4.add_table(table_obs)
|
||||||
|
|
||||||
# Widths ws4
|
|
||||||
for col in ws4.columns:
|
for col in ws4.columns:
|
||||||
max_len = max(len(str(cell.value or '')) for cell in col)
|
max_len = max(len(str(cell.value or '')) for cell in col)
|
||||||
col_letter = get_column_letter(col[0].column)
|
col_letter = get_column_letter(col[0].column)
|
||||||
|
|
@ -1074,18 +1277,18 @@ def generate_pending_analysis_excel(months=24, thematic_code=None, contract_numb
|
||||||
ws4.column_dimensions['Q'].width = 35
|
ws4.column_dimensions['Q'].width = 35
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
# ONGLET 5 : DÉTAIL DES INTERVENTIONS
|
# ONGLET 6 : DÉTAIL DES INTERVENTIONS (TABLE EXCEL OFFICIELLE)
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
ws5 = wb.create_sheet(title="05_Detail_Interventions")
|
ws5 = wb.create_sheet(title="06_Detail_Interventions")
|
||||||
ws5.views.sheetView[0].showGridLines = True
|
ws5.views.sheetView[0].showGridLines = True
|
||||||
|
|
||||||
headers_itv_detail = [
|
headers_itv_detail = [
|
||||||
"Code Itv", "Titre", "Thématique", "Catégorie Asset", "Symptôme", "Priorité",
|
"Code_Itv", "Titre", "Thematique", "Categorie_Asset", "Symptome", "Priorite",
|
||||||
"Type Maintenance", "Statut Actuel", "Date Création", "Âge Total (j)",
|
"Type_Maintenance", "Statut_Actuel", "Date_Creation", "Age_Total_Jours",
|
||||||
"Date Entrée Statut", "Jours dans Statut", "Date Début Prévue", "Date Fin Attendue",
|
"Date_Entree_Statut", "Jours_Dans_Statut", "Date_Debut_Prevue", "Date_Fin_Attendue",
|
||||||
"Statut Retard / SLA", "Prestataire Assigné", "Contrat", "Équipe", "Gestionnaire",
|
"Statut_Retard_SLA", "Prestataire_Assigne", "Contrat", "Equipe", "Gestionnaire",
|
||||||
"Motif Pause", "Détail Pause", "Raison Correction", "Dernière Activité", "Jours Inactivité",
|
"Motif_Pause", "Detail_Pause", "Raison_Correction", "Derniere_Activite", "Jours_Inactivite",
|
||||||
"Nb Docs/Photos", "Diagnostic Cause de Blocage", "Adresse", "Description"
|
"Nb_Docs_Photos", "Diagnostic_Cause", "Adresse", "Description"
|
||||||
]
|
]
|
||||||
for col_idx, h in enumerate(headers_itv_detail, 1):
|
for col_idx, h in enumerate(headers_itv_detail, 1):
|
||||||
c = ws5.cell(row=1, column=col_idx, value=h)
|
c = ws5.cell(row=1, column=col_idx, value=h)
|
||||||
|
|
@ -1156,9 +1359,12 @@ def generate_pending_analysis_excel(months=24, thematic_code=None, contract_numb
|
||||||
cell.fill = fill_zebra
|
cell.fill = fill_zebra
|
||||||
r_idx += 1
|
r_idx += 1
|
||||||
|
|
||||||
ws5.auto_filter.ref = f"A1:AB{max(r_idx-1, 1)}"
|
max_itv_row = max(r_idx - 1, 2)
|
||||||
|
# Déclaration de la Table Excel officielle
|
||||||
|
table_itv = Table(displayName="Table_Interventions", ref=f"A1:AB{max_itv_row}")
|
||||||
|
table_itv.tableStyleInfo = TableStyleInfo(name="TableStyleLight1", showRowStripes=True)
|
||||||
|
ws5.add_table(table_itv)
|
||||||
|
|
||||||
# Widths ws5
|
|
||||||
for col in ws5.columns:
|
for col in ws5.columns:
|
||||||
max_len = max(len(str(cell.value or '')) for cell in col)
|
max_len = max(len(str(cell.value or '')) for cell in col)
|
||||||
col_letter = get_column_letter(col[0].column)
|
col_letter = get_column_letter(col[0].column)
|
||||||
|
|
@ -1168,9 +1374,9 @@ def generate_pending_analysis_excel(months=24, thematic_code=None, contract_numb
|
||||||
ws5.column_dimensions['Z'].width = 45
|
ws5.column_dimensions['Z'].width = 45
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
# ONGLET 6 : GUIDE & DÉFINITIONS
|
# ONGLET 7 : GUIDE & DÉFINITIONS
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
ws6 = wb.create_sheet(title="06_Definitions_Guide")
|
ws6 = wb.create_sheet(title="07_Definitions_Guide")
|
||||||
ws6.views.sheetView[0].showGridLines = True
|
ws6.views.sheetView[0].showGridLines = True
|
||||||
|
|
||||||
ws6.merge_cells("A1:E2")
|
ws6.merge_cells("A1:E2")
|
||||||
|
|
@ -1189,16 +1395,18 @@ def generate_pending_analysis_excel(months=24, thematic_code=None, contract_numb
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"2. Signification des Diagnostics Automatiques (Observations)",
|
"2. Tableaux Croisés Dynamiques (TCD)",
|
||||||
|
[
|
||||||
|
("Onglet 02_Analyse_Thematiques", "Contient des matrices croisées pré-calculées avec formules dynamiques : Thématiques × Statuts Itv, Thématiques × Statuts Obs, Thématiques × Motifs de Pause."),
|
||||||
|
("Tables Excel Natives", "Les onglets '05_Detail_Observations' et '06_Detail_Interventions' sont configurés comme Tables Excel ('Table_Observations' et 'Table_Interventions'). Vous pouvez créer un nouveau TCD personnalisé dans Excel via 'Insertion > Tableau croisé dynamique' en 1 clic."),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"3. Signification des Diagnostics Automatiques",
|
||||||
[
|
[
|
||||||
("Orpheline ('À traiter' sans intervention)", "L'observation est validée mais n'a déclenché aucune intervention. Souvent dû à un symptôme sans auto-création activée, ou sans contrat par défaut."),
|
("Orpheline ('À traiter' sans intervention)", "L'observation est validée mais n'a déclenché aucune intervention. Souvent dû à un symptôme sans auto-création activée, ou sans contrat par défaut."),
|
||||||
("Désynchronisée ('En cours' avec intervention terminée/annulée)", "L'intervention associée a suivi son cycle de vie jusqu'à la fin, mais le statut de l'observation n'a pas été basculé en 'Traité' ou 'Clôturé'."),
|
("Désynchronisée ('En cours' avec intervention terminée/annulée)", "L'intervention associée a suivi son cycle de vie jusqu'à la fin, mais le statut de l'observation n'a pas été basculé en 'Traité' ou 'Clôturé'."),
|
||||||
("Brouillon abandonné", "Observation créée il y a plus de 15 jours en 'En préparation' sans validation ultérieure."),
|
("Brouillon abandonné", "Observation créée il y a plus de 15 jours en 'En préparation' sans validation ultérieure."),
|
||||||
]
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"3. Signification des Diagnostics Automatiques (Interventions)",
|
|
||||||
[
|
|
||||||
("Goulot d'approbation", "Intervention en 'À approuver' en attente de visa par le gestionnaire désigné."),
|
("Goulot d'approbation", "Intervention en 'À approuver' en attente de visa par le gestionnaire désigné."),
|
||||||
("Non prise en charge", "Intervention envoyée en 'À traiter' au prestataire sans action de prise en charge après plus de 10 jours."),
|
("Non prise en charge", "Intervention envoyée en 'À traiter' au prestataire sans action de prise en charge après plus de 10 jours."),
|
||||||
("Blocage en pause", "Intervention arrêtée temporairement pour commande de matériel, sous-traitant, devis, etc. Nécessite un suivi périodique."),
|
("Blocage en pause", "Intervention arrêtée temporairement pour commande de matériel, sous-traitant, devis, etc. Nécessite un suivi périodique."),
|
||||||
|
|
|
||||||
|
|
@ -193,14 +193,15 @@ class PendingAnalysisTests(TestCase):
|
||||||
def test_generate_pending_analysis_excel(self):
|
def test_generate_pending_analysis_excel(self):
|
||||||
wb = generate_pending_analysis_excel(months=24, output_format='workbook')
|
wb = generate_pending_analysis_excel(months=24, output_format='workbook')
|
||||||
|
|
||||||
# Vérification des 6 feuilles
|
# Vérification des 7 feuilles
|
||||||
expected_sheets = [
|
expected_sheets = [
|
||||||
"01_Synthese_KPI",
|
"01_Synthese_KPI",
|
||||||
"02_Diagnostics_Blocages",
|
"02_Analyse_Thematiques",
|
||||||
"03_Prestataires_Contrats",
|
"03_Diagnostics_Blocages",
|
||||||
"04_Detail_Observations",
|
"04_Prestataires_Contrats",
|
||||||
"05_Detail_Interventions",
|
"05_Detail_Observations",
|
||||||
"06_Definitions_Guide",
|
"06_Detail_Interventions",
|
||||||
|
"07_Definitions_Guide",
|
||||||
]
|
]
|
||||||
self.assertEqual(wb.sheetnames, expected_sheets)
|
self.assertEqual(wb.sheetnames, expected_sheets)
|
||||||
|
|
||||||
|
|
@ -209,16 +210,22 @@ class PendingAnalysisTests(TestCase):
|
||||||
self.assertIn("LOKO — AUDIT", ws1["A1"].value)
|
self.assertIn("LOKO — AUDIT", ws1["A1"].value)
|
||||||
self.assertEqual(ws1["A5"].value, "INTERVENTIONS EN COURS")
|
self.assertEqual(ws1["A5"].value, "INTERVENTIONS EN COURS")
|
||||||
|
|
||||||
# Vérification du détail des observations (feuille 4)
|
# Vérification de l'onglet d'analyse thématique
|
||||||
ws4 = wb["04_Detail_Observations"]
|
ws_th = wb["02_Analyse_Thematiques"]
|
||||||
self.assertEqual(ws4["A1"].value, "Code Obs.")
|
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
|
# 3 lignes de données + 1 en-tête = 4 lignes
|
||||||
self.assertEqual(ws4.max_row, 4)
|
self.assertEqual(ws4.max_row, 4)
|
||||||
|
self.assertIn("Table_Observations", ws4.tables)
|
||||||
|
|
||||||
# Vérification du détail des interventions (feuille 5)
|
# Vérification du détail des interventions (feuille 6)
|
||||||
ws5 = wb["05_Detail_Interventions"]
|
ws5 = wb["06_Detail_Interventions"]
|
||||||
self.assertEqual(ws5["A1"].value, "Code Itv")
|
self.assertEqual(ws5["A1"].value, "Code_Itv")
|
||||||
self.assertEqual(ws5.max_row, 4)
|
self.assertEqual(ws5.max_row, 4)
|
||||||
|
self.assertIn("Table_Interventions", ws5.tables)
|
||||||
|
|
||||||
def test_analyze_pending_items_command(self):
|
def test_analyze_pending_items_command(self):
|
||||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||||
|
|
@ -230,4 +237,4 @@ class PendingAnalysisTests(TestCase):
|
||||||
|
|
||||||
# Recharger le fichier sauvegardé pour vérifier son intégrité
|
# Recharger le fichier sauvegardé pour vérifier son intégrité
|
||||||
wb = openpyxl.load_workbook(out_file)
|
wb = openpyxl.load_workbook(out_file)
|
||||||
self.assertEqual(len(wb.sheetnames), 6)
|
self.assertEqual(len(wb.sheetnames), 7)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue