Files
pointeuse-optimisator/app/export.py
Jacquin Antoine 065d73e15c Ajoute les astreintes : saisie de la fin de dernière activité, retour au bureau décalé de 11h de repos
Un nouveau bouton AS marque un jour d'astreinte. Ce jour et son lendemain,
une saisie « Fin activité » enregistre l'heure de fin de la dernière
intervention hors horaires (elle peut être le matin si la nuit a dépassé
minuit). L'app en déduit l'heure de retour au bureau (fin + 11h de repos
consécutif) et retire du décompte de la semaine toute la présence avant
cette heure : heures dues réduites, présence pointée avant le retour non
comptée, préconisations Fin cible démarrées à l'heure de retour sans
avertissement de retard, demi-journée entièrement en repos considérée
comme remplie. Les exports .ods gagnent les colonnes Astreinte et Retour,
et les stats n'ont plus les retards liés au repos.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-09-08 22:05:10 +02:00

192 lines
6.9 KiB
Python

"""Export des pointages au format ODS (tableur), par semaine ou par année."""
import io
from datetime import datetime, timedelta
from odf.opendocument import OpenDocumentSpreadsheet
from odf.style import Style, TableColumnProperties, TextProperties
from odf.table import Table, TableColumn, TableRow, TableCell
from odf.text import P
from calcul import compute_week, minutes_to_hhmm
from models import (
load_week_pointages, load_conges, load_astreintes, list_all_weeks,
load_plages, heures_jour_min,
)
JOURS_FR = ["Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi"]
HEADERS = [
"Date", "Jour", "Matin entrée", "Matin sortie", "Aprem entrée", "Aprem sortie",
"Travaillé", "Dû", "Δ jour", "Congé", "Astreinte", "Retour",
]
def _week_dates(year: int, week: int) -> list[str]:
first = datetime.strptime(f"{year}-W{week:02d}-1", "%G-W%V-%u").date()
return [(first + timedelta(days=i)).strftime("%Y-%m-%d") for i in range(5)]
def _new_doc():
doc = OpenDocumentSpreadsheet()
bold = Style(name="Bold", family="table-cell")
bold.addElement(TextProperties(fontweight="bold"))
doc.styles.addElement(bold)
wide_col = Style(name="WideCol", family="table-column")
wide_col.addElement(TableColumnProperties(columnwidth="2.4cm"))
doc.styles.addElement(wide_col)
return doc, bold, wide_col
def _cell(text, style=None) -> TableCell:
c = TableCell(stylename=style) if style else TableCell()
c.addElement(P(text="" if text is None else str(text)))
return c
def _header_row(bold: Style) -> TableRow:
row = TableRow()
for h in HEADERS:
row.addElement(_cell(h, bold))
return row
def _congé_label(cg: set) -> str:
if "jour" in cg:
return "Journée"
parts = []
if "matin" in cg:
parts.append("Matin")
if "aprem" in cg:
parts.append("Après-midi")
return " + ".join(parts)
def _astreinte_label(jour: dict) -> str:
if jour.get("astreinte_fin"):
return f"fin {jour['astreinte_fin']}"
return "Astreinte" if jour.get("astreinte") else ""
def _jour_row(jour: dict, nom: str) -> TableRow:
row = TableRow()
for v in (
jour["date"], nom,
jour.get("matin_entree"), jour.get("matin_sortie"),
jour.get("aprem_entree"), jour.get("aprem_sortie"),
jour["travaille"], jour["du"],
("+" if jour["delta_min"] > 0 else "") + jour["delta"],
_congé_label(jour["conge"]),
_astreinte_label(jour),
jour.get("retour"),
):
row.addElement(_cell(v))
return row
def _totals_row(bold: Style, travaille_min: int, du_min: int) -> TableRow:
row = TableRow()
for v in ("Total", "", "", "", "", "",
minutes_to_hhmm(travaille_min), minutes_to_hhmm(du_min),
("+" if travaille_min - du_min > 0 else "") + minutes_to_hhmm(travaille_min - du_min),
"", "", ""):
row.addElement(_cell(v, bold))
return row
def _size_columns(table: Table, wide_col: Style, n: int = len(HEADERS)):
for _ in range(n):
table.addElement(TableColumn(stylename=wide_col))
def export_semaine_ods(year: int, week: int, user_id: str) -> bytes:
"""Build an .ods workbook with the daily detail for a single week."""
doc, bold, wide_col = _new_doc()
h_jour = heures_jour_min()
plages = load_plages(user_id)
conges = load_conges(user_id)
astreintes = load_astreintes(user_id)
dates = _week_dates(year, week)
raw = load_week_pointages(year, week, user_id)
p_by_date = {p["date"]: p for p in raw}
pointages = [
p_by_date.get(d, {"date": d, "matin_entree": None, "matin_sortie": None,
"aprem_entree": None, "aprem_sortie": None})
for d in dates
]
result = compute_week(pointages, conges, h_jour, plages, astreintes)
table = Table(name=f"Semaine {week:02d}-{year}")
_size_columns(table, wide_col)
table.addElement(_header_row(bold))
for jour, nom in zip(result["jours"], JOURS_FR):
table.addElement(_jour_row(jour, nom))
table.addElement(_totals_row(bold, sum(j["travaille_min"] for j in result["jours"]),
sum(j["du_min"] for j in result["jours"])))
doc.spreadsheet.addElement(table)
buf = io.BytesIO()
doc.save(buf)
return buf.getvalue()
def export_annee_ods(year: int, user_id: str) -> bytes:
"""Build an .ods workbook with one sheet per week of the year, plus a yearly total."""
doc, bold, wide_col = _new_doc()
h_jour = heures_jour_min()
plages = load_plages(user_id)
conges = load_conges(user_id)
astreintes = load_astreintes(user_id)
weeks = sorted(wk for yr, wk in list_all_weeks(user_id) if yr == year)
par_semaine = [] # (week, result, travaille_min, du_min)
for week in weeks:
dates = _week_dates(year, week)
raw = load_week_pointages(year, week, user_id)
p_by_date = {p["date"]: p for p in raw}
pointages = [
p_by_date.get(d, {"date": d, "matin_entree": None, "matin_sortie": None,
"aprem_entree": None, "aprem_sortie": None})
for d in dates
]
result = compute_week(pointages, conges, h_jour, plages, astreintes)
travaille_min = sum(j["travaille_min"] for j in result["jours"])
du_min = sum(j["du_min"] for j in result["jours"])
par_semaine.append((week, result, travaille_min, du_min))
year_travaille = sum(t for _, _, t, _ in par_semaine)
year_du = sum(d for _, _, _, d in par_semaine)
recap = Table(name="Récapitulatif")
_size_columns(recap, wide_col, n=4)
header = TableRow()
for h in ("Semaine", "Travaillé", "Dû", "Δ"):
header.addElement(_cell(h, bold))
recap.addElement(header)
for week, _, travaille_min, du_min in par_semaine:
row = TableRow()
for v in (f"S{week:02d}", minutes_to_hhmm(travaille_min), minutes_to_hhmm(du_min),
("+" if travaille_min - du_min > 0 else "") + minutes_to_hhmm(travaille_min - du_min)):
row.addElement(_cell(v))
recap.addElement(row)
total_row = TableRow()
for v in ("Total", minutes_to_hhmm(year_travaille), minutes_to_hhmm(year_du),
("+" if year_travaille - year_du > 0 else "") + minutes_to_hhmm(year_travaille - year_du)):
total_row.addElement(_cell(v, bold))
recap.addElement(total_row)
# Le récapitulatif en première feuille pour une vue d'ensemble immédiate à l'ouverture.
doc.spreadsheet.addElement(recap)
for week, result, travaille_min, du_min in par_semaine:
table = Table(name=f"S{week:02d}")
_size_columns(table, wide_col)
table.addElement(_header_row(bold))
for jour, nom in zip(result["jours"], JOURS_FR):
table.addElement(_jour_row(jour, nom))
table.addElement(_totals_row(bold, travaille_min, du_min))
doc.spreadsheet.addElement(table)
buf = io.BytesIO()
doc.save(buf)
return buf.getvalue()