aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--abrechnung.py321
-rw-r--r--out.pdfbin102877 -> 0 bytes
2 files changed, 0 insertions, 321 deletions
diff --git a/abrechnung.py b/abrechnung.py
deleted file mode 100644
index 985863c..0000000
--- a/abrechnung.py
+++ /dev/null
@@ -1,321 +0,0 @@
-import pandas as pd
-import hashlib
-import matplotlib.pyplot as plt
-from matplotlib.backends.backend_pdf import PdfPages
-from matplotlib import font_manager
-from datetime import datetime
-import os
-import re
-import argparse
-import tarfile
-import subprocess
-
-def parse_csv(filename):
- df = pd.read_csv(filename, sep=';', encoding='utf-8')
- df['Betrag'] = df['Betrag'].fillna('0')
- df['Betrag'] = df['Betrag'].apply(lambda x: str(x).replace('€', '').replace(',', '.').strip())
- df['Kilometer'] = df['Position'].str.contains('Autofahrt', case=False)
- def extract_km(row):
- if row['Kilometer']:
- match = re.search(r"((\d+,\d+\skm)|(\d+\.\d+\skm))", row['Betrag'])
- if match:
- return float(match.group(0).replace(',', '.').replace(" ", "").replace("km", ""))
- else:
- raise ValueError(f"Kilometerangabe unlesbar in Zeile: {row}")
- return 0.0
- df['km'] = df.apply(extract_km, axis=1)
- df['Betrag'] = df.apply(lambda row: 0.0 if row['Kilometer'] else float(row['Betrag']), axis=1)
- df['Datum'] = pd.to_datetime(df['Datum'], format="%Y-%m-%d", dayfirst=True)
- return df
-
-def compute_hash(filepath, base_dir="."):
- try:
- full_path = os.path.join(base_dir, filepath)
- with open(full_path, 'rb') as f:
- return hashlib.sha1(f.read()).hexdigest()
- except:
- return None
-
-def compute_settlements(df):
- personen = df['Name'].unique()
-
- # Tankdaten
- tankkosten_df = df[df['Position'].str.contains('Tanken', case=False)]
- km_df = df[df['Position'].str.contains('Autofahrt', case=False)]
- total_km = km_df['km'].sum()
- total_tank = tankkosten_df['Betrag'].sum()
- km_preis = total_tank / total_km if total_km else 0
-
- # Gruppiert nach Person
- km_pro_person = km_df.groupby('Name')['km'].sum().to_dict()
- tank_auslagen = tankkosten_df.groupby('Name')['Betrag'].sum().to_dict()
-
- # Sonstige Kosten
- sonstige_df = df[~df['Position'].str.contains('Tanken|Autofahrt', case=False)]
- total_sonstige = sonstige_df['Betrag'].sum()
- sonstige_auslagen = sonstige_df.groupby('Name')['Betrag'].sum().to_dict()
- sonstige_pro_person = total_sonstige / len(personen)
-
- # Gesamtanteil und Auslagen pro Person
- gesamt_anteil = {
- p: km_pro_person.get(p, 0) * km_preis + sonstige_pro_person
- for p in personen
- }
- gesamt_ausgelegt = {
- p: tank_auslagen.get(p, 0) + sonstige_auslagen.get(p, 0)
- for p in personen
- }
-
- # Saldo berechnen
- saldo = {p: gesamt_ausgelegt[p] - gesamt_anteil[p] for p in personen}
-
- return saldo, gesamt_ausgelegt, gesamt_anteil, km_df, km_preis
-
-def minimize_payments(saldo):
- empfaenger = []
- zahler = []
- for person, betrag in saldo.items():
- if round(betrag, 2) > 0:
- empfaenger.append([person, round(betrag, 2)])
- elif round(betrag, 2) < 0:
- zahler.append([person, -round(betrag, 2)])
- zahlungen = []
- i = j = 0
- while i < len(zahler) and j < len(empfaenger):
- payer, amount = zahler[i]
- receiver, need = empfaenger[j]
- zahlung = min(amount, need)
- zahlungen.append((payer, receiver, zahlung))
- zahler[i][1] -= zahlung
- empfaenger[j][1] -= zahlung
- if zahler[i][1] == 0:
- i += 1
- if empfaenger[j][1] == 0:
- j += 1
- return zahlungen
-
-def create_pdf(df, saldo, ausgelegt, anteil, zahlungen, km_info, km_preis, pdf_path, base_dir=".", anteil_pro_person=None):
- mono_font = font_manager.FontProperties(family='Inconsolata', size=8)
-
- with PdfPages(pdf_path) as pdf:
-
- columns = ['Datum', 'Position', 'Name', 'Betrag/km', 'Rechnung', 'SHA1']
- table_data = []
- for _, row in df.iterrows():
- hash_str = compute_hash(row['Rechnung'], base_dir) if pd.notna(row['Rechnung']) else ''
- table_data.append([
- row['Datum'].strftime('%Y-%m-%d'),
- row['Position'],
- row['Name'],
- f"{row['Betrag']:.2f} €" if not row['Kilometer'] else f"{row['km']} km",
- row['Rechnung'] if pd.notna(row['Rechnung']) else '',
- "{}\n{}".format(hash_str[:int(len(hash_str)/2)], hash_str[int(len(hash_str)/2):]) if hash_str else ""
- ])
-
- chunk_size = 25
- fontprops = font_manager.FontProperties(size=8)
-
- for start in range(0, len(table_data), chunk_size):
- fig, ax = plt.subplots(figsize=(8.27, 11.69))
- ax.axis('off')
- table_data_chunk = table_data[start:start+chunk_size]
-
- renderer = fig.canvas.get_renderer()
- def get_text_width(text, prop):
- text_obj = plt.text(0, 0, text, fontproperties=prop)
- bb = text_obj.get_window_extent(renderer=renderer)
- text_obj.remove()
- return bb.width
- col_widths = []
- for col_idx in range(len(columns)):
- max_width = get_text_width(columns[col_idx], fontprops)
- for row in table_data_chunk:
- max_width = max(max_width, get_text_width(str(row[col_idx]), fontprops))
- col_widths.append(max_width)
- col_widths_inches = [w / fig.dpi for w in col_widths]
- total_width = sum(col_widths_inches)
- scaled_widths = [w / total_width for w in col_widths_inches]
-
- table = ax.table(cellText=table_data_chunk, colLabels=columns, loc='center', cellLoc='left')
- table.auto_set_font_size(False)
- for cell in table.get_celld().values():
- cell.get_text().set_fontproperties(mono_font)
- for (row, col), cell in table.get_celld().items():
- if col < len(scaled_widths):
- cell.set_width(scaled_widths[col])
- cell.PAD = 0.05
- table.scale(1, 2)
- pdf.savefig()
- plt.close()
-
- fig, axs = plt.subplots(3, 2, figsize=(8.27, 11.69))
- fig.suptitle("Auswertung", fontsize=14, fontproperties=mono_font)
-
- df_plot = pd.DataFrame({'Ausgelegt': ausgelegt, 'Anteil': anteil})
- df_plot.plot.bar(ax=axs[0, 0])
- axs[0, 0].set_title('Ausgelegt vs. Anteil', fontproperties=mono_font)
- axs[0, 0].tick_params(axis='x', rotation=0)
- axs[0, 0].legend(prop=mono_font)
-
- for name, group in km_info.groupby('Name'):
- group_sorted = group.sort_values('Datum').copy()
- group_sorted['km_cumsum'] = group_sorted['km'].cumsum()
-
- monatserster = pd.Timestamp(group_sorted['Datum'].min().replace(day=1))
- monatsletzter = pd.Timestamp(group_sorted['Datum'].max().replace(day=1)) + pd.offsets.MonthEnd(0)
-
- null_row = pd.DataFrame({
- 'Datum': [monatserster],
- 'km_cumsum': [0]
- })
-
- end_row = pd.DataFrame({
- 'Datum': [monatsletzter],
- 'km_cumsum': [group_sorted['km_cumsum'].iloc[-1]]
- })
-
- plot_df = pd.concat([null_row, group_sorted[['Datum', 'km_cumsum']], end_row])
- plot_df = plot_df.sort_values('Datum')
-
- dates = plot_df['Datum']
- values = plot_df['km_cumsum']
-
- axs[0, 1].plot(dates, values, label=name, linewidth=1)
- axs[0, 1].fill_between(dates, values, alpha=0.15)
-
- axs[0, 1].set_title('Kumulativer km-Verlauf im Monat', fontproperties=mono_font)
- axs[0, 1].set_ylabel('km', fontproperties=mono_font)
- axs[0, 1].set_xlabel('Tag des Monats', fontproperties=mono_font)
-
- alle_daten = km_info['Datum'].sort_values().unique()
- axs[0, 1].set_xticks(alle_daten)
- axs[0, 1].set_xticklabels([d.day for d in alle_daten], rotation=0)
-
- axs[0, 1].legend(prop=mono_font)
-
- axs[1, 0].set_title("Kumulative sonstige Ausgaben", fontproperties=mono_font)
- sonstige = df[~df['Position'].str.contains('Tanken|Autofahrt', case=False)]
- for name, group in sonstige.groupby('Name'):
- group_sorted = group.sort_values('Datum').copy()
- group_sorted['betrag_cumsum'] = group_sorted['Betrag'].cumsum()
-
- monatserster = pd.Timestamp(group_sorted['Datum'].min().replace(day=1))
- monatsletzter = pd.Timestamp(group_sorted['Datum'].max().replace(day=1)) + pd.offsets.MonthEnd(0)
-
- null_row = pd.DataFrame({
- 'Datum': [monatserster],
- 'betrag_cumsum': [0]
- })
-
- end_row = pd.DataFrame({
- 'Datum': [monatsletzter],
- 'betrag_cumsum': [group_sorted['betrag_cumsum'].iloc[-1]]
- })
-
- plot_df = pd.concat([null_row, group_sorted[['Datum', 'betrag_cumsum']], end_row])
- plot_df = plot_df.sort_values('Datum')
-
- dates = plot_df['Datum']
- values = plot_df['betrag_cumsum']
-
- axs[1, 0].plot(dates, values, label=name, linewidth=1)
- axs[1, 0].fill_between(dates, values, alpha=0.15)
-
- axs[1, 0].set_ylabel("€", fontproperties=mono_font)
- axs[1, 0].set_xlabel("Tag des Monats", fontproperties=mono_font)
- alle_daten = pd.date_range(start=monatserster, end=monatsletzter)
- axs[1, 0].set_xticks(alle_daten)
- axs[1, 0].set_xticklabels([d.day for d in alle_daten], rotation=0, fontproperties=mono_font)
- axs[1, 0].legend(prop=mono_font)
-
- axs[2, 0].axis('off')
-
- # Grundlagen berechnen
- personen = list(ausgelegt.keys())
- gesamt_km = km_info['km'].sum()
- gesamt_tank = df[df['Position'].str.contains("Tanken", case=False)]['Betrag'].sum()
- gesamt_sonstige = df[~df['Position'].str.contains("Tanken|Autofahrt", case=False)]['Betrag'].sum()
- sonstige_pro_person = gesamt_sonstige / len(personen)
- km_preis = gesamt_tank / gesamt_km if gesamt_km else 0
-
- # Gruppierungen
- km_pro_person = km_info.groupby('Name')['km'].sum().to_dict()
- tank_auslagen = df[df['Position'].str.contains("Tanken", case=False)].groupby('Name')['Betrag'].sum().to_dict()
- sonstige_auslagen = df[~df['Position'].str.contains("Tanken|Autofahrt", case=False)].groupby('Name')['Betrag'].sum().to_dict()
-
- # KFZ-Ausgabe
- kfztxt = ["KFZ:"]
- for p in personen:
- km = km_pro_person.get(p, 0)
- anteil_km = (km / gesamt_km * 100) if gesamt_km > 0 else 0
- ausgelegt_tank = tank_auslagen.get(p, 0)
- anteil_tank = km * km_preis
- kfztxt.append(
- f"{p}: {km:.2f} km; {anteil_km:.1f} %; {ausgelegt_tank:.2f} €; {anteil_tank:.2f} €"
- )
- kfztxt.append(f"Gesamt: {gesamt_km:.2f} km; {gesamt_tank:.2f} €; {km_preis:.4f} € / km")
-
- # Sonstige-Ausgabe
- sonstigetxt = ["\nSonstige Auslagen:"]
- for p in personen:
- ausgelegt_sonstige = sonstige_auslagen.get(p, 0)
- sonstigetxt.append(
- f"{p}: {ausgelegt_sonstige:.2f} €; {sonstige_pro_person:.2f} €"
- )
- sonstigetxt.append(
- f"Gesamt: {gesamt_sonstige:.2f} €; {sonstige_pro_person:.2f} € p.P."
- )
-
- # Zielbeträge
- gesamttxt = ["\nBeträge ohne Ausgleich:"]
- for p in personen:
- km = km_pro_person.get(p, 0)
- anteil_km = (km / gesamt_km * 100) if gesamt_km > 0 else 0
- ausgelegt_tank = tank_auslagen.get(p, 0)
- anteil_tank = km * km_preis
- zielbetrag = ausgelegt_tank - anteil_tank + sonstige_auslagen.get(p, 0)
- gesamttxt.append(f"{p}: {zielbetrag:.2f} €")
-
- # Zusammensetzen und anzeigen
- rechenweg = "\n".join(kfztxt + sonstigetxt + gesamttxt)
- axs[2, 0].text(0, 1, rechenweg, verticalalignment='top', fontproperties=mono_font)
-
- axs[2, 1].axis('off')
- zahlungstext = "\n".join([f"{payer} → {receiver}: {betrag:.2f} €" for payer, receiver, betrag in zahlungen])
- axs[2, 1].text(0, 1, "Ausgleich:\n" + zahlungstext, verticalalignment='top', fontproperties=mono_font)
-
- for ax in axs.flat:
- for label in ax.get_xticklabels() + ax.get_yticklabels():
- label.set_fontproperties(mono_font)
-
- pdf.savefig()
- plt.close()
-
-def main():
- parser = argparse.ArgumentParser()
- parser.add_argument("monatspfad", help="Pfad zum Ordner im Format YYYY-MM")
- parser.add_argument("zielpfad", help="Zielverzeichnis für .tar.zst Archiv")
- args = parser.parse_args()
-
- monat = os.path.basename(args.monatspfad)
- csv_path = os.path.join(args.monatspfad, f"{monat}.csv")
- pdf_path = os.path.join(args.monatspfad, f"{monat}.pdf")
-
- df = parse_csv(csv_path)
- if df['Datum'].isna().any():
- fehlerhafte_zeilen = df[df['Datum'].isna()]
- raise ValueError(f"Ungültige Datumsangaben in folgenden Zeilen:\n{fehlerhafte_zeilen}")
-
- saldo, ausgelegt, anteil, km_info, km_preis = compute_settlements(df)
- zahlungen = minimize_payments(saldo)
- create_pdf(df, saldo, ausgelegt, anteil, zahlungen, km_info, km_preis, pdf_path, base_dir=args.monatspfad)
-
- # Archivieren
- archivpfad = os.path.join(args.zielpfad, f"{monat}.tar")
- with tarfile.open(archivpfad, "w") as tar:
- tar.add(args.monatspfad, arcname=monat)
-
- subprocess.run(["zstd", "--rm", archivpfad], check=True)
-
-if __name__ == '__main__':
- main()
diff --git a/out.pdf b/out.pdf
deleted file mode 100644
index bb63505..0000000
--- a/out.pdf
+++ /dev/null
Binary files differ