generated from dopt-python/py311
2679 lines
90 KiB
Python
2679 lines
90 KiB
Python
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)
|