#!/usr/bin/env python3 # -*- coding: utf-8 -*- """Limpieza de nombres de pacientes (reversible). Pasos: 1) Respaldo CSV id -> nombre original. 2) Normaliza: control chars, frases marcador (no usar exp...), barras, espacios, signos. 3) Title Case con particulas en minuscula (de, del, de la, los, las, y, etc.). 4) Dry-run por defecto; con --apply actualiza solo las filas que cambian. """ import csv import os import re import sys import unicodedata from datetime import datetime import psycopg2 DSN = "host=localhost dbname=skeen_odoo user=skeen password=skeen_dev_2026" PARTICULAS = { 'de', 'del', 'la', 'las', 'los', 'y', 'e', 'da', 'do', 'das', 'dos', 'di', 'van', 'von', 'der', 'den', 'san', 'santa', 'mc', } # Frases marcador inyectadas por el personal dentro del nombre MARKER_RE = re.compile( r'\bno\s*usar(\s+(este\s+)?exp(?:ediente)?)?\b[!.,;:/\\-]*', re.IGNORECASE, ) ERROR_RE = re.compile(r'\berror\b[!.,;:/\\-]*', re.IGNORECASE) SLASHES_RE = re.compile(r'[/\\]+') MULTISPACE_RE = re.compile(r'\s{2,}') LEAD_TRAIL_JUNK_RE = re.compile(r'^[\s.\-_,;:]+|[\s.\-_,;:]+$') CONTROL_RE = re.compile(r'[\x00-\x1f\x7f]') def cap_token(tok, first): low = tok.lower() if not first and low in PARTICULAS: return low # Mantener siglas cortas (iniciales) en mayuscula: J, GPE -> J, Gpe if len(tok) <= 1: return tok.upper() return low.capitalize() def clean_name(raw): if raw is None: return raw s = CONTROL_RE.sub('', raw) s = SLASHES_RE.sub(' ', s) # barras -> espacio s = MARKER_RE.sub(' ', s) # quita "no usar exp..." s = ERROR_RE.sub(' ', s) # quita "ERROR" suelto s = s.replace('.', ' ') # puntos (iniciales, GPE.) -> espacio s = MULTISPACE_RE.sub(' ', s) s = LEAD_TRAIL_JUNK_RE.sub('', s).strip() s = MULTISPACE_RE.sub(' ', s) if not s: return s toks = s.split(' ') toks = [cap_token(t, i == 0) for i, t in enumerate(toks) if t] return ' '.join(toks) def main(): apply = '--apply' in sys.argv conn = psycopg2.connect(DSN) conn.autocommit = False cur = conn.cursor() cur.execute("SELECT id, name FROM res_partner WHERE is_patient ORDER BY id") rows = cur.fetchall() changes = [] for rid, name in rows: new = clean_name(name) if new != name: changes.append((rid, name, new)) ts = datetime.now().strftime('%Y%m%d_%H%M%S') backup_path = f'/root/migracion/datos/name_backup_{ts}.csv' with open(backup_path, 'w', newline='', encoding='utf-8') as f: w = csv.writer(f) w.writerow(['id', 'name_original']) for rid, name, _ in changes: w.writerow([rid, name]) print(f'Total pacientes: {len(rows)}') print(f'A modificar: {len(changes)}') print(f'Sin cambios: {len(rows) - len(changes)}') print(f'Respaldo: {backup_path}') print() print('--- Ejemplos antes -> despues (30) ---') shown = 0 # prioriza los que tenian marcador o signos def score(c): n = c[1] or '' return (('no usar' in n.lower()) * 10) + (('/' in n) * 5) + ((' ' in n) * 2) for rid, old, new in sorted(changes, key=score, reverse=True)[:30]: print(f' [{rid}] "{old}" -> "{new}"') shown += 1 print(f' ({shown} ejemplos)') if not apply: print('\nDRY-RUN: no se aplicaron cambios. Ejecuta con --apply para aplicar.') conn.rollback() cur.close(); conn.close() return updated = 0 for rid, old, new in changes: cur.execute("UPDATE res_partner SET name = %s WHERE id = %s", (new, rid)) updated += 1 conn.commit() print(f'\nAPLICADO: {updated} nombres actualizados.') cur.close(); conn.close() if __name__ == '__main__': main()