commit 1de2c23995d554c053063500fa0c6c040817df94 Author: Klaas Börgmann Date: Tue Feb 25 17:12:12 2025 +0100 first commit diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..c0937b8 Binary files /dev/null and b/.DS_Store differ diff --git a/.env_example b/.env_example new file mode 100644 index 0000000..49ed74b --- /dev/null +++ b/.env_example @@ -0,0 +1,6 @@ +DB_HOST=localhost +DB_PORT=5432 +DB_NAME=postgresql +DB_USER=postgresql +DB_PASSWORD=supersafepassword + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..e69de29 diff --git a/README.MD b/README.MD new file mode 100644 index 0000000..e69de29 diff --git a/api.py b/api.py new file mode 100644 index 0000000..96d99a6 --- /dev/null +++ b/api.py @@ -0,0 +1,170 @@ +import jwt as pyjwt +import datetime +import bcrypt +import psycopg2 +import psycopg2.extras +from flask import Flask, request, jsonify +import os +from dotenv import load_dotenv + +# Flask App erstellen +app = Flask(__name__) +app.config['SECRET_KEY'] = 'supergeheimeschluessel' + +# .env-Datei laden +load_dotenv() + +# Zugriff auf die Umgebungsvariablen +DB_HOST = os.getenv("DB_HOST") +DB_PORT = os.getenv("DB_PORT") +DB_NAME = os.getenv("DB_NAME") +DB_USER = os.getenv("DB_USER") +DB_PASSWORD = os.getenv("DB_PASSWORD") + +# Verbindung zur PostgreSQL-Datenbank herstellen +def connect_db(): + return psycopg2.connect( + dbname=DB_NAME, + user=DB_USER, + password=DB_PASSWORD, + host=DB_HOST, + port=DB_PORT + ) + +def create_tables(): + conn = connect_db() + cur = conn.cursor() + cur.execute(''' + CREATE TABLE IF NOT EXISTS users ( + id SERIAL PRIMARY KEY, + username TEXT UNIQUE NOT NULL, + password TEXT NOT NULL, + is_active BOOLEAN DEFAULT TRUE, + email TEXT UNIQUE NOT NULL, + reset_password_token TEXT, + reset_password_expires TIMESTAMP + ); + ''') + conn.commit() + cur.close() + conn.close() + +def generate_jwt(user_id): + payload = { + 'user_id': user_id, + 'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1) + } + return pyjwt.encode(payload, app.config['SECRET_KEY'], algorithm='HS256') + +def verify_jwt(token): + try: + payload = pyjwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256']) + return payload['user_id'] + except pyjwt.ExpiredSignatureError: + return None + except pyjwt.InvalidTokenError: + return None + +def hash_password(password): + return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8') + +def check_password(password, hashed): + return bcrypt.checkpw(password.encode('utf-8'), hashed.encode('utf-8')) + +@app.route('/register', methods=['POST']) +def register(): + data = request.json + username = data.get('username') + email = data.get('email') + password = data.get('password') + + if not username or not email or not password: + return jsonify({'error': 'Alle Felder erforderlich'}), 400 + + hashed_password = hash_password(password) + + conn = connect_db() + cur = conn.cursor() + try: + cur.execute("INSERT INTO users (username, email, password) VALUES (%s, %s, %s) RETURNING id", (username, email, hashed_password)) + user_id = cur.fetchone()[0] + conn.commit() + except psycopg2.IntegrityError: + return jsonify({'error': 'Benutzername oder E-Mail bereits vergeben'}), 400 + finally: + cur.close() + conn.close() + + return jsonify({'message': 'Benutzer registriert', 'user_id': user_id}) + +@app.route('/login', methods=['POST']) +def login(): + data = request.json + username = data.get('username') + password = data.get('password') + + conn = connect_db() + cur = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) + cur.execute("SELECT * FROM users WHERE username = %s", (username,)) + user = cur.fetchone() + cur.close() + conn.close() + + if not user or not check_password(password, user['password']): + return jsonify({'error': 'Ungültige Anmeldeinformationen'}), 401 + + token = generate_jwt(user['id']) + return jsonify({'token': token}) + +@app.route('/kasse', methods=['GET']) +def get_kasse(): + token = request.headers.get('Authorization') + if not token: + return jsonify({'error': 'Kein Token vorhanden'}), 401 + + user_id = verify_jwt(token.split()[1]) + if not user_id: + return jsonify({'error': 'Ungültiges Token'}), 401 + + conn = connect_db() + cur = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) + cur.execute("SELECT * FROM kasse ORDER BY timestamp DESC") + rows = cur.fetchall() + cur.close() + conn.close() + + return jsonify([dict(row) for row in rows]) + +@app.route('/new_entry', methods=['POST']) +def new_entry(): + token = request.headers.get('Authorization') + if not token: + return jsonify({'error': 'Kein Token vorhanden'}), 401 + + user_id = verify_jwt(token.split()[1]) + if not user_id: + return jsonify({'error': 'Ungültiges Token'}), 401 + + data = request.json + beschreibung = data.get('beschreibung') + wert = data.get('wert') + typ = data.get('typ') + bar = data.get('bar') + + if not beschreibung or wert is None or not typ or bar is None: + return jsonify({'error': 'Alle Felder erforderlich'}), 400 + + conn = connect_db() + cur = conn.cursor() + cur.execute("INSERT INTO kasse (beschreibung, wert, typ, bar) VALUES (%s, %s, %s, %s) RETURNING id", + (beschreibung, wert, typ, bar)) + entry_id = cur.fetchone()[0] + conn.commit() + cur.close() + conn.close() + + return jsonify({'message': 'Eintrag erstellt', 'entry_id': entry_id}) + +if __name__ == '__main__': + create_tables() + app.run(debug=True) diff --git a/main.py b/main.py new file mode 100644 index 0000000..51668e0 --- /dev/null +++ b/main.py @@ -0,0 +1,227 @@ +import psycopg2 +import psycopg2.extras +import datetime +import os +from dotenv import load_dotenv + +# ANSI Escape Codes für Farben und Unterstreichung +RESET = "\033[0m" # Zurücksetzen auf Standard + +RED = "\033[31m" # Rote Schrift +GREEN = "\033[32m" # Grüne Schrift +BLUE = "\033[34m" # Blaue Schrift + +BOLD = "\033[1m" # Fett +ITALIC = "\033[3m" # Kursiv +UNDERLINE = "\033[4m" # Unterstreichen + + +# .env-Datei laden +load_dotenv() + +# Zugriff auf die Umgebungsvariablen +DB_HOST = os.getenv("DB_HOST") +DB_PORT = os.getenv("DB_PORT") +DB_NAME = os.getenv("DB_NAME") +DB_USER = os.getenv("DB_USER") +DB_PASSWORD = os.getenv("DB_PASSWORD") + +# Verbindung zur PostgreSQL-Datenbank herstellen +def connect_db(): + return psycopg2.connect( + dbname=DB_NAME, + user=DB_USER, + password=DB_PASSWORD, + host=DB_HOST, + port=DB_PORT + ) + +def create_table(): + conn = connect_db() + cur = conn.cursor() + cur.execute(''' + CREATE TABLE IF NOT EXISTS kasse ( + id SERIAL PRIMARY KEY, + beschreibung TEXT NOT NULL, + wert NUMERIC(10,2) NOT NULL, + typ TEXT CHECK (typ IN ('Einnahme', 'Ausgabe')) NOT NULL, + bar BOOLEAN NOT NULL, + timestamp TIMESTAMP NOT NULL DEFAULT NOW(), + rechnungsnummer TEXT, + kommentar TEXT, + anlass TEXT + ); + ''') + conn.commit() + cur.close() + conn.close() + +def initial_setup(): + conn = connect_db() + cur = conn.cursor() + cur.execute("SELECT COUNT(*) FROM kasse;") + count = cur.fetchone()[0] + + if count == 0: + start_konto = float(input("Startwert Konto: ")) + start_bar = float(input("Startwert Bar: ")) + cur.execute("INSERT INTO kasse (beschreibung, wert, typ, bar) VALUES (%s, %s, %s, %s)", + ('Startwert Konto', start_konto, 'Einnahme', False)) + cur.execute("INSERT INTO kasse (beschreibung, wert, typ, bar) VALUES (%s, %s, %s, %s)", + ('Startwert Bar', start_bar, 'Einnahme', True)) + conn.commit() + cur.close() + conn.close() + +def parse_timestamp(date_str, time_str): + if not date_str: + date_str = datetime.datetime.now().strftime("%d.%m.%y") + if not time_str: + time_str = datetime.datetime.now().strftime("%H:%M") + return datetime.datetime.strptime(f"{date_str} {time_str}", "%d.%m.%y %H:%M") + +def erfassen(typ): + beschreibung = input("Beschreibung: ") + wert = float(input("Wert: ")) + bar = input("Bar? (1 = Ja, 0 = Nein): ") == '1' + rechnungsnummer = input("Rechnungsnummer (optional): ") or None + kommentar = input("Kommentar (optional): ") or None + anlass = input("Anlass (optional): ") or None + date_str = input("Datum (TT.MM.JJ, leer für heute): ") + time_str = input("Uhrzeit (HH:MM, leer für jetzt): ") + timestamp = parse_timestamp(date_str, time_str) + + conn = connect_db() + cur = conn.cursor() + cur.execute("INSERT INTO kasse (beschreibung, wert, typ, bar, timestamp, rechnungsnummer, kommentar, anlass) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)", + (beschreibung, wert, typ, bar, timestamp, rechnungsnummer, kommentar, anlass)) + conn.commit() + cur.close() + conn.close() + print("Buchung erfasst!") + exit = input(f"\nEnter -> zurück zum Menü ") + +def umbuchung(): + wert = float(input("Betrag der Umbuchung: ")) + richtung = input("Umbuchung Richtung? (1 = Bar -> Konto, 0 = Konto -> Bar): ") == '1' + date_str = input("Datum (TT.MM.JJ, leer für heute): ") + time_str = input("Uhrzeit (HH:MM, leer für jetzt): ") + timestamp = parse_timestamp(date_str, time_str) + + conn = connect_db() + cur = conn.cursor() + if richtung: + cur.execute("INSERT INTO kasse (beschreibung, wert, typ, bar, timestamp, anlass, gegenueber) VALUES (%s, %s, %s, %s, %s, %s, %s)", + ('Umbuchung nach Konto', wert, 'Ausgabe', True, timestamp, 'Orga', 'SJTKD')) + cur.execute("INSERT INTO kasse (beschreibung, wert, typ, bar, timestamp, anlass, gegenueber) VALUES (%s, %s, %s, %s, %s, %s, %s)", + ('Umbuchung von Bar', wert, 'Einnahme', False, timestamp, 'Orga', 'SJTKD')) + else: + cur.execute("INSERT INTO kasse (beschreibung, wert, typ, bar, timestamp, anlass, gegenueber) VALUES (%s, %s, %s, %s, %s, %s, %s)", + ('Umbuchung nach Bar', wert, 'Ausgabe', False, timestamp, 'Orga', 'SJTKD')) + cur.execute("INSERT INTO kasse (beschreibung, wert, typ, bar, timestamp, anlass, gegenueber) VALUES (%s, %s, %s, %s, %s, %s, %s)", + ('Umbuchung von Konto', wert, 'Einnahme', True, timestamp, 'Orga', 'SJTKD')) + conn.commit() + cur.close() + conn.close() + print("Umbuchung erfasst!") + exit = input(f"\nEnter -> zurück zum Menü ") + +def kassenbestand(): + conn = connect_db() + cur = conn.cursor() + cur.execute("SELECT SUM(wert) FROM kasse WHERE typ='Einnahme' AND bar=True") + einnahmen_bar = cur.fetchone()[0] or 0 + cur.execute("SELECT SUM(wert) FROM kasse WHERE typ='Ausgabe' AND bar=True") + ausgaben_bar = cur.fetchone()[0] or 0 + cur.execute("SELECT SUM(wert) FROM kasse WHERE typ='Einnahme' AND bar=False") + einnahmen_konto = cur.fetchone()[0] or 0 + cur.execute("SELECT SUM(wert) FROM kasse WHERE typ='Ausgabe' AND bar=False") + ausgaben_konto = cur.fetchone()[0] or 0 + + barbestand = einnahmen_bar - ausgaben_bar + kontostand = einnahmen_konto - ausgaben_konto + summe = barbestand + kontostand + print(UNDERLINE + "Kassenbestand:" + RESET) + print(BOLD + "Bar: " + RESET + f"{einnahmen_bar - ausgaben_bar:>10.2f} €") + print(BOLD + "Konto: " + RESET + f"{einnahmen_konto - ausgaben_konto:>10.2f} €\n==============") + print(BOLD + "Summe: " + RESET + f"{summe:>10.2f} €") + + cur.close() + conn.close() + exit = input(f"\nEnter -> zurück zum Menü ") + +def anzeigen(filter_typ=None): + conn = connect_db() + cur = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) + if filter_typ: + cur.execute("SELECT * FROM kasse WHERE typ = %s ORDER BY timestamp ASC", (filter_typ,)) + else: + cur.execute("SELECT * FROM kasse ORDER BY timestamp ASC") + rows = cur.fetchall() + for row in rows: + formatted_date = row['timestamp'].strftime("%d.%m.%y") + print(f"{row['id']:<4} {row['beschreibung']:<35} | " + f"{GREEN if row['typ']=='Einnahme' else RED}" + f"{row['wert']:>7} € " + RESET + f" | {'Bar' if row['bar'] else 'Konto':<6} | {formatted_date} | {row['rechnungsnummer'] or '':<15} | {row['kommentar'] or '':<10} | {row['anlass'] or '':<10}") + cur.close() + conn.close() + exit = input(f"\nEnter -> zurück zum Menü ") + +def anzeigen_anlass(): + filter_typ = input("Anlass: ") + conn = connect_db() + cur = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) + cur.execute("SELECT * FROM kasse WHERE anlass = %s ORDER BY timestamp ASC", (filter_typ,)) + rows = cur.fetchall() + for row in rows: + formatted_date = row['timestamp'].strftime("%d.%m.%y") + print(f"{row['id']:<4} {row['beschreibung']:<35} | " + f"{GREEN if row['typ']=='Einnahme' else RED}" + f"{row['wert']:>7} € " + RESET + f" | {'Bar' if row['bar'] else 'Konto':<6} | {formatted_date} | {row['rechnungsnummer'] or '':<10} | {row['kommentar'] or '':<10}") + + cur.execute("SELECT SUM(wert) FROM kasse WHERE typ = 'Einnahme' AND anlass = %s", (filter_typ,)) + einnahmen = cur.fetchone()[0] or 0 + + cur.execute("SELECT SUM(wert) FROM kasse WHERE typ='Ausgabe' AND anlass = %s", (filter_typ,)) + ausgaben = cur.fetchone()[0] or 0 + + print(UNDERLINE + "\nBilanz:" + RESET) + text="Einnahmen:" + print(BOLD + f"{text:<8}" + RESET + GREEN + f"{einnahmen:>10.2f} €" + RESET) + text="Ausgaben: " + print(BOLD + f"{text:<8}" + RESET + RED + f"{ausgaben:>10.2f} €" + RESET + f"\n===================") + summe = einnahmen - ausgaben + COLOR = GREEN + if summe<0: + COLOR = RED + text="Summe: " + print(BOLD + f"{text:<8}" + RESET + COLOR + f"{summe:>10.2f} €" + RESET) + + cur.close() + conn.close() + exit = input(f"\nEnter -> zurück zum Menü ") + +def menu(): + create_table() + initial_setup() + while True: + print(UNDERLINE + "\nKassenbuch Menü:" + RESET) + print(BOLD + "(1)" + RESET + " Ausgabe erfassen") + print(BOLD + "(2)" + RESET + " Einnahme erfassen") + print(BOLD + "(3)" + RESET + " Kassenbestand anzeigen") + print(BOLD + "(4)" + RESET + " Umsätze anzeigen") + print(BOLD + "(5)" + RESET + " Ausgaben anzeigen") + print(BOLD + "(6)" + RESET + " Einnahmen anzeigen") + print(BOLD + "(7)" + RESET + " Umbuchung durchführen") + print(BOLD + "(8)" + RESET + " Bilanz Veranstaltung") + print(BOLD + "(0)" + RESET + " Beenden") + auswahl = input("Auswahl: ") + print("\n") + if auswahl == '1': erfassen('Ausgabe') + elif auswahl == '2': erfassen('Einnahme') + elif auswahl == '3': kassenbestand() + elif auswahl == '4': anzeigen() + elif auswahl == '5': anzeigen('Ausgabe') + elif auswahl == '6': anzeigen('Einnahme') + elif auswahl == '7': umbuchung() + elif auswahl == '8': anzeigen_anlass() + elif auswahl == '0': break + +if __name__ == "__main__": + menu()