Supprime l'import ODS

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
toto
2026-06-24 15:38:02 +02:00
parent 6258fd3662
commit 2a217a3331
4 changed files with 2 additions and 146 deletions

View File

@ -1,19 +1,15 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from itertools import groupby from itertools import groupby
from pathlib import Path
import shutil
import tempfile
from fastapi import FastAPI, Form, Request, UploadFile, File from fastapi import FastAPI, Form, Request
from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from models import ( from models import (
load_config, load_week_pointages, save_week_pointages, load_conges, load_config, load_week_pointages, save_week_pointages, load_conges,
toggle_conge, upsert_pointages, list_all_weeks, toggle_conge, list_all_weeks,
) )
from ods_import import parse_ods
from calcul import compute_week, minutes_to_hhmm, hhmm_to_minutes, heures_dues from calcul import compute_week, minutes_to_hhmm, hhmm_to_minutes, heures_dues
app = FastAPI(title="Décompte Horaire") app = FastAPI(title="Décompte Horaire")
@ -280,22 +276,3 @@ def toggle_conge_route(date: str, type_conge: str, request: Request):
return RedirectResponse(f"/semaine/{iso.year}/{iso.week}", status_code=303) return RedirectResponse(f"/semaine/{iso.year}/{iso.week}", status_code=303)
@app.get("/import", response_class=HTMLResponse)
def import_page(request: Request, msg: str = ""):
return templates.TemplateResponse("import.html", {"request": request, "msg": msg})
@app.post("/import")
async def do_import(request: Request, fichier: UploadFile = File(...)):
with tempfile.NamedTemporaryFile(suffix=".ods", delete=False) as tmp:
shutil.copyfileobj(fichier.file, tmp)
tmp_path = tmp.name
try:
records = parse_ods(tmp_path)
except Exception as e:
return RedirectResponse(f"/import?msg=Erreur+parsing:+{e}", status_code=303)
finally:
Path(tmp_path).unlink(missing_ok=True)
upsert_pointages(records)
return RedirectResponse(f"/import?msg={len(records)}+jours+importés", status_code=303)

View File

@ -1,69 +0,0 @@
"""Parse badge-reader ODS files and return structured data."""
from datetime import datetime, time
from odf.opendocument import load
from odf.table import Table, TableRow, TableCell
from odf.text import P
def _cell_text(cell) -> str:
parts = []
for p in cell.getElementsByType(P):
for node in p.childNodes:
if hasattr(node, "data"):
parts.append(node.data)
return "".join(parts).strip()
def _parse_time(val: str) -> str | None:
"""Return HH:MM or None. Ignores zero-times (formula placeholders)."""
val = val.strip()
if not val:
return None
for fmt in ("%H:%M:%S", "%H:%M"):
try:
t = datetime.strptime(val, fmt)
hhmm = t.strftime("%H:%M")
if hhmm == "00:00":
return None
return hhmm
except ValueError:
pass
return None
def _parse_date(val: str) -> str | None:
"""Return YYYY-MM-DD or None."""
val = val.strip()
for fmt in ("%d/%m/%Y", "%Y-%m-%d"):
try:
return datetime.strptime(val, fmt).strftime("%Y-%m-%d")
except ValueError:
pass
return None
def parse_ods(path: str) -> list[dict]:
"""Return list of pointage dicts from all sheets in the ODS file."""
doc = load(path)
sheets = doc.spreadsheet.getElementsByType(Table)
records = []
for sheet in sheets:
rows = sheet.getElementsByType(TableRow)
for row in rows:
cells = row.getElementsByType(TableCell)
if len(cells) < 5:
continue
date_val = _cell_text(cells[0])
date = _parse_date(date_val)
if not date:
continue
records.append({
"date": date,
"matin_entree": _parse_time(_cell_text(cells[1])),
"matin_sortie": _parse_time(_cell_text(cells[2])),
"aprem_entree": _parse_time(_cell_text(cells[3])),
"aprem_sortie": _parse_time(_cell_text(cells[4])),
})
return records

View File

@ -368,34 +368,6 @@
.chip:hover { border-color: var(--text); color: var(--text); } .chip:hover { border-color: var(--text); color: var(--text); }
.chip.here { background: var(--text); border-color: var(--text); color: var(--ground); } .chip.here { background: var(--text); border-color: var(--text); color: var(--ground); }
/* ── Import ── */
.import-wrap { padding: 2rem; }
.import-wrap h2 { font-size: 1rem; font-weight: 600; margin-bottom: .75rem; border-bottom: 1px solid var(--rule); padding-bottom: .5rem; }
.import-desc { color: var(--muted); font-size: 13px; margin-bottom: 1.25rem; line-height: 1.65; }
.file-input { display: block; margin-bottom: 1rem; font-size: 13px; }
.btn-submit {
display: inline-flex;
align-items: center;
gap: .4rem;
padding: .5rem 1.1rem;
border: 1.5px solid var(--text);
background: var(--text);
color: var(--ground);
font-size: 12px;
font-family: var(--mono);
font-weight: 500;
letter-spacing: .05em;
text-transform: uppercase;
cursor: pointer;
transition: background .15s;
}
.btn-submit:hover { background: #33271E; border-color: #33271E; }
.msg-ok { padding: .6rem 1rem; border-left: 3px solid var(--pos); background: var(--pos-bg); color: var(--pos); font-size: 13px; margin-bottom: 1rem; }
.msg-err { padding: .6rem 1rem; border-left: 3px solid var(--neg); background: var(--neg-bg); color: var(--neg); font-size: 13px; margin-bottom: 1rem; }
/* ── Card shell (import page) ── */
.card { background: var(--surface); border: 1.5px solid var(--rule); }
/* ── Calendrier sélecteur ── */ /* ── Calendrier sélecteur ── */
.cal-trigger-wrap { position: relative; } .cal-trigger-wrap { position: relative; }
@ -519,7 +491,6 @@
<a href="/" class="nav-brand"><span class="nav-dot"></span>Décompte Horaire</a> <a href="/" class="nav-brand"><span class="nav-dot"></span>Décompte Horaire</a>
<span class="spacer"></span> <span class="spacer"></span>
<a href="/" class="nav-link">Semaine courante</a> <a href="/" class="nav-link">Semaine courante</a>
<a href="/import" class="nav-link">Importer ODS</a>
</nav> </nav>
<div class="container"> <div class="container">
{% block content %}{% endblock %} {% block content %}{% endblock %}

View File

@ -1,23 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="card import-wrap">
<h2>Importer un fichier ODS</h2>
{% if msg %}
{% if 'Erreur' in msg %}
<div class="msg-err">{{ msg }}</div>
{% else %}
<div class="msg-ok">✓ {{ msg }}</div>
{% endif %}
{% endif %}
<p class="import-desc">
Sélectionne le fichier exporté par la badgeuse.<br>
Les données existantes sont mises à jour — les congés ne sont pas affectés.
</p>
<form method="post" enctype="multipart/form-data">
<input class="file-input" type="file" name="fichier" accept=".ods" required>
<button type="submit" class="btn-submit">Importer</button>
</form>
</div>
{% endblock %}