"""Import applications depuis le fichier IODA Sanef. Lit `deploy/ServeursAssociesAuxServicesIODA_*.xlsx` (sheet "Services Metiers") et UPSERT dans la table `applications` en utilisant `ioda_libelle` comme cle. Usage: python tools/import_applications_ioda.py [chemin_fichier.xlsx] Si chemin omis: cherche dans deploy/ le plus recent ServeursAssoci*AuxServicesIODA_*.xlsx. """ import os import sys import glob from pathlib import Path from datetime import datetime import openpyxl from sqlalchemy import create_engine, text ROOT = Path(__file__).resolve().parent.parent DATABASE_URL = (os.getenv("DATABASE_URL_DEMO") or os.getenv("DATABASE_URL") or "postgresql://patchcenter:PatchCenter2026!@localhost:5432/patchcenter_db") def find_ioda_file(): """Trouve le fichier IODA le plus recent dans deploy/.""" pattern = str(ROOT / "deploy" / "ServeursAssoci*AuxServicesIODA_*.xlsx") files = sorted(glob.glob(pattern)) if not files: # Fallback: deploy/ sans variantes accent files = sorted(glob.glob(str(ROOT / "deploy" / "*IODA*.xlsx"))) return files[-1] if files else None def parse_services(xlsx_path): """Renvoie liste de dicts {libelle, lib_court, code_pos, ...} depuis sheet 'Services Metiers'.""" wb = openpyxl.load_workbook(xlsx_path, data_only=True) ws_name = next((s for s in wb.sheetnames if "Services" in s and "Metier" in s), None) if not ws_name: raise SystemExit(f"[ERR] Sheet 'Services Metiers' introuvable. Sheets: {wb.sheetnames}") ws = wb[ws_name] services = [] for i, row in enumerate(ws.iter_rows(values_only=True)): # Header sur 2 lignes (i=0 mega-header, i=1 vrais en-tetes) if i < 2: continue # Cols: 0 Actions, 1 Type, 2 Statut, 3 Code Zone POS, 4 Lib court, 5 Libelle, # 6 Alias, 7 Editeur, 8 Description, 9 Commentaire, 10 Nb composants, # 11 Perimetre, 12 Dept/Domaine, 13 Resp Metier, 14 Resp DSI libelle = row[5] if not libelle or not str(libelle).strip(): continue def s(v): if v is None: return None t = str(v).strip() return t or None services.append({ "libelle": str(libelle).strip(), "type": s(row[1]), "statut": s(row[2]), "code_pos": s(row[3]), "lib_court": s(row[4]), "alias": s(row[6]), "editeur": s(row[7]), "description": s(row[8]), "commentaire": s(row[9]), "nb_components": int(row[10]) if row[10] and str(row[10]).strip().isdigit() else None, "perimetre": s(row[11]), "dept_domaine": s(row[12]), "resp_metier": s(row[13]), "resp_dsi": s(row[14]), }) return services SQL_FIND = text(""" SELECT id FROM applications WHERE ioda_libelle = :libelle OR (ioda_libelle IS NULL AND nom_court = :nom_court) LIMIT 1 """) SQL_UPDATE = text(""" UPDATE applications SET nom_complet = :nom_complet, description = COALESCE(:description, description), editeur = COALESCE(:editeur, editeur), ioda_libelle = :libelle, ioda_lib_court = :lib_court, ioda_code_pos = :code_pos, ioda_type = :type, ioda_statut = :statut, ioda_alias = :alias, ioda_perimetre = :perimetre, ioda_dept_domaine = :dept_domaine, ioda_resp_metier = :resp_metier, ioda_resp_dsi = :resp_dsi, ioda_nb_components = :nb_components, ioda_commentaire = :commentaire, ioda_imported_at = now(), updated_at = now() WHERE id = :id """) SQL_INSERT = text(""" INSERT INTO applications ( nom_court, nom_complet, description, editeur, criticite, ioda_libelle, ioda_lib_court, ioda_code_pos, ioda_type, ioda_statut, ioda_alias, ioda_perimetre, ioda_dept_domaine, ioda_resp_metier, ioda_resp_dsi, ioda_nb_components, ioda_commentaire, ioda_imported_at, created_at, updated_at ) VALUES ( :nom_court, :nom_complet, :description, :editeur, 'standard', :libelle, :lib_court, :code_pos, :type, :statut, :alias, :perimetre, :dept_domaine, :resp_metier, :resp_dsi, :nb_components, :commentaire, now(), now(), now() ) """) def main(): xlsx = sys.argv[1] if len(sys.argv) > 1 else find_ioda_file() if not xlsx or not os.path.exists(xlsx): print(f"[ERR] Fichier IODA introuvable. Place-le dans deploy/ (ex: deploy/ServeursAssociesAuxServicesIODA_YYYYMMDD.xlsx)") sys.exit(1) print(f"[INFO] Fichier: {xlsx}") services = parse_services(xlsx) print(f"[INFO] Services parses: {len(services)}") engine = create_engine(DATABASE_URL) print(f"[INFO] DB: {DATABASE_URL.rsplit('@', 1)[-1]}") inserted = updated = skipped = 0 with engine.begin() as conn: used_nom_court = set() for svc in services: base_nc = (svc["lib_court"] or svc["libelle"])[:50] params = { **svc, "nom_court": base_nc, "nom_complet": svc["libelle"][:200], } existing = conn.execute(SQL_FIND, params).fetchone() if existing: conn.execute(SQL_UPDATE, {**params, "id": existing[0]}) updated += 1 else: # Eviter collision nom_court avec autre app non-IODA en BDD # ou avec un autre service IODA deja traite cette session nc = base_nc if nc in used_nom_court: # ajout suffixe POS pour unicite suf = svc.get("code_pos") or "X" nc = (base_nc[:46] + "-" + suf)[:50] used_nom_court.add(nc) # Verif collision DB row = conn.execute(text("SELECT 1 FROM applications WHERE nom_court=:nc"), {"nc": nc}).fetchone() if row: nc = (base_nc[:42] + "-IODA-" + (svc.get("code_pos") or "X"))[:50] params["nom_court"] = nc conn.execute(SQL_INSERT, params) inserted += 1 print(f"[OK] Termine — INSERT: {inserted} | UPDATE: {updated} | SKIP: {skipped}") print(f"[INFO] Verif: SELECT COUNT(*) FROM applications WHERE ioda_libelle IS NOT NULL;") if __name__ == "__main__": main()