Compare commits
2 Commits
f228783193
...
0e30ed8501
| Author | SHA1 | Date | |
|---|---|---|---|
| 0e30ed8501 | |||
| 8972bcaa06 |
11
Makefile
11
Makefile
@@ -1,4 +1,4 @@
|
||||
.PHONY: dev_back dev_front dev docker
|
||||
.PHONY: dev_back dev_front dev docker docker-build
|
||||
|
||||
# Backend avec auto-reload uvicorn
|
||||
dev_back:
|
||||
@@ -12,6 +12,11 @@ dev_front:
|
||||
dev:
|
||||
$(MAKE) dev_back & $(MAKE) dev_front & wait
|
||||
|
||||
# Build et lance via podman-compose
|
||||
# Tire l'image publiee (registre Gitea) et lance via podman-compose
|
||||
docker:
|
||||
podman-compose up --build
|
||||
podman-compose pull
|
||||
podman-compose up -d
|
||||
|
||||
# Construit l'image en local (test avant publication)
|
||||
docker-build:
|
||||
podman build -f backend.Dockerfile -t plesna-gerance:local .
|
||||
|
||||
20
README.md
20
README.md
@@ -37,19 +37,27 @@ Ouvrir **http://localhost:5173** — le proxy Vite redirige `/api` vers le backe
|
||||
|
||||
## Production (Docker)
|
||||
|
||||
Un **conteneur unique** : l'image builde le frontend Vue puis le fait servir
|
||||
par le backend FastAPI (API + interface web sur le meme port). Pas de nginx.
|
||||
Un **conteneur unique** : l'image embarque le frontend Vue builde, servi par
|
||||
le backend FastAPI (API + interface web sur le meme port). Pas de nginx.
|
||||
|
||||
L'image est **publiee par la CI Gitea** (`.gitea/workflows/docker-publish.yml`)
|
||||
sur `git.opytex.org/lafrite/plesna-gerance`. Le `docker-compose.yml` la **tire**
|
||||
directement (pas de build local) :
|
||||
|
||||
```bash
|
||||
make docker # podman-compose up --build
|
||||
# ou : docker compose up --build
|
||||
make docker # podman-compose pull && podman-compose up -d
|
||||
# ou : docker compose pull && docker compose up -d
|
||||
```
|
||||
|
||||
Accessible sur **http://localhost:8080**.
|
||||
|
||||
Les donnees (base SQLite + documents) sont declarees via `PLESNA_DATA_DIR`
|
||||
(voir `docker-compose.yml`) et persistees dans le volume `./data`, donc
|
||||
conservees entre les redemarrages du conteneur.
|
||||
(voir `docker-compose.yml`) et persistees dans un volume nomme dedie
|
||||
(`plesna-data`), donc conservees entre les redemarrages et recreations du
|
||||
conteneur.
|
||||
|
||||
> Build local de l'image (test avant publication) : `make docker-build`
|
||||
> (`podman build -f backend.Dockerfile -t plesna-gerance:local .`).
|
||||
|
||||
## Application de bureau (Windows, sans ligne de commande)
|
||||
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: backend.Dockerfile
|
||||
# Image publiée par la CI Gitea (pas de build local).
|
||||
image: git.opytex.org/lafrite/plesna-gerance:latest
|
||||
ports:
|
||||
# hôte:conteneur — l'app (API + interface web) écoute sur 8000
|
||||
- "8080:8000"
|
||||
volumes:
|
||||
# Persiste base SQLite + documents hors du conteneur.
|
||||
- ./data:/app/data
|
||||
# Volume nommé dédié aux données (base SQLite + documents).
|
||||
- plesna-data:/app/data
|
||||
environment:
|
||||
# Déclare l'emplacement des données ; get_data_dir() en dérive
|
||||
# Le conteneur écrit ses données ici ; get_data_dir() en dérive
|
||||
# database.sqlite et documents/ (cf. src/plesna_gerance/paths.py).
|
||||
- PLESNA_DATA_DIR=/app/data
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
plesna-data:
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user