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 681ac69..2216e08 100644 Binary files a/__pycache__/app.cpython-310.pyc and b/__pycache__/app.cpython-310.pyc differ