Commit Graph

46 Commits

Author SHA1 Message Date
233f000b2a chore: passe en version 0.1.2
All checks were successful
Build and Publish Docker Image / Tests (push) Successful in 2m46s
Build and Publish Docker Image / Build App Image (push) Successful in 1m13s
Build and Publish Docker Image / Build Summary (push) Successful in 3s
2026-08-22 21:18:45 +02:00
24db55c46c fix(bureau): affiche les PDF et conserve les réglages dans la fenêtre native
Deux réglages du moteur manquaient pour que l'aperçu fonctionne hors
navigateur, tous deux constatés en lançant la fenêtre native.

Qt WebEngine laisse PdfViewerEnabled à False et le conditionne à
PluginsEnabled : le volet n'affichait qu'un bouton « Ouvrir » au lieu du
document. pywebview n'expose pas ces attributs et construit son propre
profil, donc on les pose sur la vue au chargement de la fenêtre — bien
avant qu'un document, et donc une <iframe>, soit ouvert. Sans objet sous
Windows, où WebView2 embarque déjà son lecteur.

pywebview démarre par ailleurs en mode privé, sur un profil éphémère qui
ne conserve ni cookies ni localStorage. L'interface y range la largeur du
volet d'aperçu, et le lecteur PDF du moteur y garde l'état de sa barre
latérale de vignettes : les deux se seraient réinitialisés à chaque
lancement. L'application ne charge que son propre serveur local, rien de
tiers n'est stocké au passage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 21:12:58 +02:00
b15f5c95b6 fix: distingue un lot reloué d'un lot sorti de la gestion
La fiche du lot 15 annonçait « dernier loyer connu, arrêté après févr. 26 »
alors qu'un locataire y est entré le 8 juillet — et la courbe juste dessous,
où la barre de juillet est bien là, la contredisait.

Le loyer en vigueur se lit sur les paliers, et un mois facturé au seul
prorata n'en ouvre pas : celui d'avant s'arrête donc à la dernière mensualité
pleine, quatre mois avant le nouveau bail. Ne rien lire après lui confond deux
états que le compte rendu sépare : un lot qui n'est plus loué, et un lot entre
deux locataires.

La distinction tient à la frontière qui sert déjà à séparer loyer plein et
prorata, la période portée par le compte rendu :

- un prorata qui finit le dernier jour du mois sans partir du premier facture
  la fin du mois : quelqu'un est entré, le bail court encore après ;
- un prorata qui part du premier sans l'atteindre facture le début : le bail
  s'arrête là ;
- un montant négatif ne fait ni l'un ni l'autre. Un avoir porte parfois la
  période exacte de la ligne qu'il annule, et le lire comme une entrée
  inventerait une relocation là où rien n'a été loué.

L'entrée l'emporte sur la sortie parce qu'elle vient après : ce lot voit son
locataire partir le 10 mars et le suivant arriver le 8 juillet, et c'est le
second qui décrit son état.

Le montant affiché ne bouge pas — le nouveau bail n'a été facturé qu'au
prorata, et en tirer un loyer mensuel l'inventerait. Seul ce qu'on en dit
change : « dernier loyer plein · nouveau bail depuis juil. 26 ».

Sans relocation, la sortie date enfin la fin de la location au mois où elle a
lieu : « arrêté en cours de mars 26 » plutôt qu'« arrêté après févr. 26 », qui
l'avançait d'un mois.

Sur le parc : 31 lots loués, 1 relouté, 1 réellement arrêté.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 20:50:54 +02:00
e81cd5ffa4 feat: filtre les dépenses sur plusieurs fournisseurs et sur l'absence de tag
Le filtre fournisseur était une recherche de sous-chaîne : impossible de
comparer deux fournisseurs, et « PPR » ramenait ses homonymes au passage.
Il devient une sélection multiple exacte, prise dans un menu déroulant
filtrable au clavier (SelectionMultiple, générique et réutilisable). La
liste vient de /api/fournisseurs, remise en ordre alphabétique — l'API la
trie par montant, ce qui se lit bien dans un classement mais rend
introuvable un fournisseur qu'on cherche à l'œil.

Côté API, `fournisseur` devient répétable et s'entend comme un OU exact.

Le filtre tag, lui, ne savait pas demander « ce qui n'est pas encore
tagué » — c'est pourtant la question qui amorce le travail de tagging.
`tag_id=0` le demande, et l'option « Sans tag » l'ouvre depuis la page.

