Tests and helpers
This commit is contained in:
+10
-4
@@ -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"}
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user