From 5e5803afa2b8cd827dec8a80bdb70ec288f643c4 Mon Sep 17 00:00:00 2001 From: Admin MPCZ Date: Thu, 7 May 2026 21:54:50 +0200 Subject: [PATCH] feat(pct/cc): fallback match par nom dans contacts si servers.responsable_email/referent_email vides Cas reel: les serveurs SANEF ont responsable_nom rempli (ex 'Laurent DELCOUR') mais responsable_email vide. Les emails ne sont QUE dans la table contacts. Solution: 4eme source = match par nom dans la table contacts. - On collecte les noms texte presents en DB: * servers.responsable_nom / referent_nom (si email correspondant vide) * patch_planning_import_rows.responsable_domaine_dts / referent_technique (split sur '/', '-', 'et', 'ou', etc. pour les co-responsables) - Pour chaque nom, match insensible casse: exact OR contains - Recupere les emails associes dans contacts --- app/routers/planning_import.py | 43 ++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/app/routers/planning_import.py b/app/routers/planning_import.py index 81f3fdd..cc06b8d 100644 --- a/app/routers/planning_import.py +++ b/app/routers/planning_import.py @@ -1151,17 +1151,56 @@ def _fetch_pct_cc_emails(db, row_ids): _add(c.name, c.email) # 3) Champs legacy texte sur servers (responsable_email / referent_email) - # + tentative de récupérer le nom propre depuis la table contacts par email legacy = db.execute(text(f""" SELECT DISTINCT s.responsable_nom, s.responsable_email, - s.referent_nom, s.referent_email + s.referent_nom, s.referent_email, + r.responsable_domaine_dts, r.referent_technique FROM patch_planning_import_rows r JOIN servers s ON s.id = r.server_id WHERE r.id IN ({placeholders}) """)).fetchall() + legacy_names = set() # noms à matcher dans contacts si email manquant for row in legacy: _add(row.responsable_nom, row.responsable_email) _add(row.referent_nom, row.referent_email) + # Si email vide côté servers, on retiendra le nom pour matcher dans contacts + if not (row.responsable_email or "").strip() and (row.responsable_nom or "").strip(): + legacy_names.add(row.responsable_nom.strip()) + if not (row.referent_email or "").strip() and (row.referent_nom or "").strip(): + legacy_names.add(row.referent_nom.strip()) + # Aussi : noms texte côté patch_planning_import_rows (responsable_domaine_dts, + # referent_technique) — peuvent contenir plusieurs noms séparés par '/' ou '-' + for nm_field in (row.responsable_domaine_dts, row.referent_technique): + if not nm_field: + continue + for part in re.split(r"[/,;\n]| - | et | ou | & ", str(nm_field)): + part = part.strip() + if len(part) >= 4 and re.search(r"[A-Za-zÀ-ÿ]{3}", part): + legacy_names.add(part) + + # 4) Match par nom dans la table contacts pour les responsables/référents + # qui n'ont pas d'email côté servers. Match insensible casse + accents. + if legacy_names: + # On construit une condition ILIKE pour chaque nom + ses variantes simples + names_list = list(legacy_names)[:50] # limite de sécurité + params = {} + ors = [] + for i, n in enumerate(names_list): + params[f"n{i}"] = n + # Match exact (case insensible) ou nom contenu (utile pour 'DUFOUR Antoine' vs 'Antoine Dufour') + ors.append(f"LOWER(c.name) = LOWER(:n{i})") + ors.append(f"LOWER(c.name) LIKE LOWER('%' || :n{i} || '%')") + sql = ( + "SELECT DISTINCT c.name, c.email FROM contacts c " + "WHERE c.email IS NOT NULL AND c.email <> '' " + f"AND ({' OR '.join(ors)})" + ) + try: + matched = db.execute(text(sql), params).fetchall() + for c in matched: + _add(c.name, c.email) + except Exception as e: + print(f"[pct_cc] match par nom failed: {e}") out.sort(key=lambda c: c["name"].lower()) return out