Etat/Environnement dropdowns alignes strict iTop SANEF

Etat: 6 valeurs lifecycle uniquement (Production, Implémentation,
Stock, Obsolète, prêt, tests). Suppression des valeurs condition
(Nouveau, Cassé, En panne, etc.) et de EOL qui n'existent pas
dans iTop SANEF.

Environnement: 7 valeurs iTop (Développement, Intégration, Pré-Prod,
Production, Recette, Test, Formation). Filtre env bascule de
e.code (legacy) vers s.environnement.

tools/import_etat_itop.py:
- CHECK 6 valeurs lifecycle + NULL
- Migration mappe les anciennes condition/EOL -> NULL
- Lit Status en priorite dans le CSV (lifecycle), fallback Etat
- Fix format print pour None

tools/import_sanef_*.py: ITOP_ETATS reduit a 6 valeurs
This commit is contained in:
Pierre & Lumière 2026-04-14 18:48:30 +02:00
parent 753d4076c9
commit 1c2d0b958e
6 changed files with 39 additions and 43 deletions

View File

@ -92,7 +92,7 @@ def update_server_ips(db, server_id, ip_reelle, ip_connexion):
SORT_COLS = { SORT_COLS = {
"hostname": "s.hostname", "hostname": "s.hostname",
"env": "e.name", "env": "s.environnement",
"domaine": "d.name", "domaine": "d.name",
"tier": "s.tier", "tier": "s.tier",
"etat": "s.etat", "etat": "s.etat",
@ -111,7 +111,7 @@ def list_servers(db, filters, page=1, per_page=50, sort="hostname", sort_dir="as
if filters.get("domain"): if filters.get("domain"):
where.append("d.code = :domain"); params["domain"] = filters["domain"] where.append("d.code = :domain"); params["domain"] = filters["domain"]
if filters.get("env"): if filters.get("env"):
where.append("e.code = :env"); params["env"] = filters["env"] where.append("s.environnement = :env"); params["env"] = filters["env"]
if filters.get("tier"): if filters.get("tier"):
where.append("s.tier = :tier"); params["tier"] = filters["tier"] where.append("s.tier = :tier"); params["tier"] = filters["tier"]
if filters.get("etat"): if filters.get("etat"):

View File

@ -58,7 +58,7 @@
<div> <div>
<label class="text-xs text-gray-500">Etat</label> <label class="text-xs text-gray-500">Etat</label>
<select name="etat" class="w-full"> <select name="etat" class="w-full">
{% for e in ['Production','Implémentation','Stock','Obsolète','EOL','prêt','tests','Nouveau','En panne','Cassé','Cédé','A récupérer','Perdu','Recyclé','Occasion','A détruire','Volé'] %}<option value="{{ e }}" {% if e == s.etat %}selected{% endif %}>{{ e }}</option>{% endfor %} {% for e in ['Production','Implémentation','Stock','Obsolète','prêt','tests'] %}<option value="{{ e }}" {% if e == s.etat %}selected{% endif %}>{{ e }}</option>{% endfor %}
</select> </select>
</div> </div>
<div> <div>

View File

@ -30,13 +30,13 @@
{% for d in domains_list %}<option value="{{ d.code }}" {% if filters.domain == d.code %}selected{% endif %}>{{ d.name }}</option>{% endfor %} {% for d in domains_list %}<option value="{{ d.code }}" {% if filters.domain == d.code %}selected{% endif %}>{{ d.name }}</option>{% endfor %}
</select> </select>
<select name="env" onchange="this.form.submit()"><option value="">Env</option> <select name="env" onchange="this.form.submit()"><option value="">Env</option>
{% for e in envs_list %}<option value="{{ e.code }}" {% if filters.env == e.code %}selected{% endif %}>{{ e.name }}</option>{% endfor %} {% for e in ['Développement','Intégration','Pré-Prod','Production','Recette','Test','Formation'] %}<option value="{{ e }}" {% if filters.env == e %}selected{% endif %}>{{ e }}</option>{% endfor %}
</select> </select>
<select name="tier" onchange="this.form.submit()"><option value="">Tier</option> <select name="tier" onchange="this.form.submit()"><option value="">Tier</option>
{% for t in ['tier0','tier1','tier2','tier3'] %}<option value="{{ t }}" {% if filters.tier == t %}selected{% endif %}>{{ t }}</option>{% endfor %} {% for t in ['tier0','tier1','tier2','tier3'] %}<option value="{{ t }}" {% if filters.tier == t %}selected{% endif %}>{{ t }}</option>{% endfor %}
</select> </select>
<select name="etat" onchange="this.form.submit()"><option value="">Etat</option> <select name="etat" onchange="this.form.submit()"><option value="">Etat</option>
{% for e in ['Production','Implémentation','Stock','Obsolète','EOL','prêt','tests','Nouveau','En panne','Cassé','Cédé','A récupérer','Perdu','Recyclé','Occasion','A détruire','Volé'] %}<option value="{{ e }}" {% if filters.etat == e %}selected{% endif %}>{{ e }}</option>{% endfor %} {% for e in ['Production','Implémentation','Stock','Obsolète','prêt','tests'] %}<option value="{{ e }}" {% if filters.etat == e %}selected{% endif %}>{{ e }}</option>{% endfor %}
</select> </select>
<select name="os" onchange="this.form.submit()"><option value="">OS</option> <select name="os" onchange="this.form.submit()"><option value="">OS</option>
<option value="linux" {% if filters.os == 'linux' %}selected{% endif %}>Linux</option> <option value="linux" {% if filters.os == 'linux' %}selected{% endif %}>Linux</option>
@ -82,7 +82,7 @@ const bulkValues = {
domain_code: [{% for d in domains_list %}{v:"{{ d.code }}", l:"{{ d.name }}"},{% endfor %}], domain_code: [{% for d in domains_list %}{v:"{{ d.code }}", l:"{{ d.name }}"},{% endfor %}],
env_code: [{% for e in envs_list %}{v:"{{ e.code }}", l:"{{ e.name }}"},{% endfor %}], env_code: [{% for e in envs_list %}{v:"{{ e.code }}", l:"{{ e.name }}"},{% endfor %}],
tier: [{v:"tier0",l:"tier0"},{v:"tier1",l:"tier1"},{v:"tier2",l:"tier2"},{v:"tier3",l:"tier3"}], tier: [{v:"tier0",l:"tier0"},{v:"tier1",l:"tier1"},{v:"tier2",l:"tier2"},{v:"tier3",l:"tier3"}],
etat: [{v:"Production",l:"Production"},{v:"Implémentation",l:"Implémentation"},{v:"Stock",l:"Stock"},{v:"Obsolète",l:"Obsolète"},{v:"EOL",l:"EOL"},{v:"prêt",l:"prêt"},{v:"tests",l:"tests"},{v:"Nouveau",l:"Nouveau"},{v:"En panne",l:"En panne"},{v:"Cassé",l:"Cassé"},{v:"Cédé",l:"Cédé"},{v:"A récupérer",l:"A récupérer"},{v:"Perdu",l:"Perdu"},{v:"Recyclé",l:"Recyclé"},{v:"Occasion",l:"Occasion"},{v:"A détruire",l:"A détruire"},{v:"Volé",l:"Volé"}], etat: [{v:"Production",l:"Production"},{v:"Implémentation",l:"Implémentation"},{v:"Stock",l:"Stock"},{v:"Obsolète",l:"Obsolète"},{v:"prêt",l:"prêt"},{v:"tests",l:"tests"}],
patch_os_owner: [{v:"secops",l:"secops"},{v:"ipop",l:"ipop"},{v:"na",l:"na"}], patch_os_owner: [{v:"secops",l:"secops"},{v:"ipop",l:"ipop"},{v:"na",l:"na"}],
licence_support: [{v:"active",l:"active"},{v:"obsolete",l:"obsolete"},{v:"els",l:"els"}], licence_support: [{v:"active",l:"active"},{v:"obsolete",l:"obsolete"},{v:"els",l:"els"}],
}; };

View File

@ -21,49 +21,51 @@ from sqlalchemy import create_engine, text
DATABASE_URL = os.getenv("DATABASE_URL_DEMO") or os.getenv("DATABASE_URL") \ DATABASE_URL = os.getenv("DATABASE_URL_DEMO") or os.getenv("DATABASE_URL") \
or "postgresql://patchcenter:PatchCenter2026!@localhost:5432/patchcenter_demo" or "postgresql://patchcenter:PatchCenter2026!@localhost:5432/patchcenter_demo"
# Etats lifecycle iTop (dropdown unique de PatchCenter)
ITOP_ETATS = [ ITOP_ETATS = [
# Lifecycle "Production", "Implémentation", "Stock", "Obsolète", "prêt", "tests",
"Production", "Implémentation", "Stock", "Obsolète", "EOL", "prêt", "tests",
# Condition
"Nouveau", "A récupérer", "Cassé", "Cédé", "En panne",
"Perdu", "Recyclé", "Occasion", "A détruire", "Volé",
] ]
# Migration valeurs existantes (lowercase) -> labels iTop verbatim # Migration anciennes valeurs (lowercase OU condition iTop) -> NULL ou lifecycle verbatim
LEGACY_MAP = { LEGACY_MAP = {
# lowercase codes (ancien schema)
"production": "Production", "production": "Production",
"implementation": "Implémentation", "implementation": "Implémentation",
"stock": "Stock", "stock": "Stock",
"obsolete": "Obsolète", "obsolete": "Obsolète",
"eol": "EOL",
"pret": "prêt", "pret": "prêt",
"tests": "tests", "tests": "tests",
"nouveau": "Nouveau", # Verbatim (idempotent)
"casse": "Cassé", "Production": "Production",
"cede": "Cédé", "Implémentation": "Implémentation",
"en_panne": "En panne", "Stock": "Stock",
"a_recuperer": "A récupérer", "Obsolète": "Obsolète",
"perdu": "Perdu", "prêt": "prêt",
"recycle": "Recyclé", # Les valeurs "condition" iTop ne sont pas des lifecycle -> NULL
"occasion": "Occasion", # (Nouveau, Cassé, En panne, ... : import_etat_itop les mettra a NULL
"a_detruire": "A détruire", # et ces serveurs seront re-syncs depuis la colonne Status du CSV)
"vole": "Volé", "eol": None, "EOL": None,
"nouveau": None, "Nouveau": None,
"casse": None, "Cassé": None,
"cede": None, "Cédé": None,
"en_panne": None, "En panne": None,
"a_recuperer": None, "A récupérer": None,
"perdu": None, "Perdu": None,
"recycle": None, "Recyclé": None,
"occasion": None, "Occasion": None,
"a_detruire": None, "A détruire": None,
"vole": None, "Volé": None,
} }
def norm_etat(raw): def norm_etat(raw):
"""Retourne l'etat lifecycle iTop verbatim, ou None si unknown/condition."""
if not raw: if not raw:
return None return None
s = raw.strip() s = raw.strip()
if not s or s in ("-", "(null)"): if not s or s in ("-", "(null)"):
return None return None
if s in ITOP_ETATS: return s if s in ITOP_ETATS else None
return s
# Tolerance case-insensitive + sans accent pour les alias courants
low = s.lower()
if low in LEGACY_MAP:
return LEGACY_MAP[low]
return None
def main(): def main():
@ -81,11 +83,12 @@ def main():
if not args.dry_run: if not args.dry_run:
conn.execute(text("ALTER TABLE servers DROP CONSTRAINT IF EXISTS servers_etat_check")) conn.execute(text("ALTER TABLE servers DROP CONSTRAINT IF EXISTS servers_etat_check"))
print("[INFO] Migration lowercase -> iTop verbatim...") print("[INFO] Migration valeurs existantes -> lifecycle iTop ou NULL...")
for old, new in LEGACY_MAP.items(): for old, new in LEGACY_MAP.items():
cnt = conn.execute(text("SELECT COUNT(*) FROM servers WHERE etat=:o"), {"o": old}).scalar() cnt = conn.execute(text("SELECT COUNT(*) FROM servers WHERE etat=:o"), {"o": old}).scalar()
if cnt: if cnt:
print(f" {old:18s} -> {new:18s} : {cnt}") label = new if new is not None else "NULL"
print(f" {old:18s} -> {label:18s} : {cnt}")
if not args.dry_run: if not args.dry_run:
conn.execute(text("UPDATE servers SET etat=:n WHERE etat=:o"), {"n": new, "o": old}) conn.execute(text("UPDATE servers SET etat=:n WHERE etat=:o"), {"n": new, "o": old})
@ -113,7 +116,8 @@ def main():
hostname = (r.get("Nom") or r.get("Hostname") or "").strip() hostname = (r.get("Nom") or r.get("Hostname") or "").strip()
if not hostname or not any(c.isalpha() for c in hostname): if not hostname or not any(c.isalpha() for c in hostname):
continue continue
raw = (r.get("Etat") or r.get("État") or "").strip() # Lit Status (lifecycle iTop) en priorite, fallback Etat
raw = (r.get("Status") or r.get("Etat") or r.get("État") or "").strip()
new_etat = norm_etat(raw) new_etat = norm_etat(raw)
if raw and new_etat is None and raw not in ("-", "(null)"): if raw and new_etat is None and raw not in ("-", "(null)"):
unknown.add(raw); continue unknown.add(raw); continue

View File

@ -26,11 +26,7 @@ def norm_os_family(famille):
return None return None
ITOP_ETATS = { ITOP_ETATS = {"Production", "Implémentation", "Stock", "Obsolète", "prêt", "tests"}
"Production", "Implémentation", "Stock", "Obsolète", "EOL", "prêt", "tests",
"Nouveau", "A récupérer", "Cassé", "Cédé", "En panne",
"Perdu", "Recyclé", "Occasion", "A détruire", "Volé",
}
def norm_etat(status, etat): def norm_etat(status, etat):

View File

@ -25,11 +25,7 @@ def norm_os_family(famille):
return None return None
ITOP_ETATS = { ITOP_ETATS = {"Production", "Implémentation", "Stock", "Obsolète", "prêt", "tests"}
"Production", "Implémentation", "Stock", "Obsolète", "EOL", "prêt", "tests",
"Nouveau", "A récupérer", "Cassé", "Cédé", "En panne",
"Perdu", "Recyclé", "Occasion", "A détruire", "Volé",
}
def norm_etat(status, etat): def norm_etat(status, etat):