diff --git a/app/export.py b/app/export.py new file mode 100644 index 0000000..9c8a311 --- /dev/null +++ b/app/export.py @@ -0,0 +1,178 @@ +"""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, 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é", +] + + +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 _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"]), + ): + 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) + + 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) + + 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) + + 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) + 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()