feat: init plesna-gerance - extracteur de comptes rendus de gerance

- Backend Python (uv + click + FastAPI)
  - CLI: plesna-gerance extract <pdf> pour extraire les donnees
  - CLI: plesna-gerance serve pour lancer le serveur API
  - API REST: POST /api/extract pour upload et extraction de PDF
  - Parsers modulaires: metadata, locataires, operations
  - Utilise pdftotext (poppler-utils) pour l'extraction de texte

- Frontend Vue.js + Tailwind CSS
  - Interface split-screen: PDF a gauche, donnees a droite
  - Preview PDF avec zoom et navigation pages (pdf.js)
  - Visualisation structuree des donnees extraites
  - Sections depliables: metadata, locataires, operations
  - Drag & drop pour upload de PDF
  - Extraction automatique a la selection du fichier
This commit is contained in:
2026-01-18 05:36:27 +01:00
commit 6bad5fbaf9
34 changed files with 5692 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
"""Utilitaires pour le parsing des données de gérance."""
from .dates import parse_french_date
from .amounts import parse_amount, extract_amounts_from_line
__all__ = ["parse_french_date", "parse_amount", "extract_amounts_from_line"]

View File

@@ -0,0 +1,69 @@
"""Utilitaires pour le parsing des montants."""
import re
def parse_amount(text: str) -> float:
"""Parse un montant français en float.
Gère les formats:
- 123,45
- 123.45
- 1 234,56 (avec espace comme séparateur de milliers)
- 1.234,56 (avec point comme séparateur de milliers)
Args:
text: Texte contenant un montant
Returns:
Montant en float, ou 0.0 si parsing impossible
"""
if not text:
return 0.0
text = text.strip().replace(" ", "").replace("\u00a0", "")
text = text.replace("", "").replace(",", ".")
# Gérer les séparateurs de milliers (ex: 1.234,56 -> 1234.56)
if text.count(".") > 1:
parts = text.rsplit(".", 1)
text = parts[0].replace(".", "") + "." + parts[1]
try:
return float(text)
except ValueError:
return 0.0
def extract_amounts_from_line(line: str) -> list[float]:
"""Extrait les montants d'une ligne en évitant les dates.
Les montants sont au format: 123.45 ou 1 234,56 ou -45.67
Les dates sont au format: 01.01.25 (exclues automatiquement)
Args:
line: Ligne de texte à analyser
Returns:
Liste des montants trouvés
"""
# Supprimer les dates du format DD.MM.YY pour éviter confusion
line_clean = re.sub(r"\b\d{2}\.\d{2}\.\d{2}\b", " DATE ", line)
amounts: list[float] = []
# Pattern plus strict: au moins un chiffre, optionnellement espace+chiffres,
# puis séparateur décimal et 2 chiffres
pattern = r"-?(\d{1,3}(?:[\s\u00a0]?\d{3})*)[,\.](\d{2})(?!\d)"
for match in re.finditer(pattern, line_clean):
full_match = match.group(0)
# Reconstruire le nombre
integer_part = match.group(1).replace(" ", "").replace("\u00a0", "")
decimal_part = match.group(2)
amount_str = f"{integer_part}.{decimal_part}"
if full_match.startswith("-"):
amount_str = "-" + amount_str
try:
amounts.append(float(amount_str))
except ValueError:
pass
return amounts

View File

@@ -0,0 +1,42 @@
"""Utilitaires pour le parsing des dates."""
import re
def parse_french_date(date_str: str) -> str:
"""Convertit une date française en format ISO (YYYY-MM-DD).
Formats supportés:
- DD.MM.YY (ex: 01.01.25 -> 2025-01-01)
- DD/MM/YYYY (ex: 01/01/2025 -> 2025-01-01)
- DD.MM.YYYY (ex: 01.01.2025 -> 2025-01-01)
Args:
date_str: Date au format français
Returns:
Date au format ISO ou chaîne originale si format non reconnu
"""
if not date_str:
return ""
# Format DD.MM.YY
m = re.match(r"(\d{2})\.(\d{2})\.(\d{2})$", date_str.strip())
if m:
d, mo, y = m.groups()
year = f"20{y}" if int(y) < 50 else f"19{y}"
return f"{year}-{mo}-{d}"
# Format DD/MM/YYYY
m = re.match(r"(\d{2})/(\d{2})/(\d{4})$", date_str.strip())
if m:
d, mo, y = m.groups()
return f"{y}-{mo}-{d}"
# Format DD.MM.YYYY
m = re.match(r"(\d{2})\.(\d{2})\.(\d{4})$", date_str.strip())
if m:
d, mo, y = m.groups()
return f"{y}-{mo}-{d}"
return date_str