Files
NAFKA-crm-gui/src/wce_crm/gui.py

2679 lines
90 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)