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:
6
src/plesna_gerance/utils/__init__.py
Normal file
6
src/plesna_gerance/utils/__init__.py
Normal 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"]
|
||||
69
src/plesna_gerance/utils/amounts.py
Normal file
69
src/plesna_gerance/utils/amounts.py
Normal 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
|
||||
42
src/plesna_gerance/utils/dates.py
Normal file
42
src/plesna_gerance/utils/dates.py
Normal 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
|
||||
Reference in New Issue
Block a user