BonPrinter v1.2.0
Thermal Printer tool
Loading...
Searching...
No Matches
main_window.py
Go to the documentation of this file.
1"""!
2********************************************************************************
3@file main_window.py
4@brief View controller for the main window
5********************************************************************************
6"""
7
8import sys
9import os
10import subprocess
11import logging
12import enum
13from typing import Optional, Callable, Any
14from datetime import datetime
15import json
16import base64
17import webbrowser
18import shutil
19
20from Source.version import __title__, __home__
21from Source.Util.app_data import EUser, L_CONFIG_USER, ETheme, EPaper, S_PW, I_GROUP_3, ELanguages, \
22 read_sound_settings, save_window_state, D_DEFAULT_USER, RESET_CODE, ICON_APP, \
23 ICON_HELP_LIGHT, S_UNIT_SYMBOL, ICON_REPORT_LIGHT, ICON_PRINTER_LIGHT, open_explorer, \
24 DEFAULT_CODE, ICON_USER_RESET_LIGHT, ICON_ARTICLES_RESET_LIGHT, reset_config_dialog, ICON_HELP_DARK, \
25 write_update_version, read_update_version, I_ITEM_ARRAY_SIZE, ItemNumber, get_default_item_config
26from Source.Util.app_err_handler import UncaughtHook
27from Source.Util.app_log import LogConfig
28from Source.Util.gui_toolkit import WINDOW_TYPE, MAIN_WINDOW, SIGNAL, \
29 emit_signal, config_window, get_window_geometry, get_window_state, \
30 filter_menubar, connect_menu, get_menu_text, config_menu, group_menu, \
31 connect_button, config_btn, get_btn_text, config_text, LABEL, config_label, \
32 config_statusbar, config_table, insert_table_item, get_selected_table_row, get_selected_table_column, \
33 open_message_box, input_dialog, open_directory, open_file, save_file, copy_to_clipboard, create_timer, \
34 ACTION, RESIZE_EVENT, CLOSE_EVENT, close_app, BUTTON
35
36from Source.Controller.help_dialog import create_help_dialog
37from Source.Controller.about_dialog import show_about_dialog
38from Source.Controller.report_dialog import show_report_dialog
39
40from Source.Model.model import Model
41from Source.Model.authentication import EWindowView
42from Source.Model.config import write_config_to_file, S_USER_TEMP_FILE, S_USER_FILE, S_ITEM_FILE, S_ITEM_TEMP_FILE
43from Source.Model.language import L_SAVE, L_PRINT, L_OPEN, L_CLEAR, L_MENU_HELP, L_CLOSE, L_YES, L_NO, L_CANCEL, \
44 L_COPY_URL, L_YOU_SURE, L_MENU_USER
45from Source.Model.report import Item, create_item
46from Source.Model.update_service import get_tool_update_status, compare_versions, S_UPDATE_URL
47from Source.Worker.open_notepad import ENotepadSelection
48
49log = logging.getLogger(__title__)
50
51B_NOTEPAD_REPORT = False # True: show report in notepad file; False: show report in GUI
52
53if B_NOTEPAD_REPORT:
54 S_REPORT_TEMP_FILE = "_temp_Report.md"
55
56
57S_NULL = "0"
58S_DOUBLE_NULL = "00"
59S_DOT = "."
60L_SPECIAL_BTNS = [S_NULL, S_DOUBLE_NULL, S_DOT]
61I_BTN_LONG_PRESS_TIME = 1000 # time in "ms" to set press button long for colored selection
62
63# autopep8: off
64L_LOGIN_KEYS = [S_NULL, "1", "4", "7", EUser.B.value + "1",
65 EUser.ADMIN.value, "2", "5", "8", EUser.B.value + "2",
66 EUser.HOST.value, "3", "6", "9", EUser.B.value + "3",
67 EUser.FREE.value, EUser.B.value + "13", EUser.B.value + "10", EUser.B.value + "7", EUser.B.value + "4",
68 EUser.LOCAL.value, EUser.B.value + "14", EUser.B.value + "11", EUser.B.value + "8", EUser.B.value + "5",
69 EUser.USER.value, EUser.B.value + "15", EUser.B.value + "12", EUser.B.value + "9", EUser.B.value + "6"]
70# autopep8: on
71
72STATUS_TEXT_TIME = 3000
73STATUS_HIGHLIGHT_TEXT_TIME = 5000
74STATUS_WARNING_TEXT_TIME = 15000
75
76DEFAULT_STYLE = "None"
77WARNING_STYLE = "orange"
78WARNING_STYLE_DARK = "darkorange"
79HIGHLIGHT_STYLE = "red"
80LOCKED_STYLE = "grey"
81
82
83class EConfigSelection(str, enum.Enum):
84 """!
85 @brief Mode of configuration change.
86 """
87 EDIT = "edit"
88 IMPORT = "import"
89 EXPORT = "export"
90 RESET = "reset"
91 PDF_IMPORT = "pdf_import"
92
93
94INI_FILE_TYPES = ("INI file", "*.ini")
95LOG_FILE_TYPES = ("CSV file", "*.csv")
96REPORT_FILE_TYPES = ("MD file", "*.md")
97I_MAX_ITEMS_TO_PRINT = 50
98
99
100def connect_menu_com_port(menu: ACTION, function: Callable[[str], None]) -> None:
101 """!
102 @brief Connect menu for COM ports.
103 @param menu : connect function to this menu
104 @param function : function to connect
105 """
106 menu_text = get_menu_text(menu)
107 connect_menu(menu, function, menu_text.split(maxsplit=1)[0])
108
109
110class MainWindow(WINDOW_TYPE, MAIN_WINDOW):
111 """!
112 @brief The view-controller for main window. Entry point of application.
113 Provides general methods that may be called by any other controller.
114 @param qt_exception_hook : exception hook
115 @param log_config : log configuration of the application
116 @param test_mode : status if application run in test mode for pytest
117 @param authenticated : status if you are authenticated to use this program
118 """
119 resized = SIGNAL() # need to defined out of scope
120
121 def __init__(self, qt_exception_hook: UncaughtHook, log_config: LogConfig, test_mode: bool = False,
122 authenticated: bool = False, *args: Any, **kwargs: Any) -> None: # pylint: disable=keyword-arg-before-vararg
123 log.debug("Initializing Main Window")
124 super().__init__(*args, **kwargs)
125 self.test_mode = test_mode
126 self.qt_exception_hook = qt_exception_hook
128 self.gui_locked = False
129 self.b_warning_active = False
130 self.setupUi(self)
131 config_window(self, title=__title__)
132 self.dialog_help = create_help_dialog(self)
133
134 # split statusbar
135 self.statusbar_left = LABEL()
136 self.statusbar_right = LABEL()
137 config_statusbar(statusbar=self.statusbar, lbl_left=self.statusbar_left, lbl_right=self.statusbar_right)
138
139 # Init settings
140 log.debug("Initializing main configuration")
141
142 # TODO Instanzen nach Module auslagern
144 self.action_port_2,
145 self.action_port_3,
146 self.action_port_4,
147 self.action_port_5,
148 self.action_port_6,
149 self.action_port_7,
150 self.action_port_8,
151 self.action_port_9,
152 self.action_port_10]
153 self.ag_port = None
154
155 self.model = Model(self, log_config)
156 log_config.ui = self # set ui here because object generates before main window
157
158 self.model.c_monitor.update_darkmode_status(self.model.c_monitor.e_style)
159
160 self.model.c_sound.update_sound_status(read_sound_settings())
161 self.status_timer = create_timer(parent=self, callback=self.clear_status) # statusbar timer
162
163 # Action Group Log Verbosity
164 self.ag_verbosity = group_menu(self,
166 self.model.log_config.i_log_level,
167 [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG])
168
169 # Action Group paper width
170 self.ag_paper_width = group_menu(self,
171 [self.action_58mm, self.action_80mm],
172 self.model.c_printer.i_paper_width,
173 [EPaper.WIDTH_58_MM, EPaper.WIDTH_80_MM])
174
175 # Sound
176 config_menu(self.action_sound, checked=self.model.c_sound.b_sound)
177
178 # Show Price
179 config_menu(self.action_show_price, checked=self.model.b_show_price)
180
181 # Enable Print Report
182 config_menu(self.action_enable_report, checked=self.model.b_enable_report)
183
184 # Action Group theme
185 self.ag_theme = group_menu(self,
186 [self.action_auto, self.action_light, self.action_dark, self.action_system],
187 self.model.c_monitor.e_style,
188 [ETheme.AUTO, ETheme.LIGHT, ETheme.DARK, ETheme.SYSTEM])
189
190 # Action Group language
191 self.ag_language = group_menu(self,
192 [self.action_english, self.action_german],
193 self.model.c_language.e_language,
194 [ELanguages.ENGLISH, ELanguages.GERMAN])
195
196 # Connect Callbacks
197 # button connections
198 self.l_bnt_fnc: list[BUTTON] = [self.btn_item1, self.btn_item2, self.btn_item3, self.btn_item4, self.btn_item5,
199 self.btn_item6, self.btn_item7, self.btn_item8, self.btn_item9, self.btn_item10,
200 self.btn_item11, self.btn_item12, self.btn_item13, self.btn_item14, self.btn_item15,
201 self.btn_item16, self.btn_item17, self.btn_item18, self.btn_item19, self.btn_item20,
202 self.btn_item21, self.btn_item22, self.btn_item23, self.btn_item24, self.btn_item25,
203 self.btn_item26, self.btn_item27, self.btn_item28, self.btn_item29, self.btn_item30,
204 self.btn_item31, self.btn_item32, self.btn_item33, self.btn_item34, self.btn_item35,
205 self.btn_item36, self.btn_item37, self.btn_item38, self.btn_item39, self.btn_item40,
206 self.btn_item41, self.btn_item42, self.btn_item43, self.btn_item44, self.btn_item45,
207 self.btn_item46, self.btn_item47, self.btn_item48, self.btn_item49, self.btn_item50,
208 self.btn_item51, self.btn_item52, self.btn_item53, self.btn_item54, self.btn_item55,
209 self.btn_item56]
210 self.b_btn_hold = False
212 self.btn_timer = create_timer(parent=self, callback=self.btn_item_held) # timer for button held
213 for i, btn in enumerate(self.l_bnt_fnc):
214 connect_button(btn,
215 clicked_fnc=self.btn_item_clicked,
216 pressed_fnc=self.btn_item_pressed,
217 released_fnc=self.btn_item_released,
218 index=i)
219 connect_button(self.btn_multi, self.btn_multi_clicked)
220 connect_button(self.btn_calc, self.btn_calc_clicked)
221 connect_button(self.btn_clear, self.btn_clear_clicked)
222 connect_button(self.btn_print, self.btn_print_clicked)
223 connect_button(self.btn_lock, self.btn_lock_clicked)
224
225 # Menu buttons
226 connect_menu(self.action_show_interim, self.btn_report_print, False)
227 connect_menu(self.action_open_folder, self.btn_open_folder)
229 connect_menu(self.action_combine_auto, self.btn_combine_auto)
230 connect_menu(self.action_print_file, self.btn_print_file)
232 connect_menu(self.action_enable_report, self.model.update_enable_report_status)
233 connect_menu(self.action_create_billing, self.btn_report_print, True)
234 # User
235 connect_menu(self.action_user_edit, self.btn_change_user, EConfigSelection.EDIT)
236 connect_menu(self.action_user_import, self.btn_change_user, EConfigSelection.IMPORT)
237 connect_menu(self.action_user_export, self.btn_change_user, EConfigSelection.EXPORT)
238 connect_menu(self.action_user_new, self.btn_change_user, EConfigSelection.RESET)
239 # Items
240 connect_menu(self.action_articles_edit, self.btn_change_item, EConfigSelection.EDIT)
241 connect_menu(self.action_articles_import, self.btn_change_item, EConfigSelection.IMPORT)
242 connect_menu(self.action_articles_export, self.btn_change_item, EConfigSelection.EXPORT)
243 connect_menu(self.action_articles_new, self.btn_change_item, EConfigSelection.RESET)
244 # Other Config
245 connect_menu(self.action_change_path, self.change_output_path)
246 # COM Port (max number of supported ports: 10 TODO use variable number of ports (not only 10) but is buggy)
247 connect_menu(self.action_none, self.model.c_printer.update_com_port, None)
248 connect_menu_com_port(self.action_port_1, self.model.c_printer.update_com_port)
249 connect_menu_com_port(self.action_port_2, self.model.c_printer.update_com_port)
250 connect_menu_com_port(self.action_port_3, self.model.c_printer.update_com_port)
251 connect_menu_com_port(self.action_port_4, self.model.c_printer.update_com_port)
252 connect_menu_com_port(self.action_port_5, self.model.c_printer.update_com_port)
253 connect_menu_com_port(self.action_port_6, self.model.c_printer.update_com_port)
254 connect_menu_com_port(self.action_port_7, self.model.c_printer.update_com_port)
255 connect_menu_com_port(self.action_port_8, self.model.c_printer.update_com_port)
256 connect_menu_com_port(self.action_port_9, self.model.c_printer.update_com_port)
257 connect_menu_com_port(self.action_port_10, self.model.c_printer.update_com_port)
258 # Paper
259 connect_menu(self.action_58mm, self.model.c_printer.update_paper_width, EPaper.WIDTH_58_MM.value)
260 connect_menu(self.action_80mm, self.model.c_printer.update_paper_width, EPaper.WIDTH_80_MM.value)
261 # Sound
262 connect_menu(self.action_sound, self.model.c_sound.update_sound_status)
263 # Dark Mode
264 connect_menu(self.action_auto, self.model.c_monitor.update_darkmode_status, ETheme.AUTO)
265 connect_menu(self.action_light, self.model.c_monitor.update_darkmode_status, ETheme.LIGHT)
266 connect_menu(self.action_dark, self.model.c_monitor.update_darkmode_status, ETheme.DARK)
267 config_menu(self.action_normal, show=False)
268 connect_menu(self.action_system, self.model.c_monitor.update_darkmode_status, ETheme.SYSTEM)
269 # Language
270 connect_menu(self.action_english, self.model.c_language.update_language, ELanguages.ENGLISH)
271 connect_menu(self.action_german, self.model.c_language.update_language, ELanguages.GERMAN)
272 # Show Price
273 connect_menu(self.action_show_price, self.model.update_show_price_status)
274 # Verbosity
275 connect_menu(self.action_log_error, self.model.log_config.update_log_level, logging.ERROR)
276 connect_menu(self.action_log_warning, self.model.log_config.update_log_level, logging.WARNING)
277 connect_menu(self.action_log_info, self.model.log_config.update_log_level, logging.INFO)
278 connect_menu(self.action_log_debug, self.model.log_config.update_log_level, logging.DEBUG)
279 # Paper change
280 # Help
281 connect_menu(self.action_help, self.show_help_dialog)
282 connect_menu(self.action_about_app, show_about_dialog, self)
283 connect_menu(self.action_reset, self.reset_config)
284
285 filter_menubar(self.menubar, self)
286
287 # initialize screen
288 self.model.c_report.create_report(b_clear_report=False, b_startup_check=True) # check for valid report at startup
289 config_window(self, fullscreen=False, resize_callback=self.model.c_monitor.resize_window)
290 self.model.c_monitor.resize_window()
291 self.update_screen()
292 if not authenticated:
294 elif self.test_mode:
295 self.model.c_config.read_user_file("user_example.ini")
296 self.model.c_config.read_item_file("articles_example.ini")
297 else:
298 newer_tool_version = get_tool_update_status()
299 if (newer_tool_version is not None) and (compare_versions(read_update_version(), newer_tool_version)):
300 self.show_update_dialog(newer_tool_version)
301 if self.model.c_config.d_user == D_DEFAULT_USER:
303 self.clear_status(b_override=True)
304
305 def resizeEvent(self, _event: RESIZE_EVENT | None) -> None:
306 """!
307 @brief Default resize Event Method to handle change of window size
308 @param _event : arrived event
309 """
310 emit_signal(self.resized)
311
312 def closeEvent(self, event: CLOSE_EVENT | None) -> None: # pylint: disable=invalid-name
313 """!
314 @brief Default close Event Method to handle application close
315 @param event : arrived event
316 """
317 if event is None:
318 return
319 log.debug("Close Event")
320 save_window_state(get_window_geometry(self), get_window_state(self))
321 if not self.close_window_dialog or self.qt_exception_hook.crash_arrived or self.test_mode or self.confirm_dialog(["Close Program", "Programm schließen"]):
322 log.debug("Application closed")
323 self.model.c_printer.terminate()
324 event.accept()
325 else:
326 event.ignore()
327
328 def confirm_dialog(self, title_value: str | list[str], s_icon_path: str = ICON_APP, l_optional_text: Optional[list[str]] = None) -> bool:
329 """!
330 @brief Show confirm dialog to accept or cancel.
331 @param title_value : dialog title
332 @param s_icon_path : icon of dialog
333 @param l_optional_text : optional text
334 @return return accept status
335 """
336 title = self.model.c_language.get_language_text(title_value) if isinstance(title_value, list) else title_value
337 fix_text = self.model.c_language.get_language_text(L_YOU_SURE)
338 if l_optional_text is None:
339 text = fix_text
340 else:
341 text = fix_text + "\n\n" + self.model.c_language.get_language_text(l_optional_text)
342 b_accept, _ = open_message_box(parent=self,
343 title=title,
344 text=text,
345 btn_yes=self.model.c_language.get_language_text(L_YES),
346 btn_cancel=self.model.c_language.get_language_text(L_CANCEL),
347 window_icon=s_icon_path)
348 return b_accept
349
350 def set_ui(self, b_state: bool) -> None:
351 """!
352 @brief Blocks/Unblock the main UI elements.
353 @param b_state : state if UI should blocked or unblocked, True: Enable, False: Disable
354 """
355 if not b_state:
356 for btn in self.l_bnt_fnc:
357 config_btn(btn, enable=b_state)
358
359 config_btn(self.btn_print, enable=b_state)
360 config_btn(self.btn_clear, enable=b_state)
361 config_btn(self.btn_multi, enable=b_state)
362 config_btn(self.btn_calc, enable=b_state)
363 config_btn(self.btn_lock, enable=b_state)
364 config_menu(self.menu_prints, enable=b_state)
365 config_menu(self.menu_configuration, enable=b_state)
366 config_menu(self.menu_settings, enable=b_state)
367 config_menu(self.menu_help, enable=b_state)
368
369 def block_ui(self) -> None:
370 """!
371 @brief Blocks the main UI elements.
372 """
373 log.debug("Block UI")
374 self.set_ui(False)
375 self.gui_locked = True
376 self.set_status("")
377
378 def unblock_ui(self) -> None:
379 """!
380 @brief Unblock the main UI elements.
381 """
382 log.debug("Unblock UI")
383 self.set_ui(True)
384 self.gui_locked = False
385 self.update_screen()
386 self.status_timer.stop() # stop timer to deactivate unlock diagnostic directly after clear status
387 self.clear_status()
388
389 def show_update_dialog(self, newer_tool_version: str) -> None:
390 """!
391 @brief Show Update dialog.
392 @param newer_tool_version : newest tool version
393 """
394 l_text = [f"New version available! \nUpdate to version {newer_tool_version}?",
395 f"Neue Version verfügbar! \nUpdate auf Version {newer_tool_version} durchführen?"]
396 l_remember = ["Don't show again",
397 "Nicht mehr anzeigen"]
398 s_title = self.model.c_language.get_language_text(["Update Service", "Update-Dienst"])
399 b_update, is_checked = open_message_box(parent=self,
400 title=s_title,
401 text=self.model.c_language.get_language_text(l_text),
402 btn_yes=self.model.c_language.get_language_text(L_YES),
403 btn_no=self.model.c_language.get_language_text(L_NO),
404 check_box=self.model.c_language.get_language_text(l_remember),
405 window_icon=ICON_APP)
406 if is_checked:
407 write_update_version(newer_tool_version) # write newest version for don't remember again
408 if b_update:
409 copy_to_clipboard(S_UPDATE_URL)
410 webbrowser.open_new_tab(S_UPDATE_URL)
411 sys.exit() # exit application without close dialog
412
413 def show_welcome_dialog(self) -> None:
414 """!
415 @brief Show welcome screen and choose admin password.
416 """
417 l_text = ["To change user and articles configuration, see the Help menu."
418 + "\n\nIf the Admin password is lost, all data can be reset:"
419 + "\n1. Select Admin User to show hidden menu"
420 + "\n2. Help→Reset"
421 + f"\n3. Enter reset code: {base64.b64decode(RESET_CODE).decode('utf-8')}"
422 + "\n\nChoose an Admin password (numbers only):",
423 "Um die Benutzer- und Artikelkonfiguration zu ändern, siehe im Menü Hilfe"
424 + "\n\nWenn das Admin-Passwort verloren geht, können alle Daten zurückgesetzt werden:"
425 + "\n1. Wählen Sie Admin User, um das ausgeblendete Menü anzuzeigen"
426 + "\n2. Help→Reset"
427 + f"\n3. Reset-Code eingeben: {base64.b64decode(RESET_CODE).decode('utf-8')}"
428 + "\n\nWählen Sie ein Admin-Passwort (nur Zahlen):"]
429 s_title = self.model.c_language.get_language_text(["Welcome to", "Willkommen zu"])
430 password, ok = input_dialog(self, f"{s_title} {__title__}", self.model.c_language.get_language_text(l_text), DEFAULT_CODE, icon=ICON_APP)
431 if ok and (password.isdigit() or password == ""):
432 self.model.c_config.d_user[EUser.ADMIN.value][S_PW] = str(password)
433 self.model.c_config.store_user_data()
434 else:
435 sys.exit() # exit application without close dialog
436
438 """!
439 @brief Show unauthenticated dialog.
440 """
441 s_text = self.model.c_language.get_language_text(["Unauthenticated use", "Unauthentifizierte Verwendung"])
442 l_text = ["You are not entitled to test a pre-release version."
443 + "\n\nUse a published version:"
444 + f"\n{__home__}",
445 "Sie sind nicht berechtigt, eine Vorabversion zu testen."
446 + "\n\nVerwenden Sie eine veröffentlichte Version:"
447 + f"\n{__home__}"]
448 b_accept, _ = open_message_box(parent=self,
449 title=s_text,
450 text=self.model.c_language.get_language_text(l_text),
451 btn_close=self.model.c_language.get_language_text(L_CLOSE),
452 btn_special=self.model.c_language.get_language_text(L_COPY_URL),
453 window_icon=ICON_APP)
454 if b_accept:
455 copy_to_clipboard(__home__)
456 sys.exit() # exit application without close dialog
457
458 def show_help_dialog(self) -> None:
459 """!
460 @brief Show help dialog.
461 """
462 log.debug("Starting help dialog")
463 icon = ICON_HELP_LIGHT if self.model.c_monitor.is_light_theme() else ICON_HELP_DARK
464 config_window(self.dialog_help, title=self.model.c_language.get_language_text(L_MENU_HELP), icon=icon, show=True)
465
466 def set_status(self, text_value: str | list[str], b_warning: bool = False, i_timeout: Optional[int] = None, b_highlight: bool = False, b_thread: bool = False) -> None:
467 """!
468 @brief Logs a status message to status bar (with timer) and logging handler
469 @param text_value : text to set
470 @param b_warning : [True] Text is a warning; [False] normal info
471 @param b_highlight : [True] highlight text; [False] normal text
472 @param i_timeout : timeout for statustext in "ms". If None use default time
473 @param b_thread : call from thread to prevent timer activations (not allowed from thread)
474 """
475 text = self.model.c_language.get_language_text(text_value) if isinstance(text_value, list) else text_value
476 if b_warning:
477 log.warning(text)
478 else:
479 log.info(text)
480
481 if self.gui_locked:
482 locked_text = self.model.c_language.get_language_text(["Window Locked!",
483 "Das Fenster ist gesperrt!"])
484 config_label(self.statusbar_left, text=locked_text, bg=LOCKED_STYLE)
485 else:
486 if not (not b_warning and self.b_warning_active):
487 if hasattr(self, "status_timer"):
488 if not b_thread: # start/stop time from thread is not allowed
489 if i_timeout is None:
490 if b_warning:
491 i_timeout = STATUS_WARNING_TEXT_TIME
492 else:
493 i_timeout = STATUS_HIGHLIGHT_TEXT_TIME if b_highlight else STATUS_TEXT_TIME
494 if i_timeout is None:
495 self.status_timer.stop()
496 else:
497 self.status_timer.start(i_timeout)
498 if b_warning:
499 foreground = None
500 background = WARNING_STYLE if self.model.c_monitor.is_light_theme() else WARNING_STYLE_DARK
501 self.b_warning_active = True
502 else:
503 foreground = HIGHLIGHT_STYLE if b_highlight else None
504 background = DEFAULT_STYLE
505 self.b_warning_active = False
506 config_label(self.statusbar_left, text=text, bg=background, fg=foreground)
507
508 def clear_status(self, b_override: bool = False) -> None:
509 """!
510 @brief Clear status bar text and set active user as default
511 @param b_override : status if actual status should override
512 """
513 if not self.gui_locked:
514 if not self.status_timer.isActive() or (b_override and not self.b_warning_active):
515 user = self.model.c_auth.s_login_user
516 if user and user[0] == EUser.B.name:
517 user_name = self.model.c_config.get_user_name(user)
518 if user_name != "":
519 user_name = f" ({user_name})"
520 else:
521 user_name = ""
522 if user:
523 user = user.replace("\n", " ") # prevent word wrapping for user in statusbar
524 s_user_text = f"{self.model.c_language.get_language_text(L_MENU_USER)}: {user}{user_name}"
525 config_label(self.statusbar_left, text=s_user_text, bg=DEFAULT_STYLE)
526 self.b_warning_active = False
527
528 def open_report_dialog(self, b_clear_report: bool, b_auto_print: bool) -> None:
529 """!
530 @brief Open report dialog.
531 @param b_clear_report : [True] create report and clear log; [False] show only status
532 @param b_auto_print : status if report was printed automatic
533 """
534 show_report_dialog(self, b_clear_report, b_auto_print)
535
536 def show_item_btns(self) -> None:
537 """!
538 @brief Update item buttons.
539 """
540 log.debug("Update item buttons")
541 for i, btn in enumerate(self.l_bnt_fnc):
542 i_pos = i + 1
543 if i < self.model.c_config.item_number_size:
544 if self.model.c_auth.check_user_login(L_CONFIG_USER):
545 # show only disabled numbers
546 config_btn(btn, enable=False, show=True, text=str(i_pos))
547 else:
548 s_item_name = self.model.c_config.get_item_button_name(i_pos)
549 s_visible_user = self.model.c_config.get_item_visible_user(i_pos)
550 if (s_item_name != "") and ((s_visible_user is None) or self.model.c_auth.check_user_login(s_visible_user)):
551 if self.model.b_show_price:
552 f_item_price = self.model.c_config.get_item_price(i_pos)
553 s_item_name += f"\n{f_item_price:.2f} {S_UNIT_SYMBOL}"
554 config_btn(btn, enable=True, show=True, text=s_item_name)
555 else:
556 config_btn(btn, show=False)
557 if self.model.c_report.l_marked_items[i]:
558 background_color = "orange"
559 elif self.model.c_auth.check_user_login(EUser.FREE):
560 background_color = "lightgreen" if self.model.c_monitor.is_light_theme() else "darkgreen"
561 else:
562 select_bg = self.model.c_config.get_item_background(i_pos)
563 if select_bg:
564 background_color = select_bg
565 else:
566 background_color = None
567 if background_color is None:
568 config_btn(btn, style=self.model.c_monitor.default_button_stylesheet)
569 else:
570 config_btn(btn, bg=background_color)
571 else:
572 config_btn(btn, show=False)
573 config_btn(self.btn_lock, enable=True)
574
575 def show_login_btns(self) -> None:
576 """!
577 @brief Update login buttons.
578 """
579 log.debug("Update login buttons")
580 for i, btn in enumerate(self.l_bnt_fnc):
581 if i < ItemNumber.MIN:
582 pressed_button = L_LOGIN_KEYS[i]
583 s_name = pressed_button
584 if not pressed_button.isdigit():
585 user_name = self.model.c_config.get_user_name(L_LOGIN_KEYS[i])
586 if user_name != "":
587 s_name = "\n".join([s_name + ":"] + user_name.split())
588 config_btn(btn, enable=True, show=True, text=s_name)
589 background_color = None
590 match pressed_button:
591 case EUser.ADMIN:
592 background_color = "red"
593 case EUser.HOST:
594 background_color = "brown"
595 case EUser.FREE:
596 background_color = "green"
597 case EUser.LOCAL:
598 background_color = "blue"
599 case EUser.USER:
600 background_color = "lightgray" if self.model.c_monitor.is_light_theme() else "#141414"
601 case _:
602 if pressed_button[0] == EUser.B:
603 background_color = "lightblue" if self.model.c_monitor.is_light_theme() else "darkblue"
604 if background_color is None:
605 config_btn(btn, style=self.model.c_monitor.default_button_stylesheet)
606 else:
607 config_btn(btn, bg=background_color)
608
609 # enable user button if activated
610 pw = self.model.c_config.get_user_pw(pressed_button)
611 b_enable_user = bool((pressed_button in self.model.c_config.d_user)
612 and (S_PW in self.model.c_config.d_user[pressed_button])
613 and (pw.isdigit() or pw == ""))
614 match pressed_button:
615 case EUser.ADMIN:
616 b_enable_user = True # admin is already enabled to be able to reset config data
617 case EUser.HOST | EUser.FREE | EUser.LOCAL:
618 pass # b_enable_user is correct
619 case EUser.USER:
620 b_enable_user = False
621 case _:
622 if pressed_button[0] == EUser.B:
623 pass # b_enable_user is correct
624 else:
625 b_enable_user = bool(self.model.c_auth.s_login_user is not None) # numbers are enabled for select user
626 config_btn(btn, enable=b_enable_user)
627 else:
628 config_btn(btn, show=False)
629 config_btn(self.btn_lock, enable=False)
630 config_btn(self.btn_print, enable=False)
631
632 def show_number_btns(self) -> None:
633 """!
634 @brief Update number buttons.
635 """
636 log.debug("Update number buttons")
637 for i, btn in enumerate(self.l_bnt_fnc):
638 if i < ItemNumber.MIN:
639 s_name = L_LOGIN_KEYS[i]
640 match s_name:
641 case EUser.ADMIN:
642 s_name = S_DOT
643 b_show = bool(self.model.c_auth.e_view == EWindowView.CALC)
644 case EUser.HOST:
645 s_name = S_DOUBLE_NULL
646 b_show = True
647 case _:
648 b_show = bool((len(s_name) == 1) and s_name.isnumeric())
649 b_enable = bool((s_name not in L_SPECIAL_BTNS)
650 or ((self.model.c_calc.s_text_total != "") and ((s_name != S_DOT) or (S_DOT not in self.model.c_calc.s_text_total))))
651 config_btn(btn, text=s_name, enable=b_enable, style=self.model.c_monitor.default_button_stylesheet, show=b_show)
652 else:
653 config_btn(btn, show=False)
654 config_btn(self.btn_lock, enable=True)
655 match self.model.c_auth.e_view:
656 case EWindowView.MULTI:
657 config_btn(self.btn_multi, enable=True, show=True)
658 config_btn(self.btn_calc, enable=False, show=False)
659 case EWindowView.CALC:
660 config_btn(self.btn_multi, enable=False, show=False)
661 config_btn(self.btn_calc, enable=True, show=True)
662 case _:
663 self.set_status("Invalid Number Button Code", True) # state not possible
664 config_btn(self.btn_print, show=False)
665
666 def update_table(self) -> None:
667 """!
668 @brief Update table and print/clear button.
669 """
670 log.debug("Update table")
671 self.model.c_auth.refresh_timer() # refresh timer for every button click
672 clear_text = self.model.c_language.get_language_text(L_CLEAR)
673 l_row_description = self.model.c_language.get_table_row_description()
674 b_items_in_list = False
675 b_update_clear_btn = True
676 table = self.table_items
677 config_table(table, single_row_selection=not self.model.c_calc.b_hold_last_print,
678 row_description=l_row_description,
679 font=self.model.c_monitor.table_font, auto_size=True) # auto to set correct for style change
680 if self.model.c_calc.b_hold_last_print:
681 if self.model.c_auth.e_view == EWindowView.CALC: # hold items in calculator mode
682 b_clear_btn_enable = bool((self.model.c_calc.s_text_total != "") or (self.model.c_calc.i_multi > 0)) # enable status in multi/calc mode
683 fg_color = "red" if b_clear_btn_enable else "grey"
684 config_btn(self.btn_clear, enable=b_clear_btn_enable, text=clear_text, fg=fg_color, icon="")
685 background_color = "lightgreen" if self.model.c_monitor.is_light_theme() else "darkgreen"
686 config_text(self.text_total, bg=background_color, fg="black") # update color if theme changed while hold
687 self.model.c_calc.b_hold_last_print = False
688 else:
689 i = 0
690 f_total = 0.0
691 for i_item_pos, entry in enumerate(self.model.c_report.l_bar_items):
692 self.model.c_report.l_table_position[i_item_pos] = None
693 if entry != 0:
694 b_items_in_list = True
695 fg = None
696 item_name = self.model.c_config.get_item_name(i_item_pos + 1)
697 if self.model.c_auth.check_user_login(EUser.FREE):
698 f_price = 0.0
699 else:
700 f_item_price = self.model.c_config.get_item_price(i_item_pos + 1)
701 f_price = f_item_price * entry
702 f_total += f_price
703 bg = "orange" if self.model.c_report.l_marked_items[i_item_pos] else None
704 insert_table_item(table, i, [str(entry),
705 item_name,
706 f"{f_price:.2f} {S_UNIT_SYMBOL}"],
707 font=self.model.c_monitor.table_font,
708 fg=fg,
709 bg=bg)
710 self.model.c_report.l_table_position[i_item_pos] = i # save table position of item to delete single entries
711 i += 1
712
713 config_table(table, row_count=i, auto_size=True)
714
715 if self.model.c_auth.b_login_state and (self.model.c_auth.e_view == EWindowView.MAIN):
716 self.model.c_calc.s_text_total = f"{f_total:.2f} {S_UNIT_SYMBOL}"
717 if self.model.c_monitor.is_light_theme():
718 config_text(self.text_total, bg="white", fg="black")
719 else:
720 config_text(self.text_total, bg="black", fg="white")
721 if get_selected_table_column(self.table_items) != 0: # clear selected table position not if first column is selected
722 config_table(table, clear_selection=True) # clear selection at end of update to prevent that last single item marker jump to next
723
724 # update print button text
725 open_text = self.model.c_language.get_language_text(L_OPEN)
726 b_items_in_list = b_items_in_list or (sum(self.model.c_report.l_bar_items) != 0)
727 if self.model.c_printer.b_select_com_port_available:
728 if b_items_in_list or not self.model.c_auth.b_login_state:
729 s_btn_print_text = self.model.c_language.get_language_text(L_PRINT)
730 else:
731 s_btn_print_text = open_text
732 else:
733 s_btn_print_text = self.model.c_language.get_language_text(L_SAVE)
734
735 # set total text
736 if self.model.c_calc.i_multi > 1:
737 text = f"{self.model.c_calc.i_multi} x"
738 elif self.model.c_calc.f_back is not None:
739 to_pay_text = self.model.c_language.get_language_text(["to pay", "zu zahlen"])
740 given_text = self.model.c_language.get_language_text(["Given", "Gegeben"])
741 s_given_sum = f"{float(self.model.c_calc.f_payed):.2f}"
742 back_text = self.model.c_language.get_language_text(["Back", "Zurück"])
743 s_back_sum = f"{self.model.c_calc.f_back:.2f}" if (self.model.c_calc.f_back >= 0) else "---"
744 text = f"{to_pay_text}: {self.model.c_calc.f_total:.2f} {S_UNIT_SYMBOL}"
745 text += f"\n{given_text}: {s_given_sum} {S_UNIT_SYMBOL}"
746 text += f"\n{back_text}: {s_back_sum} {S_UNIT_SYMBOL}"
747 self.model.c_calc.f_back = None
748 self.model.c_calc.f_total = 0
749 else:
750 text = str(self.model.c_calc.s_text_total)
751 config_text(self.text_total, text=text)
752
753 # enable/disable/show/hide table buttons
754 b_no_config_user = not self.model.c_auth.check_user_login(L_CONFIG_USER)
755 b_sell_user = bool(self.model.c_auth.b_login_state and b_no_config_user)
756 config_btn(self.btn_multi, enable=b_sell_user)
757 config_btn(self.btn_calc, enable=(self.model.c_auth.check_user_login(EUser.LOCAL) and self.model.c_auth.b_login_state and (self.model.c_calc.f_total != 0)))
758 if b_update_clear_btn:
759 match self.model.c_auth.e_view:
760 case EWindowView.CALC | EWindowView.MULTI:
761 b_clear_btn_enable = bool((self.model.c_calc.s_text_total != "") or (self.model.c_calc.i_multi > 0)) # enable status in multi/calc mode
762 case _:
763 b_clear_btn_enable = bool(b_items_in_list or ((not self.model.c_auth.b_login_state) and (self.model.c_calc.s_text_total != "")))
764 fg_color = "red" if b_clear_btn_enable else "grey"
765 config_btn(self.btn_clear, enable=b_clear_btn_enable, text=clear_text, fg=fg_color, icon="")
766 b_enable_print = bool((not self.model.c_auth.check_user_login(EUser.USER))
767 and (b_items_in_list or ((s_btn_print_text == open_text) and self.model.c_auth.b_login_state and not self.model.c_auth.check_user_login(EUser.B))))
768 config_btn(self.btn_print, enable=b_enable_print, text=s_btn_print_text)
769 if self.model.c_auth.e_view == EWindowView.MAIN:
770 config_btn(self.btn_multi, show=True)
771 config_btn(self.btn_calc, show=True)
772 config_btn(self.btn_clear, show=True)
773 config_btn(self.btn_print, show=True)
774 self.model.c_monitor.resize_window(b_update_only_total_font=True) # update only total text font size for pay back
775
776 def update_menu(self) -> None:
777 """!
778 @brief Update menu.
779 """
780 b_admin_settings = bool(self.model.c_auth.b_login_state and (self.model.c_auth.check_user_login(EUser.ADMIN)))
781 b_printer_available = bool(self.model.c_printer.s_select_com_port is not None)
782 b_edit_user = self.model.c_auth.b_login_state and (self.model.c_auth.check_user_login(L_CONFIG_USER))
783 b_log_exist = self.model.c_report.check_sales_exist()
784 b_print_menu = (self.model.b_enable_report or self.model.c_auth.check_user_login(EUser.ADMIN)) and self.model.c_auth.b_login_state and (not self.model.c_auth.check_user_login(EUser.FREE))
785 config_menu(self.menu_prints, show=b_print_menu)
786 config_menu(self.action_show_interim, enable=b_log_exist, show=self.model.b_enable_report)
787 config_menu(self.action_open_folder, show=b_edit_user)
788 config_menu(self.menu_combine_report, show=b_edit_user)
789 config_menu(self.action_print_file, enable=b_printer_available, show=b_edit_user)
790 config_menu(self.action_article_preview, show=b_edit_user)
791 config_menu(self.action_enable_report, show=b_admin_settings)
792 config_menu(self.action_create_billing, enable=b_log_exist, show=b_edit_user and self.model.b_enable_report)
793 config_menu(self.action_auto_export, show=False)
794 config_menu(self.menu_configuration, show=b_edit_user)
795 config_menu(self.action_user_import, enable=b_admin_settings)
796 config_menu(self.action_user_export, enable=b_admin_settings)
797 config_menu(self.action_user_new, enable=b_admin_settings)
798 config_menu(self.menu_articles, enable=b_admin_settings)
799 config_menu(self.action_articles_pdf_import, show=False)
800 config_menu(self.action_change_path, enable=b_admin_settings)
801 config_menu(self.action_webserver, show=False)
802 config_menu(self.menu_com_port, enable=b_admin_settings)
803 config_menu(self.menu_paper_width, enable=b_admin_settings)
804 config_menu(self.menu_smartcard, show=False)
805 config_menu(self.menu_display_port, show=False)
806 config_menu(self.action_auto_import, show=False)
807 config_menu(self.menu_log_verbosity, show=b_admin_settings)
808 config_menu(self.action_paper_change, show=False)
809 config_menu(self.action_reset, show=self.model.c_auth.check_user_login(EUser.ADMIN))
810
811 def update_screen(self) -> None:
812 """!
813 @brief Update complete screen.
814 """
815 log.debug("Update screen")
816 if not self.gui_locked:
817 self.model.c_monitor.check_for_style_change() # check every complete update cycle if style changed
818 if self.model.c_auth.b_login_state:
819 match self.model.c_auth.e_view:
820 case EWindowView.MULTI | EWindowView.CALC:
821 self.show_number_btns()
822 case _:
823 self.show_item_btns()
824 else:
825 self.model.c_report.clear_print_items()
826 self.show_login_btns()
827 self.update_table()
828 self.update_menu()
829 if self.model.c_config.item_number_size != ItemNumber.MIN: # update only required in extended item view for different button rows
830 self.model.c_monitor.resize_window()
831 else:
832 log.debug("Screen is locked - no update")
833
834 def btn_item_clicked(self, i_item_number: int) -> None:
835 """!
836 @brief Handle item button/login (1-30) clicked.
837 @param i_item_number : button index that was clicked
838 """
839 log.debug("Item button %s clicked", i_item_number)
840 if self.model.c_auth.b_login_state: # item mode
841 if not self.b_btn_hold:
842 match self.model.c_auth.e_view:
843 case EWindowView.MULTI | EWindowView.CALC:
844 s_name = L_LOGIN_KEYS[i_item_number]
845 match s_name:
846 case EUser.ADMIN:
847 s_name = S_DOT
848 case EUser.HOST:
849 s_name = S_DOUBLE_NULL
850 case _:
851 if (len(s_name) == 1) and s_name.isnumeric():
852 pass # use this number
853 else:
854 self.set_status("Invalid Multiple Button pressed", True) # state not possible
855 s_name = ""
856 self.model.c_calc.s_text_total += s_name
857 self.show_number_btns()
858 case _:
859 b_add_item = True
860 if self.model.c_calc.i_multi > 0:
861 i_factor = self.model.c_calc.i_multi
862 self.model.c_calc.i_multi = 0
863 else:
864 i_factor = 1
865 if b_add_item:
866 i_item_index = i_item_number
867 self.model.c_report.l_bar_items[i_item_index] += i_factor # add item
868 self.model.c_calc.f_total = 0
869 if self.model.c_sound.b_sound:
870 self.model.c_sound.c_sound_touch.play()
871 if self.model.c_auth.e_view == EWindowView.CALC: # hold items in calculator mode
872 self.model.c_calc.b_hold_last_print = True
873 self.update_table()
874 else: # login mode
875 s_key = L_LOGIN_KEYS[i_item_number]
876 if len(s_key) == 1: # number pressed
877 if self.model.c_auth.s_login_user is None:
878 self.set_status("None user input password", True) # state not possible
879 return
880 self.model.c_auth.s_pw += s_key
881 pw = self.model.c_config.get_user_pw(self.model.c_auth.s_login_user)
882 if len(self.model.c_auth.s_pw) == len(pw):
883 if self.model.c_auth.s_pw == pw:
884 self.model.c_auth.log_in()
885 else:
886 self.model.c_calc.s_text_total = self.model.c_auth.s_login_user
887 self.model.c_auth.s_pw = ""
888 else:
889 self.model.c_calc.s_text_total = "*" * len(self.model.c_auth.s_pw)
890 else: # user pressed
891 self.model.c_auth.s_pw = ""
892 self.model.c_auth.s_login_user = s_key
893 self.model.c_calc.s_text_total = s_key
894 pw = self.model.c_config.get_user_pw(s_key)
895 if pw == "":
896 self.model.c_auth.log_in() # no pw for no user
897 self.model.c_calc.f_total = 0
898 if self.model.c_sound.b_sound:
899 if self.model.c_auth.b_login_state:
900 self.model.c_sound.c_sound_unlock.play()
901 else:
902 self.model.c_sound.c_sound_touch.play()
903 self.update_screen()
904 self.b_btn_hold = False
905
906 def btn_item_pressed(self, i_item_number: int) -> None:
907 """!
908 @brief Handle item button/login (1-30) pressed.
909 @param i_item_number : button index that was pressed
910 """
911 log.debug("Item button %s pressed", i_item_number)
912 if self.model.c_auth.b_login_state and (self.model.c_auth.e_view == EWindowView.MAIN):
913 self.btn_timer.start(I_BTN_LONG_PRESS_TIME)
914 self.i_btn_hold_idx = i_item_number
915
916 def btn_item_released(self, i_item_number: int) -> None:
917 """!
918 @brief Handle item button/login (1-30) released.
919 @param i_item_number : button index that was released
920 """
921 log.debug("Item button %s released", i_item_number)
922 self.btn_timer.stop()
923
924 def btn_item_held(self) -> None:
925 """!
926 @brief Item button held.
927 """
928 log.debug("Item held")
929 self.b_btn_hold = True
930 self.model.c_report.l_marked_items[self.i_btn_hold_idx] = not self.model.c_report.l_marked_items[self.i_btn_hold_idx]
931 self.update_screen()
932
933 def btn_multi_clicked(self) -> None:
934 """!
935 @brief Handle multi button clicked.
936 """
937 log.debug("Multi button clicked")
938 match self.model.c_auth.e_view:
939 case EWindowView.MULTI:
940 if self.model.c_calc.s_text_total == "":
941 self.model.c_calc.s_text_total = str(0) # if multi btn clicked without input
942 self.model.c_calc.i_multi = int(self.model.c_calc.s_text_total)
943 self.model.c_auth.e_view = EWindowView.MAIN
944 case _:
945 self.model.c_calc.s_text_total = ""
946 self.model.c_calc.i_multi = 0
947 self.model.c_auth.e_view = EWindowView.MULTI
948 if self.model.c_sound.b_sound:
949 self.model.c_sound.c_sound_touch.play()
950 self.update_screen()
951
952 def btn_calc_clicked(self) -> None:
953 """!
954 @brief Handle calc button clicked.
955 """
956 log.debug("Calc button clicked")
957 b_last_calc = bool(self.model.c_auth.e_view == EWindowView.CALC)
958 match self.model.c_auth.e_view:
959 case EWindowView.CALC:
960 if self.model.c_calc.s_text_total == "":
961 self.model.c_calc.s_text_total = str(0) # if calc btn clicked without input
962 self.model.c_calc.f_payed = float(self.model.c_calc.s_text_total)
963 if self.model.c_calc.f_total > self.model.c_calc.f_payed:
964 self.set_status(["Not enough Money", "Nicht genug Geld"], b_highlight=True)
965 self.model.c_calc.f_back = self.model.c_calc.f_payed - self.model.c_calc.f_total
966 self.model.c_auth.e_view = EWindowView.MAIN
967 case _:
968 self.model.c_calc.s_text_total = ""
969 self.model.c_calc.f_back = None
970 self.model.c_auth.e_view = EWindowView.CALC
971 if self.model.c_sound.b_sound:
972 self.model.c_sound.c_sound_touch.play()
973 self.model.c_calc.b_hold_last_print = True
974 self.update_screen()
975 if b_last_calc: # Disable and set CLR button TODO update screen setzen
976 config_btn(self.btn_clear, enable=False, text=self.model.c_language.get_language_text(L_CLEAR), fg="grey", icon="")
977
978 def clear_articles(self) -> None:
979 """!
980 @brief Clear articles.
981 """
982 i_select_item = None
983 selected_row = get_selected_table_row(self.table_items)
984 selected_column = get_selected_table_column(self.table_items)
985 if selected_row is not None:
986 for i_pos, table_pos in enumerate(self.model.c_report.l_table_position):
987 if table_pos == selected_row:
988 i_select_item = i_pos
989 break
990 if i_select_item is None:
991 self.model.c_report.clear_print_items()
992 else:
993 if self.model.c_report.l_bar_items[i_select_item] != 0:
994 if selected_column == 2: # clear all counts if price column selected
995 self.model.c_report.l_bar_items[i_select_item] = 0 # delete all item counts
996 else:
997 self.model.c_report.l_bar_items[i_select_item] -= 1 # delete single item
998 else:
999 self.set_status(f"Not possible to delete single item at position {selected_row}", True) # state not possible
1000
1001 def btn_clear_clicked(self) -> None:
1002 """!
1003 @brief Handle clear button clicked.
1004 """
1005 log.debug("Clear button clicked")
1006 b_ec_pressed = False
1007 if self.model.c_auth.b_login_state:
1008 match self.model.c_auth.e_view:
1009 case EWindowView.MULTI | EWindowView.CALC:
1010 self.model.c_calc.s_text_total = ""
1011 case _:
1012 btn_text = get_btn_text(self.btn_clear)
1013 if self.model.c_language.get_language_text(L_CLEAR) == btn_text:
1014 self.clear_articles()
1015 else:
1016 pass
1017 else: # user view
1018 if self.model.c_auth.s_pw != "":
1019 self.model.c_auth.s_pw = ""
1020 if self.model.c_auth.s_login_user is not None:
1021 self.model.c_calc.s_text_total = self.model.c_auth.s_login_user
1022 else:
1023 self.model.c_calc.s_text_total = ""
1024 self.set_status("None user input password", True) # state not possible
1025 else:
1026 self.model.c_auth.s_login_user = None
1027 self.model.c_calc.s_text_total = ""
1028 # play sound
1029 if self.model.c_sound.b_sound:
1030 sound = self.model.c_sound.c_sound_touch if b_ec_pressed else self.model.c_sound.c_sound_clear
1031 sound.play()
1032 if b_ec_pressed or (self.model.c_auth.e_view == EWindowView.CALC):
1033 self.model.c_calc.b_hold_last_print = True
1034 self.update_screen()
1035
1036 def print_selected_articles(self, l_item_log: list[Item], actual_datetime: datetime) -> float:
1037 """!
1038 @brief Print selected articles.
1039 @param l_item_log : add printed articles to this list
1040 @param actual_datetime : date for printed items
1041 @return total price
1042 """
1043 f_total_price = 0.0
1044 for i_item_pos, entry in enumerate(self.model.c_report.l_bar_items):
1045 if entry != 0: # items present and not free user for deposit
1046 i_item_number = i_item_pos + 1
1047 name = self.model.c_config.get_item_name(i_item_number)
1048 f_single_price = self.model.c_config.get_item_price(i_item_number)
1049 group = self.model.c_config.get_item_group(i_item_number)
1050 number = i_item_number
1051 b_print = self.model.c_config.get_print_article_status()
1052 if b_print:
1053 b_print = self.model.c_config.get_item_print_status(i_item_pos + 1)
1054 b_free_user = self.model.c_auth.check_user_login(EUser.FREE)
1055 f_price = 0.0 if b_free_user else f_single_price * entry
1056 f_total_price += f_price
1057 l_item_log.append(create_item(entry, name, number, group, f_single_price, f_price, self.model.c_auth.s_login_user, actual_datetime, b_print))
1058 return f_total_price
1059
1060 def btn_print_clicked(self, b_smartcard: bool = False) -> None:
1061 """!
1062 @brief Handle print button clicked.
1063 @param b_smartcard : status if print triggered by smart card
1064 """
1065 log.debug("Print button clicked")
1066 btn_text = get_btn_text(self.btn_print)
1067 if btn_text != self.model.c_language.get_language_text(L_OPEN):
1068 if self.model.c_printer.b_select_com_port_available or self.model.c_printer.s_select_com_port is None:
1069 i_user_pos = self.model.c_auth.get_user_position()
1070 if i_user_pos is not None:
1071 i_number_of_items = sum(self.model.c_report.l_bar_items)
1072 if i_number_of_items != 0:
1073 if i_number_of_items <= I_MAX_ITEMS_TO_PRINT:
1074 l_item_log: list[Item] = []
1075 now = datetime.now()
1076 self.model.c_calc.f_total = 0.0
1077 # add articles
1078 self.model.c_calc.f_total += self.print_selected_articles(l_item_log, now)
1079 # open drawer
1080 if self.model.c_config.get_auto_open() and (self.model.c_auth.check_user_login(EUser.LOCAL)): # No auto open for free user
1081 self.model.c_printer.open_drawer()
1082 self.model.c_printer.add_to_queue(l_item_log, i_user_pos, self.model.c_calc.f_total)
1083 if self.model.c_sound.b_sound:
1084 self.model.c_sound.c_sound_print.play()
1085 background_color = "lightgreen" if self.model.c_monitor.is_light_theme() else "darkgreen"
1086 config_text(self.text_total, bg=background_color, fg="black")
1087 if b_smartcard:
1088 self.model.c_auth.s_login_user = EUser.USER.value # set back to user after smartcard print
1089 elif self.model.c_auth.check_user_login(EUser.B): # logout user after print
1090 self.model.c_auth.log_out()
1091 elif self.model.c_auth.check_user_login(EUser.FREE): # check free user auto logout
1092 if self.model.c_config.get_free_auto_logout():
1093 self.model.c_auth.log_out()
1094 self.model.c_calc.b_hold_last_print = True
1095 self.model.c_report.clear_print_items()
1096 else:
1097 self.set_status([f"Maximal {I_MAX_ITEMS_TO_PRINT} articles allowed to print",
1098 f"Maximal {I_MAX_ITEMS_TO_PRINT} Artikel möglich zu drucken"], b_highlight=True)
1099 else:
1100 self.set_status(["No Items to print", "Keine Artikel zu drucken"], True)
1101 self.update_screen()
1102 else:
1103 self.set_status([f"No Print: {self.model.c_printer.s_select_com_port} not available",
1104 f"Kein Druck: {self.model.c_printer.s_select_com_port} nicht verfügbar"], True)
1105 else:
1106 if not self.model.c_auth.check_user_login(EUser.B):
1107 self.model.c_printer.open_drawer()
1108 self.set_status(["Open cash drawer", "Öffne Kassenschublade"])
1109 else:
1110 self.set_status("Stuff cant open cash drawer", True) # state not possible
1111 if self.model.c_sound.b_sound:
1112 self.model.c_sound.c_sound_touch.play()
1113
1114 def btn_lock_clicked(self, b_auto_logout: bool = False) -> None:
1115 """!
1116 @brief Handle lock button clicked.
1117 @param b_auto_logout : [True] automatic logout (timeout); [False] manual logout
1118 """
1119 if b_auto_logout:
1120 log.debug("Auto logout")
1121 else:
1122 log.debug("Lock button clicked")
1123 if self.model.c_sound.b_sound:
1124 self.model.c_sound.c_sound_touch.play()
1125 self.model.c_calc.s_text_total = ""
1126 self.model.c_auth.log_out()
1127 self.update_screen()
1128
1129 def btn_report_print(self, b_clear_report: bool) -> None:
1130 """!
1131 @brief Handle report or status button clicked.
1132 @param b_clear_report : [True] create report and clear log; [False] show only status
1133 """
1134 b_call_report = True
1135 if b_clear_report:
1136 b_call_report = self.confirm_dialog(["Create Report & clear", "Bericht erstellen & löschen"], s_icon_path=ICON_REPORT_LIGHT)
1137 if b_call_report:
1138 self.set_status(["Show Status", "Zwischenstand anzeigen"])
1139 self.model.c_report.create_report(b_clear_report=b_clear_report, d_item=self.model.c_config.d_item, b_open_folder=B_NOTEPAD_REPORT)
1140 self.block_ui()
1141 if B_NOTEPAD_REPORT:
1142 with open(S_REPORT_TEMP_FILE, mode="w", encoding="utf-8") as file:
1143 file.write(self.model.c_report.s_report_text)
1144 self.model.notepad_worker.open_report_file(S_REPORT_TEMP_FILE, ENotepadSelection.STATUS, self.unblock_ui)
1145
1146 if self.model.c_printer.s_select_com_port is not None:
1147 if b_clear_report and (not self.model.c_auth.check_user_login(EUser.ADMIN)): # no auto print for admin
1148 b_print_report = True
1149 else:
1150 if B_NOTEPAD_REPORT:
1151 b_print_report = self.confirm_dialog(["Print out status", "Status drucken"], s_icon_path=ICON_PRINTER_LIGHT)
1152 else:
1153 b_print_report = False
1154 if b_print_report:
1155 self.model.c_printer.add_to_report(self.model.c_report.s_report_text)
1156 else:
1157 b_print_report = False
1158
1159 if not B_NOTEPAD_REPORT:
1160 self.open_report_dialog(b_clear_report, b_print_report)
1161 self.unblock_ui()
1162 self.update_menu()
1163
1164 def btn_change_user(self, e_config_selection: EConfigSelection) -> None:
1165 """!
1166 @brief Handle change user configuration.
1167 @param e_config_selection : mode of user change (edit/import/export/reset)
1168 """
1169 match e_config_selection:
1170 case EConfigSelection.RESET | EConfigSelection.EDIT:
1171 if e_config_selection == EConfigSelection.RESET:
1172 d_user_dict = D_DEFAULT_USER.copy() # copy that admin not delete in global data
1173 d_user_dict[EUser.ADMIN.value][S_PW] = DEFAULT_CODE
1174 b_reset = self.confirm_dialog(["Reset user", "Benutzer zurücksetzen"], s_icon_path=ICON_USER_RESET_LIGHT)
1175 else:
1176 d_user_dict = self.model.c_config.d_user.copy() # copy that admin not delete in global data
1177 b_reset = True
1178 if b_reset:
1179 if not self.model.c_auth.check_user_login(EUser.ADMIN): # only Admin can view his PW
1180 if EUser.ADMIN.value in d_user_dict:
1181 del d_user_dict[EUser.ADMIN.value]
1182 else:
1183 self.set_status(["No Admin user in config data",
1184 "Kein Admin Nutzer in den Konfigurationsdaten hinterlegt"], True)
1185 write_config_to_file(S_USER_TEMP_FILE, d_user_dict)
1186 self.block_ui()
1187 self.edit_user_in_notepad(S_USER_TEMP_FILE)
1188 case EConfigSelection.IMPORT:
1189 l_title = ["Select user configuration file to import", "Wähle eine Benutzer Konfigurationsdatei zum importieren"]
1190 config_import_file = open_file(parent=self, title=self.model.c_language.get_language_text(l_title),
1191 directory=self.model.get_last_path(), filetypes=INI_FILE_TYPES)
1192 if isinstance(config_import_file, str):
1193 if config_import_file:
1194 self.model.set_last_path(os.path.dirname(config_import_file))
1195 b_imported = self.model.c_config.read_user_file(config_import_file)
1196 if b_imported:
1197 self.set_status([f"User configuration imported: {config_import_file}",
1198 f"Benutzer Konfigurationsdaten importiert: {config_import_file}"])
1199 else:
1200 self.set_status("Config import file is not string", True) # state not possible
1201 case EConfigSelection.EXPORT:
1202 l_title = ["Export user configuration file", "Benutzer Konfigurationsdatei exportieren"]
1203 config_export_file = save_file(parent=self, title=self.model.c_language.get_language_text(l_title),
1204 directory=f"{self.model.get_last_path()}/{S_USER_FILE}",
1205 filetypes=INI_FILE_TYPES)
1206 if config_export_file:
1207 self.model.set_last_path(os.path.dirname(config_export_file))
1208 write_config_to_file(config_export_file, self.model.c_config.d_user)
1209 self.set_status([f"User configuration exported: {config_export_file}",
1210 f"Benutzer Konfigurationsdaten exportiert: {config_export_file}"])
1211 case _:
1212 self.set_status(f"Invalid user menu: {e_config_selection}", True) # state not possible
1213
1214 def edit_user_in_notepad(self, s_file_name: str) -> None:
1215 """!
1216 @brief Open user file in notepad.
1217 @param s_file_name : file name to open in notepad
1218 """
1219 self.model.notepad_worker.open_report_file(s_file_name, ENotepadSelection.USER, lambda: self.model.c_config.read_user_file(s_file_name, True))
1220
1221 def btn_change_item(self, e_config_selection: EConfigSelection) -> None:
1222 """!
1223 @brief Handle change articles configuration.
1224 @param e_config_selection : mode of articles change (edit/import/export/reset)
1225 """
1226 match e_config_selection:
1227 case EConfigSelection.RESET | EConfigSelection.EDIT:
1228 if e_config_selection == EConfigSelection.RESET:
1229 s_deposit_name = None
1230 d_articles_dict = get_default_item_config(s_deposit_name)
1231 b_reset = self.confirm_dialog(["Reset articles", "Artikel zurücksetzen"], s_icon_path=ICON_ARTICLES_RESET_LIGHT)
1232 else:
1233 d_articles_dict = self.model.c_config.d_item
1234 b_reset = True
1235 if b_reset:
1236 write_config_to_file(S_ITEM_TEMP_FILE, d_articles_dict)
1237 self.block_ui()
1238 self.edit_items_in_notepad(S_ITEM_TEMP_FILE)
1239 case EConfigSelection.IMPORT:
1240 l_title = ["Select article configuration file to import", "Wähle eine Artikel Konfigurationsdatei zum importieren"]
1241 config_import_file = open_file(parent=self, title=self.model.c_language.get_language_text(l_title),
1242 directory=self.model.get_last_path(), filetypes=INI_FILE_TYPES)
1243 if isinstance(config_import_file, str):
1244 if config_import_file:
1245 self.model.set_last_path(os.path.dirname(config_import_file))
1246 b_imported = self.model.c_config.read_item_file(config_import_file)
1247 if b_imported:
1248 self.set_status([f"Articles configuration imported: {config_import_file}",
1249 f"Artikel Konfigurationsdaten importiert: {config_import_file}"])
1250 else:
1251 self.set_status("Config import file is not string", True) # state not possible
1252 case EConfigSelection.EXPORT:
1253 l_title = ["Export article configuration file", "Artikel Konfigurationsdatei exportieren"]
1254 config_export_file = save_file(parent=self, title=self.model.c_language.get_language_text(l_title),
1255 directory=f"{self.model.get_last_path()}/{S_ITEM_FILE}",
1256 filetypes=INI_FILE_TYPES)
1257 if config_export_file:
1258 self.model.set_last_path(os.path.dirname(config_export_file))
1259 write_config_to_file(config_export_file, self.model.c_config.d_item)
1260 self.set_status([f"Articles configuration exported: {config_export_file}",
1261 f"Artikel Konfigurationsdaten exportiert: {config_export_file}"])
1262 case EConfigSelection.PDF_IMPORT:
1263 pass
1264 case _:
1265 self.set_status(f"Invalid articles menu: {e_config_selection}", True) # state not possible
1266
1267 def edit_items_in_notepad(self, s_file_name: str) -> None:
1268 """!
1269 @brief Open item file in notepad.
1270 @param s_file_name : file name to open in notepad
1271 """
1272 self.model.notepad_worker.open_report_file(s_file_name, ENotepadSelection.ARTICLES, lambda: self.model.c_config.read_item_file(s_file_name, True))
1273
1274 def btn_open_folder(self) -> None:
1275 """!
1276 @brief Handle Open output folder.
1277 """
1278 path = self.model.s_output_path
1279 if os.path.exists(path):
1280 log.debug("Open Folder: %s", path)
1281 self.set_status([f"Open Folder: {path}", f"Öffne Ordner: {path}"])
1282 with subprocess.Popen("explorer " + path.replace("/", "\\")):
1283 pass
1284 else:
1285 self.set_status([f"Folder not exist: {path}", f"Ordner existiert nicht: {path}"], True)
1286
1287 def btn_combine_manual(self) -> None:
1288 """!
1289 @brief Combine Reports manual to create single report from multiple other data.
1290 """
1291 l_title = ["Select report files to combine", "Wähle Berichte zum kombinieren"]
1292 l_select_files = open_file(parent=self, title=self.model.c_language.get_language_text(l_title),
1293 directory=self.model.get_last_path(), filetypes=LOG_FILE_TYPES,
1294 multiple=True)
1295 if isinstance(l_select_files, list):
1296 if l_select_files:
1297 self.model.set_last_path(os.path.dirname(l_select_files[0]))
1298 self.model.c_report.create_report(l_files=l_select_files, b_combine=True, b_clear_report=False,
1299 s_path=self.model.get_last_path(), b_open_folder=True)
1300 else:
1301 self.set_status("Combine files are not list", True) # state not possible
1302
1303 def btn_combine_auto(self) -> None:
1304 """!
1305 @brief Combine Reports manual to create single report from multiple other data.
1306 """
1307 l_title = ["Select directory to combine", "Wähle ein Verzeichnis zum kombinieren"]
1308 directory = open_directory(parent=self, title=self.model.c_language.get_language_text(l_title), directory=self.model.get_last_path())
1309 if directory:
1310 self.model.set_last_path(directory)
1311 l_files = []
1312
1313 for root, _dirs, files in os.walk(directory):
1314 for file in files:
1315 if file.endswith(".csv"):
1316 l_files.append(os.path.join(root, file))
1317
1318 if l_files:
1319 self.model.c_report.create_report(l_files=l_files, b_combine=True, b_clear_report=False,
1320 s_path=self.model.get_last_path(), b_open_folder=True)
1321 else:
1322 self.set_status(["No CSV files found", "Keine CSV Dateien gefunden"])
1323
1324 def btn_print_file(self) -> None:
1325 """!
1326 @brief Select and print markdown file from directory.
1327 """
1328 if self.model.c_printer.s_select_com_port is not None:
1329 l_title = ["Select file to print", "Datei zum Drucken auswählen"]
1330 select_file = open_file(parent=self, title=self.model.c_language.get_language_text(l_title),
1331 directory=self.model.get_last_path(), filetypes=REPORT_FILE_TYPES)
1332 if isinstance(select_file, str):
1333 if select_file:
1334 with open(select_file, mode="r", encoding="utf-8") as file:
1335 text = file.read()
1336 self.model.c_printer.add_to_report(text)
1337 self.set_status([f"Print file: {select_file}", f"Datei wird gedruckt: {select_file}"])
1338 else:
1339 self.set_status("Print file is not string", True) # state not possible
1340 else:
1341 self.set_status(["File cannot be printed. Printer not available.",
1342 "Datei kann nicht gedruckt werden. Drucker nicht verfügbar."], b_warning=True)
1343
1344 def btn_article_preview(self) -> None:
1345 """!
1346 @brief Article Print Preview
1347 """
1348 self.block_ui()
1349 l_preview_text = []
1350 i_break_line = int(self.model.c_printer.i_line_break / 2)
1351 line_text = "Name".ljust(i_break_line) + " | Group | Price "
1352 line_text += "| Cut"
1353 l_preview_text.append(line_text)
1354 line_text = "-" * (i_break_line + 50)
1355 l_preview_text.append(line_text)
1356
1357 for i_item_pos in range(I_ITEM_ARRAY_SIZE):
1358 i_item_number = i_item_pos + 1
1359 name = self.model.c_config.get_item_name(i_item_number)
1360 b_show = True
1361 if name != "" and b_show:
1362 group = self.model.c_config.get_item_group(i_item_number)
1363 f_item_price = self.model.c_config.get_item_price(i_item_number)
1364 s_item_price = f"{f_item_price:.2f} {S_UNIT_SYMBOL}"
1365 s_item_price = s_item_price.rjust(8)
1366 line_text = f"{name.ljust(i_break_line)[:i_break_line]}"
1367 line_text += f" | {group}"
1368 line_text += f" |{s_item_price}"
1369 line_text += f"| {name[i_break_line:]}"
1370 l_preview_text.append(line_text)
1371
1372 preview_text = '\n'.join(l_preview_text)
1373 show_report_dialog(self, report_text=preview_text)
1374 self.unblock_ui()
1375
1376 def change_output_path(self) -> None:
1377 """!
1378 @brief Handle change output path.
1379 """
1380 l_title = ["Set output path", "Wähle ein Ausgabeverzeichnis"]
1381 s_path = open_directory(parent=self, title=self.model.c_language.get_language_text(l_title), directory=self.model.s_output_path)
1382 if s_path: # if not canceled
1383 self.model.set_last_path(s_path)
1384 self.model.update_output_path(s_path)
1385
1386 def reset_config(self) -> None:
1387 """!
1388 @brief Handle reset configuration.
1389 """
1390 log.debug("Reset clicked")
1391 b_reset, ok = reset_config_dialog(self)
1392 if b_reset:
1393 self.close_window_dialog = False
1394 close_app(self)
1395 elif ok:
1396 self.set_status(["Invalid Password", "Passwort ungültig"])
The view-controller for main window.
None resizeEvent(self, RESIZE_EVENT|None _event)
Default resize Event Method to handle change of window size.
None btn_item_clicked(self, int i_item_number)
Handle item button/login (1-30) clicked.
None show_number_btns(self)
Update number buttons.
None edit_user_in_notepad(self, str s_file_name)
Open user file in notepad.
None update_screen(self)
Update complete screen.
None show_update_dialog(self, str newer_tool_version)
Show Update dialog.
None show_login_btns(self)
Update login buttons.
None btn_item_held(self)
Item button held.
None open_report_dialog(self, bool b_clear_report, bool b_auto_print)
Open report dialog.
None set_ui(self, bool b_state)
Blocks/Unblock the main UI elements.
None show_welcome_dialog(self)
Show welcome screen and choose admin password.
None show_item_btns(self)
Update item buttons.
None update_table(self)
Update table and print/clear button.
None btn_item_released(self, int i_item_number)
Handle item button/login (1-30) released.
None block_ui(self)
Blocks the main UI elements.
bool confirm_dialog(self, str|list[str] title_value, str s_icon_path=ICON_APP, Optional[list[str]] l_optional_text=None)
Show confirm dialog to accept or cancel.
None btn_item_pressed(self, int i_item_number)
Handle item button/login (1-30) pressed.
None clear_articles(self)
Clear articles.
None __init__(self, UncaughtHook qt_exception_hook, LogConfig log_config, bool test_mode=False, bool authenticated=False, *Any args, **Any kwargs)
None closeEvent(self, CLOSE_EVENT|None event)
Default close Event Method to handle application close.
None set_status(self, str|list[str] text_value, bool b_warning=False, Optional[int] i_timeout=None, bool b_highlight=False, bool b_thread=False)
Logs a status message to status bar (with timer) and logging handler.
float print_selected_articles(self, list[Item] l_item_log, datetime actual_datetime)
Print selected articles.
None clear_status(self, bool b_override=False)
Clear status bar text and set active user as default.
None show_unauthenticated_dialog(self)
Show unauthenticated dialog.
None edit_items_in_notepad(self, str s_file_name)
Open item file in notepad.
Holds the data of the application.
Definition model.py:36
None connect_menu_com_port(ACTION menu, Callable[[str], None] function)
Connect menu for COM ports.