feat: fix parsing
This commit is contained in:
@@ -6,6 +6,78 @@ from ..utils.dates import parse_french_date
|
||||
from ..utils.amounts import extract_amounts_from_line
|
||||
|
||||
|
||||
def _preprocess_locataires_text(text: str) -> str:
|
||||
"""Prétraite le texte pour fusionner les sections sur plusieurs pages.
|
||||
|
||||
Supprime les éléments répétés sur chaque page pour permettre une extraction
|
||||
continue des locataires dont les données s'étalent sur plusieurs pages.
|
||||
|
||||
Args:
|
||||
text: Texte brut du PDF
|
||||
|
||||
Returns:
|
||||
Texte nettoyé avec une seule section SITUATION DES LOCATAIRES
|
||||
"""
|
||||
lines = text.split("\n")
|
||||
cleaned_lines = []
|
||||
first_situation_found = False
|
||||
in_situation_section = False
|
||||
|
||||
# Patterns à ignorer (en-têtes répétés sur chaque page)
|
||||
skip_patterns = [
|
||||
r"^\s*ROSIER-MODICA\s*$",
|
||||
r"^\s*9 rue Juliette Récamier\s*$",
|
||||
r"^\s*69455 Lyon Cedex 06\s*$",
|
||||
r"^\s*S\.C\.I\.\s*PLESNA\s*$",
|
||||
r"^\s*Immeuble\s*:\s*\d+\s*$",
|
||||
r"^\s*\d+\s*RUE\s+", # Adresse immeuble
|
||||
r"^\s*69\d{3}\s+LYON\s*$", # Code postal + ville
|
||||
r"^\s*Lyon le \d{2}/\d{2}/\d{4}\s*$", # Date
|
||||
r"^\s*Powered by ICS\s*$",
|
||||
r"^\s*\d+\s*/\s*\d+\s*$", # Numéro de page (ex: 2/5)
|
||||
r"^\s*Capital de", # Pied de page
|
||||
r"^\s*Garantie de", # Pied de page
|
||||
]
|
||||
|
||||
# Pattern pour l'en-tête de colonnes
|
||||
header_pattern = r"^\s*Locataires\s+Période\s+Loyers"
|
||||
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
|
||||
# Ignorer les lignes vides
|
||||
if not stripped:
|
||||
cleaned_lines.append(line)
|
||||
continue
|
||||
|
||||
# Vérifier si c'est un pattern à ignorer
|
||||
should_skip = False
|
||||
for pattern in skip_patterns:
|
||||
if re.match(pattern, stripped, re.IGNORECASE):
|
||||
should_skip = True
|
||||
break
|
||||
|
||||
if should_skip:
|
||||
continue
|
||||
|
||||
# Gérer SITUATION DES LOCATAIRES
|
||||
if "SITUATION DES LOCATAIRES" in stripped:
|
||||
if not first_situation_found:
|
||||
first_situation_found = True
|
||||
in_situation_section = True
|
||||
cleaned_lines.append(line)
|
||||
# Ignorer les occurrences suivantes
|
||||
continue
|
||||
|
||||
# Ignorer les en-têtes de colonnes répétés
|
||||
if re.match(header_pattern, stripped):
|
||||
continue
|
||||
|
||||
cleaned_lines.append(line)
|
||||
|
||||
return "\n".join(cleaned_lines)
|
||||
|
||||
|
||||
def extract_situation_locataires(text: str) -> list[dict]:
|
||||
"""Extrait la situation des locataires.
|
||||
|
||||
@@ -21,6 +93,9 @@ def extract_situation_locataires(text: str) -> list[dict]:
|
||||
"""
|
||||
situations: list[dict] = []
|
||||
|
||||
# Prétraiter le texte pour gérer les lots sur plusieurs pages
|
||||
text = _preprocess_locataires_text(text)
|
||||
|
||||
# Trouver toutes les sections "SITUATION DES LOCATAIRES"
|
||||
sections = text.split("SITUATION DES LOCATAIRES")
|
||||
|
||||
|
||||
@@ -5,6 +5,37 @@ import re
|
||||
from ..utils.amounts import parse_amount
|
||||
|
||||
|
||||
def _extract_lot_code_from_description(description: str) -> str | None:
|
||||
"""Extrait le code lot depuis la description de l'opération.
|
||||
|
||||
Les codes lots suivent le format: {Lettre}{Numéro} où:
|
||||
- La lettre identifie l'immeuble (M=Marietton, S=Servient, B=Bloch, etc.)
|
||||
- Le numéro correspond au lot (ex: 06 -> lot 0006)
|
||||
|
||||
Exemples:
|
||||
- "M06 - Commande moteur pompe" -> "0006"
|
||||
- "S05 - Mise en service" -> "0005"
|
||||
- "B01 - Plaques" -> "0001"
|
||||
|
||||
Args:
|
||||
description: Description de l'opération
|
||||
|
||||
Returns:
|
||||
Code lot au format 4 chiffres (ex: "0006") ou None si non trouvé
|
||||
"""
|
||||
if not description:
|
||||
return None
|
||||
|
||||
# Pattern: lettre majuscule + 1-2 chiffres, suivi de " - " ou fin de mot
|
||||
match = re.search(r"\b[A-Z](\d{1,2})\s*-", description)
|
||||
if match:
|
||||
lot_num = match.group(1)
|
||||
# Formater sur 4 chiffres (ex: "6" -> "0006", "12" -> "0012")
|
||||
return lot_num.zfill(4)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def extract_recapitulatif_operations(text: str) -> list[dict]:
|
||||
"""Extrait le récapitulatif des opérations.
|
||||
|
||||
@@ -12,13 +43,17 @@ def extract_recapitulatif_operations(text: str) -> list[dict]:
|
||||
text: Texte complet du PDF
|
||||
|
||||
Returns:
|
||||
Liste des catégories d'opérations, chacune contenant:
|
||||
- categorie: nom de la catégorie
|
||||
- operations: liste des opérations avec type, fournisseur, description, montants
|
||||
Liste plate des opérations, chacune contenant:
|
||||
- categorie: catégorie normalisée (ex: DEPENSES_LOCATIVES)
|
||||
- sous_categorie: type d'opération (ex: Contrat entreprise nettoyage)
|
||||
- fournisseur: nom du fournisseur
|
||||
- description: description de l'opération
|
||||
- lot_concerne: lot concerné si applicable
|
||||
- montants: dict avec debit, credit, tva, locatif, deductible
|
||||
"""
|
||||
categories: list[dict] = []
|
||||
operations: list[dict] = []
|
||||
|
||||
# Catégories à identifier
|
||||
# Catégories à identifier (libellé PDF -> format normalisé)
|
||||
cat_keywords = {
|
||||
"DEPENSES LOCATIVES": "DEPENSES_LOCATIVES",
|
||||
"DEPENSES DEDUCTIBLES": "DEPENSES_DEDUCTIBLES",
|
||||
@@ -36,10 +71,10 @@ def extract_recapitulatif_operations(text: str) -> list[dict]:
|
||||
section = section.split("Solde créditeur en Euros")[0]
|
||||
|
||||
lines = section.split("\n")
|
||||
current_cat: dict | None = None
|
||||
current_cat_normalized: str | None = None
|
||||
current_fournisseur: str | None = None
|
||||
current_lot: str | None = None
|
||||
current_type_op: str | None = None
|
||||
current_sous_cat: str | None = None
|
||||
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
@@ -62,10 +97,10 @@ def extract_recapitulatif_operations(text: str) -> list[dict]:
|
||||
continue
|
||||
|
||||
# Détecter une catégorie
|
||||
found_cat = None
|
||||
for kw, _cat_id in cat_keywords.items():
|
||||
found_cat_normalized = None
|
||||
for kw, cat_normalized in cat_keywords.items():
|
||||
if kw in stripped:
|
||||
found_cat = kw
|
||||
found_cat_normalized = cat_normalized
|
||||
# Extraire le lot si présent
|
||||
lot_match = re.search(r"(?:LOT|/LOT)\s+(.+?)(?:\s{2,}|$)", stripped)
|
||||
if lot_match:
|
||||
@@ -74,19 +109,17 @@ def extract_recapitulatif_operations(text: str) -> list[dict]:
|
||||
current_lot = None
|
||||
break
|
||||
|
||||
if found_cat:
|
||||
if current_cat and current_cat["operations"]:
|
||||
categories.append(current_cat)
|
||||
current_cat = {"categorie": found_cat, "operations": []}
|
||||
if found_cat_normalized:
|
||||
current_cat_normalized = found_cat_normalized
|
||||
current_fournisseur = None
|
||||
current_type_op = None
|
||||
current_sous_cat = None
|
||||
continue
|
||||
|
||||
if not current_cat:
|
||||
if not current_cat_normalized:
|
||||
continue
|
||||
|
||||
# Type d'opération (italique/description au début)
|
||||
type_op_patterns = [
|
||||
# Type d'opération / sous-catégorie (ex: "Contrat entreprise nettoyage")
|
||||
sous_cat_patterns = [
|
||||
"Nettoyage",
|
||||
"Electricité",
|
||||
"Contrat",
|
||||
@@ -99,9 +132,9 @@ def extract_recapitulatif_operations(text: str) -> list[dict]:
|
||||
"Lavage",
|
||||
"Reglt",
|
||||
]
|
||||
for top in type_op_patterns:
|
||||
if stripped.startswith(top):
|
||||
current_type_op = stripped.split(" ")[0].strip()
|
||||
for pattern in sous_cat_patterns:
|
||||
if stripped.startswith(pattern):
|
||||
current_sous_cat = stripped.split(" ")[0].strip()
|
||||
break
|
||||
|
||||
# Fournisseur (NOM EN MAJUSCULES)
|
||||
@@ -143,11 +176,16 @@ def extract_recapitulatif_operations(text: str) -> list[dict]:
|
||||
description = description[len(current_fournisseur) :].strip()
|
||||
|
||||
if description and not description.startswith("Totaux"):
|
||||
# Extraire le numéro de lot depuis la description (ex: M06 -> 0006)
|
||||
lot_numero = _extract_lot_code_from_description(description)
|
||||
|
||||
operation = {
|
||||
"type_operation": current_type_op or "",
|
||||
"categorie": current_cat_normalized,
|
||||
"sous_categorie": current_sous_cat or "",
|
||||
"fournisseur": current_fournisseur,
|
||||
"description": description,
|
||||
"lot_concerne": current_lot,
|
||||
"lot_numero": lot_numero,
|
||||
"montants": {
|
||||
"debit": amounts_float[0]
|
||||
if len(amounts_float) > 0
|
||||
@@ -162,9 +200,6 @@ def extract_recapitulatif_operations(text: str) -> list[dict]:
|
||||
else 0.0,
|
||||
},
|
||||
}
|
||||
current_cat["operations"].append(operation)
|
||||
operations.append(operation)
|
||||
|
||||
if current_cat and current_cat["operations"]:
|
||||
categories.append(current_cat)
|
||||
|
||||
return categories
|
||||
return operations
|
||||
|
||||
Reference in New Issue
Block a user