"""Utilitaires pour la normalisation des numéros de lot.""" import re from typing import Any #: Largeur canonique d'un numéro de lot (ex: "06", "17"). LOT_NUMERO_WIDTH = 2 #: Numéro utilisé quand aucun lot n'est identifiable. LOT_NUMERO_INCONNU = "00" def normalize_lot_numero(value: str | int | None) -> str | None: """Normalise un numéro de lot sur exactement 2 chiffres. Les PDF et les saisies manuelles utilisent des formats variés ("6", "06", "0006"). Tous doivent aboutir au même numéro canonique, sinon un même lot est dupliqué en base. Gère: - "0006" -> "06" - "6" -> "06" - "06" -> "06" - "0020" -> "20" - "0000" -> "00" Les numéros comportant plus de 2 chiffres significatifs (ex: "0123") sont conservés sans leurs zéros de tête ("123") : les tronquer perdrait de l'information. Args: value: Numéro de lot dans un format quelconque Returns: Numéro sur 2 chiffres, ou None si aucun chiffre n'est présent """ if value is None: return None digits = re.sub(r"\D", "", str(value)) if not digits: return None significant = digits.lstrip("0") if not significant: return LOT_NUMERO_INCONNU return significant.zfill(LOT_NUMERO_WIDTH) def extract_lot_numero_from_description(description: str) -> str | None: """Extrait le numéro de lot depuis la description d'une opération. Les codes lots suivent le format {Lettre}{Numéro} où la lettre identifie l'immeuble (M=Marietton, S=Servient, B=Bloch…) et le numéro le lot. Exemples: - "M06 - Commande moteur pompe" -> "06" - "S05 - Mise en service" -> "05" - "B01 - Plaques" -> "01" Args: description: Description de l'opération Returns: Numéro sur 2 chiffres (ex: "06") ou None si non trouvé """ if not description: return None # Pattern: lettre majuscule + (espace optionnelle) + 1-2 chiffres, terminé par # un espace, un tiret ou la fin. Gère "S10 - ...", "S 17 - ..." (espace dans le # code) et "S01 SOLDE ..." (code lot non suivi d'un tiret). match = re.search(r"\b[A-Z]\s*(\d{1,2})(?=[\s-]|$)", description) if match: return normalize_lot_numero(match.group(1)) return None def normalize_extraction_lots(data: dict[str, Any]) -> dict[str, Any]: """Normalise, sur place, tous les numéros de lot d'une extraction. Couvre les deux emplacements où un numéro de lot apparaît: - ``situation_locataires[].lot.numero`` - ``recapitulatif_operations[].lot_numero`` Utile pour les extractions éditées à la main via l'API, où le numéro saisi peut arriver sous n'importe quelle forme. Args: data: Extraction (metadata, situation_locataires, recapitulatif_operations) Returns: Le même dict, numéros de lot normalisés """ if not isinstance(data, dict): return data for situation in data.get("situation_locataires") or []: lot = situation.get("lot") if isinstance(lot, dict) and lot.get("numero") is not None: lot["numero"] = normalize_lot_numero(lot["numero"]) for operation in data.get("recapitulatif_operations") or []: if isinstance(operation, dict) and operation.get("lot_numero") is not None: operation["lot_numero"] = normalize_lot_numero(operation["lot_numero"]) return data