Au passage, les deux endpoints dupliquaient leurs filtres : ils partagent
désormais _appliquer_filtres, pour que les totaux du résumé ne puissent
plus porter sur d'autres lignes que la table.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 18:29:40 +02:00
005675728d feat: borne l'analyse d'un lot à une période
La fiche d'un lot rendait tout son historique, et rien d'autre. Sur un
parc suivi depuis 2023, lire ce qu'un lot a fait ces trois derniers mois
supposait de faire soi-même la soustraction. Le paramètre `mois` ouvre
une fenêtre sur les chiffres, la chronologie, les intervenants, les
locataires et la courbe du loyer.

Trois décisions la gouvernent, et chacune évite un chiffre qui
mentirait :

- **la fenêtre est calée sur le dernier compte rendu du lot**, jamais sur
  aujourd'hui. Les derniers comptes rendus du parc datent de juillet
  alors qu'on est en août : une fenêtre glissante depuis la date du jour
  décalerait déjà tout d'un mois, et viderait entièrement la fiche d'un
  lot sorti de la gestion — une page vide se lisant comme une absence
  d'activité plutôt que comme un filtre trop court ;
- **le restant dû y échappe**, parce qu'un stock ne se borne pas comme un
  flux. Le ramener à la fenêtre l'annulerait dès qu'aucun compte rendu
  n'y tombe : un lot afficherait 0 € dû tout en devant plusieurs
  milliers ;
- **le loyer en vigueur et la comparaison au parc y échappent aussi.**
  La courbe, elle, est bien coupée, mais après coup : les paliers restent
  lus sur la série entière, sans quoi « depuis mai 26 » daterait de la
  borne du filtre au lieu de la révision qui a fixé ce loyer, et la
  médiane du parc changerait de mois de référence à chaque changement de
  période. Rien n'est coupé au-delà de la fenêtre : un bail trimestriel
  facturé d'avance porte des mois postérieurs au dernier compte rendu, et
  les retirer ferait croire que le lot cesse d'être loué.

Les locataires n'ont ni date d'entrée ni date de sortie — aucune n'est
extraite des comptes rendus. Leur seul rattachement au temps est donc le
document qui les porte. Hors fenêtre, la liste reste malgré tout celle de
la table : la base contient des locataires dont aucune ligne ne dépend, et
la vue par défaut ne filtre rien.

Un filtre qui masque doit dire ce qu'il masque : la réponse porte le
compte des lignes, des mois et des occupants laissés dehors, et les
bornes effectivement retenues.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 17:26:22 +02:00
b1ff0812c1 feat: met le loyer d'un lot sur un axe de temps et le rapporte au mètre carré
La fiche d'un lot cumulait tout son historique en un chiffre. Une
révision de loyer, une vacance ou un décrochage y étaient donc
invisibles. Le bloc `loyer` remet les lignes sur un axe de temps, dont
l'unité est le mois loué et non le mois du compte rendu : un rappel de
mars facturé en avril décrit mars.

Trois formes de lignes cohabitent sous le même `type_ligne = "loyer"`,
et les confondre fausse la courbe :

- le loyer d'un mois, cas courant ;
- le loyer d'un trimestre, forme réelle des baux commerciaux du parc,
  réparti sur les mois qu'il couvre — sans quoi deux mois sur trois
  paraîtraient vides alors que le local est loué ;
- le prorata d'entrée, de sortie ou l'avoir, rattaché à son mois mais
  compté à part. Les additionner ferait passer un mois de changement de
  locataire pour un mois à loyer effondré.

Un mois sans ligne reste vide plutôt qu'à zéro : zéro dirait « loué
gratuitement », ce qu'aucun compte rendu ne dit. Un mois facturé
seulement au prorata est signalé comme transition, pour ne pas se
confondre avec une vacance.

Le mètre carré vient de la fiche saisie, qu'aucun PDF ne porte : tant
qu'elle manque, le ratio reste nul et la page renvoie vers la saisie.
Les médianes qui situent le lot rejouent exactement la même répartition
pour les autres lots — les calculer autrement ne voudrait rien dire — et
s'accompagnent toujours de leur effectif et du nombre de lots exclus
faute de surface.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 22:38:41 +02:00
4fe2bf75c0 chore: passe en version 0.1.1
All checks were successful
Build and Publish Docker Image / Tests (push) Successful in 12m56s
Build and Publish Docker Image / Build App Image (push) Successful in 1m52s
Build and Publish Docker Image / Build Summary (push) Successful in 3s
2026-08-01 06:08:09 +02:00
8c959f5d5f refactor: donne une source unique aux colonnes du compte rendu
L'ordre et les libellés des colonnes étaient retapés à six endroits : l'en-tête
du tableau, deux boucles de cellules, la ligne « Totaux », la table des libellés
et la liste des colonnes recoupées. Ces copies avaient déjà divergé — le solde
antérieur, rendu hors de la boucle, était comparé aux lignes sans que son écart
puisse s'afficher dans sa cellule. `COLONNES_CRG` devient la seule déclaration,
et tout le reste en dérive ; l'écart du solde antérieur s'affiche du coup là où
on le cherche.

