Supprime l'import ODS
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
27
app/main.py
27
app/main.py
@ -1,19 +1,15 @@
|
||||
from datetime import datetime, timedelta
|
||||
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.templating import Jinja2Templates
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from models import (
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
@ -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
|
||||
@ -368,34 +368,6 @@
|
||||
.chip:hover { border-color: var(--text); color: var(--text); }
|
||||
.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 ── */
|
||||
.cal-trigger-wrap { position: relative; }
|
||||
|
||||
@ -519,7 +491,6 @@
|
||||
<a href="/" class="nav-brand"><span class="nav-dot"></span>Décompte Horaire</a>
|
||||
<span class="spacer"></span>
|
||||
<a href="/" class="nav-link">Semaine courante</a>
|
||||
<a href="/import" class="nav-link">Importer ODS</a>
|
||||
</nav>
|
||||
<div class="container">
|
||||
{% block content %}{% endblock %}
|
||||
|
||||
@ -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 %}
|
||||
Reference in New Issue
Block a user