docs: GUIDE + HANDOFF, docstrings app.py, fix syntax errors

This commit is contained in:
Jo
2026-07-07 15:04:27 +02:00
parent 3da19f55a5
commit d4d51b205f
3 changed files with 441 additions and 0 deletions
+248
View File
@@ -0,0 +1,248 @@
# Guide — 3D Pricing App
Application web Flask/SQLite de gestion du pricing et de la planification pour un atelier d'impression 3D.
---
## Architecture technique
```
Flask (Python) ──► SQLite (WAL mode)
├── Templates Jinja2 (Bootstrap 5 + vis.js)
├── Thread arrière-plan : sync Home Assistant
└── Docker : /data/pricing.db (persistant)
```
**Stack :**
- Python 3.12 + Flask
- SQLite avec WAL (pas de blocage lecture/écriture simultanée)
- Bootstrap 5 + Bootstrap Icons
- vis.js Timeline (Gantt planning)
- Authelia OIDC (optionnel, activé via variables d'env)
- Home Assistant REST API (optionnel)
- Nextcloud WebDAV (optionnel)
---
## Schéma de base de données
| Table | Rôle |
|---|---|
| `settings` | Clés/valeurs de configuration globale |
| `clients` | Clients avec profils par défaut |
| `projects` | Projets regroupant plusieurs commandes |
| `jobs` | Commandes / calculs de prix (1 plateau = 1 job) |
| `materials` | Filaments avec stock et historique de prix |
| `material_price_history` | Historique des changements de prix filament |
| `machine_profiles` | Profils machine (usure, électricité) |
| `handling_profiles` | Profils manutention (taux horaire, minutes/plateau) |
| `material_profiles` | Profils marge matière |
| `pricing_profiles` | Profils multiplicateur design + marge brute |
| `printers` | Imprimantes physiques avec préfixe HA |
| `print_slots` | Créneaux d'impression planifiés |
| `working_schedule` | Planning hebdomadaire (TT / sur site / off) |
| `calendar_blocks` | Indisponibilités ponctuelles (plage de dates) |
---
## Moteur de calcul (`calc()`)
Tout se calcule **par pièce** à partir des données du plateau Bambu.
```
Coût fixe = matière + marge_matière + usure_machine + électricité
(divisé par pièces/plateau)
Sous-total = coût_fixe + manutention + design
Marge brute = sous-total × gross_margin_pct / 100
Total marge = manutention + design + marge_brute
Prix HT = coût_fixe + total_marge
Prix HT net = prix_ht / (1 - cotisations - VFL - autres_taxes)
└─ les charges fiscales sont "self-contained" dans le prix
Prix TTC = prix_ht_net × (1 + TVA)
Prix final = prix_ttc × (1 - remise)
```
**Multiplicateur design** : coefficient appliqué au coût matière pour valoriser le temps de modélisation. `×0.8` = design simple, `×1.8` = design complexe.
---
## Pages et fonctionnalités
### Dashboard (`/`)
Vue d'ensemble : 10 dernières commandes, CA total, CA du mois, alertes stock bas.
### Commandes (`/jobs`)
Liste de toutes les commandes avec réf. plateau, poids, durée, marge, remise, prix final.
- **Nouveau job** : saisie manuelle ou import `.gcode.3mf` depuis Nextcloud
- **Import Bambu** : parse `slice_info.config` → détecte les plateaux, poids, durée, filaments
- **Duplication** (`?clone_from=ID`) : copie les paramètres + re-parse automatiquement le fichier Nextcloud si `source_nc_path` est renseigné
- **Export CSV** : toutes les commandes en CSV
### Détail commande (`/jobs/<id>`)
- Décomposition du prix (coût fixe, variable, fiscal)
- Section **Plateaux d'impression** : liste des slots avec statut, pièces, avancement HA
- Portail client : génération d'un lien de partage tokenisé
### Projets (`/projects`)
Regroupe plusieurs commandes sous un même projet client. Affiche total pièces et CA.
### Clients (`/clients`)
Clients avec profils par défaut (machine, manutention, matière, tarif). Ces profils se chargent automatiquement à la création d'un job pour ce client.
### Matières (`/materials`)
- Stock en grammes avec alerte seuil bas
- Historique des prix
- Déduction automatique du stock à la création d'un job
- Suivi du gaspillage (impressions ratées)
### Profils (`/profiles`)
4 types de profils réutilisables :
- **Machine** : prix imprimante, durée de vie, nozzle, plateau, puissance, électricité
- **Manutention** : taux horaire, minutes par plateau
- **Matière** : marge matière en %
- **Tarif** : multiplicateur design + marge brute
### Planning (`/planning`)
Gantt vis.js sur 14 jours (démarre 12h avant maintenant pour voir les impressions en cours).
- **Gantt** : slots par imprimante, codes couleur par statut, bandes rouges si capacité saturée
- **À planifier** : jobs dont tous les plateaux ne sont pas encore planifiés
- **Overlay HA live** : rafraîchissement automatique (intervalle configurable) des statuts depuis Home Assistant
- **Badges compensation** : si HA signale des pièces ignorées sur un plateau, les autres plateaux du même job proposent d'augmenter leur quantité
**Planifier un slot :**
1. Cliquer sur un job "À planifier"
2. Choisir imprimante + date/heure de lancement
3. Option "Déjà imprimé" pour enregistrer rétroactivement sans vérification de capacité
**Statuts des slots :**
| Statut | Couleur | Signification |
|---|---|---|
| `planned` | bleu | Planifié, pas encore lancé |
| `running` | orange | En cours (mis à jour par HA sync) |
| `done` | vert | Terminé |
| `failed` | rouge | Raté (gaspillage logué) |
| `cancelled` | gris | Annulé |
### Imprimantes (`/printers`)
- Activer/désactiver des imprimantes
- Configurer le préfixe HA (`a1_1` → surveille `sensor.a1_1_etat_de_l_impression`)
- Modifier nom, préfixe HA, notes
### Planning horaire (`/working-schedule`)
Configuré par jour de la semaine :
- **TT** (télétravail) : fenêtre horaire avec nb de lancements calculé automatiquement
- **Onsite** : 2 créneaux fixes (ex: 07h00 + 18h30)
- **Off** : aucun lancement possible
### Indisponibilités (`/calendar-blocks`)
Bloquer une période (date début → date fin optionnelle) pour une ou toutes les imprimantes.
Types : Off / Télétravail / Sur site / Maintenance machine.
### Page mobile (`/mobile`)
Interface simplifiée pour saisie rapide sur smartphone :
- Ajouter une indisponibilité
- Décaler un créneau
- Marquer une impression comme ratée ou terminée
### Statistiques (`/stats`)
CA par période, matières les plus utilisées, gaspillage, top clients.
### Paramètres (`/settings`)
- Taux fiscaux (cotisations, VFL, TVA, autres taxes)
- Paramètres machine par défaut
- Configuration Nextcloud (URL, user, mot de passe d'app, dossier racine)
- Configuration Home Assistant (URL, token, intervalle de sync)
- Nombre max d'imprimantes simultanées
- Cooldown entre impressions (minutes)
---
## Home Assistant Sync
Le thread HA tourne en arrière-plan et poll toutes les N secondes (configurable, défaut 60s).
**Entités surveillées** (par imprimante avec préfixe `{prefix}`) :
- `sensor.{prefix}_etat_de_l_impression``running` / `finish` / `unknown`
- `binary_sensor.{prefix}_erreur_d_impression``on` / `off`
- `sensor.{prefix}_objets_ignores` → nombre de pièces ignorées (compensation multi-plateau)
- `sensor.{prefix}_avancement_de_l_impression` → % d'avancement
**Logique de sync :**
1. Cherche un slot `planned` ou `running` qui chevauche la fenêtre actuelle (±30 min)
2. `running` → passe le slot à `running`
3. `finish` → passe le slot à `done`
4. `binary_sensor erreur = on` → passe le slot à `failed`
5. `objets_ignores > 0` → met à jour `pieces_ignored` sur le slot
**Important** : le sync utilise des connexions courtes par imprimante (autocommit) pour ne pas bloquer les requêtes Flask.
---
## Nextcloud
Utilisé comme bibliothèque de fichiers `.gcode.3mf`.
- Navigation WebDAV via le picker modal dans "Nouveau job"
- Téléchargement + parsing du `.3mf` côté serveur (`/api/nextcloud/parse`)
- Chemin stocké dans `source_nc_path` pour permettre la duplication sans re-sélection
---
## Portail client
Chaque commande peut générer un lien tokenisé (`/portal/<token>`) donnant accès à :
- Décomposition du prix en lecture seule
- Sans authentification requise
---
## Variables d'environnement (Docker)
| Variable | Défaut | Rôle |
|---|---|---|
| `SECRET_KEY` | généré aléatoirement | Clé session Flask |
| `DATABASE_PATH` | `/data/pricing.db` | Chemin SQLite |
| `OIDC_CLIENT_ID` | — | Active l'auth OIDC si défini |
| `OIDC_CLIENT_SECRET` | — | Secret OIDC |
| `OIDC_DISCOVERY_URL` | — | URL metadata OIDC (Authelia) |
| `OIDC_END_SESSION_URL` | — | URL déconnexion OIDC |
---
## Calendrier iCal (`/calendar.ics`)
Export iCal des créneaux planifiés. Protégé par token (`ical_token` dans settings).
URL : `http://host/calendar.ics?token=<ical_token>`
---
## API JSON principales
| Endpoint | Méthode | Rôle |
|---|---|---|
| `/api/calculate` | POST | Calcul de prix live (formulaire new_job) |
| `/api/slots` | GET | Liste des slots pour le Gantt (7 derniers jours → +) |
| `/api/slots/new` | POST | Créer un slot planifié |
| `/api/slots/<id>/move` | POST | Déplacer un slot |
| `/api/slots/<id>/fail` | POST | Marquer comme raté |
| `/api/slots/<id>/done` | POST | Marquer comme terminé |
| `/api/slots/<id>/status` | POST | Changer le statut |
| `/api/slots/<id>/pieces_override` | POST | Override nb pièces (compensation) |
| `/api/suggest-slot` | POST | Suggérer le prochain créneau libre |
| `/api/capacity` | POST | Calculer la capacité de production |
| `/api/ha/status` | GET | Statuts HA de toutes les imprimantes |
| `/api/parse-3mf` | POST | Parser un fichier .3mf uploadé |
| `/api/nextcloud/browse` | GET | Lister un dossier Nextcloud |
| `/api/nextcloud/parse` | GET | Fetch + parser un .3mf Nextcloud |
| `/api/nextcloud/file` | GET | Proxy téléchargement fichier Nextcloud |
| `/api/client/<id>/defaults` | GET | Profils par défaut d'un client |
+193
View File
@@ -0,0 +1,193 @@
# Handoff — 3D Pricing App
Ce document permet de reprendre le développement dans un nouveau contexte de conversation.
Lire aussi `GUIDE.md` pour la documentation fonctionnelle complète.
---
## État du projet (07/07/2026)
Application **en production** sur le réseau local. Tous les bugs critiques connus sont résolus.
### Stack
- Python 3.12 / Flask / SQLite (WAL mode)
- Bootstrap 5 + vis.js Timeline
- Docker (port 5010 → 5000 interne)
- Données persistantes dans volume Docker `/data/pricing.db`
### Dépôt
```
C:\Users\jbper\Claude\3d-pricing\
├── app.py # ~2557 lignes — backend complet
├── templates/ # 27 templates Jinja2
├── GUIDE.md # Documentation fonctionnelle
├── HANDOFF.md # Ce fichier
├── Dockerfile
├── docker-compose.yml
└── requirements.txt # flask, authlib, requests
```
---
## Règle absolue : ne jamais éditer app.py avec l'outil Edit
Le fichier `app.py` est sur un montage NTFS (Windows) — l'outil `Edit` tronque le fichier silencieusement.
**Méthode obligatoire pour modifier app.py :**
```bash
# Toujours passer par un script Python et écrire directement via le chemin bash
python3 - << 'EOF'
with open('/sessions/festive-sharp-bardeen/mnt/3d-pricing/app.py', 'r') as f:
content = f.read()
content = content.replace(old, new, 1)
with open('/sessions/festive-sharp-bardeen/mnt/3d-pricing/app.py', 'w') as f:
f.write(content)
EOF
```
La même règle s'applique aux templates si besoin (mais ils sont plus petits, moins risqués).
---
## Migrations DB
Les migrations sont dans `init_db()` sous forme de liste `migrations` avec `try/except` par commande. **Ne jamais supprimer une migration existante** — l'ordre est cumulatif.
Dernières migrations ajoutées (à la fin de la liste) :
```python
'ALTER TABLE printers ADD COLUMN ha_entity_prefix TEXT DEFAULT ""'
'ALTER TABLE print_slots ADD COLUMN pieces_ignored INTEGER DEFAULT 0'
'ALTER TABLE print_slots ADD COLUMN pieces_override INTEGER DEFAULT NULL'
'ALTER TABLE calendar_blocks ADD COLUMN date_end TEXT DEFAULT NULL'
'ALTER TABLE jobs ADD COLUMN marge_pct_on_ht REAL DEFAULT 0'
'ALTER TABLE jobs ADD COLUMN source_nc_path TEXT DEFAULT ""'
```
---
## Fonctionnalités clés et leur implémentation
### Moteur de pricing (`calc()`, ligne ~458)
Calcul par pièce. `weight_g` et `print_time_s` = valeurs du **plateau complet** Bambu, divisées par `pieces_per_plate`. Les charges fiscales (cotisations, VFL, autres taxes) sont incluses dans le prix via `prix_ht_net = prix_ht / (1 - total_charges)`.
### Thread Home Assistant (`_ha_sync_once()`, ligne ~2103)
- Poll toutes les N secondes (setting `ha_poll_interval`, défaut 60s)
- **Connexions courtes par imprimante avec `isolation_level=None`** (autocommit) pour éviter `database is locked`
- Entités surveillées : `sensor.{prefix}_etat_de_l_impression`, `binary_sensor.{prefix}_erreur_d_impression`, `sensor.{prefix}_objets_ignores`, `sensor.{prefix}_avancement_de_l_impression`
### Multi-plateau
- `pieces_per_plate` sur le job = pièces par plateau physique
- `order_qty` = quantité totale commandée
- `total_plates_needed = ceil(order_qty / pieces_per_plate)` (division entière ceiling)
- "À planifier" sur le planning : affiche les jobs tant que `scheduled_plates < total_plates_needed`
- Compensation : si HA signale `pieces_ignored > 0` sur un slot, les autres slots du job affichent un badge amber pour override le nb de pièces
### Déjà imprimé (`already_done`)
Dans `POST /api/slots/new` : si `already_done=true`, les checks capacité/overlap sont **sautés entièrement**, slot inséré avec `status='done'`. L'ordre du check est important : `already_done` est testé **avant** `_check_overlap` et `_check_capacity`.
### Gantt (`planning.html`, ligne ~366)
```javascript
start: new Date(now.getTime() - 12 * 3600000), // -12h pour voir les impressions en cours
end: new Date(now.getTime() + 14 * 86400000),
```
### Indisponibilités par plage (`calendar_blocks`)
Colonne `date_end` optionnelle. Requêtes avec `COALESCE(date_end, date)` pour compatibilité ascendante.
### Duplication avec re-parse Nextcloud (`source_nc_path`)
- Nouveau champ DB `source_nc_path` : chemin WebDAV complet du fichier `.3mf`
- À la création d'un job depuis Nextcloud, le path est sauvegardé dans un hidden input
- Au chargement de `new_job` avec `clone_from`, `autoParseOnClone()` appelle `/api/nextcloud/parse?path=...` et reconstitue le sélecteur de plateaux
---
## Bugs résolus dans cette session
| Bug | Fix |
|---|---|
| `database is locked` lors de la création de job | WAL mode + connexions autocommit par imprimante dans le thread HA |
| Slot HA-synced invisible dans le Gantt | Gantt démarre 12h avant maintenant |
| Vérification capacité bloquait "Déjà imprimé" | Check `already_done` déplacé avant `_check_capacity` |
| Vérification capacité bloquait sur impressions en cours | Filtre `AND planned_end > datetime('now')` |
| "À planifier" ne montrait qu'un seul plateau | Requête compare `scheduled_plates < total_plates_needed` |
| `datetime.utcnow()` deprecated | Remplacé par `datetime.now(timezone.utc).replace(tzinfo=None)` |
| INSERT jobs : 36 `?` pour 37 colonnes | Ajout du `?` manquant pour `marge_pct_on_ht` |
| jobs.html tronqué après écriture Python | Toujours partir d'une version git saine avant modification |
---
## Points d'attention / dette technique
### Ce qui est propre
- WAL mode SQLite : pas de blocage lecture/écriture
- Thread HA : connexions courtes, autocommit, pas de transaction longue
- Migrations cumulatives dans `init_db()` avec `try/except` par ligne
- `login_required` décorateur + `require_login_global` pour protection globale si OIDC activé
### À surveiller
- **jobs.html** : toujours utiliser le script Python pour modifier, vérifier la syntaxe Jinja2 après chaque modif avec `python3 -c "import jinja2; ..."`
- **app.py** : après ajout de docstrings avec des accents, vérifier `python3 -m py_compile app.py` — les chaînes avec apostrophes dans les docstrings `"""` sont valides mais certaines substitutions de texte peuvent créer des conflits
- **INSERT jobs** : 38 colonnes = 38 `?`. Si on ajoute une colonne, ajouter aussi le `?` ET la valeur dans le tuple
### Fonctionnalité pas encore implémentée
- Recalcul de `marge_pct_on_ht` pour les jobs existants (valeur = 0 pour les anciens jobs)
- Stats : le gaspillage filament (`total_wasted_g`) n'est pas encore affiché dans les stats globales
- Page mobile : n'enregistre pas `source_nc_path` (pas critique)
---
## Commandes utiles
```bash
# Vérifier la syntaxe Python
python3 -m py_compile /sessions/festive-sharp-bardeen/mnt/3d-pricing/app.py
# Vérifier la syntaxe Jinja2 d'un template
python3 -c "
import jinja2
env = jinja2.Environment(loader=jinja2.FileSystemLoader('templates'))
env.parse(open('templates/jobs.html').read())
print('OK')
"
# Voir les routes Flask
grep -n "^@app.route" /sessions/festive-sharp-bardeen/mnt/3d-pricing/app.py
# Chercher dans app.py sans tronquer
grep -n "mot_clé" /sessions/festive-sharp-bardeen/mnt/3d-pricing/app.py
```
---
## Git
Le dépôt est sur Windows — les commandes git **doivent être exécutées depuis un terminal Windows**, pas depuis le shell Linux (le fichier `git/index.lock` bloque les commits depuis Linux).
```powershell
cd C:\Users\jbper\Claude\3d-pricing
git add -A
git commit -m "description"
```
Derniers commits :
```
3da19f5 feat: auto-reparse Nextcloud 3mf on job clone
0069ab2 fix: INSERT jobs missing ? for marge_pct_on_ht
80307ac feat: plate_name and total_marge in jobs list
f51a789 fix: HA sync per-printer autocommit connections
522a0c5 fix: WAL mode SQLite
b55edce fix: skip capacity/overlap when already_done=true
7824124 fix: replace deprecated utcnow()
1899165 fix: gantt starts 12h before now
```
---
## Chemins importants
| Quoi | Chemin Windows | Chemin Linux (bash) |
|---|---|---|
| Dossier projet | `C:\Users\jbper\Claude\3d-pricing\` | `/sessions/festive-sharp-bardeen/mnt/3d-pricing/` |
| app.py | `C:\Users\jbper\Claude\3d-pricing\app.py` | `/sessions/festive-sharp-bardeen/mnt/3d-pricing/app.py` |
| Templates | `C:\Users\jbper\Claude\3d-pricing\templates\` | `/sessions/festive-sharp-bardeen/mnt/3d-pricing/templates/` |
| Base de données | Dans le container Docker : `/data/pricing.db` | — |
Binary file not shown.