`ecartsAvecLignes` rapporte maintenant l'extrait, le calculé et ce qui manque
entre les deux. Le composant refaisait cette soustraction pour son infobulle,
avec un `|| 0` là où l'utilitaire emploie `Number` et `Number.isFinite` : deux
règles de coercition pour un même calcul, libres de diverger.

Le contenu déplié d'un lot passe de `v-show` à `v-if`. Le tableau des lignes
compte une centaine de champs éditables ; les garder montés pour la vingtaine de
lots d'un document faisait re-rendre à chaque frappe deux mille champs que
personne ne regardait.

`setNestedValue` était recopié à l'identique dans trois composants et
`formatCurrency` redéfini dans le composant alors que `utils/format.js` existe
et dit lui-même que les nouveaux affichages passent par lui. Le premier part
dans `utils/chemin.js`, le second cède la place à `formatMontantPrecis`, dont le
formateur `Intl` est construit une fois pour toutes.

`EditableField` affirmait en dur que sa valeur venait du compte rendu. C'est vrai
des trois écrans qui l'emploient aujourd'hui, mais un formulaire de saisie
manuelle mentirait sans le savoir : l'origine devient une prop, avec cette
valeur par défaut.

Le type des lignes de report est défini des deux côtés de l'application sans
lien entre eux ; chacun renvoie désormais à l'autre.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 14:18:58 +02:00
6a014cdb71 feat: rassemble sur un lot ce que les comptes rendus en disent
Les pages existantes agrègent le parc ; aucune ne descend à un lot pour
remettre ses lignes bout à bout. `/api/lots/{id}/analyse` renvoie son
identité (fiche saisie comprise, trous laissés visibles), ses totaux, sa
chronologie de recettes et de dépenses, et les entreprises intervenues.

Deux limites sont assumées plutôt que contournées :

- les dépenses d'un lot sont celles que le compte rendu lui impute. Aucune
  clé de répartition n'existe en base — ni tantièmes, ni surfaces complètes
  — donc les charges d'immeuble ne sont pas ventilées : elles sont exposées
  à part, et le solde d'un lot n'est pas un résultat net ;
- les lignes sont rendues telles qu'extraites, sans regroupement ni
  dédoublonnage. Un acompte et son solde restent deux lignes.

Les totaux passent par `flux_par` et `restant_du_par` au lieu de resommer
sur place : ces fonctions portent la distinction entre ce qui se cumule et
ce qui est une photo, et la rejouer à la main ferait diverger cette page de
la page Recettes.

Le montant d'un intervenant est net du crédit, comme chaque ligne de la
chronologie, pour que le détail d'une entreprise retrouve son total — un
avoir rend d'ailleurs ce montant négatif, ce que le compte rendu porte.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 10:42:02 +02:00
fdffe27f06 feat: étend le référentiel au parc entier et à la suppression des lots
Trois manques que la saisie a fait apparaître :

Le tableau porte une colonne immeuble, la liste ne peut donc plus être
suspendue à un immeuble choisi d'avance : elle renvoie tout le parc, chaque
ligne emportant de quoi nommer son immeuble sans requête de plus.

Un lot qu'aucune ligne de compte rendu ne mentionne ne décrit rien : il
encombre la saisie et doit pouvoir disparaître. La garde est côté serveur —
un lot porteur de revenus ou de dépenses est refusé, ses montants
partiraient avec lui.

Le nom d'usage s'enregistre, et la réponse recalcule les compteurs de
l'immeuble plutôt que de les laisser à zéro : elle remplace l'immeuble dans
les listes du client, qui le croirait vide de lots.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 15:41:24 +02:00
9f5f46a93a feat: donne aux immeubles un nom d'usage
Les comptes rendus n'identifient un immeuble que par son code de gestion
(« 33689020 »), illisible partout où il s'affiche. La dénomination
(« Servient ») le remplace à l'écran sans toucher au code, qui reste la clé
venue des PDF.

La colonne s'ajoute à une table déjà installée : elle passe donc par le
rattrapage de schéma, qui accepte désormais l'absence de valeur de
rattrapage. Déduire un nom d'usage du code en inventerait un.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 15:40:27 +02:00
5a08b0c7e5 feat: décrit les logements dans un référentiel saisi à la main
Les lots n'étaient connus que par l'extraction PDF : un numéro, un type
souvent vide, et rien sur le bien lui-même. Cette table de caractéristiques
(surface, étage, bâtiment, chauffage, DPE, rapprochement impôts) donne au
référentiel une source de vérité indépendante des comptes rendus.

