This commit is contained in:
tcprod 2025-02-03 14:32:16 +01:00
parent 98c2d1570c
commit 0ca06c244c
17 changed files with 210 additions and 146 deletions

View File

@ -24,7 +24,7 @@ class CateringCartItem(Component):
def build(self) -> rio.Component: def build(self) -> rio.Component:
return Row( return Row(
Text(self.ellipsize_string(self.article_name), align_x=0, overflow="wrap", min_width=19, style=TextStyle(fill=self.session.theme.background_color, font_size=0.9)), Text(self.ellipsize_string(self.article_name), align_x=0, overflow="wrap", min_width=19, style=TextStyle(fill=self.session.theme.background_color, font_size=0.9)),
Text(AccountingService.make_euro_string_from_int(self.article_price), style=TextStyle(fill=self.session.theme.background_color, font_size=0.9)), Text(AccountingService.make_euro_string_from_decimal(self.article_price), style=TextStyle(fill=self.session.theme.background_color, font_size=0.9)),
IconButton(icon="material/close", min_size=2, color=self.session.theme.danger_color, style="plain-text", on_press=lambda: self.remove_item_cb(self.list_id)), IconButton(icon="material/close", min_size=2, color=self.session.theme.danger_color, style="plain-text", on_press=lambda: self.remove_item_cb(self.list_id)),
proportions=(19, 5, 2) proportions=(19, 5, 2)
) )

View File

