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

3751 lines
116 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 enum
import json
import pickle
import re
import sys
import uuid
from collections import defaultdict
from collections.abc import Container, Iterable, Sequence
from pathlib import Path
from pprint import pformat
from typing import Annotated, Any, Final, Protocol, TypeAlias, TypedDict, TypeVar, cast
from typing_extensions import override
import babel
from pydantic import (
AwareDatetime,
BaseModel,
ConfigDict,
EmailStr,
Field,
ValidationError,
field_validator,
model_validator,
)
from PySide6.QtCore import (
QDate,
QEvent,
QObject,
Qt,
QTimer,
Signal,
)
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 as be_init_rec
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
if not wce_crm.constants.Config.DEVELOPMENT_STATE:
DEBUG = False
DEBUG_SEARCH_WIDGET = False
DEBUG_NO_DATABASE = False
DEBUG_GET_SET = False
# TODO remove
# logger_gui = BASE_LOGGER.getChild("wce")
# logger_gui.setLevel(logging.DEBUG)
# logger_search_widget = logger_gui.getChild("search_widget")
# logger_search_widget.setLevel(logging.DEBUG)
# logger_get_data = logger_gui.getChild("get_data")
# logger_get_data.setLevel(logging.DEBUG)
# logger_auto_form = logger_gui.getChild("get_data_auto_form")
# logger_auto_form.setLevel(logging.DEBUG)
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+)\]")
COLUMN_SEP: Final[str] = "__"
DATETIME_FMT: Final[str] = "%d.%m.%Y %H:%M:%S"
DATE_FMT: Final[str] = "%d.%m.%Y"
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)
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)]
class FlatBaseModel(BaseModel):
"""
Optimised Pydantic base class, which parses JSON strings and column
separators recursively and correctly
"""
@classmethod
def _recursive_parse_json(cls, data: Any) -> Any:
"""look for JSON list strings and parse them"""
if isinstance(data, str) and data.startswith("[") and data.endswith("]"):
try:
parsed = json.loads(data)
# Falls die Liste selbst wieder konvertiert werden muss (z.B. Sub-Dicts)
return cls._recursive_parse_json(parsed)
except json.JSONDecodeError:
return data
elif isinstance(data, dict):
return {k: cls._recursive_parse_json(v) for k, v in data.items()}
elif isinstance(data, list):
return [cls._recursive_parse_json(item) for item in data]
return data
@classmethod
def _recursive_unflatten(cls, data: Any) -> Any:
"""building nested structure using column spearator sequence"""
if isinstance(data, dict):
unflattened_level = {}
for key, value in data.items():
if COLUMN_SEP in key:
parts = key.split(COLUMN_SEP)
aktuell = unflattened_level
for part in parts[:-1]:
if part not in aktuell or not isinstance(aktuell[part], dict):
aktuell[part] = {}
aktuell = aktuell[part]
aktuell[parts[-1]] = value
else:
unflattened_level[key] = value
return {k: cls._recursive_unflatten(v) for k, v in unflattened_level.items()}
elif isinstance(data, list):
return [cls._recursive_unflatten(item) for item in data]
return data
@model_validator(mode="before")
@classmethod
def __unflatten_input(cls, data: Any) -> Any: # type: ignore
"""entry control: prepare flat DB/GUI data for Pydantic"""
if not isinstance(data, dict):
return data
# setp 1: convert all JSON-Strings to lists
json_parsed_data = cls._recursive_parse_json(data)
# step 2: build nested structure based on defined separator sequence
final_nested_data = cls._recursive_unflatten(json_parsed_data)
return final_nested_data
def to_db(self, *args, **kwargs) -> dict[str, Any]:
"""output for DB: flat, lists as JSON-Strings"""
nested = super().model_dump(*args, **kwargs)
return self.__flatten_dict(nested, serialize_lists=True)
def to_gui(self, *args, **kwargs) -> dict[str, Any]:
"""output for GUI: flat, but lists remain Python lists"""
nested = super().model_dump(*args, **kwargs)
return self.__flatten_dict(nested, serialize_lists=False)
@classmethod
def __flatten_dict(
cls,
nested_dict: dict,
parent_key: str = "",
serialize_lists: bool = True,
) -> dict[str, Any]:
"""recursive function to flatten the structure (for outputs)"""
items = []
for k, v in nested_dict.items():
new_key = f"{parent_key}{COLUMN_SEP}{k}" if parent_key else k
if isinstance(v, dict):
items.extend(cls.__flatten_dict(v, new_key, serialize_lists).items())
elif isinstance(v, list):
processed_list = []
for item in v:
if isinstance(item, dict):
processed_list.append(
cls.__flatten_dict(item, serialize_lists=serialize_lists)
)
else:
processed_list.append(item)
if serialize_lists:
items.append((new_key, json.dumps(processed_list, default=_parse_json)))
else:
items.append((new_key, processed_list))
else:
items.append((new_key, v))
return dict(items)
def _parse_json(value: Any) -> str:
if isinstance(value, datetime.date):
return value.isoformat()
elif isinstance(value, datetime.datetime):
return value.isoformat()
else:
raise TypeError
@dc.dataclass(slots=True)
class CountryList:
iso_to_country: dict[str, str]
for_dropdown: Sequence[tuple[str, str]]
def get_country_list_german() -> CountryList:
locale = babel.Locale("de", "DE")
countries: list[tuple[str, str]] = []
iso_to_country: dict[str, str] = {}
for iso_code, country_name in locale.territories.items():
if len(iso_code) == 2 and not iso_code.isdigit():
countries.append((country_name, iso_code))
iso_to_country[iso_code] = country_name
countries.sort(key=lambda x: x[0])
return CountryList(
iso_to_country=iso_to_country,
for_dropdown=tuple(countries),
)
def get_list_germany_states() -> CountryList:
states: list[tuple[str, str]] = []
short_code_to_name: dict[str, str] = {}
STATE_LIST: list[tuple[str, str]] = [
("Bayern", "BY"),
("Niedersachen", "NI"),
("Baden-Württemberg", "BW"),
("Berlin", "BE"),
("Brandenburg", "BB"),
("Bremen", "HB"),
("Hamburg", "HH"),
("Hessen", "HE"),
("Mecklenburg", "MV"),
("Nordrhein-Westfalen", "NW"),
("Rheinland-Pfalz", "RP"),
("Saarland", "SL"),
("Sachsen", "SN"),
("Sachsen-Anhalt", "ST"),
("Schleswig-Holstein", "SH"),
("Thüringen", "TH"),
]
STATE_LIST.sort(key=lambda x: x[1])
for country_name, code in STATE_LIST:
states.append((country_name, code))
short_code_to_name[code] = country_name
return CountryList(
iso_to_country=short_code_to_name,
for_dropdown=tuple(states),
)
COUNTRY_LIST: Final[CountryList] = get_country_list_german()
GERMAN_STATE_LIST: Final[CountryList] = get_list_germany_states()
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
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}")
class FormFieldType(enum.StrEnum):
GROUP = enum.auto()
TEXT = enum.auto()
LONGTEXT = enum.auto()
DATE = enum.auto()
DATETIME = enum.auto()
DROPDOWN = enum.auto()
EXTENDED_DROPDOWN = enum.auto()
DYNAMIC_LIST = enum.auto()
DYNAMIC_DROPDOWN_NUMERIC = enum.auto()
DYNAMIC_DROPDOWN_OPTION = enum.auto()
TEXT_SEARCH = enum.auto()
CUSTOM = enum.auto()
TEXT_DATE = enum.auto()
TEXT_DATETIME = enum.auto()
@dc.dataclass(slots=True)
class DropdownOption:
label: str
_data: dc.InitVar[Any | None] = None
data: Any = dc.field(init=False)
def __post_init__(
self,
_data: Any | None,
) -> None:
if _data is None:
self.data = self.label
else:
self.data = _data
@dc.dataclass(slots=True)
class FormField:
label: str
type: FormFieldType
children: Sequence[FormField] = dc.field(default_factory=list)
parent: FormField | None = None
required: bool = False
placeholder: str = ""
fill_value: str = ""
readonly: bool = False
options: dc.InitVar[Sequence[tuple[str, Any]]] = tuple()
dropdown_options: Sequence[DropdownOption] = dc.field(default=tuple(), init=False)
key: str = ""
tooltip: str = ""
info: str = ""
custom_widget: str = ""
init_label: str = dc.field(init=False)
ignore_get_data: bool = False
trigger_value: str = ""
enable_uuid_key: bool = False
def __post_init__(
self,
options: Sequence[tuple[str, Any]],
) -> None:
if not self.key and self.enable_uuid_key:
self.key = str(uuid.uuid4())
self.label = self.label.strip()
self.init_label = self.label.replace("*", "").replace(":", "")
if not self.label.endswith(":") and self.type is not FormFieldType.GROUP:
self.label += ":"
if self.required:
self.label += "*"
if self.type is FormFieldType.CUSTOM and not self.custom_widget:
raise ValueError("Custom widget must be named using parameter >custom_widget<")
elif self.type is FormFieldType.CUSTOM and self.custom_widget not in CUSTOM_WIDGETS:
raise KeyError(
(
f"Custom widget >{self.custom_widget}< is not a known member "
"of the custom widget registry"
)
)
if self.type in (FormFieldType.DROPDOWN, FormFieldType.EXTENDED_DROPDOWN):
self.dropdown_options = tuple(DropdownOption(op[0], op[1]) for op in options)
if self.type is FormFieldType.DYNAMIC_DROPDOWN_OPTION and not self.trigger_value:
raise ValueError(
"Dynamic Dropdown Option Widget must have a defined option or decision value"
)
if self.children:
self.required = self.required or any((child.required for child in self.children))
for child in self.children:
child.parent = self
def enhanced_label(
self,
add_text: str,
) -> str:
enhanced_label = self.init_label + f" {add_text}"
if not enhanced_label.endswith(":") and self.type is not FormFieldType.GROUP:
enhanced_label += ":"
if self.required:
enhanced_label += "*"
return enhanced_label
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],
) -> None: ...
class AutoFormUpdate(Protocol):
def __call__(
self,
id_: int,
data: dict[str, Any],
) -> None: ...
class AutoFormGet(Protocol):
def __call__(
self,
id_: int,
) -> dict[str, Any]: ...
@dc.dataclass(slots=True)
class AutoFormConfig:
model: type[FlatBaseModel]
data_insert: AutoFormInsert
data_update: AutoFormUpdate
data_get: AutoFormGet
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("")
# TODO: check removal
# def _insert_nested(
# target_dict: dict[str, Any],
# key_path: Sequence[str],
# value: Any,
# ):
# # keys 'a.b.c = 1' --> {'a': {'b': {'c': 1}}}
# for part in key_path[:-1]:
# target_dict = target_dict.setdefault(part, {})
# target_dict[key_path[-1]] = value
# def _get_nested(
# target_dict: dict[str, Any],
# key_path: Sequence[str],
# ) -> Any:
# # keys 'a.b.c = 1' --> {'a': {'b': {'c': 1}}}
# current = target_dict
# for part in key_path:
# if isinstance(current, dict) and part in current:
# current = current[part]
# else:
# return None # path does not exist
# return current
@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):
# TODO add to other custom widgets
# this should be a list: each dynamic list contains a list
# of such dictionaries
value = widget.get_form_data()
elif isinstance(
widget, (DynamicDropdownWidgetNumeric, DynamicDropdownWidgetOption, CustomWidget)
):
# this is a special data structure with some assumptions of the widget's internals
value = widget.get_form_data()
# TODO: check removal
# logger_get_data.info("Key: %s", key)
# logger_get_data.info("Value: %s", value)
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
# TODO check removal
# key_path = key.split(COLUMN_SEP)
# value = _get_nested(data, key_path)
# value = data.get(key, None)
if isinstance(
widget,
(
DynamicDropdownWidgetNumeric,
DynamicDropdownWidgetOption,
Grunderfassung_SuchWidget,
),
):
value = data
else:
if key not in data:
logger_gui.debug("---- Key not in data: %s")
logger_gui.debug("-------- Data: %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_Unternehmen(FlatBaseModel):
# default in SQLAlchemy with lambda and timezone-aware datetime)
Metadaten_erstellung: AwareDatetime | None = None
Metadaten_aktualisierung: AwareDatetime | None = None # see above
Metadaten_nutzer: str | None
Grunderfassung_fallnummer: str
Grunderfassung_notiz: str | None
Partnersuche: Grunderfassung_PartnerSuche
Projektrelevanz: Grunderfassung_Projektrelevanz
Kontaktperson: Grunderfassung_Kontaktperson
Stammdaten: Grunderfassung_Stammdaten
WeitereInfos: Grunderfassung_WeitereInfos
Schulbildung: list[Grunderfassung_Schulbildung]
HoehereBildung: list[Grunderfassung_HoehereBildung]
Arbeitserfahrung: list[Grunderfassung_Arbeitserfahrung]
Sprachkenntnisse: list[Grunderfassung_Sprachen]
class Grunderfassung_PartnerSuche(BaseModel):
model_config = ConfigDict(str_strip_whitespace=True)
un_suche: int
person_suche: int
kanal_aufmerksamkeit: str | None
class Grunderfassung_Projektrelevanz(BaseModel):
model_config = ConfigDict(str_strip_whitespace=True)
relevanz: str
foerderperiode: str | None = None
# @field_validator("relevanz", mode="before")
# @classmethod
# def str_to_bool(cls, value: Any) -> Any:
# if isinstance(value, str):
# value = value.strip().lower()
# if value == "ja":
# return True
# elif value == "nein":
# return False
# raise ValueError("Wert muss 'ja', 'nein', True oder False sein.")
# return value
# TODO remove
# class Grunderfassung_ProjektrelevanzStatus_Status(BaseModel):
# model_config = ConfigDict(str_strip_whitespace=True)
# relevanz: bool
# foerderperiode: list[str | None] | None = None
# @field_validator("relevanz", mode="before")
# @classmethod
# def str_to_bool(cls, value: Any) -> Any:
# if isinstance(value, str):
# value = value.strip().lower()
# if value == "ja":
# return True
# elif value == "nein":
# return False
# raise ValueError("Wert muss 'ja', 'nein', True oder False sein.")
# return value
class Grunderfassung_Kontaktperson(BaseModel):
model_config = ConfigDict(str_strip_whitespace=True)
KP_name_partner: str | None
KP_titel: str | None
KP_anrede_anschrift: str | None
KP_name: str | None
KP_vorname: str | None
KP_festnetznummer: str | None
KP_mobilfunknummer: str | None
KP_email: EmailStr | None
KP_funktion_beziehung: str | None
KP_adresse: str | None
ValidAge = Annotated[int, Field(ge=0, le=99)]
class Grunderfassung_Stammdaten(BaseModel):
model_config = ConfigDict(str_strip_whitespace=True)
titel: str | None
anrede_anschrift: str
name: str
vorname: str | None
geburtsdatum: datetime.date | None
herkunftsland: str
staatsangehoerigkeit: str | None
rueckkehrer: bool | None
aufenthaltsort: str | None
strasse: str | None
hausnummer: str | None
PLZ: str | None
ort: str | None
bundesland: str | None
land: str | None
festnetznummer: str | None
mobilfunknummer: str | None
email: EmailStr | None
familienstand: str | None
anzahl_kinder: Grunderfassung_Stammdaten_AnzahlKinder
@field_validator("rueckkehrer", mode="before")
@classmethod
def str_to_bool(cls, value: Any) -> Any:
if isinstance(value, str):
value = value.strip().lower()
if value == "ja":
return True
elif value == "nein":
return False
raise ValueError("Wert muss 'ja', 'nein', True oder False sein.")
return value
class Grunderfassung_Stammdaten_AnzahlKinder(BaseModel):
model_config = ConfigDict(str_strip_whitespace=True)
anzahl: int | None
alter: list[ValidAge | None] | None = None
class Grunderfassung_WeitereInfos(BaseModel):
model_config = ConfigDict(str_strip_whitespace=True)
WI_deutsch_sprache: str | None
WI_aufenthaltstitel: str | None
WI_gueltigkeit_aufenthaltstitel: datetime.date | None
WI_arbeitsstatus: str | None
WI_meldung_institution: str | None
class Grunderfassung_Schulbildung(BaseModel):
model_config = ConfigDict(str_strip_whitespace=True)
SB_abschluss: str | None
SB_abschlussgrad: str | None
SB_schule: str | None
SB_ort: str | None
SB_land: str | None
SB_abschlussjahr: str | None
SB_bemerkungsfeld: str | None
class Grunderfassung_HoehereBildung(BaseModel):
model_config = ConfigDict(str_strip_whitespace=True)
HB_anerkennung: str | None
HB_abschlussgrad: str | None
HB_abschlussgrad_dokument: str | None
HB_organisation: str | None
HB_beruf: str | None
HB_land: str | None
HB_ort: str | None
HB_abschlussjahr: str | None
HB_bemerkungsfeld: str | None
class Grunderfassung_Arbeitserfahrung(BaseModel):
model_config = ConfigDict(str_strip_whitespace=True)
AE_branche: str | None
AE_bezeichnung: str | None
AE_funktion: str | None
AE_unternehmen: str | None
AE_land: str | None
AE_zeitspanne: str | None
AE_beschaeftigungsart: str | None
AE_bemerkungsfeld: str | None
class Grunderfassung_Sprachen(BaseModel):
model_config = ConfigDict(str_strip_whitespace=True)
SP_sprache: str | None
SP_niveau: str | None
SP_nachweis: str | None
SP_art_nachweis: str | None = None
SP_datum_nachweis: datetime.date | None = None
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: be_init_rec.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: be_init_rec.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 = be_init_rec.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 = be_init_rec.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 = be_init_rec.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 = be_init_rec.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"""
save_clicked_form = 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)
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)
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)
self.current_id: int = -1
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 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)
# TODO remove loading from file (obsolete)
else:
logger_auto_form.debug("Load from pickled object:")
loaded_data = load_pydantic_model_dict_db()
logger_auto_form.debug(
"Loaded data dict:\n%s Passing to Pydantic...", pformat(loaded_data)
)
model = Grunderfassung_Unternehmen(**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
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_pydantic_model_dict_db(validated_data)
# TODO 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")
self.cfg.data_insert(db_data)
else:
logger_auto_form.debug("Update triggered")
self.cfg.data_update(self.current_id, db_data)
logger_auto_form.info("Data saved successfully")
self.save_clicked_form.emit()
# 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
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]]:
# TODO code cleansing
# raw_data = get_form_data(self.widget_registry)
# form_data = get_form_data(self.widget_registry)
# logger_get_data.debug(
# "\n\n\n------- Form data DynamicListWidget:\n%s", pformat(form_data)
# )
logger_get_data.info("DynamicListWidget - SubForms")
# for sub_form in self.sub_forms:
# logger_get_data.info("SubForm index %d", sub_form.index)
# pprint_registry(sub_form.registry)
form_data = [get_form_data(sub.registry) for sub in self.sub_forms]
# ignored_keys: list[str] = []
# for d in form_data:
# ignored_keys.extend(d.ignored_keys)
# data = [d.data for d in form_data]
logger_get_data.debug("##################")
logger_get_data.debug("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
# TODO check removal
# for sub_data in data:
# self.add_entry()
# current_sub_form = self.sub_forms[-1]
# registry_entries = search_widgets_by_key(
# self.widget_registry, f"-[{current_sub_form.index}]"
# )
# for reg_entry in registry_entries:
# widget = reg_entry["widget"]
# field_def = reg_entry["form_field"]
# value = sub_data[field_def.key]
# set_widget_value(widget, value)
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)
# TODO need to get the correct index
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]
# assert key in whole_registry, "key not in Dynamic DD Option"
whole_registry = self._get_combined_registry(sub_forms_only=True)
logger_get_data.debug("[DynamicDD-Option] Whole widget registry:\n")
pprint_registry(whole_registry)
logger_get_data.debug(">>>>>>>>> Call set_form_data for DynamicDropdown")
logger_get_data.debug("Data before set of subforms:%s", pformat(data))
assert len(self.sub_forms) == num_subforms, f"{len(self.sub_forms)=}"
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(be_init_rec.FrontpageCompany)
def __init__(
self,
text: str,
data_record: be_init_rec.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: print("Person gewählt"))
layout.addWidget(back_btn)
layout.addSpacing(15)
layout.addWidget(self.title_label)
layout.addWidget(btn_company)
layout.addWidget(btn_person)
CUSTOM_WIDGETS: Final[dict[str, type[CustomWidget]]] = {
"grunderfassung_suche": Grunderfassung_SuchWidget,
}
FORM_FIELDS_SEARCH_HEAD = [
FormField(
"Suche",
FormFieldType.EXTENDED_DROPDOWN,
required=True,
key="un_suche",
placeholder="Suche...",
),
FormField(
"Name Unternehmen/Netzwerkpartner",
FormFieldType.TEXT,
required=False,
key="un_name",
readonly=True,
info="ma_unternehmensname",
),
FormField(
"Straße",
FormFieldType.TEXT,
required=False,
key="un_straße",
readonly=True,
info="ma_strasse",
),
FormField(
"Hausnummer",
FormFieldType.TEXT,
required=False,
key="un_hausnummer",
readonly=True,
info="ma_hausnummer",
),
FormField(
"PLZ",
FormFieldType.TEXT,
required=False,
key="un_PLZ",
readonly=True,
info="ma_plz",
),
FormField(
"Ort",
FormFieldType.TEXT,
required=False,
key="un_ort",
readonly=True,
info="ma_ort",
),
FormField(
"Suche Ansprechpartner",
FormFieldType.EXTENDED_DROPDOWN,
required=True,
key="person_suche",
placeholder="Suche...",
),
FormField(
"Titel",
FormFieldType.TEXT,
required=False,
key="person_titel",
readonly=True,
info="an_titel",
),
FormField(
"Anrede",
FormFieldType.TEXT,
required=False,
key="person_anrede",
readonly=True,
info="an_anrede",
),
FormField(
"Name",
FormFieldType.TEXT,
required=False,
key="person_name",
readonly=True,
info="an_nachname",
),
FormField(
"Vorname",
FormFieldType.TEXT,
required=False,
key="person_vorname",
readonly=True,
info="an_vorname",
),
FormField(
"Telefon",
FormFieldType.TEXT,
required=False,
key="person_telefon",
readonly=True,
info="an_festnetz",
),
FormField(
"Mobil",
FormFieldType.TEXT,
required=False,
key="person_mobilfunk",
readonly=True,
info="an_mobil",
),
FormField(
"E-Mail",
FormFieldType.TEXT,
required=False,
key="person_email",
readonly=True,
info="an_mail",
),
FormField(
"Funktion im Unternehmen",
FormFieldType.TEXT,
required=False,
key="person_funktion",
readonly=True,
info="an_position",
),
FormField(
"Wie sind Sie auf uns aufmerksam geworden?",
FormFieldType.DROPDOWN,
required=False,
key="kanal_aufmerksamkeit",
options=[
("Agentur für Arbeit", None),
("Ausländerbehörde", None),
("Jobcenter", None),
("Freunde/Familie", None),
("Anerkennungsstelle", None),
("Beratungsstelle", None),
("Internet", None),
("Arbeitgeber", None),
("Bildungsdienstleister", None),
("Welcome-Mappe", None),
("Newsletter WFE", None),
("Newsletter RM", None),
("Sonstiges", None),
],
),
]
FORM_FIELDS_CONTACT_PERSON = [
FormField(
"Name Unternehmen/Netzwerkpartner (pre-filled von Suche)",
FormFieldType.TEXT,
key="KP_name_partner",
required=False,
placeholder="Text wird nach gewähltem Unternehmen angezeigt",
readonly=True,
),
FormField(
"Titel",
FormFieldType.TEXT,
key="KP_titel",
required=False,
tooltip=(
"* nur wenn anrufende Person oder kontaktaufnehmende Person "
"nicht die zu beratende Person ist"
),
),
FormField(
"Anrede_Anschrift",
FormFieldType.TEXT,
key="KP_anrede_anschrift",
required=True,
),
FormField(
"Name",
FormFieldType.TEXT,
key="KP_name",
required=True,
),
FormField(
"Vorname",
FormFieldType.TEXT,
key="KP_vorname",
required=False,
),
FormField(
"Festnetznummer",
FormFieldType.TEXT,
key="KP_festnetznummer",
required=False,
),
FormField(
"Mobilfunknummer",
FormFieldType.TEXT,
key="KP_mobilfunknummer",
required=False,
),
FormField(
"E-Mail",
FormFieldType.TEXT,
key="KP_email",
required=False,
),
FormField(
"Funktion/Beziehung zur beratenden Person",
FormFieldType.TEXT,
key="KP_funktion_beziehung",
required=False,
),
FormField(
"Adresse",
FormFieldType.LONGTEXT,
key="KP_adresse",
required=False,
),
]
FORM_FIELDS_MASTER_DATA = [
FormField(
"Titel",
FormFieldType.TEXT,
key="titel",
required=False,
tooltip=(
"* nur wenn anrufende Person oder kontaktaufnehmende Person "
"nicht die zu beratende Person ist"
),
),
FormField(
"Anrede",
FormFieldType.TEXT,
key="anrede_anschrift",
required=True,
),
FormField(
"Name",
FormFieldType.TEXT,
key="name",
required=True,
),
FormField(
"Vorname",
FormFieldType.TEXT,
key="vorname",
required=False,
),
FormField(
"Geburtsdatum",
FormFieldType.DATE,
key="geburtsdatum",
required=False,
tooltip=(
"* Wichtig zu erfragen, da u.a. Mindestgehaltsschwelle davon abhängt "
"(Regelung bei Ü45 Jahre)"
),
),
FormField(
"Herkunftsland",
FormFieldType.EXTENDED_DROPDOWN,
key="herkunftsland",
required=True,
placeholder="Suche...",
options=COUNTRY_LIST.for_dropdown,
tooltip=("* Wichtig zu erfragen aufgrund eventueller EU-Freizügigkeitsregelung"),
),
FormField(
"Staatsangehörigkeit",
FormFieldType.EXTENDED_DROPDOWN,
key="staatsangehoerigkeit",
required=False,
placeholder="Suche...",
options=COUNTRY_LIST.for_dropdown,
tooltip=("* Wichtig zu erfragen aufgrund eventueller EU-Freizügigkeitsregelung"),
),
FormField(
"Rückkehrer",
FormFieldType.DROPDOWN,
key="rueckkehrer",
required=False,
options=[("ja", None), ("nein", None)],
tooltip=("* Wichtig zu erfragen aufgrund eventueller EU-Freizügigkeitsregelung"),
),
FormField(
"Wo befindet sich die Person?",
FormFieldType.DROPDOWN,
key="aufenthaltsort",
required=True,
options=[("Inland", None), ("Ausland EU/EWR", None), ("Ausland Drittstaat", None)],
),
FormField(
"Straße",
FormFieldType.TEXT,
key="strasse",
required=False,
),
FormField(
"Hausnummer",
FormFieldType.TEXT,
key="hausnummer",
required=False,
),
FormField(
"PLZ",
FormFieldType.TEXT,
key="PLZ",
required=False,
),
FormField(
"Ort",
FormFieldType.TEXT,
key="ort",
required=False,
),
FormField(
"Bundesland",
FormFieldType.DROPDOWN,
key="bundesland",
required=False,
options=GERMAN_STATE_LIST.for_dropdown,
tooltip=(
"nur wenn Inland angegeben und die Angabe zieht es in keine Dokumente "
"rüber! Liste Bundesländer verwenden"
),
),
FormField(
"Land",
FormFieldType.EXTENDED_DROPDOWN,
key="land",
required=False,
placeholder="Suche...",
options=COUNTRY_LIST.for_dropdown,
),
FormField(
"Festnetznummer",
FormFieldType.TEXT,
key="festnetznummer",
required=False,
),
FormField(
"Mobilfunknummer",
FormFieldType.TEXT,
key="mobilfunknummer",
required=False,
),
FormField(
"E-Mail",
FormFieldType.TEXT,
key="email",
required=False,
),
FormField(
"Familienstand",
FormFieldType.TEXT,
key="familienstand",
required=False,
tooltip="* Wichtig zu erfragen aufgrund Lebensunterhaltssicherung",
),
FormField(
"Anzahl Kinder",
FormFieldType.DYNAMIC_DROPDOWN_NUMERIC,
required=False,
tooltip="* Wichtig zu erfragen aufgrund Lebensunterhaltssicherung",
key="anzahl_kinder",
children=[
FormField(
"Anzahl Kinder",
FormFieldType.DROPDOWN,
required=False,
options=[(str(x), None) for x in range(11)],
tooltip="* Wichtig zu erfragen aufgrund Lebensunterhaltssicherung",
key="anzahl",
children=[
FormField("Alter Kind", FormFieldType.TEXT, key="alter"),
],
),
],
),
]
FORM_FIELDS_ADDITIONAL_DATA = [
FormField(
"Deutsch als Kommunikationssprache",
FormFieldType.DROPDOWN,
required=False,
key="WI_deutsch_sprache",
options=[
("nein", None),
("ja, als Muttersprache", None),
("ja, als Fremdsprache", None),
],
),
FormField(
"Aufenthaltstitel",
FormFieldType.DROPDOWN,
required=False,
key="WI_aufenthaltstitel",
options=[
("anerkannter Flüchtling §§ 22 - 26 AufenthG", None),
("Aufenthaltsgestattung §55 AufenthG", None),
("Blaue Karte EU § 18g AufenthG", None),
("BüMA (Bescheinigung über Meldung als Asylsuchender)", None),
("Duldung § 60 AufenthG", None),
("bisher kein Aufenthaltstitel", None),
("Deutscher", None),
("familiäre Gründe §§ 27 - 36 AufenthG", None),
("Niederlassungserlaubnis §9 AufenthG", None),
("Staatsbürger EUR/EWR/CH", None),
("Aufenthalt für Ausbildung §§ 16 - 17 AufenthG", None),
("Aufenthalt für Erwerbstätigkeit §§ 18- 21 AufenthG", None),
("Chancenaufenthaltsrecht §104c AufenthG", None),
("Sonstiges", None),
],
tooltip="sofern nicht bekannt, unbedingt einfordern",
),
FormField(
"Gültigkeit Aufenthaltsstatus",
FormFieldType.DATE,
required=False,
key="WI_gueltigkeit_aufenthaltstitel",
),
FormField(
"Arbeitsstatus aktuell",
FormFieldType.DROPDOWN,
required=False,
key="WI_arbeitsstatus",
options=[
("Arbeitslos", None),
("Ausbildung/Qualifizierung Inland", None),
("geringfügig beschäftigt", None),
("in Anstellung Inland", None),
("selbstständig Inland", None),
("Ausbildung/Qualifizierung Ausland", None),
("in Anstellung Ausland", None),
("selbstständig Ausland", None),
],
),
FormField(
"Gemeldet bei Institutionen ",
FormFieldType.DROPDOWN,
required=False,
key="WI_meldung_institution",
options=[
("bei keiner", None),
("Jobcenter mit Leistungsbezug", None),
("Jobcenter ohne Leistungsbezug", None),
("Sozialamt mit Leistungsbezug", None),
("Sozialamt ohne Leistungsbezug", None),
("Agentur für Arbeit mit Leistungsbezug", None),
("Agentur für Arbeit ohne Leistungsbezug", None),
],
),
]
FORM_FIELDS_SCHOOL = [
FormField("Abschluss", FormFieldType.TEXT, required=False, key="SB_abschluss"),
FormField(
"Abschlussgrad laut Dokument",
FormFieldType.TEXT,
required=False,
key="SB_abschlussgrad",
),
FormField(
"Schule",
FormFieldType.TEXT,
required=False,
key="SB_schule",
),
FormField(
"Ort",
FormFieldType.TEXT,
required=False,
key="SB_ort",
),
FormField(
"Land",
FormFieldType.EXTENDED_DROPDOWN,
key="SB_land",
required=False,
placeholder="Suche...",
options=COUNTRY_LIST.for_dropdown,
),
FormField("Abschlussjahr", FormFieldType.TEXT, required=False, key="SB_abschlussjahr"),
FormField(
"Bemerkungsfeld",
FormFieldType.TEXT,
required=False,
key="SB_bemerkungsfeld",
),
]
FORM_FIELDS_HIGHER_EDUCATION = [
FormField(
"Anerkennung",
FormFieldType.TEXT,
required=False,
key="HB_anerkennung",
),
FormField(
"Abschlussgrad",
FormFieldType.TEXT,
required=False,
key="HB_abschlussgrad",
tooltip=(
"bitte den Titel eingeben z.B. Doktor, Diplom oder "
"Betriebswirt (Fachschulabschluss)"
),
),
FormField(
"Abschlussgrad laut Dokument",
FormFieldType.TEXT,
required=False,
key="HB_abschlussgrad_dokument",
),
FormField(
"Hochschule / Ausbildungsbetrieb / Berufsschule",
FormFieldType.TEXT,
required=False,
key="HB_organisation",
),
FormField(
"Beruf / Fachrichtung",
FormFieldType.TEXT,
required=False,
key="HB_beruf",
tooltip=(
"bitte spezifizieren z.B. Allgemeinmedizin, Ingenieur Maschinenbau, "
"technischer Betriebswirt Datenverarbeitung"
),
),
FormField(
"Land",
FormFieldType.EXTENDED_DROPDOWN,
key="HB_land",
required=False,
placeholder="Suche...",
options=COUNTRY_LIST.for_dropdown,
),
FormField(
"Ort",
FormFieldType.TEXT,
required=False,
key="HB_ort",
),
FormField(
"Abschlussjahr",
FormFieldType.TEXT,
required=False,
key="HB_abschlussjahr",
),
FormField(
"Bemerkungsfeld",
FormFieldType.TEXT,
required=False,
key="HB_bemerkungsfeld",
tooltip="z.B. Promotionen oder den Studiengang angeben",
),
]
FORM_FIELDS_WORK_EXPERIENCE = [
FormField(
"Branche",
FormFieldType.DROPDOWN,
required=False,
key="AE_branche",
options=[
(part, None)
for part in (
"Metallerzeugung & -bearbeitung",
"Elektro, Energie, Chemie",
"IT & Software",
"Kunststoff, Papier, Textil",
"Logistik, Verkehr, Transport",
"Handwerk, Bau, Grüne Berufe",
"Gesundheit & Pflege",
"Tourismus & Gastronomie",
"Handel",
"Bildung & Soziales",
"Entwicklung, Planung, Qualität",
"Administration, Finanzen, Verwaltung",
"Marketing, Design, Vertrieb",
"Einkauf, Lager, Wartung",
"Sonstige",
"Keine Schwerpunkte, branchenübergreifende Rekrutierung",
)
],
),
FormField(
"Berufsbezeichnung/Tätigkeit",
FormFieldType.TEXT,
required=False,
key="AE_bezeichnung",
),
FormField(
"Funktion",
FormFieldType.DROPDOWN,
required=False,
key="AE_funktion",
options=[
("Auszubildender", None),
("Fachkraft", None),
("Hilfskraft", None),
("Akademiker", None),
("Führungskraft", None),
("Praktikant", None),
("FSJ/BFD", None),
("Elternzeit", None),
("Sabbatical", None),
("Sonstiges", None),
],
),
FormField(
"Unternehmen",
FormFieldType.TEXT,
required=False,
key="AE_unternehmen",
),
FormField(
"Land",
FormFieldType.EXTENDED_DROPDOWN,
key="AE_land",
required=False,
placeholder="Suche...",
options=COUNTRY_LIST.for_dropdown,
),
FormField(
"Zeitspanne (von ... bis ...)",
FormFieldType.TEXT,
required=False,
key="AE_zeitspanne",
),
FormField(
"Beschäftsigungsart",
FormFieldType.DROPDOWN,
required=False,
key="AE_beschaeftigungsart",
options=[
("Vollzeit", None),
("Teilzeit", None),
("Sonstiges", None),
],
tooltip="Minijob, Praktikum, Wehrdienst, soziale Dienste",
),
FormField(
"Bemerkungsfeld",
FormFieldType.TEXT,
required=False,
key="AE_bemerkungsfeld",
),
]
FORM_FIELDS_LANGUAGES = [
FormField(
"Sprache",
FormFieldType.TEXT,
required=False,
key="SP_sprache",
),
FormField(
"Niveau",
FormFieldType.DROPDOWN,
required=False,
key="SP_niveau",
options=[
("A1", None),
("A2", None),
("B1", None),
("B2", None),
("C1", None),
("C2", None),
],
),
FormField(
"Nachweis",
FormFieldType.DYNAMIC_DROPDOWN_OPTION,
key="",
trigger_value="vorhanden",
children=[
FormField(
"Nachweis",
FormFieldType.DROPDOWN,
required=False,
options=[("vorhanden", None), ("nicht vorhanden", None)],
key="SP_nachweis",
children=[
FormField(
"Art des Nachweises",
FormFieldType.TEXT,
required=False,
key="SP_art_nachweis",
),
FormField(
"Datum des Nachweises",
FormFieldType.DATE,
required=False,
key="SP_datum_nachweis",
),
],
),
],
),
# FormField(
# "Nachweis",
# FormFieldType.DROPDOWN,
# required=False,
# key="SP_nachweis",
# options=[
# ("vorhanden", None),
# ("nicht vorhanden", None),
# ],
# ),
# FormField(
# "Art des Nachweises (NUR WENN VORHANDEN)",
# FormFieldType.TEXT,
# required=False,
# key="SP_art_nachweis",
# ),
# FormField(
# "Datum des Nachweises (NUR WENN VORHANDEN)",
# FormFieldType.DATE,
# required=False,
# key="SP_datum_nachweis",
# ),
]
FORM_FIELDS = [
FormField(
"Ersteintrag Datum",
FormFieldType.TEXT_DATETIME,
required=False,
key="Metadaten_erstellung",
readonly=True,
ignore_get_data=True,
),
FormField(
"Aktualisierung Datum",
FormFieldType.TEXT_DATETIME,
required=False,
key="Metadaten_aktualisierung",
readonly=True,
ignore_get_data=True,
),
FormField(
"Aktualisierung Nutzer",
FormFieldType.TEXT,
required=False,
key="Metadaten_nutzer",
readonly=True,
),
FormField(
"Fallnummer",
FormFieldType.TEXT,
required=True,
key="Grunderfassung_fallnummer",
),
FormField(
"Notizen",
FormFieldType.LONGTEXT,
required=False,
key="Grunderfassung_notiz",
),
FormField(
"Suche",
FormFieldType.CUSTOM,
custom_widget="grunderfassung_suche",
key="Partnersuche",
children=FORM_FIELDS_SEARCH_HEAD,
),
FormField(
"Status && Projektrelevanz",
FormFieldType.GROUP,
key="Projektrelevanz",
children=[
FormField(
"Projektrelevanz",
FormFieldType.DYNAMIC_DROPDOWN_OPTION,
key="",
trigger_value="ja",
children=[
FormField(
"Relevanz",
FormFieldType.DROPDOWN,
required=True,
options=[("ja", None), ("nein", None)],
key="relevanz",
children=[
FormField(
"Förderperiode", FormFieldType.TEXT, key="foerderperiode"
),
],
),
],
),
],
),
FormField(
"Daten Kontaktperson",
FormFieldType.GROUP,
key="Kontaktperson",
children=FORM_FIELDS_CONTACT_PERSON,
),
FormField(
"Stammdaten",
FormFieldType.GROUP,
key="Stammdaten",
children=FORM_FIELDS_MASTER_DATA,
),
FormField(
"Weitere Informationen",
FormFieldType.GROUP,
key="WeitereInfos",
children=FORM_FIELDS_ADDITIONAL_DATA,
),
FormField(
"Schulbildung",
FormFieldType.DYNAMIC_LIST,
children=FORM_FIELDS_SCHOOL,
key="Schulbildung",
),
FormField(
"Studium/Ausbildung",
FormFieldType.DYNAMIC_LIST,
children=FORM_FIELDS_HIGHER_EDUCATION,
key="HoehereBildung",
),
FormField(
"Arbeitserfahrung",
FormFieldType.DYNAMIC_LIST,
children=FORM_FIELDS_WORK_EXPERIENCE,
key="Arbeitserfahrung",
),
FormField(
"Sprachkenntnisse",
FormFieldType.DYNAMIC_LIST,
children=FORM_FIELDS_LANGUAGES,
key="Sprachkenntnisse",
),
]
CONFIG_GRUNDERFASSUNG_UNTERNEHMEN: Final[AutoFormConfig] = AutoFormConfig(
model=Grunderfassung_Unternehmen,
data_insert=be_init_rec.insert_initial_recording,
data_update=be_init_rec.update_initial_recording,
data_get=be_init_rec.get_initial_recording,
ignored_keys=(
"Metadaten_erstellung",
"Metadaten_aktualisierung",
),
form_fields=FORM_FIELDS,
)
# TODO clean code
class PageFormCompany(QWidget):
back_main_requested = Signal() # back to main page
back_requested = Signal() # back button
save_clicked_form = 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)
# --- HAUPTINHALT ---
container = QWidget()
# SCROLL-BEREICH
scroll_area = QScrollArea()
scroll_area.setWidgetResizable(
True
) # WICHTIG: Erlaubt dem Grid im Inneren, sich an die Breite anzupassen
scroll_area.setMinimumWidth(700)
scroll_area.setMaximumWidth(1000)
scroll_area.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
# Optional: Rahmen der ScrollArea entfernen, damit es "flacher" und moderner aussieht
scroll_area.setFrameShape(QFrame.Shape.NoFrame)
scroll_area.setWidget(container)
# vert_layout.addSpacing(20)
vert_layout.addWidget(scroll_area)
container_layout = QVBoxLayout(container)
container_layout.setContentsMargins(0, 0, 0, 0)
# --- AUTO FORM LAYOUT ---
container_layout.addSpacing(20)
# title = QLabel("--- Automatische Form ---")
# title.setStyleSheet("font-size: 14px; font-style: italic;") # font-weight: bold;
# container_layout.addWidget(title)
# container_layout.addWidget(MyFormPart(FORM_FIELD_DEF, "Test-Gruppe"))
self.auto_form = AutoForm(cfg=CONFIG_GRUNDERFASSUNG_UNTERNEHMEN)
container_layout.addWidget(self.auto_form)
self.auto_form.save_clicked_form.connect(lambda: self.save_clicked_form.emit())
container_layout.addSpacing(30)
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())
# 2. Das Hauptfenster mit dem Grid-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_company_page)
self.stack.addWidget(self.new_entry_select)
# SITE: 'Grunderfassung Unternehmen'
self.company_recording_page = PageFormCompany()
self.company_recording_page.back_main_requested.connect(self.show_main_page)
self.company_recording_page.back_requested.connect(self.show_new_entry_select)
self.company_recording_page.save_clicked_form.connect(self.update_grid)
self.stack.addWidget(self.company_recording_page)
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.current_row += 1
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 = be_init_rec.get_company_list()
for entry in data:
self.add_row_to_grid(entry)
def add_row_to_grid(self, entry: be_init_rec.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: be_init_rec.FrontpageCompany,
):
self.company_recording_page.auto_form.load_data(data.erfassung_id)
self.stack.setCurrentWidget(self.company_recording_page)
# 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_company_page(self):
self.company_recording_page.reset_form()
self.stack.setCurrentWidget(self.company_recording_page)
# --- 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)
if __name__ == "__main__":
app = QApplication(sys.argv)
app.setStyleSheet(QSS)
window = MainWindow()
window.show()
sys.exit(app.exec())