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 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)
|
|
||||||
|
|||||||
@ -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: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 %}
|
||||||
|
|||||||
@ -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