Table séparée de `lots` à dessein : une ré-extraction ne peut alors pas
écraser la saisie, et le désaccord sur le type de lot reste visible au lieu
d'être arbitré en silence. La fiche gagne, le PDF comble les trous.

Échéance du DPE et écart de surface ne sont pas stockés mais calculés : une
colonne dérivée finirait par mentir après une correction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 18:00:42 +02:00
9c2d4f7f82 feat: resume l'annee civile sur l'accueil
L'accueil ne decrivait qu'un document : les quatre tuiles lisaient le
dernier compte rendu importe, sans rien dire de ce que contient la base.

Il resume desormais une annee civile, avec un selecteur des annees
presentes. L'annee par defaut est celle du dernier compte rendu et non
l'annee en cours : la fenetre etait calee sur `date.today()`, si bien que
sans import depuis quelques mois l'ecran se serait vide alors que la base
est pleine.

Ce que les tuiles annoncent :

- recettes facturees, report exclu, avec l'encaisse et le recouvrement ;
- depenses, debit et credits recus ;
- net reverse, rapproche des soldes annonces par les comptes rendus ;
- restant du, date, car un stock ne se cumule pas d'un mois sur l'autre.

Le net reverse vaut « encaisse - debit + credit », et cette egalite tombe
au centime sur le solde extrait du PDF pour avril, mai et juin 2026. Elle
s'ecarte de 288,52 EUR en fevrier et de 1 188,03 EUR en mars, les deux mois
dont l'extraction a par ailleurs des defauts. L'ecart est donc affiche et
jamais lisse : c'est le seul controle de bout en bout dont on dispose sur
la qualite d'une extraction. Les regles vivent dans services/tresorerie.py,
a cote de celles des revenus.

Le lot compte est celui qui a ete facture dans l'annee : la table `lots`
retient deux ecritures par lot (« 0001 » et « 01 »), sequelle de la
normalisation des numeros, et en annoncerait 40 la ou il y en a 20.

Le graphique passe du vert et rouge au bleu et ambre : sous deuteranopie,
green-400 et red-400 ne se separent qu'a un delta E de 7,9, sous le seuil
de 8. La paire retenue tient a 30,2.

`/recent-revenus` disparait — c'etait le dernier agregat a compter les
lignes de report — et `/immeubles-shortcuts` est borne a la meme annee que
le reste de l'ecran, deux perimetres sur un ecran donnant deux montants
sans que rien ne les distingue.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 13:52:10 +02:00
eb7eead555 feat: masque la section IA derriere un interrupteur de fonctionnalite
La section IA n'est pas prete a etre utilisee. Plutot que de supprimer le
code, deux interrupteurs alignes la retirent de l'application :

