2026-07-14 16:38:59 +00:00
|
|
|
# CLAUDE.md
|
|
|
|
|
|
|
|
|
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
|
|
|
|
|
|
|
|
## Projet
|
|
|
|
|
|
|
|
|
|
Application web interne pour le département IT d'une clinique privée : gestion
|
2026-07-14 17:06:37 +00:00
|
|
|
du stock de matériel informatique (arborescence de catégories + matériels),
|
2026-07-14 16:38:59 +00:00
|
|
|
avec recherche de matériel par scan de code-barres et alertes email quand le
|
|
|
|
|
stock passe sous un seuil défini.
|
|
|
|
|
|
|
|
|
|
Contraintes du domaine, volontairement strictes :
|
|
|
|
|
- **Pas d'authentification dans l'app** : l'accès est restreint par un
|
|
|
|
|
mécanisme externe (réseau interne, proxy...). Ne pas ajouter de login/session.
|
|
|
|
|
- **Pas de suivi d'exemplaires individuels.** Un `Materiel` est un *type*
|
|
|
|
|
d'objet (ex: "PC Dell XPS 13 modèle 9310", "Câble RJ45 2m") avec juste une
|
|
|
|
|
quantité en stock. Pas de numéro de série, pas d'historique par unité.
|
|
|
|
|
- **Douchette code-barres = clavier.** Les douchettes USB/Bluetooth émulent un
|
|
|
|
|
clavier (elles tapent le code + Entrée). Aucune intégration matérielle
|
|
|
|
|
n'est nécessaire côté app : un simple `<input>` seul dans un `<form>`
|
|
|
|
|
suffit, le navigateur soumet au Entrée.
|
2026-07-14 17:06:37 +00:00
|
|
|
- **Catégories à profondeur illimitée.** `Categorie` est auto-référencée
|
|
|
|
|
(`parent_id`), pas un modèle Catégorie/Sous-catégorie à deux niveaux fixes :
|
|
|
|
|
une catégorie peut avoir des sous-catégories qui en ont elles-mêmes, sans
|
|
|
|
|
limite (ex: Réseau > Câbles > RJ45 > 2m). Voir `Categorie.chemin()`.
|
|
|
|
|
- **Listes potentiellement longues.** Les pages Matériels/Catégories/
|
|
|
|
|
Destinataires sont conçues pour rester utilisables avec beaucoup
|
|
|
|
|
d'éléments : recherche texte côté serveur (`?q=`), lignes repliées par
|
|
|
|
|
défaut (`<details>`), jamais de tableau avec un `<input>` par colonne
|
|
|
|
|
affiché sur toutes les lignes à la fois (ça déborde horizontalement).
|
2026-07-14 16:38:59 +00:00
|
|
|
|
|
|
|
|
## Stack
|
|
|
|
|
|
|
|
|
|
Python + FastAPI + SQLModel (ORM) + SQLite (fichier `data/stock.db`) +
|
|
|
|
|
Jinja2 (rendu HTML côté serveur, formulaires classiques, pas de bundler JS).
|
|
|
|
|
|
|
|
|
|
Tout vit dans ce dossier : pas de service externe requis pour développer et
|
|
|
|
|
tester en local. Les emails d'alerte sont simulés dans la console tant
|
|
|
|
|
qu'aucun SMTP n'est configuré (voir `app/email_alerts.py` et `.env.example`).
|
|
|
|
|
|
|
|
|
|
## Commandes
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
source .venv/bin/activate # créer avec: python3 -m venv .venv
|
|
|
|
|
pip install -r requirements-dev.txt
|
|
|
|
|
uvicorn app.main:app --reload # lancer le serveur de dev (http://127.0.0.1:8000)
|
|
|
|
|
pytest # lancer les tests
|
|
|
|
|
ruff check . # linter
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Les tests utilisent une base SQLite **en mémoire** (voir `tests/conftest.py`),
|
|
|
|
|
jamais le fichier `data/stock.db` du développement.
|
|
|
|
|
|
|
|
|
|
## Convention : commentaires détaillés
|
|
|
|
|
|
|
|
|
|
Contrairement à la préférence par défaut (commentaires minimaux), ce projet
|
|
|
|
|
doit rester compréhensible par d'autres membres du département IT qui n'ont
|
|
|
|
|
pas suivi son développement. **Commenter en français, de façon détaillée**,
|
|
|
|
|
en particulier le "pourquoi" des décisions non évidentes (ex: pourquoi la
|
|
|
|
|
douchette ne nécessite pas d'intégration, pourquoi les emails sont simulés en
|
|
|
|
|
l'absence de SMTP_HOST). Ne pas se limiter au commentaire minimal habituel.
|
|
|
|
|
|
|
|
|
|
## Architecture
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
app/
|
2026-07-14 17:28:39 +00:00
|
|
|
main.py point d'entrée FastAPI, montage des routers, démarrage du planificateur
|
|
|
|
|
config.py lecture des variables d'environnement (SMTP, cron du résumé...)
|
2026-07-14 16:38:59 +00:00
|
|
|
database.py connexion SQLite + dependency get_session()
|
2026-07-14 17:06:37 +00:00
|
|
|
models.py modèles SQLModel : Categorie (arborescence auto-référencée), Materiel, DestinataireAlerte
|
|
|
|
|
categories_arbre.py parcours de l'arborescence (racines, aplatissement pour un <select> indenté)
|
2026-07-14 17:28:39 +00:00
|
|
|
email_alerts.py alerte immédiate + résumé périodique de stock, envoie/simule l'email
|
|
|
|
|
scheduler.py planification (APScheduler) de l'email récapitulatif périodique
|
2026-07-14 16:38:59 +00:00
|
|
|
templates_engine.py instance Jinja2Templates partagée par tous les routers
|
|
|
|
|
routers/ une route FastAPI par ressource (categories, materiels, scan, destinataires)
|
|
|
|
|
templates/ pages HTML (Jinja2, formulaires HTML classiques)
|
|
|
|
|
static/ CSS
|
|
|
|
|
tests/ pytest + TestClient FastAPI + base SQLite en mémoire
|
|
|
|
|
data/ base SQLite locale, ignorée par git
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Point d'attention Starlette : `Jinja2Templates.TemplateResponse()` prend
|
|
|
|
|
`request` en premier argument positionnel (`templates.TemplateResponse(request,
|
|
|
|
|
"page.html", {...})`), **pas** `{"request": request, ...}` dans le contexte —
|
|
|
|
|
c'est l'API de la version de Starlette installée ici.
|
|
|
|
|
|
2026-07-14 17:06:37 +00:00
|
|
|
Les listings (`materiels.html`, `categories.html`) utilisent `<details>`/
|
|
|
|
|
`<summary>` pour replier les lignes/branches par défaut plutôt qu'un
|
|
|
|
|
tableau tout-en-input : c'est ce qui évite le débordement horizontal et
|
|
|
|
|
garde la page utilisable avec beaucoup d'éléments. Le formulaire d'édition
|
|
|
|
|
complet d'un matériel ne s'affiche que dans la ligne dépliée (voir
|
|
|
|
|
`.formulaire-edition` dans `style.css`), pas sur toutes les lignes en
|
|
|
|
|
permanence.
|
2026-07-14 16:38:59 +00:00
|
|
|
|
2026-07-14 17:17:27 +00:00
|
|
|
`/materiels` (aussi servie sur `/`, voir `main.py`) combine recherche
|
|
|
|
|
(`?q=`), filtre par catégorie — elle et toutes ses sous-catégories, via
|
|
|
|
|
`ids_sous_arbre()` — filtre stock bas (`?stock_bas=1`) et tri (`?tri=`),
|
|
|
|
|
tous combinables. `tri=categorie` trie par chemin de catégorie puis nom,
|
|
|
|
|
ce qui rend les matériels d'une même catégorie consécutifs : le
|
|
|
|
|
regroupement visuel (en-têtes `.groupe-categorie-entete`) vient de ce tri,
|
|
|
|
|
pas d'une logique de regroupement séparée à maintenir.
|
|
|
|
|
|
|
|
|
|
L'écran de scan (`/scan`) gère 3 cas après un scan : matériel trouvé
|
|
|
|
|
(étiquette + contrôles de stock), code inconnu (formulaire de création
|
|
|
|
|
identique à celui de `/materiels`, avec le code-barre pré-rempli, pour
|
|
|
|
|
créer sans changer de page), ou matériel venant d'être créé (même rendu
|
|
|
|
|
que "trouvé"). Deux façons distinctes d'agir sur le stock, volontairement
|
|
|
|
|
séparées pour ne jamais laisser d'ambiguïté sur ce qu'un nombre représente :
|
|
|
|
|
`/scan/{id}/ajuster` applique un delta choisi (pas fixé à ±1) via un champ
|
|
|
|
|
« sens » (ajouter/retirer), `/scan/{id}/definir` fixe la quantité absolue
|
|
|
|
|
(utile après un inventaire physique).
|
|
|
|
|
|
2026-07-14 17:28:39 +00:00
|
|
|
## Emails de stock
|
2026-07-14 16:38:59 +00:00
|
|
|
|
|
|
|
|
Seuil configurable par matériel (`Materiel.seuil_alerte`), liste globale de
|
2026-07-14 17:28:39 +00:00
|
|
|
destinataires (`DestinataireAlerte`, gérée sur `/destinataires`). Si
|
|
|
|
|
`SMTP_HOST` n'est pas défini, rien n'est envoyé : l'email est juste affiché
|
|
|
|
|
dans la console du serveur — pratique pour tester sans serveur mail réel.
|
|
|
|
|
|
|
|
|
|
Deux emails distincts, tous les deux dans `app/email_alerts.py` :
|
|
|
|
|
- **Alerte immédiate** (`verifier_et_alerter`) : déclenchée après toute
|
|
|
|
|
modification de quantité qui fait passer un matériel sous son seuil.
|
|
|
|
|
Contient ce matériel, puis un résumé de *tous* les articles actuellement
|
|
|
|
|
en stock bas (`materiels_stock_bas()`), pas seulement celui qui a
|
|
|
|
|
déclenché l'alerte.
|
|
|
|
|
- **Résumé périodique** (`envoyer_resume_stock`) : stock bas en premier,
|
|
|
|
|
puis le reste du stock groupé par catégorie. Planifié par
|
|
|
|
|
`app/scheduler.py` (APScheduler, tourne dans le process de l'app, pas de
|
2026-07-14 17:37:36 +00:00
|
|
|
cron système à configurer), avec l'expression cron stockée en base
|
|
|
|
|
(`ParametrePlanification`, une seule ligne) et modifiable depuis
|
|
|
|
|
l'interface sur `/destinataires` — pas de fichier à éditer ni de
|
|
|
|
|
redémarrage nécessaire. Défaut : `0 9 * * mon` (tous les lundis à 9h).
|
|
|
|
|
Limite : ne se déclenche que si le serveur tourne au moment prévu, pas
|
|
|
|
|
de rattrapage. `scripts/envoyer_resume_stock.py` permet de le déclencher
|
|
|
|
|
manuellement (utile pour tester le contenu, ou pour brancher l'envoi sur
|
|
|
|
|
un cron système / Planificateur de tâches Windows à la place si cette
|
|
|
|
|
limite devient un problème).
|
|
|
|
|
|
|
|
|
|
**Piège APScheduler** : le champ "jour de la semaine" d'une expression
|
|
|
|
|
cron numérique NE SUIT PAS la convention Unix (0=dimanche). Chez
|
|
|
|
|
APScheduler, 0=lundi (convention `datetime.weekday()`), donc `0 9 * * 1`
|
|
|
|
|
tombe un **mardi**, pas un lundi. Pour éviter toute confusion, le code et
|
|
|
|
|
les exemples proposés à l'utilisateur (`EXEMPLES_CRON`) n'utilisent que
|
|
|
|
|
des noms de jours (`mon`, `tue`, ...), jamais de chiffres. Vérifié
|
|
|
|
|
empiriquement avec la version d'APScheduler installée (voir le
|
|
|
|
|
commentaire en tête de `app/scheduler.py`).
|