better automatic export, prepare data validation for backend export

This commit is contained in:
2026-05-18 16:53:39 +02:00
parent 2931bf0876
commit 249449cf59
4 changed files with 593 additions and 59 deletions

44
pdm.lock generated
View File

@@ -5,7 +5,7 @@
groups = ["default", "dev", "lint", "nb", "tests"] groups = ["default", "dev", "lint", "nb", "tests"]
strategy = ["inherit_metadata"] strategy = ["inherit_metadata"]
lock_version = "4.5.0" lock_version = "4.5.0"
content_hash = "sha256:ae1cefda69bbf1f63d7c5d8c8b19f5dae8525f42c8550e9a44b186d7f57fe7bc" content_hash = "sha256:611d8f56a617297efdc1daedd4a2cd7c21e58bc0a57288a8f230db9015a6adef"
[[metadata.targets]] [[metadata.targets]]
requires_python = ">=3.11,<3.14" requires_python = ">=3.11,<3.14"
@@ -951,6 +951,17 @@ files = [
{file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"}, {file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"},
] ]
[[package]]
name = "dnspython"
version = "2.8.0"
requires_python = ">=3.10"
summary = "DNS toolkit"
groups = ["default"]
files = [
{file = "dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af"},
{file = "dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f"},
]
[[package]] [[package]]
name = "docutils" name = "docutils"
version = "0.22.4" version = "0.22.4"
@@ -976,6 +987,21 @@ files = [
{file = "dopt_basics-0.2.4.tar.gz", hash = "sha256:c21fbe183bec5eab4cfd1404e10baca670035801596960822d0019e6e885983f"}, {file = "dopt_basics-0.2.4.tar.gz", hash = "sha256:c21fbe183bec5eab4cfd1404e10baca670035801596960822d0019e6e885983f"},
] ]
[[package]]
name = "email-validator"
version = "2.3.0"
requires_python = ">=3.8"
summary = "A robust email address syntax and deliverability validation library."
groups = ["default"]
dependencies = [
"dnspython>=2.0.0",
"idna>=2.0.0",
]
files = [
{file = "email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4"},
{file = "email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426"},
]
[[package]] [[package]]
name = "execnet" name = "execnet"
version = "2.1.2" version = "2.1.2"
@@ -2781,6 +2807,22 @@ files = [
{file = "pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025"}, {file = "pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025"},
] ]
[[package]]
name = "pydantic"
version = "2.13.4"
extras = ["email"]
requires_python = ">=3.9"
summary = "Data validation using Python type hints"
groups = ["default"]
dependencies = [
"email-validator>=2.0.0",
"pydantic==2.13.4",
]
files = [
{file = "pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba"},
{file = "pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6"},
]
[[package]] [[package]]
name = "pygments" name = "pygments"
version = "2.20.0" version = "2.20.0"

View File