- frontend/src/features.js : onglet de navigation, page /ia (redirigee vers
  l'accueil pour ne pas casser un signet) et section « Configuration LLM »
  de la page Config. La page n'est plus embarquee dans le bundle.
- src/plesna_gerance/features.py : les routes /api/ia ne sont plus montees,
  les endpoints n'existent donc pas du tout.

Les settings Ollama restent en base : repasser les deux interrupteurs a
true suffit a tout reactiver, sans ressaisie.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 12:14:27 +02:00
11bd3ca539 fix: aligne le dashboard sur la règle flux / stock
All checks were successful
Build and Publish Docker Image / Build App Image (push) Successful in 22s
Build and Publish Docker Image / Build Summary (push) Successful in 3s
L'accueil et la page Revenus calculaient leurs totaux chacun de leur
côté : le premier lisait le dernier compte rendu, la seconde cumulait
tout. Les deux écrans affichaient donc deux impayés différents pour la
même notion — 49 374 € contre 247 354 €.

Les raccourcis immeubles cumulaient la dette, et les courbes mensuelles
réintégraient le report chaque mois, ce qui rendait deux mois
incomparables. Tous passent par les mêmes règles que la page Revenus.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 10:50:15 +02:00
3e2d103929 fix: distingue les montants cumulables des soldes à date
Chaque compte rendu reporte la dette du précédent dans une ligne
solde_anterieur. Les agrégats sommaient ces lignes comme le reste : la
même créance était recomptée à chaque document, et un remboursement ne
pouvait jamais s'inscrire — une dette soldée restait affichée à vie.

Sur la base réelle, la page Revenus annonçait ainsi 247 354 € d'impayés
pour une dette de 49 374 €, et désignait comme deuxième et troisième
débiteurs deux locataires à jour depuis avril (SURBECK 690,10 € et
GUINAIS 445,81 €, tous deux soldés).

Deux natures cohabitent, et c'est la colonne qui la porte, pas la ligne :
une ligne de report a un `total` déjà compté le mois d'avant, mais ses
`regles` sont un encaissement bien réel de la période. Écarter la ligne
entière ferait disparaître de l'argent reçu (1 298,81 € ici).

- flux (facturé, encaissé) : cumulés sur la période, report exclu ;
- stock (restant dû) : lu dans le dernier compte rendu de chaque immeuble ;
- taux de recouvrement : réglé sur facturé, report exclu des deux côtés,
  sans quoi rattraper une vieille dette ferait dépasser 100 %.

Résultat : 85 748 € facturés, 98,2 % de recouvrement, 49 374 € encore
dus. Les règles vivent dans services/revenus_query.py, pour que le
dashboard s'y branche au lieu de les réinventer.

/summary borne désormais tous ses blocs à la période demandée : les KPIs
et by_immeuble ignoraient `months` alors que by_month le respectait.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 10:50:15 +02:00
1c817a4ac9 refactor: supprime l'endpoint /api/dashboard/stats inutilisé
Aucun appelant : le tableau de bord se construit sur financial-summary,
monthly-trends, recent-revenus et immeubles-shortcuts. Les compteurs
qu'il exposait restent disponibles en ligne de commande via
`plesna-gerance db-info`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 09:57:06 +02:00
5a132b1258 fix: renvoie un 404 sur une URL d'API inconnue
L'API et l'interface partagent le même port, et la route attrape-tout qui
sert le SPA passait avant le 404 : n'importe quelle URL /api/* erronée
repartait en index.html avec un 200. Côté client, une faute de frappe
dans une URL ne ressemblait pas à une erreur mais à une réponse vide, et
le vrai motif n'apparaissait qu'en inspectant le corps de la réponse.

Les URL commençant par /api/ sont désormais exclues de l'attrape-tout et
retombent sur un 404 JSON. Les routes de l'interface restent servies par
index.html.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 09:57:06 +02:00
49e060e6e4 refactor: supprime le code mort et unifie les routes de tags
Code sans aucun appelant, retiré : DatabaseService.get_revenus_summary
(annoté list[dict] alors qu'il retournait un dict), get_depenses_summary,
get_tag_by_id et storage.file_exists.

Les tags étaient gérés à deux adresses : /api/tags (lecture, appelée par
trois composants) et /api/config/tags (lecture, création, renommage,
appelée par la seule page de configuration). Tout est regroupé sur
/api/tags, dans le module qui leur est dédié ; config.py ne garde que les
settings et ConfigPage est recâblée. Ces routes n'avaient aucun test :
sept en couvrent maintenant la création, le renommage, l'unicité et les
noms vides.

/api/stats disparaît également : sous-ensemble de /api/dashboard/stats,
il n'était appelé par personne.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 09:44:28 +02:00
ea5bdac18a refactor: retire les parseurs texte de repli
Les parseurs géométriques (par cellules de tableau) traitent les 5 PDF du
corpus sans jamais déclencher le repli : les parseurs texte étaient 586
lignes de regex fragiles, non testées, à maintenir à chaque évolution du
format de sortie. Les références golden sont inchangées après leur
suppression — l'extraction produit exactement la même chose.

Le parseur géométrique devient donc le seul chemin : ce qu'il ne lit pas
est perdu. L'orchestrateur distingue maintenant les deux cas :

- aucun lot lu -> ExtractionError (422 côté API). Un compte rendu sans
  lot n'existe pas : c'est le tableau qui n'a pas été reconnu, et mieux
  vaut échouer que d'enregistrer un document vide découvert bien plus
  tard, au moment de relire les chiffres ;
- aucune opération -> accepté. Un mois sans dépense reste plausible.

_extract_lot_code_from_description était la seule fonction encore
utilisée : elle rejoint utils.lots sous le nom
extract_lot_numero_from_description, à côté de normalize_lot_numero.
extract_text_from_pdf, sans appelant, disparaît au passage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 09:44:20 +02:00
1b319dff4e fix: corrige le sur-comptage des revenus par immeuble
Les agrégats par immeuble joignaient revenus et locataires à partir du
lot. Comme un lot accumule ses occupants successifs, chaque ligne de
revenu était comptée autant de fois qu'il y a eu de locataires : aucune
erreur levée, juste des montants faux qui dérivent avec l'ancienneté du
parc.

Sur la base réelle (40 lots, 46 locataires), /api/revenus/immeubles et
le bloc by_immeuble de /api/revenus/summary annonçaient :

    total   348 111,95 € au lieu de 332 899,45 €   (+4,6 %)
    réglés  100 697,66 € au lieu de  85 545,16 €   (+17,7 %)
    taux           28,9 % au lieu de       25,7 %

Les montants sont désormais agrégés par immeuble AVANT la jointure des
locataires, qui ne sert plus qu'au dénombrement. Les deux endpoints
partagent ces sous-requêtes et la construction de la réponse.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 09:44:11 +02:00
92cf21309f feat: affiche la date de derniere extraction et revient aux documents
La liste des documents montrait la date d'import, figee au premier
enregistrement : c'est la date de la derniere extraction ayant produit les
donnees affichees qui renseigne, surtout apres un balayage de re-extraction. La
colonne `extracted_at` la porte, mise a jour a chaque enregistrement, et la date
d'import reste consultable en infobulle.

Le projet n'ayant pas d'outil de migration (`create_all` laisse intactes les
tables existantes), `init_db` rattrape les colonnes ajoutees apres coup : sans
cela une base deja installee cesserait de fonctionner. Les documents deja
presents recoivent leur date d'import, qui est bien celle de leur extraction.

Une fois la selection appliquee sans echec, le balayage renvoie vers la liste des
documents ; en cas d'echec on reste sur place, les lignes concernees portant leur
message.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 07:02:28 +02:00
a1da4fc97f feat: annonce les reecritures que l'enregistrement imposera
Refuser une modification, c'est reenvoyer la valeur actuelle du document. Mais
l'enregistrement ramene les donnees a une forme canonique (aujourd'hui les
numeros de lot) : sur ces champs, le refus n'est pas conserve. L'interface le
laissait croire.

Plutot que de traiter le cas des lots en dur, le serveur devient seul juge de ce
qu'il stocke et l'annonce : `canonicalize_extraction` regroupe les reecritures
faites a la persistance, `save_document` et l'apercu passent par elle, et
/api/documents/{id}/re-extract renvoie `previous_canonical` a cote de
`previous_data`. Le front compare ces deux jeux et signale, sous chaque case
decochee, la valeur que l'enregistrement reecrira — sans connaitre aucune regle.
Une transformation ajoutee plus tard remontera donc toute seule dans l'interface.

Toutes les differences restent visibles et toutes les cases actives : voir ce
qui change et pouvoir se prononcer prime, quitte a etre prevenu que ce champ-la
reviendra reecrit.

Supprime au passage frontend/src/utils/lots.js, qui dupliquait la regle de
normalisation cote client.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 07:02:01 +02:00
6f2530014f fix: conserve l'identite d'un document re-extrait
Le remplacement par ID mettait a jour le document en le supprimant puis en le
recreant : la ligne changeait d'ID, cassant les liens qui la referencent (URL
d'edition, telechargement du PDF) juste apres un balayage de re-extraction. Le
document est desormais mis a jour sur place, seules les donnees derivees
(revenus, depenses) etant regenerees.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 06:56:02 +02:00
9cf0a89aab feat: expose la re-extraction verifiable d'un document
POST /api/documents/{id}/re-extract retourne desormais, en plus des donnees
re-extraites, les donnees actuelles et les tags des depenses : un seul appel
suffit pour comparer avant/apres et reporter les tags. L'endpoint devient
synchrone pour que FastAPI l'execute dans un thread, l'extraction bloquant
plusieurs secondes par PDF.

PUT /api/documents/{id} enregistre la nouvelle extraction en visant le
document par son ID (save_document(replace_document_id=...)) : une extraction
qui corrige la reference ou la date met a jour le bon document au lieu d'en
creer un second, et refuse la collision avec un document voisin. Le PDF
stocke est conserve.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 06:56:02 +02:00
420a856e41 feat: normalise les numéros de lot sur 2 chiffres
Les PDF et les saisies manuelles écrivent le même lot de plusieurs façons
("6", "06", "0006"), et chaque forme créait jusqu'ici un lot distinct en
base. utils/lots.py fixe la forme canonique sur 2 chiffres et sert de point
d'entrée unique pour la normalisation.

Elle est appliquée à la source dans les parseurs (locataires texte et
tableau, codes lot des opérations), et en dernier recours dans
get_or_create_lot et save_document, pour couvrir les extractions éditées à
la main via l'API. Les numéros à plus de 2 chiffres significatifs ne sont
pas tronqués.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 11:46:33 +02:00
0e30ed8501 fix: corrige la perte de lots quand une page coupe le tableau des locataires
All checks were successful
Build and Publish Docker Image / Build App Image (push) Successful in 1m32s
Build and Publish Docker Image / Build Summary (push) Successful in 4s
pdfplumber fragmente parfois le tableau en plusieurs bbox disjoints quand un
filet de séparation manque entre deux lots (ex: page 3 du sample), ce qui
faisait disparaître silencieusement les lots tombés dans l'interstice ou
situés après le fragment ignoré. Corrige aussi la troncature du nom de
locataire quand il s'étale sur plusieurs lignes visuelles ou coïncide avec
la ligne de période (lot coupé entre deux pages).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 08:16:27 +02:00
c79ecc45ff feat: extraction locataires et opérations par cellules de tableau
Remplace les parseurs texte+regex (fragiles sur l'alignement en colonnes des
PDF Oralia mal construits) par une extraction géométrique : reconstruction des
lignes visuelles par regroupement vertical tolérant des mots, puis affectation
de chaque valeur à sa colonne via les filets du tableau (pdfplumber find_tables).

Corrections apportées :
- locataires : montant/libellé « divers » dans la bonne colonne (plus le total
  cumulé du lot), nom de locataire correct (le logo/en-tête hors filets est
  ignoré), lignes multi-période et pages recollées.
- opérations : fournisseur séparé de la description (TOTALENERGIES ≠ DIDIER
  NETTOYAGE, PPR ≠ BOUVARD), colonne Déductible remplie, Débit/Crédit distingués,
  fournisseur des honoraires reporté sur le bloc, fragments de description
  recollés (LATAPY, AUDOUIN).
- code lot : gère « S10 - », « S 17 - » (espace) et « S01 SOLDE » (sans tiret).

Branchés dans extractor.py avec repli sur les anciens parseurs si un tableau n'a
pas de filets détectables. Validés par réconciliation comptable : locataires
124/124 lots, opérations 7/7 PDF et 25/25 catégories, au centime.

Ajoute le bouton « Relancer l'extraction » dans l'écran d'édition : re-extrait
depuis le PDF stocké et met en évidence les différences avec la version
précédente (panneau récapitulatif + anneaux « modifié » sur les cartes). Le diff
des opérations s'aligne par contenu (robuste aux changements d'ordre/nombre).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 21:29:04 +02:00
a90d1c4f64 feat: mode application de bureau + packaging Windows autonome
Permet de distribuer l'application en executable Windows double-clic,
sans Python/Node/terminal pour l'utilisateur final.

