diff --git a/work_helpers/Vorlage UK-Abschlag, v2023-01.pdf b/work_helpers/Vorlage UK-Abschlag, v2023-01.pdf new file mode 100644 index 0000000..bf5f641 Binary files /dev/null and b/work_helpers/Vorlage UK-Abschlag, v2023-01.pdf differ diff --git a/work_helpers/fill_forms.py b/work_helpers/fill_forms.py new file mode 100644 index 0000000..8c75d3a --- /dev/null +++ b/work_helpers/fill_forms.py @@ -0,0 +1,230 @@ +import click +from datetime import datetime +import pathlib +import sys +import shutil +import pandas as pd +import warnings +import re + +from PyPDFForm import FormWrapper, PdfWrapper + +warnings.filterwarnings("ignore") + +Pathlike = pathlib.Path | str + +WINHOME = pathlib.Path("/mnt/c/Users/Holgi/") +DESKTOP = WINHOME / "Desktop" + +TODAY = datetime.now().strftime("%Y-%m-%d") + + +def _iso_date_to_german(date: str) -> str: + return ".".join(reversed(date.split("-"))) + + +def _search_files( + folder: Pathlike, partial_name: str, suffix: str +) -> list[pathlib.Path]: + parent = pathlib.Path(folder) + if not suffix.startswith("."): + suffix = f".{suffix}" + files = (item for item in parent.iterdir() if item.is_file()) + non_hidden = (item for item in files if not item.stem.startswith(".")) + non_tempfile = (item for item in files if not item.stem.startswith("~")) + types = ( + item for item in non_tempfile if item.suffix.lower() == suffix.lower() + ) + return [item for item in types if partial_name.lower() in item.stem.lower()] + + +def _get_latest_file( + folder: Pathlike, partial_name: str, suffix: str +) -> pathlib.Path | None: + results = _search_files(folder, partial_name, suffix) + if not results: + return None + creation_times = [item.stat().st_ctime for item in results] + by_creation_time = sorted(zip(creation_times, results)) + newest_with_time = by_creation_time[-1] # latest entry + return newest_with_time[1] # the path entry of the tuple + + +def _get_form_path(partial_name: str) -> pathlib.Path: + own_parent = pathlib.Path(__file__).parent + matches = _search_files(own_parent, partial_name, ".pdf") + if len(matches) == 1: + return matches[0] + counts = len(matches) + msg = f"Found {counts} matching pdf forms for '{partial_name}'" + raise IOError(msg) + + +def _get_unique(data: pd.DataFrame, column: str) -> str | int | float: + uniques = data[column].unique() + if len(uniques) != 1: + msg = f"Found multiple unique values for '{column}'" + raise ValueError(msg) + return uniques[0] + + +def _extract_travel_number(data: pd.DataFrame, column: str) -> str: + belege = data[column] + travel_nr = belege.apply( + lambda x: re.search("(5\d{7,8})", x.replace(" ", "")) + ) + match_result = travel_nr[travel_nr.first_valid_index()] + return match_result[0] + + +@click.command() +@click.argument( + "form", type=click.Path(exists=True, file_okay=True, dir_okay=False) +) +@click.option( + "-o", + "--output", + default=None, + help="Output file path, defaults to desktop folder", +) +def inspect(form, output): + form = pathlib.Path(form) + if not output: + new_name = f"inspected {form.stem}{form.suffix}" + output = DESKTOP / new_name + preview_stream = PdfWrapper(str(form)).preview + with output.open("wb+") as output_stream: + output_stream.write(preview_stream) + + +def payments(): + downloads = WINHOME / "Downloads" + forms = DESKTOP / "Formulare" + + latest_export = _get_latest_file(downloads, "Buchungen_SAP", ".xlsx") + if not latest_export: + sys.exit("Could not find an SuperX export file, aborting.") + + fields = ["BelegNr", "VorgängerBelegNr", "Kostenstelle", "Fonds", "Projekt"] + converters = {field: str for field in fields} + data = pd.read_excel(latest_export, skiprows=3, converters=converters) + + travel_nr = _extract_travel_number(data, "BelegNr") + + mask = data["Werttyp"] == "Zahlung" + payments = data[mask].copy() + + summary = ( + payments.groupby("BelegNr") + .agg({"Betrag": "sum", "BuDat": "first"}) + .reset_index() + .sort_values("BelegNr") + ) + + try: + cost_center = _get_unique(payments, "Kostenstelle") + fonds = _get_unique(payments, "Fonds") + project = _get_unique(payments, "Projekt") + if not project or len(project) <= 4: + project = "" + except ValueError as e: + sys.exit(str(e)) + + form_data = { + "Projekt": project, + "Kostenstelle": cost_center, + "Mittelbindung": travel_nr, + "Fonds": fonds, + } + print(f"Projekt: {project}") + print(f"Kostenstelle: {cost_center}") + print(f"Mittelbindung: {travel_nr}") + print(f"Fonds: {fonds}") + + print("") + print(f" Datum Betrag SuperX") + print("") + + for i, row in summary.iterrows(): + index = i + 1 + form_data.update( + { + f"Datum{index}": row["BuDat"], + f"BelegNr aus SuberX{index}": row["BelegNr"], + # no field "Euro{index}" + # the automatic form calculation would not work. + } + ) + betrag = f"{row['Betrag']:0.2f}".replace(".", ",") + print(f" {row['BuDat']} {betrag:>7} {row['BelegNr']}") + + source_path = forms / "Vorlage UK-Abschlag, v2023-01.pdf" + destination_path = DESKTOP / f"{TODAY} UK-Abschlag.pdf" + form = FormWrapper(str(source_path)) + filled = form.fill(form_data, flatten=False) + destination_path.write_bytes(filled.read()) + + +@click.command() +@click.option("-l", "--search_last_name", prompt=True, required=True) +@click.option("-d", "--iso_date", prompt=True, required=True) +@click.option("-p", "--place", prompt=True, required=True) +def prepare_payments(search_last_name: str, iso_date: str, place: str): + forms_path = DESKTOP / "Formulare" + templates_path = forms_path / "vorbereitet UK-As" + + templates = _search_files(templates_path, f" {search_last_name}", ".pdf") + if len(templates) == 0: + sys.exit( + f"Could not find a UK-A template for search '{search_last_name}'" + ) + if len(templates) > 1: + sys.exit( + f"Found multiple UK-A templates for search '{search_last_name}'" + ) + template = templates[0] + + rest, first_name = template.stem.rsplit(",", maxsplit=1) + *_, last_name = rest.split() + first_name = first_name.strip() + last_name = last_name.strip() + + first_last = f"{first_name} {last_name}" + last_first = f"{last_name}, {first_name}" + travel_short = f"{last_first}, {place}" + travel_name = f"{iso_date} {travel_short}" + date = _iso_date_to_german(iso_date) + + folder = DESKTOP / f"{travel_name} (abgerechnet)" + folder.mkdir() + + rk_path = folder / f" {travel_short}, Reisekostenabrechnung.txt" + rk_path.write_text(rk_path.stem) + + uka_hint_path = ( + folder / f" UK-A {last_first}, Dienstreise {place}, Schlusszahlung.txt" + ) + content = "\t".join( + [ + _iso_date_to_german(TODAY), + first_last, + f"Schlusszahlung Dienstreise {place}, {date}", + ] + ) + uka_hint_path.write_text(content) + + source_path = forms_path / "Vorlage UK-Abschlag, v2023-01.pdf" + destination_path = folder / f"{TODAY} {travel_short}, UK-Abschlag.pdf" + shutil.copy(source_path, destination_path) + + form = FormWrapper(str(template)) + form_data = { + "Verwendungszweck": f"Schlusszahlung Dienstreise nach {place}", + "Begründung": f"Schlusszahlung Dienstreise {first_last} nach {place} am {date}", + "Datum_Feststellung": _iso_date_to_german(TODAY), + "Datum_Anordnung": _iso_date_to_german(TODAY), + } + filled = form.fill(form_data, flatten=False) + + uka_path = folder / f"{TODAY} {travel_short}, UK-A Schlusszahlung.pdf" + uka_path.write_bytes(filled.read())