BonPrinter v1.2.0
Thermal Printer tool
Loading...
Searching...
No Matches
config.py
Go to the documentation of this file.
1"""!
2********************************************************************************
3@file config.py
4@brief configuration settings
5********************************************************************************
6"""
7
8import logging
9import os
10import configparser as cfg
11from configparser import ConfigParser, DuplicateSectionError, DuplicateOptionError
12import codecs
13from collections import OrderedDict
14from typing import Any, TYPE_CHECKING
15from datetime import datetime
16
17from Source.version import __title__
18from Source.Util.app_data import read_user_settings, write_user_settings, read_articles_settings, write_articles_settings, \
19 ItemNumber, EUser, D_DEFAULT_USER, D_DEFAULT_ITEM, S_GENERAL, DEFAULT_CODE, L_ALLOWED_USER, \
20 S_DEVICE, S_USER_TIMEOUT, S_AUTO_OPEN, S_FREE_AUTO_LOGOUT, S_NAME, S_PW, S_PRINTS, S_PRINT_ARTICLE, S_PRINT_FREE_PRICE, \
21 I_GROUP_1, I_GROUP_2, I_GROUP_3, S_TAX, S_USER_TAX, S_GROUP_1, S_GROUP_2, S_GROUP_3, S_HEADER, S_HEADER_1, S_HEADER_2, \
22 S_GROUP, S_PRICE, S_BREAK_LINE, S_USER_VISIBLE, S_PRINT, S_MARK, S_BACKGROUND
23if TYPE_CHECKING:
24 from Source.Controller.main_window import MainWindow
25
26S_ITEM_FILE = "articles.ini"
27S_ITEM_TEMP_FILE = "_temp_articles.ini"
28S_USER_FILE = "user.ini"
29S_USER_TEMP_FILE = "_temp_user.ini"
30
31log = logging.getLogger(__title__)
32
33
34def is_float(value: Any) -> bool:
35 """!
36 @brief Check if string can convert to float.
37 @param value : value to check
38 @return status if string can convert to float
39 """
40 try:
41 float(value)
42 b_float = True
43 except (TypeError, ValueError):
44 b_float = False
45 return b_float
46
47
48def is_int(value: Any) -> bool:
49 """!
50 @brief Check if string can convert to integer.
51 @param value : value to check
52 @return status if string can convert to integer
53 """
54 try:
55 int(value)
56 b_int = True
57 except (TypeError, ValueError):
58 b_int = False
59 return b_int
60
61
62def get_dict_float(d_dict: dict[str, dict[str, str]], s_section: str, s_key: str) -> float:
63 """!
64 @brief Get floating value in dictionary.
65 @param d_dict : dictionary
66 @param s_section : section
67 @param s_key : key
68 @return get floating value from dictionary. 0 if not present
69 """
70 f_float = 0.0
71 if s_section in d_dict:
72 value = d_dict[s_section].get(s_key)
73 if value is not None:
74 if is_float(value):
75 f_float = float(value)
76 return f_float
77
78
79def get_dict_int(d_dict: dict[str, dict[str, str]], s_section: str, s_key: str) -> int:
80 """!
81 @brief Get integer value in dictionary.
82 @param d_dict : dictionary
83 @param s_section : section
84 @param s_key : key
85 @return get integer value from dictionary. 0 if not present
86 """
87 i_int = 0
88 if s_section in d_dict:
89 value = d_dict[s_section].get(s_key)
90 if value is not None:
91 if is_int(value):
92 i_int = int(value)
93 return i_int
94
95
96def get_dict_value(d_dict: dict[str, dict[str, str]], s_section: str, s_key: str) -> str:
97 """!
98 @brief Get value from dictionary.
99 @param d_dict : dictionary
100 @param s_section : section
101 @param s_key : key
102 @return get integer value from dictionary. Nothing if not present
103 """
104 if s_section in d_dict and s_key in d_dict[s_section]:
105 value = str(d_dict[s_section][s_key])
106 else:
107 value = ""
108 return value
109
110
111class Config:
112 """!
113 @brief Class Configuration: Read/Write configuration data (user or articles)
114 @param ui : main window object
115 """
116
117 def __init__(self, ui: "MainWindow") -> None:
118 self.ui = ui
120 self.d_user = sort_dict(read_user_settings(), D_DEFAULT_USER)
121 self.d_item = sort_dict(read_articles_settings(), D_DEFAULT_ITEM)
122 self.item_number_size = ItemNumber.MIN
123 self.update_extended_article_status(prevent_resize=True)
124
125 def update_extended_article_status(self, prevent_resize: bool = False) -> None:
126 """!
127 @brief Update extended article status.
128 @param prevent_resize : prevent resize
129 """
130 item_number_size = ItemNumber.MIN
131 for i in range(ItemNumber.MAX, ItemNumber.MIN, -1):
132 i_item_number = i
133 s_item_name = self.get_item_button_name(i_item_number)
134 if s_item_name != "":
135 if i_item_number > ItemNumber.EXT:
136 item_number_size = ItemNumber.MAX
137 break
138 if i_item_number > ItemNumber.MIN:
139 item_number_size = ItemNumber.EXT
140 break
141 if self.item_number_size != item_number_size:
142 self.item_number_size = item_number_size
143 if not prevent_resize:
144 self.ui.model.c_monitor.resize_window() # resize only if extended view changed
145
146 def get_device_name(self) -> str:
147 """!
148 @brief Get smart card user status.
149 @return start card user activation status
150 """
151 origin_device_name = get_dict_value(self.d_user, S_GENERAL, S_DEVICE)
152 if not origin_device_name.isalnum(): # only letter and numbers allowed for file name
153 device_name = ""
154 else:
155 device_name = origin_device_name
156 return device_name
157
158 def get_logout_time(self) -> float:
159 """!
160 @brief Get timeout for inactive user.
161 @return logout timeout
162 """
163 return get_dict_float(self.d_user, S_GENERAL, S_USER_TIMEOUT)
164
165 def get_auto_open(self) -> bool:
166 """!
167 @brief Get drawer auto open status.
168 @return cash drawer auto open status
169 """
170 s_select = get_dict_value(self.d_user, S_GENERAL, S_AUTO_OPEN)
171 return not bool(s_select == "False")
172
173 def get_free_auto_logout(self) -> bool:
174 """!
175 @brief Get free user auto logout status.
176 @return free user auto logout status
177 """
178 s_select = get_dict_value(self.d_user, S_GENERAL, S_FREE_AUTO_LOGOUT)
179 return not bool(s_select == "False")
180
181 def get_user_name(self, user: str) -> str:
182 """!
183 @brief Get name of user (only for order staff).
184 @param user : get name of this user
185 @return user name
186 """
187 return get_dict_value(self.d_user, user, S_NAME)
188
189 def get_user_pw(self, user: str) -> str:
190 """!
191 @brief Get password of user.
192 @param user : get password of this user
193 @return password of user
194 """
195 return get_dict_value(self.d_user, user, S_PW)
196
197 def get_header1(self) -> str:
198 """!
199 @brief Get first bon header
200 @return first bon header string
201 """
202 return get_dict_value(self.d_item, S_HEADER, S_HEADER_1)
203
204 def get_header2(self) -> str:
205 """!
206 @brief Get second bon header
207 @return second bon header string
208 """
209 return get_dict_value(self.d_item, S_HEADER, S_HEADER_2)
210
211 def get_group_tax(self, i_group: int) -> float:
212 """!
213 @brief Get tax value from group
214 @param i_group : get tax of this group (1-3)
215 @return tax value of group
216 """
217 if i_group == I_GROUP_1:
218 s_group = S_GROUP_1
219 elif i_group == I_GROUP_2:
220 s_group = S_GROUP_2
221 else:
222 s_group = S_GROUP_3
223 return get_dict_float(self.d_item, S_TAX, s_group)
224
225 def get_user_tax(self) -> float:
226 """!
227 @brief Get user involvement
228 @return user involvement
229 """
230 return get_dict_float(self.d_item, S_TAX, S_USER_TAX)
231
232 def get_print_article_status(self) -> bool:
233 """!
234 @brief Get article print status
235 @return article print status
236 """
237 s_select = get_dict_value(self.d_item, S_PRINTS, S_PRINT_ARTICLE)
238 return not bool(s_select == "False")
239
241 """!
242 @brief Get print free user price status
243 @return print free price status
244 """
245 s_select = get_dict_value(self.d_item, S_PRINTS, S_PRINT_FREE_PRICE)
246 return not bool(s_select == "False")
247
248 def get_item_name(self, i_item_number: int) -> str:
249 """!
250 @brief Get item name
251 @param i_item_number : item number (1-30)
252 @return name of item
253 """
254 item_name = get_dict_value(self.d_item, str(i_item_number), S_NAME).replace(S_BREAK_LINE, "")
255 return item_name
256
257 def get_item_button_name(self, i_item_number: int) -> str:
258 """!
259 @brief Get item name for button. (with new lines)
260 @param i_item_number : item number (1-30)
261 @return name of item ("" for not present user)
262 """
263 if i_item_number > ItemNumber.MAX:
264 i_item_number -= ItemNumber.MAX
265 return get_dict_value(self.d_item, str(i_item_number), S_NAME).replace(S_BREAK_LINE, "\n")
266
267 def get_item_price(self, i_item_number: int) -> float:
268 """!
269 @brief Get item price
270 @param i_item_number : item number (1-30)
271 @return price of item
272 """
273 f_price = get_dict_float(self.d_item, str(i_item_number), S_PRICE)
274 return f_price
275
276 def get_item_group(self, i_item_number: int) -> int:
277 """!
278 @brief Get item group
279 @param i_item_number : item number (1-30)
280 @return group of item
281 """
282 i_group = get_dict_int(self.d_item, str(i_item_number), S_GROUP)
283 if i_group not in [I_GROUP_1, I_GROUP_2, I_GROUP_3]:
284 i_group = I_GROUP_3
285 return i_group
286
287 def get_item_visible_user(self, i_item_number: int) -> EUser | None:
288 """!
289 @brief Get visible user for item
290 @param i_item_number : item number (1-30)
291 @return visible user for item
292 """
293 s_user = get_dict_value(self.d_item, str(i_item_number), S_USER_VISIBLE)
294 user_values = [user.value for user in EUser]
295 if s_user in user_values:
296 e_user = EUser(s_user)
297 else:
298 e_user = None
299 return e_user
300
301 def get_item_print_status(self, i_item_number: int) -> bool:
302 """!
303 @brief Get item print status
304 @param i_item_number : item number (1-30)
305 @return print status of item
306 """
307 s_select = get_dict_value(self.d_item, str(i_item_number), S_PRINT)
308 return not bool(s_select == "False")
309
310 def get_item_mark_status(self, i_item_number: int) -> bool:
311 """!
312 @brief Get item mark status
313 @param i_item_number : item number (1-30)
314 @return mark status of item
315 """
316 s_select = get_dict_value(self.d_item, str(i_item_number), S_MARK)
317 return bool(s_select == "True")
318
319 def get_item_background(self, i_item_number: int) -> str:
320 """!
321 @brief Get item background
322 @param i_item_number : item number (1-30)
323 @return mark status of item
324 """
325 background = get_dict_value(self.d_item, str(i_item_number), S_BACKGROUND)
326 return background
327
328 def store_user_data(self) -> None:
329 """!
330 @brief Store actual user data
331 """
332 write_user_settings(self.d_user)
333
334 def read_user_file(self, s_file: str, b_remove_file: bool = False) -> bool:
335 """!
336 @brief Read user file configuration and save in settings.
337 @param s_file : file top read
338 @param b_remove_file : [True] = delete file; [False] = don't delete file
339 @return status if data was imported
340 """
341 log.debug("Import edited or new user file: %s", s_file)
342 config_data = self.handle_config_data_read(s_file)
343 b_set_data = False
344 self.ui.unblock_ui() # unlock before set status to show status message
345 if config_data is not None:
346 d_read_user_dict = config_data
347 if any(value in d_read_user_dict for value in L_ALLOWED_USER):
348 b_set_data = True
349 else:
350 b_set_data = self.ui.confirm_dialog(["Import file?", "Datei importieren?"],
351 l_optional_text=[f"This file does not contain any user data:\n{s_file}",
352 f"Diese Datei enthält keine Benutzer Daten:\n{s_file}"])
353 if b_set_data:
354 if (not self.ui.model.c_auth.check_user_login(EUser.ADMIN)) and (EUser.ADMIN.value in d_read_user_dict):
355 del d_read_user_dict[EUser.ADMIN.value] # delete if other user write Admin PW to edit file
356 if EUser.ADMIN.value not in d_read_user_dict:
357 if EUser.ADMIN.value in self.d_user:
358 d_read_user_dict[EUser.ADMIN.value] = self.d_user[EUser.ADMIN.value] # restore admin pw if not present
359 else:
360 self.ui.set_status("No Admin data to restore", True) # state not possible
361 self.d_user = d_read_user_dict.copy()
362 # default Admin PW if not valid
363 if EUser.ADMIN.value not in self.d_user:
364 self.d_user[EUser.ADMIN.value] = D_DEFAULT_USER[EUser.ADMIN.value].copy()
365 self.d_user[EUser.ADMIN.value][S_PW] = DEFAULT_CODE
366 self.ui.set_status([f"No Admin data. Password is set to default: {DEFAULT_CODE}",
367 f"Keine Admin Daten. Passwort wurde zurückgesetzt: {DEFAULT_CODE}"], True)
368 if S_PW not in self.d_user[EUser.ADMIN.value]:
369 self.d_user[EUser.ADMIN.value][S_PW] = DEFAULT_CODE
370 self.ui.set_status([f"No Admin Password data. Password is set to default: {DEFAULT_CODE}",
371 f"Kein Admin Passwort. Passwort wurde zurückgesetzt: {DEFAULT_CODE}"], True)
372 if not self.d_user[EUser.ADMIN.value][S_PW].isdigit() and self.d_user[EUser.ADMIN.value][S_PW] != "":
373 self.d_user[EUser.ADMIN.value][S_PW] = DEFAULT_CODE
374 self.ui.set_status([f"Admin password is invalid. Password is set to default: {DEFAULT_CODE}",
375 f"Admin Passwort ist ungültig. Passwort wurde zurückgesetzt: {DEFAULT_CODE}"], True)
376 if not self.ui.test_mode:
377 self.store_user_data()
378 if b_remove_file:
379 os.remove(s_file)
380 self.ui.set_status(["User configuration changed", "Benutzer Konfiguration wurde geändert"])
381 else:
382 if b_remove_file:
383 b_edit = self.ui.confirm_dialog(["Invalid. Edit again?", "Ungültig. Erneut bearbeiten?"])
384 if b_edit:
385 self.ui.edit_user_in_notepad(s_file)
386 else:
387 self.ui.set_status(["Unable to read user configuration with duplicate users",
388 "Benutzerkonfiguration mit doppelten Benutzern kann nicht gelesen werden"], True)
389 else:
390 self.ui.set_status(["Unable to import user configuration with duplicate users",
391 "Importieren der Benutzerkonfiguration mit doppelten Benutzern nicht möglich"], True)
392 return b_set_data
393
394 def read_item_file(self, s_file: str, b_remove_file: bool = False) -> bool:
395 """!
396 @brief Read articles file configuration and save in settings.
397 @param s_file : file top read
398 @param b_remove_file : [True] = delete file; [False] = don't delete file
399 @return status if data was imported
400 """
401 log.debug("Import edited or new articles file: %s", s_file)
402 config_data = self.handle_config_data_read(s_file)
403 b_set_data = False
404 self.ui.unblock_ui() # unlock before set status to show status message
405 if config_data is not None:
406 l_possible_articles = [str(i) for i in range(1, ItemNumber.MAX + 1)]
407 if any(value in config_data for value in l_possible_articles):
408 b_set_data = True
409 else:
410 b_set_data = self.ui.confirm_dialog(["Import file?", "Datei importieren?"],
411 l_optional_text=[f"This file does not contain any articles data:\n{s_file}",
412 f"Diese Datei enthält keine Artikel Daten:\n{s_file}"])
413 if b_set_data:
414 self.d_item = config_data
416 if not self.ui.test_mode:
417 write_articles_settings(self.d_item)
418 if b_remove_file:
419 os.remove(s_file)
420 # update header in printer
421 self.ui.model.c_printer.set_header(self.get_header1(),
422 self.get_header2())
423 self.ui.set_status(["Articles configuration changed", "Artikel Konfiguration wurde geändert"])
424 else:
425 if b_remove_file:
426 b_edit = self.ui.confirm_dialog(["Invalid. Edit again?", "Ungültig. Erneut bearbeiten?"])
427 if b_edit:
428 self.ui.edit_items_in_notepad(s_file)
429 else:
430 self.ui.set_status(["Unable to read articles configuration with duplicate numbers",
431 "Artikelkonfiguration mit doppelten Positionen kann nicht gelesen werden"], True)
432 else:
433 self.ui.set_status(["Unable to import articles configuration with duplicate numbers",
434 "Importieren der Artikelkonfiguration mit doppelten Positionen nicht möglich"], True)
435 if b_set_data:
436 self.ui.update_screen()
437 return b_set_data
438
439 def handle_config_data_read(self, s_file: str) -> dict[str, Any] | None:
440 """!
441 @brief Try to read config data to handle duplicate items.
442 @param s_file : file to read
443 @return config data from file
444 """
445 try:
446 d_data = read_data_from_config_file(s_file)
447 except (DuplicateSectionError, DuplicateOptionError) as e:
448 log.debug(e)
449 d_data = None
450 return d_data
451
452
453def sort_dict(d_dict_to_sort: dict[str, Any], d_order_reference: dict[str, Any]) -> dict[str, Any]:
454 """!
455 @brief Sort dictionary.
456 @param d_dict_to_sort : dictionary to sort
457 @param d_order_reference : reference dictionary to sort in this order
458 @return return sorted dictionary
459 """
460 sorted_dict = OrderedDict()
461 unsorted_dict = OrderedDict()
462
463 for key, _order in d_order_reference.items():
464 if key in d_dict_to_sort:
465 value = d_dict_to_sort[key]
466
467 if isinstance(value, dict):
468 sorted_dict[key] = sort_dict(value, d_order_reference[key])
469 else:
470 sorted_dict[key] = value
471
472 # insert unsorted values at the end
473 for key, value in d_dict_to_sort.items():
474 if key not in d_order_reference:
475 if isinstance(value, dict):
476 unsorted_dict[key] = sort_dict(value, {})
477 else:
478 unsorted_dict[key] = value
479
480 sorted_dict.update(unsorted_dict)
481 return sorted_dict
482
483
484def read_data_from_config_file(s_file_name: str) -> dict[str, dict[str, str | list[str]]]:
485 """!
486 @brief Read data from configuration file.
487 @param s_file_name : file to read
488 @return return configuration of the file
489 """
490 d_data: dict[str, dict[str, str | list[str]]] = {}
491 parser = ConfigParser()
492 with codecs.open(s_file_name, mode="r", encoding="utf-8") as file:
493 parser.read_file(file)
494 for s_section in parser.sections():
495 d_data[s_section] = {}
496 for key, value in parser.items(s_section):
497 b_list_key = False
498 if b_list_key and value.startswith('[') and value.endswith(']'): # read UID as list if possible
499 s_entries = value[1:-1].split(", ")
500 l_entries = [s_char.strip('\'\"') for s_char in s_entries]
501 d_data[s_section][key] = l_entries
502 else:
503 d_data[s_section][key] = value
504 return d_data
505
506
507def write_config_to_file(s_file_name: str, d_dict: dict[str, Any]) -> None:
508 """!
509 @brief Write configuration to file.
510 @param s_file_name : write to this file
511 @param d_dict : dictionary to write
512 """
513 parser = cfg.ConfigParser()
514 for s_ini_section in d_dict:
515 parser.add_section(s_ini_section)
516 for s_ini_key in d_dict[s_ini_section]:
517 s_ini_value = d_dict[s_ini_section][s_ini_key]
518 if isinstance(s_ini_value, list):
519 s_ini_value = ', '.join(f'"{item}"' for item in s_ini_value)
520 s_ini_value = f"[{s_ini_value}]"
521 parser.set(s_ini_section, s_ini_key, s_ini_value)
522 with open(s_file_name, mode="w", encoding="utf-8") as o_cfg_file:
523 parser.write(o_cfg_file)
Class Configuration: Read/Write configuration data (user or articles)
Definition config.py:111
str get_header2(self)
Get second bon header.
Definition config.py:204
None update_extended_article_status(self, bool prevent_resize=False)
Update extended article status.
Definition config.py:125
str get_item_background(self, int i_item_number)
Get item background.
Definition config.py:319
str get_item_name(self, int i_item_number)
Get item name.
Definition config.py:248
str get_header1(self)
Get first bon header.
Definition config.py:197
bool get_free_auto_logout(self)
Get free user auto logout status.
Definition config.py:173
float get_logout_time(self)
Get timeout for inactive user.
Definition config.py:158
dict[str, Any]|None handle_config_data_read(self, str s_file)
Try to read config data to handle duplicate items.
Definition config.py:439
str get_user_pw(self, str user)
Get password of user.
Definition config.py:189
bool get_item_print_status(self, int i_item_number)
Get item print status.
Definition config.py:301
bool read_item_file(self, str s_file, bool b_remove_file=False)
Read articles file configuration and save in settings.
Definition config.py:394
bool get_auto_open(self)
Get drawer auto open status.
Definition config.py:165
bool read_user_file(self, str s_file, bool b_remove_file=False)
Read user file configuration and save in settings.
Definition config.py:334
EUser|None get_item_visible_user(self, int i_item_number)
Get visible user for item.
Definition config.py:287
None store_user_data(self)
Store actual user data.
Definition config.py:328
str get_device_name(self)
Get smart card user status.
Definition config.py:146
float get_group_tax(self, int i_group)
Get tax value from group.
Definition config.py:211
float get_item_price(self, int i_item_number)
Get item price.
Definition config.py:267
bool get_print_free_price_status(self)
Get print free user price status.
Definition config.py:240
bool get_item_mark_status(self, int i_item_number)
Get item mark status.
Definition config.py:310
str get_item_button_name(self, int i_item_number)
Get item name for button.
Definition config.py:257
int get_item_group(self, int i_item_number)
Get item group.
Definition config.py:276
str get_user_name(self, str user)
Get name of user (only for order staff).
Definition config.py:181
None __init__(self, "MainWindow" ui)
Definition config.py:117
bool get_print_article_status(self)
Get article print status.
Definition config.py:232
float get_user_tax(self)
Get user involvement.
Definition config.py:225
dict[str, dict[str, str|list[str]]] read_data_from_config_file(str s_file_name)
Read data from configuration file.
Definition config.py:484
float get_dict_float(dict[str, dict[str, str]] d_dict, str s_section, str s_key)
Get floating value in dictionary.
Definition config.py:62
dict[str, Any] sort_dict(dict[str, Any] d_dict_to_sort, dict[str, Any] d_order_reference)
Sort dictionary.
Definition config.py:453
bool is_int(Any value)
Check if string can convert to integer.
Definition config.py:48
str get_dict_value(dict[str, dict[str, str]] d_dict, str s_section, str s_key)
Get value from dictionary.
Definition config.py:96
int get_dict_int(dict[str, dict[str, str]] d_dict, str s_section, str s_key)
Get integer value in dictionary.
Definition config.py:79
bool is_float(Any value)
Check if string can convert to float.
Definition config.py:34
None write_config_to_file(str s_file_name, dict[str, Any] d_dict)
Write configuration to file.
Definition config.py:507