- `desktop.py` : demarre le serveur sur un port libre dans un thread et
  ouvre une fenetre native (pywebview). `PLESNA_DEBUG=1` active les
  DevTools. Commande `plesna-gerance desktop` (import paresseux).
- `paths.py` : resolution centralisee des chemins selon le contexte
  (dev, executable PyInstaller, surcharge par variables d'env). Les
  donnees vont dans un dossier utilisateur inscriptible (%APPDATA%),
  les ressources dans le bundle. connection.py/storage.py utilisent
  `get_data_dir()`, app.py `resource_path()`.
- app.py : type MIME `.mjs` force (modules ES / worker PDF.js sous
  Windows).
- Groupes de deps optionnels desktop / desktop-linux / build.
- packaging/ : spec PyInstaller, build_windows.ps1, installeur Inno
  Setup. Workflow GitHub build-windows.yml (runner Windows).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 17:21:27 +02:00
7cebc0bb3d feat: migre l'extraction de pdftotext (poppler) vers pdfplumber
Remplace l'appel externe `pdftotext -layout` (poppler-utils) par
pdfplumber, une bibliotheque Python pure : plus aucune dependance
systeme, ce qui permettra d'empaqueter l'application en executable
autonome.

- `read_pdf()` retourne un `PdfContent` (texte layout + mots de la
  premiere page avec coordonnees) ; `extract_text_from_pdf()` est
  conserve comme wrapper de compatibilite.
- Metadonnees : la reference est desormais extraite via les
  coordonnees geometriques des mots (fiable sur l'en-tete
  multi-colonnes), regex du destinataire durcie (forme pointee
  `S.C.I.`).
- Operations : reduction des espaces multiples dans les descriptions.
- Dockerfile : suppression de poppler-utils.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 17:14:31 +02:00
d42bfecab4 chore: ajoute ruff (lint) et corrige les violations
- Config ruff dans pyproject.toml : règles E/W/F/I/UP/B, whitelist des appels
  d'injection FastAPI (Depends/File/Form/Query) pour B008, B904 ignoré
  (traduction volontaire des exceptions en réponses HTTP)
- Auto-fixes : tri des imports, suppression d'imports inutilisés, annotations
  PEP 604, f-strings sans placeholder, modes open redondants
- Suppression de variables inutilisées (config.reset_setting, parser locataires)

ruff check . : All checks passed ; 61 tests OK

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 10:20:36 +02:00
4606d6785b refactor: factorise la création d'engine et normalise solde_montant
- connection: création de l'engine centralisée dans _build_engine (source
  unique de config) ; init_db réutilise reset_connection + get_engine au lieu
  de dupliquer create_engine
