from __future__ import annotations import copy import dataclasses as dc import datetime import pickle import re import sys import traceback from collections import defaultdict from collections.abc import Container, Iterable, Sequence from pathlib import Path from pprint import pformat from typing import Any, Final, Protocol, TypeAlias, TypedDict, TypeVar, cast from typing_extensions import override from pydantic import ( ValidationError, ) from PySide6.QtCore import ( QDate, QEvent, QObject, Qt, QTimer, QtMsgType, Signal, qInstallMessageHandler, ) from PySide6.QtGui import QAction from PySide6.QtWidgets import ( QApplication, QComboBox, QCompleter, QDateEdit, QFormLayout, QFrame, QGridLayout, QGroupBox, QHBoxLayout, QLabel, QLayout, QLineEdit, QMainWindow, QMessageBox, QPlainTextEdit, QPushButton, QScrollArea, QSizePolicy, QStackedWidget, QVBoxLayout, QWidget, ) import wce_crm.constants from wce_crm.backend import backend from wce_crm.data_models import COLUMN_SEP, FlatBaseModel, Grunderfassung from wce_crm.form_defs import ( INITREC_COMP, INITREC_PERSON, FormField, FormFieldType, ) from wce_crm.logging import ( logger_auto_form, logger_get_data, logger_gui, ) K = TypeVar("K") V = TypeVar("V") DEBUG: bool = True DEBUG_SEARCH_WIDGET: bool = False DEBUG_NO_DATABASE: bool = False DEBUG_GET_SET: bool = False # fallback if deployed: disable all debug flags if not wce_crm.constants.Config.DEVELOPMENT_STATE: DEBUG = False DEBUG_SEARCH_WIDGET = False DEBUG_NO_DATABASE = False DEBUG_GET_SET = False QSS = """ *[styleClass="stempel"] { background-color: #f1f5f9; color: #333D4B; border: 1px dashed #cbd5e1; border-radius: 4px; padding: 5px; } *[styleClass="stempel"]:focus { border: 1px dashed #cbd5e1; } """ DROPDOWN_DEFAULT: Final[str] = "--- Bitte wählen ---" DYNAMIC_LIST_KEY_PATTERN: Final[re.Pattern] = re.compile(r"-\[(\d+)\]") DATETIME_FMT: Final[str] = "%d.%m.%Y %H:%M:%S" DATE_FMT: Final[str] = "%d.%m.%Y" # TODO check removal def _save_pydantic_model_dict_db( model: FlatBaseModel, path: Path | None = None, ) -> None: if path is None: path = Path(__file__).parent / "model.pkl" elif not path.is_file(): raise ValueError("Path must point to a file") export = model.to_db() with open(path, "wb") as f: pickle.dump(export, f) # TODO check removal def _load_pydantic_model_dict_db( path: Path | None = None, ) -> dict[str, Any]: if path is None: path = Path(__file__).parent / "model.pkl" elif not path.is_file(): raise ValueError("Path must point to a file") if not path.exists(): raise FileNotFoundError(f"Provided path for model dict not found: >{path}<") with open(path, "rb") as f: model_dict = pickle.load(f) return model_dict def merge_dicts_to_lists( dict_iter: Iterable[dict[K, V]], ) -> dict[K, list[V]]: merged = defaultdict(list) for d in dict_iter: for key, value in d.items(): merged[key].append(value) return dict(merged) def unmerge_dict_to_list( merged_dict: dict[K, list[V]], ) -> list[dict[K, V]]: if not merged_dict: return [] keys = merged_dict.keys() value_lists = merged_dict.values() return [dict(zip(keys, row)) for row in zip(*value_lists)] # TODO check removal def _get_leafs(data): if isinstance(data, dict): for value in data.values(): yield from _get_leafs(value) elif isinstance(data, (list, tuple, set)): for item in data: yield from _get_leafs(item) else: yield data # TODO check removal def _get_leaf_dicts(data): if isinstance(data, dict): has_inner_dicts = False for value in data.values(): for inner_dict in _get_leaf_dicts(value): has_inner_dicts = True yield inner_dict if not has_inner_dicts: yield data elif isinstance(data, (list, tuple, set)): for item in data: yield from _get_leaf_dicts(item) def pprint_registry(widget_registry: WidgetRegistry) -> None: print("\n\n>>> Widget registry:") for key, entry in widget_registry.items(): print(f"Key: {key}") print(f"\twidget: {entry['widget']}") print(f"\tfield key: {entry['form_field'].key}") print(f"\tfield type: {entry['form_field'].type}") def pformat_registry(widget_registry: WidgetRegistry) -> str: lines: list[str] = [] lines.append("\n\n>>> Widget registry:") for key, entry in widget_registry.items(): lines.append(f"Key: {key}") lines.append(f"\twidget: {entry['widget']}") lines.append(f"\tfield key: {entry['form_field'].key}") lines.append(f"\tfield type: {entry['form_field'].type}") return "\n".join(lines) class CustomForm(Protocol): def get_form_data(self) -> dict[str, Any]: ... def set_form_data(self, data: Any) -> None: ... def reset_form(self) -> None: ... def validate_form_data(self) -> list[str]: ... class AutoFormInsert(Protocol): def __call__( self, data: dict[str, Any], ) -> backend.InitRecId: ... class AutoFormUpdate(Protocol): def __call__( self, id_: int, data: dict[str, Any], ) -> None: ... class AutoFormGet(Protocol): def __call__( self, id_: int, ) -> dict[str, Any]: ... class AutoFormDelete(Protocol): def __call__( self, id_: int, ) -> None: ... @dc.dataclass(slots=True) class AutoFormConfig: model: type[FlatBaseModel] data_insert: AutoFormInsert data_update: AutoFormUpdate data_get: AutoFormGet data_delete: AutoFormDelete form_fields: Sequence[FormField] ignored_keys: Iterable[str] = tuple() add_buttons: bool = True class CustomWidget(QWidget): def __init__( self, form_fields: Sequence[FormField], label: str = "Suche", prefix: str = "", ) -> None: super().__init__() self.form_fields = form_fields self.label = label self.prefix = prefix def get_form_data(self) -> dict[str, Any]: ... def set_form_data(self, data: Any) -> None: ... def reset_form(self) -> None: ... def validate_form_data(self) -> list[str]: ... def _build_ui_recursively( schema: Sequence[FormField], parent_layout: QFormLayout, widget_registry: WidgetRegistry, prefix: str = "", ) -> list[str]: no_scroll_filter = NoScrollFilter(parent_layout) keys: list[str] = [] for field in schema: if prefix and field.key: full_key = f"{prefix}{COLUMN_SEP}{field.key}" if prefix else field.key elif prefix and not field.key: full_key = f"{prefix}" else: full_key = field.key widget: QWidget match field.type: case FormFieldType.GROUP: group_box = QGroupBox(field.label) group_layout = QFormLayout(group_box) _build_ui_recursively( schema=field.children, parent_layout=group_layout, widget_registry=widget_registry, prefix=f"{full_key}", ) parent_layout.addRow(group_box) case FormFieldType.TEXT | FormFieldType.TEXT_DATE | FormFieldType.TEXT_DATETIME: widget = QLineEdit() if field.placeholder: widget.setPlaceholderText(field.placeholder) if field.fill_value: widget.setText(field.fill_value) if field.readonly: widget.setReadOnly(True) widget.setProperty("styleClass", "stempel") widget_registry[full_key] = { "widget": widget, "form_field": field, } widget.installEventFilter(no_scroll_filter) if field.tooltip: tooltip_layout = _add_tooltip(widget, field.tooltip) parent_layout.addRow(field.label, tooltip_layout) else: parent_layout.addRow(field.label, widget) case FormFieldType.TEXT_SEARCH: widget = QLineEdit() if field.placeholder: widget.setPlaceholderText(field.placeholder) else: widget.setPlaceholderText("Tippen zum Suchen...") search_completer = QCompleter() search_completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive) search_completer.setFilterMode(Qt.MatchFlag.MatchContains) widget.setCompleter(search_completer) widget_registry[full_key] = { "widget": widget, "form_field": field, } widget.installEventFilter(no_scroll_filter) if field.tooltip: tooltip_layout = _add_tooltip(widget, field.tooltip) parent_layout.addRow(field.label, tooltip_layout) else: parent_layout.addRow(field.label, widget) case FormFieldType.LONGTEXT: widget = QPlainTextEdit() widget.setMaximumHeight(80) if field.placeholder: widget.setPlaceholderText(field.placeholder) if field.readonly: widget.setReadOnly(True) widget.setProperty("styleClass", "stempel") widget_registry[full_key] = { "widget": widget, "form_field": field, } widget.installEventFilter(no_scroll_filter) if field.tooltip: tooltip_layout = _add_tooltip(widget, field.tooltip) parent_layout.addRow(field.label, tooltip_layout) else: parent_layout.addRow(field.label, widget) case FormFieldType.DATE: widget = QDateEdit() widget.setCalendarPopup(True) cal = widget.calendarWidget() if cal: cal.setFirstDayOfWeek(Qt.DayOfWeek.Monday) cal.setGridVisible(True) cal.setStyleSheet(""" QCalendarWidget QAbstractItemView { selection-background-color: #2980b9; selection-color: white; } """) if field.fill_value: set_date = QDate.fromString(field.fill_value, "dd.MM.yyyy") if not set_date.isValid(): raise ValueError( f"Could not parse date field value >{field.fill_value}<" ) widget.setDate(set_date) else: widget.setDate(QDate.currentDate()) if field.readonly: widget = QLineEdit() widget.setReadOnly(True) widget.setProperty("styleClass", "stempel") if field.fill_value: widget.setText(field.fill_value) widget_registry[full_key] = { "widget": widget, "form_field": field, } widget.installEventFilter(no_scroll_filter) if field.tooltip: tooltip_layout = _add_tooltip(widget, field.tooltip) parent_layout.addRow(field.label, tooltip_layout) else: parent_layout.addRow(field.label, widget) case FormFieldType.DROPDOWN: widget = QComboBox() widget.addItem(DROPDOWN_DEFAULT, None) for option in field.dropdown_options: widget.addItem(option.label, option.data) if field.placeholder: widget.setPlaceholderText(field.placeholder) if field.fill_value: widget.setCurrentText(field.fill_value) else: widget.setCurrentIndex(0) if field.readonly: widget.setEnabled(False) widget.setProperty("styleClass", "stempel") widget_registry[full_key] = { "widget": widget, "form_field": field, } widget.installEventFilter(no_scroll_filter) if field.tooltip: tooltip_layout = _add_tooltip(widget, field.tooltip) parent_layout.addRow(field.label, tooltip_layout) else: parent_layout.addRow(field.label, widget) case FormFieldType.EXTENDED_DROPDOWN: widget = QComboBox() widget.setEditable(True) widget.setInsertPolicy(QComboBox.InsertPolicy.NoInsert) completer = widget.completer() assert completer completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive) completer.setFilterMode(Qt.MatchFlag.MatchContains) completer.setCompletionMode(QCompleter.CompletionMode.PopupCompletion) widget.addItem(DROPDOWN_DEFAULT, None) for option in field.dropdown_options: widget.addItem(option.label, option.data) if field.placeholder: line_edit = widget.lineEdit() assert line_edit line_edit.setPlaceholderText(field.placeholder) if field.fill_value: widget.setCurrentText(field.fill_value) else: widget.setCurrentIndex(-1) if field.readonly: widget.setEnabled(False) widget.setProperty("styleClass", "stempel") widget_registry[full_key] = { "widget": widget, "form_field": field, } widget.installEventFilter(no_scroll_filter) if field.tooltip: tooltip_layout = _add_tooltip(widget, field.tooltip) parent_layout.addRow(field.label, tooltip_layout) else: parent_layout.addRow(field.label, widget) case FormFieldType.DYNAMIC_LIST: widget = DynamicListWidget( field.children, field.label, prefix=f"{full_key}", ) widget_registry[full_key] = { "widget": widget, "form_field": field, } parent_layout.addRow(widget) case FormFieldType.DYNAMIC_DROPDOWN_NUMERIC: widget = DynamicDropdownWidgetNumeric( field.children, field.label, prefix=f"{full_key}", ) widget_registry[full_key] = { "widget": widget, "form_field": field, } parent_layout.addRow(widget) case FormFieldType.DYNAMIC_DROPDOWN_OPTION: widget = DynamicDropdownWidgetOption( field.children, field.trigger_value, field.label, prefix=f"{full_key}", ) widget_registry[full_key] = { "widget": widget, "form_field": field, } parent_layout.addRow(widget) case FormFieldType.CUSTOM: widget_class = CUSTOM_WIDGETS[field.custom_widget] widget = widget_class( form_fields=field.children, label=field.label, prefix=f"{full_key}", ) widget_registry[full_key] = { "widget": widget, "form_field": field, } parent_layout.addRow(widget) case _: raise NotImplementedError(f"Not supported field type: {field.type.value}") keys.append(full_key) return keys def _add_tooltip( widget: QWidget, tooltip: str, ) -> QHBoxLayout: field_layout = QHBoxLayout() field_layout.setContentsMargins(0, 0, 0, 0) field_layout.setSpacing(5) info_btn = QPushButton("ℹ️") info_btn.setFixedSize(28, 27) info_btn.setFlat(True) info_btn.setCursor(Qt.CursorShape.PointingHandCursor) info_btn.setToolTip(tooltip) field_layout.addWidget(widget) field_layout.addWidget(info_btn) return field_layout def reset_form( widget_registry: WidgetRegistry, ) -> None: for registry_entry in widget_registry.values(): # type: ignore widget = registry_entry["widget"] form_field = registry_entry["form_field"] # TODO check if this behaviour is correct in other contexts, deactivated because of # TODO company search widget # if form_field.readonly: # continue if isinstance(widget, QLineEdit): widget.clear() if form_field.fill_value: widget.setText(form_field.fill_value) elif isinstance(widget, QPlainTextEdit): widget.clear() elif isinstance(widget, QDateEdit): if form_field.fill_value: set_date = QDate.fromString(form_field.fill_value, "dd.MM.yyyy") if not set_date.isValid(): raise ValueError( f"Could not parse date field value >{form_field.fill_value}<" ) widget.setDate(set_date) else: widget.setDate(QDate.currentDate()) elif isinstance(widget, QComboBox): if form_field.fill_value: widget.setCurrentText(form_field.fill_value) elif form_field.type is FormFieldType.DROPDOWN: widget.setCurrentIndex(0) else: widget.setCurrentIndex(-1) elif isinstance( widget, ( DynamicListWidget, DynamicDropdownWidgetNumeric, DynamicDropdownWidgetOption, CustomWidget, ), ): # custom widget classes manage their widgets on their own widget.reset_form() widget.setStyleSheet("") @dc.dataclass(slots=True) class SubForm: entry_box: QWidget prefix_parent: str index: int prefix: str = "" registry: WidgetRegistry = dc.field(default_factory=dict) full_keys: list[str] = dc.field(default_factory=list) def __post_init__(self) -> None: self.prefix = f"{self.prefix_parent}-[{self.index}]" def update_index(self, new_idx: int) -> None: self.index = new_idx self.prefix = f"{self.prefix_parent}-[{self.index}]" def change_sub_form_widget_registry( widget_registry: WidgetRegistry, sub_form: SubForm, new_idx: int, ) -> None: if sub_form.index == new_idx: return old_key_part = sub_form.prefix new_key_part = f"{sub_form.prefix_parent}-[{new_idx}]" for key in tuple(widget_registry.keys()): splitted = key.split(COLUMN_SEP) key_part, rest = splitted[0], splitted[1:] if key_part == old_key_part: old_key = COLUMN_SEP.join([old_key_part] + rest) new_key = COLUMN_SEP.join([new_key_part] + rest) widget_registry[new_key] = widget_registry[old_key] del widget_registry[key] sub_form.update_index(new_idx) def update_sub_forms( widget_registry: WidgetRegistry, sub_forms: Sequence[SubForm], base_label: str = "", ): total_num_sub_forms = len(sub_forms) for index, sub_form in enumerate(sub_forms, start=1): if isinstance(sub_form.entry_box, QGroupBox) and base_label: sub_form.entry_box.setTitle(f"{base_label} {index}") change_sub_form_widget_registry( widget_registry, sub_form, index, ) for key in tuple(widget_registry.keys()): matches = DYNAMIC_LIST_KEY_PATTERN.search(key) if not matches: continue counter_sub_form = int(matches.group(1)) if counter_sub_form > total_num_sub_forms: del widget_registry[key] def get_form_data( widget_registry: WidgetRegistry, filter_keys: Container[str] = tuple(), ) -> dict[str, Any]: raw_data: dict[str, Any] = {} ignored_keys: list[str] = [] for key, registry_entry in widget_registry.items(): value: Any | None = None if filter_keys: data_key = key.split(COLUMN_SEP)[-1] if data_key not in filter_keys: continue widget = registry_entry["widget"] form_field = registry_entry["form_field"] if form_field.ignore_get_data: ignored_keys.append(key) continue if isinstance(widget, QLineEdit): data = widget.text() if data != "": value = data if form_field.type is FormFieldType.TEXT_DATE: value = datetime.datetime.strptime(value, DATE_FMT).date() elif form_field.type is FormFieldType.TEXT_DATETIME: value = datetime.datetime.strptime(value, DATETIME_FMT) elif isinstance(widget, QPlainTextEdit): data = widget.toPlainText() if data != "": value = data elif isinstance(widget, QDateEdit): qt_date = widget.date() value = qt_date.toPython() elif isinstance(widget, QComboBox): value = widget.currentData() elif isinstance( widget, ( DynamicListWidget, DynamicDropdownWidgetNumeric, DynamicDropdownWidgetOption, CustomWidget, ), ): # this is a special data structure with some assumptions of the widget's internals # DynamicListWidget: this is a list -> each dynamic list contains a list # of such dictionaries (handled below) value = widget.get_form_data() if isinstance(value, dict): raw_data.update(value) else: raw_data[key] = value logger_get_data.debug("[base:get_form_data]>>>>> RAW DATA:\n%s", pformat(raw_data)) return raw_data def set_widget_value( widget: QWidget, value: Any, ) -> None: if value is None: return elif value is True: value = "ja" elif value is False: value = "nein" if isinstance(widget, QLineEdit): if isinstance(value, datetime.datetime): value = value.astimezone() value = value.strftime(DATETIME_FMT) elif isinstance(value, datetime.date): value = value.strftime(DATE_FMT) widget.setText(str(value)) elif isinstance(widget, QPlainTextEdit): widget.setPlainText(str(value)) elif isinstance(widget, QDateEdit): assert isinstance(value, datetime.date) set_date = QDate(value.year, value.month, value.day) if not set_date.isValid(): raise ValueError(f"Could not parse date field value >{value}<") widget.setDate(set_date) elif isinstance(widget, QComboBox): index = widget.findData(value) if index >= 0: widget.setCurrentIndex(index) elif isinstance(widget, DynamicListWidget): assert isinstance(value, list) widget.set_form_data(value) elif isinstance( widget, ( DynamicDropdownWidgetNumeric, DynamicDropdownWidgetOption, Grunderfassung_SuchWidget, ), ): assert isinstance(value, dict) widget.set_form_data(value) def set_form_data( widget_registry: WidgetRegistry, data: dict[str, Any], filter_keys: Container[str] = tuple(), ) -> None: for key, entry in widget_registry.items(): widget = entry["widget"] if filter_keys: data_key = key.split(COLUMN_SEP)[-1] if data_key not in filter_keys: continue if isinstance( widget, ( DynamicDropdownWidgetNumeric, DynamicDropdownWidgetOption, Grunderfassung_SuchWidget, ), ): value = data else: if key not in data: logger_gui.error("Key not in data: %s", key) logger_gui.error("Data:\n%s", pformat(data)) value = data[key] logger_gui.debug("Key set: %s", key) set_widget_value(widget, value) def validate_form_data( widget_registry: WidgetRegistry, ) -> list[str]: errors: list[str] = [] for key, registry_entry in widget_registry.items(): error_post: bool = False widget = registry_entry["widget"] form_field = registry_entry["form_field"] if not form_field.readonly: widget.setStyleSheet("") if not form_field.required: continue dynamic_list_num: str = "" if ( form_field.parent is not None and form_field.parent.type is FormFieldType.DYNAMIC_LIST ): # get also number of group for enhanced error messages matches = DYNAMIC_LIST_KEY_PATTERN.search(key) if matches: dynamic_list_num = matches.group(1) if isinstance(widget, (QLineEdit, QDateEdit)): if widget.text().strip(): continue error_post = True elif isinstance(widget, QPlainTextEdit): if widget.toPlainText().strip(): continue error_post = True elif isinstance(widget, QComboBox): if widget.currentData() is not None: continue error_post = True elif isinstance( widget, ( DynamicListWidget, DynamicDropdownWidgetNumeric, DynamicDropdownWidgetOption, CustomWidget, ), ): errors_widget = widget.validate_form_data() if not errors_widget: continue errors.extend(errors_widget) if not error_post: continue error = form_field.label.replace("*", "").replace(":", "") if form_field.parent is not None: parent_label = form_field.parent.label.replace("*", "").replace(":", "") if dynamic_list_num: error = f"{parent_label} → {f'{parent_label} {dynamic_list_num}'} → {error}" else: error = f"{parent_label} → {error}" error = error.replace("&&", "&") errors.append(error) # optical feedback to highlight erroneous cells widget.setStyleSheet(""" border: 1px solid #ef4444; background-color: #ffe9e9; padding: 4px; border-radius: 4px; """) return errors class Grunderfassung_SuchWidget(CustomWidget): def __init__( self, form_fields: Sequence[FormField], label: str = "Suche", prefix: str = "", ): super().__init__( form_fields=form_fields, label=label, prefix=prefix, ) self.form_fields = form_fields self.label = label self.prefix = prefix self.widget_registry: WidgetRegistry = {} self.export_data: dict[str, Any] = {} self.PROPERTY_MA_ID = "user_ma_id" main_layout = QVBoxLayout(self) main_layout.setContentsMargins(0, 0, 0, 0) self.form_layout = QFormLayout() self.form_layout.setSpacing(10) main_layout.addLayout(self.form_layout) _build_ui_recursively( self.form_fields, self.form_layout, self.widget_registry, self.prefix, ) self.COMPANY_SEARCH_INPUT_KEY = "un_suche" lookup = search_widgets_by_key(self.widget_registry, self.COMPANY_SEARCH_INPUT_KEY) assert len(lookup) == 1 self.company_search_input = cast(QComboBox, lookup[0]["widget"]) self.company_search_input.currentIndexChanged.connect(self._selected_company) self.PERSON_SEARCH_INPUT_KEY = "person_suche" lookup = search_widgets_by_key(self.widget_registry, self.PERSON_SEARCH_INPUT_KEY) assert len(lookup) == 1 self.person_search_input = cast(QComboBox, lookup[0]["widget"]) self.person_search_input.currentIndexChanged.connect(self._selected_person) self.CHANNEL_AWARENESS_KEY = "kanal_aufmerksamkeit" lookup = search_widgets_by_key(self.widget_registry, self.CHANNEL_AWARENESS_KEY) assert len(lookup) == 1 self.channel_awareness = cast(QComboBox, lookup[0]["widget"]) self.DATA_EXPORT_FIELDS = ( self.COMPANY_SEARCH_INPUT_KEY, self.PERSON_SEARCH_INPUT_KEY, self.CHANNEL_AWARENESS_KEY, ) self.company_widgets: dict[str, QWidget] = {} self.person_widgets: dict[str, QWidget] = {} self.properties_company: tuple[str, ...] = ( "ma_unternehmensname", "ma_strasse", "ma_hausnummer", "ma_plz", "ma_ort", ) self.properties_person: tuple[str, ...] = ( "an_titel", "an_anrede", "an_nachname", "an_vorname", "an_festnetz", "an_mobil", "an_mail", "an_position", ) for entry in self.widget_registry.values(): field = entry["form_field"] widget = entry["widget"] if field.info in self.properties_company: self.company_widgets[field.info] = widget elif field.info in self.properties_person: self.person_widgets[field.info] = widget if DEBUG_SEARCH_WIDGET: button = QPushButton("Print Registry") button.clicked.connect(self.print_registry) main_layout.addWidget(button) button = QPushButton("Reset form") button.clicked.connect(self.reset_form) main_layout.addWidget(button) button = QPushButton("Get form data") button.clicked.connect(self.get_form_data) main_layout.addWidget(button) button = QPushButton("Set form data") button.clicked.connect(self.set_form_data) main_layout.addWidget(button) button = QPushButton("Validate") button.clicked.connect(self.validate_form_data) main_layout.addWidget(button) self.update_company_data() def print_registry(self) -> None: pprint_registry(self.widget_registry) def fill_out_company( self, data: backend.CompanyInfo, ) -> None: for key, widget in self.company_widgets.items(): if key not in data: raise KeyError( ( f"Key: {key} not found in company info. Add the key " f"to the info property in the form field definition" ) ) set_widget_value(widget, data[key]) def fill_out_person( self, data: backend.ContactPersonInfo, ) -> None: for key, widget in self.person_widgets.items(): if key not in data: raise KeyError( ( f"Key: {key} not found in company info. Add the key " f"to the info property in the form field definition" ) ) set_widget_value(widget, data[key]) def _clear_company_fields(self) -> None: self.person_search_input.clear() self.channel_awareness.setCurrentIndex(0) for widget in self.company_widgets.values(): widget = cast(QLineEdit, widget) widget.clear() def _clear_person_fields(self) -> None: for widget in self.person_widgets.values(): widget = cast(QLineEdit, widget) widget.clear() def update_company_data(self) -> None: self.company_search_input.clear() self.company_search_input.addItem(DROPDOWN_DEFAULT, None) search_choices = backend.initrec_comp_search_choices() for item, db_index in search_choices: self.company_search_input.addItem(item, db_index) self.company_search_input.setCurrentIndex(-1) def update_person_data( self, ma_id: int | None, ) -> None: self.person_search_input.clear() self.person_search_input.addItem(DROPDOWN_DEFAULT, None) search_choices = backend.initrec_comp_contact_person_search_choices(ma_id, True) for item, db_index in search_choices: self.person_search_input.addItem(item, db_index) self.person_search_input.setCurrentIndex(0) def _selected_company( self, index: int, ) -> None: ma_id = self.company_search_input.itemData(index) if ma_id is None or index == (-1): self._clear_company_fields() return data = backend.initrec_comp_search_get_info( ma_id=ma_id, ) self.fill_out_company(data) self.update_person_data(ma_id) def _selected_person( self, index: int, ) -> None: an_id = self.person_search_input.itemData(index) if an_id is None or index == (-1): self._clear_person_fields() return data = backend.initrec_comp_contact_person_search_get_info( an_id=an_id, ) self.fill_out_person(data) @override def reset_form(self) -> None: reset_form(self.widget_registry) self._clear_company_fields() self._clear_person_fields() @override def validate_form_data(self) -> list[str]: errors = validate_form_data(self.widget_registry) return errors @override def get_form_data(self) -> dict[str, Any]: form_data = get_form_data(self.widget_registry, filter_keys=self.DATA_EXPORT_FIELDS) logger_get_data.debug("Form data CustomWidget:\n%s", pformat(form_data)) return form_data @override def set_form_data( self, data: dict[str, Any], ) -> None: if DEBUG_SEARCH_WIDGET: if not self.export_data: raise RuntimeError("No data. Get form data first.") data = self.export_data set_form_data(self.widget_registry, data, filter_keys=self.DATA_EXPORT_FIELDS) def enhanced_label( base_label: str, add_text: str, add_colon: bool = False, ) -> str: label = base_label.strip().replace("*", "").replace(":", "") if add_text: label = label + f" {add_text}" if add_colon and not label.endswith(":"): label += ":" return label class WidgetRegistryEntry(TypedDict): widget: QWidget form_field: FormField WidgetRegistry: TypeAlias = dict[str, WidgetRegistryEntry] def search_widgets_by_key( widget_registry: WidgetRegistry, key_part: str, ) -> list[WidgetRegistryEntry]: """ needed for custom logic of auto-built forms, search for specific keys and obtain the widget to assign special logic or callbacks to them """ hits: list[WidgetRegistryEntry] = [] for key, entry in widget_registry.items(): if key_part in key: hits.append(entry) return hits class AutoForm(QWidget): """a widget, which is managed by a code-defined field definition collection""" update_triggered = Signal() # formular saved (data changed for front page) def __init__( self, cfg: AutoFormConfig, ) -> None: super().__init__() self.cfg = cfg self.setStyleSheet(""" QGroupBox { font-size: 16px; font-weight: bold; border: 1px solid #cbd5e1; /* Heller, moderner Rahmen */ border-radius: 8px; /* Abgerundete Ecken */ margin-top: 15px; /* Platz für die Überschrift schaffen */ padding-top: 15px; /* Abstand zwischen Rahmen und erstem Feld */ } QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; /* Überschrift oben links */ padding: 0 5px; /* Etwas Luft links und rechts vom Text */ color: #334155; /* Dunkelgraue Schrift */ } QComboBox { border: 1px solid #cbd5e1; border-radius: 4px; padding: 5px; } QComboBox:disabled { background-color: #f1f5f9; /* Helles System-Grau */ color: #333D4B; /* Gut lesbare, aber gedeckte Schrift */ border: 1px dashed #cbd5e1; /* Gestrichelter Rand wie beim Datum */ } QComboBox::drop-down:disabled { border: none; image: none; } """) # --- special funtionality --- self.no_scroll_filter = NoScrollFilter(self) # --- LAYOUT --- self.main_layout = QVBoxLayout(self) self.main_layout.setContentsMargins(0, 0, 0, 0) if DEBUG: separator1 = QFrame() separator1.setFrameShape(QFrame.Shape.HLine) separator1.setFrameShadow(QFrame.Shadow.Sunken) self.main_layout.addWidget(separator1) self.registry_button = QPushButton("Ausgabe Widget Registry") self.registry_button.clicked.connect(self._print_registry) self.registry_button.setFixedHeight(35) self.main_layout.addWidget(self.registry_button) self.test_button = QPushButton("Initialisiere Laden") self.test_button.clicked.connect(self.load_data) self.test_button.setFixedHeight(35) self.test_button.setSizePolicy( QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed ) self.main_layout.addWidget(self.test_button) button_get = QPushButton("GET DATA") button_get.setFixedHeight(35) button_get.clicked.connect(self.get_form_data) self.main_layout.addWidget(button_get) button_set = QPushButton("SET DATA") button_set.setFixedHeight(35) button_set.clicked.connect(self.set_form_data) self.main_layout.addWidget(button_set) id_field_layout = QHBoxLayout() id_field_layout.setContentsMargins(0, 0, 0, 0) id_field_layout.setSpacing(5) id_field_label = QLabel("ID Datenbank:") self.id_field_input = QLineEdit() id_field_layout.addWidget(id_field_label) id_field_layout.addWidget(self.id_field_input) self.main_layout.addLayout(id_field_layout) button_db_index = QPushButton("Setze DB Index") button_db_index.setFixedHeight(35) button_db_index.clicked.connect(self._set_db_index) self.main_layout.addWidget(button_db_index) separator2 = QFrame() separator2.setFrameShape(QFrame.Shape.HLine) separator2.setFrameShadow(QFrame.Shadow.Sunken) self.main_layout.addWidget(separator2) self.main_layout.addSpacing(5) self.main_layout.addSpacing(10) self.top_level_form_layout = QFormLayout() self.main_layout.addLayout(self.top_level_form_layout) self.top_level_form_layout.setSpacing(10) self.form_fields = self.cfg.form_fields self.widget_registry: WidgetRegistry = {} _build_ui_recursively( self.form_fields, self.top_level_form_layout, self.widget_registry, ) # buttons (save and reset) self.add_buttons = self.cfg.add_buttons self.debug_form_data: dict[str, Any] = {} if self.add_buttons: self.layout_btn = QHBoxLayout() self.main_layout.addLayout(self.layout_btn) # save self.save_btn_txt_enabled = "Speichern (Strg + S)" self.save_btn_txt_disabled = "Wird gespeichert..." self.save_btn = QPushButton(self.save_btn_txt_enabled) self.save_btn.setShortcut("Ctrl+S") self.save_btn.setFixedHeight(50) self.save_btn.setSizePolicy( QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed ) self.save_btn.clicked.connect(self.save_data) self.layout_btn.addWidget(self.save_btn) # reset self.reset_btn = QPushButton("Zurücksetzen (Strg + Z)") self.reset_btn.setShortcut("Ctrl+Z") self.reset_btn.setFixedHeight(50) self.reset_btn.setSizePolicy( QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed ) self.reset_btn.clicked.connect(self.reset_form) self.layout_btn.addWidget(self.reset_btn) # delete self.delete_btn = QPushButton("Eintrag löschen (Strg + L)") self.delete_btn.setShortcut("Ctrl+L") self.delete_btn.setFixedHeight(50) self.delete_btn.setSizePolicy( QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed ) self.delete_btn.clicked.connect(self.delete_data) self.layout_btn.addWidget(self.delete_btn) self.current_id: int = -1 logger_auto_form.debug( "Initialised Auto Form Widget. Registry:%s", pformat_registry(self.widget_registry), ) def _print_registry(self) -> None: pprint_registry(self.widget_registry) def _set_db_index(self) -> None: try: index = int(self.id_field_input.text()) except ValueError: index = -1 self.current_id = index logger_gui.debug("Set index to %d", self.current_id) def _disable_save(self) -> None: self.save_btn.setEnabled(False) self.save_btn.setText(self.save_btn_txt_disabled) def _enable_save( self, timeout: int = 3000, ) -> None: QTimer.singleShot(timeout, lambda: self.save_btn.setEnabled(True)) QTimer.singleShot(timeout + 1, lambda: self.save_btn.setShortcut("Ctrl+S")) self.save_btn.setText(self.save_btn_txt_enabled) def _activation_delete(self) -> None: if self.current_id > -1: self.delete_btn.setEnabled(True) else: self.delete_btn.setEnabled(False) def delete_data(self) -> None: assert self.current_id > -1, "deletion initialised despite no index set" self.cfg.data_delete(self.current_id) self.update_triggered.emit() self.reset_form() def load_data( self, lookup_id: int | None = None, ) -> None: if DEBUG_NO_DATABASE: return logger_auto_form.info(">>>> LOAD CLICKED") if lookup_id is None or lookup_id == 0: lookup_id = self.current_id self.reset_form() logger_auto_form.debug("Lookup ID: %d", lookup_id) if lookup_id > 0: logger_auto_form.debug("Load from DB:") loaded_data = self.cfg.data_get(lookup_id) else: logger_auto_form.error("Loading: Lookup ID <= 0. Do nothing!") return logger_auto_form.debug( "Loaded data dict:\n%s Passing to Pydantic...", pformat(loaded_data) ) model = Grunderfassung(**loaded_data) logger_auto_form.debug("Loaded to Pydantic.") logger_auto_form.debug("Convert to GUI structure...") form_data = model.to_gui() logger_auto_form.debug("Set form data...") logger_auto_form.debug("Form data:\n%s", pformat(form_data)) self.set_form_data(form_data) self.current_id = lookup_id self._activation_delete() def save_data(self) -> None: self._disable_save() errors = self.validate_form_data() if errors: # errors: abort saving and show pop up window error_text = ( "Bitte füllen Sie die folgenden Pflichtfelder aus:\n\n▸ " + "\n▸ ".join(errors) ) QMessageBox.warning(self, "Fehlende Angaben", error_text) self._enable_save() return # no errors: data can be saved logger_auto_form.info("Erfolg! Alle Daten sind valide.") logger_auto_form.info("Get form data call...") form_data = self.get_form_data() logger_auto_form.debug( "\n\n------------>>>>>>>>> Get form data\n%s", pformat(form_data) ) try: logger_auto_form.debug("------------>>>>>>>>> Call Pydantic") validated_data = self.cfg.model(**form_data) # # TODO add user? except ValidationError as e: # catch errors and show them in GUI error_texts = [] # Pydantic detailed error list for error in e.errors(): logger_gui.error("Error during validation phase:\n%s", pformat(error)) error_field = str(error["loc"][0]) reason = error["msg"] error_texts.append(f"- {error_field}: {reason}") # TODO: Is this needed and feasible? # formatting red # if fehlerhaftes_feld in self.widgets: # self.widgets[fehlerhaftes_feld].setStyleSheet( # "border: 1px solid red; background: #fef2f2;" # ) # tell user what went wrong QMessageBox.warning(self, "Eingabefehler", "\n".join(error_texts)) else: if DEBUG_NO_DATABASE: return # !! this code is only called if the 'try' block was successful # save data to database db_data = validated_data.to_db(exclude=self.cfg.ignored_keys) logger_auto_form.debug( ("Form data with 'exlude' (must be saved in the database):\n%s"), pformat(db_data), ) if self.current_id < 0: logger_auto_form.debug("Insert triggered") init_rec_id = self.cfg.data_insert(db_data) assert isinstance(init_rec_id, int) self.current_id = init_rec_id else: logger_auto_form.debug("Update triggered") self.cfg.data_update(self.current_id, db_data) logger_auto_form.info("Data saved successfully") self.update_triggered.emit() self._activation_delete() # self.reset_form() # TODO check if this behaviour is expected finally: # always re-enable save, even if error occurred self._enable_save() def validate_form_data(self) -> list[str]: return validate_form_data(self.widget_registry) def reset_form(self) -> None: reset_form(self.widget_registry) self.current_id = -1 self._activation_delete() def get_form_data(self) -> dict[str, Any]: form_data = get_form_data(self.widget_registry) logger_auto_form.debug("\n\n>>>>>>> [AutoForm] Call get form data:") logger_auto_form.debug("Form Data:\n%s", pformat(form_data)) if DEBUG_GET_SET: self.debug_form_data = form_data return form_data def set_form_data( self, data: dict[str, Any], ) -> None: logger_auto_form.debug("\n\n>>>>>>> [AutoForm] Call set form data:") if DEBUG_GET_SET: data = self.debug_form_data set_form_data(self.widget_registry, data) class DynamicListWidget(QWidget): """ a widget, which can generate and manage an arbitrary number of sub forms. """ def __init__( self, form_fields: Sequence[FormField], label: str = "Eintrag", prefix: str = "", add_empty_entry: bool = False, ): super().__init__() self.form_fields = form_fields self.label = label self.base_label = enhanced_label(label, add_text="") self.prefix = prefix self.widget_registry: WidgetRegistry = {} # layout for group component self.group_box = QGroupBox(label) self.main_layout = QVBoxLayout(self) self.main_layout.setContentsMargins(0, 0, 0, 0) # inner layout which contains sub forms and buttons self.inner_layout = QVBoxLayout() self.main_layout.addWidget(self.group_box) self.group_box.setLayout(self.inner_layout) # sub forms self.sub_forms: list[SubForm] = [] # button to add more sub forms self.add_btn = QPushButton("+ Hinzufügen") self.add_btn.setStyleSheet( "color: #0369a1; font-weight: bold; border: 1px dashed #0369a1; padding: 5px;" ) self.add_btn.clicked.connect(self.add_entry) self.inner_layout.addWidget(self.add_btn) # add empty sub form as initial value self.add_empty_entry = add_empty_entry if self.add_empty_entry: self.add_entry() def add_entry(self): number_form = len(self.sub_forms) + 1 entry_box = QGroupBox(f"{self.label} {number_form}") entry_layout = QHBoxLayout(entry_box) sub_form = SubForm(entry_box, prefix_parent=self.prefix, index=number_form) # widget_registry: WidgetRegistry = {} form_layout = QFormLayout() _build_ui_recursively( schema=self.form_fields, parent_layout=form_layout, widget_registry=sub_form.registry, # prefix=f"{self.prefix}-[{number_form}]", prefix="", ) self.widget_registry.update(sub_form.registry) # sub_form.registry = widget_registry del_btn = QPushButton("🗑️") del_btn.setFixedSize(30, 30) # Lambda with default parameter to delete exactly this(!) box del_btn.clicked.connect(lambda checked=False, form=sub_form: self.remove_entry(form)) entry_layout.addLayout(form_layout) entry_layout.addWidget(del_btn) self.sub_forms.append(sub_form) self.inner_layout.insertWidget(self.inner_layout.count() - 1, entry_box) self.update_sub_forms() def remove_entry( self, subform_to_remove: SubForm, ): self.inner_layout.removeWidget(subform_to_remove.entry_box) subform_to_remove.entry_box.deleteLater() self.sub_forms.remove(subform_to_remove) self.update_sub_forms() def update_sub_forms(self): update_sub_forms( self.widget_registry, sub_forms=self.sub_forms, base_label=self.base_label, ) def reset_form(self) -> None: while self.sub_forms: self.remove_entry(self.sub_forms[0]) if self.add_empty_entry: self.add_entry() def validate_form_data(self) -> list[str]: errors = validate_form_data(self.widget_registry) for form in self.sub_forms: errors.extend(validate_form_data(form.registry)) return errors def get_form_data(self) -> list[dict[str, Any]]: logger_get_data.info("DynamicListWidget - Get Data") form_data = [get_form_data(sub.registry) for sub in self.sub_forms] logger_get_data.debug("DynamicListWidget - Form data:\n%s", pformat(form_data)) return form_data def set_form_data( self, data: list[dict[str, Any]], ) -> None: while self.sub_forms: self.remove_entry(self.sub_forms[0]) # empty default row if not data and self.add_empty_entry: self.add_entry() return for sub_form_data in data: self.add_entry() current_sub_form = self.sub_forms[-1] set_form_data(current_sub_form.registry, sub_form_data) class DynamicDropdownWidgetNumeric(QWidget): """ A Widget, which can generate and manage an arbitrary number of sub forms with additional information on a combobox selection (combobox). """ def __init__( self, form_fields: Sequence[FormField], label_add_info: str = "Eintrag", prefix: str = "", ): super().__init__() # form_fields = form_fields.children if len(form_fields) == 0 or len(form_fields) > 1: raise ValueError( "Dynamic Dropdown Widget must have only one child, which is a dropdown widget" ) self.combobox_field = form_fields[0] assigned_form_fields = self.combobox_field.children if len(assigned_form_fields) == 0 or len(assigned_form_fields) > 1: raise ValueError( ( "Dynamic Dropdown Widget's dropdown element must have only one " "child, which is a single field definition" ) ) self.type = type self.assigned_form_field = assigned_form_fields[0] self.label_add_info = label_add_info self.prefix = prefix self.widget_registry: WidgetRegistry = {} # layout for group component self.main_layout = QVBoxLayout(self) self.main_layout.setContentsMargins(0, 0, 0, 0) self.form_layout = QFormLayout() self.main_layout.addLayout(self.form_layout) self.full_keys = _build_ui_recursively( [self.combobox_field], self.form_layout, self.widget_registry, prefix=f"{self.prefix}", ) dropdown_widget_entry = tuple(self.widget_registry.values())[0] dropdown_widget = dropdown_widget_entry["widget"] assert isinstance(dropdown_widget, QComboBox) self.dropdown_widget = dropdown_widget self.rows_container = QWidget() self.rows_layout = QVBoxLayout(self.rows_container) self.rows_layout.setContentsMargins(0, 0, 0, 0) self.main_layout.addWidget(self.rows_container) self.sub_forms: list[SubForm] = [] self.dropdown_widget.currentTextChanged.connect(self._selection_changed_changed) def _selection_changed_changed( self, text: str, ) -> None: current_count = len(self.sub_forms) value: int if text == DROPDOWN_DEFAULT: value = 0 else: value = int(text) if value > current_count: diff = value - current_count for _ in range(diff): self._add_row() elif value < current_count: diff = current_count - value for _ in range(diff): self._remove_row() def _add_row(self) -> None: number_form = len(self.sub_forms) + 1 container = QWidget() container.setContentsMargins(0, 0, 0, 0) form_layout = QFormLayout(container) form_layout.setContentsMargins(10, 0, 0, 0) sub_form = SubForm(container, prefix_parent=self.prefix, index=number_form) form_field_def = copy.copy(self.assigned_form_field) form_field_def.label = form_field_def.enhanced_label(f"{number_form}") sub_form.full_keys = _build_ui_recursively( schema=[form_field_def], parent_layout=form_layout, widget_registry=sub_form.registry, prefix=f"{self.prefix}", ) self.rows_layout.addWidget(container) self.sub_forms.append(sub_form) self.update_sub_forms() def _remove_row(self) -> None: last_form = self.sub_forms.pop() box_to_remove = last_form.entry_box self.rows_layout.removeWidget(box_to_remove) box_to_remove.deleteLater() self.update_sub_forms() def update_sub_forms(self) -> None: update_sub_forms( self.widget_registry, sub_forms=self.sub_forms, ) def reset_form(self) -> None: # resets dynamic content when dropdown is set back to default value self.dropdown_widget.setCurrentIndex(0) def validate_form_data(self) -> list[str]: errors = validate_form_data(self.widget_registry) for form in self.sub_forms: errors.extend(validate_form_data(form.registry)) return errors def get_form_data(self) -> dict[str, Any]: form_data = get_form_data(self.widget_registry) sub_form_data_dicts = (get_form_data(sub.registry) for sub in self.sub_forms) sub_data = merge_dicts_to_lists(sub_form_data_dicts) form_data.update(sub_data) logger_get_data.debug("Form data DynamicDropdownWidget:\n%s", pformat(form_data)) return form_data def set_form_data( self, sub_form_data: dict[str, Any], ) -> None: # delete all rows while self.sub_forms: self._remove_row() # fill in value of combobox field assert len(self.full_keys) == 1 num_subforms: int = -1 for key in self.full_keys: widget = self.widget_registry[key]["widget"] value = sub_form_data[key] set_widget_value(widget, value) if value is None: num_subforms = 0 else: num_subforms = value del sub_form_data[key] logger_get_data.debug(">>>>>>>>> Call set_form_data for DynamicDropdown") logger_get_data.debug("Data before unmerge:%s", pformat(sub_form_data)) assert len(self.sub_forms) == num_subforms if not self.sub_forms: return relevant_keys = self.sub_forms[-1].full_keys # all keys of sub forms are the same sub_data = {key: sub_form_data[key] for key in relevant_keys} logger_get_data.debug(">>>> DynamicDropdownWidget. Sub data:\n%s", pformat(sub_data)) if not sub_data: return sub_data = unmerge_dict_to_list(sub_data) logger_get_data.debug(">>>> DynamicDropdownWidget. Sub data:\n%s", pformat(sub_data)) assert len(sub_data) == len(self.sub_forms) for sub_form_data, sub_form in zip(sub_data, self.sub_forms): set_form_data(sub_form.registry, sub_form_data) class DynamicDropdownWidgetOption(QWidget): """ A Widget, which can generate and manage an arbitrary number of sub forms with additional information on a combobox selection (combobox). """ def __init__( self, form_fields: Sequence[FormField], trigger_value: str, label_add_info: str = "Eintrag", prefix: str = "", ): super().__init__() # form_fields = form_fields.children if len(form_fields) == 0 or len(form_fields) > 1: raise ValueError( "Dynamic Dropdown Widget must have only one child, which is a dropdown widget" ) self.combobox_field = form_fields[0] assigned_form_fields = self.combobox_field.children if len(assigned_form_fields) < 1: raise ValueError( ( "Option Dynamic Dropdown Widget dropdown element must have at least one " "child field definition" ) ) self.trigger_value = trigger_value self.assigned_form_fields = assigned_form_fields self.label_add_info = label_add_info self.prefix = prefix self.widget_registry: WidgetRegistry = {} # layout for group component self.main_layout = QVBoxLayout(self) self.main_layout.setContentsMargins(0, 0, 0, 0) self.form_layout = QFormLayout() self.main_layout.addLayout(self.form_layout) self.full_keys = _build_ui_recursively( [self.combobox_field], self.form_layout, self.widget_registry, prefix=f"{self.prefix}", ) dropdown_widget_entry = tuple(self.widget_registry.values())[0] dropdown_widget = dropdown_widget_entry["widget"] assert isinstance(dropdown_widget, QComboBox) self.dropdown_widget = dropdown_widget self.rows_container = QWidget() self.rows_layout = QVBoxLayout(self.rows_container) self.rows_layout.setContentsMargins(0, 0, 0, 0) self.main_layout.addWidget(self.rows_container) self.sub_forms: list[SubForm] = [] self.dropdown_widget.currentTextChanged.connect(self._selection_changed_changed) def _selection_changed_changed( self, text: str, ) -> None: current_count = len(self.sub_forms) value: int if text == self.trigger_value: value = 1 else: value = 0 if value > current_count: diff = value - current_count for _ in range(diff): self._add_row() elif value < current_count: diff = current_count - value for _ in range(diff): self._remove_row() def _add_row(self) -> None: number_form = len(self.sub_forms) + 1 container = QWidget() container.setContentsMargins(0, 0, 0, 0) form_layout = QFormLayout(container) form_layout.setContentsMargins(10, 0, 0, 0) sub_form = SubForm(container, prefix_parent=self.prefix, index=number_form) form_field_def = copy.copy(self.assigned_form_fields) # form_field_def.label = form_field_def.enhanced_label(f"{number_form}") sub_form.full_keys = _build_ui_recursively( schema=form_field_def, parent_layout=form_layout, widget_registry=sub_form.registry, prefix=f"{self.prefix}", ) self.rows_layout.addWidget(container) self.sub_forms.append(sub_form) self.update_sub_forms() def _remove_row(self) -> None: last_form = self.sub_forms.pop() box_to_remove = last_form.entry_box self.rows_layout.removeWidget(box_to_remove) box_to_remove.deleteLater() self.update_sub_forms() def update_sub_forms(self) -> None: update_sub_forms( self.widget_registry, sub_forms=self.sub_forms, ) def _get_combined_registry(self, sub_forms_only: bool = False) -> WidgetRegistry: whole_registry: WidgetRegistry = {} if not sub_forms_only: whole_registry = self.widget_registry.copy() for sub in self.sub_forms: whole_registry.update(sub.registry) return whole_registry def reset_form(self) -> None: # resets dynamic content when dropdown is set back to default value self.dropdown_widget.setCurrentIndex(0) def validate_form_data(self) -> list[str]: errors = validate_form_data(self.widget_registry) for form in self.sub_forms: errors.extend(validate_form_data(form.registry)) return errors def get_form_data(self) -> dict[str, Any]: whole_registry = self._get_combined_registry() form_data = get_form_data(whole_registry) # for sub in self.sub_forms: # form_data.update(get_form_data(sub.registry)) logger_get_data.debug( "Form data DynamicDropdownWidgetOption:\n%s", pformat(form_data) ) return form_data def set_form_data( self, data: dict[str, Any], ) -> None: # delete all rows while self.sub_forms: self._remove_row() # fill in value of combobox field assert len(self.full_keys) == 1 num_subforms: int = -1 data = data.copy() for key in self.full_keys: widget = self.widget_registry[key]["widget"] value = data[key] set_widget_value(widget, value) logger_get_data.debug("Value: %s", value) if value is None or value != self.trigger_value: num_subforms = 0 else: num_subforms = 1 del data[key] whole_registry = self._get_combined_registry(sub_forms_only=True) logger_get_data.debug("[DynamicDD-Option] Widget registry for subforms obtained") logger_get_data.debug( "[DynamicDD-Option] -- set_form_data -- Data before set of subforms:\n%s", pformat(data), ) assert len(self.sub_forms) == num_subforms if not self.sub_forms: return set_form_data(whole_registry, data) class NoScrollFilter(QObject): """disables scrolling in fields""" def eventFilter( self, obj: QObject, event: QEvent, ) -> bool: if event.type() == QEvent.Type.Wheel: # ignored Qt gives event to parent (ScrollArea) event.ignore() # event was handled return True # propagate other events return super().eventFilter(obj, event) class ClickableCell(QFrame): """cell in the table on the startup screen""" clicked = Signal(backend.FrontpageCompany) def __init__( self, text: str, data_record: backend.FrontpageCompany, ): super().__init__() self.data_record = data_record self.setStyleSheet(""" ClickableCell { background-color: white; border: 1px solid #e2e8f0; border-radius: 8px; } ClickableCell:hover { background-color: #eff6ff; border: 1px solid #60a5fa; } """) layout = QVBoxLayout(self) label = QLabel(text) label.setWordWrap(True) label.setAlignment(Qt.AlignmentFlag.AlignCenter) layout.addWidget(label) def mousePressEvent(self, event): if event.button() == Qt.MouseButton.LeftButton: self.clicked.emit(self.data_record) class HeaderCell(QLabel): def __init__(self, text): super().__init__(text) self.setAlignment(Qt.AlignmentFlag.AlignCenter) self.setStyleSheet(""" HeaderCell { background-color: #e2e8f0; color: #475569; font-weight: bold; padding: 10px; border-radius: 6px; } """) class NewEntrySelect_view(QWidget): """view for new entry for initial recording ("Grunderfassung")""" back_requested = Signal() # Signal back button company_requested = Signal() # Signal "Unternehmen" person_requested = Signal() # Signal "Individualperson" def __init__(self): super().__init__() layout = QVBoxLayout(self) layout.setAlignment(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft) # Zurück-Button back_btn = QPushButton("← Zurück zur Übersicht") back_btn.setFixedWidth(200) back_btn.clicked.connect(lambda: self.back_requested.emit()) # Platzhalter für die Details self.title_label = QLabel("Wählen Sie den Typ der Grunderfassung") self.title_label.setStyleSheet( "font-size: 24px; font-weight: bold; margin-top: 20px;" ) btn_company = QPushButton("Unternehmen →") btn_company.setFixedWidth(300) btn_company.setFixedHeight(40) btn_company.clicked.connect(lambda: self.company_requested.emit()) btn_person = QPushButton("Individualperson →") btn_person.setFixedWidth(300) btn_person.setFixedHeight(40) btn_person.clicked.connect(lambda: self.person_requested.emit()) # btn_person.clicked.connect( # lambda: logger_gui.info("[Grunderfassung: Dummy Individualperson] Person gewählt") # ) layout.addWidget(back_btn) layout.addSpacing(15) layout.addWidget(self.title_label) layout.addWidget(btn_company) layout.addWidget(btn_person) CONFIG_GRUNDERFASSUNG_UNTERNEHMEN: Final[AutoFormConfig] = AutoFormConfig( model=Grunderfassung, data_insert=backend.initrec_insert_initial_recording, data_update=backend.initrec_update_initial_recording, data_get=backend.initrec_get_initial_recording, data_delete=backend.initrec_delete_initial_recording, ignored_keys=( "Metadaten_erstellung", "Metadaten_aktualisierung", "Metadaten_wiedereintrittsdatum", ), form_fields=INITREC_COMP, ) CONFIG_GRUNDERFASSUNG_PERSONEN: Final[AutoFormConfig] = AutoFormConfig( model=Grunderfassung, data_insert=backend.initrec_insert_initial_recording, data_update=backend.initrec_update_initial_recording, data_get=backend.initrec_get_initial_recording, data_delete=backend.initrec_delete_initial_recording, ignored_keys=( "Metadaten_erstellung", "Metadaten_aktualisierung", "Partnersuche", ), form_fields=INITREC_PERSON, ) CUSTOM_WIDGETS: Final[dict[str, type[CustomWidget]]] = { "grunderfassung_suche": Grunderfassung_SuchWidget, } class Page_InitRecCompany(QWidget): back_main_requested = Signal() # back to main page back_requested = Signal() # back button update_triggered = Signal() # form saved (data changed for front page) def __init__(self): super().__init__() # Hauptlayout der Seite outer_layout = QHBoxLayout(self) vert_layout = QVBoxLayout() # main_layout.setContentsMargins(0, 0, 0, 0) outer_layout.addStretch(1) outer_layout.addLayout(vert_layout, stretch=100) # outer_layout.addWidget(scroll_area, stretch=100) outer_layout.addStretch(1) # Optional: Damit der Container oben am Rand klebt outer_layout.setAlignment(Qt.AlignmentFlag.AlignTop) # --- HEADER --- header_container = QWidget() header_container.setMinimumWidth(700) header_container.setMaximumWidth(1000) header_container.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) header_layout = QVBoxLayout(header_container) header_layout.setContentsMargins(0, 0, 0, 10) back_btn_main = QPushButton("← Zurück zur Übersicht") back_btn_main.clicked.connect(lambda: self.back_main_requested.emit()) back_btn_main.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) back_btn_main.setMinimumWidth(200) back_btn_main.setMaximumWidth(200) back_btn_step = QPushButton("← Zurück") back_btn_step.clicked.connect(lambda: self.back_requested.emit()) back_btn_step.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) back_btn_step.setMinimumWidth(200) back_btn_step.setMaximumWidth(200) title = QLabel("Grunderfassung Unternehmen") title.setStyleSheet("font-size: 20px; font-weight: bold;") header_layout.setSpacing(5) header_layout.addWidget(back_btn_step) header_layout.addWidget(back_btn_main) header_layout.addWidget(title) vert_layout.addWidget(header_container) # --- MAIN CONTENT --- container = QWidget() # SCROLL AREA scroll_area = QScrollArea() scroll_area.setWidgetResizable(True) scroll_area.setMinimumWidth(700) scroll_area.setMaximumWidth(1000) scroll_area.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) # optional: remove frame to look more modern scroll_area.setFrameShape(QFrame.Shape.NoFrame) scroll_area.setWidget(container) vert_layout.addWidget(scroll_area) container_layout = QVBoxLayout(container) container_layout.setContentsMargins(0, 0, 0, 0) # --- AUTO FORM LAYOUT --- container_layout.addSpacing(20) self.auto_form = AutoForm(cfg=CONFIG_GRUNDERFASSUNG_UNTERNEHMEN) container_layout.addWidget(self.auto_form) self.auto_form.update_triggered.connect(lambda: self.update_triggered.emit()) container_layout.addSpacing(15) # --- CUSTOM LOGIC --- # ** fill 'Kontaktperson -> Namen Unternehmen' search_res = search_widgets_by_key(self.auto_form.widget_registry, "Partnersuche") assert len(search_res) == 1 search_widget = cast(Grunderfassung_SuchWidget, search_res[0]["widget"]) self.search_widget_trigger = cast( QLineEdit, search_widget.company_widgets["ma_unternehmensname"] ) text_widget_set = search_widgets_by_key( self.auto_form.widget_registry, f"Kontaktperson{COLUMN_SEP}KP_name_partner" ) assert len(text_widget_set) == 1 self.text_widget_set = cast(QLineEdit, text_widget_set[0]["widget"]) self.search_widget_trigger.textChanged.connect( self._custom_set_company_name_contact_person ) # ** 'Bundesland' only if 'Inland' selected in 'Stammdaten' person_location = search_widgets_by_key( self.auto_form.widget_registry, f"Stammdaten{COLUMN_SEP}aufenthaltsort" ) assert len(person_location) == 1 self.person_location = cast(QComboBox, person_location[0]["widget"]) assert isinstance(self.person_location, QComboBox) self.person_location.currentIndexChanged.connect(self._custom_county_selection) selection_county = search_widgets_by_key( self.auto_form.widget_registry, f"Stammdaten{COLUMN_SEP}bundesland" ) assert len(selection_county) == 1 self.selection_county = cast(QComboBox, selection_county[0]["widget"]) assert isinstance(self.selection_county, QComboBox) self.selection_county.setProperty("styleClass", "stempel") self.selection_county.setEnabled(False) def _custom_set_company_name_contact_person(self, value: str) -> None: self.text_widget_set.setText(str(value)) def _custom_county_selection(self, idx: int) -> None: value = self.person_location.itemData(idx) if value == "Inland": self.selection_county.setEnabled(True) self.selection_county.setProperty("styleClass", "") self.selection_county.style().unpolish(self.selection_county) self.selection_county.style().polish(self.selection_county) self.selection_county.update() else: self.selection_county.setCurrentIndex(0) self.selection_county.setEnabled(False) self.selection_county.setProperty("styleClass", "stempel") self.selection_county.style().unpolish(self.selection_county) self.selection_county.style().polish(self.selection_county) self.selection_county.update() def reset_form(self) -> None: self.auto_form.reset_form() class Page_InitRecPerson(QWidget): back_main_requested = Signal() # back to main page back_requested = Signal() # back button update_triggered = Signal() # form saved (data changed for front page) def __init__(self): super().__init__() # Hauptlayout der Seite outer_layout = QHBoxLayout(self) vert_layout = QVBoxLayout() # main_layout.setContentsMargins(0, 0, 0, 0) outer_layout.addStretch(1) outer_layout.addLayout(vert_layout, stretch=100) # outer_layout.addWidget(scroll_area, stretch=100) outer_layout.addStretch(1) # Optional: Damit der Container oben am Rand klebt outer_layout.setAlignment(Qt.AlignmentFlag.AlignTop) # --- HEADER --- header_container = QWidget() header_container.setMinimumWidth(700) header_container.setMaximumWidth(1000) header_container.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) header_layout = QVBoxLayout(header_container) header_layout.setContentsMargins(0, 0, 0, 10) back_btn_main = QPushButton("← Zurück zur Übersicht") back_btn_main.clicked.connect(lambda: self.back_main_requested.emit()) back_btn_main.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) back_btn_main.setMinimumWidth(200) back_btn_main.setMaximumWidth(200) back_btn_step = QPushButton("← Zurück") back_btn_step.clicked.connect(lambda: self.back_requested.emit()) back_btn_step.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) back_btn_step.setMinimumWidth(200) back_btn_step.setMaximumWidth(200) title = QLabel("Grunderfassung Individualperson") title.setStyleSheet("font-size: 20px; font-weight: bold;") header_layout.setSpacing(5) header_layout.addWidget(back_btn_step) header_layout.addWidget(back_btn_main) header_layout.addWidget(title) vert_layout.addWidget(header_container) # --- MAIN CONTENT --- container = QWidget() # SCROLL AREA scroll_area = QScrollArea() scroll_area.setWidgetResizable(True) scroll_area.setMinimumWidth(700) scroll_area.setMaximumWidth(1000) scroll_area.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) # optional: remove frame to look more modern scroll_area.setFrameShape(QFrame.Shape.NoFrame) scroll_area.setWidget(container) vert_layout.addWidget(scroll_area) container_layout = QVBoxLayout(container) container_layout.setContentsMargins(0, 0, 0, 0) # --- AUTO FORM LAYOUT --- container_layout.addSpacing(20) self.auto_form = AutoForm(cfg=CONFIG_GRUNDERFASSUNG_PERSONEN) container_layout.addWidget(self.auto_form) self.auto_form.update_triggered.connect(lambda: self.update_triggered.emit()) container_layout.addSpacing(15) # --- CUSTOM LOGIC --- # ** fill 'Kontaktperson -> Namen Unternehmen' # search_res = search_widgets_by_key(self.auto_form.widget_registry, "Partnersuche") # assert len(search_res) == 1 # search_widget = cast(Grunderfassung_SuchWidget, search_res[0]["widget"]) # self.search_widget_trigger = cast( # QLineEdit, search_widget.company_widgets["ma_unternehmensname"] # ) # text_widget_set = search_widgets_by_key( # self.auto_form.widget_registry, f"Kontaktperson{COLUMN_SEP}KP_name_partner" # ) # assert len(text_widget_set) == 1 # self.text_widget_set = cast(QLineEdit, text_widget_set[0]["widget"]) # self.search_widget_trigger.textChanged.connect( # self._custom_set_company_name_contact_person # ) # ** 'Bundesland' only if 'Inland' selected in 'Stammdaten' person_location = search_widgets_by_key( self.auto_form.widget_registry, f"Stammdaten{COLUMN_SEP}aufenthaltsort" ) assert len(person_location) == 1 self.person_location = cast(QComboBox, person_location[0]["widget"]) assert isinstance(self.person_location, QComboBox) self.person_location.currentIndexChanged.connect(self._custom_county_selection) selection_county = search_widgets_by_key( self.auto_form.widget_registry, f"Stammdaten{COLUMN_SEP}bundesland" ) assert len(selection_county) == 1 self.selection_county = cast(QComboBox, selection_county[0]["widget"]) assert isinstance(self.selection_county, QComboBox) self.selection_county.setProperty("styleClass", "stempel") self.selection_county.setEnabled(False) # def _custom_set_company_name_contact_person(self, value: str) -> None: # self.text_widget_set.setText(str(value)) def _custom_county_selection(self, idx: int) -> None: value = self.person_location.itemData(idx) if value == "Inland": self.selection_county.setEnabled(True) self.selection_county.setProperty("styleClass", "") self.selection_county.style().unpolish(self.selection_county) self.selection_county.style().polish(self.selection_county) self.selection_county.update() else: self.selection_county.setCurrentIndex(0) self.selection_county.setEnabled(False) self.selection_county.setProperty("styleClass", "stempel") self.selection_county.style().unpolish(self.selection_county) self.selection_county.style().polish(self.selection_county) self.selection_county.update() def reset_form(self) -> None: self.auto_form.reset_form() def clear_layout( layout: QLayout | None, ) -> None: if layout is None: return # for idx in range(start_idx, layout.count()): while layout.count(): child = layout.takeAt(0) if child is None: continue widget = child.widget() if widget is not None: widget.deleteLater() elif child.layout(): clear_layout(child.layout()) class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("Master") self.resize(1800, 1000) # MENU self.create_menu() # STACK: stack to change between 'sites' self.stack = QStackedWidget() self.setCentralWidget(self.stack) # MAIN PAGE: table view self.main_page = self.setup_main_page() self.stack.addWidget(self.main_page) # SITE: add new entries for 'Grunderfassung' self.new_entry_select = NewEntrySelect_view() self.new_entry_select.back_requested.connect(self.show_main_page) self.new_entry_select.company_requested.connect(self.show_page_initrec_company) self.new_entry_select.person_requested.connect(self.show_page_initrec_person) self.stack.addWidget(self.new_entry_select) # SITE: 'Grunderfassung Unternehmen' self.initrec_company = Page_InitRecCompany() self.initrec_company.back_main_requested.connect(self.show_main_page) self.initrec_company.back_requested.connect(self.show_new_entry_select) self.initrec_company.update_triggered.connect(self.update_grid) self.stack.addWidget(self.initrec_company) # SITE: 'Grunderfassung Person' self.initrec_person = Page_InitRecPerson() self.initrec_person.back_main_requested.connect(self.show_main_page) self.initrec_person.back_requested.connect(self.show_new_entry_select) self.initrec_person.update_triggered.connect(self.update_grid) self.stack.addWidget(self.initrec_person) def setup_main_page(self): # QMainWindow defines frame --> container widget needed for the middle main_widget = QWidget() outer_layout = QHBoxLayout(main_widget) vert_layout = QVBoxLayout() # add buttons new_btn = QPushButton("Neu →") new_btn.setFixedWidth(100) new_btn.setFixedHeight(40) new_btn.clicked.connect(self.show_new_entry_select) vert_layout.addWidget(new_btn) if DEBUG: update_btn = QPushButton("UPDATE") update_btn.setFixedWidth(100) update_btn.setFixedHeight(40) update_btn.clicked.connect(self.update_grid) clear_btn = QPushButton("CLEAR") clear_btn.setFixedWidth(100) clear_btn.setFixedHeight(40) clear_btn.clicked.connect(self._clear_layout) vert_layout.addWidget(update_btn) vert_layout.addWidget(clear_btn) separator = QFrame() separator.setFrameShape(QFrame.Shape.HLine) separator.setFrameShadow(QFrame.Shadow.Sunken) vert_layout.addWidget(separator) # container for table or grid container = QWidget() scroll_area = QScrollArea() scroll_area.setWidgetResizable(True) scroll_area.setMinimumWidth(700) scroll_area.setMaximumWidth(1500) scroll_area.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) # delete frame of ScrollArea to get a flat look scroll_area.setFrameShape(QFrame.Shape.NoFrame) scroll_area.setWidget(container) vert_layout.addSpacing(20) vert_layout.addWidget(scroll_area) # springs left and right of container to center the it outer_layout.addStretch(1) outer_layout.addLayout(vert_layout, stretch=100) outer_layout.addStretch(1) outer_layout.setAlignment(Qt.AlignmentFlag.AlignTop) # Wir geben dem Container ein vertikales Layout container_layout = QVBoxLayout(container) container_layout.setContentsMargins(0, 0, 0, 0) # grid for table without direct container self.header_grid = QGridLayout() self.header_grid.setColumnStretch(0, 1) self.header_grid.setColumnStretch(1, 1) self.header_grid.setColumnStretch(2, 1) self.header_grid.setColumnStretch(3, 1) self.header_grid.setColumnMinimumWidth(4, 100) self.grid = QGridLayout() self.grid.setSpacing(10) self.grid.setColumnStretch(0, 1) self.grid.setColumnStretch(1, 1) self.grid.setColumnStretch(2, 1) self.grid.setColumnStretch(3, 1) self.grid.setColumnMinimumWidth(4, 100) # add grid to vertical layout container_layout.addLayout(self.header_grid) container_layout.addLayout(self.grid) # spring below the layout to push it to top container_layout.addStretch() self.current_row = 0 headers = [ "UN/ NWP/ Kontaktperson", "Individualberatung", "Pauschalberatung", "Verlaufsprotokoll", "Datum", ] for col_idx, title in enumerate(headers): self.header_grid.addWidget(HeaderCell(title), self.current_row, col_idx) self.update_grid() return main_widget def _clear_layout(self) -> None: clear_layout(self.grid) def update_grid(self) -> None: clear_layout(self.grid) data = backend.front_get_company_list() for entry in data: self.add_row_to_grid(entry) def add_row_to_grid( self, entry: backend.FrontpageCompany, ): row = self.current_row # NAME cell_name = ClickableCell(entry.name, entry) cell_name.clicked.connect(self.goto_initial_recording) self.grid.addWidget(cell_name, row, 0) # DATE cell_date_name = entry.Metadaten_aktualisierung.date().strftime(DATE_FMT) cell_date = ClickableCell(cell_date_name, entry) cell_date.clicked.connect(self.goto_initial_recording) self.grid.addWidget(cell_date, row, 4) # self.grid.addWidget(ClickableCell(entry["c2"], entry), row, 1) # c3_value = entry.get("c3") # if c3_value: # self.grid.addWidget(ClickableCell(c3_value, entry), row, 2) # else: # empty_box = QFrame() # empty_box.setStyleSheet( # "QFrame { background-color: #f8fafc; border: 2px # dashed #e2e8f0; border-radius: 8px; }" # ) # self.grid.addWidget(empty_box, row, 2) # self.grid.addWidget(ClickableCell(entry.get("c4", ""), entry), row, 3) # self.grid.addWidget(ClickableCell(entry.get("date", ""), entry), row, 4) self.current_row += 1 # Zähler für den nächsten Eintrag erhöhen def goto_initial_recording( self, data: backend.FrontpageCompany, ): if data.is_company: self.initrec_company.auto_form.load_data(data.erfassung_id) self.stack.setCurrentWidget(self.initrec_company) else: self.initrec_person.auto_form.load_data(data.erfassung_id) self.stack.setCurrentWidget(self.initrec_person) # 1. Daten an die Detail-Seite übergeben # self.detail_page.update_content(data) # 2. Auf die Detail-Seite umblättern # self.stack.setCurrentWidget(self.detail_page) def show_main_page(self): # Zurück zur Tabelle blättern self.stack.setCurrentWidget(self.main_page) def show_new_entry_select(self): self.stack.setCurrentWidget(self.new_entry_select) def show_page_initrec_company(self): self.initrec_company.reset_form() self.stack.setCurrentWidget(self.initrec_company) def show_page_initrec_person(self): self.initrec_person.reset_form() self.stack.setCurrentWidget(self.initrec_person) # --- MENÜ LOGIK --- def create_menu(self): menu_bar = self.menuBar() # file menu file_menu = menu_bar.addMenu("Datei") exit_action = QAction("Beenden", self) exit_action.setShortcut("Ctrl+Q") exit_action.triggered.connect(self.close) file_menu.addAction(exit_action) # help menu help_menu = menu_bar.addMenu("Hilfe") about_action = QAction("Über", self) help_menu.addAction(about_action) # ** global exception handling def global_exception_handler(exc_type, exc_value, exc_traceback): """catches all unhandled errors""" # format error error_msg = "".join(traceback.format_exception(exc_type, exc_value, exc_traceback)) logger_gui.critical(f"UNEXPECTED ERROR:\n{error_msg}") # message to user # check if QApplication exists otherwise crashing of crash handler possible) if QApplication.instance(): msg_box = QMessageBox() msg_box.setIcon(QMessageBox.Icon.Critical) msg_box.setWindowTitle("Kritischer Fehler") msg_box.setText( "Ein unerwarteter Fehler ist aufgetreten. Die Details wurden protokolliert." ) # details for user or screenshots msg_box.setDetailedText(error_msg) msg_box.exec() sys.exit(1) def qt_message_handler(mode, context, message): """forwards internal Qt C++ warnings to Python loggers""" if mode == QtMsgType.QtInfoMsg: logger_gui.info(message) elif mode == QtMsgType.QtWarningMsg: logger_gui.warning(message) elif mode == QtMsgType.QtCriticalMsg: logger_gui.error(message) elif mode == QtMsgType.QtFatalMsg: logger_gui.critical(message) if __name__ == "__main__": sys.excepthook = global_exception_handler qInstallMessageHandler(qt_message_handler) try: app = QApplication(sys.argv) app.setStyleSheet(QSS) window = MainWindow() window.show() sys.exit(app.exec()) except Exception as err: logger_gui.critical("Fehler beim Starten der Anwendung:\n%s", str(err), exc_info=True)