Browse Source

moved business travel functions to own module

master
Holger Frey 4 days ago
parent
commit
a818ef0a7c
  1. 3
      pyproject.toml
  2. 204
      work_helpers/fill_forms.py
  3. 317
      work_helpers/travels.py

3
pyproject.toml

@ -24,8 +24,7 @@ requires = [ @@ -24,8 +24,7 @@ requires = [
[tool.flit.scripts]
form_inspect = "work_helpers.fill_forms:inspect"
form_prepare_payments = "work_helpers.fill_forms:prepare_payments"
form_fill_payments = "work_helpers.fill_forms:payments"
travel_final_payment = "work_helpers.travels:final_payment"
nice_path = "work_helpers.nice_path:make_nice_path"
random_password = "work_helpers.password:get_random_password"
random_ints = "work_helpers.random_int:generate_random_number_list"

204
work_helpers/fill_forms.py

@ -1,81 +1,14 @@ @@ -1,81 +1,14 @@
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
import click
from PyPDFForm import 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(
@ -95,136 +28,3 @@ def inspect(form, output): @@ -95,136 +28,3 @@ def inspect(form, output):
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())

317
work_helpers/travels.py

@ -0,0 +1,317 @@ @@ -0,0 +1,317 @@
import pathlib
import re
import sys
import warnings
from dataclasses import dataclass
from datetime import datetime
from typing import Iterable
import click
import pandas as pd
from PyPDFForm import FormWrapper
warnings.filterwarnings("ignore")
Pathlike = pathlib.Path | str
WINHOME = pathlib.Path("/mnt/c/Users/Holgi/")
DESKTOP = WINHOME / "Desktop"
TODAY = datetime.now().strftime("%Y-%m-%d")
@dataclass
class PrePaymentEntry:
date: str
amount: float
superx_id: str
@classmethod
def from_series(cls, series: pd.Series) -> "PrePamentEntry":
return cls(
date=series["BuDat"],
amount=series["Betrag"],
superx_id=series["BelegNr"],
)
@property
def amount_localized(self, *, decimal=",", thousands=".") -> str:
non_local = f"{self.amount:_.2f}"
return non_local.replace(".", decimal).replace("_", thousands)
@dataclass
class PrePayments:
travel_nr: str
cost_center: str
fonds: str
project: str
payments: list[PrePaymentEntry]
def __iter__(self) -> Iterable[PrePaymentEntry]:
return iter(self.payments)
def to_form_data(self) -> dict[str, str]:
form_data = {
"Projekt": self.project,
"Kostenstelle": self.cost_center,
"Mittelbindung": self.travel_nr,
"Fonds": self.fonds,
}
for index, entry in enumerate(self, start=1):
form_data.update(
{
f"Datum{index}": entry.date,
f"BelegNr aus SuberX{index}": entry.superx_id,
# no field "Euro{index}"
# the automatic form calculation would not work.
}
)
return form_data
@dataclass
class TravelInfo:
first_name: str
last_name: str
iso_date: str
place: str
uka_form: pathlib.Path
@property
def first_last(self) -> str:
return f"{self.first_name} {self.last_name}"
@property
def last_first(self) -> str:
return f"{self.last_name}, {self.first_name}"
@property
def travel_short(self) -> str:
return f"{self.last_first}, {self.place}"
@property
def travel_name(self) -> str:
return f"{self.iso_date} {self.travel_short}"
@property
def ger_date(self) -> str:
return _iso_date_to_german(self.iso_date)
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 OSError(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(r"(5\d{9,10})", x.replace(" ", ""))
)
match_result = travel_nr[travel_nr.first_valid_index()]
return match_result[0]
def _read_pre_payments(file_path: Pathlike = None) -> PrePayments:
if not file_path:
downloads = WINHOME / "Downloads"
file_path = _get_latest_file(downloads, "Buchungen_SAP", ".xlsx")
if not file_path:
sys.exit("Could not find an SuperX export file, aborting.")
fields = ["BelegNr", "VorgängerBelegNr", "Kostenstelle", "Fonds", "Projekt"]
converters = {field: str for field in fields}
raw_data = pd.read_excel(file_path, skiprows=3, converters=converters)
travel_nr = _extract_travel_number(raw_data, "BelegNr")
try:
cost_center = _get_unique(raw_data, "Kostenstelle")
fonds = _get_unique(raw_data, "Fonds")
project = _get_unique(raw_data, "Projekt")
if not project or len(project) <= 5:
project = ""
except ValueError as e:
sys.exit(str(e))
mask = raw_data["Werttyp"] == "Zahlung"
raw_payments = raw_data[mask].copy()
summary = (
raw_payments.groupby("BelegNr")
.agg({"Betrag": "sum", "BuDat": "first"})
.reset_index()
.sort_values("BelegNr")
)
paymments = [
PrePaymentEntry.from_series(row) for i, row in summary.iterrows()
]
return PrePayments(
travel_nr=travel_nr,
cost_center=cost_center,
fonds=fonds,
project=project,
payments=paymments,
)
def _complete_travel_info(
search_last_name: str, iso_date: str, place: str
) -> TravelInfo:
forms_path = DESKTOP / "Formulare"
templates_path = forms_path / "vorbereitete 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}'"
)
uka_form = templates[0]
rest, first_name = uka_form.stem.rsplit(",", maxsplit=1)
*_, last_name = rest.split()
first_name = first_name.strip()
last_name = last_name.strip()
return TravelInfo(
first_name=first_name,
last_name=last_name,
iso_date=iso_date,
place=place,
uka_form=uka_form,
)
def _create_text_stub(path: Pathlike, *, content: str = None) -> None:
path = pathlib.Path(path)
content = content or path.stem
path.write_text(content)
def fill_pre_payments_form(destination_path: Pathlike) -> PrePayments:
pre_payments = _read_pre_payments()
print(f"Projekt: {pre_payments.project}")
print(f"Kostenstelle: {pre_payments.cost_center}")
print(f"Mittelbindung: {pre_payments.travel_nr}")
print(f"Fonds: {pre_payments.fonds}")
print()
print(" Datum Betrag SuperX")
print()
for row in pre_payments.payments:
print(f" {row.date} {row.amount_localized:>7} {row.superx_id}")
forms = DESKTOP / "Formulare"
source_path = forms / "Vorlage UK-Abschlag, v2023-01.pdf"
form = FormWrapper(str(source_path))
filled = form.fill(pre_payments.to_form_data(), flatten=False)
pathlib.Path(destination_path).write_bytes(filled.read())
return pre_payments
@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 final_payment(search_last_name: str, iso_date: str, place: str):
info = _complete_travel_info(search_last_name, iso_date, place)
folder = DESKTOP / f"{info.travel_name} (abgerechnet)"
folder.mkdir()
destination_path = folder / f"{TODAY} {info.travel_short}, UK-Abschlag.pdf"
payments = fill_pre_payments_form(destination_path)
rk_path = folder / f"DATE {info.travel_short}, Reisekostenabrechnung.txt"
_create_text_stub(rk_path)
uka_hint_path = (
folder
/ f"HUEL UK-A {info.last_first}, Dienstreise {info.place}, Schlusszahlung.txt"
)
content = "\t".join(
[
info.ger_date.replace(".", "/"),
info.first_last,
f"Schlusszahlung Dienstreise {info.place}, {info.ger_date}",
]
)
_create_text_stub(uka_hint_path, content=content)
sz_path = (
folder
/ f"DATE {info.travel_short}, Schlusszahlung an Unikasse, KONTO, HUELNR.txt"
)
_create_text_stub(sz_path, content="Schlusszahlung an Unikasse geschickt.")
form = FormWrapper(str(info.uka_form))
form_data = {
"Verwendungszweck": f"Schlusszahlung Dienstreise nach {info.place}",
"Begründung": f"Schlusszahlung Dienstreise {info.first_last} nach {info.place} am {info.ger_date}",
"Datum_Feststellung": _iso_date_to_german(TODAY),
"Datum_Anordnung": _iso_date_to_german(TODAY),
"KostenstelleKontierung": payments.cost_center,
"FondsKontierung": payments.fonds,
"ProjektKontierung": payments.project,
"Bezug zur Mittelbindung": payments.travel_nr,
"Schlusszahlung": True,
"UK-Abschlag": True,
}
filled = form.fill(form_data, flatten=False)
uka_path = folder / f"{TODAY} {info.travel_short}, UK-A Schlusszahlung.pdf"
uka_path.write_bytes(filled.read())
Loading…
Cancel
Save