generated from dopt-python/py311
3751 lines
116 KiB
Python
3751 lines
116 KiB
Python
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())
|