Un jour d'astreinte déclaré couvre désormais 18:00 → 18:00 le lendemain :
les interventions hors fenêtre ne comptent ni au décompte ni pour les
11h de repos, et deux astreintes consécutives se recollent sans trou.
La fin de la dernière intervention est arrondie au quart d'heure entamé
avant de poser le retour. Samedi et dimanche apparaissent dans la
semaine (heures dues nulles) pour pouvoir y déclarer des astreintes,
dans les exports aussi. Un réglage par utilisateur ajoute une durée
forfaitaire au temps travaillé de chaque jour d'astreinte déclaré.
💘 Generated with Crush
Assisted-by: Crush:glm-5.2
198 lines
7.5 KiB
Python
198 lines
7.5 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", "Samedi", "Dimanche"]
|
|
HEADERS = [
|
|
"Date", "Jour", "Matin entrée", "Matin sortie", "Aprem entrée", "Aprem sortie",
|
|
"Travaillé", "Dû", "Δ jour", "Congé", "Astreinte", "Astreinte pondérée", "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(7)]
|
|
|
|
|
|
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_min"):
|
|
return jour["astreinte_h"]
|
|
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),
|
|
minutes_to_hhmm(jour["astreinte_pondere_min"]) if jour["astreinte_pondere_min"] > 0 else "",
|
|
jour.get("retour"),
|
|
):
|
|
row.addElement(_cell(v))
|
|
return row
|
|
|
|
|
|
def _totals_row(bold: Style, travaille_min: int, du_min: int,
|
|
as_min: int = 0, as_pondere_min: int = 0) -> 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),
|
|
"", minutes_to_hhmm(as_min), minutes_to_hhmm(as_pondere_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"]),
|
|
sum(j["astreinte_min"] for j in result["jours"]),
|
|
sum(j["astreinte_pondere_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,
|
|
sum(j["astreinte_min"] for j in result["jours"]),
|
|
sum(j["astreinte_pondere_min"] for j in result["jours"])))
|
|
doc.spreadsheet.addElement(table)
|
|
|
|
buf = io.BytesIO()
|
|
doc.save(buf)
|
|
return buf.getvalue()
|