Compare commits

...

4 Commits

  1. 4
      pyproject.toml
  2. 2
      work_helpers/__init__.py
  3. 1
      work_helpers/_natural_sort.py
  4. 5
      work_helpers/excel2changelog.py
  5. 1
      work_helpers/excel2pylist.py
  6. 204
      work_helpers/fill_forms.py
  7. 4
      work_helpers/password.py
  8. 3
      work_helpers/random_int.py
  9. 6
      work_helpers/sensospot_rename.py
  10. 11
      work_helpers/sg_mbp_build.py
  11. 331
      work_helpers/travels.py

4
pyproject.toml

@ -24,8 +24,8 @@ requires = [
[tool.flit.scripts] [tool.flit.scripts]
form_inspect = "work_helpers.fill_forms:inspect" form_inspect = "work_helpers.fill_forms:inspect"
form_prepare_payments = "work_helpers.fill_forms:prepare_payments" travel_pre_payment = "work_helpers.travels:fill_prepayment_form"
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" nice_path = "work_helpers.nice_path:make_nice_path"
random_password = "work_helpers.password:get_random_password" random_password = "work_helpers.password:get_random_password"
random_ints = "work_helpers.random_int:generate_random_number_list" random_ints = "work_helpers.random_int:generate_random_number_list"

2
work_helpers/__init__.py

@ -1,3 +1,3 @@
"""Some helper scripts for the day to day work with Ubuntu in WSL2""" """Some helper scripts for the day to day work with Ubuntu in WSL2"""
__version__ = "0.0.2" __version__ = "0.1.0"

1
work_helpers/_natural_sort.py

@ -11,7 +11,6 @@ from http://www.codinghorror.com/blog/2007/12/sorting-for-humans-natural-sort-or
import re import re
_NATURAL_SORT_REGEX_DIGITS = re.compile("([0-9]+)") _NATURAL_SORT_REGEX_DIGITS = re.compile("([0-9]+)")

5
work_helpers/excel2changelog.py

@ -1,5 +1,6 @@
import click
import re import re
import click
import pyperclip import pyperclip
a2z = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" a2z = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
@ -64,7 +65,7 @@ def clipboard_to_changelog(
) )
line_count = len(result) line_count = len(result)
if line_count == 1: if line_count == 1:
print(f"Copied one line to the clipboard") print("Copied one line to the clipboard")
print(result[0]) print(result[0])
else: else:
print(f"Copied {line_count} lines to the clipboard") print(f"Copied {line_count} lines to the clipboard")

1
work_helpers/excel2pylist.py

@ -31,7 +31,6 @@ def pad_field(index, t):
value, length = t value, length = t
if index == 0: if index == 0:
return value.rjust(length) return value.rjust(length)
else:
return value.ljust(length) return value.ljust(length)

204
work_helpers/fill_forms.py

