diff --git a/core/tracker.py b/core/tracker.py index 3cbf6da..acfe0df 100644 --- a/core/tracker.py +++ b/core/tracker.py @@ -1,5 +1,6 @@ from uuid import uuid4 from core.transaction import Transaction +from utils.helpers import validate_pin, parse_price class Tracker: @@ -70,7 +71,7 @@ class Tracker: from uuid import uuid4 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.data_manager.save_transactions(self.transactions) @@ -97,12 +98,15 @@ class Tracker: def set_user_pin(self, user_id, pin): if user_id not in self.users: 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.data_manager.save_users(self.users) return {"status": "success", "message": "PIN updated successfully"} def add_user(self, user_id, name): from core.user import User + if user_id in self.users: return {"status": "error", "message": "User ID already exists"} self.users[user_id] = User(user_id, name, "4242") @@ -123,6 +127,8 @@ class Tracker: def edit_user(self, user_id, name, pin=None): if user_id not in self.users: 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 if pin is not None: self.users[user_id].pin = pin @@ -131,10 +137,11 @@ class Tracker: def add_item(self, item_id, name, price): from core.item import Item + if item_id in self.items: return {"status": "error", "message": "Item ID already exists"} try: - price_val = float(price) + price_val = parse_price(str(price)) except ValueError: return {"status": "error", "message": "Invalid price value"} self.items[item_id] = Item(item_id, name, price_val) @@ -153,11 +160,10 @@ class Tracker: if item_id not in self.items: return {"status": "error", "message": "Item not found"} try: - price_val = float(price) + price_val = parse_price(str(price)) except ValueError: return {"status": "error", "message": "Invalid price value"} self.items[item_id].name = name self.items[item_id].price = price_val self.data_manager.save_items(self.items) return {"status": "success", "message": f"Item {name} updated successfully"} - diff --git a/tests/test_helpers.py b/tests/test_helpers.py new file mode 100644 index 0000000..02728ae --- /dev/null +++ b/tests/test_helpers.py @@ -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() diff --git a/tests/test_item.py b/tests/test_item.py new file mode 100644 index 0000000..3fda76d --- /dev/null +++ b/tests/test_item.py @@ -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() diff --git a/tests/test_tracker.py b/tests/test_tracker.py index c24a350..1c49f20 100644 --- a/tests/test_tracker.py +++ b/tests/test_tracker.py @@ -6,7 +6,6 @@ from core.item import Item import shutil import os - class TestTracker(unittest.TestCase): def setUp(self): self.test_dir = "test_data" @@ -24,7 +23,6 @@ class TestTracker(unittest.TestCase): shutil.rmtree(self.test_dir) 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") self.assertEqual(res["status"], "success") 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(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): res = self.tracker.add_user("test_user_2", "Test Bob") self.assertEqual(res["status"], "success") self.assertEqual(self.tracker.users["test_user_2"].name, "Test Bob") self.assertEqual(self.tracker.users["test_user_2"].pin, "4242") + def test_remove_user(self): res = self.tracker.remove_user("test_user") 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"].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): res = self.tracker.add_item("new_item", "Donut", 1.80) self.assertEqual(res["status"], "success") self.assertEqual(self.tracker.items["new_item"].name, "Donut") 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): res = self.tracker.remove_item("test_item") 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"].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__": unittest.main() - diff --git a/tests/test_transaction.py b/tests/test_transaction.py new file mode 100644 index 0000000..09a20c2 --- /dev/null +++ b/tests/test_transaction.py @@ -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() diff --git a/tests/test_user.py b/tests/test_user.py new file mode 100644 index 0000000..013f4a9 --- /dev/null +++ b/tests/test_user.py @@ -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() diff --git a/ui/cli.py b/ui/cli.py index a45b700..9028290 100644 --- a/ui/cli.py +++ b/ui/cli.py @@ -364,7 +364,7 @@ class CLI: for t in sorted(transactions, key=lambda x: x.timestamp, reverse=True): user = self.tracker.get_user_by_id(t.user_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( f"{t.timestamp[:10]} | {user.name:.<20} {item_name:.<15} ${t.amount:.2f}" ) diff --git a/ui/kivy.py b/ui/kivy.py index 994e244..35765c4 100644 --- a/ui/kivy.py +++ b/ui/kivy.py @@ -1,6 +1,7 @@ -# type: ignore +## type: ignore import kivy from kivy.app import App +from kivy.graphics import Color, RoundedRectangle from kivy.uix.screenmanager import ScreenManager, Screen, SlideTransition from kivy.uix.boxlayout import BoxLayout 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.popup import Popup 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): @@ -104,45 +105,57 @@ class ConfirmPopup(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_float = is_float self.on_submit = on_submit self.entered_text = "" - content = BoxLayout(orientation='vertical', padding=15, spacing=15) + content = BoxLayout(orientation="vertical", padding=15, spacing=15) self.display = Label( text="PIN eingeben:" if is_password else "Betrag eingeben:", - font_size='20sp', + font_size="20sp", bold=True, size_hint_y=0.15, - halign='center', - valign='middle' + halign="center", + 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) grid = GridLayout(cols=3, spacing=10, size_hint_y=0.6) buttons = [ - '1', '2', '3', - '4', '5', '6', - '7', '8', '9', - ',' if is_float else 'C', '0', '⌫' + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "," if is_float else "C", + "0", + "⌫", ] for btn_text in buttons: btn = RoundedButton( 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) grid.add_widget(btn) 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]) cancel_btn = RoundedButton(text="Abbrechen", btn_color=[0.25, 0.25, 0.28, 1]) @@ -154,10 +167,10 @@ class NumpadPopup(Popup): title=title, content=content, size_hint=(0.85, 0.75), - title_align='center', - title_size='18sp', + title_align="center", + title_size="18sp", background_color=[0.08, 0.08, 0.09, 0.95], - **kwargs + **kwargs, ) ok_btn.bind(on_release=self.submit) @@ -165,20 +178,22 @@ class NumpadPopup(Popup): def on_key_press(self, instance): key = instance.text - if key == '⌫': + if key == "⌫": self.entered_text = self.entered_text[:-1] - elif key == 'C': + elif key == "C": self.entered_text = "" - elif key == ',': - if ',' not in self.entered_text: - self.entered_text += ',' + elif key == ",": + if "," not in self.entered_text: + self.entered_text += "," else: self.entered_text += key self.update_display() def update_display(self): 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: if self.is_password: self.display.text = "*" * len(self.entered_text) @@ -197,34 +212,36 @@ class KeyboardPopup(Popup): self.on_submit = on_submit self.entered_text = "" - content = BoxLayout(orientation='vertical', padding=15, spacing=15) + content = BoxLayout(orientation="vertical", padding=15, spacing=15) self.display = Label( text="Eingeben:" if not is_password else "****", - font_size='20sp', + font_size="20sp", bold=True, size_hint_y=0.15, - halign='center', - valign='middle' + halign="center", + 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) - 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 = [ - ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'], - ['q', 'w', 'e', 'r', 't', 'z', 'u', 'i', 'o', 'p'], - ['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l'], - ['y', 'x', 'c', 'v', 'b', 'n', 'm', '⌫', 'C'] + ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"], + ["q", "w", "e", "r", "t", "z", "u", "i", "o", "p"], + ["a", "s", "d", "f", "g", "h", "j", "k", "l"], + ["y", "x", "c", "v", "b", "n", "m", "⌫", "C"], ] for row in rows: - row_box = BoxLayout(orientation='horizontal', spacing=5) + row_box = BoxLayout(orientation="horizontal", spacing=5) for char in row: btn = RoundedButton( 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) row_box.add_widget(btn) @@ -232,7 +249,7 @@ class KeyboardPopup(Popup): 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]) cancel_btn = RoundedButton(text="Abbrechen", btn_color=[0.25, 0.25, 0.28, 1]) @@ -244,10 +261,10 @@ class KeyboardPopup(Popup): title=title, content=content, size_hint=(0.95, 0.8), - title_align='center', - title_size='18sp', + title_align="center", + title_size="18sp", background_color=[0.08, 0.08, 0.09, 0.95], - **kwargs + **kwargs, ) ok_btn.bind(on_release=self.submit) @@ -255,9 +272,9 @@ class KeyboardPopup(Popup): def on_key_press(self, instance): key = instance.text - if key == '⌫': + if key == "⌫": self.entered_text = self.entered_text[:-1] - elif key == 'C': + elif key == "C": self.entered_text = "" else: self.entered_text += key @@ -338,7 +355,7 @@ class AdjustDebtPopup(Popup): title="Betrag eingeben", is_password=False, is_float=True, - on_submit=self.on_amount_submitted + on_submit=self.on_amount_submitted, ) popup.open() @@ -362,16 +379,16 @@ class CreateUserPopup(Popup): def __init__(self, on_create, **kwargs): self.on_create = on_create - content = BoxLayout(orientation='vertical', padding=15, spacing=15) + content = BoxLayout(orientation="vertical", padding=15, spacing=15) info = Label( text="Name des neuen Benutzers eingeben:", - font_size='16sp', + font_size="16sp", size_hint_y=0.25, - halign='center', - valign='middle' + halign="center", + valign="middle", ) - info.bind(size=info.setter('text_size')) + info.bind(size=info.setter("text_size")) content.add_widget(info) self.name_input = TextInput( @@ -384,7 +401,7 @@ class CreateUserPopup(Popup): foreground_color=[1, 1, 1, 1], cursor_color=[1, 1, 1, 1], size_hint_y=0.3, - padding=[10, 10] + padding=[10, 10], ) self.name_input.bind(focus=self.trigger_keyboard) content.add_widget(self.name_input) @@ -417,7 +434,7 @@ class CreateUserPopup(Popup): popup = KeyboardPopup( title="Name eingeben", is_password=False, - on_submit=self.on_name_submitted + on_submit=self.on_name_submitted, ) popup.open() @@ -435,21 +452,23 @@ class CreateUserPopup(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.on_save = on_save self.on_delete = on_delete - content = BoxLayout(orientation='vertical', padding=15, spacing=15) + content = BoxLayout(orientation="vertical", padding=15, spacing=15) info = Label( text="Benutzer bearbeiten:", - font_size='16sp', + font_size="16sp", size_hint_y=0.15, - halign='center', - valign='middle' + halign="center", + valign="middle", ) - info.bind(size=info.setter('text_size')) + info.bind(size=info.setter("text_size")) content.add_widget(info) 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], cursor_color=[1, 1, 1, 1], size_hint_y=0.15, - padding=[10, 10] + padding=[10, 10], ) self.name_input.bind(focus=self.trigger_keyboard) content.add_widget(self.name_input) @@ -479,7 +498,7 @@ class EditUserPopup(Popup): foreground_color=[1, 1, 1, 1], cursor_color=[1, 1, 1, 1], size_hint_y=0.15, - padding=[10, 10] + padding=[10, 10], ) self.pin_input.bind(focus=self.trigger_numpad) content.add_widget(self.pin_input) @@ -515,7 +534,7 @@ class EditUserPopup(Popup): popup = KeyboardPopup( title="Name eingeben", is_password=False, - on_submit=self.on_name_submitted + on_submit=self.on_name_submitted, ) popup.open() @@ -526,9 +545,7 @@ class EditUserPopup(Popup): if value: instance.focus = False popup = NumpadPopup( - title="PIN eingeben", - is_password=False, - on_submit=self.on_pin_submitted + title="PIN eingeben", is_password=False, on_submit=self.on_pin_submitted ) popup.open() @@ -543,7 +560,9 @@ class EditUserPopup(Popup): err.open() return 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() return self.dismiss() @@ -558,16 +577,16 @@ class CreateItemPopup(Popup): def __init__(self, on_create, **kwargs): self.on_create = on_create - content = BoxLayout(orientation='vertical', padding=15, spacing=15) + content = BoxLayout(orientation="vertical", padding=15, spacing=15) info = Label( text="Neuen Artikel hinzufügen:", - font_size='16sp', + font_size="16sp", size_hint_y=0.15, - halign='center', - valign='middle' + halign="center", + valign="middle", ) - info.bind(size=info.setter('text_size')) + info.bind(size=info.setter("text_size")) content.add_widget(info) 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], cursor_color=[1, 1, 1, 1], size_hint_y=0.15, - padding=[10, 10] + padding=[10, 10], ) self.name_input.bind(focus=self.trigger_keyboard) content.add_widget(self.name_input) @@ -597,7 +616,7 @@ class CreateItemPopup(Popup): foreground_color=[1, 1, 1, 1], cursor_color=[1, 1, 1, 1], size_hint_y=0.15, - padding=[10, 10] + padding=[10, 10], ) self.price_input.bind(focus=self.trigger_numpad) content.add_widget(self.price_input) @@ -630,7 +649,7 @@ class CreateItemPopup(Popup): popup = KeyboardPopup( title="Artikelname eingeben", is_password=False, - on_submit=self.on_name_submitted + on_submit=self.on_name_submitted, ) popup.open() @@ -644,7 +663,7 @@ class CreateItemPopup(Popup): title="Preis eingeben", is_password=False, is_float=True, - on_submit=self.on_price_submitted + on_submit=self.on_price_submitted, ) popup.open() @@ -672,21 +691,23 @@ class CreateItemPopup(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.on_save = on_save self.on_delete = on_delete - content = BoxLayout(orientation='vertical', padding=15, spacing=15) + content = BoxLayout(orientation="vertical", padding=15, spacing=15) info = Label( text="Artikel bearbeiten:", - font_size='16sp', + font_size="16sp", size_hint_y=0.15, - halign='center', - valign='middle' + halign="center", + valign="middle", ) - info.bind(size=info.setter('text_size')) + info.bind(size=info.setter("text_size")) content.add_widget(info) 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], cursor_color=[1, 1, 1, 1], size_hint_y=0.15, - padding=[10, 10] + padding=[10, 10], ) self.name_input.bind(focus=self.trigger_keyboard) content.add_widget(self.name_input) @@ -716,7 +737,7 @@ class EditItemPopup(Popup): foreground_color=[1, 1, 1, 1], cursor_color=[1, 1, 1, 1], size_hint_y=0.15, - padding=[10, 10] + padding=[10, 10], ) self.price_input.bind(focus=self.trigger_numpad) content.add_widget(self.price_input) @@ -752,7 +773,7 @@ class EditItemPopup(Popup): popup = KeyboardPopup( title="Artikelname eingeben", is_password=False, - on_submit=self.on_name_submitted + on_submit=self.on_name_submitted, ) popup.open() @@ -766,7 +787,7 @@ class EditItemPopup(Popup): title="Preis eingeben", is_password=False, is_float=True, - on_submit=self.on_price_submitted + on_submit=self.on_price_submitted, ) popup.open() @@ -847,7 +868,7 @@ class TransactionRow(BoxLayout): self.bind(pos=self.update_rect, size=self.update_rect) date_lbl = Label( - text=date[:10], + text=format_date(date), font_size="14sp", size_hint_x=0.25, halign="left", @@ -1015,27 +1036,29 @@ class UserSelectionScreen(StyledScreen): popup = NumpadPopup( title=f"PIN einrichten: {user.name}", 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() else: popup = NumpadPopup( title=f"PIN eingeben: {user.name}", 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() def setup_pin_confirm(self, user_id, pin): 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() return popup = NumpadPopup( title="PIN bestätigen", 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() @@ -1048,7 +1071,7 @@ class UserSelectionScreen(StyledScreen): app = App.get_running_app() app.tracker.set_user_pin(user_id, pin) app.current_user = user_id - self.manager.current = 'user_menu' + self.manager.current = "user_menu" def verify_pin(self, user_id, pin): app = App.get_running_app() @@ -1056,7 +1079,7 @@ class UserSelectionScreen(StyledScreen): if user.pin == pin: app.current_user = user_id - self.manager.current = 'user_menu' + self.manager.current = "user_menu" else: err = MessagePopup(title="Fehler", message="Falsche PIN!") err.open() @@ -1125,7 +1148,7 @@ class UserMenuScreen(StyledScreen): popup = NumpadPopup( title="Aktuelle PIN eingeben", is_password=True, - on_submit=lambda pin: self.verify_current_pin(pin) + on_submit=lambda pin: self.verify_current_pin(pin), ) popup.open() @@ -1137,7 +1160,7 @@ class UserMenuScreen(StyledScreen): popup = NumpadPopup( title="Neue PIN eingeben", 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() else: @@ -1146,14 +1169,16 @@ class UserMenuScreen(StyledScreen): def setup_new_pin(self, new_pin): 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() return popup = NumpadPopup( title="Neue PIN bestätigen", 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() @@ -1374,7 +1399,7 @@ class AdminPasswordScreen(StyledScreen): popup = KeyboardPopup( title="Admin-Passwort eingeben", is_password=True, - on_submit=self.on_password_submitted + on_submit=self.on_password_submitted, ) popup.open() @@ -1494,7 +1519,6 @@ class AdminMenuScreen(StyledScreen): self.manager.current = "main_menu" - class AdminAdjustDebtScreen(StyledScreen): def __init__(self, **kwargs): super(AdminAdjustDebtScreen, self).__init__( @@ -1666,8 +1690,8 @@ class AdminHistoryScreen(StyledScreen): user = app.tracker.get_user_by_id(t.user_id) user_name = user.name if user else "Gelöschter Benutzer" - if t.item_id == "adjustment": - item_name = "Anpassung" + if t.item_id in ("adjustment", "Abbuchung"): + item_name = "Abbuchung" else: item = app.tracker.get_items().get(t.item_id) item_name = item.name if item else "Gelöschter Artikel" @@ -1695,7 +1719,7 @@ class AdminManageUsersScreen(StyledScreen): size_hint=(0.7, None), height="50dp", 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) self.content_layout.add_widget(btn_add) @@ -1738,6 +1762,7 @@ class AdminManageUsersScreen(StyledScreen): def create_user(self, name): import uuid + app = App.get_running_app() user_id = f"user_{uuid.uuid4().hex[:8]}" res = app.tracker.add_user(user_id, name) @@ -1745,15 +1770,12 @@ class AdminManageUsersScreen(StyledScreen): if res["status"] == "success": msg = MessagePopup( title="Erfolg", - message=f"Benutzer {name} erfolgreich erstellt!\n(Standard-PIN: 4242)" + message=f"Benutzer {name} erfolgreich erstellt!\n(Standard-PIN: 4242)", ) msg.open() self.refresh_users() else: - msg = MessagePopup( - title="Fehler", - message=f"Fehler: {res['message']}" - ) + msg = MessagePopup(title="Fehler", message=f"Fehler: {res['message']}") msg.open() def open_edit_user_popup(self, user_id, name, pin): @@ -1762,7 +1784,7 @@ class AdminManageUsersScreen(StyledScreen): current_name=name, current_pin=pin, on_save=self.save_user, - on_delete=self.confirm_delete_user + on_delete=self.confirm_delete_user, ) popup.open() @@ -1785,7 +1807,7 @@ class AdminManageUsersScreen(StyledScreen): popup = ConfirmPopup( title="Benutzer 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() @@ -1815,7 +1837,7 @@ class AdminManageItemsScreen(StyledScreen): size_hint=(0.7, None), height="50dp", 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) self.content_layout.add_widget(btn_add) @@ -1858,12 +1880,15 @@ class AdminManageItemsScreen(StyledScreen): def create_item(self, name, price): import uuid + app = App.get_running_app() item_id = f"item_{uuid.uuid4().hex[:8]}" res = app.tracker.add_item(item_id, name, price) 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() self.refresh_items() else: @@ -1876,7 +1901,7 @@ class AdminManageItemsScreen(StyledScreen): current_name=name, current_price=price, on_save=self.save_item, - on_delete=self.confirm_delete_item + on_delete=self.confirm_delete_item, ) popup.open() @@ -1900,7 +1925,7 @@ class AdminManageItemsScreen(StyledScreen): popup = ConfirmPopup( title="Artikel 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() @@ -1946,7 +1971,6 @@ class KuehlschrankApp(App): return sm - class KivyUI: def __init__(self, tracker): self.tracker = tracker diff --git a/utils/helpers.py b/utils/helpers.py new file mode 100644 index 0000000..8ab00ce --- /dev/null +++ b/utils/helpers.py @@ -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