diff --git a/prototypes/t_qt_2.py b/prototypes/t_qt_2.py index 4ffaf86..e6ef3df 100644 --- a/prototypes/t_qt_2.py +++ b/prototypes/t_qt_2.py @@ -4,21 +4,31 @@ import copy import dataclasses as dc import datetime import enum +import json import logging import pickle import re import sys import time import uuid -from collections.abc import Container, Sequence +from collections import defaultdict +from collections.abc import Container, Iterable, Sequence from pathlib import Path from pprint import pformat, pprint -from typing import Annotated, Any, Final, Protocol, TypeAlias, TypedDict, cast +from typing import Annotated, Any, Final, Protocol, TypeAlias, TypedDict, TypeVar, cast from typing_extensions import override import babel from dopt_basics.logging import BASE_LOGGER, setup_logging -from pydantic import BaseModel, ConfigDict, EmailStr, Field, ValidationError, field_validator +from pydantic import ( + BaseModel, + ConfigDict, + EmailStr, + Field, + ValidationError, + field_validator, + model_validator, +) from PySide6.QtCore import ( QDate, QEvent, @@ -53,17 +63,20 @@ from PySide6.QtWidgets import ( from wce_crm.backend import initial_recording as be_init_rec +K = TypeVar("K") +V = TypeVar("V") + setup_logging(enable_stderr=True) DEBUG: Final[bool] = True -DEBUG_SEARCH_WIDGET: Final[bool] = True +DEBUG_SEARCH_WIDGET: Final[bool] = False logger = BASE_LOGGER.getChild("wce") logger.setLevel(logging.DEBUG) logger_search_widget = logger.getChild("search_widget") logger_search_widget.setLevel(logging.DEBUG) logger_get_data = logger.getChild("get_data") -logger_get_data.setLevel(logging.INFO) +logger_get_data.setLevel(logging.DEBUG) QSS = """ *[styleClass="stempel"] { @@ -81,10 +94,12 @@ QSS = """ 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( - model: BaseModel, +def save_pydantic_model_dict_db( + model: FlatBaseModel, path: Path | None = None, ) -> None: if path is None: @@ -92,11 +107,13 @@ def save_pydantic_model_dict( 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(model.model_dump(), f) + pickle.dump(export, f) -def load_pydantic_model_dict( +def load_pydantic_model_dict_db( path: Path | None = None, ) -> dict[str, Any]: if path is None: @@ -104,12 +121,144 @@ def load_pydantic_model_dict( 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): + """ + Optimierte Pydantic-Basisklasse, die JSON-Strings und Doppel-Unterstriche + vollständig rekursiv (tiefenunabhängig) auflöst. + """ + + @classmethod + def _recursive_parse_json(cls, data: Any) -> Any: + """Sucht im gesamten Objekt nach JSON-Listen-Strings und entpackt sie.""" + 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: + """Baut im gesamten Objekt flache '__'-Schlüssel in geschachtelte Dicts um.""" + if isinstance(data, dict): + # 1. Die aktuelle Ebene dieses Dictionaries ent-flachen + unflattened_level = {} + for key, value in data.items(): + if "__" in key: + parts = key.split("__") + 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 + + # 2. Jetzt tiefer in die Werte gehen (rekursiv für Unter-Dicts) + return {k: cls._recursive_unflatten(v) for k, v in unflattened_level.items()} + + elif isinstance(data, list): + # Auch durch Listen (z.B. deine Schulbildung) wandern und Sub-Dicts ent-flachen + return [cls._recursive_unflatten(item) for item in data] + + return data + + @model_validator(mode="before") + @classmethod + def __unflatten_input(cls, data: Any) -> Any: + """Eingangskontrolle: Bereitet flache DB/GUI-Daten sauber für Pydantic vor.""" + if not isinstance(data, dict): + return data + + # Schritt 1: Alle JSON-Strings (egal wie tief) in echte Listen umwandeln + # Das verwandelt '[1, 2, 3]' zuverlässig in [1, 2, 3] + json_parsed_data = cls._recursive_parse_json(data) + + # Schritt 2: Alle '__' Schlüssel (egal wie tief) in Unter-Strukturen falten + final_nested_data = cls._recursive_unflatten(json_parsed_data) + + return final_nested_data + + def to_db(self) -> dict[str, Any]: + """Ausgang für die DB: Flach, Listen sind JSON-Strings.""" + nested = super().model_dump(mode="json") + return self.__flatten_dict(nested, serialize_lists=True) + + def to_gui(self) -> dict[str, Any]: + """Ausgang für die GUI: Flach, aber Listen bleiben Python-Listen.""" + nested = super().model_dump() + return self.__flatten_dict(nested, serialize_lists=False) + + @classmethod + def __flatten_dict( + cls, nested_dict: dict, parent_key: str = "", serialize_lists: bool = True + ) -> dict: + """Rekursiver Helfer zum Abflachen von Strukturen.""" + items = [] + for k, v in nested_dict.items(): + new_key = f"{parent_key}__{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))) + else: + items.append((new_key, processed_list)) + else: + items.append((new_key, v)) + return dict(items) + + @dc.dataclass(slots=True) class CountryList: iso_to_country: dict[str, str] @@ -221,6 +370,8 @@ class FormFieldType(enum.StrEnum): DYNAMIC_DROPDOWN = enum.auto() TEXT_SEARCH = enum.auto() CUSTOM = enum.auto() + TEXT_DATE = enum.auto() + TEXT_DATETIME = enum.auto() @dc.dataclass(slots=True) @@ -343,8 +494,9 @@ def _build_ui_recursively( parent_layout: QFormLayout, widget_registry: WidgetRegistry, prefix: str = "", -) -> None: +) -> list[str]: no_scroll_filter = NoScrollFilter(parent_layout) + keys: list[str] = [] for field in schema: full_key = f"{prefix}{COLUMN_SEP}{field.key}" if prefix else field.key widget: QWidget @@ -361,7 +513,7 @@ def _build_ui_recursively( ) parent_layout.addRow(group_box) - case FormFieldType.TEXT: + case FormFieldType.TEXT | FormFieldType.TEXT_DATE | FormFieldType.TEXT_DATETIME: widget = QLineEdit() if field.placeholder: widget.setPlaceholderText(field.placeholder) @@ -576,6 +728,10 @@ def _build_ui_recursively( case _: raise NotImplementedError(f"Not supported field type: {field.type.value}") + keys.append(full_key) + + return keys + def _add_tooltip( widget: QWidget, @@ -676,6 +832,7 @@ class SubForm: 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}]" @@ -748,10 +905,15 @@ def get_form_data( continue widget = registry_entry["widget"] + field_type = registry_entry["form_field"].type if isinstance(widget, QLineEdit): data = widget.text() if data != "": value = data + if field_type is FormFieldType.TEXT_DATE: + value = datetime.datetime.strptime(value, DATE_FMT).date() + elif field_type is FormFieldType.TEXT_DATETIME: + value = datetime.datetime.strptime(value, DATETIME_FMT) elif isinstance(widget, QPlainTextEdit): data = widget.toPlainText() if data != "": @@ -765,7 +927,7 @@ def get_form_data( ... # this should be a list: each dynamic list contains a list # of such dictionaries - _ = widget.get_form_data() + value = widget.get_form_data() # value = [val for val in form_data.values()] # print(">>>>>>>>> Key: ", key) # print(">>>>>>>>> Form Data after call:") @@ -786,7 +948,7 @@ def get_form_data( else: raw_data[key] = value - logger_get_data.info(">>>>> RAW DATA:\n%s", pformat(raw_data)) + logger_get_data.debug("[base:get_form_data]>>>>> RAW DATA:\n%s", pformat(raw_data)) return raw_data @@ -803,6 +965,11 @@ def set_widget_value( value = "nein" if isinstance(widget, QLineEdit): + if isinstance(value, datetime.datetime): + value = value.strftime(DATETIME_FMT) + elif isinstance(value, datetime.date): + value = value.strftime(DATE_FMT) + widget.setText(str(value)) elif isinstance(widget, QPlainTextEdit): @@ -832,12 +999,23 @@ def set_widget_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"] - key_path = key.split(COLUMN_SEP) - value = _get_nested(data, key_path) + if filter_keys: + data_key = key.split(COLUMN_SEP)[-1] + if data_key not in filter_keys: + continue + + # key_path = key.split(COLUMN_SEP) + # value = _get_nested(data, key_path) + # value = data.get(key, None) + if isinstance(widget, (DynamicDropdownWidget, Grunderfassung_SuchWidget)): + value = data + else: + value = data[key] set_widget_value(widget, value) @@ -909,12 +1087,12 @@ def validate_form_data( return errors -class Grunderfassung_Unternehmen(BaseModel): +class Grunderfassung_Unternehmen(FlatBaseModel): Metadaten_erstellung: datetime.datetime | None = ( None # default in SQLAlchemy with lambda and timezone-aware datetime) ) Metadaten_aktualisierung: datetime.datetime | None # see above - Metadaten_nutzer: str | None = None + Metadaten_nutzer: str | None Grunderfassung_fallnummer: str Grunderfassung_notiz: str | None @@ -932,17 +1110,17 @@ class Grunderfassung_Unternehmen(BaseModel): class Grunderfassung_PartnerSuche(BaseModel): model_config = ConfigDict(str_strip_whitespace=True) - PartnerSuche_un_suche: int - PartnerSuche_person_suche: int - PartnerSuche_kanal_aufmerksamkeit: str | None + un_suche: int + person_suche: int + kanal_aufmerksamkeit: str | None class Grunderfassung_Projektrelevanz(BaseModel): model_config = ConfigDict(str_strip_whitespace=True) - Projektrelevanz_relevanz: bool + relevanz: bool - @field_validator("Projektrelevanz_relevanz", mode="before") + @field_validator("relevanz", mode="before") @classmethod def str_to_bool(cls, value: Any) -> Any: if isinstance(value, str): @@ -979,28 +1157,28 @@ 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: Grunderfassung_Stammdaten_AnzahlKinder + 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("Stammdaten_rueckkehrer", mode="before") + @field_validator("rueckkehrer", mode="before") @classmethod def str_to_bool(cls, value: Any) -> Any: if isinstance(value, str): @@ -1082,22 +1260,6 @@ class Grunderfassung_Sprachen(BaseModel): SP_datum_nachweis: datetime.date | None -def postprocess_pydantic( - model: Grunderfassung_Unternehmen, -) -> dict[str, Any]: - logger.debug("Call to postprocessing of Pydantic model") - - CURRENT_USER = "TEST_USER" - model.Metadaten_nutzer = CURRENT_USER - flat_data: dict[str, Any] = {} - for data_dict in get_leaf_dicts(model.model_dump()): - flat_data.update(data_dict) - - logger.debug("Flatted data dict:\n%s", pformat(flat_data)) - - return flat_data - - class Grunderfassung_SuchWidget(CustomWidget): def __init__( self, @@ -1130,19 +1292,19 @@ class Grunderfassung_SuchWidget(CustomWidget): self.prefix, ) - self.COMPANY_SEARCH_INPUT_KEY = "PartnerSuche_un_suche" + 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 = "PartnerSuche_person_suche" + 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 = "PartnerSuche_kanal_aufmerksamkeit" + 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"]) @@ -1330,13 +1492,14 @@ class Grunderfassung_SuchWidget(CustomWidget): raise RuntimeError("No data. Get form data first.") data = self.export_data - formatted_data = {} - _insert_nested(formatted_data, self.prefix.split(COLUMN_SEP), data) + # formatted_data = {} + # _insert_nested(formatted_data, self.prefix.split(COLUMN_SEP), data) - if DEBUG_SEARCH_WIDGET: - logger_search_widget.debug("\nformatted data:\n%s", pformat(formatted_data)) - - set_form_data(self.widget_registry, formatted_data) + # if DEBUG_SEARCH_WIDGET: + # logger_search_widget.debug("\nformatted data:\n%s", pformat(formatted_data)) + # logger_get_data.debug(">>>>>>>>> Call set_form_data for CustomSearchWidget") + # pprint_registry(self.widget_registry) + set_form_data(self.widget_registry, data, filter_keys=self.DATA_EXPORT_FIELDS) class ContactPersonForm_Search(QWidget): @@ -1604,10 +1767,18 @@ class AutoForm(QWidget): def on_load_clicked(self) -> None: # TODO change logic to database backend - print(">>>> LOAD CLICKED") - loaded_data = load_pydantic_model_dict() - - self.set_form_data(loaded_data) + logger_get_data.info(">>>> LOAD CLICKED") + loaded_data = load_pydantic_model_dict_db() + logger_get_data.debug("Loaded data dict. Passing to Pydantic...") + model = Grunderfassung_Unternehmen(**loaded_data) + logger_get_data.debug("Loaded to Pydantic.") + logger_get_data.debug("Convert to GUI structure...") + form_data = model.to_gui() + logger_get_data.debug("Set form data...") + logger_get_data.debug("Form data:\n%s", pformat(form_data)) + # logger_get_data.debug("Widget registry:\n") + # pprint_registry(self.widget_registry) + self.set_form_data(form_data) def on_save_clicked(self) -> None: self._disable_save() @@ -1625,16 +1796,19 @@ class AutoForm(QWidget): # no errors: data can be saved # TODO: change routine to trigger data saving in the backend - print("Erfolg! Alle Daten sind valide.") - print("Get form data call...") + logger.info("Erfolg! Alle Daten sind valide.") + logger.info("Get form data call...") form_data = self.get_form_data() - logger.debug("------------>>>>>>>>> Get form data\n%s", pformat(form_data)) - logger.debug("\n------------>>>>>>>>> Call Pydantic") + + logger.debug("\n\n------------>>>>>>>>> Get form data\n%s", pformat(form_data)) + logger.debug("------------>>>>>>>>> Call Pydantic") try: validated_data = Grunderfassung_Unternehmen(**form_data) + validated_data.Metadaten_erstellung = datetime.datetime.now() logger.debug("%s", pformat(validated_data.model_dump())) - postprocess_pydantic(validated_data) + # TODO check removal + # postprocess_pydantic(validated_data) except ValidationError as e: # catch errors and show them in GUI fehler_texte = [] @@ -1660,7 +1834,13 @@ class AutoForm(QWidget): else: # !! this code is only called if the 'try' block was successful self.reset_form() - save_pydantic_model_dict(validated_data) + save_pydantic_model_dict_db(validated_data) + # TODO save data to database + logger.info( + "\n\n>>>>>>>>>>>>> The following data muste be saved in the database:\n%s", + pformat(validated_data.to_db()), + ) + logger_get_data.info("Data saved successfully") finally: # always re-enable save, even if error occurred self._enable_save() @@ -1673,10 +1853,11 @@ class AutoForm(QWidget): reset_form(self.widget_registry) def get_form_data(self) -> dict[str, Any]: - logger_get_data.debug("[AutoForm] Call get form data") - raw_data = get_form_data(self.widget_registry) + form_data = get_form_data(self.widget_registry) + logger_get_data.debug("\n\n>>>>>>> [AutoForm] Call get form data:") + logger_get_data.debug("Form Data:\n%s", pformat(form_data)) - return raw_data + return form_data def set_form_data( self, @@ -1770,6 +1951,7 @@ class DynamicListWidget(QWidget): self.update_sub_forms() def update_sub_forms(self): + # TODO: check if needed update_sub_forms( self.widget_registry, sub_forms=self.sub_forms, @@ -1786,7 +1968,11 @@ class DynamicListWidget(QWidget): self.add_entry() def validate_form_data(self) -> list[str]: - return validate_form_data(self.widget_registry) + 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): # raw_data = get_form_data(self.widget_registry) @@ -1800,10 +1986,10 @@ class DynamicListWidget(QWidget): # pprint_registry(sub_form.registry) form_data = [get_form_data(sub.registry) for sub in self.sub_forms] - print("##################") - pprint(form_data) + logger_get_data.debug("##################") + logger_get_data.debug("Form data:\n%s", pformat(form_data)) - # return form_data + return form_data def set_form_data( self, @@ -1818,6 +2004,7 @@ class DynamicListWidget(QWidget): self.add_entry() return + # TODO check removal # for sub_data in data: # self.add_entry() # current_sub_form = self.sub_forms[-1] @@ -1878,11 +2065,12 @@ class DynamicDropdownWidget(QWidget): self.form_layout = QFormLayout() self.main_layout.addLayout(self.form_layout) - _build_ui_recursively( + self.full_keys = _build_ui_recursively( [self.combobox_field], self.form_layout, self.widget_registry, - prefix=f"{self.prefix}-[0]", + prefix=f"{self.prefix}", + # prefix=f"{self.prefix}-[0]", ) dropdown_widget_entry = tuple(self.widget_registry.values())[0] dropdown_widget = dropdown_widget_entry["widget"] @@ -1931,11 +2119,13 @@ class DynamicDropdownWidget(QWidget): form_field_def = copy.copy(self.assigned_form_field) form_field_def.label = form_field_def.enhanced_label(f"{number_form}") - _build_ui_recursively( + sub_form.full_keys = _build_ui_recursively( schema=[form_field_def], parent_layout=form_layout, - widget_registry=self.widget_registry, - prefix=f"{self.prefix}-[{number_form}]", + widget_registry=sub_form.registry, + prefix=f"{self.prefix}", + # widget_registry=self.widget_registry, + # prefix=f"{self.prefix}-[{number_form}]", ) self.rows_layout.addWidget(container) @@ -1956,18 +2146,28 @@ class DynamicDropdownWidget(QWidget): 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) + 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): # raw_data = get_form_data(self.widget_registry) 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("##################") + # logger_get_data.debug("\nSub data:\n%s\n\n", pformat(sub_data)) + # logger_get_data.debug("Form data:\n%s", pformat(form_data)) logger_get_data.debug( "\n\n\n------- Form data DynamicDropdownWidget:\n%s", pformat(form_data) ) @@ -2002,36 +2202,44 @@ class DynamicDropdownWidget(QWidget): def set_form_data( self, - data: dict[str, Any], + sub_form_data: dict[str, Any], ) -> None: # delete all rows while self.sub_forms: self._remove_row() # fill in value of combobox field - entries = tuple(self.widget_registry.values()) - assert len(entries) == 1 - widget = entries[0]["widget"] - assert isinstance(widget, QComboBox) - value = data[self.combobox_field.key] - set_widget_value(widget, value) - - data_list: list[int | None] | None = data[self.assigned_form_field.key] - if not data_list: - return - # now there are as many new sub forms as the saved value for the dropdown - # assert this - assert len(data_list) == len(self.sub_forms) - - sub_form_index: int = 0 - for key, entry in self.widget_registry.items(): - if "-[0]" in key: - # this the first widget assigned which is the dropdown field - continue - widget = entry["widget"] - value = data_list[sub_form_index] + 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) - sub_form_index += 1 + 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 NoScrollFilter(QObject): @@ -2207,14 +2415,14 @@ FORM_FIELDS_SEARCH_HEAD = [ "Suche", FormFieldType.EXTENDED_DROPDOWN, required=True, - key="PartnerSuche_un_suche", + key="un_suche", placeholder="Suche...", ), FormField( "Name Unternehmen/Netzwerkpartner", FormFieldType.TEXT, required=False, - key="PartnerSuche_un_name", + key="un_name", readonly=True, info="ma_unternehmensname", ), @@ -2222,7 +2430,7 @@ FORM_FIELDS_SEARCH_HEAD = [ "Straße", FormFieldType.TEXT, required=False, - key="PartnerSuche_un_straße", + key="un_straße", readonly=True, info="ma_strasse", ), @@ -2230,7 +2438,7 @@ FORM_FIELDS_SEARCH_HEAD = [ "Hausnummer", FormFieldType.TEXT, required=False, - key="PartnerSuche_un_hausnummer", + key="un_hausnummer", readonly=True, info="ma_hausnummer", ), @@ -2238,7 +2446,7 @@ FORM_FIELDS_SEARCH_HEAD = [ "PLZ", FormFieldType.TEXT, required=False, - key="PartnerSuche_un_PLZ", + key="un_PLZ", readonly=True, info="ma_plz", ), @@ -2246,7 +2454,7 @@ FORM_FIELDS_SEARCH_HEAD = [ "Ort", FormFieldType.TEXT, required=False, - key="PartnerSuche_un_ort", + key="un_ort", readonly=True, info="ma_ort", ), @@ -2254,14 +2462,14 @@ FORM_FIELDS_SEARCH_HEAD = [ "Suche Ansprechpartner", FormFieldType.EXTENDED_DROPDOWN, required=True, - key="PartnerSuche_person_suche", + key="person_suche", placeholder="Suche...", ), FormField( "Titel", FormFieldType.TEXT, required=False, - key="PartnerSuche_person_titel", + key="person_titel", readonly=True, info="an_titel", ), @@ -2269,7 +2477,7 @@ FORM_FIELDS_SEARCH_HEAD = [ "Anrede", FormFieldType.TEXT, required=False, - key="PartnerSuche_person_anrede", + key="person_anrede", readonly=True, info="an_anrede", ), @@ -2277,7 +2485,7 @@ FORM_FIELDS_SEARCH_HEAD = [ "Name", FormFieldType.TEXT, required=False, - key="PartnerSuche_person_name", + key="person_name", readonly=True, info="an_nachname", ), @@ -2285,7 +2493,7 @@ FORM_FIELDS_SEARCH_HEAD = [ "Vorname", FormFieldType.TEXT, required=False, - key="PartnerSuche_person_vorname", + key="person_vorname", readonly=True, info="an_vorname", ), @@ -2293,7 +2501,7 @@ FORM_FIELDS_SEARCH_HEAD = [ "Telefon", FormFieldType.TEXT, required=False, - key="PartnerSuche_person_telefon", + key="person_telefon", readonly=True, info="an_festnetz", ), @@ -2301,7 +2509,7 @@ FORM_FIELDS_SEARCH_HEAD = [ "Mobil", FormFieldType.TEXT, required=False, - key="PartnerSuche_person_mobilfunk", + key="person_mobilfunk", readonly=True, info="an_mobil", ), @@ -2309,7 +2517,7 @@ FORM_FIELDS_SEARCH_HEAD = [ "E-Mail", FormFieldType.TEXT, required=False, - key="PartnerSuche_person_email", + key="person_email", readonly=True, info="an_mail", ), @@ -2317,7 +2525,7 @@ FORM_FIELDS_SEARCH_HEAD = [ "Funktion im Unternehmen", FormFieldType.TEXT, required=False, - key="PartnerSuche_person_funktion", + key="person_funktion", readonly=True, info="an_position", ), @@ -2325,7 +2533,7 @@ FORM_FIELDS_SEARCH_HEAD = [ "Wie sind Sie auf uns aufmerksam geworden?", FormFieldType.DROPDOWN, required=False, - key="PartnerSuche_kanal_aufmerksamkeit", + key="kanal_aufmerksamkeit", options=[ ("Agentur für Arbeit", None), ("Ausländerbehörde", None), @@ -2418,7 +2626,7 @@ FORM_FIELDS_MASTER_DATA = [ FormField( "Titel", FormFieldType.TEXT, - key="Stammdaten_titel", + key="titel", required=False, tooltip=( "* nur wenn anrufende Person oder kontaktaufnehmende Person " @@ -2428,25 +2636,25 @@ FORM_FIELDS_MASTER_DATA = [ FormField( "Anrede", FormFieldType.TEXT, - key="Stammdaten_anrede_anschrift", + key="anrede_anschrift", required=True, ), FormField( "Name", FormFieldType.TEXT, - key="Stammdaten_name", + key="name", required=True, ), FormField( "Vorname", FormFieldType.TEXT, - key="Stammdaten_vorname", + key="vorname", required=False, ), FormField( "Geburtsdatum", FormFieldType.DATE, - key="Stammdaten_geburtsdatum", + key="geburtsdatum", required=False, tooltip=( "* Wichtig zu erfragen, da u.a. Mindestgehaltsschwelle davon abhängt " @@ -2456,7 +2664,7 @@ FORM_FIELDS_MASTER_DATA = [ FormField( "Herkunftsland", FormFieldType.EXTENDED_DROPDOWN, - key="Stammdaten_herkunftsland", + key="herkunftsland", required=True, placeholder="Suche...", options=COUNTRY_LIST.for_dropdown, @@ -2465,7 +2673,7 @@ FORM_FIELDS_MASTER_DATA = [ FormField( "Staatsangehörigkeit", FormFieldType.EXTENDED_DROPDOWN, - key="Stammdaten_staatsangehoerigkeit", + key="staatsangehoerigkeit", required=False, placeholder="Suche...", options=COUNTRY_LIST.for_dropdown, @@ -2474,7 +2682,7 @@ FORM_FIELDS_MASTER_DATA = [ FormField( "Rückkehrer", FormFieldType.DROPDOWN, - key="Stammdaten_rueckkehrer", + key="rueckkehrer", required=False, options=[("ja", None), ("nein", None)], tooltip=("* Wichtig zu erfragen aufgrund eventueller EU-Freizügigkeitsregelung"), @@ -2482,38 +2690,38 @@ FORM_FIELDS_MASTER_DATA = [ FormField( "Wo befindet sich die Person?", FormFieldType.DROPDOWN, - key="Stammdaten_aufenthaltsort", + key="aufenthaltsort", required=True, options=[("Inland", None), ("Ausland EU/EWR", None), ("Ausland Drittstaat", None)], ), FormField( "Straße", FormFieldType.TEXT, - key="Stammdaten_strasse", + key="strasse", required=False, ), FormField( "Hausnummer", FormFieldType.TEXT, - key="Stammdaten_hausnummer", + key="hausnummer", required=False, ), FormField( "PLZ", FormFieldType.TEXT, - key="Stammdaten_PLZ", + key="PLZ", required=False, ), FormField( "Ort", FormFieldType.TEXT, - key="Stammdaten_ort", + key="ort", required=False, ), FormField( "Bundesland", FormFieldType.DROPDOWN, - key="Stammdaten_bundesland", + key="bundesland", required=False, options=GERMAN_STATE_LIST.for_dropdown, tooltip=( @@ -2524,7 +2732,7 @@ FORM_FIELDS_MASTER_DATA = [ FormField( "Land", FormFieldType.EXTENDED_DROPDOWN, - key="Stammdaten_land", + key="land", required=False, placeholder="Suche...", options=COUNTRY_LIST.for_dropdown, @@ -2532,25 +2740,25 @@ FORM_FIELDS_MASTER_DATA = [ FormField( "Festnetznummer", FormFieldType.TEXT, - key="Stammdaten_festnetznummer", + key="festnetznummer", required=False, ), FormField( "Mobilfunknummer", FormFieldType.TEXT, - key="Stammdaten_mobilfunknummer", + key="mobilfunknummer", required=False, ), FormField( "E-Mail", FormFieldType.TEXT, - key="Stammdaten_email", + key="email", required=False, ), FormField( "Familienstand", FormFieldType.TEXT, - key="Stammdaten_familienstand", + key="familienstand", required=False, tooltip="* Wichtig zu erfragen aufgrund Lebensunterhaltssicherung", ), @@ -2559,7 +2767,7 @@ FORM_FIELDS_MASTER_DATA = [ FormFieldType.DYNAMIC_DROPDOWN, required=False, tooltip="* Wichtig zu erfragen aufgrund Lebensunterhaltssicherung", - key="Stammdaten_anzahl_kinder", + key="anzahl_kinder", children=[ FormField( "Anzahl Kinder", @@ -2896,14 +3104,14 @@ FORM_FIELDS_LANGUAGES = [ FORM_FIELDS = [ FormField( "Ersteintrag Datum", - FormFieldType.DATE, + FormFieldType.TEXT_DATETIME, required=False, key="Metadaten_erstellung", readonly=True, ), FormField( "Aktualisierung Datum", - FormFieldType.DATE, + FormFieldType.TEXT_DATETIME, required=False, key="Metadaten_aktualisierung", readonly=True, @@ -2942,7 +3150,7 @@ FORM_FIELDS = [ FormField( "Projektrelevanz", FormFieldType.DROPDOWN, - key="Projektrelevanz_relevanz", + key="relevanz", required=True, options=[("ja", None), ("nein", None)], ), diff --git a/prototypes/tests.py b/prototypes/tests.py index c6b7c3b..157a467 100644 --- a/prototypes/tests.py +++ b/prototypes/tests.py @@ -6,77 +6,190 @@ import datetime import enum import json import re +from collections import defaultdict from collections.abc import Sequence from pprint import pprint -from typing import Any +from typing import Annotated, Any import babel -from pydantic import BaseModel, ConfigDict, model_validator +from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator, model_validator from PySide6.QtCore import QDate, Qt - # %% +# class FlatBaseModel(BaseModel): +# @classmethod +# def _unflatten_dict(cls, flat_dict: dict) -> dict: +# """Hilfsmethode: Macht aus {'a__b': 1} wieder {'a': {'b': 1}}""" +# result = {} +# for key, value in flat_dict.items(): +# if "__" in key: +# parts = key.split("__") +# aktuell = result +# 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: +# result[key] = value +# return result + +# @model_validator(mode="before") +# @classmethod +# def __unflatten_input(cls, data: Any) -> Any: +# """Eingangskontrolle: Verarbeitet flache DB/GUI-Daten zu Pydantic-Strukturen.""" +# if not isinstance(data, dict): +# return data + +# # 1. Haupt-Struktur wiederherstellen +# unflattened = cls._unflatten_dict(data) + +# print("\n----------------------") +# pprint(unflattened) + +# # 2. Schleife über die Felder, um JSON-Listen von Sub-Modellen zu entpacken +# for key, value in unflattened.items(): +# print(f"{value=}") +# if isinstance(value, str) and value.startswith("[") and value.endswith("]"): +# try: +# parsed = json.loads(value) +# if isinstance(parsed, list): +# print(f"Key {key}: identified list") +# # Falls die Liste Dictionaries enthält, ent-flachen wir diese einzeln +# unflattened[key] = [ +# cls._unflatten_dict(item) if isinstance(item, dict) else item +# for item in parsed +# ] +# else: +# unflattened[key] = parsed +# except json.JSONDecodeError: +# print(f"Key {key}: JSON serialize error") +# pass +# if isinstance(value, list): +# unflattened[key] = [ +# cls._unflatten_dict(item) if isinstance(item, dict) else item +# for item in value +# ] + +# print("\n\n##################Result:") +# pprint(unflattened) + +# return unflattened + +# def to_db(self) -> dict[str, Any]: +# """Ausgang für die DB: Flach, Listen sind JSON-Strings.""" +# nested = super().model_dump() +# return self.__flatten_dict(nested, serialize_lists=True) + +# def to_gui(self) -> dict[str, Any]: +# """Ausgang für die GUI: Flach, aber Listen bleiben Python-Listen für Widgets.""" +# nested = super().model_dump() +# return self.__flatten_dict(nested, serialize_lists=False) + +# @classmethod +# def __flatten_dict( +# cls, nested_dict: dict, parent_key: str = "", serialize_lists: bool = True +# ) -> dict: +# """Rekursiver Alleskönner zum Abflachen von Strukturen.""" +# items = [] +# for k, v in nested_dict.items(): +# new_key = f"{parent_key}__{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): +# # WICHTIG: Das Dict in der Liste wird isoliert abgeflacht (ohne parent_key), +# # da es ein eigenständiges Zeilen-Objekt im DynamicListWidget bleibt! +# 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))) +# else: +# items.append((new_key, processed_list)) +# else: +# items.append((new_key, v)) +# return dict(items) + + class FlatBaseModel(BaseModel): + """ + Optimierte Pydantic-Basisklasse, die JSON-Strings und Doppel-Unterstriche + vollständig rekursiv (tiefenunabhängig) auflöst. + """ + @classmethod - def _unflatten_dict(cls, flat_dict: dict) -> dict: - """Hilfsmethode: Macht aus {'a__b': 1} wieder {'a': {'b': 1}}""" - result = {} - for key, value in flat_dict.items(): - if "__" in key: - parts = key.split("__") - aktuell = result - 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: - result[key] = value - return result + def _recursive_parse_json(cls, data: Any) -> Any: + """Sucht im gesamten Objekt nach JSON-Listen-Strings und entpackt sie.""" + 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: + """Baut im gesamten Objekt flache '__'-Schlüssel in geschachtelte Dicts um.""" + if isinstance(data, dict): + # 1. Die aktuelle Ebene dieses Dictionaries ent-flachen + unflattened_level = {} + for key, value in data.items(): + if "__" in key: + parts = key.split("__") + 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 + + # 2. Jetzt tiefer in die Werte gehen (rekursiv für Unter-Dicts) + return {k: cls._recursive_unflatten(v) for k, v in unflattened_level.items()} + + elif isinstance(data, list): + # Auch durch Listen (z.B. deine Schulbildung) wandern und Sub-Dicts ent-flachen + return [cls._recursive_unflatten(item) for item in data] + + return data @model_validator(mode="before") @classmethod def __unflatten_input(cls, data: Any) -> Any: - """Eingangskontrolle: Verarbeitet flache DB/GUI-Daten zu Pydantic-Strukturen.""" + """Eingangskontrolle: Bereitet flache DB/GUI-Daten sauber für Pydantic vor.""" if not isinstance(data, dict): return data - # 1. Haupt-Struktur wiederherstellen - unflattened = cls._unflatten_dict(data) - pprint(unflattened) + # Schritt 1: Alle JSON-Strings (egal wie tief) in echte Listen umwandeln + # Das verwandelt '[1, 2, 3]' zuverlässig in [1, 2, 3] + json_parsed_data = cls._recursive_parse_json(data) - # 2. Schleife über die Felder, um JSON-Listen von Sub-Modellen zu entpacken - for key, value in unflattened.items(): - if isinstance(value, str) and value.startswith("[") and value.endswith("]"): - try: - parsed = json.loads(value) - if isinstance(parsed, list): - # Falls die Liste Dictionaries enthält, ent-flachen wir diese einzeln - unflattened[key] = [ - cls._unflatten_dict(item) if isinstance(item, dict) else item - for item in parsed - ] - else: - unflattened[key] = parsed - except json.JSONDecodeError: - pass - if isinstance(value, (list)): - unflattened[key] = [ - cls._unflatten_dict(item) if isinstance(item, dict) else item - for item in value - ] + # Schritt 2: Alle '__' Schlüssel (egal wie tief) in Unter-Strukturen falten + final_nested_data = cls._recursive_unflatten(json_parsed_data) - pprint(unflattened) + return final_nested_data - return unflattened - - def to_db(self) -> dict[str, Any]: + def to_db(self) -> Dict[str, Any]: """Ausgang für die DB: Flach, Listen sind JSON-Strings.""" nested = super().model_dump() return self.__flatten_dict(nested, serialize_lists=True) - def to_gui(self) -> dict[str, Any]: - """Ausgang für die GUI: Flach, aber Listen bleiben Python-Listen für Widgets.""" + def to_gui(self) -> Dict[str, Any]: + """Ausgang für die GUI: Flach, aber Listen bleiben Python-Listen.""" nested = super().model_dump() return self.__flatten_dict(nested, serialize_lists=False) @@ -84,7 +197,7 @@ class FlatBaseModel(BaseModel): def __flatten_dict( cls, nested_dict: dict, parent_key: str = "", serialize_lists: bool = True ) -> dict: - """Rekursiver Alleskönner zum Abflachen von Strukturen.""" + """Rekursiver Helfer zum Abflachen von Strukturen.""" items = [] for k, v in nested_dict.items(): new_key = f"{parent_key}__{k}" if parent_key else k @@ -95,8 +208,6 @@ class FlatBaseModel(BaseModel): processed_list = [] for item in v: if isinstance(item, dict): - # WICHTIG: Das Dict in der Liste wird isoliert abgeflacht (ohne parent_key), - # da es ein eigenständiges Zeilen-Objekt im DynamicListWidget bleibt! processed_list.append( cls.__flatten_dict(item, serialize_lists=serialize_lists) ) @@ -139,6 +250,7 @@ class ProjektModell(FlatBaseModel): class Grunderfassung_Unternehmen(FlatBaseModel): Schulbildung: list[Grunderfassung_Schulbildung] + Stammdaten: Grunderfassung_Stammdaten class Grunderfassung_Schulbildung(BaseModel): @@ -153,30 +265,90 @@ class Grunderfassung_Schulbildung(BaseModel): SB_bemerkungsfeld: str | None -# %% -list_schulbildung = [ - { - "SB_abschluss": None, - "SB_abschlussgrad": None, - "SB_abschlussjahr": None, - "SB_bemerkungsfeld": None, - "SB_land": None, - "SB_ort": None, - "SB_schule": None, - }, - { - "SB_abschluss": None, - "SB_abschlussgrad": None, - "SB_abschlussjahr": None, - "SB_bemerkungsfeld": None, - "SB_land": None, - "SB_ort": None, - "SB_schule": None, - }, -] -data = {"Schulbildung": list_schulbildung} +ValidAge = Annotated[int, Field(ge=0, le=99)] -Grunderfassung_Unternehmen(**data) + +class Grunderfassung_Stammdaten(BaseModel): + # 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 + anzahl_kinder: Grunderfassung_Stammdaten_AnzahlKinder + + # @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 Grunderfassung_Stammdaten_AnzahlKinder(BaseModel): + anzahl: int | None + alter: list[ValidAge | None] | None = None + + +# %% + +data = { + "Schulbildung": [ + { + "SB_abschluss": None, + "SB_abschlussgrad": None, + "SB_abschlussjahr": None, + "SB_bemerkungsfeld": None, + "SB_land": None, + "SB_ort": None, + "SB_schule": None, + }, + { + "SB_abschluss": None, + "SB_abschlussgrad": None, + "SB_abschlussjahr": None, + "SB_bemerkungsfeld": None, + "SB_land": None, + "SB_ort": None, + "SB_schule": None, + }, + ], + "Stammdaten__anzahl_kinder__anzahl": 3, + "Stammdaten__anzahl_kinder__alter": [1, 2, 3], +} + +parsed = Grunderfassung_Unternehmen(**data) +# %% +db_entry = parsed.to_db() +pprint(db_entry) +# %% +loaded_from_db = Grunderfassung_Unternehmen(**db_entry) +# %% +loaded_from_db + +# %% +loaded_from_db.to_gui() # %% projekt = ProjektModell(**gui_rohdaten) @@ -285,6 +457,48 @@ pprint(unflatted) flatted +# %% +# key merger +def merge_dicts_to_lists(dict_iter): + 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): + if not merged_dict: + return [] + + # 1. Wir trennen die Keys und die dazugehörigen Wert-Listen + keys = merged_dict.keys() + value_lists = merged_dict.values() + + # 2. zip(*value_lists) nimmt das erste Element aus jeder Liste, dann das zweite, etc. + # 3. zip(keys, row) verbindet die Keys wieder mit den jeweiligen Werten einer Zeile + return [dict(zip(keys, row)) for row in zip(*value_lists)] + + +data = [ + {"Stammdaten__anzahl_kinder__alter": None}, + {"Stammdaten__anzahl_kinder__alter": 2}, + {"Stammdaten__anzahl_kinder__alter": 3}, + {"Stammdaten__anzahl_kinder__alter": None}, +] +merged = merge_dicts_to_lists(data) +pprint(merged) + +merged = {"Stammdaten__anzahl_kinder__alter": [1, 2]} + +unmerged = unmerge_dict_to_list(merged) +pprint(unmerged) + +# %% +unmerge_dict_to_list({}) + # %% string = """ 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 diff --git a/src/wce_crm/db.py b/src/wce_crm/db.py index 53596b2..14e9e3e 100644 --- a/src/wce_crm/db.py +++ b/src/wce_crm/db.py @@ -33,6 +33,7 @@ class SafeDateTime(TypeDecorator): md_crm = sql.MetaData() +md_main = sql.MetaData() # ---------- OLD "Kontaktliste" ---------- @@ -295,3 +296,58 @@ def get_ext_crm_contact_person( df_contact_person = get_ext_crm_contact_person(None) + + +grunderfassung_unternehmen: sql.Table = Table( + "grunderfassung_unternehmen", + md_main, + Column("erfassung_id", sql.Integer, nullable=False, unique=True, autoincrement=True), + Column("Arbeitserfahrung", sql.Text, nullable=True), + Column("Grunderfassung_fallnummer", sql.Text, nullable=True), + Column("Grunderfassung_notiz", sql.Text, nullable=True), + Column("HoehereBildung", sql.Text, nullable=True), + Column("Kontaktperson__KP_adresse", sql.Text, nullable=True), + Column("Kontaktperson__KP_anrede_anschrift", sql.Text, nullable=True), + Column("Kontaktperson__KP_email", sql.Text, nullable=True), + Column("Kontaktperson__KP_festnetznummer", sql.Text, nullable=True), + Column("Kontaktperson__KP_funktion_beziehung", sql.Text, nullable=True), + Column("Kontaktperson__KP_mobilfunknummer", sql.Text, nullable=True), + Column("Kontaktperson__KP_name", sql.Text, nullable=True), + Column("Kontaktperson__KP_name_partner", sql.Text, nullable=True), + Column("Kontaktperson__KP_titel", sql.Text, nullable=True), + Column("Kontaktperson__KP_vorname", sql.Text, nullable=True), + Column("Metadaten_aktualisierung", sql.Text, nullable=True), + Column("Metadaten_erstellung", sql.Text, nullable=True), + Column("Metadaten_nutzer", sql.String(20), nullable=True), + Column("Partnersuche__kanal_aufmerksamkeit", sql.Text, nullable=True), + Column("Partnersuche__person_suche", sql.Text, nullable=True), + Column("Partnersuche__un_suche", sql.Text, nullable=True), + Column("Projektrelevanz__relevanz", sql.Boolean, nullable=True), + Column("Schulbildung", sql.Text, nullable=True), + Column("Sprachkenntnisse", sql.Text, nullable=True), + Column("Stammdaten__PLZ", sql.Text, nullable=True), + Column("Stammdaten__anrede_anschrift", sql.Text, nullable=True), + Column("Stammdaten__anzahl_kinder__alter", sql.Text, nullable=True), + Column("Stammdaten__anzahl_kinder__anzahl", sql.Text, nullable=True), + Column("Stammdaten__aufenthaltsort", sql.Text, nullable=True), + Column("Stammdaten__bundesland", sql.Text, nullable=True), + Column("Stammdaten__email", sql.Text, nullable=True), + Column("Stammdaten__familienstand", sql.Text, nullable=True), + Column("Stammdaten__festnetznummer", sql.Text, nullable=True), + Column("Stammdaten__geburtsdatum", sql.Text, nullable=True), + Column("Stammdaten__hausnummer", sql.Text, nullable=True), + Column("Stammdaten__herkunftsland", sql.Text, nullable=True), + Column("Stammdaten__mobilfunknummer", sql.Text, nullable=True), + Column("Stammdaten__name", sql.Text, nullable=True), + Column("Stammdaten__ort", sql.Text, nullable=True), + Column("Stammdaten__rueckkehrer", sql.Boolean, nullable=True), + Column("Stammdaten__staatsangehoerigkeit", sql.Text, nullable=True), + Column("Stammdaten__strasse", sql.Text, nullable=True), + Column("Stammdaten__titel", sql.Text, nullable=True), + Column("Stammdaten__vorname", sql.Text, nullable=True), + Column("WeitereInfos__WI_arbeitsstatus", sql.Text, nullable=True), + Column("WeitereInfos__WI_aufenthaltstitel", sql.Text, nullable=True), + Column("WeitereInfos__WI_deutsch_sprache", sql.Text, nullable=True), + Column("WeitereInfos__WI_gueltigkeit_aufenthaltstitel", sql.Text, nullable=True), + Column("WeitereInfos__WI_meldung_institution", sql.Text, nullable=True), +)