You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
230 lines
7.2 KiB
230 lines
7.2 KiB
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())
|
|
|