From df9391494249f7d8d8dd22a1777e22701af93cb1 Mon Sep 17 00:00:00 2001 From: foefl Date: Fri, 26 Jun 2026 14:14:45 +0200 Subject: [PATCH] switch to new data model with distinct company and person data --- ...4_baseline_data_model_with_new_approach.py | 118 +++++++++++++++ ...added_new_column_and_renamed_table_for_.py | 37 ----- data_model/project_db_schema.aml | 98 ++++++++++++ src/wce_crm/backend/backend.py | 99 ++++++++++-- src/wce_crm/custom_widget_registry.py | 2 +- src/wce_crm/data_models.py | 16 +- src/wce_crm/db.py | 142 ++++++++---------- src/wce_crm/form_defs.py | 71 +-------- src/wce_crm/gui.py | 118 ++++++++------- 9 files changed, 436 insertions(+), 265 deletions(-) create mode 100644 alembic/versions/1a8e957522a4_baseline_data_model_with_new_approach.py delete mode 100644 alembic/versions/5f2af5179c47_added_new_column_and_renamed_table_for_.py create mode 100644 data_model/project_db_schema.aml diff --git a/alembic/versions/1a8e957522a4_baseline_data_model_with_new_approach.py b/alembic/versions/1a8e957522a4_baseline_data_model_with_new_approach.py new file mode 100644 index 0000000..83e0c40 --- /dev/null +++ b/alembic/versions/1a8e957522a4_baseline_data_model_with_new_approach.py @@ -0,0 +1,118 @@ +"""Baseline: data model with new approach + +Revision ID: 1a8e957522a4 +Revises: +Create Date: 2026-06-26 13:47:48.937123 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +import wce_crm.db + +# revision identifiers, used by Alembic. +revision: str = "1a8e957522a4" +down_revision: Union[str, Sequence[str], None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "grunderfassung_personen", + sa.Column("pers_id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("Metadaten_erstellung", wce_crm.db.UTCDateTime(), nullable=True), + sa.Column("Metadaten_aktualisierung", wce_crm.db.UTCDateTime(), nullable=True), + sa.Column("Metadaten_nutzer", sa.String(length=20), nullable=True), + sa.Column("Metadaten_wiedereintrittsdatum", sa.Date(), nullable=True), + sa.Column("Arbeitserfahrung", sa.Text(), nullable=True), + sa.Column("Grunderfassung_fallnummer", sa.Text(), nullable=True), + sa.Column("Grunderfassung_notiz", sa.Text(), nullable=True), + sa.Column("HoehereBildung", sa.Text(), nullable=True), + sa.Column("Kontaktperson__KP_adresse", sa.Text(), nullable=True), + sa.Column("Kontaktperson__KP_anrede_anschrift", sa.Text(), nullable=True), + sa.Column("Kontaktperson__KP_email", sa.Text(), nullable=True), + sa.Column("Kontaktperson__KP_festnetznummer", sa.Text(), nullable=True), + sa.Column("Kontaktperson__KP_funktion_beziehung", sa.Text(), nullable=True), + sa.Column("Kontaktperson__KP_mobilfunknummer", sa.Text(), nullable=True), + sa.Column("Kontaktperson__KP_name", sa.Text(), nullable=True), + sa.Column("Kontaktperson__KP_name_partner", sa.Text(), nullable=True), + sa.Column("Kontaktperson__KP_titel", sa.Text(), nullable=True), + sa.Column("Kontaktperson__KP_vorname", sa.Text(), nullable=True), + sa.Column("Projektrelevanz__relevanz", sa.Text(), nullable=True), + sa.Column("Projektrelevanz__foerderperiode", sa.Text(), nullable=True), + sa.Column("Schulbildung", sa.Text(), nullable=True), + sa.Column("Sprachkenntnisse", sa.Text(), nullable=True), + sa.Column("Stammdaten__PLZ", sa.Text(), nullable=True), + sa.Column("Stammdaten__anrede_anschrift", sa.Text(), nullable=True), + sa.Column("Stammdaten__anzahl_kinder__alter", sa.Text(), nullable=True), + sa.Column("Stammdaten__anzahl_kinder__anzahl", sa.Integer(), nullable=True), + sa.Column("Stammdaten__aufenthaltsort", sa.Text(), nullable=True), + sa.Column("Stammdaten__bundesland", sa.Text(), nullable=True), + sa.Column("Stammdaten__land", sa.Text(), nullable=True), + sa.Column("Stammdaten__email", sa.Text(), nullable=True), + sa.Column("Stammdaten__familienstand", sa.Text(), nullable=True), + sa.Column("Stammdaten__festnetznummer", sa.Text(), nullable=True), + sa.Column("Stammdaten__geburtsdatum", sa.Date(), nullable=True), + sa.Column("Stammdaten__hausnummer", sa.Text(), nullable=True), + sa.Column("Stammdaten__herkunftsland", sa.Text(), nullable=True), + sa.Column("Stammdaten__mobilfunknummer", sa.Text(), nullable=True), + sa.Column("Stammdaten__name", sa.Text(), nullable=True), + sa.Column("Stammdaten__ort", sa.Text(), nullable=True), + sa.Column("Stammdaten__rueckkehrer", sa.Boolean(), nullable=True), + sa.Column("Stammdaten__staatsangehoerigkeit", sa.Text(), nullable=True), + sa.Column("Stammdaten__strasse", sa.Text(), nullable=True), + sa.Column("Stammdaten__titel", sa.Text(), nullable=True), + sa.Column("Stammdaten__vorname", sa.Text(), nullable=True), + sa.Column("WeitereInfos__WI_arbeitsstatus", sa.Text(), nullable=True), + sa.Column("WeitereInfos__WI_aufenthaltstitel", sa.Text(), nullable=True), + sa.Column("WeitereInfos__WI_deutsch_sprache", sa.Text(), nullable=True), + sa.Column("WeitereInfos__WI_gueltigkeit_aufenthaltstitel", sa.Date(), nullable=True), + sa.Column("WeitereInfos__WI_meldung_institution", sa.Text(), nullable=True), + sa.PrimaryKeyConstraint("pers_id"), + ) + op.create_table( + "grunderfassung_unternehmen", + sa.Column("un_id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("Metadaten_erstellung", wce_crm.db.UTCDateTime(), nullable=True), + sa.Column("Metadaten_aktualisierung", wce_crm.db.UTCDateTime(), nullable=True), + sa.Column("Metadaten_nutzer", sa.String(length=20), nullable=True), + sa.Column("Grunderfassung_fallnummer", sa.Text(), nullable=True), + sa.Column("Grunderfassung_notiz", sa.Text(), nullable=True), + sa.Column("Partnersuche__kanal_aufmerksamkeit", sa.Text(), nullable=True), + sa.Column("Partnersuche__person_suche", sa.Integer(), nullable=True), + sa.Column("Partnersuche__un_suche", sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint("un_id"), + ) + op.create_table( + "zuordnung_personen_unternehmen", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("un_id", sa.Integer(), nullable=False), + sa.Column("pers_id", sa.Integer(), nullable=False), + sa.Column("gueltig_ab", wce_crm.db.UTCDateTime(), nullable=False), + sa.Column("gueltig_bis", wce_crm.db.UTCDateTime(), nullable=True), + sa.ForeignKeyConstraint( + ["pers_id"], + ["grunderfassung_personen.pers_id"], + ), + sa.ForeignKeyConstraint( + ["un_id"], + ["grunderfassung_unternehmen.un_id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("zuordnung_personen_unternehmen") + op.drop_table("grunderfassung_unternehmen") + op.drop_table("grunderfassung_personen") + # ### end Alembic commands ### diff --git a/alembic/versions/5f2af5179c47_added_new_column_and_renamed_table_for_.py b/alembic/versions/5f2af5179c47_added_new_column_and_renamed_table_for_.py deleted file mode 100644 index dc4e595..0000000 --- a/alembic/versions/5f2af5179c47_added_new_column_and_renamed_table_for_.py +++ /dev/null @@ -1,37 +0,0 @@ -"""added new column and renamed table for initial recording - -Revision ID: 5f2af5179c47 -Revises: -Create Date: 2026-06-17 16:06:46.421562 - -""" - -from typing import Sequence, Union - -import sqlalchemy as sa -from alembic import op - -# revision identifiers, used by Alembic. -revision: str = "5f2af5179c47" -down_revision: Union[str, Sequence[str], None] = None -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - """Upgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.rename_table("grunderfassung_unternehmen", "grunderfassung") - op.add_column( - "grunderfassung", - sa.Column("Metadaten_wiedereintrittsdatum", sa.Date, nullable=True, default=None), - ) - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - op.drop_column("grunderfassung", "Metadaten_wiedereintrittsdatum") - op.rename_table("grunderfassung", "grunderfassung_unternehmen") - # ### end Alembic commands ### diff --git a/data_model/project_db_schema.aml b/data_model/project_db_schema.aml new file mode 100644 index 0000000..16e697b --- /dev/null +++ b/data_model/project_db_schema.aml @@ -0,0 +1,98 @@ +Ext_CRM.Master + ma_id int pk + + +Ext_CRM.Ansprechpartner + an_id int pk + ma_id int -> Ext_CRM.Master(ma_id) + + +Grunderfassung_Unternehmen + un_id int pk {autoIncrement} + Metadaten_erstellung "timestamp with time zone"=`datetime.datetime.now(datetime.UTC)` nullable + Metadaten_aktualisierung "timestamp with time zone"=`datetime.datetime.now(datetime.UTC)` nullable {on_update: `datetime.datetime.now(datetime.UTC)`} + Metadaten_nutzer varchar(20) nullable + Grunderfassung_fallnummer text nullable + Grunderfassung_notiz text nullable + Partnersuche__un_suche int nullable -> Ext_CRM.Master(ma_id) + Partnersuche__person_suche int nullable -> Ext_CRM.Ansprechpartner(an_id) + Partnersuche__kanal_aufmerksamkeit text nullable + + +Grunderfassung_Personen + pers_id int pk {autoIncrement} + Metadaten_erstellung "timestamp with time zone"=`datetime.datetime.now(datetime.UTC)` nullable + Metadaten_aktualisierung "timestamp with time zone"=`datetime.datetime.now(datetime.UTC)` nullable {on_update: `datetime.datetime.now(datetime.UTC)`} + Metadaten_wiedereintrittsdatum timestamp nullable + Metadaten_nutzer varchar(20) nullable + Grunderfassung_fallnummer text nullable + Grunderfassung_notiz text nullable + HoehereBildung text nullable + Kontaktperson__KP_adresse text nullable + Kontaktperson__KP_anrede_anschrift text nullable + Kontaktperson__KP_email text nullable + Kontaktperson__KP_festnetznummer text nullable + Kontaktperson__KP_funktion_beziehung text nullable + Kontaktperson__KP_mobilfunknummer text nullable + Kontaktperson__KP_name_partner text nullable + Kontaktperson__KP_titel text nullable + Kontaktperson__KP_vorname text nullable + Kontaktperson__KP_email text nullable + Kontaktperson__KP_email text nullable + Kontaktperson__KP_email text nullable + Projektrelevanz__relevanz text nullable + Projektrelevanz__foerderperiode text nullable + Schulbildung text nullable + Stammdaten__PLZ text nullable + Stammdaten__anrede_anschrift text nullable + Stammdaten__anzahl_kinder__alter text nullable + Stammdaten__anzahl_kinder__anzahl int nullable + Stammdaten__aufenthaltsort text nullable + Stammdaten__bundesland text nullable + Stammdaten__land text nullable + Stammdaten__email text nullable + Stammdaten__familienstand text nullable + Stammdaten__festnetznummer text nullable + Stammdaten__geburtsdatum date nullable + Stammdaten__hausnummer text nullable + Stammdaten__herkunftsland text nullable + Stammdaten__mobilfunknummer text nullable + Stammdaten__name text nullable + Stammdaten__ort text nullable + Stammdaten__rueckkehrer bool nullable + Stammdaten__staatsangehoerigkeit text nullable + Stammdaten__strasse text nullable + Stammdaten__titel text nullable + Stammdaten__vorname text nullable + WeitereInfos__WI_arbeitsstatus text nullable + WeitereInfos__WI_aufenthaltstitel text nullable + WeitereInfos__WI_deutsch_sprache text nullable + WeitereInfos__WI_gueltigkeit_aufenthaltstitel text nullable + WeitereInfos__WI_meldung_institution text nullable + + +Zuordnung_Personen_Unternehmen | Eine Person kann zu mehreren Unternehmen gehören, ein Unternehmen kann mehrere Personen betreuen. + id int pk {autoIncrement} + un_id int -> Grunderfassung_Unternehmen(un_id) + pers_id int -> Grunderfassung_Personen(pers_id) + gueltig_ab date + gueltig_bis date nullable + + +Beratung_Vorgang | Beratung kann an Unternehmen allein (pauschal), Person allein (pauschal) oder beide (individual) gebunden sein + vorgang_id int pk {autoIncrement} + un_id int nullable -> Grunderfassung_Unternehmen(un_id) | Pauschalberatungen sind sowohl für Unternehmen als auch Personen zulässig + pers_id int nullable -> Grunderfassung_Personen(pers_id) | sowohl Pauschal- als auch Individualberatung bei Personenbindung möglich + titel text + beratung_typ berat_typ(pauschal, individual) + erstellt "timestamp with time zone"=`datetime.datetime.now(datetime.UTC)` nullable + aktualisiert "timestamp with time zone"=`datetime.datetime.now(datetime.UTC)` nullable {on_update: `datetime.datetime.now(datetime.UTC)`} + temporaer bool | Gibt an, ob der Vorgang losgelöst von der Grunderfassung gestartet wurde (Daten müssen nachträglich zugeordnet werden); theoretisch auch erfüllt, wenn `un_id` und `pers_id` beide NULL sind + + +Beratung_Einzelberatung | Änderungen müssen über einen DB-Trigger an Elterntabelle zurückgeschrieben werden + beratung_id int pk {autoIncrement} + vorgang_id int -> Beratung_Vorgang(vorgang_id) | Jede Beratung muss zu einem Beratungsvorgang gehören. + erstellt "timestamp with time zone"=`datetime.datetime.now(datetime.UTC)` nullable + aktualisiert "timestamp with time zone"=`datetime.datetime.now(datetime.UTC)` nullable {on_update: `datetime.datetime.now(datetime.UTC)`} + weitere_merkmale_ausstehend text nullable # Merkmale noch zu ergänzen diff --git a/src/wce_crm/backend/backend.py b/src/wce_crm/backend/backend.py index bbf3524..d729aac 100644 --- a/src/wce_crm/backend/backend.py +++ b/src/wce_crm/backend/backend.py @@ -154,11 +154,11 @@ def initrec_comp_contact_person_search_get_info( # // internals -def initrec_insert_initial_recording( +def initrec_company_insert_initial_recording( data: dict[str, Any], ) -> InitRecId: logger.debug("[Call backend] insert_initial_recording") - stmt = db.grunderfassung.insert() + stmt = db.grunderfassung_unternehmen.insert() with db.ENGINE.begin() as conn: ret = conn.execute(stmt, data) @@ -171,21 +171,25 @@ def initrec_insert_initial_recording( return prim_keys[0] -def initrec_update_initial_recording( +def initrec_company_update_initial_recording( id_: InitRecId, data: dict[str, Any], ) -> None: logger.debug("[Call backend] update_initial_recording") - stmt = db.grunderfassung.update().where(db.grunderfassung.c.erfassung_id == id_) + stmt = db.grunderfassung_unternehmen.update().where( + db.grunderfassung_unternehmen.c.un_id == id_ + ) with db.ENGINE.begin() as conn: conn.execute(stmt, data) -def initrec_get_initial_recording( +def initrec_company_get_initial_recording( id_: InitRecId, ) -> dict[str, Any]: logger.debug("[Call backend] get_initial_recording") - stmt = db.grunderfassung.select().where(db.grunderfassung.c.erfassung_id == id_) + stmt = db.grunderfassung_unternehmen.select().where( + db.grunderfassung_unternehmen.c.un_id == id_ + ) with db.ENGINE.begin() as conn: ret = conn.execute(stmt) @@ -198,11 +202,75 @@ def initrec_get_initial_recording( return row._asdict() -def initrec_delete_initial_recording( +def initrec_company_delete_initial_recording( id_: InitRecId, ) -> None: logger.debug("[Call backend] delete_initial_recording") - stmt = db.grunderfassung.delete().where(db.grunderfassung.c.erfassung_id == id_) + stmt = db.grunderfassung_unternehmen.delete().where( + db.grunderfassung_unternehmen.c.un_id == id_ + ) + with db.ENGINE.begin() as conn: + ret = conn.execute(stmt) + + if ret.rowcount == 0: + raise KeyError(f"Database ID {id_} not found for deletion") + + +def initrec_person_insert_initial_recording( + data: dict[str, Any], +) -> InitRecId: + logger.debug("[Call backend] insert_initial_recording") + stmt = db.grunderfassung_personen.insert() + with db.ENGINE.begin() as conn: + ret = conn.execute(stmt, data) + + if ret.rowcount == 0: + raise IOError("Entry was not inserted correctly") + + prim_keys = ret.inserted_primary_key + assert prim_keys + + return prim_keys[0] + + +def initrec_person_update_initial_recording( + id_: InitRecId, + data: dict[str, Any], +) -> None: + logger.debug("[Call backend] update_initial_recording") + stmt = db.grunderfassung_personen.update().where( + db.grunderfassung_personen.c.pers_id == id_ + ) + with db.ENGINE.begin() as conn: + conn.execute(stmt, data) + + +def initrec_person_get_initial_recording( + id_: InitRecId, +) -> dict[str, Any]: + logger.debug("[Call backend] get_initial_recording") + stmt = db.grunderfassung_personen.select().where( + db.grunderfassung_personen.c.pers_id == id_ + ) + with db.ENGINE.begin() as conn: + ret = conn.execute(stmt) + + if ret.rowcount == 0: + raise KeyError(f"Database ID {id_} not found") + + row = ret.fetchone() + assert row, "row was not obtained" + + return row._asdict() + + +def initrec_person_delete_initial_recording( + id_: InitRecId, +) -> None: + logger.debug("[Call backend] delete_initial_recording") + stmt = db.grunderfassung_personen.delete().where( + db.grunderfassung_personen.c.pers_id == id_ + ) with db.ENGINE.begin() as conn: ret = conn.execute(stmt) @@ -218,15 +286,16 @@ class FrontpageCompany: is_company: bool +# TODO separate functions for companies and person: focus on companies first def front_get_company_list() -> list[FrontpageCompany]: logger.debug("[Call backend] get_company_list") stmt = sql.select( - db.grunderfassung.c.erfassung_id, - db.grunderfassung.c.Partnersuche__un_suche, - db.grunderfassung.c.Stammdaten__vorname, - db.grunderfassung.c.Stammdaten__name, - db.grunderfassung.c.Metadaten_aktualisierung, - ).order_by(db.grunderfassung.c.Metadaten_aktualisierung.desc()) + db.grunderfassung_unternehmen.c.un_id, + db.grunderfassung_unternehmen.c.Partnersuche__un_suche, + # db.grunderfassung_unternehmen.c.Stammdaten__vorname, + # db.grunderfassung_unternehmen.c.Stammdaten__name, + db.grunderfassung_unternehmen.c.Metadaten_aktualisierung, + ).order_by(db.grunderfassung_unternehmen.c.Metadaten_aktualisierung.desc()) with db.ENGINE.connect() as conn: res = conn.execute(stmt) @@ -234,7 +303,7 @@ def front_get_company_list() -> list[FrontpageCompany]: front_page_companies: list[FrontpageCompany] = [] for entry in res.mappings(): - erfassung_id = entry["erfassung_id"] + erfassung_id = entry["un_id"] ma_id = entry["Partnersuche__un_suche"] datetime_akt = cast(datetime.datetime, entry["Metadaten_aktualisierung"]) datetime_akt = datetime_akt.astimezone() diff --git a/src/wce_crm/custom_widget_registry.py b/src/wce_crm/custom_widget_registry.py index f1c6d8a..1d4cb09 100644 --- a/src/wce_crm/custom_widget_registry.py +++ b/src/wce_crm/custom_widget_registry.py @@ -4,6 +4,6 @@ from typing import Final CUSTOM_WIDGET_NAMES: Final[frozenset] = frozenset( [ - "grunderfassung_suche", + "grunderfassung_unternehmen_suche", ] ) diff --git a/src/wce_crm/data_models.py b/src/wce_crm/data_models.py index 9575ea7..88efdbd 100644 --- a/src/wce_crm/data_models.py +++ b/src/wce_crm/data_models.py @@ -83,7 +83,7 @@ class FlatBaseModel(BaseModel): @model_validator(mode="before") @classmethod - def __unflatten_input( + def _unflatten_input( cls, data: Any, ) -> Any: # type: ignore @@ -141,7 +141,18 @@ class FlatBaseModel(BaseModel): return dict(items) -class Grunderfassung(FlatBaseModel): +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 + + +class Grunderfassung_Personen(FlatBaseModel): # default in SQLAlchemy with lambda and timezone-aware datetime Metadaten_erstellung: AwareDatetime | None = None Metadaten_aktualisierung: AwareDatetime | None = None # see above @@ -150,7 +161,6 @@ class Grunderfassung(FlatBaseModel): Grunderfassung_fallnummer: str Grunderfassung_notiz: str | None - Partnersuche: Grunderfassung_PartnerSuche | None = None Projektrelevanz: Grunderfassung_Projektrelevanz Kontaktperson: Grunderfassung_Kontaktperson Stammdaten: Grunderfassung_Stammdaten diff --git a/src/wce_crm/db.py b/src/wce_crm/db.py index 2c2a2a2..749fddb 100644 --- a/src/wce_crm/db.py +++ b/src/wce_crm/db.py @@ -62,83 +62,6 @@ MD_CRM = sql.MetaData() MD_MAIN = sql.MetaData() ENGINE = sql.create_engine(f"sqlite:///{constants.Config.DB_PATH_MAIN}") -# ---------- OLD "Kontaktliste" ---------- - -# md_kontaktliste = sql.MetaData() - -# ext_kl_unternehmen: sql.Table = Table( -# "Unternehmen", -# md_kontaktliste, -# Column("u_id", sql.Integer, nullable=False, unique=True), -# Column("u_zeitstempel_eintrag", sql.DateTime, nullable=False), -# Column("u_rechtsform", sql.Text, nullable=False), -# Column("u_firmenname", sql.Text, nullable=False), -# Column("u_strasse", sql.Text, nullable=False), -# Column("u_hausnummer", sql.Text, nullable=False), -# Column("u_adresszusatz", sql.Text, nullable=True), -# Column("u_plz", sql.Text, nullable=False), -# Column("u_ort", sql.Text, nullable=False), -# Column("u_postfach", sql.Text, nullable=True), -# Column("u_website", sql.Text, nullable=True), -# Column("u_anrede", sql.Text, nullable=False), -# Column("u_titel", sql.Text, nullable=True), -# Column("u_vorname", sql.Text, nullable=False), -# Column("u_nachname", sql.Text, nullable=False), -# Column("u_funktion", sql.Text, nullable=False), -# Column("u_mail", sql.Text, nullable=False), -# Column("u_telefon", sql.Text, nullable=False), -# Column("u_plz_postfach", sql.Text, nullable=True), -# Column("u_einwilligung_inhaber", sql.Boolean, nullable=True), -# Column("u_einwilligung_ansprechpartner", sql.Boolean, nullable=True), -# Column("u_aktiv", sql.Boolean, nullable=False, default=1), -# ) - -# ext_kl_unternehmen_schema: t.PolarsSchema = { -# "u_id": pl.UInt64, -# "u_zeitstempel_eintrag": pl.Datetime, -# "u_rechtsform": pl.String, -# "u_firmenname": pl.String, -# "u_strasse": pl.String, -# "u_hausnummer": pl.String, -# "u_adresszusatz": pl.String, -# "u_plz": pl.String, -# "u_ort": pl.String, -# "u_postfach": pl.String, -# "u_website": pl.String, -# "u_anrede": pl.String, -# "u_titel": pl.String, -# "u_vorname": pl.String, -# "u_nachname": pl.String, -# "u_funktion": pl.String, -# "u_mail": pl.String, -# "u_telefon": pl.String, -# "u_plz_postfach": pl.String, -# "u_einwilligung_inhaber": pl.Boolean, -# "u_einwilligung_ansprechpartner": pl.Boolean, -# "u_aktiv": pl.Boolean, -# } - - -# def get_ext_kontaktliste( -# db_path: Path | None, -# ) -> pl.DataFrame: -# if db_path is None: -# ENV_PTH = os.environ.get("DOPT_DB_KONTAKTLISTE", None) -# if ENV_PTH is None: -# raise ValueError("No database path provided or found as ENV var.") -# db_path = Path(ENV_PTH) - -# if not db_path.exists(): -# raise FileNotFoundError(f"Database not found under >{db_path}<") - -# engine = sql.create_engine(f"sqlite:///{db_path}") -# stmt = sql.select(ext_kl_unternehmen) -# return pl.read_database(stmt, engine, schema_overrides=ext_kl_unternehmen_schema) - - -# df_kontaktliste = get_ext_kontaktliste(None) - -# ---------------------------------------------------- ext_crm_master: sql.Table = Table( "Master", @@ -324,11 +247,41 @@ def get_ext_crm_contact_person( # df_contact_person = get_ext_crm_contact_person(None) DF_CONTACT_PERSON = get_ext_crm_contact_person(constants.Config.DB_PATH_CRM) -grunderfassung: sql.Table = Table( - "grunderfassung", +grunderfassung_unternehmen: sql.Table = Table( + "grunderfassung_unternehmen", MD_MAIN, Column( - "erfassung_id", + "un_id", + sql.Integer, + primary_key=True, + autoincrement=True, + ), + Column( + "Metadaten_erstellung", + UTCDateTime, + nullable=True, + default=lambda: datetime.datetime.now(datetime.UTC), + ), + Column( + "Metadaten_aktualisierung", + UTCDateTime, + nullable=True, + default=lambda: datetime.datetime.now(datetime.UTC), + onupdate=lambda: datetime.datetime.now(datetime.UTC), + ), + Column("Metadaten_nutzer", sql.String(20), nullable=True), + Column("Grunderfassung_fallnummer", sql.Text, nullable=True), + Column("Grunderfassung_notiz", sql.Text, nullable=True), + Column("Partnersuche__kanal_aufmerksamkeit", sql.Text, nullable=True, default=None), + Column("Partnersuche__person_suche", sql.Integer, nullable=True, default=None), + Column("Partnersuche__un_suche", sql.Integer, nullable=True, default=None), +) + +grunderfassung_personen: sql.Table = Table( + "grunderfassung_personen", + MD_MAIN, + Column( + "pers_id", sql.Integer, primary_key=True, autoincrement=True, @@ -364,9 +317,6 @@ grunderfassung: sql.Table = Table( ), # TODO: check if needed when set by trigger Column("Kontaktperson__KP_titel", sql.Text, nullable=True), Column("Kontaktperson__KP_vorname", sql.Text, nullable=True), - Column("Partnersuche__kanal_aufmerksamkeit", sql.Text, nullable=True, default=None), - Column("Partnersuche__person_suche", sql.Integer, nullable=True, default=None), - Column("Partnersuche__un_suche", sql.Integer, nullable=True, default=None), Column("Projektrelevanz__relevanz", sql.Text, nullable=True), Column("Projektrelevanz__foerderperiode", sql.Text, nullable=True), Column("Schulbildung", sql.Text, nullable=True), @@ -398,3 +348,29 @@ grunderfassung: sql.Table = Table( Column("WeitereInfos__WI_gueltigkeit_aufenthaltstitel", sql.Date, nullable=True), Column("WeitereInfos__WI_meldung_institution", sql.Text, nullable=True), ) + +zuordnung_personen_unternehmen: sql.Table = Table( + "zuordnung_personen_unternehmen", + MD_MAIN, + Column( + "id", + sql.Integer, + primary_key=True, + autoincrement=True, + ), + Column("un_id", sql.ForeignKey("grunderfassung_unternehmen.un_id"), nullable=False), + Column("pers_id", sql.ForeignKey("grunderfassung_personen.pers_id"), nullable=False), + Column( + "gueltig_ab", + UTCDateTime, + nullable=False, + default=lambda: datetime.datetime.now(datetime.UTC), + ), + Column( + "gueltig_bis", + UTCDateTime, + nullable=True, + default=lambda: datetime.datetime.now(datetime.UTC), + onupdate=lambda: datetime.datetime.now(datetime.UTC), + ), +) diff --git a/src/wce_crm/form_defs.py b/src/wce_crm/form_defs.py index 3986bbc..c000d8c 100644 --- a/src/wce_crm/form_defs.py +++ b/src/wce_crm/form_defs.py @@ -920,79 +920,10 @@ INITREC_COMP = [ FormField( "Suche", FormFieldType.CUSTOM, - custom_widget="grunderfassung_suche", + custom_widget="grunderfassung_unternehmen_suche", key="Partnersuche", children=INITREC_COMP_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=INITREC_CONTACT_PERSON, - ), - FormField( - "Stammdaten", - FormFieldType.GROUP, - key="Stammdaten", - children=INITREC_MASTER_DATA, - ), - FormField( - "Weitere Informationen", - FormFieldType.GROUP, - key="WeitereInfos", - children=INITREC_ADDITIONAL_DATA, - ), - FormField( - "Schulbildung", - FormFieldType.DYNAMIC_LIST, - children=INITREC_SCHOOL, - key="Schulbildung", - ), - FormField( - "Studium/Ausbildung", - FormFieldType.DYNAMIC_LIST, - children=INITREC_HIGHER_EDUCATION, - key="HoehereBildung", - ), - FormField( - "Arbeitserfahrung", - FormFieldType.DYNAMIC_LIST, - children=INITREC_WORK_EXPERIENCE, - key="Arbeitserfahrung", - ), - FormField( - "Sprachkenntnisse", - FormFieldType.DYNAMIC_LIST, - children=INITREC_LANGUAGES, - key="Sprachkenntnisse", - ), ] INITREC_PERSON = [ diff --git a/src/wce_crm/gui.py b/src/wce_crm/gui.py index 156f27a..d3eeb85 100644 --- a/src/wce_crm/gui.py +++ b/src/wce_crm/gui.py @@ -58,7 +58,12 @@ from PySide6.QtWidgets import ( import wce_crm.constants from wce_crm.backend import backend -from wce_crm.data_models import COLUMN_SEP, FlatBaseModel, Grunderfassung +from wce_crm.data_models import ( + COLUMN_SEP, + FlatBaseModel, + Grunderfassung_Personen, + Grunderfassung_Unternehmen, +) from wce_crm.form_defs import ( INITREC_COMP, INITREC_PERSON, @@ -1351,7 +1356,7 @@ class AutoForm(QWidget): logger_auto_form.debug( "Loaded data dict:\n%s Passing to Pydantic...", pformat(loaded_data) ) - model = Grunderfassung(**loaded_data) + model = self.cfg.model(**loaded_data) logger_auto_form.debug("Loaded to Pydantic.") logger_auto_form.debug("Convert to GUI structure...") form_data = model.to_gui() @@ -2081,35 +2086,33 @@ class NewEntrySelect_view(QWidget): CONFIG_GRUNDERFASSUNG_UNTERNEHMEN: Final[AutoFormConfig] = AutoFormConfig( - model=Grunderfassung, - data_insert=backend.initrec_insert_initial_recording, - data_update=backend.initrec_update_initial_recording, - data_get=backend.initrec_get_initial_recording, - data_delete=backend.initrec_delete_initial_recording, + model=Grunderfassung_Unternehmen, + data_insert=backend.initrec_company_insert_initial_recording, + data_update=backend.initrec_company_update_initial_recording, + data_get=backend.initrec_company_get_initial_recording, + data_delete=backend.initrec_company_delete_initial_recording, ignored_keys=( "Metadaten_erstellung", "Metadaten_aktualisierung", - "Metadaten_wiedereintrittsdatum", ), form_fields=INITREC_COMP, ) CONFIG_GRUNDERFASSUNG_PERSONEN: Final[AutoFormConfig] = AutoFormConfig( - model=Grunderfassung, - data_insert=backend.initrec_insert_initial_recording, - data_update=backend.initrec_update_initial_recording, - data_get=backend.initrec_get_initial_recording, - data_delete=backend.initrec_delete_initial_recording, + model=Grunderfassung_Personen, + data_insert=backend.initrec_person_insert_initial_recording, + data_update=backend.initrec_person_update_initial_recording, + data_get=backend.initrec_person_get_initial_recording, + data_delete=backend.initrec_person_delete_initial_recording, ignored_keys=( "Metadaten_erstellung", "Metadaten_aktualisierung", - "Partnersuche", ), form_fields=INITREC_PERSON, ) CUSTOM_WIDGETS: Final[dict[str, type[CustomWidget]]] = { - "grunderfassung_suche": Grunderfassung_SuchWidget, + "grunderfassung_unternehmen_suche": Grunderfassung_SuchWidget, } @@ -2203,51 +2206,53 @@ class Page_InitRecCompany(QWidget): self.search_widget_trigger = cast( QLineEdit, search_widget.company_widgets["ma_unternehmensname"] ) - text_widget_set = search_widgets_by_key( - self.auto_form.widget_registry, f"Kontaktperson{COLUMN_SEP}KP_name_partner" - ) - assert len(text_widget_set) == 1 - self.text_widget_set = cast(QLineEdit, text_widget_set[0]["widget"]) - self.search_widget_trigger.textChanged.connect( - self._custom_set_company_name_contact_person - ) + # TODO check how this should be done with new architecture + # text_widget_set = search_widgets_by_key( + # self.auto_form.widget_registry, f"Kontaktperson{COLUMN_SEP}KP_name_partner" + # ) + # assert len(text_widget_set) == 1 + # self.text_widget_set = cast(QLineEdit, text_widget_set[0]["widget"]) + # self.search_widget_trigger.textChanged.connect( + # self._custom_set_company_name_contact_person + # ) # ** 'Bundesland' only if 'Inland' selected in 'Stammdaten' - person_location = search_widgets_by_key( - self.auto_form.widget_registry, f"Stammdaten{COLUMN_SEP}aufenthaltsort" - ) - assert len(person_location) == 1 - self.person_location = cast(QComboBox, person_location[0]["widget"]) - assert isinstance(self.person_location, QComboBox) - self.person_location.currentIndexChanged.connect(self._custom_county_selection) + # TODO check removal + # person_location = search_widgets_by_key( + # self.auto_form.widget_registry, f"Stammdaten{COLUMN_SEP}aufenthaltsort" + # ) + # assert len(person_location) == 1 + # self.person_location = cast(QComboBox, person_location[0]["widget"]) + # assert isinstance(self.person_location, QComboBox) + # self.person_location.currentIndexChanged.connect(self._custom_county_selection) - selection_county = search_widgets_by_key( - self.auto_form.widget_registry, f"Stammdaten{COLUMN_SEP}bundesland" - ) - assert len(selection_county) == 1 - self.selection_county = cast(QComboBox, selection_county[0]["widget"]) - assert isinstance(self.selection_county, QComboBox) - self.selection_county.setProperty("styleClass", "stempel") - self.selection_county.setEnabled(False) + # selection_county = search_widgets_by_key( + # self.auto_form.widget_registry, f"Stammdaten{COLUMN_SEP}bundesland" + # ) + # assert len(selection_county) == 1 + # self.selection_county = cast(QComboBox, selection_county[0]["widget"]) + # assert isinstance(self.selection_county, QComboBox) + # self.selection_county.setProperty("styleClass", "stempel") + # self.selection_county.setEnabled(False) - def _custom_set_company_name_contact_person(self, value: str) -> None: - self.text_widget_set.setText(str(value)) + # def _custom_set_company_name_contact_person(self, value: str) -> None: + # self.text_widget_set.setText(str(value)) - def _custom_county_selection(self, idx: int) -> None: - value = self.person_location.itemData(idx) - if value == "Inland": - self.selection_county.setEnabled(True) - self.selection_county.setProperty("styleClass", "") - self.selection_county.style().unpolish(self.selection_county) - self.selection_county.style().polish(self.selection_county) - self.selection_county.update() - else: - self.selection_county.setCurrentIndex(0) - self.selection_county.setEnabled(False) - self.selection_county.setProperty("styleClass", "stempel") - self.selection_county.style().unpolish(self.selection_county) - self.selection_county.style().polish(self.selection_county) - self.selection_county.update() + # def _custom_county_selection(self, idx: int) -> None: + # value = self.person_location.itemData(idx) + # if value == "Inland": + # self.selection_county.setEnabled(True) + # self.selection_county.setProperty("styleClass", "") + # self.selection_county.style().unpolish(self.selection_county) + # self.selection_county.style().polish(self.selection_county) + # self.selection_county.update() + # else: + # self.selection_county.setCurrentIndex(0) + # self.selection_county.setEnabled(False) + # self.selection_county.setProperty("styleClass", "stempel") + # self.selection_county.style().unpolish(self.selection_county) + # self.selection_county.style().polish(self.selection_county) + # self.selection_county.update() def reset_form(self) -> None: self.auto_form.reset_form() @@ -2330,6 +2335,7 @@ class Page_InitRecPerson(QWidget): # --- CUSTOM LOGIC --- # ** fill 'Kontaktperson -> Namen Unternehmen' + # TODO check how partners are selected with new architecture # search_res = search_widgets_by_key(self.auto_form.widget_registry, "Partnersuche") # assert len(search_res) == 1 # search_widget = cast(Grunderfassung_SuchWidget, search_res[0]["widget"]) @@ -2628,7 +2634,7 @@ class Page_CompanyProfile(QWidget): self.current_id = id_ return - init_rec = backend.initrec_get_initial_recording(id_) + init_rec = backend.initrec_company_get_initial_recording(id_) ma_id = cast(ExtMaId, init_rec["Partnersuche__un_suche"]) assert ma_id is not None, "no ma_id obtained" comp_info = backend.initrec_comp_search_get_info(ma_id)