Tests and helpers

This commit is contained in:
2026-06-02 15:25:32 +02:00
parent faf5d440f3
commit d249c6a013
9 changed files with 372 additions and 121 deletions
+10 -4
View File
@@ -1,5 +1,6 @@
from uuid import uuid4 from uuid import uuid4
from core.transaction import Transaction from core.transaction import Transaction
from utils.helpers import validate_pin, parse_price
class Tracker: class Tracker:
@@ -70,7 +71,7 @@ class Tracker:
from uuid import uuid4 from uuid import uuid4
from core.transaction import Transaction from core.transaction import Transaction
transaction = Transaction(str(uuid4()), user_id, "adjustment", -amount) transaction = Transaction(str(uuid4()), user_id, "Abbuchung", -amount)
self.transactions.append(transaction) self.transactions.append(transaction)
self.data_manager.save_transactions(self.transactions) self.data_manager.save_transactions(self.transactions)
@@ -97,12 +98,15 @@ class Tracker:
def set_user_pin(self, user_id, pin): def set_user_pin(self, user_id, pin):
if user_id not in self.users: if user_id not in self.users:
return {"status": "error", "message": "User not found"} return {"status": "error", "message": "User not found"}
if not validate_pin(pin):
return {"status": "error", "message": "Invalid PIN format"}
self.users[user_id].pin = pin self.users[user_id].pin = pin
self.data_manager.save_users(self.users) self.data_manager.save_users(self.users)
return {"status": "success", "message": "PIN updated successfully"} return {"status": "success", "message": "PIN updated successfully"}
def add_user(self, user_id, name): def add_user(self, user_id, name):
from core.user import User from core.user import User
if user_id in self.users: if user_id in self.users:
return {"status": "error", "message": "User ID already exists"} return {"status": "error", "message": "User ID already exists"}
self.users[user_id] = User(user_id, name, "4242") self.users[user_id] = User(user_id, name, "4242")
@@ -123,6 +127,8 @@ class Tracker:
def edit_user(self, user_id, name, pin=None): def edit_user(self, user_id, name, pin=None):
if user_id not in self.users: if user_id not in self.users:
return {"status": "error", "message": "User not found"} return {"status": "error", "message": "User not found"}
if pin is not None and not validate_pin(pin):
return {"status": "error", "message": "Invalid PIN format"}
self.users[user_id].name = name self.users[user_id].name = name
if pin is not None: if pin is not None:
self.users[user_id].pin = pin self.users[user_id].pin = pin
@@ -131,10 +137,11 @@ class Tracker:
def add_item(self, item_id, name, price): def add_item(self, item_id, name, price):
from core.item import Item from core.item import Item
if item_id in self.items: if item_id in self.items:
return {"status": "error", "message": "Item ID already exists"} return {"status": "error", "message": "Item ID already exists"}
try: try:
price_val = float(price) price_val = parse_price(str(price))
except ValueError: except ValueError:
return {"status": "error", "message": "Invalid price value"} return {"status": "error", "message": "Invalid price value"}
self.items[item_id] = Item(item_id, name, price_val) self.items[item_id] = Item(item_id, name, price_val)
@@ -153,11 +160,10 @@ class Tracker:
if item_id not in self.items: if item_id not in self.items:
return {"status": "error", "message": "Item not found"} return {"status": "error", "message": "Item not found"}
try: try:
price_val = float(price) price_val = parse_price(str(price))
except ValueError: except ValueError:
return {"status": "error", "message": "Invalid price value"} return {"status": "error", "message": "Invalid price value"}
self.items[item_id].name = name self.items[item_id].name = name
self.items[item_id].price = price_val self.items[item_id].price = price_val
self.data_manager.save_items(self.items) self.data_manager.save_items(self.items)
return {"status": "success", "message": f"Item {name} updated successfully"} return {"status": "success", "message": f"Item {name} updated successfully"}
+37
View File
@@ -0,0 +1,37 @@
import unittest
from utils.helpers import format_currency, format_date, validate_pin, parse_price
class TestHelpers(unittest.TestCase):
def test_format_currency_german(self):
self.assertEqual(format_currency(2.50), "2,50 €")
self.assertEqual(format_currency(1234.567), "1234,57 €")
self.assertEqual(format_currency(0.0), "0,00 €")
def test_format_currency_non_german(self):
self.assertEqual(format_currency(2.50, symbol="$", is_german=False), "$2.50")
self.assertEqual(format_currency(0.99, symbol="£", is_german=False), "£0.99")
def test_format_date(self):
self.assertEqual(format_date("2026-06-02T15:00:00.000000"), "02.06.2026")
self.assertEqual(format_date("2026-12-25T18:30:00"), "25.12.2026")
self.assertEqual(format_date("invalid-date-format"), "invalid-da")
def test_validate_pin(self):
self.assertTrue(validate_pin("1234"))
self.assertTrue(validate_pin("4242"))
self.assertTrue(validate_pin("123456"))
self.assertFalse(validate_pin("123")) # too short
self.assertFalse(validate_pin("123a")) # non-numeric
self.assertFalse(validate_pin("")) # empty
def test_parse_price(self):
self.assertEqual(parse_price("2.50"), 2.50)
self.assertEqual(parse_price("2,50"), 2.50)
self.assertEqual(parse_price(" 0,99 "), 0.99)
with self.assertRaises(ValueError):
parse_price("-2.50") # negative
with self.assertRaises(ValueError):
parse_price("abc") # non-numeric
if __name__ == "__main__":
unittest.main()
+37
View File
@@ -0,0 +1,37 @@
import unittest
from core.item import Item
class TestItem(unittest.TestCase):
def test_item_initialization(self):
item = Item("coffee", "Coffee", 2.50)
self.assertEqual(item.id, "coffee")
self.assertEqual(item.name, "Coffee")
self.assertEqual(item.price, 2.50)
def test_item_price_conversion(self):
item = Item("tea", "Tea", "1.80")
self.assertEqual(item.price, 1.80)
self.assertIsInstance(item.price, float)
def test_to_json(self):
item = Item("sandwich", "Sandwich", 5.00)
expected = {
"id": "sandwich",
"name": "Sandwich",
"price": 5.00
}
self.assertEqual(item.to_json(), expected)
def test_from_json(self):
data = {
"id": "sandwich",
"name": "Sandwich",
"price": 5.00
}
item = Item.from_json(data)
self.assertEqual(item.id, "sandwich")
self.assertEqual(item.name, "Sandwich")
self.assertEqual(item.price, 5.00)
if __name__ == "__main__":
unittest.main()
+19 -7
View File
@@ -6,7 +6,6 @@ from core.item import Item
import shutil import shutil
import os import os
class TestTracker(unittest.TestCase): class TestTracker(unittest.TestCase):
def setUp(self): def setUp(self):
self.test_dir = "test_data" self.test_dir = "test_data"
@@ -24,7 +23,6 @@ class TestTracker(unittest.TestCase):
shutil.rmtree(self.test_dir) shutil.rmtree(self.test_dir)
def test_adjust_debt_new_user(self): def test_adjust_debt_new_user(self):
# Verify adjust_debt does not raise KeyError on uninitialized user debt
res = self.tracker.adjust_debt("test_user", 2.0, "admin123") res = self.tracker.adjust_debt("test_user", 2.0, "admin123")
self.assertEqual(res["status"], "success") self.assertEqual(res["status"], "success")
self.assertEqual(self.tracker.get_user_debt("test_user"), -2.0) self.assertEqual(self.tracker.get_user_debt("test_user"), -2.0)
@@ -35,15 +33,12 @@ class TestTracker(unittest.TestCase):
self.assertEqual(self.tracker.get_user_debt("test_user"), 2.50) self.assertEqual(self.tracker.get_user_debt("test_user"), 2.50)
self.assertEqual(len(self.tracker.transactions), 1) self.assertEqual(len(self.tracker.transactions), 1)
def test_default_pin(self):
user = User("new_user", "New User")
self.assertEqual(user.pin, "4242")
def test_add_user(self): def test_add_user(self):
res = self.tracker.add_user("test_user_2", "Test Bob") res = self.tracker.add_user("test_user_2", "Test Bob")
self.assertEqual(res["status"], "success") self.assertEqual(res["status"], "success")
self.assertEqual(self.tracker.users["test_user_2"].name, "Test Bob") self.assertEqual(self.tracker.users["test_user_2"].name, "Test Bob")
self.assertEqual(self.tracker.users["test_user_2"].pin, "4242") self.assertEqual(self.tracker.users["test_user_2"].pin, "4242")
def test_remove_user(self): def test_remove_user(self):
res = self.tracker.remove_user("test_user") res = self.tracker.remove_user("test_user")
self.assertEqual(res["status"], "success") self.assertEqual(res["status"], "success")
@@ -56,12 +51,26 @@ class TestTracker(unittest.TestCase):
self.assertEqual(self.tracker.users["test_user"].name, "Updated Name") self.assertEqual(self.tracker.users["test_user"].name, "Updated Name")
self.assertEqual(self.tracker.users["test_user"].pin, "9999") self.assertEqual(self.tracker.users["test_user"].pin, "9999")
def test_set_user_pin_validation(self):
res = self.tracker.set_user_pin("test_user", "invalid_pin")
self.assertEqual(res["status"], "error")
self.assertEqual(self.tracker.users["test_user"].pin, "4242") # Unchanged
res2 = self.tracker.set_user_pin("test_user", "1234")
self.assertEqual(res2["status"], "success")
self.assertEqual(self.tracker.users["test_user"].pin, "1234")
def test_add_item(self): def test_add_item(self):
res = self.tracker.add_item("new_item", "Donut", 1.80) res = self.tracker.add_item("new_item", "Donut", 1.80)
self.assertEqual(res["status"], "success") self.assertEqual(res["status"], "success")
self.assertEqual(self.tracker.items["new_item"].name, "Donut") self.assertEqual(self.tracker.items["new_item"].name, "Donut")
self.assertEqual(self.tracker.items["new_item"].price, 1.80) self.assertEqual(self.tracker.items["new_item"].price, 1.80)
def test_add_item_invalid_price(self):
res = self.tracker.add_item("invalid_item", "Cake", "-2.50")
self.assertEqual(res["status"], "error")
self.assertNotIn("invalid_item", self.tracker.items)
def test_remove_item(self): def test_remove_item(self):
res = self.tracker.remove_item("test_item") res = self.tracker.remove_item("test_item")
self.assertEqual(res["status"], "success") self.assertEqual(res["status"], "success")
@@ -73,7 +82,10 @@ class TestTracker(unittest.TestCase):
self.assertEqual(self.tracker.items["test_item"].name, "Premium Coffee") self.assertEqual(self.tracker.items["test_item"].name, "Premium Coffee")
self.assertEqual(self.tracker.items["test_item"].price, 3.20) self.assertEqual(self.tracker.items["test_item"].price, 3.20)
def test_edit_item_invalid_price(self):
res = self.tracker.edit_item("test_item", "Premium Coffee", "not_a_number")
self.assertEqual(res["status"], "error")
self.assertEqual(self.tracker.items["test_item"].price, 2.50) # Unchanged
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+51
View File
@@ -0,0 +1,51 @@
import unittest
from core.transaction import Transaction
class TestTransaction(unittest.TestCase):
def test_transaction_initialization_with_timestamp(self):
timestamp = "2026-06-02T15:00:00.000000"
t = Transaction("tx_1", "user_1", "item_1", 2.50, timestamp)
self.assertEqual(t.id, "tx_1")
self.assertEqual(t.user_id, "user_1")
self.assertEqual(t.item_id, "item_1")
self.assertEqual(t.amount, 2.50)
self.assertEqual(t.timestamp, timestamp)
def test_transaction_auto_timestamp(self):
t = Transaction("tx_2", "user_2", "item_2", 1.50)
self.assertIsNotNone(t.timestamp)
# Check if timestamp format is valid ISO string (at least parses or contains dates)
self.assertIn("-", t.timestamp)
self.assertIn("T", t.timestamp)
def test_to_json(self):
timestamp = "2026-06-02T15:00:00"
t = Transaction("tx_1", "user_1", "item_1", 2.50, timestamp)
expected = {
"id": "tx_1",
"user_id": "user_1",
"item_id": "item_1",
"amount": 2.50,
"timestamp": timestamp,
}
self.assertEqual(t.to_json(), expected)
def test_from_json(self):
data = {
"id": "tx_1",
"user_id": "user_1",
"item_id": "item_1",
"amount": 2.50,
"timestamp": "2026-06-02T15:00:00",
}
t = Transaction.from_json(data)
self.assertEqual(t.id, "tx_1")
self.assertEqual(t.user_id, "user_1")
self.assertEqual(t.item_id, "item_1")
self.assertEqual(t.amount, 2.50)
self.assertEqual(t.timestamp, "2026-06-02T15:00:00")
if __name__ == "__main__":
unittest.main()
+50
View File
@@ -0,0 +1,50 @@
import unittest
from core.user import User
class TestUser(unittest.TestCase):
def test_default_values(self):
user = User("alice", "Alice")
self.assertEqual(user.id, "alice")
self.assertEqual(user.name, "Alice")
self.assertEqual(user.pin, "4242")
self.assertEqual(user.debt, 0.0)
def test_custom_pin(self):
user = User("bob", "Bob", "1234")
self.assertEqual(user.pin, "1234")
def test_to_json(self):
user = User("charlie", "Charlie", "9999")
user.debt = 12.50
expected = {
"id": "charlie",
"name": "Charlie",
"debt": 12.50,
"pin": "9999"
}
self.assertEqual(user.to_json(), expected)
def test_from_json(self):
data = {
"id": "charlie",
"name": "Charlie",
"debt": 12.50,
"pin": "9999"
}
user = User.from_json(data)
self.assertEqual(user.id, "charlie")
self.assertEqual(user.name, "Charlie")
self.assertEqual(user.debt, 12.50)
self.assertEqual(user.pin, "9999")
def test_from_json_missing_pin(self):
data = {
"id": "charlie",
"name": "Charlie",
"debt": 12.50
}
user = User.from_json(data)
self.assertEqual(user.pin, "4242")
if __name__ == "__main__":
unittest.main()
+1 -1
View File
@@ -364,7 +364,7 @@ class CLI:
for t in sorted(transactions, key=lambda x: x.timestamp, reverse=True): for t in sorted(transactions, key=lambda x: x.timestamp, reverse=True):
user = self.tracker.get_user_by_id(t.user_id) user = self.tracker.get_user_by_id(t.user_id)
item = self.tracker.get_items().get(t.item_id) item = self.tracker.get_items().get(t.item_id)
item_name = item.name if item else ("Adjustment" if t.item_id == "adjustment" else "Deleted Item") item_name = item.name if item else ("Abbuchung" if t.item_id in ("adjustment", "Abbuchung") else "Deleted Item")
print( print(
f"{t.timestamp[:10]} | {user.name:.<20} {item_name:.<15} ${t.amount:.2f}" f"{t.timestamp[:10]} | {user.name:.<20} {item_name:.<15} ${t.amount:.2f}"
) )
+133 -109
View File
@@ -1,6 +1,7 @@
# type: ignore ## type: ignore
import kivy import kivy
from kivy.app import App from kivy.app import App
from kivy.graphics import Color, RoundedRectangle
from kivy.uix.screenmanager import ScreenManager, Screen, SlideTransition from kivy.uix.screenmanager import ScreenManager, Screen, SlideTransition
from kivy.uix.boxlayout import BoxLayout from kivy.uix.boxlayout import BoxLayout
from kivy.uix.gridlayout import GridLayout from kivy.uix.gridlayout import GridLayout
@@ -10,7 +11,7 @@ from kivy.uix.button import Button
from kivy.uix.textinput import TextInput from kivy.uix.textinput import TextInput
from kivy.uix.popup import Popup from kivy.uix.popup import Popup
from kivy.core.window import Window from kivy.core.window import Window
from kivy.graphics import Color, RoundedRectangle from utils.helpers import format_currency, format_date, validate_pin, parse_price
class RoundedButton(Button): class RoundedButton(Button):
@@ -104,45 +105,57 @@ class ConfirmPopup(Popup):
class NumpadPopup(Popup): class NumpadPopup(Popup):
def __init__(self, title, is_password=False, is_float=False, on_submit=None, **kwargs): def __init__(
self, title, is_password=False, is_float=False, on_submit=None, **kwargs
):
self.is_password = is_password self.is_password = is_password
self.is_float = is_float self.is_float = is_float
self.on_submit = on_submit self.on_submit = on_submit
self.entered_text = "" self.entered_text = ""
content = BoxLayout(orientation='vertical', padding=15, spacing=15) content = BoxLayout(orientation="vertical", padding=15, spacing=15)
self.display = Label( self.display = Label(
text="PIN eingeben:" if is_password else "Betrag eingeben:", text="PIN eingeben:" if is_password else "Betrag eingeben:",
font_size='20sp', font_size="20sp",
bold=True, bold=True,
size_hint_y=0.15, size_hint_y=0.15,
halign='center', halign="center",
valign='middle' valign="middle",
) )
self.display.bind(size=self.display.setter('text_size')) self.display.bind(size=self.display.setter("text_size"))
content.add_widget(self.display) content.add_widget(self.display)
grid = GridLayout(cols=3, spacing=10, size_hint_y=0.6) grid = GridLayout(cols=3, spacing=10, size_hint_y=0.6)
buttons = [ buttons = [
'1', '2', '3', "1",
'4', '5', '6', "2",
'7', '8', '9', "3",
',' if is_float else 'C', '0', '' "4",
"5",
"6",
"7",
"8",
"9",
"," if is_float else "C",
"0",
"",
] ]
for btn_text in buttons: for btn_text in buttons:
btn = RoundedButton( btn = RoundedButton(
text=btn_text, text=btn_text,
btn_color=[0.15, 0.15, 0.17, 1] if btn_text in ['C', '', ','] else [0.0, 0.65, 0.57, 1] btn_color=[0.15, 0.15, 0.17, 1]
if btn_text in ["C", "", ","]
else [0.0, 0.65, 0.57, 1],
) )
btn.bind(on_release=self.on_key_press) btn.bind(on_release=self.on_key_press)
grid.add_widget(btn) grid.add_widget(btn)
content.add_widget(grid) content.add_widget(grid)
actions = BoxLayout(orientation='horizontal', spacing=10, size_hint_y=0.25) actions = BoxLayout(orientation="horizontal", spacing=10, size_hint_y=0.25)
ok_btn = RoundedButton(text="OK", btn_color=[0.0, 0.65, 0.57, 1]) ok_btn = RoundedButton(text="OK", btn_color=[0.0, 0.65, 0.57, 1])
cancel_btn = RoundedButton(text="Abbrechen", btn_color=[0.25, 0.25, 0.28, 1]) cancel_btn = RoundedButton(text="Abbrechen", btn_color=[0.25, 0.25, 0.28, 1])
@@ -154,10 +167,10 @@ class NumpadPopup(Popup):
title=title, title=title,
content=content, content=content,
size_hint=(0.85, 0.75), size_hint=(0.85, 0.75),
title_align='center', title_align="center",
title_size='18sp', title_size="18sp",
background_color=[0.08, 0.08, 0.09, 0.95], background_color=[0.08, 0.08, 0.09, 0.95],
**kwargs **kwargs,
) )
ok_btn.bind(on_release=self.submit) ok_btn.bind(on_release=self.submit)
@@ -165,20 +178,22 @@ class NumpadPopup(Popup):
def on_key_press(self, instance): def on_key_press(self, instance):
key = instance.text key = instance.text
if key == '': if key == "":
self.entered_text = self.entered_text[:-1] self.entered_text = self.entered_text[:-1]
elif key == 'C': elif key == "C":
self.entered_text = "" self.entered_text = ""
elif key == ',': elif key == ",":
if ',' not in self.entered_text: if "," not in self.entered_text:
self.entered_text += ',' self.entered_text += ","
else: else:
self.entered_text += key self.entered_text += key
self.update_display() self.update_display()
def update_display(self): def update_display(self):
if not self.entered_text: if not self.entered_text:
self.display.text = "PIN eingeben:" if self.is_password else "Betrag eingeben:" self.display.text = (
"PIN eingeben:" if self.is_password else "Betrag eingeben:"
)
else: else:
if self.is_password: if self.is_password:
self.display.text = "*" * len(self.entered_text) self.display.text = "*" * len(self.entered_text)
@@ -197,34 +212,36 @@ class KeyboardPopup(Popup):
self.on_submit = on_submit self.on_submit = on_submit
self.entered_text = "" self.entered_text = ""
content = BoxLayout(orientation='vertical', padding=15, spacing=15) content = BoxLayout(orientation="vertical", padding=15, spacing=15)
self.display = Label( self.display = Label(
text="Eingeben:" if not is_password else "****", text="Eingeben:" if not is_password else "****",
font_size='20sp', font_size="20sp",
bold=True, bold=True,
size_hint_y=0.15, size_hint_y=0.15,
halign='center', halign="center",
valign='middle' valign="middle",
) )
self.display.bind(size=self.display.setter('text_size')) self.display.bind(size=self.display.setter("text_size"))
content.add_widget(self.display) content.add_widget(self.display)
kbd_layout = BoxLayout(orientation='vertical', spacing=5, size_hint_y=0.6) kbd_layout = BoxLayout(orientation="vertical", spacing=5, size_hint_y=0.6)
rows = [ rows = [
['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'], ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"],
['q', 'w', 'e', 'r', 't', 'z', 'u', 'i', 'o', 'p'], ["q", "w", "e", "r", "t", "z", "u", "i", "o", "p"],
['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l'], ["a", "s", "d", "f", "g", "h", "j", "k", "l"],
['y', 'x', 'c', 'v', 'b', 'n', 'm', '', 'C'] ["y", "x", "c", "v", "b", "n", "m", "", "C"],
] ]
for row in rows: for row in rows:
row_box = BoxLayout(orientation='horizontal', spacing=5) row_box = BoxLayout(orientation="horizontal", spacing=5)
for char in row: for char in row:
btn = RoundedButton( btn = RoundedButton(
text=char, text=char,
btn_color=[0.15, 0.15, 0.17, 1] if char in ['C', ''] else [0.0, 0.65, 0.57, 1] btn_color=[0.15, 0.15, 0.17, 1]
if char in ["C", ""]
else [0.0, 0.65, 0.57, 1],
) )
btn.bind(on_release=self.on_key_press) btn.bind(on_release=self.on_key_press)
row_box.add_widget(btn) row_box.add_widget(btn)
@@ -232,7 +249,7 @@ class KeyboardPopup(Popup):
content.add_widget(kbd_layout) content.add_widget(kbd_layout)
actions = BoxLayout(orientation='horizontal', spacing=10, size_hint_y=0.25) actions = BoxLayout(orientation="horizontal", spacing=10, size_hint_y=0.25)
ok_btn = RoundedButton(text="OK", btn_color=[0.0, 0.65, 0.57, 1]) ok_btn = RoundedButton(text="OK", btn_color=[0.0, 0.65, 0.57, 1])
cancel_btn = RoundedButton(text="Abbrechen", btn_color=[0.25, 0.25, 0.28, 1]) cancel_btn = RoundedButton(text="Abbrechen", btn_color=[0.25, 0.25, 0.28, 1])
@@ -244,10 +261,10 @@ class KeyboardPopup(Popup):
title=title, title=title,
content=content, content=content,
size_hint=(0.95, 0.8), size_hint=(0.95, 0.8),
title_align='center', title_align="center",
title_size='18sp', title_size="18sp",
background_color=[0.08, 0.08, 0.09, 0.95], background_color=[0.08, 0.08, 0.09, 0.95],
**kwargs **kwargs,
) )
ok_btn.bind(on_release=self.submit) ok_btn.bind(on_release=self.submit)
@@ -255,9 +272,9 @@ class KeyboardPopup(Popup):
def on_key_press(self, instance): def on_key_press(self, instance):
key = instance.text key = instance.text
if key == '': if key == "":
self.entered_text = self.entered_text[:-1] self.entered_text = self.entered_text[:-1]
elif key == 'C': elif key == "C":
self.entered_text = "" self.entered_text = ""
else: else:
self.entered_text += key self.entered_text += key
@@ -338,7 +355,7 @@ class AdjustDebtPopup(Popup):
title="Betrag eingeben", title="Betrag eingeben",
is_password=False, is_password=False,
is_float=True, is_float=True,
on_submit=self.on_amount_submitted on_submit=self.on_amount_submitted,
) )
popup.open() popup.open()
@@ -362,16 +379,16 @@ class CreateUserPopup(Popup):
def __init__(self, on_create, **kwargs): def __init__(self, on_create, **kwargs):
self.on_create = on_create self.on_create = on_create
content = BoxLayout(orientation='vertical', padding=15, spacing=15) content = BoxLayout(orientation="vertical", padding=15, spacing=15)
info = Label( info = Label(
text="Name des neuen Benutzers eingeben:", text="Name des neuen Benutzers eingeben:",
font_size='16sp', font_size="16sp",
size_hint_y=0.25, size_hint_y=0.25,
halign='center', halign="center",
valign='middle' valign="middle",
) )
info.bind(size=info.setter('text_size')) info.bind(size=info.setter("text_size"))
content.add_widget(info) content.add_widget(info)
self.name_input = TextInput( self.name_input = TextInput(
@@ -384,7 +401,7 @@ class CreateUserPopup(Popup):
foreground_color=[1, 1, 1, 1], foreground_color=[1, 1, 1, 1],
cursor_color=[1, 1, 1, 1], cursor_color=[1, 1, 1, 1],
size_hint_y=0.3, size_hint_y=0.3,
padding=[10, 10] padding=[10, 10],
) )
self.name_input.bind(focus=self.trigger_keyboard) self.name_input.bind(focus=self.trigger_keyboard)
content.add_widget(self.name_input) content.add_widget(self.name_input)
@@ -417,7 +434,7 @@ class CreateUserPopup(Popup):
popup = KeyboardPopup( popup = KeyboardPopup(
title="Name eingeben", title="Name eingeben",
is_password=False, is_password=False,
on_submit=self.on_name_submitted on_submit=self.on_name_submitted,
) )
popup.open() popup.open()
@@ -435,21 +452,23 @@ class CreateUserPopup(Popup):
class EditUserPopup(Popup): class EditUserPopup(Popup):
def __init__(self, user_id, current_name, current_pin, on_save, on_delete, **kwargs): def __init__(
self, user_id, current_name, current_pin, on_save, on_delete, **kwargs
):
self.user_id = user_id self.user_id = user_id
self.on_save = on_save self.on_save = on_save
self.on_delete = on_delete self.on_delete = on_delete
content = BoxLayout(orientation='vertical', padding=15, spacing=15) content = BoxLayout(orientation="vertical", padding=15, spacing=15)
info = Label( info = Label(
text="Benutzer bearbeiten:", text="Benutzer bearbeiten:",
font_size='16sp', font_size="16sp",
size_hint_y=0.15, size_hint_y=0.15,
halign='center', halign="center",
valign='middle' valign="middle",
) )
info.bind(size=info.setter('text_size')) info.bind(size=info.setter("text_size"))
content.add_widget(info) content.add_widget(info)
content.add_widget(Label(text="Name:", size_hint_y=0.08, halign="left")) content.add_widget(Label(text="Name:", size_hint_y=0.08, halign="left"))
@@ -463,7 +482,7 @@ class EditUserPopup(Popup):
foreground_color=[1, 1, 1, 1], foreground_color=[1, 1, 1, 1],
cursor_color=[1, 1, 1, 1], cursor_color=[1, 1, 1, 1],
size_hint_y=0.15, size_hint_y=0.15,
padding=[10, 10] padding=[10, 10],
) )
self.name_input.bind(focus=self.trigger_keyboard) self.name_input.bind(focus=self.trigger_keyboard)
content.add_widget(self.name_input) content.add_widget(self.name_input)
@@ -479,7 +498,7 @@ class EditUserPopup(Popup):
foreground_color=[1, 1, 1, 1], foreground_color=[1, 1, 1, 1],
cursor_color=[1, 1, 1, 1], cursor_color=[1, 1, 1, 1],
size_hint_y=0.15, size_hint_y=0.15,
padding=[10, 10] padding=[10, 10],
) )
self.pin_input.bind(focus=self.trigger_numpad) self.pin_input.bind(focus=self.trigger_numpad)
content.add_widget(self.pin_input) content.add_widget(self.pin_input)
@@ -515,7 +534,7 @@ class EditUserPopup(Popup):
popup = KeyboardPopup( popup = KeyboardPopup(
title="Name eingeben", title="Name eingeben",
is_password=False, is_password=False,
on_submit=self.on_name_submitted on_submit=self.on_name_submitted,
) )
popup.open() popup.open()
@@ -526,9 +545,7 @@ class EditUserPopup(Popup):
if value: if value:
instance.focus = False instance.focus = False
popup = NumpadPopup( popup = NumpadPopup(
title="PIN eingeben", title="PIN eingeben", is_password=False, on_submit=self.on_pin_submitted
is_password=False,
on_submit=self.on_pin_submitted
) )
popup.open() popup.open()
@@ -543,7 +560,9 @@ class EditUserPopup(Popup):
err.open() err.open()
return return
if len(pin) < 4: if len(pin) < 4:
err = MessagePopup(title="Fehler", message="Die PIN must mindestens 4 Ziffern lang sein!") err = MessagePopup(
title="Fehler", message="Die PIN must mindestens 4 Ziffern lang sein!"
)
err.open() err.open()
return return
self.dismiss() self.dismiss()
@@ -558,16 +577,16 @@ class CreateItemPopup(Popup):
def __init__(self, on_create, **kwargs): def __init__(self, on_create, **kwargs):
self.on_create = on_create self.on_create = on_create
content = BoxLayout(orientation='vertical', padding=15, spacing=15) content = BoxLayout(orientation="vertical", padding=15, spacing=15)
info = Label( info = Label(
text="Neuen Artikel hinzufügen:", text="Neuen Artikel hinzufügen:",
font_size='16sp', font_size="16sp",
size_hint_y=0.15, size_hint_y=0.15,
halign='center', halign="center",
valign='middle' valign="middle",
) )
info.bind(size=info.setter('text_size')) info.bind(size=info.setter("text_size"))
content.add_widget(info) content.add_widget(info)
content.add_widget(Label(text="Name:", size_hint_y=0.08, halign="left")) content.add_widget(Label(text="Name:", size_hint_y=0.08, halign="left"))
@@ -581,7 +600,7 @@ class CreateItemPopup(Popup):
foreground_color=[1, 1, 1, 1], foreground_color=[1, 1, 1, 1],
cursor_color=[1, 1, 1, 1], cursor_color=[1, 1, 1, 1],
size_hint_y=0.15, size_hint_y=0.15,
padding=[10, 10] padding=[10, 10],
) )
self.name_input.bind(focus=self.trigger_keyboard) self.name_input.bind(focus=self.trigger_keyboard)
content.add_widget(self.name_input) content.add_widget(self.name_input)
@@ -597,7 +616,7 @@ class CreateItemPopup(Popup):
foreground_color=[1, 1, 1, 1], foreground_color=[1, 1, 1, 1],
cursor_color=[1, 1, 1, 1], cursor_color=[1, 1, 1, 1],
size_hint_y=0.15, size_hint_y=0.15,
padding=[10, 10] padding=[10, 10],
) )
self.price_input.bind(focus=self.trigger_numpad) self.price_input.bind(focus=self.trigger_numpad)
content.add_widget(self.price_input) content.add_widget(self.price_input)
@@ -630,7 +649,7 @@ class CreateItemPopup(Popup):
popup = KeyboardPopup( popup = KeyboardPopup(
title="Artikelname eingeben", title="Artikelname eingeben",
is_password=False, is_password=False,
on_submit=self.on_name_submitted on_submit=self.on_name_submitted,
) )
popup.open() popup.open()
@@ -644,7 +663,7 @@ class CreateItemPopup(Popup):
title="Preis eingeben", title="Preis eingeben",
is_password=False, is_password=False,
is_float=True, is_float=True,
on_submit=self.on_price_submitted on_submit=self.on_price_submitted,
) )
popup.open() popup.open()
@@ -672,21 +691,23 @@ class CreateItemPopup(Popup):
class EditItemPopup(Popup): class EditItemPopup(Popup):
def __init__(self, item_id, current_name, current_price, on_save, on_delete, **kwargs): def __init__(
self, item_id, current_name, current_price, on_save, on_delete, **kwargs
):
self.item_id = item_id self.item_id = item_id
self.on_save = on_save self.on_save = on_save
self.on_delete = on_delete self.on_delete = on_delete
content = BoxLayout(orientation='vertical', padding=15, spacing=15) content = BoxLayout(orientation="vertical", padding=15, spacing=15)
info = Label( info = Label(
text="Artikel bearbeiten:", text="Artikel bearbeiten:",
font_size='16sp', font_size="16sp",
size_hint_y=0.15, size_hint_y=0.15,
halign='center', halign="center",
valign='middle' valign="middle",
) )
info.bind(size=info.setter('text_size')) info.bind(size=info.setter("text_size"))
content.add_widget(info) content.add_widget(info)
content.add_widget(Label(text="Name:", size_hint_y=0.08, halign="left")) content.add_widget(Label(text="Name:", size_hint_y=0.08, halign="left"))
@@ -700,7 +721,7 @@ class EditItemPopup(Popup):
foreground_color=[1, 1, 1, 1], foreground_color=[1, 1, 1, 1],
cursor_color=[1, 1, 1, 1], cursor_color=[1, 1, 1, 1],
size_hint_y=0.15, size_hint_y=0.15,
padding=[10, 10] padding=[10, 10],
) )
self.name_input.bind(focus=self.trigger_keyboard) self.name_input.bind(focus=self.trigger_keyboard)
content.add_widget(self.name_input) content.add_widget(self.name_input)
@@ -716,7 +737,7 @@ class EditItemPopup(Popup):
foreground_color=[1, 1, 1, 1], foreground_color=[1, 1, 1, 1],
cursor_color=[1, 1, 1, 1], cursor_color=[1, 1, 1, 1],
size_hint_y=0.15, size_hint_y=0.15,
padding=[10, 10] padding=[10, 10],
) )
self.price_input.bind(focus=self.trigger_numpad) self.price_input.bind(focus=self.trigger_numpad)
content.add_widget(self.price_input) content.add_widget(self.price_input)
@@ -752,7 +773,7 @@ class EditItemPopup(Popup):
popup = KeyboardPopup( popup = KeyboardPopup(
title="Artikelname eingeben", title="Artikelname eingeben",
is_password=False, is_password=False,
on_submit=self.on_name_submitted on_submit=self.on_name_submitted,
) )
popup.open() popup.open()
@@ -766,7 +787,7 @@ class EditItemPopup(Popup):
title="Preis eingeben", title="Preis eingeben",
is_password=False, is_password=False,
is_float=True, is_float=True,
on_submit=self.on_price_submitted on_submit=self.on_price_submitted,
) )
popup.open() popup.open()
@@ -847,7 +868,7 @@ class TransactionRow(BoxLayout):
self.bind(pos=self.update_rect, size=self.update_rect) self.bind(pos=self.update_rect, size=self.update_rect)
date_lbl = Label( date_lbl = Label(
text=date[:10], text=format_date(date),
font_size="14sp", font_size="14sp",
size_hint_x=0.25, size_hint_x=0.25,
halign="left", halign="left",
@@ -1015,27 +1036,29 @@ class UserSelectionScreen(StyledScreen):
popup = NumpadPopup( popup = NumpadPopup(
title=f"PIN einrichten: {user.name}", title=f"PIN einrichten: {user.name}",
is_password=True, is_password=True,
on_submit=lambda pin: self.setup_pin_confirm(user_id, pin) on_submit=lambda pin: self.setup_pin_confirm(user_id, pin),
) )
popup.open() popup.open()
else: else:
popup = NumpadPopup( popup = NumpadPopup(
title=f"PIN eingeben: {user.name}", title=f"PIN eingeben: {user.name}",
is_password=True, is_password=True,
on_submit=lambda pin: self.verify_pin(user_id, pin) on_submit=lambda pin: self.verify_pin(user_id, pin),
) )
popup.open() popup.open()
def setup_pin_confirm(self, user_id, pin): def setup_pin_confirm(self, user_id, pin):
if len(pin) < 4: if len(pin) < 4:
err = MessagePopup(title="Fehler", message="Die PIN muss mindestens 4 Ziffern lang sein!") err = MessagePopup(
title="Fehler", message="Die PIN muss mindestens 4 Ziffern lang sein!"
)
err.open() err.open()
return return
popup = NumpadPopup( popup = NumpadPopup(
title="PIN bestätigen", title="PIN bestätigen",
is_password=True, is_password=True,
on_submit=lambda confirm_pin: self.save_new_pin(user_id, pin, confirm_pin) on_submit=lambda confirm_pin: self.save_new_pin(user_id, pin, confirm_pin),
) )
popup.open() popup.open()
@@ -1048,7 +1071,7 @@ class UserSelectionScreen(StyledScreen):
app = App.get_running_app() app = App.get_running_app()
app.tracker.set_user_pin(user_id, pin) app.tracker.set_user_pin(user_id, pin)
app.current_user = user_id app.current_user = user_id
self.manager.current = 'user_menu' self.manager.current = "user_menu"
def verify_pin(self, user_id, pin): def verify_pin(self, user_id, pin):
app = App.get_running_app() app = App.get_running_app()
@@ -1056,7 +1079,7 @@ class UserSelectionScreen(StyledScreen):
if user.pin == pin: if user.pin == pin:
app.current_user = user_id app.current_user = user_id
self.manager.current = 'user_menu' self.manager.current = "user_menu"
else: else:
err = MessagePopup(title="Fehler", message="Falsche PIN!") err = MessagePopup(title="Fehler", message="Falsche PIN!")
err.open() err.open()
@@ -1125,7 +1148,7 @@ class UserMenuScreen(StyledScreen):
popup = NumpadPopup( popup = NumpadPopup(
title="Aktuelle PIN eingeben", title="Aktuelle PIN eingeben",
is_password=True, is_password=True,
on_submit=lambda pin: self.verify_current_pin(pin) on_submit=lambda pin: self.verify_current_pin(pin),
) )
popup.open() popup.open()
@@ -1137,7 +1160,7 @@ class UserMenuScreen(StyledScreen):
popup = NumpadPopup( popup = NumpadPopup(
title="Neue PIN eingeben", title="Neue PIN eingeben",
is_password=True, is_password=True,
on_submit=lambda new_pin: self.setup_new_pin(new_pin) on_submit=lambda new_pin: self.setup_new_pin(new_pin),
) )
popup.open() popup.open()
else: else:
@@ -1146,14 +1169,16 @@ class UserMenuScreen(StyledScreen):
def setup_new_pin(self, new_pin): def setup_new_pin(self, new_pin):
if len(new_pin) < 4: if len(new_pin) < 4:
err = MessagePopup(title="Fehler", message="Die PIN muss mindestens 4 Ziffern lang sein!") err = MessagePopup(
title="Fehler", message="Die PIN muss mindestens 4 Ziffern lang sein!"
)
err.open() err.open()
return return
popup = NumpadPopup( popup = NumpadPopup(
title="Neue PIN bestätigen", title="Neue PIN bestätigen",
is_password=True, is_password=True,
on_submit=lambda confirm_pin: self.save_changed_pin(new_pin, confirm_pin) on_submit=lambda confirm_pin: self.save_changed_pin(new_pin, confirm_pin),
) )
popup.open() popup.open()
@@ -1374,7 +1399,7 @@ class AdminPasswordScreen(StyledScreen):
popup = KeyboardPopup( popup = KeyboardPopup(
title="Admin-Passwort eingeben", title="Admin-Passwort eingeben",
is_password=True, is_password=True,
on_submit=self.on_password_submitted on_submit=self.on_password_submitted,
) )
popup.open() popup.open()
@@ -1494,7 +1519,6 @@ class AdminMenuScreen(StyledScreen):
self.manager.current = "main_menu" self.manager.current = "main_menu"
class AdminAdjustDebtScreen(StyledScreen): class AdminAdjustDebtScreen(StyledScreen):
def __init__(self, **kwargs): def __init__(self, **kwargs):
super(AdminAdjustDebtScreen, self).__init__( super(AdminAdjustDebtScreen, self).__init__(
@@ -1666,8 +1690,8 @@ class AdminHistoryScreen(StyledScreen):
user = app.tracker.get_user_by_id(t.user_id) user = app.tracker.get_user_by_id(t.user_id)
user_name = user.name if user else "Gelöschter Benutzer" user_name = user.name if user else "Gelöschter Benutzer"
if t.item_id == "adjustment": if t.item_id in ("adjustment", "Abbuchung"):
item_name = "Anpassung" item_name = "Abbuchung"
else: else:
item = app.tracker.get_items().get(t.item_id) item = app.tracker.get_items().get(t.item_id)
item_name = item.name if item else "Gelöschter Artikel" item_name = item.name if item else "Gelöschter Artikel"
@@ -1695,7 +1719,7 @@ class AdminManageUsersScreen(StyledScreen):
size_hint=(0.7, None), size_hint=(0.7, None),
height="50dp", height="50dp",
pos_hint={"center_x": 0.5}, pos_hint={"center_x": 0.5},
btn_color=[0.0, 0.65, 0.57, 1] btn_color=[0.0, 0.65, 0.57, 1],
) )
btn_add.bind(on_release=self.open_create_user_popup) btn_add.bind(on_release=self.open_create_user_popup)
self.content_layout.add_widget(btn_add) self.content_layout.add_widget(btn_add)
@@ -1738,6 +1762,7 @@ class AdminManageUsersScreen(StyledScreen):
def create_user(self, name): def create_user(self, name):
import uuid import uuid
app = App.get_running_app() app = App.get_running_app()
user_id = f"user_{uuid.uuid4().hex[:8]}" user_id = f"user_{uuid.uuid4().hex[:8]}"
res = app.tracker.add_user(user_id, name) res = app.tracker.add_user(user_id, name)
@@ -1745,15 +1770,12 @@ class AdminManageUsersScreen(StyledScreen):
if res["status"] == "success": if res["status"] == "success":
msg = MessagePopup( msg = MessagePopup(
title="Erfolg", title="Erfolg",
message=f"Benutzer {name} erfolgreich erstellt!\n(Standard-PIN: 4242)" message=f"Benutzer {name} erfolgreich erstellt!\n(Standard-PIN: 4242)",
) )
msg.open() msg.open()
self.refresh_users() self.refresh_users()
else: else:
msg = MessagePopup( msg = MessagePopup(title="Fehler", message=f"Fehler: {res['message']}")
title="Fehler",
message=f"Fehler: {res['message']}"
)
msg.open() msg.open()
def open_edit_user_popup(self, user_id, name, pin): def open_edit_user_popup(self, user_id, name, pin):
@@ -1762,7 +1784,7 @@ class AdminManageUsersScreen(StyledScreen):
current_name=name, current_name=name,
current_pin=pin, current_pin=pin,
on_save=self.save_user, on_save=self.save_user,
on_delete=self.confirm_delete_user on_delete=self.confirm_delete_user,
) )
popup.open() popup.open()
@@ -1785,7 +1807,7 @@ class AdminManageUsersScreen(StyledScreen):
popup = ConfirmPopup( popup = ConfirmPopup(
title="Benutzer löschen", title="Benutzer löschen",
message=f"Möchtest du den Benutzer {user.name} wirklich unwiderruflich löschen?", message=f"Möchtest du den Benutzer {user.name} wirklich unwiderruflich löschen?",
on_confirm=lambda: self.delete_user(user_id) on_confirm=lambda: self.delete_user(user_id),
) )
popup.open() popup.open()
@@ -1815,7 +1837,7 @@ class AdminManageItemsScreen(StyledScreen):
size_hint=(0.7, None), size_hint=(0.7, None),
height="50dp", height="50dp",
pos_hint={"center_x": 0.5}, pos_hint={"center_x": 0.5},
btn_color=[0.0, 0.65, 0.57, 1] btn_color=[0.0, 0.65, 0.57, 1],
) )
btn_add.bind(on_release=self.open_create_item_popup) btn_add.bind(on_release=self.open_create_item_popup)
self.content_layout.add_widget(btn_add) self.content_layout.add_widget(btn_add)
@@ -1858,12 +1880,15 @@ class AdminManageItemsScreen(StyledScreen):
def create_item(self, name, price): def create_item(self, name, price):
import uuid import uuid
app = App.get_running_app() app = App.get_running_app()
item_id = f"item_{uuid.uuid4().hex[:8]}" item_id = f"item_{uuid.uuid4().hex[:8]}"
res = app.tracker.add_item(item_id, name, price) res = app.tracker.add_item(item_id, name, price)
if res["status"] == "success": if res["status"] == "success":
msg = MessagePopup(title="Erfolg", message=f"Artikel {name} erfolgreich erstellt!") msg = MessagePopup(
title="Erfolg", message=f"Artikel {name} erfolgreich erstellt!"
)
msg.open() msg.open()
self.refresh_items() self.refresh_items()
else: else:
@@ -1876,7 +1901,7 @@ class AdminManageItemsScreen(StyledScreen):
current_name=name, current_name=name,
current_price=price, current_price=price,
on_save=self.save_item, on_save=self.save_item,
on_delete=self.confirm_delete_item on_delete=self.confirm_delete_item,
) )
popup.open() popup.open()
@@ -1900,7 +1925,7 @@ class AdminManageItemsScreen(StyledScreen):
popup = ConfirmPopup( popup = ConfirmPopup(
title="Artikel löschen", title="Artikel löschen",
message=f"Möchtest du den Artikel {item.name} wirklich unwiderruflich löschen?", message=f"Möchtest du den Artikel {item.name} wirklich unwiderruflich löschen?",
on_confirm=lambda: self.delete_item(item_id) on_confirm=lambda: self.delete_item(item_id),
) )
popup.open() popup.open()
@@ -1946,7 +1971,6 @@ class KuehlschrankApp(App):
return sm return sm
class KivyUI: class KivyUI:
def __init__(self, tracker): def __init__(self, tracker):
self.tracker = tracker self.tracker = tracker
+34
View File
@@ -0,0 +1,34 @@
import re
def format_currency(amount: float, symbol: str = "", is_german: bool = True) -> str:
"""Formats a numeric amount to a currency string representation."""
if is_german:
return f"{amount:.2f}".replace(".", ",") + f" {symbol}"
else:
return f"{symbol}{amount:.2f}"
def format_date(iso_timestamp: str) -> str:
"""Converts ISO timestamp to German date format DD.MM.YYYY."""
try:
date_part = iso_timestamp.split("T")[0]
parts = date_part.split("-")
if len(parts) == 3 and all(p.isdigit() for p in parts) and len(parts[0]) == 4:
return f"{parts[2]}.{parts[1]}.{parts[0]}"
except Exception:
pass
return iso_timestamp[:10]
def validate_pin(pin: str) -> bool:
"""Validates that a PIN consists of at least 4 numeric digits."""
return bool(re.match(r"^\d{4,}$", pin))
def parse_price(price_str: str) -> float:
"""Parses a string containing a price (handling comma and dot) to a float.
Raises ValueError if invalid.
"""
clean_str = price_str.replace(",", ".").strip()
price = float(clean_str)
if price < 0:
raise ValueError("Price cannot be negative")
return price