- service: _normalize_amount garantit qu'un montant non numérique issu de
  l'extraction (string, dict…) n'entre jamais en base dans une colonne Float ;
  appliqué à solde_montant
- tests: couverture de _normalize_amount et du solde sous forme de string

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 10:15:09 +02:00
635a094591 feat: durcit l'exécution SQL IA, borne les uploads et ajoute des tests
- sql_executor: remplace le filtre regex fragile par une autorisation SQLite
  (set_authorizer) en complément de mode=ro ; rejette les instructions multiples
- uploads: lecture bornée des PDF (helper read_upload_limited, limite 20 Mo,
  HTTP 413) branchée sur /extract et /save-with-pdf
- tests: suite pytest (54 tests) couvrant amounts, dates, storage, sql_executor,
  uploads et DatabaseService.save_document ; pytest ajouté en dépendance dev

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 10:11:07 +02:00
c15b65f0d9 fix: corrige path traversal, fuites d'erreurs et dépréciations
- Empêche le path traversal du catch-all SPA (vérif is_relative_to dist)
- Migre on_event("startup") vers le handler lifespan de FastAPI
- Masque les exceptions internes des erreurs 500 (log au lieu de str(e))
- Remplace datetime.utcnow déprécié par un helper UTC timezone-aware
- Simplifie except (ValueError, Exception) redondant

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 10:01:48 +02:00
a1d708d083 feat: add settings service and config API routes
Add a key-value settings table (Setting model) with DB > env > default
resolution chain. Replace hardcoded Ollama constants in ollama_service
with dynamic _get_config() lookups. Add /api/config/ endpoints for
settings CRUD and tag management (create, rename).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 16:09:02 +01:00
4fe6a4b2c9 feat: first IA page 2026-02-13 12:18:24 +01:00
5c9a8c9b5e feat: highlight source element when navigating from tables to edit page
Clicking the pencil icon in revenus/depenses tables now passes query
params to identify the source row. The edit page auto-expands the
matching section, scrolls to the element, and briefly highlights it
with a blue ring that fades after 2 seconds.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 06:16:29 +01:00
04db85c688 feat: seed predefined tags on database initialization
Automatically insert predefined tags when the tags table is empty
during DB init.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 05:34:48 +01:00
4f0ca67dc4 feat: reimport pdf 2026-01-22 21:23:46 +01:00
981fabcc27 feat: add rental income dashboard with comprehensive analytics
- Add new /api/revenus endpoints for rental income data
  - GET /summary: KPIs, monthly trends, breakdown by property, top unpaid
  - GET /details: detailed rental records with filtering
  - GET /by-lot: revenue aggregation by unit
  - GET /immeubles: properties list with revenue stats

