Suivi consommables : compteur papier, ruban et photos avec diagnostic ratio
- Onglet Consommables dans General : barres de progression papier/ruban, compteur photos - Capacites configurables (feuilles par bobine / poses par bobine) - Reset individuel papier/ruban (quand on change la bobine) - Diagnostic ratio ruban/papier pour verifier que le rembobinage fonctionne - Tracking automatique a chaque impression (format-aware) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,7 @@ from backend.config import charger_config, mettre_a_jour_config
|
|||||||
log = logging.getLogger("photobooth.destinations")
|
log = logging.getLogger("photobooth.destinations")
|
||||||
|
|
||||||
|
|
||||||
def distribuer_photo(chemin_photo: Path, imprimee: bool = False, copies: int = 1):
|
def distribuer_photo(chemin_photo: Path, imprimee: bool = False, copies: int = 1, format_papier: str | None = None):
|
||||||
"""Copie la photo vers toutes les destinations activees."""
|
"""Copie la photo vers toutes les destinations activees."""
|
||||||
config = charger_config()
|
config = charger_config()
|
||||||
dest = config.get("destinations", {})
|
dest = config.get("destinations", {})
|
||||||
@@ -43,6 +43,7 @@ def distribuer_photo(chemin_photo: Path, imprimee: bool = False, copies: int = 1
|
|||||||
if compteur.get("actif", False):
|
if compteur.get("actif", False):
|
||||||
compteur["photos_prises"] = compteur.get("photos_prises", 0) + copies
|
compteur["photos_prises"] = compteur.get("photos_prises", 0) + copies
|
||||||
mettre_a_jour_config({"compteur": compteur})
|
mettre_a_jour_config({"compteur": compteur})
|
||||||
|
maj_consommables(config, copies, format_papier)
|
||||||
|
|
||||||
|
|
||||||
def copier_usb(chemin_photo: Path, chemin_usb: str, sous_dossier: str | None = None):
|
def copier_usb(chemin_photo: Path, chemin_usb: str, sous_dossier: str | None = None):
|
||||||
@@ -218,3 +219,65 @@ def compteur_restant() -> dict:
|
|||||||
def reset_compteur():
|
def reset_compteur():
|
||||||
"""Remet le compteur a zero."""
|
"""Remet le compteur a zero."""
|
||||||
mettre_a_jour_config({"compteur": {"photos_prises": 0}})
|
mettre_a_jour_config({"compteur": {"photos_prises": 0}})
|
||||||
|
|
||||||
|
|
||||||
|
# --- Suivi consommables ---
|
||||||
|
|
||||||
|
TAILLE_RUBAN = {
|
||||||
|
"15x20": 1.0,
|
||||||
|
"10x15": 0.667,
|
||||||
|
"10x15-2up": 1.0,
|
||||||
|
"15x20-2up": 1.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
def maj_consommables(config: dict, copies: int, format_papier: str | None):
|
||||||
|
"""Met a jour le suivi consommables (papier, ruban, photos)."""
|
||||||
|
conso = config.get("consommables", {})
|
||||||
|
fmt = format_papier or config.get("impression", {}).get("format", "15x20")
|
||||||
|
rembobinage = config.get("impression", {}).get("rembobinage_ruban", False)
|
||||||
|
is_2up = "-2up" in fmt
|
||||||
|
|
||||||
|
feuilles = copies if not is_2up else (copies + 1) // 2
|
||||||
|
conso["papier_utilise"] = conso.get("papier_utilise", 0) + feuilles
|
||||||
|
conso["photos_imprimees"] = conso.get("photos_imprimees", 0) + copies
|
||||||
|
|
||||||
|
if rembobinage:
|
||||||
|
ruban_par_feuille = TAILLE_RUBAN.get(fmt, 1.0)
|
||||||
|
else:
|
||||||
|
ruban_par_feuille = 1.0
|
||||||
|
conso["ruban_utilise"] = round(conso.get("ruban_utilise", 0) + feuilles * ruban_par_feuille, 1)
|
||||||
|
|
||||||
|
mettre_a_jour_config({"consommables": conso})
|
||||||
|
|
||||||
|
|
||||||
|
def consommables_etat() -> dict:
|
||||||
|
"""Retourne l'etat des consommables."""
|
||||||
|
config = charger_config()
|
||||||
|
conso = config.get("consommables", {})
|
||||||
|
papier_cap = conso.get("papier_capacite", 400)
|
||||||
|
ruban_cap = conso.get("ruban_capacite", 400)
|
||||||
|
papier_used = conso.get("papier_utilise", 0)
|
||||||
|
ruban_used = conso.get("ruban_utilise", 0)
|
||||||
|
photos = conso.get("photos_imprimees", 0)
|
||||||
|
return {
|
||||||
|
"papier_capacite": papier_cap,
|
||||||
|
"papier_utilise": papier_used,
|
||||||
|
"papier_restant": max(0, papier_cap - papier_used),
|
||||||
|
"ruban_capacite": ruban_cap,
|
||||||
|
"ruban_utilise": ruban_used,
|
||||||
|
"ruban_restant": max(0, round(ruban_cap - ruban_used, 1)),
|
||||||
|
"photos_imprimees": photos,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def reset_consommables(quoi: str):
|
||||||
|
"""Reinitialise papier, ruban ou tout."""
|
||||||
|
config = charger_config()
|
||||||
|
conso = config.get("consommables", {})
|
||||||
|
if quoi in ("papier", "tout"):
|
||||||
|
conso["papier_utilise"] = 0
|
||||||
|
if quoi in ("ruban", "tout"):
|
||||||
|
conso["ruban_utilise"] = 0
|
||||||
|
if quoi == "tout":
|
||||||
|
conso["photos_imprimees"] = 0
|
||||||
|
mettre_a_jour_config({"consommables": conso})
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ except ImportError:
|
|||||||
from backend.gallery import lister_photos, compter_photos, supprimer_photo, vider_galerie
|
from backend.gallery import lister_photos, compter_photos, supprimer_photo, vider_galerie
|
||||||
from backend.effects import appliquer_filtre, appliquer_overlay, chroma_key, lister_overlays, lister_fonds, lister_cadres, FILTRES
|
from backend.effects import appliquer_filtre, appliquer_overlay, chroma_key, lister_overlays, lister_fonds, lister_cadres, FILTRES
|
||||||
from backend.collage import creer_strip, creer_collage
|
from backend.collage import creer_strip, creer_collage
|
||||||
from backend.destinations import distribuer_photo, detecter_usb, compteur_restant, reset_compteur, recuperer_booth_password
|
from backend.destinations import distribuer_photo, detecter_usb, compteur_restant, reset_compteur, recuperer_booth_password, consommables_etat, reset_consommables
|
||||||
from backend.printer import lister_imprimantes, imprimer
|
from backend.printer import lister_imprimantes, imprimer
|
||||||
from backend.mailer import envoyer_photo, charger_emails_historique, effacer_emails_historique, ajouter_au_spool, taille_spool, tache_spool_demarrage
|
from backend.mailer import envoyer_photo, charger_emails_historique, effacer_emails_historique, ajouter_au_spool, taille_spool, tache_spool_demarrage
|
||||||
from backend.qrcode_gen import generer_qr, qr_galerie
|
from backend.qrcode_gen import generer_qr, qr_galerie
|
||||||
@@ -903,10 +903,41 @@ async def api_imprimer(donnees: dict):
|
|||||||
format_papier = donnees.get("format_papier") or None
|
format_papier = donnees.get("format_papier") or None
|
||||||
resultat = imprimer(chemin, copies=copies, cadre_override=cadre_override, format_papier=format_papier)
|
resultat = imprimer(chemin, copies=copies, cadre_override=cadre_override, format_papier=format_papier)
|
||||||
if resultat.get("succes"):
|
if resultat.get("succes"):
|
||||||
distribuer_photo(chemin, imprimee=True, copies=copies)
|
distribuer_photo(chemin, imprimee=True, copies=copies, format_papier=format_papier)
|
||||||
return resultat
|
return resultat
|
||||||
|
|
||||||
|
|
||||||
|
# --- API Consommables ---
|
||||||
|
|
||||||
|
@app.get("/api/consommables")
|
||||||
|
async def api_consommables():
|
||||||
|
return consommables_etat()
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/consommables/reset")
|
||||||
|
async def api_reset_consommables(request: Request):
|
||||||
|
body = await request.json()
|
||||||
|
quoi = body.get("quoi", "tout")
|
||||||
|
reset_consommables(quoi)
|
||||||
|
return consommables_etat()
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/consommables/capacite")
|
||||||
|
async def api_consommables_capacite(request: Request):
|
||||||
|
body = await request.json()
|
||||||
|
updates = {}
|
||||||
|
if "papier_capacite" in body:
|
||||||
|
updates["papier_capacite"] = int(body["papier_capacite"])
|
||||||
|
if "ruban_capacite" in body:
|
||||||
|
updates["ruban_capacite"] = int(body["ruban_capacite"])
|
||||||
|
if updates:
|
||||||
|
config = charger_config()
|
||||||
|
conso = config.get("consommables", {})
|
||||||
|
conso.update(updates)
|
||||||
|
mettre_a_jour_config({"consommables": conso})
|
||||||
|
return consommables_etat()
|
||||||
|
|
||||||
|
|
||||||
# --- API Booth (galerie live) ---
|
# --- API Booth (galerie live) ---
|
||||||
|
|
||||||
@app.get("/api/booth/info")
|
@app.get("/api/booth/info")
|
||||||
|
|||||||
@@ -935,6 +935,42 @@ html, body {
|
|||||||
}
|
}
|
||||||
.admin-onglets.cache { display: none; }
|
.admin-onglets.cache { display: none; }
|
||||||
|
|
||||||
|
.conso-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
.conso-card {
|
||||||
|
background: rgba(255,255,255,0.04);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.conso-icon { font-size: 2rem; margin-bottom: 0.3rem; }
|
||||||
|
.conso-titre { font-weight: 600; margin-bottom: 0.8rem; font-size: 0.95rem; }
|
||||||
|
.conso-barre-fond {
|
||||||
|
height: 12px;
|
||||||
|
background: rgba(255,255,255,0.08);
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.conso-barre {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: width 0.5s ease, background 0.5s ease;
|
||||||
|
}
|
||||||
|
.conso-chiffres { font-size: 0.85rem; color: var(--texte-secondaire); margin-bottom: 0.8rem; }
|
||||||
|
.conso-gros { font-size: 2.5rem; font-weight: 800; margin: 0.5rem 0; }
|
||||||
|
.conso-diag {
|
||||||
|
background: rgba(255,255,255,0.03);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
.video-situation-bloc {
|
.video-situation-bloc {
|
||||||
background: rgba(255,255,255,0.03);
|
background: rgba(255,255,255,0.03);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||||||
<meta name="google" content="notranslate">
|
<meta name="google" content="notranslate">
|
||||||
<meta http-equiv="Content-Language" content="fr">
|
<meta http-equiv="Content-Language" content="fr">
|
||||||
<link rel="stylesheet" href="/css/style.css?v=13">
|
<link rel="stylesheet" href="/css/style.css?v=14">
|
||||||
<link rel="stylesheet" href="/css/themes.css?v=2">
|
<link rel="stylesheet" href="/css/themes.css?v=2">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -379,6 +379,7 @@
|
|||||||
<div class="admin-onglets" id="onglets-general">
|
<div class="admin-onglets" id="onglets-general">
|
||||||
<button class="onglet actif" data-onglet="materiel">Materiel</button>
|
<button class="onglet actif" data-onglet="materiel">Materiel</button>
|
||||||
<button class="onglet" data-onglet="compteur-admin">Compteur</button>
|
<button class="onglet" data-onglet="compteur-admin">Compteur</button>
|
||||||
|
<button class="onglet" data-onglet="consommables-admin" onclick="chargerConsommables()">Consommables</button>
|
||||||
<button class="onglet" data-onglet="fonctions">Fonctions</button>
|
<button class="onglet" data-onglet="fonctions">Fonctions</button>
|
||||||
<button class="onglet" data-onglet="personnalisation">Personnalisation</button>
|
<button class="onglet" data-onglet="personnalisation">Personnalisation</button>
|
||||||
<button class="onglet" data-onglet="videos-admin" onclick="chargerVideosAdmin()">Videos</button>
|
<button class="onglet" data-onglet="videos-admin" onclick="chargerVideosAdmin()">Videos</button>
|
||||||
@@ -476,6 +477,48 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Panneau Consommables -->
|
||||||
|
<div class="admin-panneau" id="panneau-consommables-admin">
|
||||||
|
<h3>Suivi consommables</h3>
|
||||||
|
<p class="aide">Suivez l'usure du papier et du ruban. Reinitalisez quand vous changez une bobine.</p>
|
||||||
|
|
||||||
|
<div class="conso-grid">
|
||||||
|
<div class="conso-card">
|
||||||
|
<div class="conso-icon">📄</div>
|
||||||
|
<div class="conso-titre">Papier</div>
|
||||||
|
<div class="conso-barre-fond"><div class="conso-barre conso-barre-papier" id="conso-barre-papier"></div></div>
|
||||||
|
<div class="conso-chiffres"><span id="conso-papier-restant">--</span> / <span id="conso-papier-cap">--</span> feuilles</div>
|
||||||
|
<button class="btn-secondaire btn-petit" onclick="resetConsommable('papier')">Bobine changee</button>
|
||||||
|
</div>
|
||||||
|
<div class="conso-card">
|
||||||
|
<div class="conso-icon">🎨</div>
|
||||||
|
<div class="conso-titre">Ruban</div>
|
||||||
|
<div class="conso-barre-fond"><div class="conso-barre conso-barre-ruban" id="conso-barre-ruban"></div></div>
|
||||||
|
<div class="conso-chiffres"><span id="conso-ruban-restant">--</span> / <span id="conso-ruban-cap">--</span> poses</div>
|
||||||
|
<button class="btn-secondaire btn-petit" onclick="resetConsommable('ruban')">Ruban change</button>
|
||||||
|
</div>
|
||||||
|
<div class="conso-card">
|
||||||
|
<div class="conso-icon">📷</div>
|
||||||
|
<div class="conso-titre">Photos sorties</div>
|
||||||
|
<div class="conso-gros" id="conso-photos">--</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 style="margin-top:1.5rem">Capacites</h3>
|
||||||
|
<div class="champ">
|
||||||
|
<label>Capacite papier (feuilles par bobine)</label>
|
||||||
|
<input type="number" id="conso-cap-papier" min="1" max="9999" value="400">
|
||||||
|
</div>
|
||||||
|
<div class="champ">
|
||||||
|
<label>Capacite ruban (poses par bobine)</label>
|
||||||
|
<input type="number" id="conso-cap-ruban" min="1" max="9999" value="400">
|
||||||
|
</div>
|
||||||
|
<button class="btn-action" onclick="sauvegarderCapacites()">Sauvegarder capacites</button>
|
||||||
|
<button class="btn-danger" style="margin-top:0.5rem" onclick="resetConsommable('tout')">Tout reinitialiser</button>
|
||||||
|
|
||||||
|
<div class="conso-diag" id="conso-diagnostic" style="margin-top:1.5rem"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Panneau Destinations -->
|
<!-- Panneau Destinations -->
|
||||||
<div class="admin-panneau" id="panneau-destinations-admin">
|
<div class="admin-panneau" id="panneau-destinations-admin">
|
||||||
<h3>Ou sauvegarder les photos ?</h3>
|
<h3>Ou sauvegarder les photos ?</h3>
|
||||||
@@ -1017,6 +1060,6 @@
|
|||||||
<script src="/js/effects.js?v=4"></script>
|
<script src="/js/effects.js?v=4"></script>
|
||||||
<script src="/js/gallery.js?v=4"></script>
|
<script src="/js/gallery.js?v=4"></script>
|
||||||
<script src="/js/share.js?v=8"></script>
|
<script src="/js/share.js?v=8"></script>
|
||||||
<script src="/js/admin.js?v=10"></script>
|
<script src="/js/admin.js?v=11"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1401,6 +1401,67 @@ async function chargerDiagCamera() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === CONSOMMABLES ===
|
||||||
|
|
||||||
|
async function chargerConsommables() {
|
||||||
|
try {
|
||||||
|
const data = await apiGet('/api/consommables');
|
||||||
|
document.getElementById('conso-papier-restant').textContent = data.papier_restant;
|
||||||
|
document.getElementById('conso-papier-cap').textContent = data.papier_capacite;
|
||||||
|
document.getElementById('conso-ruban-restant').textContent = data.ruban_restant;
|
||||||
|
document.getElementById('conso-ruban-cap').textContent = data.ruban_capacite;
|
||||||
|
document.getElementById('conso-photos').textContent = data.photos_imprimees;
|
||||||
|
setValue('conso-cap-papier', data.papier_capacite);
|
||||||
|
setValue('conso-cap-ruban', data.ruban_capacite);
|
||||||
|
|
||||||
|
const pctPapier = data.papier_capacite > 0 ? Math.max(0, (data.papier_restant / data.papier_capacite) * 100) : 0;
|
||||||
|
const pctRuban = data.ruban_capacite > 0 ? Math.max(0, (data.ruban_restant / data.ruban_capacite) * 100) : 0;
|
||||||
|
|
||||||
|
const barrePapier = document.getElementById('conso-barre-papier');
|
||||||
|
const barreRuban = document.getElementById('conso-barre-ruban');
|
||||||
|
barrePapier.style.width = pctPapier + '%';
|
||||||
|
barreRuban.style.width = pctRuban + '%';
|
||||||
|
barrePapier.style.background = pctPapier < 10 ? '#f44336' : pctPapier < 25 ? '#fb8c00' : '#43a047';
|
||||||
|
barreRuban.style.background = pctRuban < 10 ? '#f44336' : pctRuban < 25 ? '#fb8c00' : '#43a047';
|
||||||
|
|
||||||
|
const diag = document.getElementById('conso-diagnostic');
|
||||||
|
const papierUsed = data.papier_utilise;
|
||||||
|
const rubanUsed = data.ruban_utilise;
|
||||||
|
let html = '<strong>Diagnostic</strong><br>';
|
||||||
|
html += `Feuilles consommees : ${papierUsed}<br>`;
|
||||||
|
html += `Poses ruban consommees : ${rubanUsed}<br>`;
|
||||||
|
html += `Photos imprimees : ${data.photos_imprimees}<br>`;
|
||||||
|
if (papierUsed > 0) {
|
||||||
|
const ratio = (rubanUsed / papierUsed).toFixed(2);
|
||||||
|
html += `<br>Ratio ruban/papier : <strong>${ratio}</strong>`;
|
||||||
|
if (parseFloat(ratio) <= 0.75) {
|
||||||
|
html += ' ✅ Rembobinage actif (economie ruban)';
|
||||||
|
} else if (parseFloat(ratio) >= 0.95) {
|
||||||
|
html += ' ⚠ Ratio 1:1 — pas d\'economie ruban';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
diag.innerHTML = html;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Erreur chargement consommables:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resetConsommable(quoi) {
|
||||||
|
const labels = { papier: 'Reinitialiser le compteur papier ?', ruban: 'Reinitialiser le compteur ruban ?', tout: 'Reinitialiser tous les compteurs ?' };
|
||||||
|
if (!confirm(labels[quoi] || 'Reinitialiser ?')) return;
|
||||||
|
await apiPost('/api/consommables/reset', { quoi });
|
||||||
|
chargerConsommables();
|
||||||
|
afficherStatut('Consommable reinitialise', 'succes');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sauvegarderCapacites() {
|
||||||
|
const papier = parseInt(getValue('conso-cap-papier')) || 400;
|
||||||
|
const ruban = parseInt(getValue('conso-cap-ruban')) || 400;
|
||||||
|
await apiPost('/api/consommables/capacite', { papier_capacite: papier, ruban_capacite: ruban });
|
||||||
|
chargerConsommables();
|
||||||
|
afficherStatut('Capacites sauvegardees', 'succes');
|
||||||
|
}
|
||||||
|
|
||||||
// === VIDEOS DE SITUATION ===
|
// === VIDEOS DE SITUATION ===
|
||||||
|
|
||||||
async function chargerVideosAdmin() {
|
async function chargerVideosAdmin() {
|
||||||
|
|||||||
Reference in New Issue
Block a user