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 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"}
+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 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()
+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):
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}"
)
+133 -109
View File
@@ -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
+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