@@ -1,6 +1,8 @@
from __future__ import annotations from __future__ import annotations
import copy
import dataclasses as dc import dataclasses as dc
import datetime
import enum import enum
import re import re
import sys import sys
@@ -8,9 +10,10 @@ import time
import uuid import uuid
from collections.abc import Sequence from collections.abc import Sequence
from pprint import pprint from pprint import pprint
from typing import Any, Protocol, TypeAlias, TypedDict from typing import Annotated, Any, Protocol, TypeAlias, TypedDict
import babel import babel
from pydantic import BaseModel, ConfigDict, EmailStr, Field, ValidationError, field_validator
from PySide6.QtCore import QDate, QModelIndex, QStringListModel, Qt, QTimer, Signal from PySide6.QtCore import QDate, QModelIndex, QStringListModel, Qt, QTimer, Signal
from PySide6.QtGui import QAction, QStandardItem, QStandardItemModel from PySide6.QtGui import QAction, QStandardItem, QStandardItemModel
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
@@ -83,7 +86,137 @@ def get_country_list_german() -> CountryList:
) )
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 iso_code, country_name in STATE_LIST:
states.append((country_name, iso_code))
short_code_to_name[iso_code] = country_name
return CountryList(
iso_to_country=short_code_to_name,
for_dropdown=tuple(states),
)
COUNTRY_LIST = get_country_list_german() COUNTRY_LIST = get_country_list_german()
GERMAN_STATE_LIST = get_country_list_german()
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 Grunderfassung_Unternehmen(BaseModel):
Projektrelevanz: Grunderfassung_Projektrelevanz
Kontaktperson: Grunderfassung_Kontaktperson
Stammdaten: Grunderfassung_Stammdaten
class Grunderfassung_Projektrelevanz(BaseModel):
model_config = ConfigDict(str_strip_whitespace=True)
Projektrelevanz_relevanz: bool
@field_validator("Projektrelevanz_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)
Stammdaten_titel: str | None
Stammdaten_anrede_anschrift: str
Stammdaten_name: str
Stammdaten_vorname: str | None
Stammdaten_geburtsdatum: datetime.date | None
Stammdaten_herkunftsland: str
Stammdaten_staatsangehoerigkeit: str | None
Stammdaten_rueckkehrer: bool | None
Stammdaten_aufenthaltsort: str | None
Stammdaten_strasse: str | None
Stammdaten_hausnummer: str | None
Stammdaten_PLZ: str | None
Stammdaten_ort: str | None
Stammdaten_bundesland: str | None
Stammdaten_land: str | None
Stammdaten_festnetznummer: str | None
Stammdaten_mobilfunknummer: str | None
Stammdaten_email: EmailStr | None
Stammdaten_familienstand: str | None
Stammdaten_anzahl_kinder: int | None
Stammdaten_alter_kinder: list[ValidAge] = Field(default_factory=list)
@field_validator("Stammdaten_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 CompanyForm_Search(QWidget): class CompanyForm_Search(QWidget):
@@ -302,6 +435,7 @@ class FormFieldType(enum.StrEnum):
DROPDOWN = enum.auto() DROPDOWN = enum.auto()
EXTENDED_DROPDOWN = enum.auto() EXTENDED_DROPDOWN = enum.auto()
DYNAMIC_LIST = enum.auto() DYNAMIC_LIST = enum.auto()
DYNAMIC_DROPDOWN = enum.auto()
@dc.dataclass(slots=True) @dc.dataclass(slots=True)
@@ -334,6 +468,7 @@ class FormField:
dropdown_options: Sequence[DropdownOption] = dc.field(default=tuple(), init=False) dropdown_options: Sequence[DropdownOption] = dc.field(default=tuple(), init=False)
key: str = "" key: str = ""
tooltip: str = "" tooltip: str = ""
init_label: str = dc.field(init=False)
def __post_init__( def __post_init__(
self, self,
@@ -343,6 +478,7 @@ class FormField:
self.key = str(uuid.uuid4()) self.key = str(uuid.uuid4())
self.label = self.label.strip() self.label = self.label.strip()
self.init_label = self.label.replace("*", "").replace(":", "")
if not self.label.endswith(":") and self.type is not FormFieldType.GROUP: if not self.label.endswith(":") and self.type is not FormFieldType.GROUP:
self.label += ":" self.label += ":"
if self.required: if self.required:
@@ -361,6 +497,32 @@ class FormField:
for child in self.children: for child in self.children:
child.parent = self 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
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): class WidgetRegistryEntry(TypedDict):
widget: QWidget widget: QWidget
@@ -475,7 +637,7 @@ FORM_FIELDS_CONTACT_PERSON = [
FormField( FormField(
"Name Unternehmen/Netzwerkpartner (pre-filled von Suche)", "Name Unternehmen/Netzwerkpartner (pre-filled von Suche)",
FormFieldType.TEXT, FormFieldType.TEXT,
key="t1", key="KP_name_partner",
required=False, required=False,
placeholder="Text wird nach gewähltem Unternehmen angezeigt", placeholder="Text wird nach gewähltem Unternehmen angezeigt",
readonly=True, readonly=True,
@@ -483,7 +645,7 @@ FORM_FIELDS_CONTACT_PERSON = [
FormField( FormField(
"Titel", "Titel",
FormFieldType.TEXT, FormFieldType.TEXT,
key="t2", key="KP_titel",
required=False, required=False,
tooltip=( tooltip=(
"* nur wenn anrufende Person oder kontaktaufnehmende Person " "* nur wenn anrufende Person oder kontaktaufnehmende Person "
@@ -493,49 +655,49 @@ FORM_FIELDS_CONTACT_PERSON = [
FormField( FormField(
"Anrede_Anschrift", "Anrede_Anschrift",
FormFieldType.TEXT, FormFieldType.TEXT,
key="t3", key="KP_anrede_anschrift",
required=True, required=True,
), ),
FormField( FormField(
"Name", "Name",
FormFieldType.TEXT, FormFieldType.TEXT,
key="t4", key="KP_name",
required=True, required=True,
), ),
FormField( FormField(
"Vorname", "Vorname",
FormFieldType.TEXT, FormFieldType.TEXT,
key="t5", key="KP_vorname",
required=False, required=False,
), ),
FormField( FormField(
"Festnetznummer", "Festnetznummer",
FormFieldType.TEXT, FormFieldType.TEXT,
key="t6", key="KP_festnetznummer",
required=False, required=False,
), ),
FormField( FormField(
"Mobilfunknummer", "Mobilfunknummer",
FormFieldType.TEXT, FormFieldType.TEXT,
key="t7", key="KP_mobilfunknummer",
required=False, required=False,
), ),
FormField( FormField(
"E-Mail", "E-Mail",
FormFieldType.TEXT, FormFieldType.TEXT,
key="t8", key="KP_email",
required=False, required=False,
), ),
FormField( FormField(
"Funktion/Beziehung zur beratenden Person", "Funktion/Beziehung zur beratenden Person",
FormFieldType.TEXT, FormFieldType.TEXT,
key="t9", key="KP_funktion_beziehung",
required=False, required=False,
), ),
FormField( FormField(
"Adresse", "Adresse",
FormFieldType.LONGTEXT, FormFieldType.LONGTEXT,
key="t10", key="KP_adresse",
required=False, required=False,
), ),
] ]
@@ -544,6 +706,7 @@ FORM_FIELDS_MASTER_DATA = [
FormField( FormField(
"Titel", "Titel",
FormFieldType.TEXT, FormFieldType.TEXT,
key="Stammdaten_titel",
required=False, required=False,
tooltip=( tooltip=(
"* nur wenn anrufende Person oder kontaktaufnehmende Person " "* nur wenn anrufende Person oder kontaktaufnehmende Person "
@@ -553,21 +716,25 @@ FORM_FIELDS_MASTER_DATA = [
FormField( FormField(
"Anrede", "Anrede",
FormFieldType.TEXT, FormFieldType.TEXT,
key="Stammdaten_anrede_anschrift",
required=True, required=True,
), ),
FormField( FormField(
"Name", "Name",
FormFieldType.TEXT, FormFieldType.TEXT,
key="Stammdaten_name",
required=True, required=True,
), ),
FormField( FormField(
"Vorname", "Vorname",
FormFieldType.TEXT, FormFieldType.TEXT,
key="Stammdaten_vorname",
required=False, required=False,
), ),
FormField( FormField(
"Geburtsdatum", "Geburtsdatum",
FormFieldType.DATE, FormFieldType.DATE,
key="Stammdaten_geburtsdatum",
required=False, required=False,
tooltip=( tooltip=(
"* Wichtig zu erfragen, da u.a. Mindestgehaltsschwelle davon abhängt " "* Wichtig zu erfragen, da u.a. Mindestgehaltsschwelle davon abhängt "
@@ -576,21 +743,26 @@ FORM_FIELDS_MASTER_DATA = [
), ),
FormField( FormField(
"Herkunftsland", "Herkunftsland",
FormFieldType.DROPDOWN, FormFieldType.EXTENDED_DROPDOWN,
key="Stammdaten_herkunftsland",
required=True, required=True,
options=[("LÄNDERLISTE NOCH ZU ERGÄNZEN", None)], placeholder="Suche...",
options=COUNTRY_LIST.for_dropdown,
tooltip=("* Wichtig zu erfragen aufgrund eventueller EU-Freizügigkeitsregelung"), tooltip=("* Wichtig zu erfragen aufgrund eventueller EU-Freizügigkeitsregelung"),
), ),
FormField( FormField(
"Staatsangehörigkeit", "Staatsangehörigkeit",
FormFieldType.DROPDOWN, FormFieldType.EXTENDED_DROPDOWN,
key="Stammdaten_herkunftsland",
required=False, required=False,
options=[("LÄNDERLISTE NOCH ZU ERGÄNZEN", None)], placeholder="Suche...",
options=COUNTRY_LIST.for_dropdown,
tooltip=("* Wichtig zu erfragen aufgrund eventueller EU-Freizügigkeitsregelung"), tooltip=("* Wichtig zu erfragen aufgrund eventueller EU-Freizügigkeitsregelung"),
), ),
FormField( FormField(
"Rückkehrer", "Rückkehrer",
FormFieldType.DROPDOWN, FormFieldType.DROPDOWN,
key="Stammdaten_rueckkehrer",
required=False, required=False,
options=[("ja", None), ("nein", None)], options=[("ja", None), ("nein", None)],
tooltip=("* Wichtig zu erfragen aufgrund eventueller EU-Freizügigkeitsregelung"), tooltip=("* Wichtig zu erfragen aufgrund eventueller EU-Freizügigkeitsregelung"),
@@ -598,34 +770,40 @@ FORM_FIELDS_MASTER_DATA = [
FormField( FormField(
"Wo befindet sich die Person?", "Wo befindet sich die Person?",
FormFieldType.DROPDOWN, FormFieldType.DROPDOWN,
key="Stammdaten_aufenthaltsort",
required=True, required=True,
options=[("Inland", None), ("Ausland EU/EWR", None), ("Ausland Drittstaat", None)], options=[("Inland", None), ("Ausland EU/EWR", None), ("Ausland Drittstaat", None)],
), ),
FormField( FormField(
"Straße", "Straße",
FormFieldType.TEXT, FormFieldType.TEXT,
key="Stammdaten_strasse",
required=False, required=False,
), ),
FormField( FormField(
"Hausnummer", "Hausnummer",
FormFieldType.TEXT, FormFieldType.TEXT,
key="Stammdaten_hausnummer",
required=False, required=False,
), ),
FormField( FormField(
"PLZ", "PLZ",
FormFieldType.TEXT, FormFieldType.TEXT,
key="Stammdaten_PLZ",
required=False, required=False,
), ),
FormField( FormField(
"Ort", "Ort",
FormFieldType.TEXT, FormFieldType.TEXT,
key="Stammdaten_ort",
required=False, required=False,
), ),
FormField( FormField(
"Bundesland", "Bundesland",
FormFieldType.DROPDOWN, FormFieldType.DROPDOWN,
key="Stammdaten_bundesland",
required=False, required=False,
options=[("BUNDESLÄNDER NOCH ZU ERGÄNZEN", None)], options=GERMAN_STATE_LIST.for_dropdown,
tooltip=( tooltip=(
"nur wenn Inland angegeben und die Angabe zieht es in keine Dokumente " "nur wenn Inland angegeben und die Angabe zieht es in keine Dokumente "
"rüber! Liste Bundesländer verwenden" "rüber! Liste Bundesländer verwenden"
@@ -634,35 +812,63 @@ FORM_FIELDS_MASTER_DATA = [
FormField( FormField(
"Land", "Land",
FormFieldType.TEXT, FormFieldType.TEXT,
key="Stammdaten_land",
required=False, required=False,
), ),
FormField( FormField(
"Festnetznummer", "Festnetznummer",
FormFieldType.TEXT, FormFieldType.TEXT,
key="Stammdaten_festnetznummer",
required=False, required=False,
), ),
FormField( FormField(
"Mobilfunknummer", "Mobilfunknummer",
FormFieldType.TEXT, FormFieldType.TEXT,
key="Stammdaten_mobilfunknummer",
required=False, required=False,
), ),
FormField( FormField(
"E-Mail", "E-Mail",
FormFieldType.TEXT, FormFieldType.TEXT,
key="Stammdaten_email",
required=False, required=False,
), ),
FormField( FormField(
"Familienstand", "Familienstand",
FormFieldType.TEXT, FormFieldType.TEXT,
key="Stammdaten_familienstand",
required=False, required=False,
tooltip="* Wichtig zu erfragen aufgrund Lebensunterhaltssicherung", tooltip="* Wichtig zu erfragen aufgrund Lebensunterhaltssicherung",
), ),
# FormField(
# "Anzahl Kinder",
# FormFieldType.DYNAMIC_DROPDOWN,
# key="Stammdaten_anzahl_kinder",
# required=False,
# options=[(str(x), None) for x in range(11)],
# tooltip="* Wichtig zu erfragen aufgrund Lebensunterhaltssicherung",
# ),
FormField(
"Anzahl Kinder",
FormFieldType.DYNAMIC_DROPDOWN,
required=False,
tooltip="* Wichtig zu erfragen aufgrund Lebensunterhaltssicherung",
key="Stammdaten_anzahl_kinder",
children=[
FormField( FormField(
"Anzahl Kinder", "Anzahl Kinder",
FormFieldType.DROPDOWN, FormFieldType.DROPDOWN,
required=False, required=False,
options=[(str(x), None) for x in range(11)], options=[(str(x), None) for x in range(11)],
tooltip="* Wichtig zu erfragen aufgrund Lebensunterhaltssicherung", tooltip="* Wichtig zu erfragen aufgrund Lebensunterhaltssicherung",
key="Stammdaten_anzahl_kinder",
children=[
FormField(
"Alter Kind", FormFieldType.TEXT, key="Stammdaten_alter_kinder"
),
],
),
],
), ),
] ]
@@ -736,25 +942,21 @@ FORM_FIELDS_ADDITIONAL_DATA = [
] ]
FORM_FIELDS_SCHOOL = [ FORM_FIELDS_SCHOOL = [
FormField("Abschluss", FormFieldType.TEXT, required=False, key="abschluss"),
FormField( FormField(
"Abschluss", "Abschlussgrad laut Dokument", FormFieldType.TEXT, required=False, key="abschlussgrad"
FormFieldType.TEXT,
required=True,
),
FormField(
"Abschlussgrad laut Dokument",
FormFieldType.TEXT,
required=False,
), ),
FormField( FormField(
"Schule", "Schule",
FormFieldType.TEXT, FormFieldType.TEXT,
required=False, required=False,
key="schule",
), ),
FormField( FormField(
"Ort", "Ort",
FormFieldType.TEXT, FormFieldType.TEXT,
required=False, required=False,
key="ort",
), ),
FormField( FormField(
"Land", "Land",
@@ -764,15 +966,12 @@ FORM_FIELDS_SCHOOL = [
placeholder="Suche...", placeholder="Suche...",
options=COUNTRY_LIST.for_dropdown, options=COUNTRY_LIST.for_dropdown,
), ),
FormField( FormField("Abschlussjahr", FormFieldType.TEXT, required=False, key="abschlussjahr"),
"Abschlussjahr",
FormFieldType.TEXT,
required=False,
),
FormField( FormField(
"Bemerkungsfeld", "Bemerkungsfeld",
FormFieldType.TEXT, FormFieldType.TEXT,
required=False, required=False,
key="bemerkung",
), ),
] ]
@@ -946,12 +1145,12 @@ FORM_FIELDS = [
FormField( FormField(
"Status && Projektrelevanz", "Status && Projektrelevanz",
FormFieldType.GROUP, FormFieldType.GROUP,
key="state_relevance", key="Projektrelevanz",
children=[ children=[
FormField( FormField(
"Projektrelevanz", "Projektrelevanz",
FormFieldType.DROPDOWN, FormFieldType.DROPDOWN,
key="projektrelevanz", key="Projektrelevanz_relevanz",
required=True, required=True,
options=[("ja", None), ("nein", None)], options=[("ja", None), ("nein", None)],
), ),
@@ -960,30 +1159,57 @@ FORM_FIELDS = [
FormField( FormField(
"Daten Kontaktperson", "Daten Kontaktperson",
FormFieldType.GROUP, FormFieldType.GROUP,
key="data_contact_person", key="Kontaktperson",
children=FORM_FIELDS_CONTACT_PERSON, children=FORM_FIELDS_CONTACT_PERSON,
), ),
FormField(
"Stammdaten",
FormFieldType.GROUP,
key="Stammdaten",
children=FORM_FIELDS_MASTER_DATA,
),
FormField( FormField(
"Schulbildung", "Schulbildung",
FormFieldType.DYNAMIC_LIST, FormFieldType.DYNAMIC_LIST,
children=FORM_FIELDS_SCHOOL, children=FORM_FIELDS_SCHOOL,
key="Schulbildung", key="Schulbildung",
), ),
FormField( # FormField(
"Test Länderauswahl", # "Test Länderauswahl",
FormFieldType.GROUP, # FormFieldType.GROUP,
key="countries", # key="countries",
children=[ # children=[
FormField( # FormField(
"Länderauswahl", # "Länderauswahl",
FormFieldType.EXTENDED_DROPDOWN, # FormFieldType.EXTENDED_DROPDOWN,
key="country", # key="country",
required=True, # required=True,
placeholder="Suche...", # placeholder="Suche...",
options=COUNTRY_LIST.for_dropdown, # options=COUNTRY_LIST.for_dropdown,
), # ),
], # ],
), # ),
# FormField(
# "Anzahl Kinder (dynamischer Dropdown)",
# FormFieldType.DYNAMIC_DROPDOWN,
# required=False,
# options=[(str(x), None) for x in range(11)],
# tooltip="* Wichtig zu erfragen aufgrund Lebensunterhaltssicherung",
# key="DynamicDropdown",
# children=[
# FormField(
# "Anzahl Kinder",
# FormFieldType.DROPDOWN,
# required=False,
# options=[(str(x), None) for x in range(11)],
# tooltip="* Wichtig zu erfragen aufgrund Lebensunterhaltssicherung",
# key="MainDropdown",
# children=[
# FormField("Alter Kind", FormFieldType.TEXT),
# ],
# ),
# ],
# ),
# FormFieldGroup("Daten Kontaktperson", FORM_FIELDS_CONTACT_PERSON), # FormFieldGroup("Daten Kontaktperson", FORM_FIELDS_CONTACT_PERSON),
# FormFieldGroup("Stammdaten (ERGÄNZUNG KINDERALTER DYNAMISCH)", FORM_FIELDS_MASTER_DATA), # FormFieldGroup("Stammdaten (ERGÄNZUNG KINDERALTER DYNAMISCH)", FORM_FIELDS_MASTER_DATA),
# FormFieldGroup("weitere Informationen", FORM_FIELDS_ADDITIONAL_DATA), # FormFieldGroup("weitere Informationen", FORM_FIELDS_ADDITIONAL_DATA),
@@ -1184,6 +1410,18 @@ def _build_ui_recursively(
} }
parent_layout.addRow(widget) parent_layout.addRow(widget)
case FormFieldType.DYNAMIC_DROPDOWN:
widget = DynamicDropdownWidget(
field.children,
field.label,
prefix=f"{full_key}",
)
widget_registry[full_key] = {
"widget": widget,
"form_field": field,
}
parent_layout.addRow(widget)
case _: case _:
raise NotImplementedError(f"Not supported field type: {field.type.value}") raise NotImplementedError(f"Not supported field type: {field.type.value}")
@@ -1240,6 +1478,9 @@ def reset_form(
elif isinstance(widget, DynamicListWidget): elif isinstance(widget, DynamicListWidget):
# dynamic list widget manages its widgets by itself # dynamic list widget manages its widgets by itself
widget.reset_form() widget.reset_form()
elif isinstance(widget, DynamicDropdownWidget):
# dynamic list widget manages its widgets by itself
widget.reset_form()
widget.setStyleSheet("") widget.setStyleSheet("")
@@ -1255,6 +1496,31 @@ def _insert_nested(
target_dict[key_path[-1]] = value target_dict[key_path[-1]] = value
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( def get_form_data(
widget_registry: WidgetRegistry, widget_registry: WidgetRegistry,
) -> dict[str, Any]: ) -> dict[str, Any]:
@@ -1277,6 +1543,15 @@ def get_form_data(
# of such dictionaries # of such dictionaries
form_data = widget.get_form_data() form_data = widget.get_form_data()
value = [val for val in form_data.values()] value = [val for val in form_data.values()]
# print(">>>>>>>>> Form Data:")
# pprint(form_data)
elif isinstance(widget, DynamicDropdownWidget):
# this should be a list: each dynamic list contains a list
# of such dictionaries
form_data = widget.get_form_data()
value = [val for val in form_data.values()]
# print(">>>>>>>>> Form Data:")
# pprint(form_data)
_insert_nested(raw_data, key.split("."), value) _insert_nested(raw_data, key.split("."), value)
@@ -1459,6 +1734,16 @@ class AutoForm(QWidget):
print("Erfolg! Alle Daten sind valide.") print("Erfolg! Alle Daten sind valide.")
print("Get form data call...") print("Get form data call...")
form_data = self.get_form_data() form_data = self.get_form_data()
post_proc_1 = form_data["Stammdaten"]["Stammdaten_anzahl_kinder"]
Stammdaten_anzahl_kinder = post_proc_1[0]["Stammdaten_anzahl_kinder-[0]"][
"'Stammdaten_anzahl_kinder'"
]
Stammdaten_alter_kinder: list[str] = []
if len(post_proc_1) > 1:
for i in range(1, len(post_proc_1)):
content = post_proc_1[i]
print("------------>>>>>>>>> Get form data") print("------------>>>>>>>>> Get form data")
pprint(form_data) pprint(form_data)
# ------------------------------------------------------------ # ------------------------------------------------------------
@@ -1478,7 +1763,7 @@ class AutoForm(QWidget):
@dc.dataclass(slots=True) @dc.dataclass(slots=True)
class SubForm: class SubForm:
entry_box: QGroupBox entry_box: QWidget
prefix_parent: str prefix_parent: str
index: int index: int
prefix: str = "" prefix: str = ""
@@ -1529,6 +1814,7 @@ class DynamicListWidget(QWidget):
super().__init__() super().__init__()
self.form_fields = form_fields self.form_fields = form_fields
self.label = label self.label = label
self.base_label = enhanced_label(label, add_text="")
self.prefix = prefix self.prefix = prefix
self.widget_registry: WidgetRegistry = {} self.widget_registry: WidgetRegistry = {}
@@ -1551,6 +1837,8 @@ class DynamicListWidget(QWidget):
self.inner_layout.addWidget(self.add_btn) self.inner_layout.addWidget(self.add_btn)
self.widget_registry_base_size = len(self.widget_registry)
# add empty sub form as initial value # add empty sub form as initial value
self.add_entry() self.add_entry()
@@ -1590,14 +1878,14 @@ class DynamicListWidget(QWidget):
self.update_sub_forms() self.update_sub_forms()
def update_sub_forms(self): def update_sub_forms(self):
for index, sub_form in enumerate(self.sub_forms, start=1): update_sub_forms(
sub_form.entry_box.setTitle(f"{self.label} {index}")
change_sub_form_widget_registry(
self.widget_registry, self.widget_registry,
sub_form, sub_forms=self.sub_forms,
index, base_label=self.base_label,
) )
# pprint_registry(self.widget_registry)
def reset_form(self) -> None: def reset_form(self) -> None:
reset_form(self.widget_registry) reset_form(self.widget_registry)
@@ -1614,6 +1902,146 @@ class DynamicListWidget(QWidget):
return raw_data return raw_data
class DynamicDropdownWidget(QWidget):
"""
A Widget, which can generate and manage an arbitrary number of sub forms with additional
information on a combobox selection (integer in 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.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.group_box = QGroupBox(label)
self.main_layout = QVBoxLayout(self)
self.main_layout.setContentsMargins(0, 0, 0, 0)
self.form_layout = QFormLayout()
self.main_layout.addLayout(self.form_layout)
_build_ui_recursively(
[self.combobox_field],
self.form_layout,
self.widget_registry,
prefix=f"{self.prefix}-[0]",
)
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.on_anzahl_changed)
self.widget_registry_base_size = len(self.widget_registry)
def on_anzahl_changed(
self,
text: str,
) -> None:
target_count: int
if text == DROPDOWN_DEFAULT:
target_count = 0
else:
target_count = int(text)
current_count = len(self.sub_forms)
if target_count > current_count:
differenz = target_count - current_count
for _ in range(differenz):
self._add_row()
elif target_count < current_count:
differenz = current_count - target_count
for _ in range(differenz):
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}")
_build_ui_recursively(
schema=[form_field_def],
parent_layout=form_layout,
widget_registry=self.widget_registry,
prefix=f"{self.prefix}-[{number_form}]",
)
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,
)
# pprint_registry(self.widget_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]:
return validate_form_data(self.widget_registry)
def load_form_data(self) -> None:
# TODO add way to load data when initialised (probably with click)
...
def get_form_data(self):
raw_data = get_form_data(self.widget_registry)
return raw_data
class ClickableCell(QFrame): class ClickableCell(QFrame):
"""cell in the table on the startup screen""" """cell in the table on the startup screen"""

View File

@@ -3,10 +3,74 @@ import dataclasses as dc
import enum import enum
import re import re
from collections.abc import Sequence from collections.abc import Sequence
from typing import Any
import babel import babel
from PySide6.QtCore import QDate, Qt from PySide6.QtCore import QDate, Qt
# %%
DYNAMIC_LIST_KEY_PATTERN = re.compile(r"-\[(\d+)\]")
dynamic_content = {
"Stammdaten_anzahl_kinder-[0]": {"Stammdaten_anzahl_kinder": "5"},
"Stammdaten_anzahl_kinder-[1]": {"Stammdaten_alter_kinder": "23213"},
"Stammdaten_anzahl_kinder-[2]": {"Stammdaten_alter_kinder": "123123"},
"Stammdaten_anzahl_kinder-[3]": {"Stammdaten_alter_kinder": "123213"},
"Stammdaten_anzahl_kinder-[4]": {"Stammdaten_alter_kinder": "123123"},
"Stammdaten_anzahl_kinder-[5]": {"Stammdaten_alter_kinder": "123123"},
}
def find_dynamic_content(content: dict[str, Any]) -> dict[str, Any] | None:
found = None
for key in dynamic_content.keys():
if DYNAMIC_LIST_KEY_PATTERN.search(key):
# found an match: this is dynamic content dictionary
print("found")
found = dynamic_content
break
return found
# %%
new_content = {
"Stammdaten": {
"Stammdaten_PLZ": "",
"Stammdaten_anrede_anschrift": "asdasdas",
"Stammdaten_anzahl_kinder": [
{
"Stammdaten_anzahl_kinder-[0]": {"Stammdaten_anzahl_kinder": "5"},
"Stammdaten_anzahl_kinder-[1]": {"Stammdaten_alter_kinder": "23213"},
"Stammdaten_anzahl_kinder-[2]": {"Stammdaten_alter_kinder": "123123"},
"Stammdaten_anzahl_kinder-[3]": {"Stammdaten_alter_kinder": "123213"},
"Stammdaten_anzahl_kinder-[4]": {"Stammdaten_alter_kinder": "123123"},
"Stammdaten_anzahl_kinder-[5]": {"Stammdaten_alter_kinder": "123123"},
}
],
}
}
def flat_dict(contents):
for x in contents:
if isinstance(contents, dict):
yield from flat_dict(tuple(contents[x]))
elif isinstance(x, (list, tuple, set)):
yield from flat_dict(x)
else:
yield x
# %%
for x in flat_dict(new_content):
print(x)
# %%
find_dynamic_content(dynamic_content)
# %% # %%
@dc.dataclass(slots=True) @dc.dataclass(slots=True)

View File

@@ -5,7 +5,7 @@ description = "GUI for CRM of NAFKA project with WCE"
authors = [ authors = [
{name = "d-opt GmbH, resp. Florian Förster", email = "f.foerster@d-opt.com"}, {name = "d-opt GmbH, resp. Florian Förster", email = "f.foerster@d-opt.com"},
] ]
dependencies = ["nicegui>=3.10.0", "pyside6>=6.11.0", "sqlalchemy>=2.0.49", "polars>=1.40.1", "dopt-basics>=0.2.4", "pydantic>=2.13.4", "babel>=2.18.0"] dependencies = ["nicegui>=3.10.0", "pyside6>=6.11.0", "sqlalchemy>=2.0.49", "polars>=1.40.1", "dopt-basics>=0.2.4", "pydantic[email]>=2.13.4", "babel>=2.18.0"]
requires-python = "<3.14,>=3.11" requires-python = "<3.14,>=3.11"
readme = "README.md" readme = "README.md"
license = {text = "LicenseRef-Proprietary"} license = {text = "LicenseRef-Proprietary"}