@ -41,7 +41,7 @@ class CateringSelectionItem(Component):
content=Column( content=Column(
Row( Row(
Text(article_name_top, align_x=0, overflow="wrap", min_width=19, style=TextStyle(fill=self.session.theme.background_color, font_size=0.9)), Text(article_name_top, align_x=0, overflow="wrap", min_width=19, style=TextStyle(fill=self.session.theme.background_color, font_size=0.9)),
Text(AccountingService.make_euro_string_from_int(self.article_price), style=TextStyle(fill=self.session.theme.background_color, font_size=0.9)), Text(AccountingService.make_euro_string_from_decimal(self.article_price), style=TextStyle(fill=self.session.theme.background_color, font_size=0.9)),
IconButton( IconButton(
icon="material/add", icon="material/add",
min_size=2, min_size=2,

View File

@ -1,4 +1,5 @@
from datetime import datetime from datetime import datetime
from decimal import Decimal
from typing import Optional from typing import Optional
from rio import Component, Column, NumberInput, ThemeContextSwitcher, TextInput, Row, Button, EventHandler from rio import Component, Column, NumberInput, ThemeContextSwitcher, TextInput, Row, Button, EventHandler
@ -18,7 +19,7 @@ class NewTransactionForm(Component):
self.new_transaction_cb, self.new_transaction_cb,
Transaction( Transaction(
user_id=self.user.user_id, user_id=self.user.user_id,
value=round(self.input_value * 100), value=Decimal(str(self.input_value)),
is_debit=True, is_debit=True,
reference=self.input_reason, reference=self.input_reason,
transaction_date=datetime.now() transaction_date=datetime.now()
@ -30,7 +31,7 @@ class NewTransactionForm(Component):
self.new_transaction_cb, self.new_transaction_cb,
Transaction( Transaction(
user_id=self.user.user_id, user_id=self.user.user_id,
value=round(self.input_value * 100), value=Decimal(str(self.input_value)),
is_debit=False, is_debit=False,
reference=self.input_reason, reference=self.input_reason,
transaction_date=datetime.now() transaction_date=datetime.now()

View File

@ -96,7 +96,7 @@ class ShoppingCartAndOrders(Component):
{ {
"Artikel": [item.name for item in order.items.keys()] + ["Gesamtpreis:"], "Artikel": [item.name for item in order.items.keys()] + ["Gesamtpreis:"],
"Anzahl": [item for item in order.items.values()] + [""], "Anzahl": [item for item in order.items.values()] + [""],
"Preis": [AccountingService.make_euro_string_from_int(item.price) for item in order.items.keys()] + [AccountingService.make_euro_string_from_int(order.price)], "Preis": [AccountingService.make_euro_string_from_decimal(item.price) for item in order.items.keys()] + [AccountingService.make_euro_string_from_decimal(order.price)],
}, },
show_row_numbers=False show_row_numbers=False
) )
@ -158,7 +158,7 @@ class ShoppingCartAndOrders(Component):
), ),
Row( Row(
Text( Text(
text=f"Preis: {AccountingService.make_euro_string_from_int(sum(cart_item.price for cart_item in cart))}", text=f"Preis: {AccountingService.make_euro_string_from_decimal(sum(cart_item.price for cart_item in cart))}",
style=TextStyle( style=TextStyle(
fill=self.session.theme.background_color, fill=self.session.theme.background_color,
font_size=0.8 font_size=0.8

View File

@ -67,7 +67,7 @@ class TicketBuyCard(Component):
margin_right=1 margin_right=1
), ),
Row( Row(
Text(f"{AccountingService.make_euro_string_from_int(self.price)}", margin_left=1, margin_top=1, grow_x=True), Text(f"{AccountingService.make_euro_string_from_decimal(self.price)}", margin_left=1, margin_top=1, grow_x=True),
Button( Button(
Text("Kaufen", align_x=0.5, margin=0.4), Text("Kaufen", align_x=0.5, margin=0.4),
margin_right=1, margin_right=1,

View File

@ -91,7 +91,7 @@ class UserInfoBox(Component):
grow_y=False grow_y=False
), ),
UserInfoBoxButton("Profil bearbeiten", "./edit-profile"), UserInfoBoxButton("Profil bearbeiten", "./edit-profile"),
UserInfoBoxButton(f"Guthaben: {self.session[AccountingService].make_euro_string_from_int(self.user_balance)}", "./account"), UserInfoBoxButton(f"Guthaben: {self.session[AccountingService].make_euro_string_from_decimal(self.user_balance)}", "./account"),
Button( Button(
content=Text("Ausloggen", style=TextStyle(fill=Color.from_hex("02dac5"), font_size=0.6)), content=Text("Ausloggen", style=TextStyle(fill=Color.from_hex("02dac5"), font_size=0.6)),
shape="rectangle", shape="rectangle",

View File

@ -1,6 +1,7 @@
# USE THIS ON AN EMPTY DATABASE TO GENERATE DEMO DATA # USE THIS ON AN EMPTY DATABASE TO GENERATE DEMO DATA
import asyncio import asyncio
from datetime import date from datetime import date
from decimal import Decimal
import sys import sys
@ -9,13 +10,16 @@ from src.ez_lan_manager.types.CateringMenuItem import CateringMenuItemCategory
from src.ez_lan_manager.types.News import News from src.ez_lan_manager.types.News import News
DEMO_USERS = [ DEMO_USERS = [
{ "user_name": "manfred", "user_mail": "manfred@demomail.com", "password_clear_text": "manfred" }, # Gast {"user_name": "manfred", "user_mail": "manfred@demomail.com", "password_clear_text": "manfred"}, # Gast
{ "user_name": "gustav", "user_mail": "gustav@demomail.com", "password_clear_text": "gustav" }, # Gast + Ticket(NORMAL) {"user_name": "gustav", "user_mail": "gustav@demomail.com", "password_clear_text": "gustav"},
{ "user_name": "jason", "user_mail": "juergen@demomail.com", "password_clear_text": "jason" }, # Gast + Ticket(NORMAL) + Sitzplatz # Gast + Ticket(NORMAL)
{ "user_name": "lisa", "user_mail": "lisa@demomail.com", "password_clear_text": "lisa" }, # Teamler {"user_name": "jason", "user_mail": "juergen@demomail.com", "password_clear_text": "jason"},
{ "user_name": "thomas", "user_mail": "thomas@demomail.com", "password_clear_text": "thomas" } # Teamler + Admin # Gast + Ticket(NORMAL) + Sitzplatz
{"user_name": "lisa", "user_mail": "lisa@demomail.com", "password_clear_text": "lisa"}, # Teamler
{"user_name": "thomas", "user_mail": "thomas@demomail.com", "password_clear_text": "thomas"} # Teamler + Admin
] ]
async def run() -> None: async def run() -> None:
services = init_services() services = init_services()
await services[3].init_db_pool() await services[3].init_db_pool()
@ -31,38 +35,47 @@ async def run() -> None:
if not input("Generate users? (Y/n): ").lower() == "n": if not input("Generate users? (Y/n): ").lower() == "n":
# MANFRED # MANFRED
manfred = await user_service.create_user(DEMO_USERS[0]["user_name"], DEMO_USERS[0]["user_mail"], DEMO_USERS[0]["password_clear_text"]) manfred = await user_service.create_user(DEMO_USERS[0]["user_name"], DEMO_USERS[0]["user_mail"],
DEMO_USERS[0]["password_clear_text"])
# GUSTAV # GUSTAV
gustav = await user_service.create_user(DEMO_USERS[1]["user_name"], DEMO_USERS[1]["user_mail"], DEMO_USERS[1]["password_clear_text"]) gustav = await user_service.create_user(DEMO_USERS[1]["user_name"], DEMO_USERS[1]["user_mail"],
await accounting_service.add_balance(gustav.user_id, 100000, "DEMO EINZAHLUNG") DEMO_USERS[1]["password_clear_text"])
await accounting_service.add_balance(gustav.user_id, Decimal("1000.00"), "DEMO EINZAHLUNG")
await ticket_service.purchase_ticket(gustav.user_id, "NORMAL") await ticket_service.purchase_ticket(gustav.user_id, "NORMAL")
# JASON # JASON
jason = await user_service.create_user(DEMO_USERS[2]["user_name"], DEMO_USERS[2]["user_mail"], DEMO_USERS[2]["password_clear_text"]) jason = await user_service.create_user(DEMO_USERS[2]["user_name"], DEMO_USERS[2]["user_mail"],
await accounting_service.add_balance(jason.user_id, 100000, "DEMO EINZAHLUNG") DEMO_USERS[2]["password_clear_text"])
await accounting_service.add_balance(jason.user_id, Decimal("1000.00"), "DEMO EINZAHLUNG")
await ticket_service.purchase_ticket(jason.user_id, "NORMAL") await ticket_service.purchase_ticket(jason.user_id, "NORMAL")
await seating_service.seat_user(jason.user_id, "D10") await seating_service.seat_user(jason.user_id, "D10")
# LISA # LISA
lisa = await user_service.create_user(DEMO_USERS[3]["user_name"], DEMO_USERS[3]["user_mail"], DEMO_USERS[3]["password_clear_text"]) lisa = await user_service.create_user(DEMO_USERS[3]["user_name"], DEMO_USERS[3]["user_mail"],
await accounting_service.add_balance(lisa.user_id, 100000, "DEMO EINZAHLUNG") DEMO_USERS[3]["password_clear_text"])
await accounting_service.add_balance(lisa.user_id, Decimal("1000.00"), "DEMO EINZAHLUNG")
lisa.is_team_member = True lisa.is_team_member = True
await user_service.update_user(lisa) await user_service.update_user(lisa)
# THOMAS # THOMAS
thomas = await user_service.create_user(DEMO_USERS[4]["user_name"], DEMO_USERS[4]["user_mail"], DEMO_USERS[4]["password_clear_text"]) thomas = await user_service.create_user(DEMO_USERS[4]["user_name"], DEMO_USERS[4]["user_mail"],
await accounting_service.add_balance(thomas.user_id, 100000, "DEMO EINZAHLUNG") DEMO_USERS[4]["password_clear_text"])
await accounting_service.add_balance(thomas.user_id, Decimal("1000.00"), "DEMO EINZAHLUNG")
thomas.is_team_member = True thomas.is_team_member = True
thomas.is_admin = True thomas.is_admin = True
await user_service.update_user(thomas) await user_service.update_user(thomas)
if not input("Generate catering menu? (Y/n): ").lower() == "n": if not input("Generate catering menu? (Y/n): ").lower() == "n":
# MAIN_COURSE # MAIN_COURSE
await catering_service.add_menu_item("Schnitzel Wiener Art", "mit Pommes", 1050, CateringMenuItemCategory.MAIN_COURSE) await catering_service.add_menu_item("Schnitzel Wiener Art", "mit Pommes", 1050,
await catering_service.add_menu_item("Jäger Schnitzel mit Champignonrahm Sauce", "mit Pommes", 1150, CateringMenuItemCategory.MAIN_COURSE) CateringMenuItemCategory.MAIN_COURSE)
await catering_service.add_menu_item("Tortellini in Käsesauce mit Fleischfüllung", "", 1050, CateringMenuItemCategory.MAIN_COURSE) await catering_service.add_menu_item("Jäger Schnitzel mit Champignonrahm Sauce", "mit Pommes", 1150,
await catering_service.add_menu_item("Tortellini in Käsesauce ohne Fleischfüllung", "Vegetarisch", 1050, CateringMenuItemCategory.MAIN_COURSE) CateringMenuItemCategory.MAIN_COURSE)
await catering_service.add_menu_item("Tortellini in Käsesauce mit Fleischfüllung", "", 1050,
CateringMenuItemCategory.MAIN_COURSE)
await catering_service.add_menu_item("Tortellini in Käsesauce ohne Fleischfüllung", "Vegetarisch", 1050,
CateringMenuItemCategory.MAIN_COURSE)
# SNACK # SNACK
await catering_service.add_menu_item("Käse Schinken Wrap", "", 500, CateringMenuItemCategory.SNACK) await catering_service.add_menu_item("Käse Schinken Wrap", "", 500, CateringMenuItemCategory.SNACK)
@ -93,37 +106,58 @@ async def run() -> None:
await catering_service.add_menu_item("Smacks", "", 150, CateringMenuItemCategory.BREAKFAST) await catering_service.add_menu_item("Smacks", "", 150, CateringMenuItemCategory.BREAKFAST)
await catering_service.add_menu_item("Knuspermüsli", "Schoko", 200, CateringMenuItemCategory.BREAKFAST) await catering_service.add_menu_item("Knuspermüsli", "Schoko", 200, CateringMenuItemCategory.BREAKFAST)
await catering_service.add_menu_item("Cini Minis", "", 150, CateringMenuItemCategory.BREAKFAST) await catering_service.add_menu_item("Cini Minis", "", 150, CateringMenuItemCategory.BREAKFAST)
await catering_service.add_menu_item("Brötchen - Schinken", "mit Margarine", 120, CateringMenuItemCategory.BREAKFAST) await catering_service.add_menu_item("Brötchen - Schinken", "mit Margarine", 120,
await catering_service.add_menu_item("Brötchen - Käse", "mit Margarine", 120, CateringMenuItemCategory.BREAKFAST) CateringMenuItemCategory.BREAKFAST)
await catering_service.add_menu_item("Brötchen - Schinken/Käse", "mit Margarine", 140, CateringMenuItemCategory.BREAKFAST) await catering_service.add_menu_item("Brötchen - Käse", "mit Margarine", 120,
await catering_service.add_menu_item("Brötchen - Salami", "mit Margarine", 120, CateringMenuItemCategory.BREAKFAST) CateringMenuItemCategory.BREAKFAST)
await catering_service.add_menu_item("Brötchen - Salami/Käse", "mit Margarine", 140, CateringMenuItemCategory.BREAKFAST) await catering_service.add_menu_item("Brötchen - Schinken/Käse", "mit Margarine", 140,
await catering_service.add_menu_item("Brötchen - Nutella", "mit Margarine", 120, CateringMenuItemCategory.BREAKFAST) CateringMenuItemCategory.BREAKFAST)
await catering_service.add_menu_item("Brötchen - Salami", "mit Margarine", 120,
CateringMenuItemCategory.BREAKFAST)
await catering_service.add_menu_item("Brötchen - Salami/Käse", "mit Margarine", 140,
CateringMenuItemCategory.BREAKFAST)
await catering_service.add_menu_item("Brötchen - Nutella", "mit Margarine", 120,
CateringMenuItemCategory.BREAKFAST)
# BEVERAGE_NON_ALCOHOLIC # BEVERAGE_NON_ALCOHOLIC
await catering_service.add_menu_item("Wasser - Still", "1L Flasche", 200, CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC) await catering_service.add_menu_item("Wasser - Still", "1L Flasche", 200,
await catering_service.add_menu_item("Wasser - Medium", "1L Flasche", 200, CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC) CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC)
await catering_service.add_menu_item("Wasser - Spritzig", "1L Flasche", 200, CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC) await catering_service.add_menu_item("Wasser - Medium", "1L Flasche", 200,
await catering_service.add_menu_item("Coca-Cola", "1L Flasche", 200, CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC) CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC)
await catering_service.add_menu_item("Coca-Cola Zero", "1L Flasche", 200, CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC) await catering_service.add_menu_item("Wasser - Spritzig", "1L Flasche", 200,
await catering_service.add_menu_item("Fanta", "1L Flasche", 200, CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC) CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC)
await catering_service.add_menu_item("Sprite", "1L Flasche", 200, CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC) await catering_service.add_menu_item("Coca-Cola", "1L Flasche", 200,
await catering_service.add_menu_item("Spezi", "von Paulaner, 0,5L Flasche", 150, CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC) CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC)
await catering_service.add_menu_item("Coca-Cola Zero", "1L Flasche", 200,
CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC)
await catering_service.add_menu_item("Fanta", "1L Flasche", 200,
CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC)
await catering_service.add_menu_item("Sprite", "1L Flasche", 200,
CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC)
await catering_service.add_menu_item("Spezi", "von Paulaner, 0,5L Flasche", 150,
CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC)
await catering_service.add_menu_item("Red Bull", "", 200, CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC) await catering_service.add_menu_item("Red Bull", "", 200, CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC)
await catering_service.add_menu_item("Energy", "Hausmarke", 150, CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC) await catering_service.add_menu_item("Energy", "Hausmarke", 150,
CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC)
# BEVERAGE_ALCOHOLIC # BEVERAGE_ALCOHOLIC
await catering_service.add_menu_item("Pils", "0,33L Flasche", 190, CateringMenuItemCategory.BEVERAGE_ALCOHOLIC) await catering_service.add_menu_item("Pils", "0,33L Flasche", 190, CateringMenuItemCategory.BEVERAGE_ALCOHOLIC)
await catering_service.add_menu_item("Radler", "0,33L Flasche", 190, CateringMenuItemCategory.BEVERAGE_ALCOHOLIC) await catering_service.add_menu_item("Radler", "0,33L Flasche", 190,
await catering_service.add_menu_item("Diesel", "0,33L Flasche", 190, CateringMenuItemCategory.BEVERAGE_ALCOHOLIC) CateringMenuItemCategory.BEVERAGE_ALCOHOLIC)
await catering_service.add_menu_item("Apfelwein Pur", "0,33L Flasche", 190, CateringMenuItemCategory.BEVERAGE_ALCOHOLIC) await catering_service.add_menu_item("Diesel", "0,33L Flasche", 190,
await catering_service.add_menu_item("Apfelwein Sauer", "0,33L Flasche", 190, CateringMenuItemCategory.BEVERAGE_ALCOHOLIC) CateringMenuItemCategory.BEVERAGE_ALCOHOLIC)
await catering_service.add_menu_item("Apfelwein Cola", "0,33L Flasche", 190, CateringMenuItemCategory.BEVERAGE_ALCOHOLIC) await catering_service.add_menu_item("Apfelwein Pur", "0,33L Flasche", 190,
CateringMenuItemCategory.BEVERAGE_ALCOHOLIC)
await catering_service.add_menu_item("Apfelwein Sauer", "0,33L Flasche", 190,
CateringMenuItemCategory.BEVERAGE_ALCOHOLIC)
await catering_service.add_menu_item("Apfelwein Cola", "0,33L Flasche", 190,
CateringMenuItemCategory.BEVERAGE_ALCOHOLIC)
# BEVERAGE_COCKTAIL # BEVERAGE_COCKTAIL
await catering_service.add_menu_item("Vodka Energy", "", 400, CateringMenuItemCategory.BEVERAGE_COCKTAIL) await catering_service.add_menu_item("Vodka Energy", "", 400, CateringMenuItemCategory.BEVERAGE_COCKTAIL)
await catering_service.add_menu_item("Vodka O-Saft", "", 400, CateringMenuItemCategory.BEVERAGE_COCKTAIL) await catering_service.add_menu_item("Vodka O-Saft", "", 400, CateringMenuItemCategory.BEVERAGE_COCKTAIL)
await catering_service.add_menu_item("Whiskey Cola", "mit Bourbon", 400, CateringMenuItemCategory.BEVERAGE_COCKTAIL) await catering_service.add_menu_item("Whiskey Cola", "mit Bourbon", 400,
CateringMenuItemCategory.BEVERAGE_COCKTAIL)
await catering_service.add_menu_item("Jägermeister Energy", "", 400, CateringMenuItemCategory.BEVERAGE_COCKTAIL) await catering_service.add_menu_item("Jägermeister Energy", "", 400, CateringMenuItemCategory.BEVERAGE_COCKTAIL)
await catering_service.add_menu_item("Sex on the Beach", "", 550, CateringMenuItemCategory.BEVERAGE_COCKTAIL) await catering_service.add_menu_item("Sex on the Beach", "", 550, CateringMenuItemCategory.BEVERAGE_COCKTAIL)
await catering_service.add_menu_item("Long Island Ice Tea", "", 550, CateringMenuItemCategory.BEVERAGE_COCKTAIL) await catering_service.add_menu_item("Long Island Ice Tea", "", 550, CateringMenuItemCategory.BEVERAGE_COCKTAIL)
@ -132,11 +166,13 @@ async def run() -> None:
# BEVERAGE_SHOT # BEVERAGE_SHOT
await catering_service.add_menu_item("Jägermeister", "", 200, CateringMenuItemCategory.BEVERAGE_SHOT) await catering_service.add_menu_item("Jägermeister", "", 200, CateringMenuItemCategory.BEVERAGE_SHOT)
await catering_service.add_menu_item("Tequila", "", 200, CateringMenuItemCategory.BEVERAGE_SHOT) await catering_service.add_menu_item("Tequila", "", 200, CateringMenuItemCategory.BEVERAGE_SHOT)
await catering_service.add_menu_item("PfEZzi", "Getunter Pfefferminz-Schnaps", 199, CateringMenuItemCategory.BEVERAGE_SHOT) await catering_service.add_menu_item("PfEZzi", "Getunter Pfefferminz-Schnaps", 199,
CateringMenuItemCategory.BEVERAGE_SHOT)
# NON_FOOD # NON_FOOD
await catering_service.add_menu_item("Zigaretten", "Elixyr", 800, CateringMenuItemCategory.NON_FOOD) await catering_service.add_menu_item("Zigaretten", "Elixyr", 800, CateringMenuItemCategory.NON_FOOD)
await catering_service.add_menu_item("Mentholfilter", "passend für Elixyr", 120, CateringMenuItemCategory.NON_FOOD) await catering_service.add_menu_item("Mentholfilter", "passend für Elixyr", 120,
CateringMenuItemCategory.NON_FOOD)
if not input("Generate default new post? (Y/n): ").lower() == "n": if not input("Generate default new post? (Y/n): ").lower() == "n":
loops = 0 loops = 0
@ -154,11 +190,14 @@ async def run() -> None:
news_id=None, news_id=None,
title="Der EZ LAN Manager", title="Der EZ LAN Manager",
subtitle="Eine Software des EZ GG e.V.", subtitle="Eine Software des EZ GG e.V.",
content="Dies ist eine WIP-Version des EZ LAN Managers. Diese Software soll uns helfen in Zukunft die LAN Parties des EZ GG e.V.'s zu organisieren. Wer Fehler findet darf sie behalten. (Oder er meldet sie)", content="Dies ist eine WIP-Version des EZ LAN Managers. Diese Software soll uns helfen in Zukunft die LAN "
"Parties des EZ GG e.V.'s zu organisieren. Wer Fehler findet darf sie behalten. (Oder er meldet "
"sie)",
author=user, author=user,
news_date=date.today() news_date=date.today()
)) ))
if __name__ == "__main__": if __name__ == "__main__":
with asyncio.Runner() as loop: with asyncio.Runner() as loop:
loop.run(run()) loop.run(run())

View File

@ -159,7 +159,7 @@ class AccountPage(Component):
align_x=0 align_x=0
), ),
Text( Text(
f"{'-' if transaction.is_debit else '+'}{AccountingService.make_euro_string_from_int(transaction.value)}", f"{'-' if transaction.is_debit else '+'}{AccountingService.make_euro_string_from_decimal(transaction.value)}",
style=TextStyle( style=TextStyle(
fill=self.session.theme.danger_color if transaction.is_debit else self.session.theme.success_color, fill=self.session.theme.danger_color if transaction.is_debit else self.session.theme.success_color,
font_size=0.8 font_size=0.8
@ -175,7 +175,7 @@ class AccountPage(Component):
return Column( return Column(
MainViewContentBox( MainViewContentBox(
content=Text( content=Text(
f"Kontostand: {AccountingService.make_euro_string_from_int(self.balance)}", f"Kontostand: {AccountingService.make_euro_string_from_decimal(self.balance)}",
style=TextStyle( style=TextStyle(
fill=self.session.theme.background_color, fill=self.session.theme.background_color,
font_size=1.2 font_size=1.2

View File

@ -17,6 +17,7 @@ from src.ez_lan_manager.types.User import User
logger = logging.getLogger(__name__.split(".")[-1]) logger = logging.getLogger(__name__.split(".")[-1])
class ClickableGridContent(Component): class ClickableGridContent(Component):
text: str = "" text: str = ""
is_hovered: bool = False is_hovered: bool = False
@ -36,7 +37,8 @@ class ClickableGridContent(Component):
content=Rectangle( content=Rectangle(
content=Text( content=Text(
self.text, self.text,
style=TextStyle(fill=self.session.theme.success_color) if self.is_hovered else TextStyle(fill=self.session.theme.background_color), style=TextStyle(fill=self.session.theme.success_color) if self.is_hovered else TextStyle(
fill=self.session.theme.background_color),
grow_x=True grow_x=True
), ),
fill=Color.TRANSPARENT, fill=Color.TRANSPARENT,
@ -47,6 +49,7 @@ class ClickableGridContent(Component):
on_press=self.on_mouse_click on_press=self.on_mouse_click
) )
class ManageUsersPage(Component): class ManageUsersPage(Component):
selected_user: Optional[User] = None selected_user: Optional[User] = None
all_users: Optional[list] = None all_users: Optional[list] = None
@ -66,13 +69,15 @@ class ManageUsersPage(Component):
async def on_user_clicked(self, user_name: str) -> None: async def on_user_clicked(self, user_name: str) -> None:
self.selected_user = next(filter(lambda user: user.user_name == user_name, self.all_users)) self.selected_user = next(filter(lambda user: user.user_name == user_name, self.all_users))
user_account_balance_raw = await self.session[AccountingService].get_balance(self.selected_user.user_id) user_account_balance_raw = await self.session[AccountingService].get_balance(self.selected_user.user_id)
self.user_account_balance = AccountingService.make_euro_string_from_int(user_account_balance_raw) self.user_account_balance = AccountingService.make_euro_string_from_decimal(user_account_balance_raw)
seat = await self.session[SeatingService].get_user_seat(self.selected_user.user_id) seat = await self.session[SeatingService].get_user_seat(self.selected_user.user_id)
self.user_seat = seat.seat_id if seat else "-" self.user_seat = seat.seat_id if seat else "-"
self.is_user_account_locked = not self.selected_user.is_active self.is_user_account_locked = not self.selected_user.is_active
async def on_search_parameters_changed(self, e: TextInputChangeEvent) -> None: async def on_search_parameters_changed(self, e: TextInputChangeEvent) -> None:
self.search_results = list(filter(lambda user: (e.text.lower() in user.user_name.lower()) or e.text.lower() in str(user.user_id), self.all_users)) self.search_results = list(
filter(lambda user: (e.text.lower() in user.user_name.lower()) or e.text.lower() in str(user.user_id),
self.all_users))
async def change_account_active(self, _: SwitchChangeEvent) -> None: async def change_account_active(self, _: SwitchChangeEvent) -> None:
self.selected_user.is_active = not self.is_user_account_locked self.selected_user.is_active = not self.is_user_account_locked
@ -84,7 +89,7 @@ class ManageUsersPage(Component):
logger.info(f"Got new transaction for user with ID '{transaction.user_id}' over " logger.info(f"Got new transaction for user with ID '{transaction.user_id}' over "
f"{'-' if transaction.is_debit else '+'}" f"{'-' if transaction.is_debit else '+'}"
f"{AccountingService.make_euro_string_from_int(transaction.value)} " f"{AccountingService.make_euro_string_from_decimal(transaction.value)} "
f"with reference '{transaction.reference}'") f"with reference '{transaction.reference}'")
if transaction.is_debit: if transaction.is_debit:
@ -108,7 +113,6 @@ class ManageUsersPage(Component):
self.accounting_section_result_text = f"Guthaben {'entfernt' if transaction.is_debit else 'hinzugefügt'}!" self.accounting_section_result_text = f"Guthaben {'entfernt' if transaction.is_debit else 'hinzugefügt'}!"
self.accounting_section_result_success = True self.accounting_section_result_success = True
def build(self) -> Component: def build(self) -> Component:
return Column( return Column(
MainViewContentBox( MainViewContentBox(

View File

@ -1,15 +1,18 @@
import logging import logging
from collections.abc import Callable from collections.abc import Callable
from datetime import datetime from datetime import datetime
from decimal import Decimal, ROUND_DOWN
from src.ez_lan_manager.services.DatabaseService import DatabaseService from src.ez_lan_manager.services.DatabaseService import DatabaseService
from src.ez_lan_manager.types.Transaction import Transaction from src.ez_lan_manager.types.Transaction import Transaction
logger = logging.getLogger(__name__.split(".")[-1]) logger = logging.getLogger(__name__.split(".")[-1])
class InsufficientFundsError(Exception): class InsufficientFundsError(Exception):
pass pass
class AccountingService: class AccountingService:
def __init__(self, db_service: DatabaseService) -> None: def __init__(self, db_service: DatabaseService) -> None:
self._db_service = db_service self._db_service = db_service
@ -19,7 +22,7 @@ class AccountingService:
""" Adds a function to this service, which is called whenever the account balance changes """ """ Adds a function to this service, which is called whenever the account balance changes """
self._update_hooks.add(update_hook) self._update_hooks.add(update_hook)
async def add_balance(self, user_id: int, balance_to_add: int, reference: str) -> int: async def add_balance(self, user_id: int, balance_to_add: Decimal, reference: str) -> Decimal:
await self._db_service.add_transaction(Transaction( await self._db_service.add_transaction(Transaction(
user_id=user_id, user_id=user_id,
value=balance_to_add, value=balance_to_add,
@ -27,12 +30,12 @@ class AccountingService:
reference=reference, reference=reference,
transaction_date=datetime.now() transaction_date=datetime.now()
)) ))
logger.debug(f"Added balance of {self.make_euro_string_from_int(balance_to_add)} to user with ID {user_id}") logger.debug(f"Added balance of {self.make_euro_string_from_decimal(balance_to_add)} to user with ID {user_id}")
for update_hook in self._update_hooks: for update_hook in self._update_hooks:
await update_hook() await update_hook()
return await self.get_balance(user_id) return await self.get_balance(user_id)
async def remove_balance(self, user_id: int, balance_to_remove: int, reference: str) -> int: async def remove_balance(self, user_id: int, balance_to_remove: Decimal, reference: str) -> Decimal:
current_balance = await self.get_balance(user_id) current_balance = await self.get_balance(user_id)
if (current_balance - balance_to_remove) < 0: if (current_balance - balance_to_remove) < 0:
raise InsufficientFundsError raise InsufficientFundsError
@ -43,13 +46,14 @@ class AccountingService:
reference=reference, reference=reference,
transaction_date=datetime.now() transaction_date=datetime.now()
)) ))
logger.debug(f"Removed balance of {self.make_euro_string_from_int(balance_to_remove)} to user with ID {user_id}") logger.debug(
f"Removed balance of {self.make_euro_string_from_decimal(balance_to_remove)} to user with ID {user_id}")
for update_hook in self._update_hooks: for update_hook in self._update_hooks:
await update_hook() await update_hook()
return await self.get_balance(user_id) return await self.get_balance(user_id)
async def get_balance(self, user_id: int) -> int: async def get_balance(self, user_id: int) -> Decimal:
balance_buffer = 0 balance_buffer = Decimal("0")
for transaction in await self._db_service.get_all_transactions_for_user(user_id): for transaction in await self._db_service.get_all_transactions_for_user(user_id):
if transaction.is_debit: if transaction.is_debit:
balance_buffer -= transaction.value balance_buffer -= transaction.value
@ -61,23 +65,8 @@ class AccountingService:
return await self._db_service.get_all_transactions_for_user(user_id) return await self._db_service.get_all_transactions_for_user(user_id)
@staticmethod @staticmethod
def make_euro_string_from_int(cent_int: int) -> str: def make_euro_string_from_decimal(euros: Decimal) -> str:
""" Internally, all money values are cents as ints. Only when showing them to the user we generate a string. Prevents float inaccuracy. """ """ Internally, all money values are cents as ints. Only when showing them to the user we generate a string.
as_str = str(cent_int) Prevents float inaccuracy."""
if as_str[0] == "-": rounded_decimal = str(euros.quantize(Decimal(".01"), rounding=ROUND_DOWN))
is_negative = True return f"{rounded_decimal}"
as_str = as_str[1:]
else:
is_negative = False
if len(as_str) == 1:
result = f"0.0{as_str}"
elif len(as_str) == 2:
result = f"0.{as_str}"
else:
result = f"{as_str[:-2]}.{as_str[-2:]}"
if is_negative:
result = f"-{result}"
return result

View File

@ -46,7 +46,7 @@ class CateringService:
order = await self._db_service.add_new_order(menu_items, user_id, is_delivery) order = await self._db_service.add_new_order(menu_items, user_id, is_delivery)
if order: if order:
await self._accounting_service.remove_balance(user_id, total_price, f"CATERING - {order.order_id}") await self._accounting_service.remove_balance(user_id, total_price, f"CATERING - {order.order_id}")
logger.info(f"User '{order.customer.user_name}' (ID:{order.customer.user_id}) ordered from catering for {self._accounting_service.make_euro_string_from_int(total_price)}") logger.info(f"User '{order.customer.user_name}' (ID:{order.customer.user_id}) ordered from catering for {self._accounting_service.make_euro_string_from_decimal(total_price)}")
# await self.cancel_order(order) # ToDo: Check if commented out before commit. Un-comment to auto-cancel every placed order # await self.cancel_order(order) # ToDo: Check if commented out before commit. Un-comment to auto-cancel every placed order
return order return order

View File

@ -1,15 +1,18 @@
import sys import sys
from datetime import datetime from datetime import datetime
from decimal import Decimal
from pathlib import Path from pathlib import Path
import logging import logging
import tomllib import tomllib
from from_root import from_root from from_root import from_root
from src.ez_lan_manager.types.ConfigurationTypes import DatabaseConfiguration, MailingServiceConfiguration, LanInfo, SeatingConfiguration, TicketInfo from src.ez_lan_manager.types.ConfigurationTypes import DatabaseConfiguration, MailingServiceConfiguration, LanInfo, \
SeatingConfiguration, TicketInfo
logger = logging.getLogger(__name__.split(".")[-1]) logger = logging.getLogger(__name__.split(".")[-1])
class ConfigurationService: class ConfigurationService:
def __init__(self, config_file_path: Path) -> None: def __init__(self, config_file_path: Path) -> None:
try: try:
@ -40,7 +43,6 @@ class ConfigurationService:
logger.fatal("Error loading DatabaseConfiguration, exiting...") logger.fatal("Error loading DatabaseConfiguration, exiting...")
sys.exit(1) sys.exit(1)
def get_mailing_service_configuration(self) -> MailingServiceConfiguration: def get_mailing_service_configuration(self) -> MailingServiceConfiguration:
try: try:
mailing_configuration = self._config["mailing"] mailing_configuration = self._config["mailing"]
@ -83,7 +85,7 @@ class ConfigurationService:
return tuple([TicketInfo( return tuple([TicketInfo(
category=value, category=value,
total_tickets=self._config["tickets"][value]["total_tickets"], total_tickets=self._config["tickets"][value]["total_tickets"],
price=self._config["tickets"][value]["price"], price=Decimal(self._config["tickets"][value]["price"]),
description=self._config["tickets"][value]["description"], description=self._config["tickets"][value]["description"],
additional_info=self._config["tickets"][value]["additional_info"], additional_info=self._config["tickets"][value]["additional_info"],
is_default=self._config["tickets"][value]["is_default"] is_default=self._config["tickets"][value]["is_default"]

View File

@ -2,6 +2,7 @@ import logging
from datetime import date, datetime from datetime import date, datetime
from typing import Optional from typing import Optional
from decimal import Decimal
import aiomysql import aiomysql
@ -17,14 +18,18 @@ from src.ez_lan_manager.types.User import User
logger = logging.getLogger(__name__.split(".")[-1]) logger = logging.getLogger(__name__.split(".")[-1])
class DuplicationError(Exception): class DuplicationError(Exception):
pass pass
class NoDatabaseConnectionError(Exception): class NoDatabaseConnectionError(Exception):
pass pass
class DatabaseService: class DatabaseService:
MAX_CONNECTION_RETRIES = 5 MAX_CONNECTION_RETRIES = 5
def __init__(self, database_config: DatabaseConfiguration) -> None: def __init__(self, database_config: DatabaseConfiguration) -> None:
self._database_config = database_config self._database_config = database_config
self._connection_pool: Optional[aiomysql.Pool] = None self._connection_pool: Optional[aiomysql.Pool] = None
@ -85,7 +90,6 @@ class DatabaseService:
return return
return self._map_db_result_to_user(result) return self._map_db_result_to_user(result)
async def get_user_by_id(self, user_id: int) -> Optional[User]: async def get_user_by_id(self, user_id: int) -> Optional[User]:
async with self._connection_pool.acquire() as conn: async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor: async with conn.cursor(aiomysql.Cursor) as cursor:
@ -110,7 +114,7 @@ class DatabaseService:
try: try:
await cursor.execute( await cursor.execute(
"INSERT INTO users (user_name, user_mail, user_password) " "INSERT INTO users (user_name, user_mail, user_password) "
"VALUES (%s, %s, %s)", (user_name, user_mail.lower(), password_hash) "VALUES (%s, %s, %s)", (user_name, user_mail.lower(), password_hash)
) )
await conn.commit() await conn.commit()
except aiomysql.InterfaceError: except aiomysql.InterfaceError:
@ -123,19 +127,19 @@ class DatabaseService:
raise DuplicationError raise DuplicationError
return await self.get_user_by_name(user_name) return await self.get_user_by_name(user_name)
async def update_user(self, user: User) -> User: async def update_user(self, user: User) -> User:
async with self._connection_pool.acquire() as conn: async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor: async with conn.cursor(aiomysql.Cursor) as cursor:
try: try:
await cursor.execute( await cursor.execute(
"UPDATE users SET user_name=%s, user_mail=%s, user_password=%s, user_first_name=%s, user_last_name=%s, user_birth_date=%s, " "UPDATE users SET user_name=%s, user_mail=%s, user_password=%s, user_first_name=%s, "
"is_active=%s, is_team_member=%s, is_admin=%s WHERE (user_id=%s)", (user.user_name, user.user_mail.lower(), user.user_password, "user_last_name=%s, user_birth_date=%s, is_active=%s, is_team_member=%s, is_admin=%s "
user.user_first_name, user.user_last_name, user.user_birth_day, "WHERE (user_id=%s)",
user.is_active, user.is_team_member, user.is_admin, (user.user_name, user.user_mail.lower(), user.user_password,
user.user_id) user.user_first_name, user.user_last_name, user.user_birth_day,
user.is_active, user.is_team_member, user.is_admin,
user.user_id)
) )
await conn.commit() await conn.commit()
except aiomysql.InterfaceError: except aiomysql.InterfaceError:
@ -155,7 +159,8 @@ class DatabaseService:
await cursor.execute( await cursor.execute(
"INSERT INTO transactions (user_id, value, is_debit, transaction_date, transaction_reference) " "INSERT INTO transactions (user_id, value, is_debit, transaction_date, transaction_reference) "
"VALUES (%s, %s, %s, %s, %s)", "VALUES (%s, %s, %s, %s, %s)",
(transaction.user_id, transaction.value, transaction.is_debit, transaction.transaction_date, transaction.reference) (transaction.user_id, transaction.value, transaction.is_debit, transaction.transaction_date,
transaction.reference)
) )
await conn.commit() await conn.commit()
except aiomysql.InterfaceError: except aiomysql.InterfaceError:
@ -189,14 +194,13 @@ class DatabaseService:
for transaction_raw in result: for transaction_raw in result:
transactions.append(Transaction( transactions.append(Transaction(
user_id=user_id, user_id=user_id,
value=int(transaction_raw[2]), value=Decimal(transaction_raw[2]),
is_debit=bool(transaction_raw[3]), is_debit=bool(transaction_raw[3]),
transaction_date=transaction_raw[4], transaction_date=transaction_raw[4],
reference=transaction_raw[5] reference=transaction_raw[5]
)) ))
return transactions return transactions
async def add_news(self, news: News) -> None: async def add_news(self, news: News) -> None:
async with self._connection_pool.acquire() as conn: async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor: async with conn.cursor(aiomysql.Cursor) as cursor:
@ -215,13 +219,15 @@ class DatabaseService:
except Exception as e: except Exception as e:
logger.warning(f"Error adding Transaction: {e}") logger.warning(f"Error adding Transaction: {e}")
async def get_news(self, dt_start: date, dt_end: date) -> list[News]: async def get_news(self, dt_start: date, dt_end: date) -> list[News]:
async with self._connection_pool.acquire() as conn: async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor: async with conn.cursor(aiomysql.Cursor) as cursor:
results = [] results = []
try: try:
await cursor.execute("SELECT * FROM news INNER JOIN users ON news.news_author = users.user_id WHERE news_date BETWEEN %s AND %s;", (dt_start, dt_end)) await cursor.execute(
"SELECT * FROM news INNER JOIN users ON news.news_author = users.user_id WHERE news_date"
" BETWEEN %s AND %s;",
(dt_start, dt_end))
await conn.commit() await conn.commit()
except aiomysql.InterfaceError: except aiomysql.InterfaceError:
pool_init_result = await self.init_db_pool() pool_init_result = await self.init_db_pool()
@ -315,12 +321,13 @@ class DatabaseService:
return results return results
async def get_ticket_for_user(self, user_id: int) -> Optional[Ticket]: async def get_ticket_for_user(self, user_id: int) -> Optional[Ticket]:
async with self._connection_pool.acquire() as conn: async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor: async with conn.cursor(aiomysql.Cursor) as cursor:
try: try:
await cursor.execute("SELECT * FROM tickets INNER JOIN users ON tickets.user = users.user_id WHERE user_id=%s;", (user_id, )) await cursor.execute(
"SELECT * FROM tickets INNER JOIN users ON tickets.user = users.user_id WHERE user_id=%s;",
(user_id,))
await conn.commit() await conn.commit()
except aiomysql.InterfaceError: except aiomysql.InterfaceError:
pool_init_result = await self.init_db_pool() pool_init_result = await self.init_db_pool()
@ -347,7 +354,8 @@ class DatabaseService:
async with self._connection_pool.acquire() as conn: async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor: async with conn.cursor(aiomysql.Cursor) as cursor:
try: try:
await cursor.execute("INSERT INTO tickets (ticket_category, user) VALUES (%s, %s)", (category, user_id)) await cursor.execute("INSERT INTO tickets (ticket_category, user) VALUES (%s, %s)",
(category, user_id))
await conn.commit() await conn.commit()
except aiomysql.InterfaceError: except aiomysql.InterfaceError:
pool_init_result = await self.init_db_pool() pool_init_result = await self.init_db_pool()
@ -360,12 +368,12 @@ class DatabaseService:
return await self.get_ticket_for_user(user_id) return await self.get_ticket_for_user(user_id)
async def change_ticket_owner(self, ticket_id: int, new_owner_id: int) -> bool: async def change_ticket_owner(self, ticket_id: int, new_owner_id: int) -> bool:
async with self._connection_pool.acquire() as conn: async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor: async with conn.cursor(aiomysql.Cursor) as cursor:
try: try:
await cursor.execute("UPDATE tickets SET user = %s WHERE ticket_id = %s;", (new_owner_id, ticket_id)) await cursor.execute("UPDATE tickets SET user = %s WHERE ticket_id = %s;",
(new_owner_id, ticket_id))
affected_rows = cursor.rowcount affected_rows = cursor.rowcount
await conn.commit() await conn.commit()
except aiomysql.InterfaceError: except aiomysql.InterfaceError:
@ -382,7 +390,7 @@ class DatabaseService:
async with self._connection_pool.acquire() as conn: async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor: async with conn.cursor(aiomysql.Cursor) as cursor:
try: try:
await cursor.execute("DELETE FROM tickets WHERE ticket_id = %s;", (ticket_id, )) await cursor.execute("DELETE FROM tickets WHERE ticket_id = %s;", (ticket_id,))
await conn.commit() await conn.commit()
except aiomysql.InterfaceError: except aiomysql.InterfaceError:
pool_init_result = await self.init_db_pool() pool_init_result = await self.init_db_pool()
@ -401,7 +409,8 @@ class DatabaseService:
try: try:
await cursor.execute("TRUNCATE seats;") await cursor.execute("TRUNCATE seats;")
for seat in seats: for seat in seats:
await cursor.execute("INSERT INTO seats (seat_id, seat_category) VALUES (%s, %s);", (seat[0], seat[1])) await cursor.execute("INSERT INTO seats (seat_id, seat_category) VALUES (%s, %s);",
(seat[0], seat[1]))
await conn.commit() await conn.commit()
except aiomysql.InterfaceError: except aiomysql.InterfaceError:
pool_init_result = await self.init_db_pool() pool_init_result = await self.init_db_pool()
@ -417,7 +426,8 @@ class DatabaseService:
async with conn.cursor(aiomysql.Cursor) as cursor: async with conn.cursor(aiomysql.Cursor) as cursor:
results = [] results = []
try: try:
await cursor.execute("SELECT seats.*, users.* FROM seats LEFT JOIN users ON seats.user = users.user_id;") await cursor.execute(
"SELECT seats.*, users.* FROM seats LEFT JOIN users ON seats.user = users.user_id;")
await conn.commit() await conn.commit()
except aiomysql.InterfaceError: except aiomysql.InterfaceError:
pool_init_result = await self.init_db_pool() pool_init_result = await self.init_db_pool()
@ -486,7 +496,8 @@ class DatabaseService:
async with self._connection_pool.acquire() as conn: async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor: async with conn.cursor(aiomysql.Cursor) as cursor:
try: try:
await cursor.execute("SELECT * FROM catering_menu_items WHERE catering_menu_item_id = %s;", (menu_item_id, )) await cursor.execute("SELECT * FROM catering_menu_items WHERE catering_menu_item_id = %s;",
(menu_item_id,))
await conn.commit() await conn.commit()
except aiomysql.InterfaceError: except aiomysql.InterfaceError:
pool_init_result = await self.init_db_pool() pool_init_result = await self.init_db_pool()
@ -501,20 +512,22 @@ class DatabaseService:
if raw_data is None: if raw_data is None:
return return
return CateringMenuItem( return CateringMenuItem(
item_id=raw_data[0], item_id=raw_data[0],
name=raw_data[1], name=raw_data[1],
additional_info=raw_data[2], additional_info=raw_data[2],
price=raw_data[3], price=raw_data[3],
category=CateringMenuItemCategory(raw_data[4]), category=CateringMenuItemCategory(raw_data[4]),
is_disabled=bool(raw_data[5]) is_disabled=bool(raw_data[5])
) )
async def add_menu_item(self, name: str, info: str, price: int, category: CateringMenuItemCategory, is_disabled: bool = False) -> Optional[CateringMenuItem]: async def add_menu_item(self, name: str, info: str, price: int, category: CateringMenuItemCategory,
is_disabled: bool = False) -> Optional[CateringMenuItem]:
async with self._connection_pool.acquire() as conn: async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor: async with conn.cursor(aiomysql.Cursor) as cursor:
try: try:
await cursor.execute( await cursor.execute(
"INSERT INTO catering_menu_items (name, additional_info, price, category, is_disabled) VALUES (%s, %s, %s, %s, %s);", "INSERT INTO catering_menu_items (name, additional_info, price, category, is_disabled) VALUES "
"(%s, %s, %s, %s, %s);",
(name, info, price, category.value, is_disabled) (name, info, price, category.value, is_disabled)
) )
await conn.commit() await conn.commit()
@ -540,7 +553,8 @@ class DatabaseService:
async with self._connection_pool.acquire() as conn: async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor: async with conn.cursor(aiomysql.Cursor) as cursor:
try: try:
await cursor.execute("DELETE FROM catering_menu_items WHERE catering_menu_item_id = %s;", (menu_item_id,)) await cursor.execute("DELETE FROM catering_menu_items WHERE catering_menu_item_id = %s;",
(menu_item_id,))
await conn.commit() await conn.commit()
except aiomysql.InterfaceError: except aiomysql.InterfaceError:
pool_init_result = await self.init_db_pool() pool_init_result = await self.init_db_pool()
@ -557,8 +571,10 @@ class DatabaseService:
async with conn.cursor(aiomysql.Cursor) as cursor: async with conn.cursor(aiomysql.Cursor) as cursor:
try: try:
await cursor.execute( await cursor.execute(
"UPDATE catering_menu_items SET name = %s, additional_info = %s, price = %s, category = %s, is_disabled = %s WHERE catering_menu_item_id = %s;", "UPDATE catering_menu_items SET name = %s, additional_info = %s, price = %s, category = %s, "
(updated_item.name, updated_item.additional_info, updated_item.price, updated_item.category.value, updated_item.is_disabled, updated_item.item_id) "is_disabled = %s WHERE catering_menu_item_id = %s;",
(updated_item.name, updated_item.additional_info, updated_item.price,
updated_item.category.value, updated_item.is_disabled, updated_item.item_id)
) )
affected_rows = cursor.rowcount affected_rows = cursor.rowcount
await conn.commit() await conn.commit()
@ -584,7 +600,8 @@ class DatabaseService:
order_id = cursor.lastrowid order_id = cursor.lastrowid
for menu_item, quantity in menu_items.items(): for menu_item, quantity in menu_items.items():
await cursor.execute( await cursor.execute(
"INSERT INTO order_catering_menu_item (order_id, catering_menu_item_id, quantity) VALUES (%s, %s, %s);", "INSERT INTO order_catering_menu_item (order_id, catering_menu_item_id, quantity) VALUES "
"(%s, %s, %s);",
(order_id, menu_item.item_id, quantity) (order_id, menu_item.item_id, quantity)
) )
await conn.commit() await conn.commit()
@ -673,7 +690,7 @@ class DatabaseService:
"SELECT * FROM order_catering_menu_item " "SELECT * FROM order_catering_menu_item "
"LEFT JOIN catering_menu_items ON order_catering_menu_item.catering_menu_item_id = catering_menu_items.catering_menu_item_id " "LEFT JOIN catering_menu_items ON order_catering_menu_item.catering_menu_item_id = catering_menu_items.catering_menu_item_id "
"WHERE order_id = %s;", "WHERE order_id = %s;",
(order_id, ) (order_id,)
) )
await conn.commit() await conn.commit()
except aiomysql.InterfaceError: except aiomysql.InterfaceError:
@ -718,7 +735,7 @@ class DatabaseService:
async with self._connection_pool.acquire() as conn: async with self._connection_pool.acquire() as conn:
async with conn.cursor(aiomysql.Cursor) as cursor: async with conn.cursor(aiomysql.Cursor) as cursor:
try: try:
await cursor.execute("SELECT (picture) FROM user_profile_picture WHERE user_id = %s", (user_id, )) await cursor.execute("SELECT (picture) FROM user_profile_picture WHERE user_id = %s", (user_id,))
await conn.commit() await conn.commit()
r = await cursor.fetchone() r = await cursor.fetchone()
if r is None: if r is None:

View File

@ -8,15 +8,19 @@ from src.ez_lan_manager.types.Ticket import Ticket
logger = logging.getLogger(__name__.split(".")[-1]) logger = logging.getLogger(__name__.split(".")[-1])
class TicketNotAvailableError(Exception): class TicketNotAvailableError(Exception):
def __init__(self, category: str): def __init__(self, category: str):
self.category = category self.category = category
class UserAlreadyHasTicketError(Exception): class UserAlreadyHasTicketError(Exception):
pass pass
class TicketingService: class TicketingService:
def __init__(self, ticket_infos: tuple[TicketInfo, ...], db_service: DatabaseService, accounting_service: AccountingService) -> None: def __init__(self, ticket_infos: tuple[TicketInfo, ...], db_service: DatabaseService,
accounting_service: AccountingService) -> None:
self._ticket_infos = ticket_infos self._ticket_infos = ticket_infos
self._db_service = db_service self._db_service = db_service
self._accounting_service = accounting_service self._accounting_service = accounting_service
@ -75,7 +79,8 @@ class TicketingService:
ticket_info = self.get_ticket_info_by_category(user_ticket.category) ticket_info = self.get_ticket_info_by_category(user_ticket.category)
if await self._db_service.delete_ticket(user_ticket.ticket_id): if await self._db_service.delete_ticket(user_ticket.ticket_id):
await self._accounting_service.add_balance(user_id, ticket_info.price, f"TICKET REFUND {user_ticket.ticket_id}") await self._accounting_service.add_balance(user_id, ticket_info.price,
f"TICKET REFUND {user_ticket.ticket_id}")
logger.debug(f"User {user_id} refunded ticket {user_ticket.ticket_id}") logger.debug(f"User {user_id} refunded ticket {user_ticket.ticket_id}")
return True return True

View File

@ -1,11 +1,13 @@
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import datetime
from pathlib import Path
from decimal import Decimal
class NoSuchCategoryError(Exception): class NoSuchCategoryError(Exception):
pass pass
@dataclass(frozen=True) @dataclass(frozen=True)
class DatabaseConfiguration: class DatabaseConfiguration:
db_user: str db_user: str
@ -14,15 +16,17 @@ class DatabaseConfiguration:
db_port: int db_port: int
db_name: str db_name: str
@dataclass(frozen=True) @dataclass(frozen=True)
class TicketInfo: class TicketInfo:
category: str category: str
total_tickets: int total_tickets: int
price: int price: Decimal
description: str description: str
additional_info: str additional_info: str
is_default: bool is_default: bool
@dataclass(frozen=True) @dataclass(frozen=True)
class MailingServiceConfiguration: class MailingServiceConfiguration:
smtp_server: str smtp_server: str
@ -31,6 +35,7 @@ class MailingServiceConfiguration:
username: str username: str
password: str password: str
@dataclass(frozen=True) @dataclass(frozen=True)
class LanInfo: class LanInfo:
name: str name: str
@ -39,6 +44,7 @@ class LanInfo:
date_till: datetime date_till: datetime
organizer_mail: str organizer_mail: str
@dataclass(frozen=True) @dataclass(frozen=True)
class SeatingConfiguration: class SeatingConfiguration:
seats: dict[str, str] seats: dict[str, str]

View File

@ -1,11 +1,12 @@
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import datetime
from decimal import Decimal
@dataclass(frozen=True) @dataclass(frozen=True)
class Transaction: class Transaction:
user_id: int user_id: int
value: int value: Decimal
is_debit: bool is_debit: bool
reference: str reference: str
transaction_date: datetime transaction_date: datetime

View File

@ -1,6 +1,7 @@
import unittest import unittest
from datetime import datetime from datetime import datetime
from unittest.mock import MagicMock, AsyncMock from unittest.mock import MagicMock, AsyncMock
from decimal import Decimal
from src.ez_lan_manager.services.AccountingService import AccountingService, InsufficientFundsError from src.ez_lan_manager.services.AccountingService import AccountingService, InsufficientFundsError
from src.ez_lan_manager.types.Transaction import Transaction from src.ez_lan_manager.types.Transaction import Transaction
@ -12,7 +13,6 @@ class AccountingServiceTests(unittest.IsolatedAsyncioTestCase):
self.mock_database_service.add_transaction = AsyncMock() self.mock_database_service.add_transaction = AsyncMock()
self.accounting_service = AccountingService(self.mock_database_service) self.accounting_service = AccountingService(self.mock_database_service)
def test_importing_unit_under_test_works(self) -> None: def test_importing_unit_under_test_works(self) -> None:
""" """
This test asserts that the object produced in setUp is AccountingService object, This test asserts that the object produced in setUp is AccountingService object,
@ -21,59 +21,59 @@ class AccountingServiceTests(unittest.IsolatedAsyncioTestCase):
self.assertIsInstance(self.accounting_service, AccountingService) self.assertIsInstance(self.accounting_service, AccountingService)
def test_making_string_from_euro_value_works_correctly(self) -> None: def test_making_string_from_euro_value_works_correctly(self) -> None:
test_value = 13466 test_value = Decimal("134.66")
expected_result = "134.66 €" expected_result = "134.66 €"
self.assertEqual(expected_result, AccountingService.make_euro_string_from_int(test_value)) self.assertEqual(expected_result, AccountingService.make_euro_string_from_decimal(test_value))
def test_making_euro_string_from_negative_value_works_correctly(self) -> None: def test_making_euro_string_from_negative_value_works_correctly(self) -> None:
test_value = -99741 test_value = Decimal("-997.41")
expected_result = "-997.41 €" expected_result = "-997.41 €"
self.assertEqual(expected_result, AccountingService.make_euro_string_from_int(test_value)) self.assertEqual(expected_result, AccountingService.make_euro_string_from_decimal(test_value))
def test_making_euro_string_from_less_than_ten_cents_works_correctly(self) -> None: def test_making_euro_string_from_less_than_ten_cents_works_correctly(self) -> None:
test_value = 4 test_value = Decimal("0.04")
expected_result = "0.04 €" expected_result = "0.04 €"
self.assertEqual(expected_result, AccountingService.make_euro_string_from_int(test_value)) self.assertEqual(expected_result, AccountingService.make_euro_string_from_decimal(test_value))
async def test_get_balance_correctly_adds_up_transactions(self) -> None: async def test_get_balance_correctly_adds_up_transactions(self) -> None:
self.mock_database_service.get_all_transactions_for_user = AsyncMock(return_value=[ self.mock_database_service.get_all_transactions_for_user = AsyncMock(return_value=[
Transaction( Transaction(
user_id=0, user_id=0,
value=5, value=Decimal("0.05"),
is_debit=True, is_debit=True,
reference="", reference="",
transaction_date=datetime.now() transaction_date=datetime.now()
), ),
Transaction( Transaction(
user_id=0, user_id=0,
value=99, value=Decimal("0.99"),
is_debit=False, is_debit=False,
reference="", reference="",
transaction_date=datetime.now() transaction_date=datetime.now()
), ),
Transaction( Transaction(
user_id=0, user_id=0,
value=101, value=Decimal("1.01"),
is_debit=False, is_debit=False,
reference="", reference="",
transaction_date=datetime.now() transaction_date=datetime.now()
), ),
Transaction( Transaction(
user_id=0, user_id=0,
value=77, value=Decimal("0.77"),
is_debit=True, is_debit=True,
reference="", reference="",
transaction_date=datetime.now() transaction_date=datetime.now()
), ),
]) ])
expected_result = 118 expected_result = Decimal("1.18")
actual_result = await self.accounting_service.get_balance(0) actual_result = await self.accounting_service.get_balance(0)
self.assertEqual(expected_result, actual_result) self.assertEqual(expected_result, actual_result)
async def test_trying_to_remove_more_than_is_on_account_balance_raises_exception(self) -> None: async def test_trying_to_remove_more_than_is_on_account_balance_raises_exception(self) -> None:
user_balance = 100 user_balance = Decimal("1.00")
balance_to_remove = 101 balance_to_remove = Decimal("1.01")
self.mock_database_service.get_all_transactions_for_user = AsyncMock(return_value=[ self.mock_database_service.get_all_transactions_for_user = AsyncMock(return_value=[
Transaction( Transaction(
user_id=0, user_id=0,
@ -88,8 +88,8 @@ class AccountingServiceTests(unittest.IsolatedAsyncioTestCase):
await self.accounting_service.remove_balance(0, balance_to_remove, "TestRef") await self.accounting_service.remove_balance(0, balance_to_remove, "TestRef")
async def test_trying_to_remove_less_than_is_on_account_balance_spawns_correct_transaction(self) -> None: async def test_trying_to_remove_less_than_is_on_account_balance_spawns_correct_transaction(self) -> None:
user_balance = 101 user_balance = Decimal("1.01")
balance_to_remove = 100 balance_to_remove = Decimal("1.00")
reference = "Yey, a reference" reference = "Yey, a reference"
self.mock_database_service.get_all_transactions_for_user = AsyncMock(return_value=[ self.mock_database_service.get_all_transactions_for_user = AsyncMock(return_value=[