- Add RevenusPage with full dashboard layout
  - 6 KPI cards: total revenue, payments, unpaid, collection rate, active tenants, occupied units
  - Monthly evolution chart (rent, payments, unpaid)
  - Property breakdown donut chart
  - Top unpaid tenants list
  - Detailed revenue table with pagination and CSV export

- Add filters panel (property, type, dates, unpaid only, history months)
- Display property address instead of code in all components
- Add navigation link in header (Revenus)
2026-01-20 10:05:57 +01:00
281f69f39e feat: enhance homepage with financial summary and sparklines
- Add dashboard API endpoints for financial summary, recent revenus, monthly trends, and immeuble shortcuts
- Create new dashboard components: FinancialSummary with sparklines, QuickActions, RecentRevenus, MiniTrendChart, ImmeubleShortcuts
- Refactor HomePage with prominent drag & drop zone and data-driven cards
- Financial cards now show last document data with 6-month trend sparklines in background
- Remove redundant import button, keep single upload zone at top
2026-01-20 04:53:04 +01:00
aac4d194e3 feat: add analytics 2026-01-19 21:40:10 +01:00
0fd6bdaeb3 feat: add depense tagging 2026-01-19 05:19:11 +01:00
c5e51a7513 feat: add homepage 2026-01-18 21:11:35 +01:00
ee1db93298 feat: fix parsing 2026-01-18 10:33:09 +01:00
6bad5fbaf9 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
2026-01-18 05:36:27 +01:00