diff --git a/src/plesna_gerance/parsers/locataires_table.py b/src/plesna_gerance/parsers/locataires_table.py index f7ee34a..39e83cd 100644 --- a/src/plesna_gerance/parsers/locataires_table.py +++ b/src/plesna_gerance/parsers/locataires_table.py @@ -88,8 +88,9 @@ def _rows_from_page(page) -> list[dict]: ne contient pas ce tableau. """ # Repérer le tableau « situation locataires » (en-tête commençant par « Locataires »). + candidates = page.find_tables() table = None - for candidate in page.find_tables(): + for candidate in candidates: header = candidate.rows[0].cells if header and header[0] is not None: first = _strip_accents(page.crop(header[0]).extract_text() or "") @@ -109,7 +110,20 @@ def _rows_from_page(page) -> list[dict]: return i return None - words = page.crop(table.bbox).extract_words() + # Quand un filet de séparation manque entre deux lots, pdfplumber détecte + # la table logique en plusieurs fragments dont les bbox se touchent sans + # se recouvrir : une ligne à cheval sur la coupure tombe alors hors des + # deux bbox et serait perdue. On étend donc la zone scannée jusqu'au bas + # du fragment le plus bas partageant la même marge gauche que l'ancre, + # pour balayer une zone continue plutôt que des bbox disjointes. + x0, top, x1, bottom = table.bbox + for candidate in candidates: + if candidate is table: + continue + if abs(candidate.bbox[0] - x0) <= 2: + bottom = max(bottom, candidate.bbox[3]) + + words = page.crop((x0, top, x1, bottom)).extract_words() words.sort(key=lambda w: (round((w["top"] + w["bottom"]) / 2, 1), w["x0"])) # Regrouper les mots en lignes visuelles (clustering vertical tolérant). @@ -271,6 +285,11 @@ def extract_situation_locataires_from_pdf(pdf_path: str) -> list[dict]: _fill_totaux(current, row) continue + # Ligne de total général de fin de tableau (« TOTAUX », tout en + # majuscules) : ne concerne aucun lot, à ignorer. + if loc == "TOTAUX": + continue + # Solde Antérieur (libellé + montant dans la colonne période). if periode.startswith("Solde Antérieur"): montant = _num(periode) @@ -307,14 +326,22 @@ def extract_situation_locataires_from_pdf(pdf_path: str) -> list[dict]: ) continue - # Ligne de période (loyer ou divers). + # Ligne de période (loyer ou divers). Quand l'en-tête du lot est en bas + # d'une page et ses données en haut de la suivante, le nom du locataire + # peut se retrouver sur cette même ligne visuelle (colonne « Locataires » + # non vide) plutôt que sur la ligne d'en-tête du lot. if _PERIODE_RE.search(periode): + if loc: + nom = current["locataire"]["nom"] + current["locataire"]["nom"] = f"{nom} {loc}".strip() if nom else loc _append_periode_line(current, row) continue - # Nom du locataire (cellule « Locataires » seule, nom pas encore trouvé). - if loc and not current["locataire"]["nom"]: - current["locataire"]["nom"] = loc + # Nom du locataire (cellule « Locataires » seule). Peut être réparti sur + # plusieurs lignes visuelles quand le nom est trop long pour la colonne. + if loc: + nom = current["locataire"]["nom"] + current["locataire"]["nom"] = f"{nom} {loc}".strip() if nom else loc if current: situations.append(current)