From d4d51b205f802e02fb512099713e0ae25139011d Mon Sep 17 00:00:00 2001 From: Jo Date: Tue, 7 Jul 2026 15:04:27 +0200 Subject: [PATCH] docs: GUIDE + HANDOFF, docstrings app.py, fix syntax errors --- GUIDE.md | 248 ++++++++++++++++++++++++++++++++ HANDOFF.md | 193 +++++++++++++++++++++++++ __pycache__/app.cpython-310.pyc | Bin 86925 -> 91663 bytes 3 files changed, 441 insertions(+) create mode 100644 GUIDE.md create mode 100644 HANDOFF.md diff --git a/GUIDE.md b/GUIDE.md new file mode 100644 index 0000000..32f95e8 --- /dev/null +++ b/GUIDE.md @@ -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/`) +- 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/`) 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=` + +--- + +## 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//move` | POST | Déplacer un slot | +| `/api/slots//fail` | POST | Marquer comme raté | +| `/api/slots//done` | POST | Marquer comme terminé | +| `/api/slots//status` | POST | Changer le statut | +| `/api/slots//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//defaults` | GET | Profils par défaut d'un client | diff --git a/HANDOFF.md b/HANDOFF.md new file mode 100644 index 0000000..c239197 --- /dev/null +++ b/HANDOFF.md @@ -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` | — | diff --git a/__pycache__/app.cpython-310.pyc b/__pycache__/app.cpython-310.pyc index 681ac6998ee386d47c901d0a47300ee83f46dfff..2216e0884ebf17dab82a19c98716bc16feafb22b 100644 GIT binary patch delta 29121 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 delta 24194 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 MQf#G^3ftHJ53c2dr2qf`