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
|
# Backend avec auto-reload uvicorn
|
||||||
dev_back:
|
dev_back:
|
||||||
@@ -12,6 +12,11 @@ dev_front:
|
|||||||
dev:
|
dev:
|
||||||
$(MAKE) dev_back & $(MAKE) dev_front & wait
|
$(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:
|
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)
|
## Production (Docker)
|
||||||
|
|
||||||
Un **conteneur unique** : l'image builde le frontend Vue puis le fait servir
|
Un **conteneur unique** : l'image embarque le frontend Vue builde, servi par
|
||||||
par le backend FastAPI (API + interface web sur le meme port). Pas de nginx.
|
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
|
```bash
|
||||||
make docker # podman-compose up --build
|
make docker # podman-compose pull && podman-compose up -d
|
||||||
# ou : docker compose up --build
|
# ou : docker compose pull && docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
Accessible sur **http://localhost:8080**.
|
Accessible sur **http://localhost:8080**.
|
||||||
|
|
||||||
Les donnees (base SQLite + documents) sont declarees via `PLESNA_DATA_DIR`
|
Les donnees (base SQLite + documents) sont declarees via `PLESNA_DATA_DIR`
|
||||||
(voir `docker-compose.yml`) et persistees dans le volume `./data`, donc
|
(voir `docker-compose.yml`) et persistees dans un volume nomme dedie
|
||||||
conservees entre les redemarrages du conteneur.
|
(`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)
|
## Application de bureau (Windows, sans ligne de commande)
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
build:
|
# Image publiée par la CI Gitea (pas de build local).
|
||||||
context: .
|
image: git.opytex.org/lafrite/plesna-gerance:latest
|
||||||
dockerfile: backend.Dockerfile
|
|
||||||
ports:
|
ports:
|
||||||
# hôte:conteneur — l'app (API + interface web) écoute sur 8000
|
# hôte:conteneur — l'app (API + interface web) écoute sur 8000
|
||||||
- "8080:8000"
|
- "8080:8000"
|
||||||
volumes:
|
volumes:
|
||||||
# Persiste base SQLite + documents hors du conteneur.
|
# Volume nommé dédié aux données (base SQLite + documents).
|
||||||
- ./data:/app/data
|
- plesna-data:/app/data
|
||||||
environment:
|
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).
|
# database.sqlite et documents/ (cf. src/plesna_gerance/paths.py).
|
||||||
- PLESNA_DATA_DIR=/app/data
|
- PLESNA_DATA_DIR=/app/data
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
plesna-data:
|
||||||
|
|||||||
@@ -88,8 +88,9 @@ def _rows_from_page(page) -> list[dict]:
|
|||||||
ne contient pas ce tableau.
|
ne contient pas ce tableau.
|
||||||
"""
|
"""
|
||||||
# Repérer le tableau « situation locataires » (en-tête commençant par « Locataires »).
|
# Repérer le tableau « situation locataires » (en-tête commençant par « Locataires »).
|
||||||
|
candidates = page.find_tables()
|
||||||
table = None
|
table = None
|
||||||
for candidate in page.find_tables():
|
for candidate in candidates:
|
||||||
header = candidate.rows[0].cells
|
header = candidate.rows[0].cells
|
||||||
if header and header[0] is not None:
|
if header and header[0] is not None:
|
||||||
first = _strip_accents(page.crop(header[0]).extract_text() or "")
|
first = _strip_accents(page.crop(header[0]).extract_text() or "")
|
||||||
@@ -109,7 +110,20 @@ def _rows_from_page(page) -> list[dict]:
|
|||||||
return i
|
return i
|
||||||
return None
|
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"]))
|
words.sort(key=lambda w: (round((w["top"] + w["bottom"]) / 2, 1), w["x0"]))
|
||||||
|
|
||||||
# Regrouper les mots en lignes visuelles (clustering vertical tolérant).
|
# 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)
|
_fill_totaux(current, row)
|
||||||
continue
|
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).
|
# Solde Antérieur (libellé + montant dans la colonne période).
|
||||||
if periode.startswith("Solde Antérieur"):
|
if periode.startswith("Solde Antérieur"):
|
||||||
montant = _num(periode)
|
montant = _num(periode)
|
||||||
@@ -307,14 +326,22 @@ def extract_situation_locataires_from_pdf(pdf_path: str) -> list[dict]:
|
|||||||
)
|
)
|
||||||
continue
|
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 _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)
|
_append_periode_line(current, row)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Nom du locataire (cellule « Locataires » seule, nom pas encore trouvé).
|
# Nom du locataire (cellule « Locataires » seule). Peut être réparti sur
|
||||||
if loc and not current["locataire"]["nom"]:
|
# plusieurs lignes visuelles quand le nom est trop long pour la colonne.
|
||||||
current["locataire"]["nom"] = loc
|
if loc:
|
||||||
|
nom = current["locataire"]["nom"]
|
||||||
|
current["locataire"]["nom"] = f"{nom} {loc}".strip() if nom else loc
|
||||||
|
|
||||||
if current:
|
if current:
|
||||||
situations.append(current)
|
situations.append(current)
|
||||||
|
|||||||
Reference in New Issue
Block a user