@ -1,81 +1,14 @@
import click
from datetime import datetime
import pathlib import pathlib
import sys
import shutil
import pandas as pd
import warnings import warnings
import re
from PyPDFForm import FormWrapper, PdfWrapper import click
from PyPDFForm import PdfWrapper
warnings.filterwarnings("ignore") warnings.filterwarnings("ignore")
Pathlike = pathlib.Path | str
WINHOME = pathlib.Path("/mnt/c/Users/Holgi/") WINHOME = pathlib.Path("/mnt/c/Users/Holgi/")
DESKTOP = WINHOME / "Desktop" 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.command()
@click.argument( @click.argument(
@ -95,136 +28,3 @@ def inspect(form, output):
preview_stream = PdfWrapper(str(form)).preview preview_stream = PdfWrapper(str(form)).preview
with output.open("wb+") as output_stream: with output.open("wb+") as output_stream:
output_stream.write(preview_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())

4
work_helpers/password.py

@ -1,8 +1,8 @@
import click
import itertools import itertools
import random import random
import pyperclip
import click
import pyperclip
lowers = "abcdefghijklmnopqrstuvwxyz" lowers = "abcdefghijklmnopqrstuvwxyz"
uppers = lowers.upper() uppers = lowers.upper()

3
work_helpers/random_int.py

@ -1,5 +1,6 @@
import click
import random import random
import click
import pyperclip import pyperclip

6
work_helpers/sensospot_rename.py

@ -1,9 +1,9 @@
"""rename sensospot images""" """rename sensospot images"""
import click
import pathlib import pathlib
import shutil import shutil
import click
RENAME_MAP_NAME = "rename_map.txt" RENAME_MAP_NAME = "rename_map.txt"
IMAGE_SUFFIXES = {".tif", ".jpg"} IMAGE_SUFFIXES = {".tif", ".jpg"}
@ -50,7 +50,7 @@ def _parse_rename_map(content: str) -> tuple[str, str]:
for i, line in enumerate(lines, start=1): for i, line in enumerate(lines, start=1):
if not line or line.startswith("#"): if not line or line.startswith("#"):
continue continue
if not "\t" in line: if "\t" not in line:
raise ValueError(f"No tab in line {i}: '{line}'") raise ValueError(f"No tab in line {i}: '{line}'")
yield line.split("\t", maxsplit=1) yield line.split("\t", maxsplit=1)
@ -81,7 +81,7 @@ def sensospot_rename(directory):
if not map_file.is_file(): if not map_file.is_file():
write_rename_map(map_file) write_rename_map(map_file)
click.echo(f"Prepared rename map at '{map_file!s}'") click.echo(f"Prepared rename map at '{map_file!s}'")
click.echo(f"Rerun the command after editing the file.") click.echo("Rerun the command after editing the file.")
else: else:
click.echo(f"Reading rename map at '{map_file!s}'") click.echo(f"Reading rename map at '{map_file!s}'")
try: try:

11
work_helpers/sg_mbp_build.py

@ -1,12 +1,12 @@
import click
import pathlib import pathlib
import pyperclip
import shutil import shutil
import sys import sys
from typing import Iterable
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import datetime
from typing import Iterable
import click
import pyperclip
Pathlike = str | pathlib.Path Pathlike = str | pathlib.Path
@ -285,7 +285,6 @@ def sg_mbp_build(dev_version):
new_folder_path = PATH_WIN_DESKTOP / new_folder_name new_folder_path = PATH_WIN_DESKTOP / new_folder_name
if new_folder_path.exists(): if new_folder_path.exists():
raise MBPExcecption(f"Folder exists on desktop: {new_folder_name}") raise MBPExcecption(f"Folder exists on desktop: {new_folder_name}")
else:
new_folder_path.mkdir() new_folder_path.mkdir()
copy_workbooks(group, new_folder_path, build_version) copy_workbooks(group, new_folder_path, build_version)
@ -364,7 +363,7 @@ def create_changelog_entry(cwd: Pathlike, new_version: str) -> None:
show_default="next minor version", show_default="next minor version",
) )
def sg_mbp_new_version(version): def sg_mbp_new_version(version):
""" r"""
creates a new version folder, new excel changes files and modifies the overall changelog creates a new version folder, new excel changes files and modifies the overall changelog
in "E:\Safeguard-MBP-issues" in "E:\Safeguard-MBP-issues"
""" """

331
work_helpers/travels.py

@ -0,0 +1,331 @@
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)
@property
def nice_number(self) -> str:
if len(self.travel_nr) > 6:
return " ".join(self.travel_nr[:5], self.travel_nr[5:])
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()
def fill_prepayment_form():
destination_path = DESKTOP / f"{TODAY} UK-Abschlag.pdf"
_fill_pre_payments_form(destination_path)
@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):
print("---")
info = _complete_travel_info(search_last_name, iso_date, place)
print(f"Name: {info.first_last}")
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}",
payments.nice_number
]
)
_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