From 416a686877977be7e2280cd49c8a1bf1452b2e00 Mon Sep 17 00:00:00 2001 From: foefl Date: Wed, 22 Jul 2026 16:19:45 +0200 Subject: [PATCH 01/13] data modelling: extension table pattern --- data_model/project_db_schema.aml | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/data_model/project_db_schema.aml b/data_model/project_db_schema.aml index 2738be5..1a716a4 100644 --- a/data_model/project_db_schema.aml +++ b/data_model/project_db_schema.aml @@ -6,22 +6,19 @@ Ext_CRM.Ansprechpartner an_id int pk ma_id int -> Ext_CRM.Master(ma_id) +Unternehmen | Es wird nur die ID für die Kontaktliste referenziert. Alle relevanten Infos werden live aus der Kontaktliste geladen via 'ATTACH'. + ma_id int pk -> Ext_CRM.Master(ma_id) + hinzugefuegt_am "timestamp with time zone" + geloescht_am "timestamp with time zone" nullable -Grunderfassung_Unternehmen - un_id int pk {autoIncrement} - geloescht "timestamp with time zone" nullable - 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) +Grunderfassung_Personen_Zusatzdaten_Unternehmen + erf_id int pk -> Grunderfassung_Personen(erf_id) + Partnersuche__un_suche int -> Ext_CRM.Master(ma_id) + Partnersuche__person_suche int -> Ext_CRM.Ansprechpartner(an_id) Partnersuche__kanal_aufmerksamkeit text nullable - Grunderfassung_Personen - pers_id int pk {autoIncrement} + erf_id int pk {autoIncrement} geloescht "timestamp with time zone" nullable 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)`} -- 2.34.1 From fd25576b0e06d6eade515eafae0ee16ce7fc792b Mon Sep 17 00:00:00 2001 From: foefl Date: Thu, 23 Jul 2026 17:31:38 +0200 Subject: [PATCH 02/13] sketch for new data model --- data_model/project_db_schema.aml | 37 ++++++++++++++++---------------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/data_model/project_db_schema.aml b/data_model/project_db_schema.aml index 1a716a4..8d0e991 100644 --- a/data_model/project_db_schema.aml +++ b/data_model/project_db_schema.aml @@ -1,24 +1,26 @@ -Ext_CRM.Master +Ext_Kontaktliste.Master ma_id int pk - -Ext_CRM.Ansprechpartner +Ext_Kontaktliste.Ansprechpartner an_id int pk - ma_id int -> Ext_CRM.Master(ma_id) + ma_id int -> Ext_Kontaktliste.Master(ma_id) Unternehmen | Es wird nur die ID für die Kontaktliste referenziert. Alle relevanten Infos werden live aus der Kontaktliste geladen via 'ATTACH'. - ma_id int pk -> Ext_CRM.Master(ma_id) + un_id int pk -> Ext_Kontaktliste.Master(ma_id) hinzugefuegt_am "timestamp with time zone" geloescht_am "timestamp with time zone" nullable -Grunderfassung_Personen_Zusatzdaten_Unternehmen - erf_id int pk -> Grunderfassung_Personen(erf_id) - Partnersuche__un_suche int -> Ext_CRM.Master(ma_id) - Partnersuche__person_suche int -> Ext_CRM.Ansprechpartner(an_id) +Grunderfassung_Personen_Unternehmen_Zuordnung + id int pk {autoIncrement} + pers_id int pk -> Grunderfassung_Personen(pers_id) unique=uq_aktive_zuordnung + un_id int -> Unternehmen(un_id) unique=uq_aktive_zuordnung | alias 'Partnersuche__un_suche' + an_id int -> Ext_Kontaktliste.Ansprechpartner(an_id) | alias 'Partnersuche__person_suche Partnersuche__kanal_aufmerksamkeit text nullable + gueltig_ab "timestamp with time zone"=`datetime.datetime.now(datetime.UTC)` + gueltig_bis "timestamp with time zone" nullable | Soft-Delete Marker (partieller Unique-Index: WHERE deleted_at IS NULL) Grunderfassung_Personen - erf_id int pk {autoIncrement} + pers_id int pk {autoIncrement} geloescht "timestamp with time zone" nullable 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)`} @@ -70,17 +72,17 @@ Grunderfassung_Personen 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 "timestamp with time zone"=`datetime.datetime.now(datetime.UTC)` - gueltig_bis "timestamp with time zone" 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 "timestamp with time zone"=`datetime.datetime.now(datetime.UTC)` +# gueltig_bis "timestamp with time zone" 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 + un_id int nullable -> 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 beratungs_typ berat_typ(pauschal, individual) @@ -89,7 +91,6 @@ Beratung_Vorgang | Beratung kann an Unternehmen allein (pauschal), Person allein geloescht "timestamp with time zone" nullable # 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. -- 2.34.1 From 868db09d9438a72b542e5b3bbb907b91850925cb Mon Sep 17 00:00:00 2001 From: foefl Date: Thu, 23 Jul 2026 19:17:49 +0200 Subject: [PATCH 03/13] iteration for new data model --- data_model/project_db_schema.aml | 19 +- prototypes/tests.py | 59 +- src/wce_crm/backend/backend.py | 427 ++++----- src/wce_crm/data_models.py | 78 +- src/wce_crm/db.py | 177 ++-- src/wce_crm/gui.py | 1376 ++++++++++++++++-------------- src/wce_crm/logging.py | 21 +- src/wce_crm/types.py | 164 +++- 8 files changed, 1338 insertions(+), 983 deletions(-) diff --git a/data_model/project_db_schema.aml b/data_model/project_db_schema.aml index 8d0e991..12c8bfc 100644 --- a/data_model/project_db_schema.aml +++ b/data_model/project_db_schema.aml @@ -7,23 +7,24 @@ Ext_Kontaktliste.Ansprechpartner Unternehmen | Es wird nur die ID für die Kontaktliste referenziert. Alle relevanten Infos werden live aus der Kontaktliste geladen via 'ATTACH'. un_id int pk -> Ext_Kontaktliste.Master(ma_id) - hinzugefuegt_am "timestamp with time zone" - geloescht_am "timestamp with time zone" nullable + erstellt "timestamp with time zone" + geloescht "timestamp with time zone" nullable -Grunderfassung_Personen_Unternehmen_Zuordnung +Personen_Unternehmen_Zuordnung id int pk {autoIncrement} pers_id int pk -> Grunderfassung_Personen(pers_id) unique=uq_aktive_zuordnung un_id int -> Unternehmen(un_id) unique=uq_aktive_zuordnung | alias 'Partnersuche__un_suche' an_id int -> Ext_Kontaktliste.Ansprechpartner(an_id) | alias 'Partnersuche__person_suche Partnersuche__kanal_aufmerksamkeit text nullable - gueltig_ab "timestamp with time zone"=`datetime.datetime.now(datetime.UTC)` - gueltig_bis "timestamp with time zone" nullable | Soft-Delete Marker (partieller Unique-Index: WHERE deleted_at IS NULL) + erstellt "timestamp with time zone"=`datetime.datetime.now(datetime.UTC)` + aktualisiert "timestamp with time zone"=`datetime.datetime.now(datetime.UTC)` {on_update: `datetime.datetime.now(datetime.UTC)`} + geloescht "timestamp with time zone" nullable | Soft-Delete Marker (partieller Unique-Index: WHERE geloescht IS NULL) Grunderfassung_Personen pers_id int pk {autoIncrement} - geloescht "timestamp with time zone" nullable - 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)`} + erstellt "timestamp with time zone"=`datetime.datetime.now(datetime.UTC)` + aktualisiert "timestamp with time zone"=`datetime.datetime.now(datetime.UTC)` {on_update: `datetime.datetime.now(datetime.UTC)`} + geloescht "timestamp with time zone" nullable Metadaten_wiedereintrittsdatum timestamp nullable Metadaten_nutzer varchar(20) nullable Grunderfassung_fallnummer text nullable @@ -70,6 +71,8 @@ Grunderfassung_Personen WeitereInfos__WI_deutsch_sprache text nullable WeitereInfos__WI_gueltigkeit_aufenthaltstitel text nullable WeitereInfos__WI_meldung_institution text nullable +# 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)`} # Zuordnung_Personen_Unternehmen | Eine Person kann zu mehreren Unternehmen gehören, ein Unternehmen kann mehrere Personen betreuen. diff --git a/prototypes/tests.py b/prototypes/tests.py index 4e61039..a2cb9a6 100644 --- a/prototypes/tests.py +++ b/prototypes/tests.py @@ -19,9 +19,48 @@ from PySide6.QtCore import QDate, Qt from wce_crm import constants, db from wce_crm.backend import backend +from wce_crm.types import NewEntityIds, RecordingType # %% -int("test") +raw_ent_ids = NewEntityIds() +db_ent_ids = NewEntityIds(un_id=3, an_id=2) + +print(f"{raw_ent_ids=}\n{db_ent_ids=}") +print(f"{raw_ent_ids.empty()=}") +print(f"{raw_ent_ids.valid()=}") +# %% +raw_ent_ids.update(db_ent_ids) +print(f"{raw_ent_ids=}") +db_ent_ids = NewEntityIds(un_id=None, an_id=2, link_id=1) +raw_ent_ids.update(db_ent_ids) +print(f"{raw_ent_ids=}") +print(f"{raw_ent_ids.empty()=}") +print(f"{raw_ent_ids.valid()=}") +raw_ent_ids.update(NewEntityIds()) +print(f"{raw_ent_ids=}") +print(f"{raw_ent_ids.empty()=}") +print(f"{raw_ent_ids.valid()=}") + +# %% +raw_ent_ids.update(NewEntityIds(un_id=None, pers_id=2)) +print(f"{raw_ent_ids=}") +print(f"{raw_ent_ids.empty()=}") +print(f"{raw_ent_ids.empty(RecordingType.COMPANY)=}") +print(f"{raw_ent_ids.empty(RecordingType.PERSON)=}") +print(f"{raw_ent_ids.valid(RecordingType.COMPANY)=}") +print(f"{raw_ent_ids.valid(RecordingType.PERSON)=}") +raw_ent_ids.update(NewEntityIds(un_id=None, an_id=2, link_id=3, pers_id=2)) +print(f"{raw_ent_ids=}") +print(f"{raw_ent_ids.empty(RecordingType.COMPANY)=}") +print(f"{raw_ent_ids.empty(RecordingType.PERSON)=}") +print(f"{raw_ent_ids.valid(RecordingType.COMPANY)=}") +print(f"{raw_ent_ids.valid(RecordingType.PERSON)=}") +raw_ent_ids.update(NewEntityIds(un_id=2, pers_id=None, an_id=2, link_id=3)) +print(f"{raw_ent_ids=}") +print(f"{raw_ent_ids.empty(RecordingType.COMPANY)=}") +print(f"{raw_ent_ids.empty(RecordingType.PERSON)=}") +print(f"{raw_ent_ids.valid(RecordingType.COMPANY)=}") +print(f"{raw_ent_ids.valid(RecordingType.PERSON)=}") # %% db_path = constants.Config.DB_PATH_MAIN @@ -29,22 +68,10 @@ crm_path = constants.Config.DB_PATH_CRM engine = sql.create_engine(f"sqlite:///{str(db_path)}") engine_crm = sql.create_engine(f"sqlite:///{str(crm_path)}") # %% -stmt = sql.select( - db.grunderfassung.c.erfassung_id, - db.grunderfassung.c.Partnersuche__un_suche, -) - -with engine.connect() as conn: - res = conn.execute(stmt) - - -for r in res: - print(r) -# %% # %% -backend._main_page_get_company_list() +backend._main_page_get_company_list() # type: ignore # %% @@ -218,12 +245,12 @@ class FlatBaseModel(BaseModel): return final_nested_data - 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]: + 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) diff --git a/src/wce_crm/backend/backend.py b/src/wce_crm/backend/backend.py index f7e5ee0..5ee9f50 100644 --- a/src/wce_crm/backend/backend.py +++ b/src/wce_crm/backend/backend.py @@ -24,11 +24,11 @@ from wce_crm.types import ( CompanyProfileConsultations, ConsultingType, ContactPersonInfo, - EntityIds, - EntityType, LinkingConsultationsEntryCompany, LinkingConsultationsEntryPerson, MainPageEntry, + NewEntityIds, + RecordingType, ) if TYPE_CHECKING: @@ -152,200 +152,210 @@ def initrec_comp_contact_person_search_get_info( # conn.execute(stmt, data) -def initrec_company_to_db( - auto_form_data: InitRec, -) -> InitRec: - logger.debug("[AutoForm -- backend] Call database saving routine...") +# def initrec_company_to_db( +# auto_form_data: InitRec, +# ) -> InitRec: +# logger.debug("[AutoForm -- backend] Call database saving routine...") - dump_data = copy.deepcopy(auto_form_data.db_data) - dump_data["geloescht"] = auto_form_data.geloescht +# dump_data = copy.deepcopy(auto_form_data.db_data) +# dump_data["geloescht"] = auto_form_data.geloescht - with db.ENGINE.begin() as conn: - if auto_form_data.ids.un_id is None: - logger.debug("[AutoForm -- backend] Insert...") +# with db.ENGINE.begin() as conn: +# if auto_form_data.ids.un_id is None: +# logger.debug("[AutoForm -- backend] Insert...") - stmt = db.grunderfassung_unternehmen.insert().returning( - db.grunderfassung_unternehmen.c.un_id, - db.grunderfassung_unternehmen.c.Metadaten_aktualisierung, - db.grunderfassung_unternehmen.c.geloescht, - ) - ret = conn.execute(stmt, dump_data) +# stmt = db.grunderfassung_unternehmen.insert().returning( +# db.grunderfassung_unternehmen.c.un_id, +# db.grunderfassung_unternehmen.c.Metadaten_aktualisierung, +# db.grunderfassung_unternehmen.c.geloescht, +# ) +# ret = conn.execute(stmt, dump_data) - from_db = ret.mappings().fetchall() - assert len(from_db) == 1, "expected excactly one returned row" - data_from_db = from_db[0] - auto_form_data.ids.un_id = data_from_db["un_id"] - auto_form_data.geloescht = data_from_db["geloescht"] - auto_form_data.db_data["Metadaten_aktualisierung"] = data_from_db[ - "Metadaten_aktualisierung" - ] +# from_db = ret.mappings().fetchall() +# assert len(from_db) == 1, "expected excactly one returned row" +# data_from_db = from_db[0] +# auto_form_data.ids.un_id = data_from_db["un_id"] +# auto_form_data.geloescht = data_from_db["geloescht"] +# auto_form_data.db_data["Metadaten_aktualisierung"] = data_from_db[ +# "Metadaten_aktualisierung" +# ] - logger.debug("[AutoForm -- backend] Inserted InitRec Company successfully") +# logger.debug("[AutoForm -- backend] Inserted InitRec Company successfully") - else: - logger.debug("[AutoForm -- backend] Update...") - stmt = ( - db.grunderfassung_unternehmen.update() - .where(db.grunderfassung_unternehmen.c.un_id == auto_form_data.ids.un_id) - .returning( - db.grunderfassung_unternehmen.c.Metadaten_aktualisierung, - db.grunderfassung_unternehmen.c.geloescht, - ) - ) - ret = conn.execute(stmt, dump_data) +# else: +# logger.debug("[AutoForm -- backend] Update...") +# stmt = ( +# db.grunderfassung_unternehmen.update() +# .where(db.grunderfassung_unternehmen.c.un_id == auto_form_data.ids.un_id) +# .returning( +# db.grunderfassung_unternehmen.c.Metadaten_aktualisierung, +# db.grunderfassung_unternehmen.c.geloescht, +# ) +# ) +# ret = conn.execute(stmt, dump_data) - from_db = ret.mappings().fetchall() - assert len(from_db) == 1, "expected excatly one returned row" - data_from_db = from_db[0] +# from_db = ret.mappings().fetchall() +# assert len(from_db) == 1, "expected excatly one returned row" +# data_from_db = from_db[0] - auto_form_data.db_data["Metadaten_aktualisierung"] = data_from_db[ - "Metadaten_aktualisierung" - ] +# auto_form_data.db_data["Metadaten_aktualisierung"] = data_from_db[ +# "Metadaten_aktualisierung" +# ] - logger.debug( - "[AutoForm -- backend] Updated InitRec Company with ID %d successfully", - auto_form_data.ids.un_id, - ) +# logger.debug( +# "[AutoForm -- backend] Updated InitRec Company with ID %d successfully", +# auto_form_data.ids.un_id, +# ) - return auto_form_data +# return auto_form_data -def initrec_company_from_db( - id_: RecId, -) -> InitRec: - logger.debug("[AutoForm -- backend] Call database loading routine...") - stmt = db.grunderfassung_unternehmen.select().where( - db.grunderfassung_unternehmen.c.un_id == id_ - ) - with db.ENGINE.connect() as conn: - ret = conn.execute(stmt) +# def initrec_company_from_db( +# id_: RecId, +# ) -> InitRec: +# logger.debug("[AutoForm -- backend] Call database loading routine...") +# stmt = db.grunderfassung_unternehmen.select().where( +# db.grunderfassung_unternehmen.c.un_id == id_ +# ) +# with db.ENGINE.connect() as conn: +# ret = conn.execute(stmt) - from_db = ret.mappings().all() - assert len(from_db) == 1, "not excactly one company initial recording obtained" - data_from_db = dict(from_db[0]) +# from_db = ret.mappings().all() +# assert len(from_db) == 1, "not excactly one company initial recording obtained" +# data_from_db = dict(from_db[0]) - geloescht = data_from_db["geloescht"] - del data_from_db["geloescht"] +# geloescht = data_from_db["geloescht"] +# del data_from_db["geloescht"] - return InitRec( - ids=EntityIds(un_id=id_), - # rec_id=id_, # TODO remove - geloescht=geloescht, - db_data=data_from_db, - ) +# return InitRec( +# ids=EntityIds(un_id=id_), +# # rec_id=id_, # TODO remove +# geloescht=geloescht, +# db_data=data_from_db, +# ) -def initrec_person_to_db( - auto_form_data: InitRec, -) -> InitRec: - logger.debug("[AutoForm -- backend] Call database saving routine...") +# def initrec_person_to_db( +# auto_form_data: InitRec, +# ) -> InitRec: +# logger.debug("[AutoForm -- backend] Call database saving routine...") - dump_data = copy.deepcopy(auto_form_data.db_data) - dump_data["geloescht"] = auto_form_data.geloescht +# dump_data = copy.deepcopy(auto_form_data.db_data) +# dump_data["geloescht"] = auto_form_data.geloescht - with db.ENGINE.begin() as conn: - if auto_form_data.ids.pers_id is None: - logger.debug("[AutoForm -- backend] Insert...") +# with db.ENGINE.begin() as conn: +# if auto_form_data.ids.pers_id is None: +# logger.debug("[AutoForm -- backend] Insert...") - stmt = sa.insert(db.grunderfassung_personen).returning( - db.grunderfassung_personen.c.pers_id, - db.grunderfassung_personen.c.Metadaten_aktualisierung, - db.grunderfassung_personen.c.geloescht, - ) - stmt_compiled = str(stmt.compile(db.ENGINE)) - logger.debug( - "[AutoForm -- backend] Data to insert:\nStatement: %s\n%s", - stmt_compiled, - pformat(dump_data), - ) +# stmt = sa.insert(db.t_grunderfassung_personen).returning( +# db.t_grunderfassung_personen.c.pers_id, +# db.t_grunderfassung_personen.c.Metadaten_aktualisierung, +# db.t_grunderfassung_personen.c.geloescht, +# ) +# stmt_compiled = str(stmt.compile(db.ENGINE)) +# logger.debug( +# "[AutoForm -- backend] Data to insert:\nStatement: %s\n%s", +# stmt_compiled, +# pformat(dump_data), +# ) - ret = conn.execute(stmt, dump_data) +# ret = conn.execute(stmt, dump_data) - from_db = ret.mappings().fetchall() - assert len(from_db) == 1, "expected excactly one returned row" - data_from_db = from_db[0] - auto_form_data.ids.pers_id = data_from_db["pers_id"] - auto_form_data.geloescht = data_from_db["geloescht"] - auto_form_data.db_data["Metadaten_aktualisierung"] = data_from_db[ - "Metadaten_aktualisierung" - ] +# from_db = ret.mappings().fetchall() +# assert len(from_db) == 1, "expected excactly one returned row" +# data_from_db = from_db[0] +# auto_form_data.ids.pers_id = data_from_db["pers_id"] +# auto_form_data.geloescht = data_from_db["geloescht"] +# auto_form_data.db_data["Metadaten_aktualisierung"] = data_from_db[ +# "Metadaten_aktualisierung" +# ] - logger.debug("[AutoForm -- backend] Inserted InitRec Person successfully") +# logger.debug("[AutoForm -- backend] Inserted InitRec Person successfully") - else: - logger.debug("[AutoForm -- backend] Update...") - stmt = ( - db.grunderfassung_personen.update() - .where(db.grunderfassung_personen.c.pers_id == auto_form_data.ids.pers_id) - .returning( - db.grunderfassung_personen.c.Metadaten_aktualisierung, - db.grunderfassung_personen.c.geloescht, - ) - ) - ret = conn.execute(stmt, dump_data) +# else: +# logger.debug("[AutoForm -- backend] Update...") +# stmt = ( +# db.t_grunderfassung_personen.update() +# .where(db.t_grunderfassung_personen.c.pers_id == auto_form_data.ids.pers_id) +# .returning( +# db.t_grunderfassung_personen.c.Metadaten_aktualisierung, +# db.t_grunderfassung_personen.c.geloescht, +# ) +# ) +# ret = conn.execute(stmt, dump_data) - from_db = ret.mappings().fetchall() - assert len(from_db) == 1, "expected excatly one returned row" - data_from_db = from_db[0] +# from_db = ret.mappings().fetchall() +# assert len(from_db) == 1, "expected excatly one returned row" +# data_from_db = from_db[0] - auto_form_data.db_data["Metadaten_aktualisierung"] = data_from_db[ - "Metadaten_aktualisierung" - ] +# auto_form_data.db_data["Metadaten_aktualisierung"] = data_from_db[ +# "Metadaten_aktualisierung" +# ] - logger.debug( - "[AutoForm -- backend] Updated InitRec Person with ID %d successfully", - auto_form_data.ids.pers_id, - ) +# logger.debug( +# "[AutoForm -- backend] Updated InitRec Person with ID %d successfully", +# auto_form_data.ids.pers_id, +# ) - return auto_form_data +# return auto_form_data -def initrec_person_from_db( - id_: RecId, -) -> InitRec: - logger.debug("[AutoForm -- backend] Call database loading routine...") - stmt = db.grunderfassung_personen.select().where( - db.grunderfassung_personen.c.pers_id == id_ - ) - with db.ENGINE.connect() as conn: - ret = conn.execute(stmt) +# def initrec_person_from_db( +# id_: RecId, +# ) -> InitRec: +# logger.debug("[AutoForm -- backend] Call database loading routine...") +# stmt = db.t_grunderfassung_personen.select().where( +# db.t_grunderfassung_personen.c.pers_id == id_ +# ) +# with db.ENGINE.connect() as conn: +# ret = conn.execute(stmt) - from_db = ret.mappings().all() - assert len(from_db) == 1, "not excactly one company initial recording obtained" - data_from_db = dict(from_db[0]) +# from_db = ret.mappings().all() +# assert len(from_db) == 1, "not excactly one company initial recording obtained" +# data_from_db = dict(from_db[0]) - geloescht = data_from_db["geloescht"] - del data_from_db["geloescht"] +# geloescht = data_from_db["geloescht"] +# del data_from_db["geloescht"] - return InitRec( - ids=EntityIds(pers_id=id_), - # rec_id=id_, # TODO remove - geloescht=geloescht, - db_data=data_from_db, - ) +# return InitRec( +# ids=EntityIds(pers_id=id_), +# # rec_id=id_, # TODO remove +# geloescht=geloescht, +# db_data=data_from_db, +# ) # TODO check removal -def initrec_company_get_initial_recording( - id_: RecId, -) -> dict[str, Any]: - logger.debug("[Call backend] get_initial_recording") - stmt = db.grunderfassung_unternehmen.select().where( - db.grunderfassung_unternehmen.c.un_id == id_ - ) - with db.ENGINE.connect() as conn: - ret = conn.execute(stmt) +# def initrec_company_get_initial_recording( +# id_: RecId, +# ) -> dict[str, Any]: +# logger.debug("[Call backend] get_initial_recording") +# stmt = db.grunderfassung_unternehmen.select().where( +# db.grunderfassung_unternehmen.c.un_id == id_ +# ) +# with db.ENGINE.connect() as conn: +# ret = conn.execute(stmt) - results = ret.mappings().all() - if not results: - raise KeyError(f"Database ID {id_} not found") +# results = ret.mappings().all() +# if not results: +# raise KeyError(f"Database ID {id_} not found") - assert len(results) == 1, "more than one company initial recording obtained" +# assert len(results) == 1, "more than one company initial recording obtained" - row = results[0] - assert row, "row was not obtained" +# row = results[0] +# assert row, "row was not obtained" - return dict(row) +# return dict(row) + + +def page_initrec_to_db( + data: InitRec, +) -> InitRec: ... + + +def page_initrec_from_db( + ids: NewEntityIds, +) -> InitRec: ... # def initrec_company_delete_initial_recording( @@ -444,7 +454,7 @@ def page_consulting_to_db( pformat(insert_data_process), ) - stmt = db.beratung_vorgang.insert() + stmt = db.t_beratung_vorgang.insert() ret = conn.execute(stmt, insert_data_process) if ret.rowcount == 0: raise IOError("Entry was not inserted correctly") @@ -460,8 +470,8 @@ def page_consulting_to_db( consultation_data.vorgang_id, ) update_data_process = consultation_data.model_dump(exclude={"beratungen"}) - stmt = db.beratung_vorgang.update().where( - db.beratung_vorgang.c.vorgang_id == consultation_data.vorgang_id + stmt = db.t_beratung_vorgang.update().where( + db.t_beratung_vorgang.c.vorgang_id == consultation_data.vorgang_id ) conn.execute(stmt, update_data_process) @@ -487,8 +497,8 @@ def page_consulting_to_db( logger.debug( "[Consulting Page] Call update for sessions:\n%s", pformat(rows_for_db_update) ) - stmt = db.beratung_einzelberatung.update().where( - db.beratung_einzelberatung.c.beratung_id == sa.bindparam("b_beratung_id") + stmt = db.t_beratung_einzelberatung.update().where( + db.t_beratung_einzelberatung.c.beratung_id == sa.bindparam("b_beratung_id") ) conn.execute(stmt, rows_for_db_update) @@ -497,8 +507,8 @@ def page_consulting_to_db( logger.debug( "[Consulting Page] Call insert for sessions:\n%s", pformat(rows_for_db_insert) ) - stmt = db.beratung_einzelberatung.insert().returning( - db.beratung_einzelberatung.c.beratung_id + stmt = db.t_beratung_einzelberatung.insert().returning( + db.t_beratung_einzelberatung.c.beratung_id ) res = conn.execute(stmt, rows_for_db_insert) new_cons_session_ids = cast(list[int], [row[0] for row in res.fetchall()]) @@ -518,12 +528,14 @@ def page_consulting_delete( dump_data: dict[str, Any] = {"geloescht": timestamp_deleted} with db.ENGINE.begin() as conn: - stmt = db.beratung_vorgang.update().where(db.beratung_vorgang.c.vorgang_id == cons_id) + stmt = db.t_beratung_vorgang.update().where( + db.t_beratung_vorgang.c.vorgang_id == cons_id + ) conn.execute(stmt, dump_data) - stmt = db.beratung_einzelberatung.update().where( - db.beratung_einzelberatung.c.vorgang_id == cons_id, - db.beratung_einzelberatung.c.geloescht.is_(None), + stmt = db.t_beratung_einzelberatung.update().where( + db.t_beratung_einzelberatung.c.vorgang_id == cons_id, + db.t_beratung_einzelberatung.c.geloescht.is_(None), ) conn.execute(stmt, dump_data) @@ -537,7 +549,9 @@ def page_consulting_from_db( with db.ENGINE.connect() as conn: # get the consultation process - stmt = db.beratung_vorgang.select().where(db.beratung_vorgang.c.vorgang_id == cons_id) + stmt = db.t_beratung_vorgang.select().where( + db.t_beratung_vorgang.c.vorgang_id == cons_id + ) ret = conn.execute(stmt) results = ret.mappings().all() @@ -570,9 +584,9 @@ def page_consulting_from_db( # get all consultation sessions of this process, only the ones # which are not marked as deleted - stmt = db.beratung_einzelberatung.select().where( - db.beratung_einzelberatung.c.vorgang_id == cons_id, - db.beratung_einzelberatung.c.geloescht.is_(None), + stmt = db.t_beratung_einzelberatung.select().where( + db.t_beratung_einzelberatung.c.vorgang_id == cons_id, + db.t_beratung_einzelberatung.c.geloescht.is_(None), ) ret = conn.execute(stmt) cons_sessions = ret.mappings().all() @@ -604,9 +618,12 @@ def page_consulting_from_db( # // consulting page interaction +# TODO change to correct logic def page_consulting_linking_companies( un_id: RecId | None, ) -> list[LinkingConsultationsEntryCompany]: + raise RuntimeError("TODOs to implement") + with db.ENGINE.connect() as conn: q_relevant_companies = sa.select( db.grunderfassung_unternehmen.c.un_id, @@ -624,12 +641,12 @@ def page_consulting_linking_companies( stmt = sa.select( q_relevant_companies.c.un_id, - db.ext_crm_master_attach.c.ma_unternehmensname, + db.t_ext_crm_master_attach.c.ma_unternehmensname, ).select_from( q_relevant_companies.join( - db.ext_crm_master_attach, + db.t_ext_crm_master_attach, q_relevant_companies.c.Partnersuche__un_suche - == db.ext_crm_master_attach.c.ma_id, + == db.t_ext_crm_master_attach.c.ma_id, ) ) @@ -660,20 +677,20 @@ def page_consulting_linking_persons( # else: list just the persons associated with the company person_select = sa.select( - db.grunderfassung_personen.c.pers_id, - db.grunderfassung_personen.c.Stammdaten__name, - db.grunderfassung_personen.c.Stammdaten__vorname, - db.grunderfassung_personen.c.Stammdaten__ort, + db.t_grunderfassung_personen.c.pers_id, + db.t_grunderfassung_personen.c.Stammdaten__name, + db.t_grunderfassung_personen.c.Stammdaten__vorname, + db.t_grunderfassung_personen.c.Stammdaten__ort, ) if un_id is not None: person_select = person_select.select_from( - db.grunderfassung_personen.join( - db.zuordnung_personen_unternehmen, - db.grunderfassung_personen.c.pers_id - == db.zuordnung_personen_unternehmen.c.pers_id, + db.t_grunderfassung_personen.join( + db.t_zuordnung_personen_unternehmen, + db.t_grunderfassung_personen.c.pers_id + == db.t_zuordnung_personen_unternehmen.c.pers_id, ) - ).where(db.zuordnung_personen_unternehmen.c.un_id == un_id) + ).where(db.t_zuordnung_personen_unternehmen.c.un_id == un_id) ret = conn.execute(person_select) person_names = ret.mappings().all() @@ -711,8 +728,8 @@ def page_consulting_eastablish_link( ) -> None: stmt = ( - sa.update(db.beratung_vorgang) - .where(db.beratung_vorgang.c.vorgang_id == vorgang_id) + sa.update(db.t_beratung_vorgang) + .where(db.t_beratung_vorgang.c.vorgang_id == vorgang_id) .values(un_id=un_id, pers_id=pers_id) ) @@ -725,11 +742,13 @@ def companyprofile_page_get_consultations( ) -> CompanyProfileConsultations: logger.debug("[Call backend] _companyprofile_page_get_consultations") stmt = sa.select( - db.beratung_vorgang.c.vorgang_id, - db.beratung_vorgang.c.aktualisiert, - db.beratung_vorgang.c.titel, - db.beratung_vorgang.c.beratungs_typ, - ).where(db.beratung_vorgang.c.un_id == un_id, db.beratung_vorgang.c.geloescht.is_(None)) + db.t_beratung_vorgang.c.vorgang_id, + db.t_beratung_vorgang.c.aktualisiert, + db.t_beratung_vorgang.c.titel, + db.t_beratung_vorgang.c.beratungs_typ, + ).where( + db.t_beratung_vorgang.c.un_id == un_id, db.t_beratung_vorgang.c.geloescht.is_(None) + ) with db.ENGINE.connect() as conn: res = conn.execute(stmt) @@ -782,17 +801,17 @@ def initrec_link_person_company( if unlink: deleted_ts = datetime.datetime.now(tz=datetime.UTC) stmt = ( - sa.update(db.zuordnung_personen_unternehmen) + sa.update(db.t_zuordnung_personen_unternehmen) .where( - db.zuordnung_personen_unternehmen.c.un_id == un_id, - db.zuordnung_personen_unternehmen.c.pers_id == pers_id, - db.zuordnung_personen_unternehmen.c.gueltig_bis.is_(None), + db.t_zuordnung_personen_unternehmen.c.un_id == un_id, + db.t_zuordnung_personen_unternehmen.c.pers_id == pers_id, + db.t_zuordnung_personen_unternehmen.c.gueltig_bis.is_(None), ) .values(gueltig_bis=deleted_ts) ) else: stmt = ( - sqlite_insert(db.zuordnung_personen_unternehmen) + sqlite_insert(db.t_zuordnung_personen_unternehmen) .values(un_id=un_id, pers_id=pers_id) .on_conflict_do_nothing( index_elements=["un_id", "pers_id"], @@ -805,8 +824,12 @@ def initrec_link_person_company( # // main page interaction +# TODO change to correct logic def _main_page_get_company_list() -> list[MainPageEntry]: logger.debug("[Call backend] get_company_list") + + raise RuntimeError("TODOs to implement") + stmt = sa.select( db.grunderfassung_unternehmen.c.un_id, db.grunderfassung_unternehmen.c.Partnersuche__un_suche, @@ -834,7 +857,7 @@ def _main_page_get_company_list() -> list[MainPageEntry]: rec_id=rec_id, display_name=display_name, Metadaten_aktualisierung=datetime_akt, - type=EntityType.COMPANY, + type=RecordingType.COMPANY, ) ) @@ -844,11 +867,11 @@ def _main_page_get_company_list() -> list[MainPageEntry]: def _main_page_get_person_list() -> list[MainPageEntry]: logger.debug("[Call backend] get_company_list") stmt = sa.select( - db.grunderfassung_personen.c.pers_id, - db.grunderfassung_personen.c.Stammdaten__vorname, - db.grunderfassung_personen.c.Stammdaten__name, - db.grunderfassung_personen.c.Metadaten_aktualisierung, - ).where(db.grunderfassung_personen.c.geloescht.is_(None)) + db.t_grunderfassung_personen.c.pers_id, + db.t_grunderfassung_personen.c.Stammdaten__vorname, + db.t_grunderfassung_personen.c.Stammdaten__name, + db.t_grunderfassung_personen.c.Metadaten_aktualisierung, + ).where(db.t_grunderfassung_personen.c.geloescht.is_(None)) with db.ENGINE.connect() as conn: res = conn.execute(stmt) @@ -871,7 +894,7 @@ def _main_page_get_person_list() -> list[MainPageEntry]: rec_id=rec_id, display_name=display_name, Metadaten_aktualisierung=datetime_akt, - type=EntityType.PERSON, + type=RecordingType.PERSON, ) ) diff --git a/src/wce_crm/data_models.py b/src/wce_crm/data_models.py index 72a6c8a..83c83bb 100644 --- a/src/wce_crm/data_models.py +++ b/src/wce_crm/data_models.py @@ -19,11 +19,11 @@ from pydantic import ( from pydantic_core import ErrorDetails from wce_crm.form_defs import FormField -from wce_crm.types import EntityIds +from wce_crm.types import NewEntityIds if TYPE_CHECKING: - from wce_crm.gui import Page_Consulting_ConsultingSession # noqa: F401 - from wce_crm.types import ConsId, ConsultingType, EntityType, RecId, UserId + from wce_crm.gui import InitRecForm, Page_Consulting_ConsultingSession # noqa: F401 + from wce_crm.types import ConsId, ConsultingType, RecId, RecordingType, UserId ValidAge = Annotated[int, Field(ge=0, le=99)] COLUMN_SEP: Final[str] = "__" @@ -163,7 +163,7 @@ class InitRecFormToDb(Protocol): class InitRecFormFromDb(Protocol): def __call__( self, - id_: int, + ids: NewEntityIds, ) -> InitRec: ... @@ -177,7 +177,7 @@ def set_page_state( transfer all field values generically from 'source' to 'target' """ if not (dc.is_dataclass(target) and dc.is_dataclass(source)): - raise TypeError("Both source and atrget must be dataclasses") + raise TypeError("Both source and target must be dataclasses") if type(target) is not type(source): raise TypeError("Both source and target must have the same type") @@ -212,47 +212,61 @@ class Page_MainPage_State(PageState[Module]): @dc.dataclass(slots=True, kw_only=True) -class Page_NewInitRec_State(PageState[Module]): +class Page_NewEntry_State(PageState[Module]): session: Session +# !! there must be exactly one InitRecForm for each recording type @dc.dataclass(slots=True, kw_only=True) -class InitRecFormConfig: - model: type[FlatBaseModel] - to_db: InitRecFormToDb - from_db: InitRecFormFromDb +class Page_InitRec_State(PageState["InitRecForm"]): + session: Session + ids: NewEntityIds = dc.field(default_factory=NewEntityIds) + record_type: RecordingType + # un_id: RecId | None = None + locked: bool + geloescht: bool = False + form_data: dict[RecordingType, dict[str, Any]] = dc.field(default_factory=dict) + pydantic_models: dict[RecordingType, type[FlatBaseModel]] = dc.field(default_factory=dict) + + +@dc.dataclass(slots=True, kw_only=True) +class Page_InitRec_Form_Config: + pydantic_model: type[FlatBaseModel] + # to_db: InitRecFormToDb # TODO remove + # from_db: InitRecFormFromDb # TODO remove form_fields: Sequence[FormField] ignored_keys: Iterable[str] = tuple() - add_buttons: bool = True + add_buttons: bool = False @dc.dataclass(slots=True, kw_only=True) -class InitRecForm_State(PageState[Module]): +class Page_InitRec_Form_State(PageState[Module]): session: Session - cfg: InitRecFormConfig - ids: EntityIds - ent_type: EntityType + cfg: Page_InitRec_Form_Config + ids: NewEntityIds + recording_type: RecordingType # rec_id: RecId | None = None - form_data: dict[str, Any] | None = None + form_data: dict[str, Any] = dc.field(default_factory=dict) locked: bool geloescht: bool = False -@dc.dataclass(slots=True, kw_only=True) -class Page_InitRecCompany_State(PageState[Module]): - session: Session - ids: EntityIds - # un_id: RecId | None = None - locked: bool +# TODO remove +# @dc.dataclass(slots=True, kw_only=True) +# class Page_InitRecCompany_State(PageState[Module]): +# session: Session +# ids: EntityIds +# # un_id: RecId | None = None +# locked: bool -@dc.dataclass(slots=True, kw_only=True) -class Page_InitRecPerson_State(PageState[Module]): - session: Session - ids: EntityIds - # pers_id: RecId | None = None - # un_id: RecId | None = None - locked: bool +# @dc.dataclass(slots=True, kw_only=True) +# class Page_InitRecPerson_State(PageState[Module]): +# session: Session +# ids: EntityIds +# # pers_id: RecId | None = None +# # un_id: RecId | None = None +# locked: bool # TODO change to entity IDs @@ -308,7 +322,7 @@ class Page_Consulting_Table_State(PageState["Page_Consulting_ConsultingSession"] @dc.dataclass(slots=True, kw_only=True) class Page_CompanyProfile_State(PageState[Module]): session: Session - ids: EntityIds + ids: NewEntityIds # rec_id: RecId | None = None @@ -427,10 +441,10 @@ class FlatBaseModel(BaseModel): # ** InitRec class InitRec(BaseModel): - ids: EntityIds + ids: NewEntityIds # rec_id: RecId | None geloescht: AwareDatetime | None = None - db_data: dict[str, Any] + db_data: dict[RecordingType, dict[str, Any]] class Grunderfassung_Unternehmen(FlatBaseModel): diff --git a/src/wce_crm/db.py b/src/wce_crm/db.py index af0734e..1c6b883 100644 --- a/src/wce_crm/db.py +++ b/src/wce_crm/db.py @@ -255,7 +255,7 @@ 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) -ext_crm_master_attach: Table = Table( +t_ext_crm_master_attach: Table = Table( "Master", MD_CRM, Column("ma_id", sa.Integer, nullable=False, unique=True), @@ -285,9 +285,71 @@ ext_crm_master_attach: Table = Table( schema="kontaktliste", ) +t_ext_crm_contact_person_attach: Table = Table( + "Ansprechpartner", + MD_CRM, + Column("an_id", sa.Integer, nullable=False, unique=True), + Column("ma_id", sa.ForeignKey("Master.ma_id")), + Column("wce_id", sa.ForeignKey("Nutzer.wce_id")), + Column("st_id", sa.Integer, nullable=False), + Column("an_sachgebiet", sa.Text, nullable=True), + Column("an_anrede", sa.Text, nullable=True), + Column("an_titel", sa.Text, nullable=True), + Column("an_nachname", sa.Text, nullable=True), + Column("an_vorname", sa.Text, nullable=True), + Column("an_position", sa.Text, nullable=True), + Column("an_mail", sa.Text, nullable=True), + Column("an_festnetz", sa.Text, nullable=True), + Column("an_mobil", sa.Text, nullable=True), + Column("an_faxnummer", sa.Text, nullable=True), + Column("an_hauptansprechpartner", sa.Text, nullable=True), + Column("an_anrede_anschrift", sa.Text, nullable=True), + Column("an_bemerkung", sa.Text, nullable=True), + Column("an_aktualisierung_datum", SafeDateTime, nullable=True), + Column("an_aktualisierung_nutzer", sa.Text, nullable=True), + Column("an_letztes_kontaktdatum", SafeDateTime, nullable=True), + Column("an_ersteintrag_datum", SafeDateTime, nullable=True), + Column("an_archiviert", sa.Boolean, nullable=True, default=0), + schema="kontaktliste", +) -grunderfassung_unternehmen: Table = Table( - "grunderfassung_unternehmen", +# grunderfassung_unternehmen: Table = Table( +# "grunderfassung_unternehmen", +# MD_MAIN, +# Column( +# "un_id", +# sa.Integer, +# primary_key=True, +# autoincrement=True, +# ), +# Column( +# "geloescht", +# UTCDateTime, +# nullable=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", sa.String(20), nullable=True), +# Column("Grunderfassung_fallnummer", sa.Text, nullable=True), +# Column("Grunderfassung_notiz", sa.Text, nullable=True), +# Column("Partnersuche__kanal_aufmerksamkeit", sa.Text, nullable=True, default=None), +# Column("Partnersuche__person_suche", sa.Integer, nullable=True, default=None), +# Column("Partnersuche__un_suche", sa.Integer, nullable=True, default=None), +# ) + +t_unternehmen: Table = Table( + "Unternehmen", MD_MAIN, Column( "un_id", @@ -295,34 +357,21 @@ grunderfassung_unternehmen: Table = Table( primary_key=True, autoincrement=True, ), + Column( + "erstellt", + UTCDateTime, + nullable=False, + default=lambda: datetime.datetime.now(datetime.UTC), + ), Column( "geloescht", UTCDateTime, nullable=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", sa.String(20), nullable=True), - Column("Grunderfassung_fallnummer", sa.Text, nullable=True), - Column("Grunderfassung_notiz", sa.Text, nullable=True), - Column("Partnersuche__kanal_aufmerksamkeit", sa.Text, nullable=True, default=None), - Column("Partnersuche__person_suche", sa.Integer, nullable=True, default=None), - Column("Partnersuche__un_suche", sa.Integer, nullable=True, default=None), ) -grunderfassung_personen: Table = Table( - "grunderfassung_personen", +t_grunderfassung_personen: Table = Table( + "Grunderfassung_Personen", MD_MAIN, Column( "pers_id", @@ -330,24 +379,37 @@ grunderfassung_personen: Table = Table( primary_key=True, autoincrement=True, ), + Column( + "erstellt", + UTCDateTime, + nullable=False, + default=lambda: datetime.datetime.now(datetime.UTC), + ), + Column( + "aktualisiert", + UTCDateTime, + nullable=False, + default=lambda: datetime.datetime.now(datetime.UTC), + onupdate=lambda: datetime.datetime.now(datetime.UTC), + ), Column( "geloescht", UTCDateTime, nullable=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_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", sa.String(20), nullable=True), Column("Metadaten_wiedereintrittsdatum", sa.Date, nullable=True, default=None), Column("Arbeitserfahrung", sa.Text, nullable=True), @@ -398,8 +460,8 @@ grunderfassung_personen: Table = Table( Column("WeitereInfos__WI_meldung_institution", sa.Text, nullable=True), ) -zuordnung_personen_unternehmen: Table = Table( - "zuordnung_personen_unternehmen", +t_zuordnung_personen_unternehmen: Table = Table( + "Personen_Unternehmen_Zuordnung", MD_MAIN, Column( "id", @@ -407,31 +469,50 @@ zuordnung_personen_unternehmen: Table = Table( primary_key=True, autoincrement=True, ), - Column("un_id", sa.ForeignKey("grunderfassung_unternehmen.un_id"), nullable=False), - Column("pers_id", sa.ForeignKey("grunderfassung_personen.pers_id"), nullable=False), + Column("un_id", sa.ForeignKey("Unternehmen.un_id"), nullable=False), + Column("an_id", sa.Integer, nullable=False), + Column("pers_id", sa.ForeignKey("Grunderfassung_Personen.pers_id"), nullable=False), Column( - "gueltig_ab", + "erstellt", UTCDateTime, nullable=False, default=lambda: datetime.datetime.now(datetime.UTC), ), Column( - "gueltig_bis", + "aktualisiert", + UTCDateTime, + nullable=False, + default=lambda: datetime.datetime.now(datetime.UTC), + onupdate=lambda: datetime.datetime.now(datetime.UTC), + ), + Column( + "geloescht", UTCDateTime, nullable=True, ), + # Column( + # "gueltig_ab", + # UTCDateTime, + # nullable=False, + # default=lambda: datetime.datetime.now(datetime.UTC), + # ), + # Column( + # "gueltig_bis", + # UTCDateTime, + # nullable=True, + # ), Index( "uq_aktive_zuordnung", "un_id", "pers_id", unique=True, - sqlite_where=sql.text("gueltig_bis IS NULL"), + sqlite_where=sql.text("geloescht IS NULL"), ), ) -beratung_vorgang: Table = Table( - "beratung_vorgang", +t_beratung_vorgang: Table = Table( + "Beratung_Vorgang", MD_MAIN, Column( "vorgang_id", @@ -464,8 +545,8 @@ beratung_vorgang: Table = Table( ) -beratung_einzelberatung: Table = Table( - "beratung_einzelberatung", +t_beratung_einzelberatung: Table = Table( + "Beratung_Einzelberatung", MD_MAIN, Column( "beratung_id", diff --git a/src/wce_crm/gui.py b/src/wce_crm/gui.py index 878f6ea..9c82681 100644 --- a/src/wce_crm/gui.py +++ b/src/wce_crm/gui.py @@ -74,6 +74,7 @@ from PySide6.QtWidgets import ( QStackedWidget, QTableWidget, QTableWidgetItem, + QToolButton, QVBoxLayout, QWidget, ) @@ -89,17 +90,16 @@ from wce_crm.data_models import ( Grunderfassung_Personen, Grunderfassung_Unternehmen, InitRec, - InitRecForm_State, - InitRecFormConfig, Page_CompanyProfile_State, Page_Consulting_ConsultingSession_State, Page_Consulting_Linking_State, Page_Consulting_State, Page_Consulting_Table_State, - Page_InitRecCompany_State, - Page_InitRecPerson_State, + Page_InitRec_Form_Config, + Page_InitRec_Form_State, + Page_InitRec_State, Page_MainPage_State, - Page_NewInitRec_State, + Page_NewEntry_State, Session, set_page_state, translate_pydantic_errors, @@ -111,24 +111,25 @@ from wce_crm.form_defs import ( FormFieldType, ) from wce_crm.logging import ( - logger_auto_form, logger_custom_fields, logger_get_data, logger_gui, + logger_initrecform, logger_page_consulting, + logger_page_initrec, ) from wce_crm.types import ( CompanyProfileConsultationEntry, ConsultingType, - EntityIds, - EntityType, + NewEntityIds, RecId, + RecordingType, ) if TYPE_CHECKING: from wce_crm.data_models import ( - InitRecForm_State, Module, + Page_InitRec_Form_State, WrapperModule, ) from wce_crm.types import ( @@ -1383,7 +1384,7 @@ class InitRecForm(QWidget): def __init__( self, - state: InitRecForm_State, + state: Page_InitRec_Form_State, ) -> None: super().__init__() self.STATE = state @@ -1437,21 +1438,21 @@ class InitRecForm(QWidget): self.registry_button.setFixedHeight(35) self.main_layout.addWidget(self.registry_button) - self.test_button = QPushButton("Initialisiere Laden") - self.test_button.clicked.connect(self._load_from_id) - self.test_button.setFixedHeight(35) - self.test_button.setSizePolicy( - QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed - ) - self.main_layout.addWidget(self.test_button) + # self.test_button = QPushButton("Initialisiere Laden") + # self.test_button.clicked.connect(self.load_from_ids) + # self.test_button.setFixedHeight(35) + # self.test_button.setSizePolicy( + # QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed + # ) + # self.main_layout.addWidget(self.test_button) button_get = QPushButton("GET DATA") button_get.setFixedHeight(35) - button_get.clicked.connect(self._get_form_data) + button_get.clicked.connect(self._get_data) self.main_layout.addWidget(button_get) button_set = QPushButton("SET DATA") button_set.setFixedHeight(35) - button_set.clicked.connect(self._set_form_data) + button_set.clicked.connect(self._set_data) self.main_layout.addWidget(button_set) id_field_layout = QHBoxLayout() @@ -1489,11 +1490,11 @@ class InitRecForm(QWidget): self.save_btn = self.edit_buttons.save_btn self.delete_btn = self.edit_buttons.delete_btn - self.modify_btn.modify_activated.connect(self.unlock_form) - self.modify_btn.modify_deactivated.connect(self.lock_form) - self.save_btn.clicked.connect(self.save_data) + self.modify_btn.modify_activated.connect(self.unlock) + self.modify_btn.modify_deactivated.connect(self.lock) + # self.save_btn.clicked.connect(self.save_data) self.reset_btn.clicked.connect(self.reset_form) - self.delete_btn.clicked.connect(self._delete_data) + # self.delete_btn.clicked.connect(self._delete_data) self.main_layout.addWidget(self.edit_buttons) @@ -1512,8 +1513,8 @@ class InitRecForm(QWidget): self.debug_form_data: dict[str, Any] = {} - logger_auto_form.debug( - "[Auto-Form] Initialised Auto Form Widget. Registry:%s", + logger_initrecform.debug( + "[InitRecForm] Initialised Auto Form Widget. Registry:%s", pformat_registry(self.widget_registry), ) @@ -1527,119 +1528,123 @@ class InitRecForm(QWidget): index = None self.STATE.ids.un_id = index self.STATE.ids.pers_id = index - logger_gui.debug("[Auto-Form] Set index to %s, new state: %s", index, self.STATE) + logger_gui.debug("[InitRecForm] Set index to %s, new state: %s", index, self.STATE) - def _disable_save(self) -> None: - assert self.save_btn is not None - assert self.edit_buttons is not None - self.save_btn.setEnabled(False) - self.save_btn.setText(self.edit_buttons.save_btn_txt_disabled) + # def _disable_save(self) -> None: + # assert self.save_btn is not None + # assert self.edit_buttons is not None + # self.save_btn.setEnabled(False) + # self.save_btn.setText(self.edit_buttons.save_btn_txt_disabled) - def _enable_save( - self, - timeout: int = 3000, - ) -> None: - assert self.save_btn is not None - assert self.edit_buttons is not None - QTimer.singleShot(timeout, lambda: self.save_btn.setEnabled(True)) - QTimer.singleShot(timeout + 1, lambda: self.save_btn.setShortcut("Ctrl+S")) - self.save_btn.setText(self.edit_buttons.save_btn_txt_enabled) + # def _enable_save( + # self, + # timeout: int = 3000, + # ) -> None: + # assert self.save_btn is not None + # assert self.edit_buttons is not None + # QTimer.singleShot(timeout, lambda: self.save_btn.setEnabled(True)) + # QTimer.singleShot(timeout + 1, lambda: self.save_btn.setShortcut("Ctrl+S")) + # self.save_btn.setText(self.edit_buttons.save_btn_txt_enabled) - def _activate_delete(self) -> None: - if self.STATE.ids.valid(): - self.delete_btn.setEnabled(True) - else: - self.delete_btn.setEnabled(False) + def relevant_ids_set(self) -> bool: + return self.STATE.ids.valid(self.STATE.recording_type) - def _delete_data(self) -> None: - assert self.STATE.ids.valid(), "deletion initialised despite IDs are not properly set" - confirm = get_user_confirmation( - self, - "Löschen bestätigen", - "Soll der Eintrag wirklich gelöscht werden?", - ) - if not confirm: - return + # def _activate_delete(self) -> None: + # if self.STATE.ids.valid(): + # self.delete_btn.setEnabled(True) + # else: + # self.delete_btn.setEnabled(False) - self.STATE.geloescht = True - self.save_data() - self.update_triggered.emit() + # def _delete_data(self) -> None: + # assert self.STATE.ids.valid(), "deletion initialised despite IDs are not properly set" + # confirm = get_user_confirmation( + # self, + # "Löschen bestätigen", + # "Soll der Eintrag wirklich gelöscht werden?", + # ) + # if not confirm: + # return - def _load_from_id( - self, - id_: RecId | None, - ) -> None: - if DEBUG_NO_DATABASE: - return + # self.STATE.geloescht = True + # self.save_data() + # self.update_triggered.emit() - logger_auto_form.info("[Auto-Form] Load data method...") - logger_auto_form.debug("[Auto-Form] Lookup ID: %s", id_) - self.reset_form() + # !! must be part of the managing page + # def load_from_ids( + # self, + # ids: NewEntityIds, + # ) -> None: + # if DEBUG_NO_DATABASE: + # return - if id_ is None: - logger_auto_form.debug("[Auto-Form] Loading: Lookup ID NULL. Do nothing!") - return - logger_auto_form.debug("Load from DB:") - returned = self.cfg.from_db(id_) + # logger_initrecform.info("[InitRecForm] Load data method...") + # logger_initrecform.debug("[InitRecForm] Lookup ID: %s", ids) + # self.reset_form() - # TODO add path to avoid unnecessary loading (where the same ID is loaded - # twice in a row) -- use an internal marker (not state-related) + # if ids.empty(self.STATE.recording_type): + # logger_initrecform.debug("[InitRecForm] Loading: Lookup ID NULL. Do nothing!") + # return + # logger_initrecform.debug("Load from DB:") + # from_db = self.cfg.from_db(ids) - # logger_auto_form.debug( - # "[Auto-Form] Loaded data dict:\n%s Passing to Pydantic...", pformat(loaded_data) - # ) - logger_auto_form.debug( - "[Auto-Form] Loaded data dict:\n%s Passing to Pydantic...", - pformat(returned.db_data), - ) - model = self.cfg.model(**returned.db_data) - logger_auto_form.debug("[Auto-Form] Loaded to Pydantic.") - logger_auto_form.debug("[Auto-Form] Convert to GUI structure...") - form_data = model.to_gui() - logger_auto_form.debug("[Auto-Form] Set form data...") - logger_auto_form.debug("[Auto-Form] Form data:\n%s", pformat(form_data)) - self._set_form_data(form_data) - self.STATE.ids.set_id(id_, self.STATE.ent_type) - # TODO remove - # self.STATE.rec_id = id_ - self.STATE.form_data = self._get_form_data() - logger_auto_form.debug( - "####### model dump vs get form data:\n%s\n\n%s", - pformat(model.model_dump(mode="python")), - pformat(self.STATE.form_data), - ) + # # TODO add path to avoid unnecessary loading (where the same ID is loaded + # # twice in a row) -- use an internal marker (not state-related) + + # # logger_auto_form.debug( + # # "[InitRecForm] Loaded data dict:\n%s Passing to Pydantic...", pformat(loaded_data) + # # ) + # logger_initrecform.debug( + # "[InitRecForm] Loaded data dict:\n%s Passing to Pydantic...", + # pformat(from_db.db_data), + # ) + # model = self.cfg.pydantic_model(**from_db.db_data) + # logger_initrecform.debug("[InitRecForm] Loaded to Pydantic.") + # logger_initrecform.debug("[InitRecForm] Convert to GUI structure...") + # form_data = model.to_gui() + # logger_initrecform.debug("[InitRecForm] Set form data...") + # logger_initrecform.debug("[InitRecForm] Form data:\n%s", pformat(form_data)) + # self._set_data(form_data) + # # self.STATE.ids.set_id(id_, self.STATE.ent_type) + # self.STATE.ids.update(from_db.ids) + # self.STATE.form_data = self._get_data() + # logger_initrecform.debug( + # "[InitRecForm] Model dump vs. get form data:\n%s\n\n%s", + # pformat(model.model_dump(mode="python")), + # pformat(self.STATE.form_data), + # ) def reset_form(self) -> None: reset_form(self.widget_registry) - self.STATE.ids.set_id(None, self.STATE.ent_type) + # self.STATE.ids.set_id(None, self.STATE.ent_type) + self.STATE.ids.update(NewEntityIds()) # TODO remove # self.STATE.rec_id = None - self.STATE.form_data = None + self.STATE.form_data.clear() self.STATE.geloescht = False - def lock_form(self) -> None: + def lock(self) -> None: self.STATE.locked = True self._sync_state_to_GUI("locking") - def unlock_form(self) -> None: + def unlock(self) -> None: self.STATE.locked = False self._sync_state_to_GUI("locking") - def _get_form_data(self) -> dict[str, Any]: + def _get_data(self) -> dict[str, Any]: form_data = get_form_data(self.widget_registry) - logger_auto_form.debug("[Auto-Form] Call get form data:") - logger_auto_form.debug("[Auto-Form] Form Data:\n%s", pformat(form_data)) + logger_initrecform.debug("[InitRecForm] Call get form data:") + logger_initrecform.debug("[InitRecForm] Form Data:\n%s", pformat(form_data)) if DEBUG_GET_SET: self.debug_form_data = form_data return form_data - def _set_form_data( + def _set_data( self, data: dict[str, Any], ) -> None: - logger_auto_form.debug("[Auto-Form] Call set form data...") + logger_initrecform.debug("[InitRecForm] Call set form data...") if DEBUG_GET_SET: data = self.debug_form_data @@ -1649,9 +1654,11 @@ class InitRecForm(QWidget): self, section: Literal["form", "locking"] | None, ) -> None: + # TODO this should be initialised by states analogous to consulting sessions + # !! loading from IDs is only performed on top-level modules aka pages if section is None or section == "form": if not self.STATE.geloescht: - self._load_from_id(self.STATE.ids.get_id(self.STATE.ent_type)) + self.load_from_ids(self.STATE.ids) else: self.reset_form() @@ -1664,12 +1671,13 @@ class InitRecForm(QWidget): if self.edit_buttons is not None: self.edit_buttons.set_modify_state(True, set_delete_btn=False) unlock_form(self.widget_registry) - self._activate_delete() + # self._activate_delete() def _sync_GUI_to_state(self) -> None: - form_data = self._get_form_data() + form_data = self._get_data() # TODO change usage of user ID (needs changes in model and backend) - form_data[self.SESSION_USER_KEY] = self.STATE.session.user_name + if self.SESSION_USER_KEY in form_data: + form_data[self.SESSION_USER_KEY] = self.STATE.session.user_name self.STATE.form_data = form_data def validate(self) -> list[str]: @@ -1677,12 +1685,12 @@ class InitRecForm(QWidget): def load_state( self, - new_state: InitRecForm_State, + new_state: Page_InitRec_Form_State, ) -> None: set_page_state(self.STATE, new_state) self._sync_state_to_GUI(None) - def get_state(self) -> InitRecForm_State: + def get_state(self) -> Page_InitRec_Form_State: self._sync_GUI_to_state() return self.STATE @@ -1691,121 +1699,125 @@ class InitRecForm(QWidget): self, data: InitRec, ) -> None: - ent_type = self.STATE.ent_type - self.STATE.ids.set_id(data.ids.get_id(ent_type), ent_type) + # ent_type = self.STATE.recording_type + # self.STATE.ids.set_id(data.ids.get_id(ent_type), ent_type) + self.STATE.ids.update(data.ids) # self.STATE.rec_id = data.rec_id - self.STATE.form_data = None + self.STATE.form_data.clear() self.STATE.geloescht = True if data.geloescht else False self._sync_state_to_GUI(None) # TODO change to correct loading + # !! must be part of the managing page def save_data(self) -> None: - relevant_id = self.STATE.ids.get_id(self.STATE.ent_type) - if relevant_id is None and self.STATE.geloescht: - logger_auto_form.debug( - "[Auto-Form] This was never saved in the database " - "and is marked for deletion - ignore." - ) - return - elif relevant_id is not None and self.STATE.geloescht: - logger_auto_form.debug( - "[Auto-Form] This was saved in the database " - "and is marked for deletion - fast path." - ) - deleted_datetime = datetime.datetime.now(datetime.UTC) - assert self.STATE.form_data, "cannot delete entry with uninitialised form data" - to_db = InitRec( - ids=self.STATE.ids, - # rec_id=self.STATE.rec_id, # TODO remove - geloescht=deleted_datetime, - db_data={}, - ) - returned = self.cfg.to_db(to_db) - self._load_from_pydantic(returned) - msg_box = get_message_box( - QMessageBox.Icon.Information, - "Löschen erfolgreich", - "Der Eintrag wurde erfolgreich gelöscht", - ) - msg_box.exec() + pass - return + # def save_data(self) -> None: + # if self.STATE.ids.empty() and self.STATE.geloescht: + # logger_initrecform.debug( + # "[InitRecForm] This was never saved in the database " + # "and is marked for deletion - ignore." + # ) + # return + # elif not self.STATE.ids.empty() and self.STATE.geloescht: + # logger_initrecform.debug( + # "[InitRecForm] This was saved in the database " + # "and is marked for deletion - fast path." + # ) + # deleted_datetime = datetime.datetime.now(datetime.UTC) + # assert self.STATE.form_data, "cannot delete entry with uninitialised form data" + # to_db = InitRec( + # ids=self.STATE.ids, + # geloescht=deleted_datetime, + # db_data={}, + # ) + # returned = self.cfg.to_db(to_db) + # self._load_from_pydantic(returned) + # msg_box = get_message_box( + # QMessageBox.Icon.Information, + # "Löschen erfolgreich", + # "Der Eintrag wurde erfolgreich gelöscht", + # ) + # msg_box.exec() - self._disable_save() - errors = self.validate() + # return - if errors: - logger_auto_form.error( - "[Auto-Form] Error during GUI validation phase:\n%s", pformat(errors) - ) - GUI_pydantic_validation_error_handling(errors) - self._enable_save() - return + # self._disable_save() + # errors = self.validate() - logger_auto_form.info("[Auto-Form] Success! All data is valid") - logger_auto_form.info("[Auto-Form] Get form data call...") - state = self.get_state() - form_data = state.form_data - assert form_data, "form data not set" + # if errors: + # logger_initrecform.error( + # "[InitRecForm] Error during GUI validation phase:\n%s", pformat(errors) + # ) + # GUI_pydantic_validation_error_handling(errors) + # self._enable_save() + # return - logger_auto_form.debug("[Auto-Form] Get form data\n%s", pformat(form_data)) + # logger_initrecform.info("[InitRecForm] Success! All data is valid") + # logger_initrecform.info("[InitRecForm] Get form data call...") + # state = self.get_state() + # form_data = state.form_data + # assert form_data, "form data not set" - try: - logger_auto_form.debug("[Auto-Form] Call Pydantic...") - validated_data = self.cfg.model(**form_data) - except ValidationError as e: - logger_auto_form.error( - "[Auto-Form] Error during Pydantic validation phase:\n%s", pformat(e.errors()) - ) - pydantic_validation_error_handling(e) - else: - if DEBUG_NO_DATABASE: - return - # !! this code is only called if the 'try' block was successful - # save data to database - db_data = validated_data.to_db(exclude=self.cfg.ignored_keys) - logger_auto_form.debug( - "[Auto-Form] Form data with 'exlude' (must be saved in the database):\n%s", - pformat(db_data), - ) - deleted_datetime: datetime.datetime | None = None - if self.STATE.geloescht: - deleted_datetime = datetime.datetime.now(datetime.UTC) - try: - # TODO change to result pattern - to_db = InitRec( - ids=self.STATE.ids, - # rec_id=self.STATE.rec_id, - geloescht=deleted_datetime, - db_data=db_data, - ) - returned = self.cfg.to_db(to_db) + # logger_initrecform.debug("[InitRecForm] Get form data\n%s", pformat(form_data)) - self._load_from_pydantic(returned) - self.update_triggered.emit() - except Exception as err: - msg_box = get_message_box( - QMessageBox.Icon.Warning, - "Speichern fehlgeschlagen", - ( - "Beim Speichern der Daten ist ein Fehler aufgetreten. Details sind " - "unten angefügt." - ), - detailed_text=str(err), - ) - msg_box.exec() - else: - logger_auto_form.info("[Auto-Form] Data saved successfully") - msg_box = get_message_box( - QMessageBox.Icon.Information, - "Speichern erfolgreich", - "Die Daten wurden erfolgreich gespeichert", - ) - msg_box.exec() + # try: + # logger_initrecform.debug("[InitRecForm] Call Pydantic...") + # validated_data = self.cfg.pydantic_model(**form_data) + # except ValidationError as e: + # logger_initrecform.error( + # "[InitRecForm] Error during Pydantic validation phase:\n%s", + # pformat(e.errors()), + # ) + # pydantic_validation_error_handling(e) + # else: + # if DEBUG_NO_DATABASE: + # return + # # !! this code is only called if the 'try' block was successful + # # save data to database + # db_data = validated_data.to_db(exclude=self.cfg.ignored_keys) + # logger_initrecform.debug( + # "[InitRecForm] Form data with 'exlude' (must be saved in the database):\n%s", + # pformat(db_data), + # ) + # deleted_datetime: datetime.datetime | None = None + # if self.STATE.geloescht: + # deleted_datetime = datetime.datetime.now(datetime.UTC) + # try: + # # TODO change to result pattern + # to_db = InitRec( + # ids=self.STATE.ids, + # # rec_id=self.STATE.rec_id, + # geloescht=deleted_datetime, + # db_data=db_data, + # ) + # returned = self.cfg.to_db(to_db) - finally: - # always re-enable save, even if error occurred - self._enable_save() + # self._load_from_pydantic(returned) + # self.update_triggered.emit() + # except Exception as err: + # msg_box = get_message_box( + # QMessageBox.Icon.Warning, + # "Speichern fehlgeschlagen", + # ( + # "Beim Speichern der Daten ist ein Fehler aufgetreten. Details sind " + # "unten angefügt." + # ), + # detailed_text=str(err), + # ) + # msg_box.exec() + # else: + # logger_initrecform.info("[InitRecForm] Data saved successfully") + # msg_box = get_message_box( + # QMessageBox.Icon.Information, + # "Speichern erfolgreich", + # "Die Daten wurden erfolgreich gespeichert", + # ) + # msg_box.exec() + + # finally: + # # always re-enable save, even if error occurred + # self._enable_save() class FlexibleDateInput(QWidget): @@ -2867,17 +2879,19 @@ class MainPage_ClickableCell(ClickableCell): self.clicked.emit(self.data_record) -class Page_NewInitRec(QWidget): +class Page_NewEntry(QWidget): """view for new entry for initial recording ("Grunderfassung")""" back_requested = Signal() # Signal back button - company_requested = Signal(Page_InitRecCompany_State) # Signal "Unternehmen" - person_requested = Signal(Page_InitRecPerson_State) # Signal "Individualperson" + # TODO re-enable with correct settings + # company_requested = Signal(Page_InitRecCompany_State) # Signal "Unternehmen" + # person_requested = Signal(Page_InitRecPerson_State) # Signal "Individualperson" consulting_requested = Signal(Page_Consulting_State) # Signal "Beratungsgespräch" + new_initrec_requested = Signal(Page_InitRec_State) # TODO: rename def __init__( self, - state: Page_NewInitRec_State, + state: Page_NewEntry_State, ): super().__init__() self.STATE = state @@ -2911,35 +2925,46 @@ class Page_NewInitRec(QWidget): btn_consulting.setFixedHeight(40) btn_consulting.clicked.connect(self._request_consulting) + btn_newinitrec = QPushButton("NEUE GRUNDERFASSUNG →") + btn_newinitrec.setFixedWidth(300) + btn_newinitrec.setFixedHeight(40) + btn_newinitrec.clicked.connect(self._request_new_initrec) + layout.addWidget(back_btn) layout.addSpacing(15) layout.addWidget(self.title_label) + layout.addSpacing(20) + layout.addWidget(btn_newinitrec) + layout.addSpacing(20) layout.addWidget(btn_company) layout.addWidget(btn_person) layout.addSpacing(10) layout.addWidget(btn_consulting) + # TODO configure correctly def _request_initrec_company(self) -> None: - ids = EntityIds(type=EntityType.COMPANY) - req_state = Page_InitRecCompany_State( - session=self.STATE.session, - ids=ids, - # un_id=None, # TODO remove - locked=False, - ) - logger_gui.debug("[Page -- InitRec] State to call: %s", req_state) - self.company_requested.emit(req_state) + logger_gui.debug("[Page -- NewEntry] Call for company...") + # ids = EntityIds(type=RecordingType.COMPANY) + # req_state = Page_InitRecCompany_State( + # session=self.STATE.session, + # ids=ids, + # # un_id=None, # TODO remove + # locked=False, + # ) + # logger_gui.debug("[Page -- InitRec] State to call: %s", req_state) + # self.company_requested.emit(req_state) def _request_initrec_person(self) -> None: - ids = EntityIds(type=EntityType.PERSON) - req_state = Page_InitRecPerson_State( - session=self.STATE.session, - ids=ids, - # pers_id=None, # TODO remove - locked=False, - ) - logger_gui.debug("[Page -- InitRec] State to call: %s", req_state) - self.person_requested.emit(req_state) + logger_gui.debug("[Page -- NewEntry] Call for person...") + # ids = EntityIds(type=RecordingType.PERSON) + # req_state = Page_InitRecPerson_State( + # session=self.STATE.session, + # ids=ids, + # # pers_id=None, # TODO remove + # locked=False, + # ) + # logger_gui.debug("[Page -- InitRec] State to call: %s", req_state) + # self.person_requested.emit(req_state) def _request_consulting(self) -> None: req_state = Page_Consulting_State( @@ -2951,6 +2976,15 @@ class Page_NewInitRec(QWidget): logger_gui.debug("[Page -- InitRec] State to call: %s", req_state) self.consulting_requested.emit(req_state) + def _request_new_initrec(self) -> None: + req_state = Page_InitRec_State( + session=self.STATE.session, + record_type=RecordingType.COMPANY, + locked=False, + ) + logger_gui.debug("[Page -- NewInitRec] State to call: %s", req_state) + self.new_initrec_requested.emit(req_state) + def _sync_state_to_GUI(self) -> None: pass @@ -2962,12 +2996,12 @@ class Page_NewInitRec(QWidget): def load_state( self, - new_state: Page_NewInitRec_State, + new_state: Page_NewEntry_State, ) -> None: set_page_state(self.STATE, new_state) self._sync_state_to_GUI() - def get_state(self) -> Page_NewInitRec_State: + def get_state(self) -> Page_NewEntry_State: self._sync_GUI_to_state() return self.STATE @@ -2975,26 +3009,28 @@ class Page_NewInitRec(QWidget): pass -CONFIG_GRUNDERFASSUNG_UNTERNEHMEN: Final[InitRecFormConfig] = InitRecFormConfig( - model=Grunderfassung_Unternehmen, - to_db=backend.initrec_company_to_db, - from_db=backend.initrec_company_from_db, +CONFIG_GRUNDERFASSUNG_UNTERNEHMEN: Final[Page_InitRec_Form_Config] = Page_InitRec_Form_Config( + pydantic_model=Grunderfassung_Unternehmen, + # to_db=backend.initrec_company_to_db, # TODO remove + # from_db=backend.initrec_company_from_db, # TODO remove ignored_keys=( "Metadaten_erstellung", "Metadaten_aktualisierung", ), form_fields=INITREC_COMP, + add_buttons=False, ) -CONFIG_GRUNDERFASSUNG_PERSONEN: Final[InitRecFormConfig] = InitRecFormConfig( - model=Grunderfassung_Personen, - to_db=backend.initrec_person_to_db, - from_db=backend.initrec_person_from_db, +CONFIG_GRUNDERFASSUNG_PERSONEN: Final[Page_InitRec_Form_Config] = Page_InitRec_Form_Config( + pydantic_model=Grunderfassung_Personen, + # to_db=backend.initrec_person_to_db, # TODO remove + # from_db=backend.initrec_person_from_db, # TODO remove ignored_keys=( "Metadaten_erstellung", "Metadaten_aktualisierung", ), form_fields=INITREC_PERSON, + add_buttons=False, ) CUSTOM_WIDGETS: Final[dict[str, type[CustomFormWidget]]] = { @@ -3002,263 +3038,63 @@ CUSTOM_WIDGETS: Final[dict[str, type[CustomFormWidget]]] = { } -class Page_InitRecCompany(QWidget): - back_main_requested = Signal() # back to main page - back_requested = Signal() # back button - update_main_page = Signal() # form saved (data changed for front page) - company_profile_requested = Signal(Page_CompanyProfile_State) - initrec_person_requested = Signal(Page_InitRecPerson_State) # TODO add corresponding call +class CollapsibleBox(QWidget): + def __init__(self, title="", parent=None): + super().__init__(parent) - def __init__( + # 1. Header-Button mit Pfeil + self.toggle_button = QToolButton() + self.toggle_button.setText(f" {title}") + self.toggle_button.setCheckable(True) + self.toggle_button.setChecked(False) + self.toggle_button.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) + self.toggle_button.setArrowType(Qt.ArrowType.RightArrow) + self.toggle_button.setStyleSheet( + "QToolButton { border: none; font-weight: bold; font-size: 13px; }" + ) + self.toggle_button.toggled.connect(self._on_toggle) + + # 2. Inhaltsbereich (standardmäßig ausgeblendet) + self.content_area = QWidget() + self.content_area.setVisible(False) + + # Layout des Box-Containers + main_layout = QVBoxLayout(self) + main_layout.setContentsMargins(0, 5, 0, 5) + main_layout.addWidget(self.toggle_button) + main_layout.addWidget(self.content_area) + + def _on_toggle(self, checked: bool): + # Pfeilrichtung ändern & Inhalt ein-/ausblenden + self.toggle_button.setArrowType( + Qt.ArrowType.DownArrow if checked else Qt.ArrowType.RightArrow + ) + self.content_area.setVisible(checked) + + def setContentLayout( self, - state: Page_InitRecCompany_State, + layout: QLayout, ): - super().__init__() - self.STATE = state + self.content_area.setLayout(layout) - # Hauptlayout der Seite - outer_layout = QHBoxLayout(self) - vert_layout = QVBoxLayout() - # main_layout.setContentsMargins(0, 0, 0, 0) - outer_layout.addStretch(1) - outer_layout.addLayout(vert_layout, stretch=100) - # outer_layout.addWidget(scroll_area, stretch=100) - outer_layout.addStretch(1) - # Optional: Damit der Container oben am Rand klebt - outer_layout.setAlignment(Qt.AlignmentFlag.AlignTop) - - # --- HEADER --- - header_container = QWidget() - header_container.setMinimumWidth(700) - header_container.setMaximumWidth(1000) - header_container.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) - header_layout = QVBoxLayout(header_container) - header_layout.setContentsMargins(0, 0, 0, 10) - - back_btn_main = QPushButton("← Zurück zur Übersicht") - back_btn_main.clicked.connect(lambda: self.back_main_requested.emit()) - back_btn_main.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) - back_btn_main.setMinimumWidth(200) - back_btn_main.setMaximumWidth(200) - back_btn_step = QPushButton("← Zurück") - back_btn_step.clicked.connect(lambda: self.back_requested.emit()) - back_btn_step.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) - back_btn_step.setMinimumWidth(200) - back_btn_step.setMaximumWidth(200) - - title = QLabel("Grunderfassung Unternehmen") - title.setStyleSheet("font-size: 20px; font-weight: bold;") - - self.comp_profile_btn = QPushButton("Unternehmensprofil") - self.comp_profile_btn.clicked.connect(self._request_company_profile) - self.comp_profile_btn.setSizePolicy( - QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed - ) - self.comp_profile_btn.setFixedHeight(50) - - self.initrec_person_btn = QPushButton("Individualperson erfassen/anzeigen") - self.initrec_person_btn.clicked.connect(self._request_initrec_person) - self.initrec_person_btn.setSizePolicy( - QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed - ) - self.initrec_person_btn.setFixedHeight(50) - - header_layout.setSpacing(5) - header_layout.addWidget(back_btn_step) - header_layout.addWidget(back_btn_main) - header_layout.addWidget(title) - header_layout.addSpacing(15) - header_layout.addWidget(self.comp_profile_btn) - header_layout.addWidget(self.initrec_person_btn) - vert_layout.addWidget(header_container) - - # --- BANNER --- - self.info_banners_container = QWidget() - self.info_banners: list[InfoBanner] = [] - self.info_banners_layout = QVBoxLayout(self.info_banners_container) - self.info_banners_layout.setContentsMargins(0, 0, 0, 0) - self.info_banners_layout.setSpacing(6) - header_layout.addWidget(self.info_banners_container) - - # --- MAIN CONTENT --- - container = QWidget() - # SCROLL AREA - scroll_area = QScrollArea() - scroll_area.setWidgetResizable(True) - scroll_area.setMinimumWidth(700) - scroll_area.setMaximumWidth(1000) - scroll_area.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) - # optional: remove frame to look more modern - scroll_area.setFrameShape(QFrame.Shape.NoFrame) - scroll_area.setWidget(container) - - vert_layout.addWidget(scroll_area) - - container_layout = QVBoxLayout(container) - container_layout.setContentsMargins(0, 0, 0, 0) - - # --- AUTO FORM LAYOUT --- - container_layout.addSpacing(20) - self.INITREC_FORM_CFG = CONFIG_GRUNDERFASSUNG_UNTERNEHMEN - assert self.STATE.ids.ent_type is EntityType.COMPANY, "mismatch of entity types" - auto_form_state = InitRecForm_State( - session=self.STATE.session, - cfg=self.INITREC_FORM_CFG, - ids=self.STATE.ids, - ent_type=EntityType.COMPANY, - locked=False, - ) - self.auto_form = InitRecForm(auto_form_state) - container_layout.addWidget(self.auto_form) - self.auto_form.update_triggered.connect(lambda: self.update_main_page.emit()) - self.auto_form.update_triggered.connect(self._auto_form_updated) - - container_layout.addSpacing(15) - - # --- CUSTOM LOGIC --- - # ** fill 'Kontaktperson -> Namen Unternehmen' - 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"]) - self.search_widget_trigger = cast( - QLineEdit, search_widget.company_widgets["ma_unternehmensname"] - ) - - def _clear_info_banners(self) -> None: - while self.info_banners: - banner = self.info_banners[0] - self.info_banners_layout.removeWidget(banner) - self.info_banners.remove(banner) - banner.deleteLater() - - def _add_info_banner( + def setContentWidget( self, - text: str, - button_text: str, - button_func: Callable[[], None] | None = None, - ) -> None: - if button_text and button_func is None: - raise ValueError("Can not add button without calling function") - - banner = InfoBanner( - text=text, - button_text=button_text, - ) - if button_func is not None: - banner.btn_clicked.connect(button_func) - self.info_banners.append(banner) - self.info_banners_layout.addWidget(banner) - - def _auto_form_updated(self) -> None: - self._sync_GUI_to_state() - self._sync_state_to_GUI("comp_profile") - self._sync_state_to_GUI("banners") - - def _request_company_profile(self) -> None: - assert self.STATE.ids.un_id is not None, ( - "requested company profile for a company which is not known (no ID)" - ) - req_state = Page_CompanyProfile_State( - session=self.STATE.session, - # rec_id=self.STATE.un_id, - ids=self.STATE.ids, - ) - logger_gui.debug("[Page -- InitRec Company] State to call: %s", req_state) - self.company_profile_requested.emit(req_state) - - def _request_initrec_person(self) -> None: - assert self.STATE.ids.un_id is not None, ( - "requested initrec of person for a company which is not known (no ID)" - ) - if not self.STATE.locked: - self.auto_form.save_data() - - req_state = Page_InitRecPerson_State( - session=self.STATE.session, - ids=self.STATE.ids, - # pers_id=None, # TODO remove - # un_id=self.STATE.un_id, - locked=False, - ) - logger_gui.debug("[Page -- InitRec Company] State to call: %s", req_state) - self.initrec_person_requested.emit(req_state) - - def reset(self) -> None: - self.auto_form.reset_form() - - def _sync_state_to_GUI( - self, - section: Literal["auto_form", "comp_profile", "initrec_person", "banners"] | None, - ) -> None: - if section is None or section == "auto_form": - assert self.STATE.ids.ent_type is EntityType.COMPANY, "mismatch of entity types" - auto_form_state = InitRecForm_State( - session=self.STATE.session, - cfg=self.INITREC_FORM_CFG, - ids=self.STATE.ids, - ent_type=EntityType.COMPANY, - # rec_id=self.STATE.un_id, # TODO remove - form_data=None, - locked=self.STATE.locked, - ) - logger_gui.debug(">>>>>>>>>>>>> AutoForm State:\n%s", pformat(auto_form_state)) - self.auto_form.load_state(auto_form_state) - - if section is None or section == "comp_profile": - if self.STATE.ids.valid(): - self.comp_profile_btn.setEnabled(True) - else: - self.comp_profile_btn.setEnabled(False) - - if section is None or section == "initrec_person": - if self.STATE.ids.un_id is not None: - self.initrec_person_btn.setEnabled(True) - else: - self.initrec_person_btn.setEnabled(False) - - if section is None or section == "banners": - self._clear_info_banners() - if self.STATE.ids.un_id is None: - self._add_info_banner( - text="⚠️ Dieser Eintrag ist noch nicht gespeichert.", - button_text="", - ) - - def _sync_GUI_to_state(self) -> None: - auto_form_state = self.auto_form.get_state() - self.STATE.ids = auto_form_state.ids - # self.STATE.un_id = auto_form_state.rec_id # TODO remove - - def load_state( - self, - new_state: Page_InitRecCompany_State, - ) -> None: - set_page_state(self.STATE, new_state) - - self._sync_state_to_GUI(None) - - def get_state(self) -> Page_InitRecCompany_State: - self._sync_GUI_to_state() - - return self.STATE - - def lock(self) -> None: - self.auto_form.lock_form() - self.STATE.locked = True - - def unlock(self) -> None: - self.auto_form.unlock_form() - self.STATE.locked = False + widget: QWidget, + ): + layout = QVBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) # Keine zusätzlichen Ränder + layout.addWidget(widget) + self.setContentLayout(layout) -class Page_InitRecPerson(QWidget): +class Page_InitRec(QWidget): back_main_requested = Signal() # back to main page back_requested = Signal() # back button update_triggered = Signal() # form saved (data changed for front page) def __init__( self, - state: Page_InitRecPerson_State, + state: Page_InitRec_State, ): super().__init__() self.STATE = state @@ -3292,7 +3128,12 @@ class Page_InitRecPerson(QWidget): back_btn_step.setMinimumWidth(200) back_btn_step.setMaximumWidth(200) - title = QLabel("Grunderfassung Individualperson") + if self.STATE.record_type is RecordingType.COMPANY: + title = QLabel("Grunderfassung Unternehmen") + elif self.STATE.record_type is RecordingType.PERSON: + title = QLabel("Grunderfassung Individualperson") + else: + raise RuntimeError(f"Unspecified recording type {self.STATE.record_type}") title.setStyleSheet("font-size: 20px; font-weight: bold;") header_layout.setSpacing(5) @@ -3309,6 +3150,25 @@ class Page_InitRecPerson(QWidget): self.info_banners_layout.setSpacing(6) header_layout.addWidget(self.info_banners_container) + # --- BUTTONS --- + self.edit_buttons = EditButtons(add_modify_btn=True, add_reset_btn=True) + modify_btn = self.edit_buttons.modify_button + assert modify_btn + self.modify_btn = modify_btn + reset_btn = self.edit_buttons.reset_btn + assert reset_btn + self.reset_btn = reset_btn + self.save_btn = self.edit_buttons.save_btn + self.delete_btn = self.edit_buttons.delete_btn + + self.modify_btn.modify_activated.connect(self.unlock) + self.modify_btn.modify_deactivated.connect(self.lock) + self.save_btn.clicked.connect(self.save_data) + self.reset_btn.clicked.connect(self._reset) + self.delete_btn.clicked.connect(self._delete_data) + header_layout.addSpacing(5) + header_layout.addWidget(self.edit_buttons) + # --- MAIN CONTENT --- container = QWidget() # SCROLL AREA @@ -3326,27 +3186,38 @@ class Page_InitRecPerson(QWidget): container_layout = QVBoxLayout(container) container_layout.setContentsMargins(0, 0, 0, 0) - # --- AUTO FORM LAYOUT --- + # --- AUTO FORM LAYOUT 1 --- + # now we try to render the layout within the collapsable box container_layout.addSpacing(20) - self.AUTO_FORM_CFG = CONFIG_GRUNDERFASSUNG_PERSONEN - auto_form_state = InitRecForm_State( + + self.initrec_form_company: InitRecForm | None = None + if self.STATE.record_type is RecordingType.COMPANY: + # add company + pass + + box1 = CollapsibleBox("Angaben zur Person") + initrec_form_state_person = Page_InitRec_Form_State( session=self.STATE.session, - cfg=self.AUTO_FORM_CFG, + cfg=CONFIG_GRUNDERFASSUNG_PERSONEN, ids=self.STATE.ids, - ent_type=EntityType.PERSON, + recording_type=RecordingType.PERSON, locked=False, ) - self.auto_form = InitRecForm(auto_form_state) - container_layout.addWidget(self.auto_form) - self.auto_form.update_triggered.connect(lambda: self.update_triggered.emit()) - self.auto_form.update_triggered.connect(self._auto_form_updated) - - container_layout.addSpacing(15) + self.initrec_form_person = InitRecForm(initrec_form_state_person) + self.STATE.child_modules.append(self.initrec_form_person) + box1.setContentWidget(self.initrec_form_person) + container_layout.addWidget(box1) + # container_layout.addSpacing(15) + self.initrec_form_person.update_triggered.connect( + lambda: self.update_triggered.emit() + ) + self.initrec_form_person.update_triggered.connect(self._auto_form_updated) # --- CUSTOM LOGIC --- # ** 'Bundesland' only if 'Inland' selected in 'Stammdaten' person_location = search_widgets_by_key( - self.auto_form.widget_registry, f"Stammdaten{COLUMN_SEP}aufenthaltsort" + self.initrec_form_person.widget_registry, + f"Stammdaten{COLUMN_SEP}aufenthaltsort", ) assert len(person_location) == 1 self.person_location = cast(QComboBox, person_location[0]["widget"]) @@ -3354,7 +3225,7 @@ class Page_InitRecPerson(QWidget): 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" + self.initrec_form_person.widget_registry, f"Stammdaten{COLUMN_SEP}bundesland" ) assert len(selection_county) == 1 self.selection_county = cast(QComboBox, selection_county[0]["widget"]) @@ -3362,6 +3233,9 @@ class Page_InitRecPerson(QWidget): self.selection_county.setProperty("styleClass", "stempel") self.selection_county.setEnabled(False) + container_layout.addStretch() + + # ** custom logic def _custom_county_selection(self, idx: int) -> None: value = self.person_location.itemData(idx) if value == "Inland": @@ -3378,6 +3252,7 @@ class Page_InitRecPerson(QWidget): self.selection_county.style().polish(self.selection_county) self.selection_county.update() + # ** GUI behaviour def _clear_info_banners(self) -> None: while self.info_banners: banner = self.info_banners[0] @@ -3403,29 +3278,109 @@ class Page_InitRecPerson(QWidget): self.info_banners.append(banner) self.info_banners_layout.addWidget(banner) + def _disable_save(self) -> None: + assert self.save_btn is not None + assert self.edit_buttons is not None + self.save_btn.setEnabled(False) + self.save_btn.setText(self.edit_buttons.save_btn_txt_disabled) + + def _enable_save( + self, + timeout: int = 3000, + ) -> None: + assert self.save_btn is not None + assert self.edit_buttons is not None + QTimer.singleShot(timeout, lambda: self.save_btn.setEnabled(True)) + QTimer.singleShot(timeout + 1, lambda: self.save_btn.setShortcut("Ctrl+S")) + self.save_btn.setText(self.edit_buttons.save_btn_txt_enabled) + + def _activate_delete(self) -> None: + if self._deletion_valid(): + self.delete_btn.setEnabled(True) + else: + self.delete_btn.setEnabled(False) + + def _deletion_valid(self) -> bool: + valid_id_states = (form.relevant_ids_set() for form in self.STATE.child_modules) + return all(valid_id_states) + + def _delete_data(self) -> None: + assert self._deletion_valid(), "deletion initialised despite IDs are not properly set" + confirm = get_user_confirmation( + self, + "Löschen bestätigen", + "Soll der Eintrag wirklich gelöscht werden?", + ) + if not confirm: + return + + self.STATE.geloescht = True + self.save_data() + self.update_triggered.emit() + def _auto_form_updated(self) -> None: self._sync_GUI_to_state() self._sync_state_to_GUI("banners") # TODO saving logic to connect company and person if needed - def reset_form(self) -> None: - self.auto_form.reset_form() + def _reset(self) -> None: + # TODO change to make these calls implicit in child modules via + # TODO staes + for form in self.STATE.child_modules: + form.reset_form() + self.STATE.ids = NewEntityIds() + self.STATE.geloescht = False + + def _load_database( + self, + ids: NewEntityIds, + ) -> None: + logger_initrecform.info("[Page -- InitRec] Load data method...") + logger_initrecform.debug("[Page -- InitRec] Entity IDs: %s", ids) + self._reset() + # TODO add loading logic + initrec: InitRec = ... + + self._load_from_pydantic(initrec, sync_to_GUI=False) + + def _load_from_pydantic( + self, + data: InitRec, + update_only: bool = False, + sync_to_GUI: bool = True, + ) -> None: + # TODO add logic to update internal state + + if update_only: + ... + else: + ... + + if sync_to_GUI: + self._sync_state_to_GUI(None) def _sync_state_to_GUI( self, - section: Literal["auto_form", "banners"] | None, + section: Literal["content", "banners", "locking"] | None, ) -> None: - if section is None or section == "auto_form": - auto_form_state = InitRecForm_State( + logger_page_initrec.debug( + "[Page -- InitRec] Syncing state to GUI: State...\n%s", pformat(self.STATE) + ) + + if section is None or section == "content": + if not self.STATE.geloescht: + self._load_database(self.STATE.ids) + else: + self._reset() + + initrec_form_state_person = Page_InitRec_Form_State( session=self.STATE.session, - cfg=self.AUTO_FORM_CFG, + cfg=CONFIG_GRUNDERFASSUNG_PERSONEN, ids=self.STATE.ids, - ent_type=EntityType.PERSON, - # rec_id=self.STATE.pers_id, # TODO remove - form_data=None, - locked=self.STATE.locked, + recording_type=RecordingType.PERSON, + locked=False, ) - self.auto_form.load_state(auto_form_state) + self.initrec_form_person.load_state(initrec_form_state_person) if section is None or section == "banners": self._clear_info_banners() @@ -3435,30 +3390,165 @@ class Page_InitRecPerson(QWidget): button_text="", ) + if section is None or section == "locking": + if self.STATE.locked: + self.edit_buttons.set_modify_state(False, set_delete_btn=True) + for form in self.STATE.child_modules: + form.lock() + else: + self.edit_buttons.set_modify_state(True, set_delete_btn=False) + for form in self.STATE.child_modules: + form.unlock() + def _sync_GUI_to_state(self) -> None: - auto_form_state = self.auto_form.get_state() - self.STATE.ids = auto_form_state.ids - # self.STATE.ids.pers_id = auto_form_state.rec_id # TODO remove + for form in self.STATE.child_modules: + # ?? is this the same reference to use a similar pattern as + # ?? the consulting page (initialise once with the same state + # ?? as reference which are then automatically updated) + form_state = form.get_state() + self.STATE.ids.update(form_state.ids, ignore_none=True) + self.STATE.form_data[form_state.recording_type] = form_state.form_data + self.STATE.pydantic_models[form_state.recording_type] = ( + form_state.cfg.pydantic_model + ) def load_state( self, - new_state: Page_InitRecPerson_State, + new_state: Page_InitRec_State, ) -> None: set_page_state(self.STATE, new_state) self._sync_state_to_GUI(None) - def get_state(self) -> Page_InitRecPerson_State: + def get_state(self) -> Page_InitRec_State: self._sync_GUI_to_state() return self.STATE + def validate(self) -> list[str]: + errors: list[str] = [] + for m in self.STATE.child_modules: + errors.extend(m.validate()) + + return errors + def lock(self) -> None: - self.auto_form.lock_form() + logger_page_initrec.debug("[Page -- InitRec] Locking...") self.STATE.locked = True + self._sync_state_to_GUI("locking") def unlock(self) -> None: - self.auto_form.unlock_form() + logger_page_initrec.debug("[Page -- InitRec] Unlocking...") self.STATE.locked = False + self._sync_state_to_GUI("locking") + + def save_data(self) -> None: + logger_page_initrec.debug("[Page -- InitRec] Save button clicked...") + + if self.STATE.ids.empty() and self.STATE.geloescht: + logger_page_initrec.debug( + "[Page -- InitRec] This was never saved in the database " + "and is marked for deletion - ignore." + ) + return + elif not self.STATE.ids.empty() and self.STATE.geloescht: + logger_page_initrec.debug( + "[Page -- InitRec] This was saved in the database " + "and is marked for deletion - fast path." + ) + deleted_datetime = datetime.datetime.now(datetime.UTC) + assert self.STATE.form_data, "cannot delete entry with uninitialised form data" + to_db = InitRec( + ids=self.STATE.ids, + geloescht=deleted_datetime, + db_data={}, + ) + returned = self.cfg.to_db(to_db) + self._load_from_pydantic(returned) + msg_box = get_message_box( + QMessageBox.Icon.Information, + "Löschen erfolgreich", + "Der Eintrag wurde erfolgreich gelöscht", + ) + msg_box.exec() + + return + + self._disable_save() + errors = self.validate() + + if errors: + logger_page_initrec.error( + "[Page -- InitRec] Error during GUI validation phase:\n%s", pformat(errors) + ) + GUI_pydantic_validation_error_handling(errors) + self._enable_save() + return + + logger_page_initrec.info("[Page -- InitRec] Success! All data is valid") + logger_page_initrec.info("[Page -- InitRec] Get form data call...") + state = self.get_state() + form_data = state.form_data + assert form_data, "form data not set" + + logger_page_initrec.debug("[Page -- InitRec] Get form data\n%s", pformat(form_data)) + + try: + logger_page_initrec.debug("[Page -- InitRec] Call Pydantic...") + validated_data = self.cfg.pydantic_model(**form_data) + except ValidationError as e: + logger_page_initrec.error( + "[Page -- InitRec] Error during Pydantic validation phase:\n%s", + pformat(e.errors()), + ) + pydantic_validation_error_handling(e) + else: + if DEBUG_NO_DATABASE: + return + # !! this code is only called if the 'try' block was successful + # save data to database + db_data = validated_data.to_db(exclude=self.cfg.ignored_keys) + logger_page_initrec.debug( + "[Page -- InitRec] Form data with 'exlude' (must be saved in the database):\n%s", + pformat(db_data), + ) + deleted_datetime: datetime.datetime | None = None + if self.STATE.geloescht: + deleted_datetime = datetime.datetime.now(datetime.UTC) + try: + # TODO change to result pattern + to_db = InitRec( + ids=self.STATE.ids, + # rec_id=self.STATE.rec_id, + geloescht=deleted_datetime, + db_data=db_data, + ) + returned = self.cfg.to_db(to_db) + + self._load_from_pydantic(returned) + self.update_triggered.emit() + except Exception as err: + msg_box = get_message_box( + QMessageBox.Icon.Warning, + "Speichern fehlgeschlagen", + ( + "Beim Speichern der Daten ist ein Fehler aufgetreten. Details sind " + "unten angefügt." + ), + detailed_text=str(err), + ) + msg_box.exec() + else: + logger_page_initrec.info("[Page -- InitRec] Data saved successfully") + msg_box = get_message_box( + QMessageBox.Icon.Information, + "Speichern erfolgreich", + "Die Daten wurden erfolgreich gespeichert", + ) + msg_box.exec() + + finally: + # always re-enable save, even if error occurred + self._enable_save() class Page_CompanyProfile(QWidget): @@ -3838,6 +3928,8 @@ class Page_CompanyProfile(QWidget): return self._enable_add_btns() + # TODO change to correct backend function + raise RuntimeError("TODOs to implement") init_rec = backend.initrec_company_get_initial_recording(self.STATE.ids.un_id) ma_id = cast("ExtMaId", init_rec["Partnersuche__un_suche"]) assert ma_id is not None, "no ma_id obtained" @@ -4521,12 +4613,14 @@ class Page_Consulting(QWidget): cons_id: ConsId | None, ) -> None: # load data model from database - logger_auto_form.info("[Consulting Page] Load data method...") - logger_auto_form.debug("[Consulting Page] Lookup ID: %s", cons_id) + logger_page_consulting.info("[Consulting Page] Load data method...") + logger_page_consulting.debug("[Consulting Page] Lookup ID: %s", cons_id) self._reset() if cons_id is None: - logger_auto_form.debug("[Consulting Page] Loading: Lookup ID NULL. Do nothing!") + logger_page_consulting.debug( + "[Consulting Page] Loading: Lookup ID NULL. Do nothing!" + ) return res = backend.page_consulting_from_db(cons_id) @@ -4611,7 +4705,7 @@ class Page_Consulting(QWidget): def _delete_entry(self) -> None: assert self.STATE.vorgang_id is not None and self.STATE.geloescht - logger_auto_form.debug( + logger_page_consulting.debug( "[Consulting Page] This was saved in the database " "and is marked for deletion - fast path." ) @@ -4652,7 +4746,7 @@ class Page_Consulting(QWidget): def save_data(self) -> None: logger_page_consulting.debug("[Consulting Page] Call saving data...") if self.STATE.vorgang_id is None and self.STATE.geloescht: - logger_auto_form.debug( + logger_page_consulting.debug( "[Consulting Page] This was never saved in the database " "and is marked for deletion - ignore." ) @@ -4721,7 +4815,7 @@ class Page_Consulting(QWidget): ) except ValidationError as e: - logger_auto_form.error( + logger_page_consulting.error( "[Consulting Page] Error during Pydantic validation phase:\n%s", pformat(e.errors()), ) @@ -5586,6 +5680,9 @@ class InfoBanner(QFrame): self.button.clicked.connect(self.btn_clicked.emit) +# // test components + + #################################################################### @@ -5830,38 +5927,53 @@ class MainWindow(QMainWindow): self.stack.addWidget(self.main_page) # SITE: add new entries for 'Grunderfassung' - new_initrec_select_state = Page_NewInitRec_State(session=self.STATE.session) - self.new_initrec_select = Page_NewInitRec(new_initrec_select_state) - self.new_initrec_select.back_requested.connect(self.show_main_page) - self.new_initrec_select.company_requested.connect(self.show_page_initrec_company) - self.new_initrec_select.person_requested.connect(self.show_page_initrec_person) - self.new_initrec_select.consulting_requested.connect(self.show_page_consulting) - self.stack.addWidget(self.new_initrec_select) + new_initrec_select_state = Page_NewEntry_State(session=self.STATE.session) + self.new_entry_select = Page_NewEntry(new_initrec_select_state) + self.new_entry_select.back_requested.connect(self.show_main_page) + # self.new_initrec_select.company_requested.connect(self.show_page_initrec_company) + # self.new_initrec_select.person_requested.connect(self.show_page_initrec_person) + self.new_entry_select.consulting_requested.connect(self.show_page_consulting) + self.new_entry_select.new_initrec_requested.connect(self.show_page_newinitrec) + self.stack.addWidget(self.new_entry_select) + + # SITE: 'Grunderfassung' + newinitrec_state = Page_InitRec_State( + session=session, + ids=NewEntityIds(), + record_type=RecordingType.COMPANY, + locked=False, + ) + self.newinitrec = Page_InitRec(newinitrec_state) + self.newinitrec.back_main_requested.connect(self.show_main_page) + self.newinitrec.back_requested.connect(self.simple_go_back) + # self.newinitrec.update_main_page.connect(self.update_grid) + self.stack.addWidget(self.newinitrec) + # SITE: 'Grunderfassung Unternehmen' - ids = EntityIds(type=EntityType.COMPANY) - initrec_company_state = Page_InitRecCompany_State( - session=self.STATE.session, ids=ids, locked=False - ) - self.initrec_company = Page_InitRecCompany(initrec_company_state) - self.initrec_company.back_main_requested.connect(self.show_main_page) - self.initrec_company.back_requested.connect(self.simple_go_back) - self.initrec_company.update_main_page.connect(self.update_grid) - self.initrec_company.initrec_person_requested.connect(self.show_page_initrec_person) - self.initrec_company.company_profile_requested.connect(self.show_page_company_profile) - self.stack.addWidget(self.initrec_company) + # ids = EntityIds(type=RecordingType.COMPANY) + # initrec_company_state = Page_InitRecCompany_State( + # session=self.STATE.session, ids=ids, locked=False + # ) + # self.initrec_company = Page_InitRecCompany(initrec_company_state) + # self.initrec_company.back_main_requested.connect(self.show_main_page) + # self.initrec_company.back_requested.connect(self.simple_go_back) + # self.initrec_company.update_main_page.connect(self.update_grid) + # self.initrec_company.initrec_person_requested.connect(self.show_page_initrec_person) + # self.initrec_company.company_profile_requested.connect(self.show_page_company_profile) + # self.stack.addWidget(self.initrec_company) # SITE: 'Grunderfassung Person' - ids = EntityIds(type=EntityType.PERSON) - initrec_person_state = Page_InitRecPerson_State( - session=self.STATE.session, ids=ids, locked=False - ) - self.initrec_person = Page_InitRecPerson(initrec_person_state) - self.initrec_person.back_main_requested.connect(self.show_main_page) - self.initrec_person.back_requested.connect(self.simple_go_back) - self.initrec_person.update_triggered.connect(self.update_grid) - self.stack.addWidget(self.initrec_person) + # ids = EntityIds(type=RecordingType.PERSON) + # initrec_person_state = Page_InitRecPerson_State( + # session=self.STATE.session, ids=ids, locked=False + # ) + # self.initrec_person = Page_InitRecPerson(initrec_person_state) + # self.initrec_person.back_main_requested.connect(self.show_main_page) + # self.initrec_person.back_requested.connect(self.simple_go_back) + # self.initrec_person.update_triggered.connect(self.update_grid) + # self.stack.addWidget(self.initrec_person) # SITE: 'Unternehmensprofil' comp_profile_state = Page_CompanyProfile_State( - session=self.STATE.session, ids=EntityIds() + session=self.STATE.session, ids=NewEntityIds() ) self.company_profile = Page_CompanyProfile(comp_profile_state) self.company_profile.back_main_requested.connect(self.show_main_page) @@ -5891,16 +6003,17 @@ class MainWindow(QMainWindow): vert_layout.addWidget(new_btn) if DEBUG: - update_btn = QPushButton("UPDATE") - update_btn.setFixedWidth(100) - update_btn.setFixedHeight(40) - update_btn.clicked.connect(self.update_grid) - clear_btn = QPushButton("CLEAR") - clear_btn.setFixedWidth(100) - clear_btn.setFixedHeight(40) - clear_btn.clicked.connect(self._clear_layout) - vert_layout.addWidget(update_btn) - vert_layout.addWidget(clear_btn) + # TODO reactivate + # update_btn = QPushButton("UPDATE") + # update_btn.setFixedWidth(100) + # update_btn.setFixedHeight(40) + # update_btn.clicked.connect(self.update_grid) + # clear_btn = QPushButton("CLEAR") + # clear_btn.setFixedWidth(100) + # clear_btn.setFixedHeight(40) + # clear_btn.clicked.connect(self._clear_layout) + # vert_layout.addWidget(update_btn) + # vert_layout.addWidget(clear_btn) separator = QFrame() separator.setFrameShape(QFrame.Shape.HLine) separator.setFrameShadow(QFrame.Shadow.Sunken) @@ -5961,68 +6074,70 @@ class MainWindow(QMainWindow): for col_idx, title in enumerate(headers): self.header_grid.addWidget(HeaderCell(title), self.current_row, col_idx) - self.update_grid() + # self.update_grid() return main_widget def _clear_layout(self) -> None: clear_layout(self.grid) - def update_grid(self) -> None: - clear_layout(self.grid) + # def update_grid(self) -> None: + # clear_layout(self.grid) - data = backend.main_page_get_entries() + # data = backend.main_page_get_entries() - for entry in data: - self.add_row_to_grid(entry) + # for entry in data: + # self.add_row_to_grid(entry) - def add_row_to_grid( - self, - entry: types.MainPageEntry, - ): - row = self.current_row + # TODO rework + # def add_row_to_grid( + # self, + # entry: types.MainPageEntry, + # ): + # row = self.current_row - # NAME - cell_name = MainPage_ClickableCell(entry.display_name, entry) - cell_name.clicked.connect(self.goto_initial_recording) - self.grid.addWidget(cell_name, row, 0) - # DATE - cell_date_name = entry.Metadaten_aktualisierung.date().strftime(DATE_FMT) - cell_date = MainPage_ClickableCell(cell_date_name, entry) - cell_date.clicked.connect(self.goto_initial_recording) - self.grid.addWidget(cell_date, row, 4) + # # NAME + # cell_name = MainPage_ClickableCell(entry.display_name, entry) + # cell_name.clicked.connect(self.goto_initial_recording) + # self.grid.addWidget(cell_name, row, 0) + # # DATE + # cell_date_name = entry.Metadaten_aktualisierung.date().strftime(DATE_FMT) + # cell_date = MainPage_ClickableCell(cell_date_name, entry) + # cell_date.clicked.connect(self.goto_initial_recording) + # self.grid.addWidget(cell_date, row, 4) - self.current_row += 1 # Zähler für den nächsten Eintrag erhöhen + # self.current_row += 1 # Zähler für den nächsten Eintrag erhöhen - def goto_initial_recording( - self, - data: types.MainPageEntry, - ): - target: QWidget - if data.type is types.EntityType.COMPANY: - ids = EntityIds(un_id=data.rec_id, type=EntityType.COMPANY) - new_state = Page_InitRecCompany_State( - session=self.STATE.session, - ids=ids, - # un_id=data.rec_id, # TODO remove - locked=True, - ) - self.initrec_company.load_state(new_state) - target = self.initrec_company - elif data.type is types.EntityType.PERSON: - ids = EntityIds(pers_id=data.rec_id, type=EntityType.PERSON) - new_state = Page_InitRecPerson_State( - session=self.STATE.session, - ids=ids, - # pers_id=data.rec_id, # TODO remove - locked=True, - ) - self.initrec_person.load_state(new_state) - target = self.initrec_person - else: - raise TypeError(f"Unknown init rec type: {data.type}") + # TODO rework + # def goto_initial_recording( + # self, + # data: types.MainPageEntry, + # ): + # target: QWidget + # if data.type is types.RecordingType.COMPANY: + # ids = EntityIds(un_id=data.rec_id, type=RecordingType.COMPANY) + # new_state = Page_InitRecCompany_State( + # session=self.STATE.session, + # ids=ids, + # # un_id=data.rec_id, # TODO remove + # locked=True, + # ) + # self.initrec_company.load_state(new_state) + # target = self.initrec_company + # elif data.type is types.RecordingType.PERSON: + # ids = EntityIds(pers_id=data.rec_id, type=RecordingType.PERSON) + # new_state = Page_InitRecPerson_State( + # session=self.STATE.session, + # ids=ids, + # # pers_id=data.rec_id, # TODO remove + # locked=True, + # ) + # self.initrec_person.load_state(new_state) + # target = self.initrec_person + # else: + # raise TypeError(f"Unknown init rec type: {data.type}") - self._navigate_to_target(target) + # self._navigate_to_target(target) def _navigate_to_target( self, @@ -6047,21 +6162,28 @@ class MainWindow(QMainWindow): self._navigate_to_target(self.main_page) def show_new_entry_select(self): - self._navigate_to_target(self.new_initrec_select) + self._navigate_to_target(self.new_entry_select) - def show_page_initrec_company( + def show_page_newinitrec( self, - new_state: Page_InitRecCompany_State, + new_state: Page_InitRec_State, ): - self.initrec_company.load_state(new_state) - self._navigate_to_target(self.initrec_company) + self.newinitrec.load_state(new_state) + self._navigate_to_target(self.newinitrec) - def show_page_initrec_person( - self, - new_state: Page_InitRecPerson_State, - ): - self.initrec_person.load_state(new_state) - self._navigate_to_target(self.initrec_person) + # def show_page_initrec_company( + # self, + # new_state: Page_InitRecCompany_State, + # ): + # self.initrec_company.load_state(new_state) + # self._navigate_to_target(self.initrec_company) + + # def show_page_initrec_person( + # self, + # new_state: Page_InitRecPerson_State, + # ): + # self.initrec_person.load_state(new_state) + # self._navigate_to_target(self.initrec_person) def show_page_company_profile( self, @@ -6135,8 +6257,10 @@ if TYPE_CHECKING: _m1: type[Module] = Page_Consulting _m2: type[Module] = Page_Consulting_Table _m3: type[Module] = Page_Consulting_ConsultingSession - _w1: type[WrapperModule] = Page_InitRecCompany - _w2: type[WrapperModule] = Page_InitRecPerson + _m4: type[Module] = Page_InitRec + _m5: type[Module] = InitRecForm + # _w1: type[WrapperModule] = Page_InitRecCompany + # _w2: type[WrapperModule] = Page_InitRecPerson _f1: type[CustomForm] = DynamicListWidget _f2: type[CustomForm] = DynamicDropdownWidgetNumeric _f3: type[CustomForm] = DynamicDropdownWidgetOption diff --git a/src/wce_crm/logging.py b/src/wce_crm/logging.py index db7b996..d011a09 100644 --- a/src/wce_crm/logging.py +++ b/src/wce_crm/logging.py @@ -30,18 +30,27 @@ logger_base.setLevel(logging.DEBUG) # ** GUI logger_gui = logger_base.getChild("gui") logger_gui.setLevel(logging.DEBUG) + logger_search_widget = logger_gui.getChild("search_widget") -logger_search_widget.setLevel(logging.DEBUG) +logger_search_widget.setLevel(logging.WARNING) + logger_get_data = logger_gui.getChild("get_data") -logger_get_data.setLevel(logging.DEBUG) -logger_auto_form = logger_gui.getChild("get_data_auto_form") -logger_auto_form.setLevel(logging.DEBUG) +logger_get_data.setLevel(logging.WARNING) logger_custom_fields = logger_gui.getChild("custom_fields") -logger_custom_fields.setLevel(logging.DEBUG) +logger_custom_fields.setLevel(logging.WARNING) logger_page_consulting = logger_gui.getChild("page_consulting") -logger_page_consulting.setLevel(logging.DEBUG) +logger_page_consulting.setLevel(logging.WARNING) + +logger_page_initrec = logger_gui.getChild("page_initrec") +logger_page_initrec.setLevel(logging.DEBUG) + +logger_initrecform = logger_gui.getChild("initrecform") +logger_initrecform.setLevel(logging.DEBUG) + +logger_prototyping = logger_gui.getChild("prototyping") +logger_prototyping.setLevel(logging.DEBUG) # ** Backend logger_back = logger_base.getChild("backend") diff --git a/src/wce_crm/types.py b/src/wce_crm/types.py index 9d4fc34..c53072f 100644 --- a/src/wce_crm/types.py +++ b/src/wce_crm/types.py @@ -1,8 +1,10 @@ from __future__ import annotations +import copy import dataclasses as dc import datetime import enum +from collections.abc import Generator from typing import TYPE_CHECKING, TypeAlias, TypedDict if TYPE_CHECKING: @@ -12,13 +14,16 @@ if TYPE_CHECKING: UserId: TypeAlias = int RecId: TypeAlias = int ConsId: TypeAlias = int +IntLinkId: TypeAlias = int ExtMaId: TypeAlias = int ExtAnId: TypeAlias = int PolarsSchema: TypeAlias = dict[str, type["pl.DataType"]] -class EntityType(enum.IntEnum): +# TODO rename: these are not entity types or entities but different forms of +# initial recordings +class RecordingType(enum.IntEnum): COMPANY = enum.auto() PERSON = enum.auto() @@ -29,55 +34,124 @@ class ConsultingType(enum.StrEnum): @dc.dataclass(slots=True, kw_only=True) -class EntityIds: - un_id: RecId | None = None +class NewEntityIds: + un_id: ExtMaId | None = None + an_id: ExtAnId | None = None pers_id: RecId | None = None - type: dc.InitVar[EntityType | None] = None - _ent_type: EntityType | None = dc.field(init=False) + link_id: IntLinkId | None = None - def __post_init__(self, type: EntityType | None) -> None: - self._ent_type = type - - @property - def ent_type(self) -> EntityType | None: - return self._ent_type - - def any(self) -> bool: - return self.un_id is not None or self.pers_id is not None - - def all(self) -> bool: - return self.un_id is not None and self.pers_id is not None - - def valid(self) -> bool: - if self.ent_type is None: - raise TypeError("Initrec type not defined. Can not determine valid state.") - elif self.ent_type is EntityType.COMPANY: - return self.all() - elif self.ent_type is EntityType.PERSON: - return self.any() - else: - raise RuntimeError( - f"Unknown initrec type {self.ent_type}. This is not supported." - ) - - def set_id( + def update( self, - id_: RecId | None, - ent_type: EntityType, + ids: NewEntityIds, + ignore_none: bool = False, + deep: bool = False, ) -> None: - if ent_type is EntityType.COMPANY: - self.un_id = id_ - elif ent_type is EntityType.PERSON: - self.pers_id = id_ + if not isinstance(ids, NewEntityIds): + raise TypeError("New IDs must be have the same type as this object") - def get_id( + for field in dc.fields(ids): + new_value = getattr(ids, field.name) + + if ignore_none and new_value is None: + continue + + if deep and new_value is not None: + new_value = copy.deepcopy(new_value) + + setattr(self, field.name, new_value) + + def _get_relevant_fields( self, - ent_type: EntityType, - ) -> RecId | None: - if ent_type is EntityType.COMPANY: - return self.un_id - elif ent_type is EntityType.PERSON: - return self.pers_id + recording_type: RecordingType | None = None, + ) -> Generator[int | None, None, None]: + if recording_type is None: + relevant_fields = (getattr(self, field.name) for field in dc.fields(self)) + elif recording_type is RecordingType.COMPANY: + RELEVANT_ATTRIBUTES = {"un_id", "an_id", "link_id"} + relevant_fields = ( + getattr(self, field.name) + for field in dc.fields(self) + if field.name in RELEVANT_ATTRIBUTES + ) + elif recording_type is RecordingType.PERSON: + RELEVANT_ATTRIBUTES = {"pers_id"} + relevant_fields = ( + getattr(self, field.name) + for field in dc.fields(self) + if field.name in RELEVANT_ATTRIBUTES + ) + else: + raise RuntimeError(f"Unspecified recording type {recording_type}") + + return relevant_fields + + def empty( + self, + recording_type: RecordingType | None = None, + ) -> bool: + relevant_fields = self._get_relevant_fields(recording_type=recording_type) + + return not any(relevant_fields) + + def valid( + self, + recording_type: RecordingType | None = None, + ) -> bool: + relevant_fields = self._get_relevant_fields(recording_type=recording_type) + + return all(relevant_fields) + + +# @dc.dataclass(slots=True, kw_only=True) +# class EntityIds: +# un_id: RecId | None = None +# pers_id: RecId | None = None +# type: dc.InitVar[RecordingType | None] = None +# _rec_type: RecordingType | None = dc.field(init=False) + +# def __post_init__(self, type: RecordingType | None) -> None: +# self._rec_type = type + +# @property +# def rec_type(self) -> RecordingType | None: +# return self._rec_type + +# def any(self) -> bool: +# return self.un_id is not None or self.pers_id is not None + +# def all(self) -> bool: +# return self.un_id is not None and self.pers_id is not None + +# def valid(self) -> bool: +# if self.rec_type is None: +# raise TypeError("Initrec type not defined. Can not determine valid state.") +# elif self.rec_type is RecordingType.COMPANY: +# return self.all() +# elif self.rec_type is RecordingType.PERSON: +# return self.any() +# else: +# raise RuntimeError( +# f"Unknown initrec type {self.rec_type}. This is not supported." +# ) + +# def set_id( +# self, +# id_: RecId | None, +# ent_type: RecordingType, +# ) -> None: +# if ent_type is RecordingType.COMPANY: +# self.un_id = id_ +# elif ent_type is RecordingType.PERSON: +# self.pers_id = id_ + +# def get_id( +# self, +# ent_type: RecordingType, +# ) -> RecId | None: +# if ent_type is RecordingType.COMPANY: +# return self.un_id +# elif ent_type is RecordingType.PERSON: +# return self.pers_id # // database interaction data structures @@ -140,7 +214,7 @@ class MainPageEntry: rec_id: RecId display_name: str Metadaten_aktualisierung: datetime.datetime - type: EntityType + type: RecordingType @dc.dataclass(slots=True) -- 2.34.1 From acfc4a824ae7044306b98b86e597c7d5111470b8 Mon Sep 17 00:00:00 2001 From: foefl Date: Fri, 24 Jul 2026 17:10:17 +0200 Subject: [PATCH 04/13] continue work on new data model --- ...8000d15faf03_major_change_in_data_model.py | 317 ++++++++++++++ prototypes/dual_dict.py | 54 +++ prototypes/tests.py | 40 +- src/wce_crm/backend/backend.py | 205 ++++++++- src/wce_crm/data_models.py | 112 ++++- src/wce_crm/db.py | 7 +- src/wce_crm/form_defs.py | 72 ++-- src/wce_crm/gui.py | 390 +++++++++++++----- src/wce_crm/types.py | 49 ++- 9 files changed, 1054 insertions(+), 192 deletions(-) create mode 100644 alembic/versions/8000d15faf03_major_change_in_data_model.py create mode 100644 prototypes/dual_dict.py diff --git a/alembic/versions/8000d15faf03_major_change_in_data_model.py b/alembic/versions/8000d15faf03_major_change_in_data_model.py new file mode 100644 index 0000000..5242e0a --- /dev/null +++ b/alembic/versions/8000d15faf03_major_change_in_data_model.py @@ -0,0 +1,317 @@ +"""major change in data model + +Revision ID: 8000d15faf03 +Revises: 665259cdfdc0 +Create Date: 2026-07-24 15:13:57.443684 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +import wce_crm.db + +# revision identifiers, used by Alembic. +revision: str = "8000d15faf03" +down_revision: Union[str, Sequence[str], None] = "665259cdfdc0" +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.drop_table("grunderfassung_personen") + op.drop_table("beratung_vorgang") + op.drop_table("grunderfassung_unternehmen") + with op.batch_alter_table("zuordnung_personen_unternehmen", schema=None) as batch_op: + batch_op.drop_index( + batch_op.f("uq_aktive_zuordnung"), sqlite_where=sa.text("gueltig_bis IS NULL") + ) + + op.drop_table("zuordnung_personen_unternehmen") + op.drop_table("beratung_einzelberatung") + + op.create_table( + "Grunderfassung_Personen", + sa.Column("pers_id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("erstellt", wce_crm.db.UTCDateTime(), nullable=False), + sa.Column("aktualisiert", wce_crm.db.UTCDateTime(), nullable=False), + sa.Column("geloescht", 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( + "Unternehmen", + sa.Column("un_id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("erstellt", wce_crm.db.UTCDateTime(), nullable=False), + sa.Column("geloescht", wce_crm.db.UTCDateTime(), nullable=True), + sa.PrimaryKeyConstraint("un_id"), + ) + op.create_table( + "Beratung_Vorgang", + sa.Column("vorgang_id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("un_id", sa.Integer(), nullable=True), + sa.Column("pers_id", sa.Integer(), nullable=True), + sa.Column("titel", sa.Text(), nullable=False), + sa.Column("beratungs_typ", sa.String(length=20), nullable=False), + sa.Column("erstellt", wce_crm.db.UTCDateTime(), nullable=False), + sa.Column("aktualisiert", wce_crm.db.UTCDateTime(), nullable=False), + sa.Column("geloescht", wce_crm.db.UTCDateTime(), nullable=True), + sa.ForeignKeyConstraint( + ["pers_id"], + ["Grunderfassung_Personen.pers_id"], + ), + sa.ForeignKeyConstraint( + ["un_id"], + ["Unternehmen.un_id"], + ), + sa.PrimaryKeyConstraint("vorgang_id"), + ) + op.create_table( + "Personen_Unternehmen_Zuordnung", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("un_id", sa.Integer(), nullable=False), + sa.Column("an_id", sa.Integer(), nullable=False), + sa.Column("pers_id", sa.Integer(), nullable=False), + sa.Column("erstellt", wce_crm.db.UTCDateTime(), nullable=False), + sa.Column("aktualisiert", wce_crm.db.UTCDateTime(), nullable=False), + sa.Column("geloescht", wce_crm.db.UTCDateTime(), nullable=True), + sa.ForeignKeyConstraint( + ["pers_id"], + ["Grunderfassung_Personen.pers_id"], + ), + sa.ForeignKeyConstraint( + ["un_id"], + ["Unternehmen.un_id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + with op.batch_alter_table("Personen_Unternehmen_Zuordnung", schema=None) as batch_op: + batch_op.create_index( + "uq_aktive_zuordnung", + ["un_id", "pers_id"], + unique=True, + sqlite_where=sa.text("geloescht IS NULL"), + ) + + op.create_table( + "Beratung_Einzelberatung", + sa.Column("beratung_id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("vorgang_id", sa.Integer(), nullable=False), + sa.Column("nutzer_id", sa.Integer(), nullable=False), + sa.Column("nutzer_name", sa.String(length=20), nullable=False), + sa.Column("zeitstempel", wce_crm.db.UTCDateTime(), nullable=False), + sa.Column("ansprechpartner", sa.Text(), nullable=False), + sa.Column("kommunikationsweg", sa.Text(), nullable=False), + sa.Column("thema_crm_matrix", sa.Text(), nullable=True), + sa.Column("anmerkungen", sa.Text(), nullable=True), + sa.Column("rueckmeldung", sa.Text(), nullable=True), + sa.Column("erstellt", wce_crm.db.UTCDateTime(), nullable=False), + sa.Column("aktualisiert", wce_crm.db.UTCDateTime(), nullable=False), + sa.Column("geloescht", wce_crm.db.UTCDateTime(), nullable=True), + sa.ForeignKeyConstraint( + ["vorgang_id"], + ["Beratung_Vorgang.vorgang_id"], + ), + sa.PrimaryKeyConstraint("beratung_id"), + ) + + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("Beratung_Einzelberatung") + with op.batch_alter_table("Personen_Unternehmen_Zuordnung", schema=None) as batch_op: + batch_op.drop_index("uq_aktive_zuordnung", sqlite_where=sa.text("geloescht IS NULL")) + + op.drop_table("Personen_Unternehmen_Zuordnung") + op.drop_table("Beratung_Vorgang") + op.drop_table("Unternehmen") + op.drop_table("Grunderfassung_Personen") + + op.create_table( + "beratung_einzelberatung", + sa.Column("beratung_id", sa.INTEGER(), nullable=False), + sa.Column("vorgang_id", sa.INTEGER(), nullable=False), + sa.Column("nutzer_id", sa.INTEGER(), nullable=False), + sa.Column("nutzer_name", sa.VARCHAR(length=20), nullable=False), + sa.Column("zeitstempel", sa.DATETIME(), nullable=False), + sa.Column("ansprechpartner", sa.TEXT(), nullable=False), + sa.Column("kommunikationsweg", sa.TEXT(), nullable=False), + sa.Column("thema_crm_matrix", sa.TEXT(), nullable=True), + sa.Column("anmerkungen", sa.TEXT(), nullable=True), + sa.Column("rueckmeldung", sa.TEXT(), nullable=True), + sa.Column("erstellt", sa.DATETIME(), nullable=False), + sa.Column("aktualisiert", sa.DATETIME(), nullable=False), + sa.Column("geloescht", sa.DATETIME(), nullable=True), + sa.ForeignKeyConstraint( + ["vorgang_id"], + ["beratung_vorgang.vorgang_id"], + ), + sa.PrimaryKeyConstraint("beratung_id"), + ) + op.create_table( + "zuordnung_personen_unternehmen", + sa.Column("id", sa.INTEGER(), nullable=False), + sa.Column("un_id", sa.INTEGER(), nullable=False), + sa.Column("pers_id", sa.INTEGER(), nullable=False), + sa.Column("gueltig_ab", sa.DATETIME(), nullable=False), + sa.Column("gueltig_bis", sa.DATETIME(), nullable=True), + sa.ForeignKeyConstraint( + ["pers_id"], + ["grunderfassung_personen.pers_id"], + ), + sa.ForeignKeyConstraint( + ["un_id"], + ["grunderfassung_unternehmen.un_id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + with op.batch_alter_table("zuordnung_personen_unternehmen", schema=None) as batch_op: + batch_op.create_index( + batch_op.f("uq_aktive_zuordnung"), + ["un_id", "pers_id"], + unique=1, + sqlite_where=sa.text("gueltig_bis IS NULL"), + ) + + op.create_table( + "grunderfassung_unternehmen", + sa.Column("un_id", sa.INTEGER(), nullable=False), + sa.Column("Metadaten_erstellung", sa.DATETIME(), nullable=True), + sa.Column("Metadaten_aktualisierung", sa.DATETIME(), nullable=True), + sa.Column("Metadaten_nutzer", sa.VARCHAR(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.Column("geloescht", sa.DATETIME(), nullable=True), + sa.PrimaryKeyConstraint("un_id"), + ) + op.create_table( + "beratung_vorgang", + sa.Column("vorgang_id", sa.INTEGER(), nullable=False), + sa.Column("un_id", sa.INTEGER(), nullable=True), + sa.Column("pers_id", sa.INTEGER(), nullable=True), + sa.Column("titel", sa.TEXT(), nullable=False), + sa.Column("beratungs_typ", sa.VARCHAR(length=20), nullable=False), + sa.Column("erstellt", sa.DATETIME(), nullable=False), + sa.Column("aktualisiert", sa.DATETIME(), nullable=False), + sa.Column("geloescht", sa.DATETIME(), nullable=True), + sa.ForeignKeyConstraint( + ["pers_id"], + ["grunderfassung_personen.pers_id"], + ), + sa.ForeignKeyConstraint( + ["un_id"], + ["grunderfassung_unternehmen.un_id"], + ), + sa.PrimaryKeyConstraint("vorgang_id"), + ) + op.create_table( + "grunderfassung_personen", + sa.Column("pers_id", sa.INTEGER(), nullable=False), + sa.Column("Metadaten_erstellung", sa.DATETIME(), nullable=True), + sa.Column("Metadaten_aktualisierung", sa.DATETIME(), nullable=True), + sa.Column("Metadaten_nutzer", sa.VARCHAR(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.Column("geloescht", sa.DATETIME(), nullable=True), + sa.PrimaryKeyConstraint("pers_id"), + ) + + # ### end Alembic commands ### diff --git a/prototypes/dual_dict.py b/prototypes/dual_dict.py new file mode 100644 index 0000000..17fc7b3 --- /dev/null +++ b/prototypes/dual_dict.py @@ -0,0 +1,54 @@ +# %% +from __future__ import annotations + +import copy +from typing import Any + +from dopt_basics.datastructures import DualDict + +from wce_crm.data_models import FIELD_DB_MAPPING_GRUNDERERFASSUNG_PERSONEN + +# %% +dd: DualDict[str, str] = DualDict(peter="test") + + +# %% +def rename_db_data( + db_data: dict[str, Any], + mapping: DualDict, +) -> dict[str, Any]: + new_db_data = copy.deepcopy(db_data) + + for k, v in db_data.items(): + if k in mapping: + new_db_data[mapping[k]] = v + del new_db_data[k] + + if k in mapping.inverted: + new_db_data[mapping.inverted[k]] = v + del new_db_data[k] + + return new_db_data + + +form_data = { + "Metadaten_erstellung": "t1", + "Metadaten_aktualisierung": "t2", + "Metadaten_nutzer": "t3", +} +db_data = rename_db_data(form_data, FIELD_DB_MAPPING_GRUNDERERFASSUNG_PERSONEN) +# %% +db_data + +# %% +cvt_form_data = rename_db_data(db_data, FIELD_DB_MAPPING_GRUNDERERFASSUNG_PERSONEN) +cvt_form_data +# %% +dd["test"] = "peter" +# %% +dd["test"] +# %% +dd.inverted["peter"] +# %% +dd +# %% diff --git a/prototypes/tests.py b/prototypes/tests.py index a2cb9a6..3644836 100644 --- a/prototypes/tests.py +++ b/prototypes/tests.py @@ -19,11 +19,11 @@ from PySide6.QtCore import QDate, Qt from wce_crm import constants, db from wce_crm.backend import backend -from wce_crm.types import NewEntityIds, RecordingType +from wce_crm.types import EntityIds, RecordingType # %% -raw_ent_ids = NewEntityIds() -db_ent_ids = NewEntityIds(un_id=3, an_id=2) +raw_ent_ids = EntityIds() +db_ent_ids = EntityIds(un_id=3, an_id=2) print(f"{raw_ent_ids=}\n{db_ent_ids=}") print(f"{raw_ent_ids.empty()=}") @@ -31,36 +31,36 @@ print(f"{raw_ent_ids.valid()=}") # %% raw_ent_ids.update(db_ent_ids) print(f"{raw_ent_ids=}") -db_ent_ids = NewEntityIds(un_id=None, an_id=2, link_id=1) +db_ent_ids = EntityIds(un_id=None, an_id=2, link_id=1) raw_ent_ids.update(db_ent_ids) print(f"{raw_ent_ids=}") print(f"{raw_ent_ids.empty()=}") print(f"{raw_ent_ids.valid()=}") -raw_ent_ids.update(NewEntityIds()) +raw_ent_ids.update(EntityIds()) print(f"{raw_ent_ids=}") print(f"{raw_ent_ids.empty()=}") print(f"{raw_ent_ids.valid()=}") # %% -raw_ent_ids.update(NewEntityIds(un_id=None, pers_id=2)) +raw_ent_ids.update(EntityIds(un_id=None, pers_id=2)) print(f"{raw_ent_ids=}") print(f"{raw_ent_ids.empty()=}") -print(f"{raw_ent_ids.empty(RecordingType.COMPANY)=}") -print(f"{raw_ent_ids.empty(RecordingType.PERSON)=}") -print(f"{raw_ent_ids.valid(RecordingType.COMPANY)=}") -print(f"{raw_ent_ids.valid(RecordingType.PERSON)=}") -raw_ent_ids.update(NewEntityIds(un_id=None, an_id=2, link_id=3, pers_id=2)) +print(f"{raw_ent_ids.empty(RecordingType.WITH_COMPANY)=}") +print(f"{raw_ent_ids.empty(RecordingType.WITHOUT_COMPANY)=}") +print(f"{raw_ent_ids.valid(RecordingType.WITH_COMPANY)=}") +print(f"{raw_ent_ids.valid(RecordingType.WITHOUT_COMPANY)=}") +raw_ent_ids.update(EntityIds(un_id=None, an_id=2, link_id=3, pers_id=2)) print(f"{raw_ent_ids=}") -print(f"{raw_ent_ids.empty(RecordingType.COMPANY)=}") -print(f"{raw_ent_ids.empty(RecordingType.PERSON)=}") -print(f"{raw_ent_ids.valid(RecordingType.COMPANY)=}") -print(f"{raw_ent_ids.valid(RecordingType.PERSON)=}") -raw_ent_ids.update(NewEntityIds(un_id=2, pers_id=None, an_id=2, link_id=3)) +print(f"{raw_ent_ids.empty(RecordingType.WITH_COMPANY)=}") +print(f"{raw_ent_ids.empty(RecordingType.WITHOUT_COMPANY)=}") +print(f"{raw_ent_ids.valid(RecordingType.WITH_COMPANY)=}") +print(f"{raw_ent_ids.valid(RecordingType.WITHOUT_COMPANY)=}") +raw_ent_ids.update(EntityIds(un_id=2, pers_id=None, an_id=2, link_id=3)) print(f"{raw_ent_ids=}") -print(f"{raw_ent_ids.empty(RecordingType.COMPANY)=}") -print(f"{raw_ent_ids.empty(RecordingType.PERSON)=}") -print(f"{raw_ent_ids.valid(RecordingType.COMPANY)=}") -print(f"{raw_ent_ids.valid(RecordingType.PERSON)=}") +print(f"{raw_ent_ids.empty(RecordingType.WITH_COMPANY)=}") +print(f"{raw_ent_ids.empty(RecordingType.WITHOUT_COMPANY)=}") +print(f"{raw_ent_ids.valid(RecordingType.WITH_COMPANY)=}") +print(f"{raw_ent_ids.valid(RecordingType.WITHOUT_COMPANY)=}") # %% db_path = constants.Config.DB_PATH_MAIN diff --git a/src/wce_crm/backend/backend.py b/src/wce_crm/backend/backend.py index 5ee9f50..f21406c 100644 --- a/src/wce_crm/backend/backend.py +++ b/src/wce_crm/backend/backend.py @@ -13,9 +13,12 @@ from sqlalchemy.dialects.sqlite import insert as sqlite_insert from wce_crm import db from wce_crm.constants import TIMEZONE_CEST from wce_crm.data_models import ( + FIELD_DB_MAPPING_GRUNDERERFASSUNG_PERSONEN, + FIELD_DB_MAPPING_GRUNDERERFASSUNG_UNTERNEHMEN, Beratungsgespraech_Einzelgespraech, Beratungsgespraech_Vorgang, InitRec, + Page_InitRec_Form_Data, ) from wce_crm.logging import logger_back as logger from wce_crm.types import ( @@ -24,14 +27,17 @@ from wce_crm.types import ( CompanyProfileConsultations, ConsultingType, ContactPersonInfo, + EntityIds, + EntityType, LinkingConsultationsEntryCompany, LinkingConsultationsEntryPerson, MainPageEntry, - NewEntityIds, RecordingType, ) if TYPE_CHECKING: + from dopt_basics.datastructures import DualDict + from wce_crm.types import ConsId, ExtAnId, ExtMaId, RecId @@ -348,14 +354,201 @@ def initrec_comp_contact_person_search_get_info( # return dict(row) +def _rename_db_data( + db_data: dict[str, Any], + mapping: DualDict, +) -> dict[str, Any]: + new_db_data = copy.deepcopy(db_data) + + for k, v in db_data.items(): + if k in mapping: + new_db_data[mapping[k]] = v + del new_db_data[k] + + if k in mapping.inverted: + new_db_data[mapping.inverted[k]] = v + del new_db_data[k] + + return new_db_data + + def page_initrec_to_db( data: InitRec, -) -> InitRec: ... +) -> InitRec: + + with db.ENGINE.begin() as conn: + # person + dump_data = _rename_db_data( + data.form_data.person, FIELD_DB_MAPPING_GRUNDERERFASSUNG_PERSONEN + ) + dump_data["geloescht"] = data.geloescht + + if data.ids.pers_id is None: + logger.debug("[InitRec -- backend] Insert...") + + stmt = sa.insert(db.t_grunderfassung_personen).returning( + db.t_grunderfassung_personen.c.pers_id, + db.t_grunderfassung_personen.c.aktualisiert, + db.t_grunderfassung_personen.c.geloescht, + ) + stmt_compiled = str(stmt.compile(db.ENGINE)) + logger.debug( + "[InitRec -- backend] Data to insert:\nStatement: %s\n%s", + stmt_compiled, + pformat(dump_data), + ) + + ret = conn.execute(stmt, dump_data) + + from_db = ret.mappings().fetchall() + assert len(from_db) == 1, "expected excactly one returned row" + data_from_db = from_db[0] + + data.ids.pers_id = data_from_db["pers_id"] + data.geloescht = data_from_db["geloescht"] + data.form_data.person["Metadaten_aktualisierung"] = data_from_db["aktualisiert"] + + logger.debug("[AutoForm -- backend] Inserted InitRec Person successfully") + + else: + logger.debug("[AutoForm -- backend] Update...") + stmt = ( + db.t_grunderfassung_personen.update() + .where(db.t_grunderfassung_personen.c.pers_id == data.ids.pers_id) + .returning( + db.t_grunderfassung_personen.c.Metadaten_aktualisierung, + db.t_grunderfassung_personen.c.geloescht, + ) + ) + ret = conn.execute(stmt, dump_data) + + from_db = ret.mappings().fetchall() + assert len(from_db) == 1, "expected excatly one returned row" + data_from_db = from_db[0] + + data.geloescht = data_from_db["geloescht"] + data.form_data.person["Metadaten_aktualisierung"] = data_from_db[ + "Metadaten_aktualisierung" + ] + + logger.debug( + "[InitRec -- backend] Updated InitRec Person with ID %d successfully", + data.ids.pers_id, + ) + + # company + if data.recording_type is RecordingType.WITH_COMPANY: + # insert into company table and add additional information + assert data.ids.un_id, "company ID not set" + assert data.ids.an_id, "contact person (an) ID not set" + assert data.ids.pers_id, "person ID not set" + + stmt = ( + sqlite_insert(db.t_unternehmen) + .values(un_id=data.ids.un_id, geloescht=None) + .on_conflict_do_nothing(index_elements=["un_id"]) + ) + conn.execute(stmt) + + # additional information + dump_data = _rename_db_data( + data.form_data.company, FIELD_DB_MAPPING_GRUNDERERFASSUNG_UNTERNEHMEN + ) + dump_data["geloescht"] = data.geloescht + stmt = sqlite_insert(db.t_zuordnung_personen_unternehmen).values( + **dump_data, pers_id=data.ids.pers_id + ) + stmt = stmt.on_conflict_do_update( + index_elements=["un_id", "pers_id"], + index_where=sa.text("geloescht IS NULL"), + set_={ + "an_id": stmt.excluded.an_id, + "Partnersuche__kanal_aufmerksamkeit": stmt.excluded.Partnersuche__kanal_aufmerksamkeit, + "aktualisiert": datetime.datetime.now(datetime.UTC), + }, + ).returning(db.t_zuordnung_personen_unternehmen.c.id) + ret = conn.execute(stmt) + + from_db = ret.mappings().fetchall() + assert len(from_db) == 1, "expected excatly one returned row" + data_from_db = from_db[0] + data.ids.link_id = data_from_db["id"] + + return data def page_initrec_from_db( - ids: NewEntityIds, -) -> InitRec: ... + ids: EntityIds, +) -> InitRec: + recording_type = RecordingType.WITHOUT_COMPANY + if ids.valid(EntityType.COMPANY): + recording_type = RecordingType.WITH_COMPANY + + # person + assert ids.pers_id, "person ID not set" + logger.debug("[AutoForm -- backend] Call database loading routine...") + stmt = db.t_grunderfassung_personen.select().where( + db.t_grunderfassung_personen.c.pers_id == ids.pers_id + ) + with db.ENGINE.connect() as conn: + ret = conn.execute(stmt) + + from_db = ret.mappings().all() + assert len(from_db) == 1, "not excactly one company initial recording obtained" + data_person_table = dict(from_db[0]) + + data_person_table = _rename_db_data( + data_person_table, FIELD_DB_MAPPING_GRUNDERERFASSUNG_PERSONEN + ) + geloescht = data_person_table["geloescht"] + del data_person_table["geloescht"] + + data_company_table: dict[str, Any] = {} + if recording_type is RecordingType.WITH_COMPANY: + assert ids.un_id, "company ID not set" + + if ids.link_id: + stmt = sa.select( + db.t_zuordnung_personen_unternehmen.c.un_id, + db.t_zuordnung_personen_unternehmen.c.an_id, + db.t_zuordnung_personen_unternehmen.c.Partnersuche__kanal_aufmerksamkeit, + ).where( + db.t_zuordnung_personen_unternehmen.c.id == ids.link_id, + ) + else: + stmt = sa.select( + db.t_zuordnung_personen_unternehmen.c.un_id, + db.t_zuordnung_personen_unternehmen.c.an_id, + db.t_zuordnung_personen_unternehmen.c.Partnersuche__kanal_aufmerksamkeit, + ).where( + db.t_zuordnung_personen_unternehmen.c.pers_id == ids.pers_id, + db.t_zuordnung_personen_unternehmen.c.un_id == ids.un_id, + db.t_zuordnung_personen_unternehmen.c.geloescht.is_(None), + ) + + with db.ENGINE.connect() as conn: + ret = conn.execute(stmt) + + from_db = ret.mappings().all() + assert len(from_db) == 1, "not excactly one company initial recording obtained" + data_company_table = dict(from_db[0]) + + data_company_table = _rename_db_data( + data_person_table, FIELD_DB_MAPPING_GRUNDERERFASSUNG_UNTERNEHMEN + ) + + form_data = Page_InitRec_Form_Data( + metadata={}, + person=data_person_table, + company=data_company_table, + ) + + return InitRec( + ids=ids, + recording_type=recording_type, + geloescht=geloescht, + form_data=form_data, + ) # def initrec_company_delete_initial_recording( @@ -857,7 +1050,7 @@ def _main_page_get_company_list() -> list[MainPageEntry]: rec_id=rec_id, display_name=display_name, Metadaten_aktualisierung=datetime_akt, - type=RecordingType.COMPANY, + type=RecordingType.WITH_COMPANY, ) ) @@ -894,7 +1087,7 @@ def _main_page_get_person_list() -> list[MainPageEntry]: rec_id=rec_id, display_name=display_name, Metadaten_aktualisierung=datetime_akt, - type=RecordingType.PERSON, + type=RecordingType.WITHOUT_COMPANY, ) ) diff --git a/src/wce_crm/data_models.py b/src/wce_crm/data_models.py index 83c83bb..e52daeb 100644 --- a/src/wce_crm/data_models.py +++ b/src/wce_crm/data_models.py @@ -7,6 +7,7 @@ import json from collections.abc import Iterable, Sequence from typing import TYPE_CHECKING, Annotated, Any, Final, Generic, Protocol, TypeVar +from dopt_basics.datastructures import DualDict from pydantic import ( AwareDatetime, BaseModel, @@ -18,12 +19,12 @@ from pydantic import ( ) from pydantic_core import ErrorDetails -from wce_crm.form_defs import FormField -from wce_crm.types import NewEntityIds +from wce_crm.form_defs import INITREC_COMP, INITREC_PERSON, FormField +from wce_crm.types import EntityIds, EntityType, RecordingType if TYPE_CHECKING: from wce_crm.gui import InitRecForm, Page_Consulting_ConsultingSession # noqa: F401 - from wce_crm.types import ConsId, ConsultingType, RecId, RecordingType, UserId + from wce_crm.types import ConsId, ConsultingType, EntityType, RecId, UserId ValidAge = Annotated[int, Field(ge=0, le=99)] COLUMN_SEP: Final[str] = "__" @@ -163,7 +164,7 @@ class InitRecFormToDb(Protocol): class InitRecFormFromDb(Protocol): def __call__( self, - ids: NewEntityIds, + ids: EntityIds, ) -> InitRec: ... @@ -171,6 +172,7 @@ class InitRecFormFromDb(Protocol): def set_page_state( target: S, source: S, + ignore_attr: set[str] | None = None, deep: bool = False, ) -> None: """ @@ -182,8 +184,12 @@ def set_page_state( if type(target) is not type(source): raise TypeError("Both source and target must have the same type") + if ignore_attr is None: + ignore_attr = set() + ignore_attr.add("child_modules") + for field in dc.fields(source): - if field.name == "child_modules": + if field.name in ignore_attr: continue new_value = getattr(source, field.name) @@ -216,35 +222,63 @@ class Page_NewEntry_State(PageState[Module]): session: Session +INITREC_REC_TO_ENTITIES: Final[dict[RecordingType, frozenset[EntityType]]] = { + RecordingType.WITH_COMPANY: frozenset((EntityType.COMPANY, EntityType.PERSON)), + RecordingType.WITHOUT_COMPANY: frozenset((EntityType.PERSON,)), +} + + +@dc.dataclass(slots=True, kw_only=True) +class Page_InitRec_Form_Data: + metadata: dict[str, Any] + person: dict[str, Any] + company: dict[str, Any] + + # !! there must be exactly one InitRecForm for each recording type @dc.dataclass(slots=True, kw_only=True) class Page_InitRec_State(PageState["InitRecForm"]): session: Session - ids: NewEntityIds = dc.field(default_factory=NewEntityIds) - record_type: RecordingType + ids: EntityIds = dc.field(default_factory=EntityIds) + recording_type: RecordingType # un_id: RecId | None = None locked: bool geloescht: bool = False - form_data: dict[RecordingType, dict[str, Any]] = dc.field(default_factory=dict) - pydantic_models: dict[RecordingType, type[FlatBaseModel]] = dc.field(default_factory=dict) + form_data: Page_InitRec_Form_Data + initrec_form_states: dict[EntityType, Page_InitRec_Form_State] = dc.field( + init=False, default_factory=dict + ) + # pydantic_models: dict[RecordingType, type[FlatBaseModel]] = dc.field(default_factory=dict) + + def __post_init__(self) -> None: + relevant_entities = INITREC_REC_TO_ENTITIES[self.recording_type] + for ent_type in relevant_entities: + initrec_form_state = Page_InitRec_Form_State( + session=self.session, + cfg=INITREC_CONFIGS[ent_type], + ids=self.ids, + ent_type=ent_type, + locked=self.locked, + geloescht=self.geloescht, + ) + self.initrec_form_states[ent_type] = initrec_form_state @dc.dataclass(slots=True, kw_only=True) class Page_InitRec_Form_Config: pydantic_model: type[FlatBaseModel] - # to_db: InitRecFormToDb # TODO remove - # from_db: InitRecFormFromDb # TODO remove form_fields: Sequence[FormField] ignored_keys: Iterable[str] = tuple() add_buttons: bool = False + id_mapping: dict[str, str] @dc.dataclass(slots=True, kw_only=True) class Page_InitRec_Form_State(PageState[Module]): session: Session cfg: Page_InitRec_Form_Config - ids: NewEntityIds - recording_type: RecordingType + ids: EntityIds + ent_type: EntityType # rec_id: RecId | None = None form_data: dict[str, Any] = dc.field(default_factory=dict) locked: bool @@ -269,7 +303,6 @@ class Page_InitRec_Form_State(PageState[Module]): # locked: bool -# TODO change to entity IDs @dc.dataclass(slots=True, kw_only=True) class Page_Consulting_State(PageState[Module]): session: Session @@ -285,7 +318,6 @@ class Page_Consulting_State(PageState[Module]): locked: bool -# TODO change to entity IDs @dc.dataclass(slots=True, kw_only=True) class Page_Consulting_Linking_State(PageState[Module]): session: Session @@ -322,7 +354,7 @@ class Page_Consulting_Table_State(PageState["Page_Consulting_ConsultingSession"] @dc.dataclass(slots=True, kw_only=True) class Page_CompanyProfile_State(PageState[Module]): session: Session - ids: NewEntityIds + ids: EntityIds # rec_id: RecId | None = None @@ -441,10 +473,21 @@ class FlatBaseModel(BaseModel): # ** InitRec class InitRec(BaseModel): - ids: NewEntityIds - # rec_id: RecId | None + ids: EntityIds + recording_type: RecordingType geloescht: AwareDatetime | None = None - db_data: dict[RecordingType, dict[str, Any]] + form_data: Page_InitRec_Form_Data + + +# mapping between initrec form field keys and database properties +FIELD_DB_MAPPING_GRUNDERERFASSUNG_PERSONEN: Final[DualDict[str, str]] = DualDict( + Metadaten_erstellung="erstellt", + Metadaten_aktualisierung="aktualisiert", +) +FIELD_DB_MAPPING_GRUNDERERFASSUNG_UNTERNEHMEN: Final[DualDict[str, str]] = DualDict( + Partnersuche__un_suche="un_id", + Partnersuche__person_suche="an_id", +) class Grunderfassung_Unternehmen(FlatBaseModel): @@ -649,3 +692,34 @@ class Beratungsgespraech_Einzelgespraech(BaseModel): erstellt: AwareDatetime | None = Field(default=None, exclude=True) aktualisiert: AwareDatetime | None = Field(default=None, exclude=True) # see above geloescht: AwareDatetime | None = None + + +INITREC_CONFIGS: Final[dict[EntityType, Page_InitRec_Form_Config]] = { + EntityType.COMPANY: Page_InitRec_Form_Config( + pydantic_model=Grunderfassung_Unternehmen, + # to_db=backend.initrec_company_to_db, # TODO remove + # from_db=backend.initrec_company_from_db, # TODO remove + ignored_keys=( + "Metadaten_erstellung", + "Metadaten_aktualisierung", + ), + form_fields=INITREC_COMP, + add_buttons=False, + id_mapping={ + "Partnersuche__un_suche": "un_id", + "Partnersuche__person_suche": "an_id", + }, + ), + EntityType.PERSON: Page_InitRec_Form_Config( + pydantic_model=Grunderfassung_Personen, + # to_db=backend.initrec_person_to_db, # TODO remove + # from_db=backend.initrec_person_from_db, # TODO remove + ignored_keys=( + "Metadaten_erstellung", + "Metadaten_aktualisierung", + ), + form_fields=INITREC_PERSON, + add_buttons=False, + id_mapping={}, + ), +} diff --git a/src/wce_crm/db.py b/src/wce_crm/db.py index 1c6b883..d5450b3 100644 --- a/src/wce_crm/db.py +++ b/src/wce_crm/db.py @@ -490,6 +490,7 @@ t_zuordnung_personen_unternehmen: Table = Table( UTCDateTime, nullable=True, ), + Column("Partnersuche__kanal_aufmerksamkeit", sa.Text, nullable=True), # Column( # "gueltig_ab", # UTCDateTime, @@ -520,8 +521,8 @@ t_beratung_vorgang: Table = Table( primary_key=True, autoincrement=True, ), - Column("un_id", sa.ForeignKey("grunderfassung_unternehmen.un_id"), nullable=True), - Column("pers_id", sa.ForeignKey("grunderfassung_personen.pers_id"), nullable=True), + Column("un_id", sa.ForeignKey("Unternehmen.un_id"), nullable=True), + Column("pers_id", sa.ForeignKey("Grunderfassung_Personen.pers_id"), nullable=True), Column("titel", sa.Text, nullable=False), Column("beratungs_typ", sa.String(20), nullable=False), Column( @@ -554,7 +555,7 @@ t_beratung_einzelberatung: Table = Table( primary_key=True, autoincrement=True, ), - Column("vorgang_id", sa.ForeignKey("beratung_vorgang.vorgang_id"), nullable=False), + Column("vorgang_id", sa.ForeignKey("Beratung_Vorgang.vorgang_id"), nullable=False), Column("nutzer_id", sa.Integer, nullable=False), Column("nutzer_name", sa.String(20), nullable=False), Column( diff --git a/src/wce_crm/form_defs.py b/src/wce_crm/form_defs.py index 765f6e4..9650dc5 100644 --- a/src/wce_crm/form_defs.py +++ b/src/wce_crm/form_defs.py @@ -889,42 +889,42 @@ INITREC_LANGUAGES = [ INITREC_COMP = [ - FormField( - "Ersteintrag Datum", - FormFieldType.TEXT_DATETIME, - required=False, - key="Metadaten_erstellung", - readonly=True, - ignore_get_data=True, - ), - FormField( - "Aktualisierung Datum", - FormFieldType.TEXT_DATETIME, - required=False, - key="Metadaten_aktualisierung", - readonly=True, - ignore_get_data=True, - ), - FormField( - "Aktualisierung Nutzer", - FormFieldType.TEXT, - required=False, - key="Metadaten_nutzer", - readonly=True, - ignore_get_data=True, - ), - FormField( - "Fallnummer", - FormFieldType.TEXT, - required=True, - key="Grunderfassung_fallnummer", - ), - FormField( - "Notizen", - FormFieldType.LONGTEXT, - required=False, - key="Grunderfassung_notiz", - ), + # FormField( + # "Ersteintrag Datum", + # FormFieldType.TEXT_DATETIME, + # required=False, + # key="Metadaten_erstellung", + # readonly=True, + # ignore_get_data=True, + # ), + # FormField( + # "Aktualisierung Datum", + # FormFieldType.TEXT_DATETIME, + # required=False, + # key="Metadaten_aktualisierung", + # readonly=True, + # ignore_get_data=True, + # ), + # FormField( + # "Aktualisierung Nutzer", + # FormFieldType.TEXT, + # required=False, + # key="Metadaten_nutzer", + # readonly=True, + # ignore_get_data=True, + # ), + # FormField( + # "Fallnummer", + # FormFieldType.TEXT, + # required=True, + # key="Grunderfassung_fallnummer", + # ), + # FormField( + # "Notizen", + # FormFieldType.LONGTEXT, + # required=False, + # key="Grunderfassung_notiz", + # ), FormField( "Suche", FormFieldType.CUSTOM, diff --git a/src/wce_crm/gui.py b/src/wce_crm/gui.py index 9c82681..b9ef80f 100644 --- a/src/wce_crm/gui.py +++ b/src/wce_crm/gui.py @@ -85,17 +85,16 @@ from wce_crm.backend import backend from wce_crm.constants import TIMEZONE_CEST from wce_crm.data_models import ( COLUMN_SEP, + INITREC_CONFIGS, Beratungsgespraech_Einzelgespraech, Beratungsgespraech_Vorgang, - Grunderfassung_Personen, - Grunderfassung_Unternehmen, InitRec, Page_CompanyProfile_State, Page_Consulting_ConsultingSession_State, Page_Consulting_Linking_State, Page_Consulting_State, Page_Consulting_Table_State, - Page_InitRec_Form_Config, + Page_InitRec_Form_Data, Page_InitRec_Form_State, Page_InitRec_State, Page_MainPage_State, @@ -105,8 +104,6 @@ from wce_crm.data_models import ( translate_pydantic_errors, ) from wce_crm.form_defs import ( - INITREC_COMP, - INITREC_PERSON, FormField, FormFieldType, ) @@ -121,7 +118,8 @@ from wce_crm.logging import ( from wce_crm.types import ( CompanyProfileConsultationEntry, ConsultingType, - NewEntityIds, + EntityIds, + EntityType, RecId, RecordingType, ) @@ -1547,7 +1545,7 @@ class InitRecForm(QWidget): # self.save_btn.setText(self.edit_buttons.save_btn_txt_enabled) def relevant_ids_set(self) -> bool: - return self.STATE.ids.valid(self.STATE.recording_type) + return self.STATE.ids.valid(self.STATE.ent_type) # def _activate_delete(self) -> None: # if self.STATE.ids.valid(): @@ -1613,12 +1611,23 @@ class InitRecForm(QWidget): # pformat(self.STATE.form_data), # ) + def _load_state_form_data(self) -> None: + logger_initrecform.info("[InitRecForm] Load data method...") + logger_initrecform.debug( + "[InitRecForm] Loaded data dict:\n%s Passing to Pydantic...", + pformat(self.STATE.form_data), + ) + # TODO error catching? + model = self.cfg.pydantic_model(**self.STATE.form_data) + logger_initrecform.debug("[InitRecForm] Loaded to Pydantic.") + logger_initrecform.debug("[InitRecForm] Convert to GUI structure...") + form_data_gui = model.to_gui() + # TODO include set data in this method? + self._set_data(form_data_gui) + def reset_form(self) -> None: reset_form(self.widget_registry) - # self.STATE.ids.set_id(None, self.STATE.ent_type) - self.STATE.ids.update(NewEntityIds()) - # TODO remove - # self.STATE.rec_id = None + self.STATE.ids.update(EntityIds()) self.STATE.form_data.clear() self.STATE.geloescht = False @@ -1658,7 +1667,7 @@ class InitRecForm(QWidget): # !! loading from IDs is only performed on top-level modules aka pages if section is None or section == "form": if not self.STATE.geloescht: - self.load_from_ids(self.STATE.ids) + self._load_state_form_data() else: self.reset_form() @@ -1678,6 +1687,10 @@ class InitRecForm(QWidget): # TODO change usage of user ID (needs changes in model and backend) if self.SESSION_USER_KEY in form_data: form_data[self.SESSION_USER_KEY] = self.STATE.session.user_name + + for form_key, id_key in self.STATE.cfg.id_mapping.items(): + self.STATE.ids.set_by_name(id_key, form_data[form_key]) + self.STATE.form_data = form_data def validate(self) -> list[str]: @@ -1695,18 +1708,18 @@ class InitRecForm(QWidget): return self.STATE - def _load_from_pydantic( - self, - data: InitRec, - ) -> None: - # ent_type = self.STATE.recording_type - # self.STATE.ids.set_id(data.ids.get_id(ent_type), ent_type) - self.STATE.ids.update(data.ids) - # self.STATE.rec_id = data.rec_id - self.STATE.form_data.clear() - self.STATE.geloescht = True if data.geloescht else False + # def _load_from_pydantic( + # self, + # data: InitRec, + # ) -> None: + # # ent_type = self.STATE.recording_type + # # self.STATE.ids.set_id(data.ids.get_id(ent_type), ent_type) + # self.STATE.ids.update(data.ids) + # # self.STATE.rec_id = data.rec_id + # self.STATE.form_data.clear() + # self.STATE.geloescht = True if data.geloescht else False - self._sync_state_to_GUI(None) # TODO change to correct loading + # self._sync_state_to_GUI(None) # TODO change to correct loading # !! must be part of the managing page def save_data(self) -> None: @@ -2979,7 +2992,8 @@ class Page_NewEntry(QWidget): def _request_new_initrec(self) -> None: req_state = Page_InitRec_State( session=self.STATE.session, - record_type=RecordingType.COMPANY, + recording_type=RecordingType.WITH_COMPANY, + form_data=Page_InitRec_Form_Data(metadata={}, person={}, company={}), locked=False, ) logger_gui.debug("[Page -- NewInitRec] State to call: %s", req_state) @@ -3009,29 +3023,29 @@ class Page_NewEntry(QWidget): pass -CONFIG_GRUNDERFASSUNG_UNTERNEHMEN: Final[Page_InitRec_Form_Config] = Page_InitRec_Form_Config( - pydantic_model=Grunderfassung_Unternehmen, - # to_db=backend.initrec_company_to_db, # TODO remove - # from_db=backend.initrec_company_from_db, # TODO remove - ignored_keys=( - "Metadaten_erstellung", - "Metadaten_aktualisierung", - ), - form_fields=INITREC_COMP, - add_buttons=False, -) +# CONFIG_GRUNDERFASSUNG_UNTERNEHMEN: Final[Page_InitRec_Form_Config] = Page_InitRec_Form_Config( +# pydantic_model=Grunderfassung_Unternehmen, +# # to_db=backend.initrec_company_to_db, # TODO remove +# # from_db=backend.initrec_company_from_db, # TODO remove +# ignored_keys=( +# "Metadaten_erstellung", +# "Metadaten_aktualisierung", +# ), +# form_fields=INITREC_COMP, +# add_buttons=False, +# ) -CONFIG_GRUNDERFASSUNG_PERSONEN: Final[Page_InitRec_Form_Config] = Page_InitRec_Form_Config( - pydantic_model=Grunderfassung_Personen, - # to_db=backend.initrec_person_to_db, # TODO remove - # from_db=backend.initrec_person_from_db, # TODO remove - ignored_keys=( - "Metadaten_erstellung", - "Metadaten_aktualisierung", - ), - form_fields=INITREC_PERSON, - add_buttons=False, -) +# CONFIG_GRUNDERFASSUNG_PERSONEN: Final[Page_InitRec_Form_Config] = Page_InitRec_Form_Config( +# pydantic_model=Grunderfassung_Personen, +# # to_db=backend.initrec_person_to_db, # TODO remove +# # from_db=backend.initrec_person_from_db, # TODO remove +# ignored_keys=( +# "Metadaten_erstellung", +# "Metadaten_aktualisierung", +# ), +# form_fields=INITREC_PERSON, +# add_buttons=False, +# ) CUSTOM_WIDGETS: Final[dict[str, type[CustomFormWidget]]] = { "grunderfassung_unternehmen_suche": Grunderfassung_SuchWidget, @@ -3128,12 +3142,12 @@ class Page_InitRec(QWidget): back_btn_step.setMinimumWidth(200) back_btn_step.setMaximumWidth(200) - if self.STATE.record_type is RecordingType.COMPANY: + if self.STATE.recording_type is RecordingType.WITH_COMPANY: title = QLabel("Grunderfassung Unternehmen") - elif self.STATE.record_type is RecordingType.PERSON: + elif self.STATE.recording_type is RecordingType.WITHOUT_COMPANY: title = QLabel("Grunderfassung Individualperson") else: - raise RuntimeError(f"Unspecified recording type {self.STATE.record_type}") + raise RuntimeError(f"Unspecified recording type {self.STATE.recording_type}") title.setStyleSheet("font-size: 20px; font-weight: bold;") header_layout.setSpacing(5) @@ -3142,6 +3156,64 @@ class Page_InitRec(QWidget): header_layout.addWidget(title) vert_layout.addWidget(header_container) + if DEBUG: + separator1 = QFrame() + separator1.setFrameShape(QFrame.Shape.HLine) + separator1.setFrameShadow(QFrame.Shadow.Sunken) + header_layout.addWidget(separator1) + + btn_gui2state = QPushButton("Sync GUI to state") + btn_gui2state.clicked.connect(self._sync_GUI_to_state) + btn_gui2state.setFixedHeight(35) + btn_gui2state.setSizePolicy( + QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed + ) + header_layout.addWidget(btn_gui2state) + + btn_state2gui = QPushButton("Sync state to GUI") + btn_state2gui.clicked.connect(self._sync_state_to_GUI) + btn_state2gui.setFixedHeight(35) + btn_state2gui.setSizePolicy( + QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed + ) + header_layout.addWidget(btn_state2gui) + + # self.test_button = QPushButton("Initialisiere Laden") + # self.test_button.clicked.connect(self._load_from_id) + # self.test_button.setFixedHeight(35) + # self.test_button.setSizePolicy( + # QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed + # ) + # header_layout.addWidget(self.test_button) + + # button_get = QPushButton("GET DATA") + # button_get.setFixedHeight(35) + # button_get.clicked.connect(self._get_form_data) + # header_layout.addWidget(button_get) + # button_set = QPushButton("SET DATA") + # button_set.setFixedHeight(35) + # button_set.clicked.connect(self._set_form_data) + # header_layout.addWidget(button_set) + + # id_field_layout = QHBoxLayout() + # id_field_layout.setContentsMargins(0, 0, 0, 0) + # id_field_layout.setSpacing(5) + # id_field_label = QLabel("ID Datenbank:") + # self.id_field_input = QLineEdit() + # id_field_layout.addWidget(id_field_label) + # id_field_layout.addWidget(self.id_field_input) + # header_layout.addLayout(id_field_layout) + # button_db_index = QPushButton("Setze DB Index") + # button_db_index.setFixedHeight(35) + # button_db_index.clicked.connect(self._set_db_index) + # header_layout.addWidget(button_db_index) + + separator2 = QFrame() + separator2.setFrameShape(QFrame.Shape.HLine) + separator2.setFrameShadow(QFrame.Shadow.Sunken) + header_layout.addWidget(separator2) + header_layout.addSpacing(5) + # --- BANNER --- self.info_banners_container = QWidget() self.info_banners: list[InfoBanner] = [] @@ -3183,35 +3255,29 @@ class Page_InitRec(QWidget): vert_layout.addWidget(scroll_area) - container_layout = QVBoxLayout(container) - container_layout.setContentsMargins(0, 0, 0, 0) + self.container_layout = QVBoxLayout(container) + self.container_layout.setContentsMargins(0, 0, 0, 0) - # --- AUTO FORM LAYOUT 1 --- + # --- AUTO FORM LAYOUT --- # now we try to render the layout within the collapsable box - container_layout.addSpacing(20) + self.container_layout.addSpacing(20) - self.initrec_form_company: InitRecForm | None = None - if self.STATE.record_type is RecordingType.COMPANY: - # add company - pass + # ** person + self.initrec_form_person_box = CollapsibleBox("Angaben zur Person") + ent_type = EntityType.PERSON + self.initrec_form_person = InitRecForm(self.STATE.initrec_form_states[ent_type]) + + assert self.STATE.initrec_form_states[ent_type] is self.initrec_form_person.STATE - box1 = CollapsibleBox("Angaben zur Person") - initrec_form_state_person = Page_InitRec_Form_State( - session=self.STATE.session, - cfg=CONFIG_GRUNDERFASSUNG_PERSONEN, - ids=self.STATE.ids, - recording_type=RecordingType.PERSON, - locked=False, - ) - self.initrec_form_person = InitRecForm(initrec_form_state_person) self.STATE.child_modules.append(self.initrec_form_person) - box1.setContentWidget(self.initrec_form_person) - container_layout.addWidget(box1) + self.initrec_form_person_box.setContentWidget(self.initrec_form_person) + self.container_layout.addWidget(self.initrec_form_person_box) # container_layout.addSpacing(15) - self.initrec_form_person.update_triggered.connect( - lambda: self.update_triggered.emit() - ) - self.initrec_form_person.update_triggered.connect(self._auto_form_updated) + # TODO trigger needed? + # self.initrec_form_person.update_triggered.connect( + # lambda: self.update_triggered.emit() + # ) + # self.initrec_form_person.update_triggered.connect(self._auto_form_updated) # --- CUSTOM LOGIC --- # ** 'Bundesland' only if 'Inland' selected in 'Stammdaten' @@ -3233,7 +3299,68 @@ class Page_InitRec(QWidget): self.selection_county.setProperty("styleClass", "stempel") self.selection_county.setEnabled(False) - container_layout.addStretch() + # ** company + ent_type = EntityType.COMPANY + self.initrec_form_company_box = CollapsibleBox("Angaben zum Unternehmen") + self.initrec_form_company = InitRecForm(self.STATE.initrec_form_states[ent_type]) + self.STATE.child_modules.append(self.initrec_form_company) + + assert self.STATE.initrec_form_states[ent_type] is self.initrec_form_company.STATE + + target_idx = self.container_layout.indexOf(self.initrec_form_person_box) - 1 + self.initrec_form_company_box.setContentWidget(self.initrec_form_company) + self.container_layout.insertWidget(target_idx, self.initrec_form_company_box) + + # container_layout.addSpacing(15) + # TODO trigger needed? + # self.initrec_form_person.update_triggered.connect( + # lambda: self.update_triggered.emit() + # ) + # self.initrec_form_person.update_triggered.connect(self._auto_form_updated) + + # --- CUSTOM LOGIC --- + # ** fill 'Kontaktperson -> Namen Unternehmen' + search_res = search_widgets_by_key( + self.initrec_form_company.widget_registry, "Partnersuche" + ) + assert len(search_res) == 1 + search_widget = cast(Grunderfassung_SuchWidget, search_res[0]["widget"]) + self.search_widget_trigger = cast( + QLineEdit, search_widget.company_widgets["ma_unternehmensname"] + ) + + self.container_layout.addStretch() + self._update_layout() + + def _update_layout(self) -> None: + if self.STATE.recording_type is RecordingType.WITH_COMPANY: + # add company + self.initrec_form_company_box.setVisible(True) + else: + self.initrec_form_company_box.setVisible(False) + + # elif self.initrec_form_company_box is not None: + # self.container_layout.removeWidget(self.initrec_form_company_box) + # # ** state + # assert self.initrec_form_company + # self.STATE.child_modules.remove(self.initrec_form_company) + # del self.STATE.initrec_form_states[EntityType.COMPANY] + + # self.initrec_form_company_box = None + # self.initrec_form_company = None + + # def _set_new_initrec_state( + # self, + # ent_type: EntityType, + # ) -> None: + # initrec_form_state = Page_InitRec_Form_State( + # session=self.STATE.session, + # cfg=INITREC_CONFIGS[ent_type], + # ids=self.STATE.ids, + # ent_type=ent_type, + # locked=False, + # ) + # self.STATE.initrec_form_states[ent_type] = initrec_form_state # ** custom logic def _custom_county_selection(self, idx: int) -> None: @@ -3328,18 +3455,28 @@ class Page_InitRec(QWidget): # TODO staes for form in self.STATE.child_modules: form.reset_form() - self.STATE.ids = NewEntityIds() + self.STATE.ids = EntityIds() self.STATE.geloescht = False def _load_database( self, - ids: NewEntityIds, + ids: EntityIds, ) -> None: logger_initrecform.info("[Page -- InitRec] Load data method...") logger_initrecform.debug("[Page -- InitRec] Entity IDs: %s", ids) self._reset() - # TODO add loading logic - initrec: InitRec = ... + # TODO add loading logic with backend call + # !! just a dummy + initrec: InitRec = InitRec( + ids=ids, + recording_type=RecordingType.WITHOUT_COMPANY, + geloescht=None, + form_data=Page_InitRec_Form_Data( + metadata={}, + person={}, + company={}, + ), + ) self._load_from_pydantic(initrec, sync_to_GUI=False) @@ -3349,7 +3486,23 @@ class Page_InitRec(QWidget): update_only: bool = False, sync_to_GUI: bool = True, ) -> None: + logger_initrecform.info("[Page -- InitRec] Load from Pydantic model...") # TODO add logic to update internal state + self.STATE.recording_type = data.recording_type + self.STATE.form_data = data.form_data + self.STATE.geloescht = True if data.geloescht else False + + # set form data to correct initrec form states + for ent_type, state in self.STATE.initrec_form_states.items(): + match ent_type: + case EntityType.COMPANY: + state.form_data = self.STATE.form_data.company + # self.initrec_form_company.load_state(state) + case EntityType.PERSON: + state.form_data = self.STATE.form_data.person + # self.initrec_form_person.load_state(state) + case _: + raise RuntimeError(f"Unspecified entity type {ent_type}") if update_only: ... @@ -3368,19 +3521,20 @@ class Page_InitRec(QWidget): ) if section is None or section == "content": + self._update_layout() if not self.STATE.geloescht: self._load_database(self.STATE.ids) else: self._reset() - initrec_form_state_person = Page_InitRec_Form_State( - session=self.STATE.session, - cfg=CONFIG_GRUNDERFASSUNG_PERSONEN, - ids=self.STATE.ids, - recording_type=RecordingType.PERSON, - locked=False, - ) - self.initrec_form_person.load_state(initrec_form_state_person) + # initrec_form_state_person = Page_InitRec_Form_State( + # session=self.STATE.session, + # cfg=CONFIG_GRUNDERFASSUNG_PERSONEN, + # ids=self.STATE.ids, + # ent_type=RecordingType.WITHOUT_COMPANY, + # locked=False, + # ) + # self.initrec_form_person.load_state(initrec_form_state_person) if section is None or section == "banners": self._clear_info_banners() @@ -3401,22 +3555,65 @@ class Page_InitRec(QWidget): form.unlock() def _sync_GUI_to_state(self) -> None: + logger_page_initrec.debug("[Page -- InitRec] Syncing GUI to state...") + + logger_page_initrec.debug( + "[Page -- InitRec] Syncing GUI to state: Child modules...\n%s", + pformat(self.STATE.child_modules), + ) + + logger_page_initrec.debug( + "[Page -- InitRec] Syncing GUI to state: Existing form states keys...\n%s", + pformat(self.STATE.initrec_form_states.keys()), + ) + for form in self.STATE.child_modules: # ?? is this the same reference to use a similar pattern as # ?? the consulting page (initialise once with the same state # ?? as reference which are then automatically updated) + # !! check if this is working as expected + # new entries' states are linked to this instance by the page state form_state = form.get_state() - self.STATE.ids.update(form_state.ids, ignore_none=True) - self.STATE.form_data[form_state.recording_type] = form_state.form_data - self.STATE.pydantic_models[form_state.recording_type] = ( - form_state.cfg.pydantic_model + logger_page_initrec.debug( + "[Page -- InitRec] Syncing GUI to state: Form state ent_type %s", + form_state.ent_type, ) + logger_page_initrec.debug( + "[Page -- InitRec] Syncing GUI to state: identity of form states: %s", + (self.STATE.initrec_form_states[form_state.ent_type] is form_state), + ) + assert self.STATE.initrec_form_states[form_state.ent_type] is form_state, ( + "passing by reference not working" + ) + self.STATE.ids.update(form_state.ids, ignore_none=True) + # self.STATE.form_data[form_state.ent_type] = form_state.form_data + match form_state.ent_type: + case EntityType.COMPANY: + self.STATE.form_data.company = form_state.form_data + case EntityType.PERSON: + self.STATE.form_data.person = form_state.form_data + case _: + raise RuntimeError(f"Unspecified entity type {form_state.ent_type}") + + logger_page_initrec.debug( + "[Page -- InitRec] Syncing GUI to state: IDs after...\n%s", + pformat(self.STATE.ids), + ) + logger_page_initrec.debug( + "[Page -- InitRec] Syncing GUI to state: Form data after...\n%s", + pformat(self.STATE.form_data), + ) + def load_state( self, new_state: Page_InitRec_State, ) -> None: - set_page_state(self.STATE, new_state) + set_page_state( + self.STATE, + new_state, + ignore_attr={"form_data", "initrec_form_states"}, + ) self._sync_state_to_GUI(None) def get_state(self) -> Page_InitRec_State: @@ -3444,6 +3641,8 @@ class Page_InitRec(QWidget): def save_data(self) -> None: logger_page_initrec.debug("[Page -- InitRec] Save button clicked...") + return + if self.STATE.ids.empty() and self.STATE.geloescht: logger_page_initrec.debug( "[Page -- InitRec] This was never saved in the database " @@ -3460,7 +3659,7 @@ class Page_InitRec(QWidget): to_db = InitRec( ids=self.STATE.ids, geloescht=deleted_datetime, - db_data={}, + form_data={}, ) returned = self.cfg.to_db(to_db) self._load_from_pydantic(returned) @@ -3520,7 +3719,7 @@ class Page_InitRec(QWidget): ids=self.STATE.ids, # rec_id=self.STATE.rec_id, geloescht=deleted_datetime, - db_data=db_data, + form_data=db_data, ) returned = self.cfg.to_db(to_db) @@ -5939,8 +6138,9 @@ class MainWindow(QMainWindow): # SITE: 'Grunderfassung' newinitrec_state = Page_InitRec_State( session=session, - ids=NewEntityIds(), - record_type=RecordingType.COMPANY, + ids=EntityIds(), + recording_type=RecordingType.WITH_COMPANY, + form_data=Page_InitRec_Form_Data(metadata={}, person={}, company={}), locked=False, ) self.newinitrec = Page_InitRec(newinitrec_state) @@ -5973,7 +6173,7 @@ class MainWindow(QMainWindow): # self.stack.addWidget(self.initrec_person) # SITE: 'Unternehmensprofil' comp_profile_state = Page_CompanyProfile_State( - session=self.STATE.session, ids=NewEntityIds() + session=self.STATE.session, ids=EntityIds() ) self.company_profile = Page_CompanyProfile(comp_profile_state) self.company_profile.back_main_requested.connect(self.show_main_page) diff --git a/src/wce_crm/types.py b/src/wce_crm/types.py index c53072f..c24c900 100644 --- a/src/wce_crm/types.py +++ b/src/wce_crm/types.py @@ -5,7 +5,7 @@ import dataclasses as dc import datetime import enum from collections.abc import Generator -from typing import TYPE_CHECKING, TypeAlias, TypedDict +from typing import TYPE_CHECKING, Final, TypeAlias, TypedDict if TYPE_CHECKING: import polars as pl @@ -24,6 +24,12 @@ PolarsSchema: TypeAlias = dict[str, type["pl.DataType"]] # TODO rename: these are not entity types or entities but different forms of # initial recordings class RecordingType(enum.IntEnum): + WITH_COMPANY = enum.auto() + WITHOUT_COMPANY = enum.auto() + + +class EntityType(enum.IntEnum): + METADATA = enum.auto() COMPANY = enum.auto() PERSON = enum.auto() @@ -34,19 +40,36 @@ class ConsultingType(enum.StrEnum): @dc.dataclass(slots=True, kw_only=True) -class NewEntityIds: +class EntityIds: un_id: ExtMaId | None = None an_id: ExtAnId | None = None pers_id: RecId | None = None link_id: IntLinkId | None = None + _field_names: frozenset[str] = dc.field(init=False, repr=False, compare=False) + + def __post_init__(self) -> None: + self._field_names = frozenset(f.name for f in dc.fields(EntityIds)) + + def set_by_name( + self, + attr_name: str, + value: int | None, + ) -> None: + if attr_name not in self._field_names: + raise KeyError( + f"Attribute {attr_name} not member of this class. Valid " + f"options: {self._field_names}" + ) + + setattr(self, attr_name, value) def update( self, - ids: NewEntityIds, + ids: EntityIds, ignore_none: bool = False, deep: bool = False, ) -> None: - if not isinstance(ids, NewEntityIds): + if not isinstance(ids, EntityIds): raise TypeError("New IDs must be have the same type as this object") for field in dc.fields(ids): @@ -62,18 +85,18 @@ class NewEntityIds: def _get_relevant_fields( self, - recording_type: RecordingType | None = None, + ent_type: EntityType | None = None, ) -> Generator[int | None, None, None]: - if recording_type is None: + if ent_type is None: relevant_fields = (getattr(self, field.name) for field in dc.fields(self)) - elif recording_type is RecordingType.COMPANY: + elif ent_type is EntityType.COMPANY: RELEVANT_ATTRIBUTES = {"un_id", "an_id", "link_id"} relevant_fields = ( getattr(self, field.name) for field in dc.fields(self) if field.name in RELEVANT_ATTRIBUTES ) - elif recording_type is RecordingType.PERSON: + elif ent_type is EntityType.PERSON: RELEVANT_ATTRIBUTES = {"pers_id"} relevant_fields = ( getattr(self, field.name) @@ -81,23 +104,23 @@ class NewEntityIds: if field.name in RELEVANT_ATTRIBUTES ) else: - raise RuntimeError(f"Unspecified recording type {recording_type}") + raise RuntimeError(f"Unspecified entity type {ent_type}") return relevant_fields def empty( self, - recording_type: RecordingType | None = None, + ent_type: EntityType | None = None, ) -> bool: - relevant_fields = self._get_relevant_fields(recording_type=recording_type) + relevant_fields = self._get_relevant_fields(ent_type=ent_type) return not any(relevant_fields) def valid( self, - recording_type: RecordingType | None = None, + ent_type: EntityType | None = None, ) -> bool: - relevant_fields = self._get_relevant_fields(recording_type=recording_type) + relevant_fields = self._get_relevant_fields(ent_type=ent_type) return all(relevant_fields) -- 2.34.1 From dc9374f5b7288affcf31b2861f783f2b07030f75 Mon Sep 17 00:00:00 2001 From: foefl Date: Mon, 27 Jul 2026 11:41:02 +0200 Subject: [PATCH 05/13] migration script to latest data model --- ...18132d_update_outstanding_table_changes.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 alembic/versions/e290fe18132d_update_outstanding_table_changes.py diff --git a/alembic/versions/e290fe18132d_update_outstanding_table_changes.py b/alembic/versions/e290fe18132d_update_outstanding_table_changes.py new file mode 100644 index 0000000..ab4f5ea --- /dev/null +++ b/alembic/versions/e290fe18132d_update_outstanding_table_changes.py @@ -0,0 +1,38 @@ +"""update outstanding table changes + +Revision ID: e290fe18132d +Revises: 8000d15faf03 +Create Date: 2026-07-27 09:05:06.301043 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "e290fe18132d" +down_revision: Union[str, Sequence[str], None] = "8000d15faf03" +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! ### + with op.batch_alter_table("Personen_Unternehmen_Zuordnung", schema=None) as batch_op: + batch_op.add_column( + sa.Column("Partnersuche__kanal_aufmerksamkeit", sa.Text(), nullable=True) + ) + + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("Personen_Unternehmen_Zuordnung", schema=None) as batch_op: + batch_op.drop_column("Partnersuche__kanal_aufmerksamkeit") + + # ### end Alembic commands ### -- 2.34.1 From 1ff55038fa9efded3a346999977da703a7c26608 Mon Sep 17 00:00:00 2001 From: foefl Date: Mon, 27 Jul 2026 11:41:24 +0200 Subject: [PATCH 06/13] basic functionality of loading/saving for initial recording with new data model --- src/wce_crm/backend/backend.py | 99 +++++++++++++------ src/wce_crm/data_models.py | 14 +-- src/wce_crm/form_defs.py | 2 +- src/wce_crm/gui.py | 173 +++++++++++++++++++++++---------- src/wce_crm/types.py | 21 ++-- 5 files changed, 207 insertions(+), 102 deletions(-) diff --git a/src/wce_crm/backend/backend.py b/src/wce_crm/backend/backend.py index f21406c..3d9ff09 100644 --- a/src/wce_crm/backend/backend.py +++ b/src/wce_crm/backend/backend.py @@ -402,11 +402,16 @@ def page_initrec_to_db( from_db = ret.mappings().fetchall() assert len(from_db) == 1, "expected excactly one returned row" - data_from_db = from_db[0] + data_from_db = dict(from_db[0]) + data_from_db = _rename_db_data( + data_from_db, FIELD_DB_MAPPING_GRUNDERERFASSUNG_PERSONEN + ) data.ids.pers_id = data_from_db["pers_id"] data.geloescht = data_from_db["geloescht"] - data.form_data.person["Metadaten_aktualisierung"] = data_from_db["aktualisiert"] + data.form_data.person["Metadaten_aktualisierung"] = data_from_db[ + "Metadaten_aktualisierung" + ] logger.debug("[AutoForm -- backend] Inserted InitRec Person successfully") @@ -416,7 +421,7 @@ def page_initrec_to_db( db.t_grunderfassung_personen.update() .where(db.t_grunderfassung_personen.c.pers_id == data.ids.pers_id) .returning( - db.t_grunderfassung_personen.c.Metadaten_aktualisierung, + db.t_grunderfassung_personen.c.aktualisiert, db.t_grunderfassung_personen.c.geloescht, ) ) @@ -424,7 +429,10 @@ def page_initrec_to_db( from_db = ret.mappings().fetchall() assert len(from_db) == 1, "expected excatly one returned row" - data_from_db = from_db[0] + data_from_db = dict(from_db[0]) + data_from_db = _rename_db_data( + data_from_db, FIELD_DB_MAPPING_GRUNDERERFASSUNG_PERSONEN + ) data.geloescht = data_from_db["geloescht"] data.form_data.person["Metadaten_aktualisierung"] = data_from_db[ @@ -450,14 +458,28 @@ def page_initrec_to_db( ) conn.execute(stmt) - # additional information + # additional information --> linking table dump_data = _rename_db_data( data.form_data.company, FIELD_DB_MAPPING_GRUNDERERFASSUNG_UNTERNEHMEN ) - dump_data["geloescht"] = data.geloescht + logger.debug( + "[InitRec -- backend] Company - additional info: Dump Data = \n%s", + pformat(dump_data), + ) + if "un_id" not in dump_data: + # only case when updated + dump_data["un_id"] = data.ids.un_id + dump_data["an_id"] = data.ids.an_id + stmt = sqlite_insert(db.t_zuordnung_personen_unternehmen).values( **dump_data, pers_id=data.ids.pers_id ) + # dump_data["geloescht"] = data.geloescht + # logger.debug( + # "[InitRec -- backend] Company - additional info: Dump Data = \n%s", + # pformat(dump_data), + # ) + stmt = stmt.on_conflict_do_update( index_elements=["un_id", "pers_id"], index_where=sa.text("geloescht IS NULL"), @@ -465,13 +487,17 @@ def page_initrec_to_db( "an_id": stmt.excluded.an_id, "Partnersuche__kanal_aufmerksamkeit": stmt.excluded.Partnersuche__kanal_aufmerksamkeit, "aktualisiert": datetime.datetime.now(datetime.UTC), + "geloescht": data.geloescht, }, ).returning(db.t_zuordnung_personen_unternehmen.c.id) ret = conn.execute(stmt) from_db = ret.mappings().fetchall() assert len(from_db) == 1, "expected excatly one returned row" - data_from_db = from_db[0] + data_from_db = dict(from_db[0]) + data_from_db = _rename_db_data( + data_from_db, FIELD_DB_MAPPING_GRUNDERERFASSUNG_UNTERNEHMEN + ) data.ids.link_id = data_from_db["id"] return data @@ -480,35 +506,18 @@ def page_initrec_to_db( def page_initrec_from_db( ids: EntityIds, ) -> InitRec: + logger.debug("[InitRec -- backend] Call database loading routine...") + recording_type = RecordingType.WITHOUT_COMPANY - if ids.valid(EntityType.COMPANY): + if ids.link_id is not None or ids.valid(EntityType.COMPANY): recording_type = RecordingType.WITH_COMPANY - # person - assert ids.pers_id, "person ID not set" - logger.debug("[AutoForm -- backend] Call database loading routine...") - stmt = db.t_grunderfassung_personen.select().where( - db.t_grunderfassung_personen.c.pers_id == ids.pers_id - ) - with db.ENGINE.connect() as conn: - ret = conn.execute(stmt) - - from_db = ret.mappings().all() - assert len(from_db) == 1, "not excactly one company initial recording obtained" - data_person_table = dict(from_db[0]) - - data_person_table = _rename_db_data( - data_person_table, FIELD_DB_MAPPING_GRUNDERERFASSUNG_PERSONEN - ) - geloescht = data_person_table["geloescht"] - del data_person_table["geloescht"] - data_company_table: dict[str, Any] = {} if recording_type is RecordingType.WITH_COMPANY: - assert ids.un_id, "company ID not set" - + logger.debug("[InitRec -- backend] With company. Link ID: %s", ids.link_id) if ids.link_id: stmt = sa.select( + db.t_zuordnung_personen_unternehmen.c.pers_id, db.t_zuordnung_personen_unternehmen.c.un_id, db.t_zuordnung_personen_unternehmen.c.an_id, db.t_zuordnung_personen_unternehmen.c.Partnersuche__kanal_aufmerksamkeit, @@ -516,7 +525,10 @@ def page_initrec_from_db( db.t_zuordnung_personen_unternehmen.c.id == ids.link_id, ) else: + assert ids.un_id, "company ID not set" + assert ids.pers_id, "person ID not set" stmt = sa.select( + db.t_zuordnung_personen_unternehmen.c.pers_id, db.t_zuordnung_personen_unternehmen.c.un_id, db.t_zuordnung_personen_unternehmen.c.an_id, db.t_zuordnung_personen_unternehmen.c.Partnersuche__kanal_aufmerksamkeit, @@ -533,16 +545,43 @@ def page_initrec_from_db( assert len(from_db) == 1, "not excactly one company initial recording obtained" data_company_table = dict(from_db[0]) + ids.pers_id = data_company_table.pop("pers_id") + ids.un_id = data_company_table["un_id"] + ids.an_id = data_company_table["an_id"] data_company_table = _rename_db_data( - data_person_table, FIELD_DB_MAPPING_GRUNDERERFASSUNG_UNTERNEHMEN + data_company_table, FIELD_DB_MAPPING_GRUNDERERFASSUNG_UNTERNEHMEN ) + # person + assert ids.pers_id, "person ID not set" + logger.debug("[InitRec -- backend] Person... Person ID: %s", ids.pers_id) + stmt = db.t_grunderfassung_personen.select().where( + db.t_grunderfassung_personen.c.pers_id == ids.pers_id + ) + with db.ENGINE.connect() as conn: + ret = conn.execute(stmt) + + from_db = ret.mappings().all() + assert len(from_db) == 1, "not excactly one company initial recording obtained" + data_person_table = dict(from_db[0]) + + data_person_table = _rename_db_data( + data_person_table, FIELD_DB_MAPPING_GRUNDERERFASSUNG_PERSONEN + ) + geloescht = data_person_table["geloescht"] + del data_person_table["geloescht"] + form_data = Page_InitRec_Form_Data( metadata={}, person=data_person_table, company=data_company_table, ) + if geloescht is not None: + logger.warning( + "[InitRec -- backend] Loaded entries which seem to be deleted - IDs: %s", ids + ) + return InitRec( ids=ids, recording_type=recording_type, diff --git a/src/wce_crm/data_models.py b/src/wce_crm/data_models.py index e52daeb..eb3b028 100644 --- a/src/wce_crm/data_models.py +++ b/src/wce_crm/data_models.py @@ -492,11 +492,11 @@ FIELD_DB_MAPPING_GRUNDERERFASSUNG_UNTERNEHMEN: Final[DualDict[str, str]] = DualD 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 + # 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 @@ -697,8 +697,6 @@ class Beratungsgespraech_Einzelgespraech(BaseModel): INITREC_CONFIGS: Final[dict[EntityType, Page_InitRec_Form_Config]] = { EntityType.COMPANY: Page_InitRec_Form_Config( pydantic_model=Grunderfassung_Unternehmen, - # to_db=backend.initrec_company_to_db, # TODO remove - # from_db=backend.initrec_company_from_db, # TODO remove ignored_keys=( "Metadaten_erstellung", "Metadaten_aktualisierung", @@ -712,8 +710,6 @@ INITREC_CONFIGS: Final[dict[EntityType, Page_InitRec_Form_Config]] = { ), EntityType.PERSON: Page_InitRec_Form_Config( pydantic_model=Grunderfassung_Personen, - # to_db=backend.initrec_person_to_db, # TODO remove - # from_db=backend.initrec_person_from_db, # TODO remove ignored_keys=( "Metadaten_erstellung", "Metadaten_aktualisierung", diff --git a/src/wce_crm/form_defs.py b/src/wce_crm/form_defs.py index 9650dc5..49afab2 100644 --- a/src/wce_crm/form_defs.py +++ b/src/wce_crm/form_defs.py @@ -957,7 +957,7 @@ INITREC_PERSON = [ required=False, key="Metadaten_nutzer", readonly=True, - ignore_get_data=True, + ignore_get_data=False, ), FormField( "Wiedereintrittsdatum", diff --git a/src/wce_crm/gui.py b/src/wce_crm/gui.py index b9ef80f..ddeae88 100644 --- a/src/wce_crm/gui.py +++ b/src/wce_crm/gui.py @@ -1622,7 +1622,6 @@ class InitRecForm(QWidget): logger_initrecform.debug("[InitRecForm] Loaded to Pydantic.") logger_initrecform.debug("[InitRecForm] Convert to GUI structure...") form_data_gui = model.to_gui() - # TODO include set data in this method? self._set_data(form_data_gui) def reset_form(self) -> None: @@ -2996,7 +2995,7 @@ class Page_NewEntry(QWidget): form_data=Page_InitRec_Form_Data(metadata={}, person={}, company={}), locked=False, ) - logger_gui.debug("[Page -- NewInitRec] State to call: %s", req_state) + logger_gui.debug("[Page -- NewInitRec] State to call (IDs): %s", req_state.ids) self.new_initrec_requested.emit(req_state) def _sync_state_to_GUI(self) -> None: @@ -3178,6 +3177,14 @@ class Page_InitRec(QWidget): ) header_layout.addWidget(btn_state2gui) + btn_print_state = QPushButton("Print State") + btn_print_state.clicked.connect(self._debug_print_state) + btn_print_state.setFixedHeight(35) + btn_print_state.setSizePolicy( + QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed + ) + header_layout.addWidget(btn_print_state) + # self.test_button = QPushButton("Initialisiere Laden") # self.test_button.clicked.connect(self._load_from_id) # self.test_button.setFixedHeight(35) @@ -3332,6 +3339,11 @@ class Page_InitRec(QWidget): self.container_layout.addStretch() self._update_layout() + def _debug_print_state(self) -> None: + logger_initrecform.info( + "[Page -- InitRec] Printing state IDs:\n%s", pformat(self.STATE.ids) + ) + def _update_layout(self) -> None: if self.STATE.recording_type is RecordingType.WITH_COMPANY: # add company @@ -3428,7 +3440,16 @@ class Page_InitRec(QWidget): self.delete_btn.setEnabled(False) def _deletion_valid(self) -> bool: - valid_id_states = (form.relevant_ids_set() for form in self.STATE.child_modules) + # valid_id_states: list[bool] = [] + relevant_ent_types: set[EntityType] = {EntityType.METADATA, EntityType.PERSON} + if self.STATE.recording_type is RecordingType.WITH_COMPANY: + relevant_ent_types.add(EntityType.COMPANY) + + valid_id_states = ( + form.relevant_ids_set() + for form in self.STATE.child_modules + if form.STATE.ent_type in relevant_ent_types + ) return all(valid_id_states) def _delete_data(self) -> None: @@ -3465,18 +3486,50 @@ class Page_InitRec(QWidget): logger_initrecform.info("[Page -- InitRec] Load data method...") logger_initrecform.debug("[Page -- InitRec] Entity IDs: %s", ids) self._reset() + + ids = EntityIds( + # un_id=3, + # an_id=7, + pers_id=5, + ) + + ids = EntityIds( + link_id=7, + ) + self.STATE.ids.update(ids, ignore_none=False) + + if ids.empty(): + logger_initrecform.debug("[Page -- InitRec] Loading: No ID set. Do nothing!") + return + # TODO add loading logic with backend call # !! just a dummy - initrec: InitRec = InitRec( - ids=ids, - recording_type=RecordingType.WITHOUT_COMPANY, - geloescht=None, - form_data=Page_InitRec_Form_Data( - metadata={}, - person={}, - company={}, - ), - ) + initrec = backend.page_initrec_from_db(ids) + + # TODO re-enable later + # if res.status != STATUS_HANDLER.SUCCESS: + # logger_page_consulting.error( + # ( + # "[Consulting Page] There were errors during the database " + # "loading procedure." + # ), + # stack_info=True, + # ) + + # exc_formatted = ( + # f"Exception: {res.status.ExceptionType}\nMessage: {res.status.message}" + # ) + # msg_box = get_message_box( + # QMessageBox.Icon.Warning, + # "Laden fehlgeschlagen", + # ( + # "Beim Laden der Daten ist ein Fehler aufgetreten. Details sind " + # "unten angefügt." + # ), + # detailed_text=exc_formatted, + # ) + # msg_box.exec() + # return self._load_from_pydantic(initrec, sync_to_GUI=False) @@ -3487,28 +3540,31 @@ class Page_InitRec(QWidget): sync_to_GUI: bool = True, ) -> None: logger_initrecform.info("[Page -- InitRec] Load from Pydantic model...") - # TODO add logic to update internal state + self.STATE.ids.update(data.ids) self.STATE.recording_type = data.recording_type self.STATE.form_data = data.form_data self.STATE.geloescht = True if data.geloescht else False # set form data to correct initrec form states for ent_type, state in self.STATE.initrec_form_states.items(): + state.ids.update(self.STATE.ids) + state.geloescht = self.STATE.geloescht + match ent_type: case EntityType.COMPANY: state.form_data = self.STATE.form_data.company - # self.initrec_form_company.load_state(state) + if ( + not update_only + and self.STATE.recording_type is RecordingType.WITH_COMPANY + ): + self.initrec_form_company.load_state(state) case EntityType.PERSON: state.form_data = self.STATE.form_data.person - # self.initrec_form_person.load_state(state) + if not update_only: + self.initrec_form_person.load_state(state) case _: raise RuntimeError(f"Unspecified entity type {ent_type}") - if update_only: - ... - else: - ... - if sync_to_GUI: self._sync_state_to_GUI(None) @@ -3517,24 +3573,15 @@ class Page_InitRec(QWidget): section: Literal["content", "banners", "locking"] | None, ) -> None: logger_page_initrec.debug( - "[Page -- InitRec] Syncing state to GUI: State...\n%s", pformat(self.STATE) + "[Page -- InitRec] Syncing state to GUI: State IDs: %s", self.STATE.ids ) if section is None or section == "content": - self._update_layout() if not self.STATE.geloescht: self._load_database(self.STATE.ids) else: self._reset() - - # initrec_form_state_person = Page_InitRec_Form_State( - # session=self.STATE.session, - # cfg=CONFIG_GRUNDERFASSUNG_PERSONEN, - # ids=self.STATE.ids, - # ent_type=RecordingType.WITHOUT_COMPANY, - # locked=False, - # ) - # self.initrec_form_person.load_state(initrec_form_state_person) + self._update_layout() if section is None or section == "banners": self._clear_info_banners() @@ -3546,13 +3593,14 @@ class Page_InitRec(QWidget): if section is None or section == "locking": if self.STATE.locked: - self.edit_buttons.set_modify_state(False, set_delete_btn=True) for form in self.STATE.child_modules: form.lock() + self.edit_buttons.set_modify_state(False, set_delete_btn=True) else: - self.edit_buttons.set_modify_state(True, set_delete_btn=False) for form in self.STATE.child_modules: form.unlock() + self.edit_buttons.set_modify_state(True, set_delete_btn=False) + self._activate_delete() def _sync_GUI_to_state(self) -> None: logger_page_initrec.debug("[Page -- InitRec] Syncing GUI to state...") @@ -3641,7 +3689,13 @@ class Page_InitRec(QWidget): def save_data(self) -> None: logger_page_initrec.debug("[Page -- InitRec] Save button clicked...") - return + logger_page_initrec.info("[Page -- InitRec] Syncing current state...") + # state = self.get_state() + self._sync_GUI_to_state() + form_data = self.STATE.form_data + assert form_data, "form data not set" + logger_page_initrec.debug("[Page -- InitRec] Get form data\n%s", pformat(form_data)) + db_data = Page_InitRec_Form_Data(metadata={}, person={}, company={}) if self.STATE.ids.empty() and self.STATE.geloescht: logger_page_initrec.debug( @@ -3656,12 +3710,13 @@ class Page_InitRec(QWidget): ) deleted_datetime = datetime.datetime.now(datetime.UTC) assert self.STATE.form_data, "cannot delete entry with uninitialised form data" - to_db = InitRec( + data_to_db = InitRec( ids=self.STATE.ids, + recording_type=self.STATE.recording_type, geloescht=deleted_datetime, - form_data={}, + form_data=db_data, ) - returned = self.cfg.to_db(to_db) + returned = backend.page_initrec_to_db(data_to_db) self._load_from_pydantic(returned) msg_box = get_message_box( QMessageBox.Icon.Information, @@ -3679,33 +3734,45 @@ class Page_InitRec(QWidget): logger_page_initrec.error( "[Page -- InitRec] Error during GUI validation phase:\n%s", pformat(errors) ) - GUI_pydantic_validation_error_handling(errors) + GUI_validation_error_handling(errors) self._enable_save() return logger_page_initrec.info("[Page -- InitRec] Success! All data is valid") - logger_page_initrec.info("[Page -- InitRec] Get form data call...") - state = self.get_state() - form_data = state.form_data - assert form_data, "form data not set" - - logger_page_initrec.debug("[Page -- InitRec] Get form data\n%s", pformat(form_data)) try: logger_page_initrec.debug("[Page -- InitRec] Call Pydantic...") - validated_data = self.cfg.pydantic_model(**form_data) + # validated_data = self.cfg.pydantic_model(**form_data) + + for ent_type, state in self.STATE.initrec_form_states.items(): + logger_page_initrec.debug( + "[Page -- InitRec] Ent type: %s, Current state CFG model: %s", + ent_type, + state.cfg.pydantic_model, + ) + match ent_type: + case EntityType.COMPANY: + val_data = state.cfg.pydantic_model(**self.STATE.form_data.company) + db_data.company = val_data.to_db(exclude=state.cfg.ignored_keys) + case EntityType.PERSON: + val_data = state.cfg.pydantic_model(**self.STATE.form_data.person) + db_data.person = val_data.to_db(exclude=state.cfg.ignored_keys) + case _: + raise RuntimeError(f"Unspecified entity type {ent_type}") + except ValidationError as e: logger_page_initrec.error( "[Page -- InitRec] Error during Pydantic validation phase:\n%s", pformat(e.errors()), ) pydantic_validation_error_handling(e) + else: if DEBUG_NO_DATABASE: return # !! this code is only called if the 'try' block was successful # save data to database - db_data = validated_data.to_db(exclude=self.cfg.ignored_keys) + # db_data = validated_data.to_db(exclude=self.cfg.ignored_keys) logger_page_initrec.debug( "[Page -- InitRec] Form data with 'exlude' (must be saved in the database):\n%s", pformat(db_data), @@ -3715,15 +3782,15 @@ class Page_InitRec(QWidget): deleted_datetime = datetime.datetime.now(datetime.UTC) try: # TODO change to result pattern - to_db = InitRec( + data_to_db = InitRec( ids=self.STATE.ids, - # rec_id=self.STATE.rec_id, + recording_type=self.STATE.recording_type, geloescht=deleted_datetime, form_data=db_data, ) - returned = self.cfg.to_db(to_db) + returned = backend.page_initrec_to_db(data_to_db) - self._load_from_pydantic(returned) + self._load_from_pydantic(returned, update_only=True, sync_to_GUI=False) self.update_triggered.emit() except Exception as err: msg_box = get_message_box( @@ -4958,7 +5025,7 @@ class Page_Consulting(QWidget): logger_page_consulting.error( "[Consulting Page] Error during GUI validation phase:\n%s", pformat(errors) ) - GUI_pydantic_validation_error_handling(errors) + GUI_validation_error_handling(errors) self._enable_save() return @@ -5964,7 +6031,7 @@ def pydantic_validation_error_handling( msg_box.exec() -def GUI_pydantic_validation_error_handling( +def GUI_validation_error_handling( errors: list[str], ) -> None: error_text = "Bitte füllen Sie die folgenden Pflichtfelder aus:\n\n▸ " + "\n▸ ".join( diff --git a/src/wce_crm/types.py b/src/wce_crm/types.py index c24c900..1bb14aa 100644 --- a/src/wce_crm/types.py +++ b/src/wce_crm/types.py @@ -48,7 +48,9 @@ class EntityIds: _field_names: frozenset[str] = dc.field(init=False, repr=False, compare=False) def __post_init__(self) -> None: - self._field_names = frozenset(f.name for f in dc.fields(EntityIds)) + field_names = set(f.name for f in dc.fields(EntityIds)) + field_names.remove("_field_names") + self._field_names = frozenset(field_names) def set_by_name( self, @@ -88,20 +90,21 @@ class EntityIds: ent_type: EntityType | None = None, ) -> Generator[int | None, None, None]: if ent_type is None: - relevant_fields = (getattr(self, field.name) for field in dc.fields(self)) + relevant_fields = (getattr(self, field_name) for field_name in self._field_names) elif ent_type is EntityType.COMPANY: - RELEVANT_ATTRIBUTES = {"un_id", "an_id", "link_id"} + # RELEVANT_ATTRIBUTES = {"un_id", "an_id", "link_id"} + RELEVANT_ATTRIBUTES = {"un_id", "an_id"} relevant_fields = ( - getattr(self, field.name) - for field in dc.fields(self) - if field.name in RELEVANT_ATTRIBUTES + getattr(self, field_name) + for field_name in self._field_names + if field_name in RELEVANT_ATTRIBUTES ) elif ent_type is EntityType.PERSON: RELEVANT_ATTRIBUTES = {"pers_id"} relevant_fields = ( - getattr(self, field.name) - for field in dc.fields(self) - if field.name in RELEVANT_ATTRIBUTES + getattr(self, field_name) + for field_name in self._field_names + if field_name in RELEVANT_ATTRIBUTES ) else: raise RuntimeError(f"Unspecified entity type {ent_type}") -- 2.34.1 From ba26baa2254c5d0503fbb45eb7794aa52e8b4777 Mon Sep 17 00:00:00 2001 From: foefl Date: Mon, 27 Jul 2026 12:38:00 +0200 Subject: [PATCH 07/13] re-enable company profile --- src/wce_crm/backend/backend.py | 24 ++-- src/wce_crm/data_models.py | 7 +- src/wce_crm/gui.py | 240 ++++++++++++++------------------- 3 files changed, 114 insertions(+), 157 deletions(-) diff --git a/src/wce_crm/backend/backend.py b/src/wce_crm/backend/backend.py index 3d9ff09..ea5da04 100644 --- a/src/wce_crm/backend/backend.py +++ b/src/wce_crm/backend/backend.py @@ -75,7 +75,8 @@ def initrec_comp_search_choices() -> tuple[tuple[str, int], ...]: return tuple(zip(df["dedupl"], df["ma_id"])) -def initrec_comp_search_get_info( +# TODO change to direct SQL interaction +def external_company_get_info( ma_id: ExtMaId, ) -> CompanyInfo: logger.debug("[Call backend] comp_search_get_info") @@ -858,11 +859,11 @@ def page_consulting_linking_companies( with db.ENGINE.connect() as conn: q_relevant_companies = sa.select( - db.grunderfassung_unternehmen.c.un_id, - db.grunderfassung_unternehmen.c.Partnersuche__un_suche, + db.t_unternehmen.c.un_id, + db.t_unternehmen.c.Partnersuche__un_suche, ).where( - db.grunderfassung_unternehmen.c.geloescht.is_(None), - db.grunderfassung_unternehmen.c.Partnersuche__un_suche.is_not(None), + db.t_unternehmen.c.geloescht.is_(None), + db.t_unternehmen.c.Partnersuche__un_suche.is_not(None), ) if un_id is not None: q_relevant_companies = q_relevant_companies.where( @@ -904,6 +905,8 @@ def page_consulting_linking_companies( def page_consulting_linking_persons( un_id: RecId | None, ) -> list[LinkingConsultationsEntryPerson]: + raise RuntimeError("TODOs to implement") + with db.ENGINE.connect() as conn: # if un_id None: list all persons # else: list just the persons associated with the company @@ -958,6 +961,7 @@ def page_consulting_eastablish_link( un_id: RecId | None, pers_id: RecId | None, ) -> None: + raise RuntimeError("TODOs to implement") stmt = ( sa.update(db.t_beratung_vorgang) @@ -969,6 +973,7 @@ def page_consulting_eastablish_link( conn.execute(stmt) +# // company profile interaction def companyprofile_page_get_consultations( un_id: RecId, ) -> CompanyProfileConsultations: @@ -994,11 +999,8 @@ def companyprofile_page_get_consultations( datetime_updated = cast(datetime.datetime, entry["aktualisiert"]) datetime_updated = datetime_updated.astimezone(TIMEZONE_CEST) - base_title = entry["titel"] - - _cons_type = entry["beratungs_typ"] - cons_type = ConsultingType(_cons_type) + cons_type = ConsultingType(entry["beratungs_typ"]) con_entry = CompanyProfileConsultationEntry( cons_id=cons_id, @@ -1012,7 +1014,7 @@ def companyprofile_page_get_consultations( elif cons_type is ConsultingType.INDIVIDUAL: cons_entries_individual.append(con_entry) else: - raise TypeError(f"Unknown consulting type: {_cons_type}") + raise TypeError(f"Unknown consulting type: {cons_type}") cons_entries_pauschal.sort(key=lambda x: x.date_updated, reverse=True) cons_entries_individual.sort(key=lambda x: x.date_updated, reverse=True) @@ -1081,7 +1083,7 @@ def _main_page_get_company_list() -> list[MainPageEntry]: datetime_akt = cast(datetime.datetime, entry["Metadaten_aktualisierung"]) datetime_akt = datetime_akt.astimezone(TIMEZONE_CEST) - comp_info = initrec_comp_search_get_info(ma_id_external) + comp_info = external_company_get_info(ma_id_external) display_name = comp_info["ma_unternehmensname"] main_page_companies.append( diff --git a/src/wce_crm/data_models.py b/src/wce_crm/data_models.py index eb3b028..a0fcad7 100644 --- a/src/wce_crm/data_models.py +++ b/src/wce_crm/data_models.py @@ -20,11 +20,11 @@ from pydantic import ( from pydantic_core import ErrorDetails from wce_crm.form_defs import INITREC_COMP, INITREC_PERSON, FormField -from wce_crm.types import EntityIds, EntityType, RecordingType +from wce_crm.types import ConsultingType, EntityIds, EntityType, RecordingType if TYPE_CHECKING: from wce_crm.gui import InitRecForm, Page_Consulting_ConsultingSession # noqa: F401 - from wce_crm.types import ConsId, ConsultingType, EntityType, RecId, UserId + from wce_crm.types import ConsId, EntityType, RecId, UserId ValidAge = Annotated[int, Field(ge=0, le=99)] COLUMN_SEP: Final[str] = "__" @@ -354,8 +354,7 @@ class Page_Consulting_Table_State(PageState["Page_Consulting_ConsultingSession"] @dc.dataclass(slots=True, kw_only=True) class Page_CompanyProfile_State(PageState[Module]): session: Session - ids: EntityIds - # rec_id: RecId | None = None + un_id: RecId | None = None # // Pydantic models diff --git a/src/wce_crm/gui.py b/src/wce_crm/gui.py index ddeae88..ea8a5e5 100644 --- a/src/wce_crm/gui.py +++ b/src/wce_crm/gui.py @@ -85,7 +85,6 @@ from wce_crm.backend import backend from wce_crm.constants import TIMEZONE_CEST from wce_crm.data_models import ( COLUMN_SEP, - INITREC_CONFIGS, Beratungsgespraech_Einzelgespraech, Beratungsgespraech_Vorgang, InitRec, @@ -128,7 +127,6 @@ if TYPE_CHECKING: from wce_crm.data_models import ( Module, Page_InitRec_Form_State, - WrapperModule, ) from wce_crm.types import ( ConsId, @@ -1273,7 +1271,7 @@ class Grunderfassung_SuchWidget(CustomFormWidget): if ma_id is None or index == (-1): self._clear_company_fields() return - data = backend.initrec_comp_search_get_info( + data = backend.external_company_get_info( ma_id=ma_id, ) self.fill_out_company(data) @@ -1378,7 +1376,7 @@ def search_widgets_by_key( class InitRecForm(QWidget): """a widget, which is managed by a code-defined field definition collection""" - update_triggered = Signal() # formular saved (data changed for front page) + # update_triggered = Signal() # formular saved (data changed for front page) def __init__( self, @@ -1425,54 +1423,54 @@ class InitRecForm(QWidget): self.main_layout = QVBoxLayout(self) self.main_layout.setContentsMargins(0, 0, 0, 0) - if DEBUG: - separator1 = QFrame() - separator1.setFrameShape(QFrame.Shape.HLine) - separator1.setFrameShadow(QFrame.Shadow.Sunken) - self.main_layout.addWidget(separator1) + # if DEBUG: + # separator1 = QFrame() + # separator1.setFrameShape(QFrame.Shape.HLine) + # separator1.setFrameShadow(QFrame.Shadow.Sunken) + # self.main_layout.addWidget(separator1) - self.registry_button = QPushButton("Ausgabe Widget Registry") - self.registry_button.clicked.connect(self._print_registry) - self.registry_button.setFixedHeight(35) - self.main_layout.addWidget(self.registry_button) + # self.registry_button = QPushButton("Ausgabe Widget Registry") + # self.registry_button.clicked.connect(self._print_registry) + # self.registry_button.setFixedHeight(35) + # self.main_layout.addWidget(self.registry_button) - # self.test_button = QPushButton("Initialisiere Laden") - # self.test_button.clicked.connect(self.load_from_ids) - # self.test_button.setFixedHeight(35) - # self.test_button.setSizePolicy( - # QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed - # ) - # self.main_layout.addWidget(self.test_button) + # # self.test_button = QPushButton("Initialisiere Laden") + # # self.test_button.clicked.connect(self.load_from_ids) + # # self.test_button.setFixedHeight(35) + # # self.test_button.setSizePolicy( + # # QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed + # # ) + # # self.main_layout.addWidget(self.test_button) - button_get = QPushButton("GET DATA") - button_get.setFixedHeight(35) - button_get.clicked.connect(self._get_data) - self.main_layout.addWidget(button_get) - button_set = QPushButton("SET DATA") - button_set.setFixedHeight(35) - button_set.clicked.connect(self._set_data) - self.main_layout.addWidget(button_set) + # button_get = QPushButton("GET DATA") + # button_get.setFixedHeight(35) + # button_get.clicked.connect(self._get_data) + # self.main_layout.addWidget(button_get) + # button_set = QPushButton("SET DATA") + # button_set.setFixedHeight(35) + # button_set.clicked.connect(self._set_data) + # self.main_layout.addWidget(button_set) - id_field_layout = QHBoxLayout() - id_field_layout.setContentsMargins(0, 0, 0, 0) - id_field_layout.setSpacing(5) - id_field_label = QLabel("ID Datenbank:") - self.id_field_input = QLineEdit() - id_field_layout.addWidget(id_field_label) - id_field_layout.addWidget(self.id_field_input) - self.main_layout.addLayout(id_field_layout) - button_db_index = QPushButton("Setze DB Index") - button_db_index.setFixedHeight(35) - button_db_index.clicked.connect(self._set_db_index) - self.main_layout.addWidget(button_db_index) + # id_field_layout = QHBoxLayout() + # id_field_layout.setContentsMargins(0, 0, 0, 0) + # id_field_layout.setSpacing(5) + # id_field_label = QLabel("ID Datenbank:") + # self.id_field_input = QLineEdit() + # id_field_layout.addWidget(id_field_label) + # id_field_layout.addWidget(self.id_field_input) + # self.main_layout.addLayout(id_field_layout) + # button_db_index = QPushButton("Setze DB Index") + # button_db_index.setFixedHeight(35) + # button_db_index.clicked.connect(self._set_db_index) + # self.main_layout.addWidget(button_db_index) - separator2 = QFrame() - separator2.setFrameShape(QFrame.Shape.HLine) - separator2.setFrameShadow(QFrame.Shadow.Sunken) - self.main_layout.addWidget(separator2) - self.main_layout.addSpacing(5) + # separator2 = QFrame() + # separator2.setFrameShape(QFrame.Shape.HLine) + # separator2.setFrameShadow(QFrame.Shadow.Sunken) + # self.main_layout.addWidget(separator2) + # self.main_layout.addSpacing(5) - self.main_layout.addSpacing(10) + # self.main_layout.addSpacing(10) # buttons (save and reset) self.add_buttons = self.cfg.add_buttons @@ -1519,14 +1517,14 @@ class InitRecForm(QWidget): def _print_registry(self) -> None: pprint_registry(self.widget_registry) - def _set_db_index(self) -> None: - try: - index = int(self.id_field_input.text()) - except ValueError: - index = None - self.STATE.ids.un_id = index - self.STATE.ids.pers_id = index - logger_gui.debug("[InitRecForm] Set index to %s, new state: %s", index, self.STATE) + # def _set_db_index(self) -> None: + # try: + # index = int(self.id_field_input.text()) + # except ValueError: + # index = None + # self.STATE.ids.un_id = index + # self.STATE.ids.pers_id = index + # logger_gui.debug("[InitRecForm] Set index to %s, new state: %s", index, self.STATE) # def _disable_save(self) -> None: # assert self.save_btn is not None @@ -3104,6 +3102,7 @@ class Page_InitRec(QWidget): back_main_requested = Signal() # back to main page back_requested = Signal() # back button update_triggered = Signal() # form saved (data changed for front page) + company_profile_requested = Signal(Page_CompanyProfile_State) def __init__( self, @@ -3153,6 +3152,15 @@ class Page_InitRec(QWidget): header_layout.addWidget(back_btn_step) header_layout.addWidget(back_btn_main) header_layout.addWidget(title) + + self.comp_profile_btn = QPushButton("Unternehmensprofil") + self.comp_profile_btn.clicked.connect(self._request_company_profile) + self.comp_profile_btn.setSizePolicy( + QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed + ) + self.comp_profile_btn.setFixedHeight(50) + header_layout.addWidget(self.comp_profile_btn) + vert_layout.addWidget(header_container) if DEBUG: @@ -3185,36 +3193,6 @@ class Page_InitRec(QWidget): ) header_layout.addWidget(btn_print_state) - # self.test_button = QPushButton("Initialisiere Laden") - # self.test_button.clicked.connect(self._load_from_id) - # self.test_button.setFixedHeight(35) - # self.test_button.setSizePolicy( - # QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed - # ) - # header_layout.addWidget(self.test_button) - - # button_get = QPushButton("GET DATA") - # button_get.setFixedHeight(35) - # button_get.clicked.connect(self._get_form_data) - # header_layout.addWidget(button_get) - # button_set = QPushButton("SET DATA") - # button_set.setFixedHeight(35) - # button_set.clicked.connect(self._set_form_data) - # header_layout.addWidget(button_set) - - # id_field_layout = QHBoxLayout() - # id_field_layout.setContentsMargins(0, 0, 0, 0) - # id_field_layout.setSpacing(5) - # id_field_label = QLabel("ID Datenbank:") - # self.id_field_input = QLineEdit() - # id_field_layout.addWidget(id_field_label) - # id_field_layout.addWidget(self.id_field_input) - # header_layout.addLayout(id_field_layout) - # button_db_index = QPushButton("Setze DB Index") - # button_db_index.setFixedHeight(35) - # button_db_index.clicked.connect(self._set_db_index) - # header_layout.addWidget(button_db_index) - separator2 = QFrame() separator2.setFrameShape(QFrame.Shape.HLine) separator2.setFrameShadow(QFrame.Shadow.Sunken) @@ -3279,12 +3257,6 @@ class Page_InitRec(QWidget): self.STATE.child_modules.append(self.initrec_form_person) self.initrec_form_person_box.setContentWidget(self.initrec_form_person) self.container_layout.addWidget(self.initrec_form_person_box) - # container_layout.addSpacing(15) - # TODO trigger needed? - # self.initrec_form_person.update_triggered.connect( - # lambda: self.update_triggered.emit() - # ) - # self.initrec_form_person.update_triggered.connect(self._auto_form_updated) # --- CUSTOM LOGIC --- # ** 'Bundesland' only if 'Inland' selected in 'Stammdaten' @@ -3318,13 +3290,6 @@ class Page_InitRec(QWidget): self.initrec_form_company_box.setContentWidget(self.initrec_form_company) self.container_layout.insertWidget(target_idx, self.initrec_form_company_box) - # container_layout.addSpacing(15) - # TODO trigger needed? - # self.initrec_form_person.update_triggered.connect( - # lambda: self.update_triggered.emit() - # ) - # self.initrec_form_person.update_triggered.connect(self._auto_form_updated) - # --- CUSTOM LOGIC --- # ** fill 'Kontaktperson -> Namen Unternehmen' search_res = search_widgets_by_key( @@ -3344,35 +3309,31 @@ class Page_InitRec(QWidget): "[Page -- InitRec] Printing state IDs:\n%s", pformat(self.STATE.ids) ) + def _request_company_profile(self) -> None: + assert self.STATE.ids.un_id is not None, ( + "requested company profile for a company which is not known (no ID)" + ) + req_state = Page_CompanyProfile_State( + session=self.STATE.session, + un_id=self.STATE.ids.un_id, + ) + logger_gui.debug("[Page -- InitRec Company] State to call: %s", req_state) + self.company_profile_requested.emit(req_state) + def _update_layout(self) -> None: if self.STATE.recording_type is RecordingType.WITH_COMPANY: # add company self.initrec_form_company_box.setVisible(True) + self.comp_profile_btn.setVisible(True) + if self.STATE.ids.valid(EntityType.COMPANY): + self.comp_profile_btn.setEnabled(True) + else: + self.comp_profile_btn.setEnabled(False) + else: self.initrec_form_company_box.setVisible(False) - - # elif self.initrec_form_company_box is not None: - # self.container_layout.removeWidget(self.initrec_form_company_box) - # # ** state - # assert self.initrec_form_company - # self.STATE.child_modules.remove(self.initrec_form_company) - # del self.STATE.initrec_form_states[EntityType.COMPANY] - - # self.initrec_form_company_box = None - # self.initrec_form_company = None - - # def _set_new_initrec_state( - # self, - # ent_type: EntityType, - # ) -> None: - # initrec_form_state = Page_InitRec_Form_State( - # session=self.STATE.session, - # cfg=INITREC_CONFIGS[ent_type], - # ids=self.STATE.ids, - # ent_type=ent_type, - # locked=False, - # ) - # self.STATE.initrec_form_states[ent_type] = initrec_form_state + self.comp_profile_btn.setVisible(False) + self.comp_profile_btn.setEnabled(False) # ** custom logic def _custom_county_selection(self, idx: int) -> None: @@ -4099,13 +4060,13 @@ class Page_CompanyProfile(QWidget): self, cons_type: ConsultingType, ) -> None: - assert self.STATE.ids.un_id is not None, ( + assert self.STATE.un_id is not None, ( "tried to call consulting from comp profile without set ID" ) new_state = Page_Consulting_State( session=self.STATE.session, vorgang_id=None, - un_id=self.STATE.ids.un_id, + un_id=self.STATE.un_id, beratungs_typ=cons_type, locked=False, ) @@ -4115,13 +4076,13 @@ class Page_CompanyProfile(QWidget): self, data: CompanyProfileConsultationEntry, ) -> None: - assert self.STATE.ids.un_id is not None, ( + assert self.STATE.un_id is not None, ( "tried to call consulting from comp profile without set ID" ) new_state = Page_Consulting_State( session=self.STATE.session, vorgang_id=data.cons_id, - un_id=self.STATE.ids.un_id, + un_id=self.STATE.un_id, beratungs_typ=data.cons_type, locked=True, ) @@ -4143,11 +4104,9 @@ class Page_CompanyProfile(QWidget): widget.deleteLater() def _update_consultations(self) -> None: - if self.STATE.ids.un_id is None: + if self.STATE.un_id is None: return - consultations = backend.companyprofile_page_get_consultations( - un_id=self.STATE.ids.un_id - ) + consultations = backend.companyprofile_page_get_consultations(un_id=self.STATE.un_id) logger_gui.debug( "[Page -- Company Profile] Consultations: %s", pformat(consultations) @@ -4185,7 +4144,7 @@ class Page_CompanyProfile(QWidget): self.add_btn_individual.setEnabled(True) def _sync_state_to_GUI(self) -> None: - if self.STATE.ids.un_id is None: + if self.STATE.un_id is None: # just clear and do nothing else logger_gui.debug("just reset, ID is None") self.table_master_data.reset() @@ -4195,11 +4154,11 @@ class Page_CompanyProfile(QWidget): self._enable_add_btns() # TODO change to correct backend function - raise RuntimeError("TODOs to implement") - init_rec = backend.initrec_company_get_initial_recording(self.STATE.ids.un_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) + # raise RuntimeError("TODOs to implement") + # init_rec = backend.initrec_company_get_initial_recording(self.STATE.un_id) + # ma_id = cast("ExtMaId", init_rec["Partnersuche__un_suche"]) + # assert ma_id is not None, "no ma_id obtained" + comp_info = backend.external_company_get_info(self.STATE.un_id) data: dict[str, Any] = {} @@ -4221,8 +4180,8 @@ class Page_CompanyProfile(QWidget): self, new_state: Page_CompanyProfile_State, ) -> None: - logger_gui.debug("Called function to load with IDs: %s", pformat(new_state.ids)) - self.STATE.ids = new_state.ids + logger_gui.debug("Called function to load with ID: %s", pformat(new_state.un_id)) + self.STATE.un_id = new_state.un_id self._sync_state_to_GUI() @@ -6213,7 +6172,8 @@ class MainWindow(QMainWindow): self.newinitrec = Page_InitRec(newinitrec_state) self.newinitrec.back_main_requested.connect(self.show_main_page) self.newinitrec.back_requested.connect(self.simple_go_back) - # self.newinitrec.update_main_page.connect(self.update_grid) + self.newinitrec.company_profile_requested.connect(self.show_page_company_profile) + # self.newinitrec.update_triggered.connect(self.update_grid) self.stack.addWidget(self.newinitrec) # SITE: 'Grunderfassung Unternehmen' @@ -6239,9 +6199,7 @@ class MainWindow(QMainWindow): # self.initrec_person.update_triggered.connect(self.update_grid) # self.stack.addWidget(self.initrec_person) # SITE: 'Unternehmensprofil' - comp_profile_state = Page_CompanyProfile_State( - session=self.STATE.session, ids=EntityIds() - ) + comp_profile_state = Page_CompanyProfile_State(session=self.STATE.session) self.company_profile = Page_CompanyProfile(comp_profile_state) self.company_profile.back_main_requested.connect(self.show_main_page) # TODO remove @@ -6526,8 +6484,6 @@ if TYPE_CHECKING: _m3: type[Module] = Page_Consulting_ConsultingSession _m4: type[Module] = Page_InitRec _m5: type[Module] = InitRecForm - # _w1: type[WrapperModule] = Page_InitRecCompany - # _w2: type[WrapperModule] = Page_InitRecPerson _f1: type[CustomForm] = DynamicListWidget _f2: type[CustomForm] = DynamicDropdownWidgetNumeric _f3: type[CustomForm] = DynamicDropdownWidgetOption -- 2.34.1 From 449d401c04f4671daca08af3df8989132eb8c9f5 Mon Sep 17 00:00:00 2001 From: foefl Date: Mon, 27 Jul 2026 19:50:24 +0200 Subject: [PATCH 08/13] handle metadata separately in GUI, but common in database --- prototypes/tests.py | 32 +++++++++++++ src/wce_crm/backend/backend.py | 73 ++++++++++++++++++----------- src/wce_crm/data_models.py | 84 ++++++++++++++++++++++++++-------- src/wce_crm/form_defs.py | 80 +++++++++++++++++++++++++------- src/wce_crm/gui.py | 60 ++++++++++++++++++++++-- src/wce_crm/types.py | 7 +++ 6 files changed, 271 insertions(+), 65 deletions(-) diff --git a/prototypes/tests.py b/prototypes/tests.py index 3644836..b6ee6a0 100644 --- a/prototypes/tests.py +++ b/prototypes/tests.py @@ -21,6 +21,38 @@ from wce_crm import constants, db from wce_crm.backend import backend from wce_crm.types import EntityIds, RecordingType +# %% +empty = tuple() + +# not any(empty) + +all(empty) +# %% +dic = { + "t1": "test1", + "t2": "test2", + "t3": "test3", + "t4": "test4", +} +filter_keys = frozenset(("t3", "t2")) + +dic_in, dic_out = backend.split_dict(dic, filter_keys) + +print(f"{dic_in=}") +print(f"{dic_out=}") + + +# %% +dic_update = { + "t5": "new", + "t6": "new_2", +} + +dic_updated = backend.dic_deep_update(dic, dic_update) +print(f"{dic=}") +print(f"{dic_update=}") +print(f"{dic_updated=}") + # %% raw_ent_ids = EntityIds() db_ent_ids = EntityIds(un_id=3, an_id=2) diff --git a/src/wce_crm/backend/backend.py b/src/wce_crm/backend/backend.py index ea5da04..6781ce7 100644 --- a/src/wce_crm/backend/backend.py +++ b/src/wce_crm/backend/backend.py @@ -2,8 +2,9 @@ from __future__ import annotations import copy import datetime +from collections.abc import Container, Mapping from pprint import pformat -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any, TypeVar, cast import polars as pl import sqlalchemy as sa @@ -15,6 +16,7 @@ from wce_crm.constants import TIMEZONE_CEST from wce_crm.data_models import ( FIELD_DB_MAPPING_GRUNDERERFASSUNG_PERSONEN, FIELD_DB_MAPPING_GRUNDERERFASSUNG_UNTERNEHMEN, + FIELD_DB_SPLIT_METADATEN, Beratungsgespraech_Einzelgespraech, Beratungsgespraech_Vorgang, InitRec, @@ -40,6 +42,9 @@ if TYPE_CHECKING: from wce_crm.types import ConsId, ExtAnId, ExtMaId, RecId +K = TypeVar("K") +V = TypeVar("V") + def _transform_for_gui_output( data: pl.DataFrame, @@ -355,6 +360,30 @@ def initrec_comp_contact_person_search_get_info( # return dict(row) +def split_dict( + dic: dict[K, V], + split_keys: Container[K], +) -> tuple[dict[K, V], dict[K, V]]: + keys_in: dict[K, V] = {} + keys_out = copy.deepcopy(dic) + + for check_key in dic: + if check_key in split_keys: + keys_in[check_key] = keys_out.pop(check_key) + + return keys_in, keys_out + + +def dic_deep_update( + dic: dict[K, V], + data: Mapping[K, V], +) -> dict[K, V]: + dic_copy = copy.deepcopy(dic) + dic_copy.update(data) + + return dic_copy + + def _rename_db_data( db_data: dict[str, Any], mapping: DualDict, @@ -379,18 +408,15 @@ def page_initrec_to_db( with db.ENGINE.begin() as conn: # person - dump_data = _rename_db_data( - data.form_data.person, FIELD_DB_MAPPING_GRUNDERERFASSUNG_PERSONEN - ) + dump_data = dic_deep_update(data.form_data.person, data.form_data.metadata) + dump_data = _rename_db_data(dump_data, FIELD_DB_MAPPING_GRUNDERERFASSUNG_PERSONEN) dump_data["geloescht"] = data.geloescht if data.ids.pers_id is None: logger.debug("[InitRec -- backend] Insert...") stmt = sa.insert(db.t_grunderfassung_personen).returning( - db.t_grunderfassung_personen.c.pers_id, - db.t_grunderfassung_personen.c.aktualisiert, - db.t_grunderfassung_personen.c.geloescht, + db.t_grunderfassung_personen ) stmt_compiled = str(stmt.compile(db.ENGINE)) logger.debug( @@ -409,10 +435,6 @@ def page_initrec_to_db( ) data.ids.pers_id = data_from_db["pers_id"] - data.geloescht = data_from_db["geloescht"] - data.form_data.person["Metadaten_aktualisierung"] = data_from_db[ - "Metadaten_aktualisierung" - ] logger.debug("[AutoForm -- backend] Inserted InitRec Person successfully") @@ -421,10 +443,7 @@ def page_initrec_to_db( stmt = ( db.t_grunderfassung_personen.update() .where(db.t_grunderfassung_personen.c.pers_id == data.ids.pers_id) - .returning( - db.t_grunderfassung_personen.c.aktualisiert, - db.t_grunderfassung_personen.c.geloescht, - ) + .returning(db.t_grunderfassung_personen) ) ret = conn.execute(stmt, dump_data) @@ -435,16 +454,22 @@ def page_initrec_to_db( data_from_db, FIELD_DB_MAPPING_GRUNDERERFASSUNG_PERSONEN ) - data.geloescht = data_from_db["geloescht"] - data.form_data.person["Metadaten_aktualisierung"] = data_from_db[ - "Metadaten_aktualisierung" - ] + # data.geloescht = data_from_db["geloescht"] + # data.form_data.person["Metadaten_aktualisierung"] = data_from_db[ + # "Metadaten_aktualisierung" + # ] logger.debug( "[InitRec -- backend] Updated InitRec Person with ID %d successfully", data.ids.pers_id, ) + data.geloescht = data_from_db["geloescht"] + del data_from_db["geloescht"] + in_filter, out_filter = split_dict(data_from_db, FIELD_DB_SPLIT_METADATEN) + data.form_data.metadata = in_filter + data.form_data.person = out_filter + # company if data.recording_type is RecordingType.WITH_COMPANY: # insert into company table and add additional information @@ -475,11 +500,6 @@ def page_initrec_to_db( stmt = sqlite_insert(db.t_zuordnung_personen_unternehmen).values( **dump_data, pers_id=data.ids.pers_id ) - # dump_data["geloescht"] = data.geloescht - # logger.debug( - # "[InitRec -- backend] Company - additional info: Dump Data = \n%s", - # pformat(dump_data), - # ) stmt = stmt.on_conflict_do_update( index_elements=["un_id", "pers_id"], @@ -571,10 +591,11 @@ def page_initrec_from_db( ) geloescht = data_person_table["geloescht"] del data_person_table["geloescht"] + in_filter, out_filter = split_dict(data_person_table, FIELD_DB_SPLIT_METADATEN) form_data = Page_InitRec_Form_Data( - metadata={}, - person=data_person_table, + metadata=in_filter, + person=out_filter, company=data_company_table, ) diff --git a/src/wce_crm/data_models.py b/src/wce_crm/data_models.py index a0fcad7..fb6b97f 100644 --- a/src/wce_crm/data_models.py +++ b/src/wce_crm/data_models.py @@ -19,7 +19,7 @@ from pydantic import ( ) from pydantic_core import ErrorDetails -from wce_crm.form_defs import INITREC_COMP, INITREC_PERSON, FormField +from wce_crm.form_defs import INITREC_COMP, INITREC_METADATEN, INITREC_PERSON, FormField from wce_crm.types import ConsultingType, EntityIds, EntityType, RecordingType if TYPE_CHECKING: @@ -222,10 +222,22 @@ class Page_NewEntry_State(PageState[Module]): session: Session -INITREC_REC_TO_ENTITIES: Final[dict[RecordingType, frozenset[EntityType]]] = { - RecordingType.WITH_COMPANY: frozenset((EntityType.COMPANY, EntityType.PERSON)), - RecordingType.WITHOUT_COMPANY: frozenset((EntityType.PERSON,)), -} +# TODO check removal +# INITREC_REC_TO_ENTITIES: Final[dict[RecordingType, frozenset[EntityType]]] = { +# RecordingType.WITH_COMPANY: frozenset( +# ( +# EntityType.METADATA, +# EntityType.COMPANY, +# EntityType.PERSON, +# ) +# ), +# RecordingType.WITHOUT_COMPANY: frozenset( +# ( +# EntityType.METADATA, +# EntityType.PERSON, +# ) +# ), +# } @dc.dataclass(slots=True, kw_only=True) @@ -251,7 +263,16 @@ class Page_InitRec_State(PageState["InitRecForm"]): # pydantic_models: dict[RecordingType, type[FlatBaseModel]] = dc.field(default_factory=dict) def __post_init__(self) -> None: - relevant_entities = INITREC_REC_TO_ENTITIES[self.recording_type] + # TODO check removal + # relevant_entities = INITREC_REC_TO_ENTITIES[self.recording_type] + relevant_entities = frozenset( + ( + EntityType.METADATA, + EntityType.COMPANY, + EntityType.PERSON, + ) + ) + for ent_type in relevant_entities: initrec_form_state = Page_InitRec_Form_State( session=self.session, @@ -483,11 +504,33 @@ FIELD_DB_MAPPING_GRUNDERERFASSUNG_PERSONEN: Final[DualDict[str, str]] = DualDict Metadaten_erstellung="erstellt", Metadaten_aktualisierung="aktualisiert", ) + FIELD_DB_MAPPING_GRUNDERERFASSUNG_UNTERNEHMEN: Final[DualDict[str, str]] = DualDict( Partnersuche__un_suche="un_id", Partnersuche__person_suche="an_id", ) +FIELD_DB_SPLIT_METADATEN: Final[frozenset[str]] = frozenset( + ( + "Metadaten_erstellung", + "Metadaten_aktualisierung", + "Metadaten_nutzer", + "Metadaten_wiedereintrittsdatum", + "Grunderfassung_fallnummer", + "Grunderfassung_notiz", + ) +) + + +class Grunderfassung_Metadaten(FlatBaseModel): + # displayed and handled separately, on database level included in 'Person' + Metadaten_erstellung: AwareDatetime | None = None + Metadaten_aktualisierung: AwareDatetime | None = None # see above + Metadaten_nutzer: str | None + Metadaten_wiedereintrittsdatum: datetime.date | None = None + Grunderfassung_fallnummer: str + Grunderfassung_notiz: str | None + class Grunderfassung_Unternehmen(FlatBaseModel): # default in SQLAlchemy with lambda and timezone-aware datetime @@ -502,12 +545,9 @@ class Grunderfassung_Unternehmen(FlatBaseModel): 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 - Metadaten_nutzer: str | None - Metadaten_wiedereintrittsdatum: datetime.date | None = None - Grunderfassung_fallnummer: str - Grunderfassung_notiz: str | None + # Metadaten_erstellung: AwareDatetime | None = None + # Metadaten_aktualisierung: AwareDatetime | None = None # see above + # Metadaten_nutzer: str | None Projektrelevanz: Grunderfassung_Projektrelevanz Kontaktperson: Grunderfassung_Kontaktperson @@ -694,12 +734,19 @@ class Beratungsgespraech_Einzelgespraech(BaseModel): INITREC_CONFIGS: Final[dict[EntityType, Page_InitRec_Form_Config]] = { - EntityType.COMPANY: Page_InitRec_Form_Config( - pydantic_model=Grunderfassung_Unternehmen, + EntityType.METADATA: Page_InitRec_Form_Config( + pydantic_model=Grunderfassung_Metadaten, ignored_keys=( "Metadaten_erstellung", "Metadaten_aktualisierung", ), + form_fields=INITREC_METADATEN, + add_buttons=False, + id_mapping={}, + ), + EntityType.COMPANY: Page_InitRec_Form_Config( + pydantic_model=Grunderfassung_Unternehmen, + ignored_keys=tuple(), form_fields=INITREC_COMP, add_buttons=False, id_mapping={ @@ -709,10 +756,11 @@ INITREC_CONFIGS: Final[dict[EntityType, Page_InitRec_Form_Config]] = { ), EntityType.PERSON: Page_InitRec_Form_Config( pydantic_model=Grunderfassung_Personen, - ignored_keys=( - "Metadaten_erstellung", - "Metadaten_aktualisierung", - ), + ignored_keys=tuple(), + # ignored_keys=( + # "Metadaten_erstellung", + # "Metadaten_aktualisierung", + # ), form_fields=INITREC_PERSON, add_buttons=False, id_mapping={}, diff --git a/src/wce_crm/form_defs.py b/src/wce_crm/form_defs.py index 49afab2..59e8005 100644 --- a/src/wce_crm/form_defs.py +++ b/src/wce_crm/form_defs.py @@ -187,7 +187,7 @@ COUNTRY_LIST: Final[CountryList] = get_country_list_german() GERMAN_STATE_LIST: Final[CountryList] = get_list_germany_states() -INITREC_COMP_SEARCH_HEAD = [ +_INITREC_COMP_SEARCH_HEAD = [ FormField( "Suche", FormFieldType.EXTENDED_DROPDOWN, @@ -329,7 +329,7 @@ INITREC_COMP_SEARCH_HEAD = [ ), ] -INITREC_CONTACT_PERSON = [ +_INITREC_CONTACT_PERSON = [ # TODO check removal FormField( "Name Unternehmen/Netzwerkpartner (vorausgefüllt von Suche)", @@ -400,7 +400,7 @@ INITREC_CONTACT_PERSON = [ ] -INITREC_MASTER_DATA = [ +_INITREC_MASTER_DATA = [ FormField( "Titel", FormFieldType.TEXT, @@ -560,7 +560,7 @@ INITREC_MASTER_DATA = [ ] -INITREC_ADDITIONAL_DATA = [ +_INITREC_ADDITIONAL_DATA = [ FormField( "Deutsch als Kommunikationssprache", FormFieldType.DROPDOWN, @@ -635,7 +635,7 @@ INITREC_ADDITIONAL_DATA = [ ] -INITREC_SCHOOL = [ +_INITREC_SCHOOL = [ FormField("Abschluss", FormFieldType.TEXT, required=False, key="SB_abschluss"), FormField( "Abschlussgrad laut Dokument", @@ -673,7 +673,7 @@ INITREC_SCHOOL = [ ] -INITREC_HIGHER_EDUCATION = [ +_INITREC_HIGHER_EDUCATION = [ FormField( "Anerkennung", FormFieldType.TEXT, @@ -742,7 +742,7 @@ INITREC_HIGHER_EDUCATION = [ ] -INITREC_WORK_EXPERIENCE = [ +_INITREC_WORK_EXPERIENCE = [ FormField( "Branche", FormFieldType.DROPDOWN, @@ -835,7 +835,7 @@ INITREC_WORK_EXPERIENCE = [ ] -INITREC_LANGUAGES = [ +_INITREC_LANGUAGES = [ FormField( "Sprache", FormFieldType.TEXT, @@ -930,11 +930,11 @@ INITREC_COMP = [ FormFieldType.CUSTOM, custom_widget="grunderfassung_unternehmen_suche", key="Partnersuche", - children=INITREC_COMP_SEARCH_HEAD, + children=_INITREC_COMP_SEARCH_HEAD, ), ] -INITREC_PERSON = [ +INITREC_METADATEN = [ FormField( "Ersteintrag Datum", FormFieldType.TEXT_DATETIME, @@ -978,6 +978,52 @@ INITREC_PERSON = [ required=False, key="Grunderfassung_notiz", ), +] + +INITREC_PERSON = [ + # FormField( + # "Ersteintrag Datum", + # FormFieldType.TEXT_DATETIME, + # required=False, + # key="Metadaten_erstellung", + # readonly=True, + # ignore_get_data=True, + # ), + # FormField( + # "Aktualisierung Datum", + # FormFieldType.TEXT_DATETIME, + # required=False, + # key="Metadaten_aktualisierung", + # readonly=True, + # ignore_get_data=True, + # ), + # FormField( + # "Aktualisierung Nutzer", + # FormFieldType.TEXT, + # required=False, + # key="Metadaten_nutzer", + # readonly=True, + # ignore_get_data=False, + # ), + # FormField( + # "Wiedereintrittsdatum", + # FormFieldType.DATE, + # required=False, + # key="Metadaten_wiedereintrittsdatum", + # readonly=False, + # ), + # FormField( + # "Fallnummer", + # FormFieldType.TEXT, + # required=True, + # key="Grunderfassung_fallnummer", + # ), + # FormField( + # "Notizen", + # FormFieldType.LONGTEXT, + # required=False, + # key="Grunderfassung_notiz", + # ), FormField( "Status && Projektrelevanz", FormFieldType.GROUP, @@ -1009,42 +1055,42 @@ INITREC_PERSON = [ "Daten Kontaktperson", FormFieldType.GROUP, key="Kontaktperson", - children=INITREC_CONTACT_PERSON, + children=_INITREC_CONTACT_PERSON, ), FormField( "Stammdaten", FormFieldType.GROUP, key="Stammdaten", - children=INITREC_MASTER_DATA, + children=_INITREC_MASTER_DATA, ), FormField( "Weitere Informationen", FormFieldType.GROUP, key="WeitereInfos", - children=INITREC_ADDITIONAL_DATA, + children=_INITREC_ADDITIONAL_DATA, ), FormField( "Schulbildung", FormFieldType.DYNAMIC_LIST, - children=INITREC_SCHOOL, + children=_INITREC_SCHOOL, key="Schulbildung", ), FormField( "Studium/Ausbildung", FormFieldType.DYNAMIC_LIST, - children=INITREC_HIGHER_EDUCATION, + children=_INITREC_HIGHER_EDUCATION, key="HoehereBildung", ), FormField( "Arbeitserfahrung", FormFieldType.DYNAMIC_LIST, - children=INITREC_WORK_EXPERIENCE, + children=_INITREC_WORK_EXPERIENCE, key="Arbeitserfahrung", ), FormField( "Sprachkenntnisse", FormFieldType.DYNAMIC_LIST, - children=INITREC_LANGUAGES, + children=_INITREC_LANGUAGES, key="Sprachkenntnisse", ), ] diff --git a/src/wce_crm/gui.py b/src/wce_crm/gui.py index ea8a5e5..e3cf0cb 100644 --- a/src/wce_crm/gui.py +++ b/src/wce_crm/gui.py @@ -1616,6 +1616,11 @@ class InitRecForm(QWidget): pformat(self.STATE.form_data), ) # TODO error catching? + logger_initrecform.info( + "[InitRecForm] Pydantic model name: %s", + self.STATE.cfg.pydantic_model.__name__, + ) + model = self.cfg.pydantic_model(**self.STATE.form_data) logger_initrecform.debug("[InitRecForm] Loaded to Pydantic.") logger_initrecform.debug("[InitRecForm] Convert to GUI structure...") @@ -3097,6 +3102,12 @@ class CollapsibleBox(QWidget): layout.addWidget(widget) self.setContentLayout(layout) + def set_open( + self, + open: bool, + ) -> None: + self._on_toggle(open) + class Page_InitRec(QWidget): back_main_requested = Signal() # back to main page @@ -3247,6 +3258,28 @@ class Page_InitRec(QWidget): # now we try to render the layout within the collapsable box self.container_layout.addSpacing(20) + # ** metadata + self.initrec_form_metadata_box = CollapsibleBox("Metadaten") + ent_type = EntityType.METADATA + self.initrec_form_metadata = InitRecForm(self.STATE.initrec_form_states[ent_type]) + + assert self.STATE.initrec_form_states[ent_type] is self.initrec_form_metadata.STATE + + self.STATE.child_modules.append(self.initrec_form_metadata) + self.initrec_form_metadata_box.setContentWidget(self.initrec_form_metadata) + self.container_layout.addWidget(self.initrec_form_metadata_box) + + # --- CUSTOM LOGIC --- + # ** 'Wiedereintrittsdatum' -> only relevant without company + search_res = search_widgets_by_key( + self.initrec_form_metadata.widget_registry, "wiedereintrittsdatum" + ) + print(f">>>> {pformat(search_res)}") + assert len(search_res) == 1 + field_wiedereintrittsdatum = search_res[0]["widget"] + assert isinstance(field_wiedereintrittsdatum, QDateEdit) + self.field_wiedereintrittsdatum = field_wiedereintrittsdatum + # ** person self.initrec_form_person_box = CollapsibleBox("Angaben zur Person") ent_type = EntityType.PERSON @@ -3286,7 +3319,7 @@ class Page_InitRec(QWidget): assert self.STATE.initrec_form_states[ent_type] is self.initrec_form_company.STATE - target_idx = self.container_layout.indexOf(self.initrec_form_person_box) - 1 + target_idx = self.container_layout.indexOf(self.initrec_form_person_box) self.initrec_form_company_box.setContentWidget(self.initrec_form_company) self.container_layout.insertWidget(target_idx, self.initrec_form_company_box) @@ -3321,9 +3354,13 @@ class Page_InitRec(QWidget): self.company_profile_requested.emit(req_state) def _update_layout(self) -> None: + self.initrec_form_metadata_box.set_open(True) if self.STATE.recording_type is RecordingType.WITH_COMPANY: # add company self.initrec_form_company_box.setVisible(True) + self.initrec_form_metadata.top_level_form_layout.setRowVisible( + self.field_wiedereintrittsdatum, False + ) self.comp_profile_btn.setVisible(True) if self.STATE.ids.valid(EntityType.COMPANY): self.comp_profile_btn.setEnabled(True) @@ -3332,6 +3369,9 @@ class Page_InitRec(QWidget): else: self.initrec_form_company_box.setVisible(False) + self.initrec_form_metadata.top_level_form_layout.setRowVisible( + self.field_wiedereintrittsdatum, True + ) self.comp_profile_btn.setVisible(False) self.comp_profile_btn.setEnabled(False) @@ -3454,9 +3494,9 @@ class Page_InitRec(QWidget): pers_id=5, ) - ids = EntityIds( - link_id=7, - ) + # ids = EntityIds( + # link_id=7, + # ) self.STATE.ids.update(ids, ignore_none=False) if ids.empty(): @@ -3523,6 +3563,13 @@ class Page_InitRec(QWidget): state.form_data = self.STATE.form_data.person if not update_only: self.initrec_form_person.load_state(state) + case EntityType.METADATA: + state.form_data = self.STATE.form_data.metadata + logger_initrecform.info( + "[Page -- InitRec] Load METADATA from Pydantic model...\n%s", + pformat(self.STATE.form_data.metadata), + ) + self.initrec_form_metadata.load_state(state) case _: raise RuntimeError(f"Unspecified entity type {ent_type}") @@ -3602,6 +3649,8 @@ class Page_InitRec(QWidget): self.STATE.form_data.company = form_state.form_data case EntityType.PERSON: self.STATE.form_data.person = form_state.form_data + case EntityType.METADATA: + self.STATE.form_data.metadata = form_state.form_data case _: raise RuntimeError(f"Unspecified entity type {form_state.ent_type}") @@ -3718,6 +3767,9 @@ class Page_InitRec(QWidget): case EntityType.PERSON: val_data = state.cfg.pydantic_model(**self.STATE.form_data.person) db_data.person = val_data.to_db(exclude=state.cfg.ignored_keys) + case EntityType.METADATA: + val_data = state.cfg.pydantic_model(**self.STATE.form_data.metadata) + db_data.metadata = val_data.to_db(exclude=state.cfg.ignored_keys) case _: raise RuntimeError(f"Unspecified entity type {ent_type}") diff --git a/src/wce_crm/types.py b/src/wce_crm/types.py index 1bb14aa..45d2a17 100644 --- a/src/wce_crm/types.py +++ b/src/wce_crm/types.py @@ -106,6 +106,13 @@ class EntityIds: for field_name in self._field_names if field_name in RELEVANT_ATTRIBUTES ) + elif ent_type is EntityType.METADATA: + RELEVANT_ATTRIBUTES = set() + relevant_fields = ( + getattr(self, field_name) + for field_name in self._field_names + if field_name in RELEVANT_ATTRIBUTES + ) else: raise RuntimeError(f"Unspecified entity type {ent_type}") -- 2.34.1 From a515f3b820bf09601b5071e0b3a073e80f9d05eb Mon Sep 17 00:00:00 2001 From: foefl Date: Tue, 28 Jul 2026 08:27:45 +0200 Subject: [PATCH 09/13] adapt customer requirements, REQ 1006, 1007 --- ...fc1dfdf_update_schema_to_meet_customer_.py | 48 +++++ data_model/project_db_schema.aml | 3 +- src/wce_crm/backend/backend.py | 6 +- src/wce_crm/data_models.py | 3 +- src/wce_crm/db.py | 31 +--- src/wce_crm/form_defs.py | 166 ++++-------------- src/wce_crm/gui.py | 19 +- 7 files changed, 100 insertions(+), 176 deletions(-) create mode 100644 alembic/versions/5dfa1fc1dfdf_update_schema_to_meet_customer_.py diff --git a/alembic/versions/5dfa1fc1dfdf_update_schema_to_meet_customer_.py b/alembic/versions/5dfa1fc1dfdf_update_schema_to_meet_customer_.py new file mode 100644 index 0000000..24e62ec --- /dev/null +++ b/alembic/versions/5dfa1fc1dfdf_update_schema_to_meet_customer_.py @@ -0,0 +1,48 @@ +"""update schema to meet customer requirements + +Revision ID: 5dfa1fc1dfdf +Revises: e290fe18132d +Create Date: 2026-07-28 08:39:44.168872 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "5dfa1fc1dfdf" +down_revision: Union[str, Sequence[str], None] = "e290fe18132d" +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! ### + with op.batch_alter_table("Grunderfassung_Personen", schema=None) as batch_op: + batch_op.add_column( + sa.Column("Stammdaten__kanal_aufmerksamkeit", sa.Text(), nullable=True) + ) + + with op.batch_alter_table("Personen_Unternehmen_Zuordnung", schema=None) as batch_op: + batch_op.add_column(sa.Column("Partnersuche__kontaktweg", sa.Text(), nullable=True)) + batch_op.drop_column("Partnersuche__kanal_aufmerksamkeit") + + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("Personen_Unternehmen_Zuordnung", schema=None) as batch_op: + batch_op.add_column( + sa.Column("Partnersuche__kanal_aufmerksamkeit", sa.TEXT(), nullable=True) + ) + batch_op.drop_column("Partnersuche__kontaktweg") + + with op.batch_alter_table("Grunderfassung_Personen", schema=None) as batch_op: + batch_op.drop_column("Stammdaten__kanal_aufmerksamkeit") + + # ### end Alembic commands ### diff --git a/data_model/project_db_schema.aml b/data_model/project_db_schema.aml index 12c8bfc..7b10d53 100644 --- a/data_model/project_db_schema.aml +++ b/data_model/project_db_schema.aml @@ -15,7 +15,7 @@ Personen_Unternehmen_Zuordnung pers_id int pk -> Grunderfassung_Personen(pers_id) unique=uq_aktive_zuordnung un_id int -> Unternehmen(un_id) unique=uq_aktive_zuordnung | alias 'Partnersuche__un_suche' an_id int -> Ext_Kontaktliste.Ansprechpartner(an_id) | alias 'Partnersuche__person_suche - Partnersuche__kanal_aufmerksamkeit text nullable + Partnersuche__kontaktweg text nullable erstellt "timestamp with time zone"=`datetime.datetime.now(datetime.UTC)` aktualisiert "timestamp with time zone"=`datetime.datetime.now(datetime.UTC)` {on_update: `datetime.datetime.now(datetime.UTC)`} geloescht "timestamp with time zone" nullable | Soft-Delete Marker (partieller Unique-Index: WHERE geloescht IS NULL) @@ -45,6 +45,7 @@ Grunderfassung_Personen Projektrelevanz__relevanz text nullable Projektrelevanz__foerderperiode text nullable Schulbildung text nullable + Stammdaten__kanal_aufmerksamkeit text nullable Stammdaten__PLZ text nullable Stammdaten__anrede_anschrift text nullable Stammdaten__anzahl_kinder__alter text nullable diff --git a/src/wce_crm/backend/backend.py b/src/wce_crm/backend/backend.py index 6781ce7..228e51e 100644 --- a/src/wce_crm/backend/backend.py +++ b/src/wce_crm/backend/backend.py @@ -506,7 +506,7 @@ def page_initrec_to_db( index_where=sa.text("geloescht IS NULL"), set_={ "an_id": stmt.excluded.an_id, - "Partnersuche__kanal_aufmerksamkeit": stmt.excluded.Partnersuche__kanal_aufmerksamkeit, + "Partnersuche__kontaktweg": stmt.excluded.Partnersuche__kontaktweg, "aktualisiert": datetime.datetime.now(datetime.UTC), "geloescht": data.geloescht, }, @@ -541,7 +541,7 @@ def page_initrec_from_db( db.t_zuordnung_personen_unternehmen.c.pers_id, db.t_zuordnung_personen_unternehmen.c.un_id, db.t_zuordnung_personen_unternehmen.c.an_id, - db.t_zuordnung_personen_unternehmen.c.Partnersuche__kanal_aufmerksamkeit, + db.t_zuordnung_personen_unternehmen.c.Partnersuche__kontaktweg, ).where( db.t_zuordnung_personen_unternehmen.c.id == ids.link_id, ) @@ -552,7 +552,7 @@ def page_initrec_from_db( db.t_zuordnung_personen_unternehmen.c.pers_id, db.t_zuordnung_personen_unternehmen.c.un_id, db.t_zuordnung_personen_unternehmen.c.an_id, - db.t_zuordnung_personen_unternehmen.c.Partnersuche__kanal_aufmerksamkeit, + db.t_zuordnung_personen_unternehmen.c.Partnersuche__kontaktweg, ).where( db.t_zuordnung_personen_unternehmen.c.pers_id == ids.pers_id, db.t_zuordnung_personen_unternehmen.c.un_id == ids.un_id, diff --git a/src/wce_crm/data_models.py b/src/wce_crm/data_models.py index fb6b97f..922c425 100644 --- a/src/wce_crm/data_models.py +++ b/src/wce_crm/data_models.py @@ -564,7 +564,7 @@ class Grunderfassung_PartnerSuche(BaseModel): un_suche: int | None person_suche: int | None - kanal_aufmerksamkeit: str | None + kontaktweg: str | None class Grunderfassung_Projektrelevanz(BaseModel): @@ -592,6 +592,7 @@ class Grunderfassung_Kontaktperson(BaseModel): class Grunderfassung_Stammdaten(BaseModel): model_config = ConfigDict(str_strip_whitespace=True) + kanal_aufmerksamkeit: str | None titel: str | None anrede_anschrift: str name: str diff --git a/src/wce_crm/db.py b/src/wce_crm/db.py index d5450b3..2b4b231 100644 --- a/src/wce_crm/db.py +++ b/src/wce_crm/db.py @@ -397,19 +397,6 @@ t_grunderfassung_personen: Table = Table( UTCDateTime, nullable=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", sa.String(20), nullable=True), Column("Metadaten_wiedereintrittsdatum", sa.Date, nullable=True, default=None), Column("Arbeitserfahrung", sa.Text, nullable=True), @@ -423,15 +410,14 @@ t_grunderfassung_personen: Table = Table( Column("Kontaktperson__KP_funktion_beziehung", sa.Text, nullable=True), Column("Kontaktperson__KP_mobilfunknummer", sa.Text, nullable=True), Column("Kontaktperson__KP_name", sa.Text, nullable=True), - Column( - "Kontaktperson__KP_name_partner", sa.Text, nullable=True - ), # TODO: check if needed when set by trigger + Column("Kontaktperson__KP_name_partner", sa.Text, nullable=True), Column("Kontaktperson__KP_titel", sa.Text, nullable=True), Column("Kontaktperson__KP_vorname", sa.Text, nullable=True), Column("Projektrelevanz__relevanz", sa.Text, nullable=True), Column("Projektrelevanz__foerderperiode", sa.Text, nullable=True), Column("Schulbildung", sa.Text, nullable=True), Column("Sprachkenntnisse", sa.Text, nullable=True), + Column("Stammdaten__kanal_aufmerksamkeit", sa.Text, nullable=True), Column("Stammdaten__PLZ", sa.Text, nullable=True), Column("Stammdaten__anrede_anschrift", sa.Text, nullable=True), Column("Stammdaten__anzahl_kinder__alter", sa.Text, nullable=True), @@ -490,18 +476,7 @@ t_zuordnung_personen_unternehmen: Table = Table( UTCDateTime, nullable=True, ), - Column("Partnersuche__kanal_aufmerksamkeit", sa.Text, nullable=True), - # Column( - # "gueltig_ab", - # UTCDateTime, - # nullable=False, - # default=lambda: datetime.datetime.now(datetime.UTC), - # ), - # Column( - # "gueltig_bis", - # UTCDateTime, - # nullable=True, - # ), + Column("Partnersuche__kontaktweg", sa.Text, nullable=True), Index( "uq_aktive_zuordnung", "un_id", diff --git a/src/wce_crm/form_defs.py b/src/wce_crm/form_defs.py index 59e8005..aa476d5 100644 --- a/src/wce_crm/form_defs.py +++ b/src/wce_crm/form_defs.py @@ -307,53 +307,38 @@ _INITREC_COMP_SEARCH_HEAD = [ info="an_position", ), FormField( - "Wie sind Sie auf uns aufmerksam geworden?", + "Kontaktweg", FormFieldType.DROPDOWN, required=False, - key="kanal_aufmerksamkeit", + key="kontaktweg", options=[ - ("Agentur für Arbeit", None), - ("Ausländerbehörde", None), - ("Jobcenter", None), - ("Freunde/Familie", None), - ("Anerkennungsstelle", None), - ("Beratungsstelle", None), - ("Internet", None), - ("Arbeitgeber", None), - ("Bildungsdienstleister", None), - ("Welcome-Mappe", None), - ("Newsletter WFE", None), - ("Newsletter RM", None), - ("Sonstiges", None), + ("Telefon", None), + ("Mail", None), + ("Persönliches Gespräch", None), ], ), ] _INITREC_CONTACT_PERSON = [ - # TODO check removal FormField( - "Name Unternehmen/Netzwerkpartner (vorausgefüllt von Suche)", + "Name Unternehmen/Netzwerkpartner", FormFieldType.TEXT, key="KP_name_partner", required=False, - placeholder="[TODO: Logik in Verbindung mit Unternehmen unklar]", - readonly=True, + readonly=False, ), FormField( "Titel", FormFieldType.TEXT, key="KP_titel", required=False, - tooltip=( - "* nur wenn anrufende Person oder kontaktaufnehmende Person " - "nicht die zu beratende Person ist" - ), ), FormField( - "Anrede_Anschrift", - FormFieldType.TEXT, + "Anrede", + FormFieldType.DROPDOWN, key="KP_anrede_anschrift", required=True, + options=[("Sehr geehrte Frau", None), ("Sehr geehrter Herr", None)], ), FormField( "Name", @@ -401,21 +386,39 @@ _INITREC_CONTACT_PERSON = [ _INITREC_MASTER_DATA = [ + FormField( + "Wie sind Sie auf uns aufmerksam geworden?", + FormFieldType.DROPDOWN, + required=False, + key="kanal_aufmerksamkeit", + options=[ + ("Agentur für Arbeit", None), + ("Ausländerbehörde", None), + ("Jobcenter", None), + ("Freunde/Familie", None), + ("Anerkennungsstelle", None), + ("Beratungsstelle", None), + ("Internet", None), + ("Arbeitgeber", None), + ("Bildungsdienstleister", None), + ("Welcome-Mappe", None), + ("Newsletter WFE", None), + ("Newsletter RM", None), + ("Sonstiges", None), + ], + ), FormField( "Titel", FormFieldType.TEXT, key="titel", required=False, - tooltip=( - "* nur wenn anrufende Person oder kontaktaufnehmende Person " - "nicht die zu beratende Person ist" - ), ), FormField( "Anrede", - FormFieldType.TEXT, + FormFieldType.DROPDOWN, key="anrede_anschrift", required=True, + options=[("Sehr geehrte Frau", None), ("Sehr geehrter Herr", None)], ), FormField( "Name", @@ -427,17 +430,13 @@ _INITREC_MASTER_DATA = [ "Vorname", FormFieldType.TEXT, key="vorname", - required=False, + required=True, ), FormField( "Geburtsdatum", FormFieldType.DATE, key="geburtsdatum", required=False, - tooltip=( - "* Wichtig zu erfragen, da u.a. Mindestgehaltsschwelle davon abhängt " - "(Regelung bei Ü45 Jahre)" - ), ), FormField( "Herkunftsland", @@ -446,7 +445,6 @@ _INITREC_MASTER_DATA = [ required=True, placeholder="Suche...", options=COUNTRY_LIST.for_dropdown, - tooltip=("* Wichtig zu erfragen aufgrund eventueller EU-Freizügigkeitsregelung"), ), FormField( "Staatsangehörigkeit", @@ -455,7 +453,6 @@ _INITREC_MASTER_DATA = [ required=False, placeholder="Suche...", options=COUNTRY_LIST.for_dropdown, - tooltip=("* Wichtig zu erfragen aufgrund eventueller EU-Freizügigkeitsregelung"), ), FormField( "Rückkehrer", @@ -463,7 +460,6 @@ _INITREC_MASTER_DATA = [ key="rueckkehrer", required=False, options=[("ja", None), ("nein", None)], - tooltip=("* Wichtig zu erfragen aufgrund eventueller EU-Freizügigkeitsregelung"), ), FormField( "Wo befindet sich die Person?", @@ -502,7 +498,6 @@ _INITREC_MASTER_DATA = [ key="bundesland", required=False, options=GERMAN_STATE_LIST.for_dropdown, - tooltip=("nur wenn Inland angegeben"), ), FormField( "Land", @@ -535,13 +530,11 @@ _INITREC_MASTER_DATA = [ FormFieldType.TEXT, key="familienstand", required=False, - tooltip="* Wichtig zu erfragen aufgrund Lebensunterhaltssicherung", ), FormField( "Anzahl Kinder", FormFieldType.DYNAMIC_DROPDOWN_NUMERIC, required=False, - tooltip="* Wichtig zu erfragen aufgrund Lebensunterhaltssicherung", key="anzahl_kinder", children=[ FormField( @@ -549,7 +542,6 @@ _INITREC_MASTER_DATA = [ FormFieldType.DROPDOWN, required=False, options=[(str(x), None) for x in range(11)], - tooltip="* Wichtig zu erfragen aufgrund Lebensunterhaltssicherung", key="anzahl", children=[ FormField("Alter Kind", FormFieldType.TEXT, key="alter"), @@ -593,7 +585,6 @@ _INITREC_ADDITIONAL_DATA = [ ("Chancenaufenthaltsrecht §104c AufenthG", None), ("Sonstiges", None), ], - tooltip="sofern nicht bekannt, unbedingt einfordern", ), FormField( "Gültigkeit Aufenthaltsstatus", @@ -685,10 +676,6 @@ _INITREC_HIGHER_EDUCATION = [ FormFieldType.TEXT, required=False, key="HB_abschlussgrad", - tooltip=( - "bitte den Titel eingeben z.B. Doktor, Diplom oder " - "Betriebswirt (Fachschulabschluss)" - ), ), FormField( "Abschlussgrad laut Dokument", @@ -707,10 +694,6 @@ _INITREC_HIGHER_EDUCATION = [ FormFieldType.TEXT, required=False, key="HB_beruf", - tooltip=( - "bitte spezifizieren z.B. Allgemeinmedizin, Ingenieur Maschinenbau, " - "technischer Betriebswirt Datenverarbeitung" - ), ), FormField( "Land", @@ -737,7 +720,6 @@ _INITREC_HIGHER_EDUCATION = [ FormFieldType.TEXT, required=False, key="HB_bemerkungsfeld", - tooltip="z.B. Promotionen oder den Studiengang angeben", ), ] @@ -824,7 +806,6 @@ _INITREC_WORK_EXPERIENCE = [ ("Teilzeit", None), ("Sonstiges", None), ], - tooltip="Minijob, Praktikum, Wehrdienst, soziale Dienste", ), FormField( "Bemerkungsfeld", @@ -889,42 +870,6 @@ _INITREC_LANGUAGES = [ INITREC_COMP = [ - # FormField( - # "Ersteintrag Datum", - # FormFieldType.TEXT_DATETIME, - # required=False, - # key="Metadaten_erstellung", - # readonly=True, - # ignore_get_data=True, - # ), - # FormField( - # "Aktualisierung Datum", - # FormFieldType.TEXT_DATETIME, - # required=False, - # key="Metadaten_aktualisierung", - # readonly=True, - # ignore_get_data=True, - # ), - # FormField( - # "Aktualisierung Nutzer", - # FormFieldType.TEXT, - # required=False, - # key="Metadaten_nutzer", - # readonly=True, - # ignore_get_data=True, - # ), - # FormField( - # "Fallnummer", - # FormFieldType.TEXT, - # required=True, - # key="Grunderfassung_fallnummer", - # ), - # FormField( - # "Notizen", - # FormFieldType.LONGTEXT, - # required=False, - # key="Grunderfassung_notiz", - # ), FormField( "Suche", FormFieldType.CUSTOM, @@ -981,49 +926,6 @@ INITREC_METADATEN = [ ] INITREC_PERSON = [ - # FormField( - # "Ersteintrag Datum", - # FormFieldType.TEXT_DATETIME, - # required=False, - # key="Metadaten_erstellung", - # readonly=True, - # ignore_get_data=True, - # ), - # FormField( - # "Aktualisierung Datum", - # FormFieldType.TEXT_DATETIME, - # required=False, - # key="Metadaten_aktualisierung", - # readonly=True, - # ignore_get_data=True, - # ), - # FormField( - # "Aktualisierung Nutzer", - # FormFieldType.TEXT, - # required=False, - # key="Metadaten_nutzer", - # readonly=True, - # ignore_get_data=False, - # ), - # FormField( - # "Wiedereintrittsdatum", - # FormFieldType.DATE, - # required=False, - # key="Metadaten_wiedereintrittsdatum", - # readonly=False, - # ), - # FormField( - # "Fallnummer", - # FormFieldType.TEXT, - # required=True, - # key="Grunderfassung_fallnummer", - # ), - # FormField( - # "Notizen", - # FormFieldType.LONGTEXT, - # required=False, - # key="Grunderfassung_notiz", - # ), FormField( "Status && Projektrelevanz", FormFieldType.GROUP, diff --git a/src/wce_crm/gui.py b/src/wce_crm/gui.py index e3cf0cb..75e9bad 100644 --- a/src/wce_crm/gui.py +++ b/src/wce_crm/gui.py @@ -1142,15 +1142,15 @@ class Grunderfassung_SuchWidget(CustomFormWidget): self.person_search_input = cast(QComboBox, lookup[0]["widget"]) self.person_search_input.currentIndexChanged.connect(self._selected_person) - self.CHANNEL_AWARENESS_KEY = "kanal_aufmerksamkeit" - lookup = search_widgets_by_key(self.widget_registry, self.CHANNEL_AWARENESS_KEY) + self.CHANNEL_CONTACT_KEY = "kontaktweg" + lookup = search_widgets_by_key(self.widget_registry, self.CHANNEL_CONTACT_KEY) assert len(lookup) == 1 - self.channel_awareness = cast(QComboBox, lookup[0]["widget"]) + self.channel_contact = cast(QComboBox, lookup[0]["widget"]) self.DATA_EXPORT_FIELDS = ( self.COMPANY_SEARCH_INPUT_KEY, self.PERSON_SEARCH_INPUT_KEY, - self.CHANNEL_AWARENESS_KEY, + self.CHANNEL_CONTACT_KEY, ) self.company_widgets: dict[str, QWidget] = {} @@ -1234,7 +1234,7 @@ class Grunderfassung_SuchWidget(CustomFormWidget): def _clear_company_fields(self) -> None: self.person_search_input.clear() - self.channel_awareness.setCurrentIndex(0) + self.channel_contact.setCurrentIndex(0) for widget in self.company_widgets.values(): widget = cast(QLineEdit, widget) widget.clear() @@ -3473,8 +3473,6 @@ class Page_InitRec(QWidget): # TODO saving logic to connect company and person if needed def _reset(self) -> None: - # TODO change to make these calls implicit in child modules via - # TODO staes for form in self.STATE.child_modules: form.reset_form() self.STATE.ids = EntityIds() @@ -3494,9 +3492,9 @@ class Page_InitRec(QWidget): pers_id=5, ) - # ids = EntityIds( - # link_id=7, - # ) + ids = EntityIds( + link_id=7, + ) self.STATE.ids.update(ids, ignore_none=False) if ids.empty(): @@ -3752,7 +3750,6 @@ class Page_InitRec(QWidget): try: logger_page_initrec.debug("[Page -- InitRec] Call Pydantic...") - # validated_data = self.cfg.pydantic_model(**form_data) for ent_type, state in self.STATE.initrec_form_states.items(): logger_page_initrec.debug( -- 2.34.1 From 2894d3a1a9c8da7a213e75c4aadf96bec023218f Mon Sep 17 00:00:00 2001 From: foefl Date: Tue, 28 Jul 2026 09:18:47 +0200 Subject: [PATCH 10/13] fulfil customer requirements REQ 1011, test for REQ 1013, 1014 --- src/wce_crm/form_defs.py | 8 ++++---- src/wce_crm/gui.py | 18 +++++++++++++----- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/wce_crm/form_defs.py b/src/wce_crm/form_defs.py index aa476d5..c0da687 100644 --- a/src/wce_crm/form_defs.py +++ b/src/wce_crm/form_defs.py @@ -434,7 +434,7 @@ _INITREC_MASTER_DATA = [ ), FormField( "Geburtsdatum", - FormFieldType.DATE, + FormFieldType.DATE_FLEXIBLE, key="geburtsdatum", required=False, ), @@ -588,7 +588,7 @@ _INITREC_ADDITIONAL_DATA = [ ), FormField( "Gültigkeit Aufenthaltsstatus", - FormFieldType.DATE, + FormFieldType.DATE_FLEXIBLE, required=False, key="WI_gueltigkeit_aufenthaltstitel", ), @@ -858,7 +858,7 @@ _INITREC_LANGUAGES = [ ), FormField( "Datum des Nachweises", - FormFieldType.DATE, + FormFieldType.DATE_FLEXIBLE, required=False, key="SP_datum_nachweis", ), @@ -906,7 +906,7 @@ INITREC_METADATEN = [ ), FormField( "Wiedereintrittsdatum", - FormFieldType.DATE, + FormFieldType.DATE_FLEXIBLE, required=False, key="Metadaten_wiedereintrittsdatum", readonly=False, diff --git a/src/wce_crm/gui.py b/src/wce_crm/gui.py index 75e9bad..b8204a5 100644 --- a/src/wce_crm/gui.py +++ b/src/wce_crm/gui.py @@ -862,13 +862,13 @@ def set_widget_value( raise ValueError(f"Could not parse date field value >{value}<") widget.setDate(set_date) elif isinstance(widget, FlexibleDateInput): - assert isinstance(value, datetime.datetime) + assert isinstance(value, (datetime.datetime, datetime.date)) or value is None try: widget.set_pydate(value, raise_error=True) except ValueError: raise ValueError(f"Could not parse date field value >{value}<") elif isinstance(widget, FlexibleDateTimeInput): - assert isinstance(value, datetime.datetime) + assert isinstance(value, datetime.datetime) or value is None try: widget.set_pydatetime(value, raise_error=True) except ValueError: @@ -2005,9 +2005,13 @@ class FlexibleDateInput(QWidget): def set_pydate( self, - date: datetime.datetime, + date: datetime.datetime | datetime.date | None, raise_error: bool = False, ) -> None: + if date is None: + self.reset() + return + date_to_set = QDate(date.year, date.month, date.day) if raise_error and not date_to_set.isValid(): raise ValueError("Could not parse date object correctly") @@ -2224,9 +2228,13 @@ class FlexibleDateTimeInput(QWidget): def set_pydatetime( self, - dt: datetime.datetime, + dt: datetime.datetime | None, raise_error: bool = False, ) -> None: + if dt is None: + self.reset() + return + dt_to_set = QDateTime(dt.year, dt.month, dt.day, dt.hour, dt.minute, 0) if raise_error and not dt_to_set.isValid(): raise ValueError("Could not parse datetime object correctly") @@ -3277,7 +3285,7 @@ class Page_InitRec(QWidget): print(f">>>> {pformat(search_res)}") assert len(search_res) == 1 field_wiedereintrittsdatum = search_res[0]["widget"] - assert isinstance(field_wiedereintrittsdatum, QDateEdit) + assert isinstance(field_wiedereintrittsdatum, FlexibleDateInput) self.field_wiedereintrittsdatum = field_wiedereintrittsdatum # ** person -- 2.34.1 From 07c2bd92daa4dec8f370ba824034713eab17fc8d Mon Sep 17 00:00:00 2001 From: foefl Date: Tue, 28 Jul 2026 09:45:28 +0200 Subject: [PATCH 11/13] adaptive validation for initial recording page --- src/wce_crm/gui.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/wce_crm/gui.py b/src/wce_crm/gui.py index b8204a5..1fc8a6e 100644 --- a/src/wce_crm/gui.py +++ b/src/wce_crm/gui.py @@ -3500,9 +3500,9 @@ class Page_InitRec(QWidget): pers_id=5, ) - ids = EntityIds( - link_id=7, - ) + # ids = EntityIds( + # link_id=7, + # ) self.STATE.ids.update(ids, ignore_none=False) if ids.empty(): @@ -3688,6 +3688,12 @@ class Page_InitRec(QWidget): def validate(self) -> list[str]: errors: list[str] = [] for m in self.STATE.child_modules: + if ( + m.STATE.ent_type is EntityType.COMPANY + and self.STATE.recording_type is not RecordingType.WITH_COMPANY + ): + continue + errors.extend(m.validate()) return errors -- 2.34.1 From 02d94c4a80e13e967bcb5ce05af22a0c3c1e46ab Mon Sep 17 00:00:00 2001 From: foefl Date: Tue, 28 Jul 2026 12:14:51 +0200 Subject: [PATCH 12/13] adapted linking dialog for consulting sessions --- prototypes/tests.py | 80 +++++++++- src/wce_crm/backend/backend.py | 98 +++++------- src/wce_crm/data_models.py | 70 +-------- src/wce_crm/gui.py | 274 +++++++++++++++------------------ src/wce_crm/logging.py | 2 +- src/wce_crm/types.py | 1 + 6 files changed, 247 insertions(+), 278 deletions(-) diff --git a/prototypes/tests.py b/prototypes/tests.py index b6ee6a0..5894c3e 100644 --- a/prototypes/tests.py +++ b/prototypes/tests.py @@ -8,12 +8,12 @@ import json import re from collections import defaultdict from collections.abc import Sequence -from pprint import pprint +from pprint import pformat, pprint from typing import Annotated, Any import babel import polars as pl -import sqlalchemy as sql +import sqlalchemy as sa from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator, model_validator from PySide6.QtCore import QDate, Qt @@ -22,11 +22,81 @@ from wce_crm.backend import backend from wce_crm.types import EntityIds, RecordingType # %% -empty = tuple() +# backend.page_consulting_linking_companies(None) +# backend.page_consulting_linking_persons(3) -# not any(empty) -all(empty) +# %% +@dc.dataclass(slots=True, kw_only=True) +class LinkingPerson: + pers_id: int + display_name: str + un_ids: set[int] + + +with db.ENGINE.connect() as conn: + # if un_id None: list all persons + # else: list just the persons associated with the company + + stmt = ( + sa.select( + db.t_grunderfassung_personen.c.pers_id, + db.t_zuordnung_personen_unternehmen.c.un_id, + db.t_grunderfassung_personen.c.Stammdaten__name, + db.t_grunderfassung_personen.c.Stammdaten__vorname, + db.t_grunderfassung_personen.c.Stammdaten__ort, + ) + .outerjoin( + db.t_zuordnung_personen_unternehmen, + db.t_grunderfassung_personen.c.pers_id + == db.t_zuordnung_personen_unternehmen.c.pers_id, + ) + .order_by(db.t_grunderfassung_personen.c.Stammdaten__name) + ) + + # if un_id is not None: + # person_select = person_select.select_from( + # db.t_grunderfassung_personen.join( + # db.t_zuordnung_personen_unternehmen, + # db.t_grunderfassung_personen.c.pers_id + # == db.t_zuordnung_personen_unternehmen.c.pers_id, + # ) + # ).where(db.t_zuordnung_personen_unternehmen.c.un_id == un_id) + + ret = conn.execute(stmt) + + persons_map: dict[str, Any] = {} + for row in ret: + p_id = row.pers_id + + if p_id not in persons_map: + first_name = row.Stammdaten__vorname + last_name = row.Stammdaten__name + location_city = row.Stammdaten__ort + + display_name = f"{first_name} {last_name}" + if location_city: + display_name += f" (Ort: {location_city})" + + persons_map[p_id] = { + "pers_id": p_id, + "display_name": display_name, + "un_ids": set(), + } + + # Falls verknüpft, ID zur Menge hinzufügen + if row.un_id is not None: + persons_map[p_id]["un_ids"].add(row.un_id) + + person_names = [LinkingPerson(**data) for data in persons_map.values()] + + +# person_names = ret.mappings().all() +# if not person_names: +# print(f"No persons found for 'un_id': {un_id}") + +print(f"[Page Consulting -- Linking] Retrieved person names:\n{pformat(person_names)}") + # %% dic = { "t1": "test1", diff --git a/src/wce_crm/backend/backend.py b/src/wce_crm/backend/backend.py index 228e51e..e412388 100644 --- a/src/wce_crm/backend/backend.py +++ b/src/wce_crm/backend/backend.py @@ -872,23 +872,18 @@ def page_consulting_from_db( # // consulting page interaction -# TODO change to correct logic def page_consulting_linking_companies( un_id: RecId | None, ) -> list[LinkingConsultationsEntryCompany]: - raise RuntimeError("TODOs to implement") - with db.ENGINE.connect() as conn: q_relevant_companies = sa.select( db.t_unternehmen.c.un_id, - db.t_unternehmen.c.Partnersuche__un_suche, ).where( db.t_unternehmen.c.geloescht.is_(None), - db.t_unternehmen.c.Partnersuche__un_suche.is_not(None), ) if un_id is not None: q_relevant_companies = q_relevant_companies.where( - db.grunderfassung_unternehmen.c.un_id == un_id + db.t_unternehmen.c.un_id == un_id ) q_relevant_companies = q_relevant_companies.subquery("relevant_companies") @@ -899,8 +894,7 @@ def page_consulting_linking_companies( ).select_from( q_relevant_companies.join( db.t_ext_crm_master_attach, - q_relevant_companies.c.Partnersuche__un_suche - == db.t_ext_crm_master_attach.c.ma_id, + q_relevant_companies.c.un_id == db.t_ext_crm_master_attach.c.ma_id, ) ) @@ -923,57 +917,49 @@ def page_consulting_linking_companies( return linking_entries -def page_consulting_linking_persons( - un_id: RecId | None, -) -> list[LinkingConsultationsEntryPerson]: - raise RuntimeError("TODOs to implement") - +def page_consulting_linking_persons() -> list[LinkingConsultationsEntryPerson]: with db.ENGINE.connect() as conn: - # if un_id None: list all persons - # else: list just the persons associated with the company - - person_select = sa.select( - db.t_grunderfassung_personen.c.pers_id, - db.t_grunderfassung_personen.c.Stammdaten__name, - db.t_grunderfassung_personen.c.Stammdaten__vorname, - db.t_grunderfassung_personen.c.Stammdaten__ort, - ) - - if un_id is not None: - person_select = person_select.select_from( - db.t_grunderfassung_personen.join( - db.t_zuordnung_personen_unternehmen, - db.t_grunderfassung_personen.c.pers_id - == db.t_zuordnung_personen_unternehmen.c.pers_id, - ) - ).where(db.t_zuordnung_personen_unternehmen.c.un_id == un_id) - - ret = conn.execute(person_select) - person_names = ret.mappings().all() - if not person_names: - logger.warning(f"No persons found for 'un_id': {un_id}") - return [] - - logger.debug("[Page Consulting -- Linking] Retrieved person names:\n%s", person_names) - - linking_entries: list[LinkingConsultationsEntryPerson] = [] - for entry in person_names: - first_name = entry["Stammdaten__vorname"] - last_name = entry["Stammdaten__name"] - location_city = entry["Stammdaten__ort"] - - display_name = f"{first_name} {last_name}" - if location_city: - display_name += f" (Ort: {location_city})" - - linking_entries.append( - LinkingConsultationsEntryPerson( - pers_id=entry["pers_id"], - display_name=display_name, + stmt = ( + sa.select( + db.t_grunderfassung_personen.c.pers_id, + db.t_zuordnung_personen_unternehmen.c.un_id, + db.t_grunderfassung_personen.c.Stammdaten__name, + db.t_grunderfassung_personen.c.Stammdaten__vorname, + db.t_grunderfassung_personen.c.Stammdaten__ort, ) + .outerjoin( + db.t_zuordnung_personen_unternehmen, + db.t_grunderfassung_personen.c.pers_id + == db.t_zuordnung_personen_unternehmen.c.pers_id, + ) + .order_by(db.t_grunderfassung_personen.c.Stammdaten__name) ) - return linking_entries + ret = conn.execute(stmt) + + persons_map: dict[str, LinkingConsultationsEntryPerson] = {} + for row in ret: + p_id = row.pers_id + + if p_id not in persons_map: + first_name = row.Stammdaten__vorname + last_name = row.Stammdaten__name + location_city = row.Stammdaten__ort + + display_name = f"{first_name} {last_name}" + if location_city: + display_name += f" (Ort: {location_city})" + + persons_map[p_id] = LinkingConsultationsEntryPerson( + pers_id=p_id, + un_ids=set(), + display_name=display_name, + ) + + if row.un_id is not None: + persons_map[p_id].un_ids.add(row.un_id) + + return list(persons_map.values()) # TODO wrap result @@ -982,8 +968,6 @@ def page_consulting_eastablish_link( un_id: RecId | None, pers_id: RecId | None, ) -> None: - raise RuntimeError("TODOs to implement") - stmt = ( sa.update(db.t_beratung_vorgang) .where(db.t_beratung_vorgang.c.vorgang_id == vorgang_id) diff --git a/src/wce_crm/data_models.py b/src/wce_crm/data_models.py index 922c425..6ce0bf0 100644 --- a/src/wce_crm/data_models.py +++ b/src/wce_crm/data_models.py @@ -124,36 +124,6 @@ class Module(WrapperModule, Protocol): def save_data(self) -> None: ... -# TODO check removal -# class AutoFormInsert(Protocol): -# def __call__( -# self, -# data: dict[str, Any], -# ) -> RecId: ... - - -# class AutoFormUpdate(Protocol): -# def __call__( -# self, -# id_: int, -# data: dict[str, Any], -# ) -> None: ... - - -# class AutoFormGet(Protocol): -# def __call__( -# self, -# id_: int, -# ) -> dict[str, Any]: ... - - -# class AutoFormDelete(Protocol): -# def __call__( -# self, -# id_: int, -# ) -> None: ... - - class InitRecFormToDb(Protocol): def __call__( self, @@ -222,24 +192,6 @@ class Page_NewEntry_State(PageState[Module]): session: Session -# TODO check removal -# INITREC_REC_TO_ENTITIES: Final[dict[RecordingType, frozenset[EntityType]]] = { -# RecordingType.WITH_COMPANY: frozenset( -# ( -# EntityType.METADATA, -# EntityType.COMPANY, -# EntityType.PERSON, -# ) -# ), -# RecordingType.WITHOUT_COMPANY: frozenset( -# ( -# EntityType.METADATA, -# EntityType.PERSON, -# ) -# ), -# } - - @dc.dataclass(slots=True, kw_only=True) class Page_InitRec_Form_Data: metadata: dict[str, Any] @@ -260,11 +212,8 @@ class Page_InitRec_State(PageState["InitRecForm"]): initrec_form_states: dict[EntityType, Page_InitRec_Form_State] = dc.field( init=False, default_factory=dict ) - # pydantic_models: dict[RecordingType, type[FlatBaseModel]] = dc.field(default_factory=dict) def __post_init__(self) -> None: - # TODO check removal - # relevant_entities = INITREC_REC_TO_ENTITIES[self.recording_type] relevant_entities = frozenset( ( EntityType.METADATA, @@ -306,24 +255,6 @@ class Page_InitRec_Form_State(PageState[Module]): geloescht: bool = False -# TODO remove -# @dc.dataclass(slots=True, kw_only=True) -# class Page_InitRecCompany_State(PageState[Module]): -# session: Session -# ids: EntityIds -# # un_id: RecId | None = None -# locked: bool - - -# @dc.dataclass(slots=True, kw_only=True) -# class Page_InitRecPerson_State(PageState[Module]): -# session: Session -# ids: EntityIds -# # pers_id: RecId | None = None -# # un_id: RecId | None = None -# locked: bool - - @dc.dataclass(slots=True, kw_only=True) class Page_Consulting_State(PageState[Module]): session: Session @@ -345,6 +276,7 @@ class Page_Consulting_Linking_State(PageState[Module]): un_id: RecId | None = None pers_id: RecId | None = None beratungs_typ: ConsultingType + filter_un_ids: set[RecId] | None = None @dc.dataclass(slots=True, kw_only=True) diff --git a/src/wce_crm/gui.py b/src/wce_crm/gui.py index 1fc8a6e..572571f 100644 --- a/src/wce_crm/gui.py +++ b/src/wce_crm/gui.py @@ -2907,9 +2907,9 @@ class Page_NewEntry(QWidget): back_requested = Signal() # Signal back button # TODO re-enable with correct settings - # company_requested = Signal(Page_InitRecCompany_State) # Signal "Unternehmen" - # person_requested = Signal(Page_InitRecPerson_State) # Signal "Individualperson" - consulting_requested = Signal(Page_Consulting_State) # Signal "Beratungsgespräch" + # company_requested = Signal(Page_InitRec_State) # Signal "Unternehmen" + # person_requested = Signal(Page_InitRec_State) # Signal "Individualperson" + new_consulting_requested = Signal(Page_Consulting_State) # Signal "Beratungsgespräch" new_initrec_requested = Signal(Page_InitRec_State) # TODO: rename def __init__( @@ -2919,75 +2919,101 @@ class Page_NewEntry(QWidget): super().__init__() self.STATE = state - layout = QVBoxLayout(self) - layout.setAlignment(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft) + outer_layout = QHBoxLayout(self) + layout_container = QWidget() + layout_container.setMinimumWidth(700) + layout_container.setMaximumWidth(1000) + layout = QVBoxLayout(layout_container) + + outer_layout.addStretch(1) + outer_layout.addWidget(layout_container, stretch=100) + # outer_layout.addWidget(scroll_area, stretch=100) + outer_layout.addStretch(1) + outer_layout.setAlignment(Qt.AlignmentFlag.AlignTop) - # Zurück-Button back_btn = QPushButton("← Zurück zur Übersicht") back_btn.setFixedWidth(200) back_btn.clicked.connect(lambda: self.back_requested.emit()) - # Platzhalter für die Details self.title_label = QLabel("Wählen Sie den Typ der Grunderfassung") self.title_label.setStyleSheet( "font-size: 24px; font-weight: bold; margin-top: 20px;" ) + title_initrec = QLabel("Grunderfassung anlegen") + title_initrec.setStyleSheet( + "font-size: 16px; font-weight: bold; margin-top: 20px; margin-bottom: 5px;" + ) btn_company = QPushButton("Unternehmen →") btn_company.setFixedWidth(300) btn_company.setFixedHeight(40) btn_company.clicked.connect(self._request_initrec_company) - btn_person = QPushButton("Individualperson →") btn_person.setFixedWidth(300) btn_person.setFixedHeight(40) btn_person.clicked.connect(self._request_initrec_person) + title_consulting = QLabel("Beratung durchführen") + title_consulting.setStyleSheet( + "font-size: 16px; font-weight: bold; margin-top: 20px; margin-bottom: 5px;" + ) btn_consulting = QPushButton("Beratungsgespräch →") btn_consulting.setFixedWidth(300) btn_consulting.setFixedHeight(40) btn_consulting.clicked.connect(self._request_consulting) - btn_newinitrec = QPushButton("NEUE GRUNDERFASSUNG →") - btn_newinitrec.setFixedWidth(300) - btn_newinitrec.setFixedHeight(40) - btn_newinitrec.clicked.connect(self._request_new_initrec) + # TODO check removal + # btn_newinitrec = QPushButton("NEUE GRUNDERFASSUNG →") + # btn_newinitrec.setFixedWidth(300) + # btn_newinitrec.setFixedHeight(40) + # btn_newinitrec.clicked.connect(self._debug_request_new_initrec) layout.addWidget(back_btn) layout.addSpacing(15) layout.addWidget(self.title_label) - layout.addSpacing(20) - layout.addWidget(btn_newinitrec) - layout.addSpacing(20) + # layout.addSpacing(20) + # layout.addWidget(btn_newinitrec) + # layout.addSpacing(20) + layout.addWidget(title_initrec) layout.addWidget(btn_company) layout.addWidget(btn_person) layout.addSpacing(10) + layout.addWidget(title_consulting) layout.addWidget(btn_consulting) # TODO configure correctly def _request_initrec_company(self) -> None: logger_gui.debug("[Page -- NewEntry] Call for company...") - # ids = EntityIds(type=RecordingType.COMPANY) - # req_state = Page_InitRecCompany_State( - # session=self.STATE.session, - # ids=ids, - # # un_id=None, # TODO remove - # locked=False, - # ) - # logger_gui.debug("[Page -- InitRec] State to call: %s", req_state) - # self.company_requested.emit(req_state) + logger_gui.debug("[Page -- NewEntry] Call for person...") + req_state = Page_InitRec_State( + session=self.STATE.session, + recording_type=RecordingType.WITH_COMPANY, + form_data=Page_InitRec_Form_Data(metadata={}, person={}, company={}), + locked=False, + ) + logger_gui.debug("[Page -- NewEntry] State to call (IDs): %s", req_state.ids) + self.new_initrec_requested.emit(req_state) def _request_initrec_person(self) -> None: logger_gui.debug("[Page -- NewEntry] Call for person...") - # ids = EntityIds(type=RecordingType.PERSON) - # req_state = Page_InitRecPerson_State( - # session=self.STATE.session, - # ids=ids, - # # pers_id=None, # TODO remove - # locked=False, - # ) - # logger_gui.debug("[Page -- InitRec] State to call: %s", req_state) - # self.person_requested.emit(req_state) + req_state = Page_InitRec_State( + session=self.STATE.session, + recording_type=RecordingType.WITHOUT_COMPANY, + form_data=Page_InitRec_Form_Data(metadata={}, person={}, company={}), + locked=False, + ) + logger_gui.debug("[Page -- NewEntry] State to call (IDs): %s", req_state.ids) + self.new_initrec_requested.emit(req_state) + + def _debug_request_new_initrec(self) -> None: + req_state = Page_InitRec_State( + session=self.STATE.session, + recording_type=RecordingType.WITH_COMPANY, + form_data=Page_InitRec_Form_Data(metadata={}, person={}, company={}), + locked=False, + ) + logger_gui.debug("[Page -- NewEntry] State to call (IDs): %s", req_state.ids) + self.new_initrec_requested.emit(req_state) def _request_consulting(self) -> None: req_state = Page_Consulting_State( @@ -2997,17 +3023,7 @@ class Page_NewEntry(QWidget): locked=False, ) logger_gui.debug("[Page -- InitRec] State to call: %s", req_state) - self.consulting_requested.emit(req_state) - - def _request_new_initrec(self) -> None: - req_state = Page_InitRec_State( - session=self.STATE.session, - recording_type=RecordingType.WITH_COMPANY, - form_data=Page_InitRec_Form_Data(metadata={}, person={}, company={}), - locked=False, - ) - logger_gui.debug("[Page -- NewInitRec] State to call (IDs): %s", req_state.ids) - self.new_initrec_requested.emit(req_state) + self.new_consulting_requested.emit(req_state) def _sync_state_to_GUI(self) -> None: pass @@ -5177,46 +5193,14 @@ class Page_Consulting_LinkingDialog(QDialog): parent: QWidget | None = None, ): super().__init__(parent) + # State and data self.STATE = state + self.all_companies = backend.page_consulting_linking_companies(None) + self.all_persons = backend.page_consulting_linking_persons() self.setWindowTitle("Beziehung verknüpfen") self.resize(700, 450) - # firmen_aus_db = [{"id": 1, "name": "Siemens AG"}, {"id": 2, "name": "Bosch GmbH"}] - # personen_aus_db = [ - # { - # "id": 50, - # "name": "Max Mustermann", - # "unternehmen_id": 1, - # "firma_name": "Siemens AG", - # }, - # { - # "id": 51, - # "name": "Erika Muster", - # "unternehmen_id": 1, - # "firma_name": "Siemens AG", - # }, - # { - # "id": 52, - # "name": "Peter Müller", - # "unternehmen_id": 2, - # "firma_name": "Bosch GmbH", - # }, - # { - # "id": 53, - # "name": "Freiberufler Frank", - # "unternehmen_id": None, - # "firma_name": None, - # }, - # ] - - # self.all_unternehmen = firmen_aus_db - # self.all_personen = personen_aus_db - - # # Rückgabewerte - # self.gewaehltes_unternehmen = None - # self.gewaehlte_person = None - # --- UI LAYOUT --- main_layout = QVBoxLayout(self) column_layout = QHBoxLayout() @@ -5241,7 +5225,7 @@ class Page_Consulting_LinkingDialog(QDialog): column_layout.addLayout(self.vbox_persons) main_layout.addLayout(column_layout) - # Unten: OK / Abbrechen Buttons + # OK / Cancel buttons self.button_box = QDialogButtonBox( QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel ) @@ -5251,10 +5235,7 @@ class Page_Consulting_LinkingDialog(QDialog): self.button_box.button(QDialogButtonBox.StandardButton.Cancel).setText("Abbrechen") main_layout.addWidget(self.button_box) - # initial loading - # self._load_companies() - # self._load_persons() - + # TODO check removal # list_style = """ # QListWidget::item:selected { # background-color: #0078D4; /* Blau, wenn die Liste aktiv fokussiert ist */ @@ -5305,20 +5286,28 @@ class Page_Consulting_LinkingDialog(QDialog): def _sync_state_to_GUI( self, reset: bool, - section: Literal["person_selection", "validation"] | None = None, + section: Literal["person_selection", "company_selection", "validation"] | None = None, ) -> None: if reset: self._reset() - self._load_companies() + self._load_companies_to_list(None) + self._load_persons_to_list(None) if section is None or section == "person_selection": if self.STATE.beratungs_typ is ConsultingType.PAUSCHAL: self._enable_person_selection(False) elif self.STATE.beratungs_typ is ConsultingType.INDIVIDUAL: self._enable_person_selection(True) - self.list_persons.blockSignals(True) - self._load_persons(self.STATE.un_id) - self.list_persons.blockSignals(False) + if not self.list_persons.selectedItems(): + self.list_persons.blockSignals(True) + self._load_persons_to_list(self.STATE.un_id) + self.list_persons.blockSignals(False) + + if section is None or section == "company_selection": + if not self.list_companies.selectedItems(): + self.list_companies.blockSignals(True) + self._load_companies_to_list(self.STATE.filter_un_ids) + self.list_companies.blockSignals(False) if section is None or section == "validation": self._validate() @@ -5341,31 +5330,49 @@ class Page_Consulting_LinkingDialog(QDialog): else: self.ok_btn.setEnabled(False) - def _load_companies(self) -> None: + def _reload_all_companies(self) -> None: + self.all_companies = backend.page_consulting_linking_companies(None) + + def _reload_all_persons(self) -> None: + self.all_persons = backend.page_consulting_linking_persons() + + def _load_companies_to_list( + self, + un_ids: set[RecId] | None, + ) -> None: + # search_text = self.suche_firma.text().strip().lower() self.list_companies.clear() - companies = backend.page_consulting_linking_companies(un_id=self.STATE.un_id) + # TODO check reload of companies + # companies = backend.page_consulting_linking_companies(None) logger_page_consulting.debug( - "[Page Consulting -- Linking] Retrieved company entries:\n%s", companies + "[Page Consulting -- Linking] Retrieved company entries:\n%s", self.all_companies ) - for company in companies: + for company in self.all_companies: + if un_ids and company.un_id not in un_ids: + continue + item = QListWidgetItem(company.display_name) item.setData(Qt.ItemDataRole.UserRole, company) self.list_companies.addItem(item) - def _load_persons( + def _load_persons_to_list( self, un_id: RecId | None, ) -> None: """Lädt Personen. Wenn firma_id übergeben wird, wird gefiltert.""" self.list_persons.clear() - persons = backend.page_consulting_linking_persons(un_id) + # TODO check reload of persons + # persons = backend.page_consulting_linking_persons() logger_page_consulting.debug( - "[Page Consulting -- Linking] Retrieved person entries:\n%s", persons + "[Page Consulting -- Linking] Retrieved person entries:\n%s", self.all_persons ) - for person in persons: + for person in self.all_persons: + if un_id and un_id not in person.un_ids: + continue + item = QListWidgetItem(person.display_name) item.setData(Qt.ItemDataRole.UserRole, person) self.list_persons.addItem(item) @@ -5386,10 +5393,6 @@ class Page_Consulting_LinkingDialog(QDialog): pformat(self.STATE), ) self._sync_state_to_GUI(reset=False) - # if self.STATE.beratungs_typ is ConsultingType.INDIVIDUAL: - # self.list_persons.blockSignals(True) - # self._load_persons(un_id=self.STATE.un_id) - # self.list_persons.blockSignals(False) def _person_selected(self) -> None: items = self.list_persons.selectedItems() @@ -5400,29 +5403,29 @@ class Page_Consulting_LinkingDialog(QDialog): "LinkingConsultationsEntryPerson", items[0].data(Qt.ItemDataRole.UserRole) ) self.STATE.pers_id = person.pers_id - self._sync_state_to_GUI(reset=False, section="validation") - # TODO this backlink is a really good idea and should be implemented - # if person["unternehmen_id"]: - # self.list_companies.blockSignals(True) - # - # for i in range(self.list_companies.count()): - # item = self.list_companies.item(i) - # firma_data = item.data(Qt.ItemDataRole.UserRole) - # if firma_data["id"] == person["unternehmen_id"]: - # item.setSelected(True) - # self.gewaehltes_unternehmen = firma_data - # break - # self.list_companies.blockSignals(False) + if person.un_ids: + self.STATE.filter_un_ids = person.un_ids + else: + self.STATE.filter_un_ids = None + + self._sync_state_to_GUI(reset=False) def _clear_company_selection(self) -> None: + logger_page_consulting.debug( + "[Page Consulting -- Linking] Clear company selection...", + ) self.list_companies.clearSelection() self.STATE.un_id = None self._sync_state_to_GUI(reset=False) def _clear_person_selection(self) -> None: + logger_page_consulting.debug( + "[Page Consulting -- Linking] Clear person selection...", + ) self.list_persons.clearSelection() self.STATE.pers_id = None + self.STATE.filter_un_ids = None self._sync_state_to_GUI(reset=False) def get_state(self) -> Page_Consulting_Linking_State: @@ -6220,8 +6223,8 @@ class MainWindow(QMainWindow): self.new_entry_select.back_requested.connect(self.show_main_page) # self.new_initrec_select.company_requested.connect(self.show_page_initrec_company) # self.new_initrec_select.person_requested.connect(self.show_page_initrec_person) - self.new_entry_select.consulting_requested.connect(self.show_page_consulting) - self.new_entry_select.new_initrec_requested.connect(self.show_page_newinitrec) + self.new_entry_select.new_consulting_requested.connect(self.show_page_consulting) + self.new_entry_select.new_initrec_requested.connect(self.show_page_initrec) self.stack.addWidget(self.new_entry_select) # SITE: 'Grunderfassung' @@ -6232,35 +6235,13 @@ class MainWindow(QMainWindow): form_data=Page_InitRec_Form_Data(metadata={}, person={}, company={}), locked=False, ) - self.newinitrec = Page_InitRec(newinitrec_state) - self.newinitrec.back_main_requested.connect(self.show_main_page) - self.newinitrec.back_requested.connect(self.simple_go_back) - self.newinitrec.company_profile_requested.connect(self.show_page_company_profile) + self.initrec = Page_InitRec(newinitrec_state) + self.initrec.back_main_requested.connect(self.show_main_page) + self.initrec.back_requested.connect(self.simple_go_back) + self.initrec.company_profile_requested.connect(self.show_page_company_profile) # self.newinitrec.update_triggered.connect(self.update_grid) - self.stack.addWidget(self.newinitrec) + self.stack.addWidget(self.initrec) - # SITE: 'Grunderfassung Unternehmen' - # ids = EntityIds(type=RecordingType.COMPANY) - # initrec_company_state = Page_InitRecCompany_State( - # session=self.STATE.session, ids=ids, locked=False - # ) - # self.initrec_company = Page_InitRecCompany(initrec_company_state) - # self.initrec_company.back_main_requested.connect(self.show_main_page) - # self.initrec_company.back_requested.connect(self.simple_go_back) - # self.initrec_company.update_main_page.connect(self.update_grid) - # self.initrec_company.initrec_person_requested.connect(self.show_page_initrec_person) - # self.initrec_company.company_profile_requested.connect(self.show_page_company_profile) - # self.stack.addWidget(self.initrec_company) - # SITE: 'Grunderfassung Person' - # ids = EntityIds(type=RecordingType.PERSON) - # initrec_person_state = Page_InitRecPerson_State( - # session=self.STATE.session, ids=ids, locked=False - # ) - # self.initrec_person = Page_InitRecPerson(initrec_person_state) - # self.initrec_person.back_main_requested.connect(self.show_main_page) - # self.initrec_person.back_requested.connect(self.simple_go_back) - # self.initrec_person.update_triggered.connect(self.update_grid) - # self.stack.addWidget(self.initrec_person) # SITE: 'Unternehmensprofil' comp_profile_state = Page_CompanyProfile_State(session=self.STATE.session) self.company_profile = Page_CompanyProfile(comp_profile_state) @@ -6270,6 +6251,7 @@ class MainWindow(QMainWindow): self.company_profile.back_requested.connect(self.simple_go_back) self.company_profile.consulting_requested.connect(self.show_page_consulting) self.stack.addWidget(self.company_profile) + # SITE: 'Beratungsgespräch' self.consulting_page = Page_Consulting(self.STATE.session) self.consulting_page.back_main_requested.connect(self.show_main_page) @@ -6452,12 +6434,12 @@ class MainWindow(QMainWindow): def show_new_entry_select(self): self._navigate_to_target(self.new_entry_select) - def show_page_newinitrec( + def show_page_initrec( self, new_state: Page_InitRec_State, ): - self.newinitrec.load_state(new_state) - self._navigate_to_target(self.newinitrec) + self.initrec.load_state(new_state) + self._navigate_to_target(self.initrec) # def show_page_initrec_company( # self, diff --git a/src/wce_crm/logging.py b/src/wce_crm/logging.py index d011a09..7c12017 100644 --- a/src/wce_crm/logging.py +++ b/src/wce_crm/logging.py @@ -41,7 +41,7 @@ logger_custom_fields = logger_gui.getChild("custom_fields") logger_custom_fields.setLevel(logging.WARNING) logger_page_consulting = logger_gui.getChild("page_consulting") -logger_page_consulting.setLevel(logging.WARNING) +logger_page_consulting.setLevel(logging.DEBUG) logger_page_initrec = logger_gui.getChild("page_initrec") logger_page_initrec.setLevel(logging.DEBUG) diff --git a/src/wce_crm/types.py b/src/wce_crm/types.py index 45d2a17..8fe1d55 100644 --- a/src/wce_crm/types.py +++ b/src/wce_crm/types.py @@ -273,4 +273,5 @@ class LinkingConsultationsEntryCompany: @dc.dataclass(slots=True) class LinkingConsultationsEntryPerson: pers_id: RecId + un_ids: set[RecId] display_name: str -- 2.34.1 From b635e7eb750f3f1a2db59eff46ed5bb77560ebe8 Mon Sep 17 00:00:00 2001 From: foefl Date: Tue, 28 Jul 2026 19:02:24 +0200 Subject: [PATCH 13/13] updated main page interaction --- prototypes/tests.py | 117 ++++++++- src/wce_crm/backend/backend.py | 453 +++++---------------------------- src/wce_crm/data_models.py | 12 +- src/wce_crm/gui.py | 98 +++---- src/wce_crm/types.py | 6 +- 5 files changed, 232 insertions(+), 454 deletions(-) diff --git a/prototypes/tests.py b/prototypes/tests.py index 5894c3e..46d3b72 100644 --- a/prototypes/tests.py +++ b/prototypes/tests.py @@ -19,13 +19,122 @@ from PySide6.QtCore import QDate, Qt from wce_crm import constants, db from wce_crm.backend import backend -from wce_crm.types import EntityIds, RecordingType +from wce_crm.types import EntityIds, MainPageEntry, RecordingType # %% # backend.page_consulting_linking_companies(None) # backend.page_consulting_linking_persons(3) +ret = backend.main_page_get_entries() +pprint(ret) +# %% +with db.ENGINE.connect() as conn: + q_relevant_entries = ( + sa.select( + db.t_grunderfassung_personen.c.pers_id, + db.t_grunderfassung_personen.c.aktualisiert, + db.t_zuordnung_personen_unternehmen.c.id.label("link_id"), + db.t_zuordnung_personen_unternehmen.c.un_id, + db.t_zuordnung_personen_unternehmen.c.an_id, + db.t_grunderfassung_personen.c.Stammdaten__name, + db.t_grunderfassung_personen.c.Stammdaten__vorname, + db.t_grunderfassung_personen.c.Stammdaten__ort, + ) + .outerjoin( + db.t_zuordnung_personen_unternehmen, + db.t_grunderfassung_personen.c.pers_id + == db.t_zuordnung_personen_unternehmen.c.pers_id, + ) + .order_by(db.t_grunderfassung_personen.c.Stammdaten__name) + ).subquery("relevant_entries") + + stmt = sa.select( + q_relevant_entries, + db.t_ext_crm_master_attach.c.ma_unternehmensname, + ).outerjoin( + db.t_ext_crm_master_attach, + q_relevant_entries.c.un_id == db.t_ext_crm_master_attach.c.ma_id, + ) + + ret = conn.execute(stmt) + + # results = list(ret.mappings()) + # print(f"Results are:\n{pformat(results)}") + + main_page_entries: list[MainPageEntry] = [] + for entry in ret.mappings(): + ids = EntityIds( + pers_id=entry["pers_id"], + link_id=entry["link_id"], + un_id=entry["un_id"], + an_id=entry["an_id"], + ) + recording_type = RecordingType.WITHOUT_COMPANY + if ids.un_id or ids.link_id: + recording_type = RecordingType.WITH_COMPANY + + first_name = entry["Stammdaten__vorname"] + last_name = entry["Stammdaten__name"] + location_city = entry["Stammdaten__ort"] + person_name = f"{first_name} {last_name}" + if location_city: + person_name += f" (Ort: {location_city})" + + company_name = entry["ma_unternehmensname"] + + display_name = person_name + if recording_type is RecordingType.WITH_COMPANY: + display_name = company_name + + main_page_entries.append( + MainPageEntry( + ids=ids, + display_name=display_name, + dt_updated=entry["aktualisiert"], + recording_type=recording_type, + ) + ) + main_page_entries.sort(key=lambda x: x.dt_updated, reverse=True) + pprint(main_page_entries) +# %% +# %% +entry = results[0] +ids = EntityIds( + pers_id=entry["pers_id"], + link_id=entry["link_id"], + un_id=entry["un_id"], + an_id=entry["an_id"], +) +recording_type = RecordingType.WITHOUT_COMPANY +if ids.un_id or ids.link_id: + recording_type = RecordingType.WITH_COMPANY + +first_name = entry["Stammdaten__vorname"] +last_name = entry["Stammdaten__name"] +location_city = entry["Stammdaten__ort"] +person_name = f"{first_name} {last_name}" +if location_city: + person_name += f" (Ort: {location_city})" + +company_name = entry["ma_unternehmensname"] + +display_name = person_name +if recording_type is RecordingType.WITH_COMPANY: + display_name = company_name + +main_page_entry = MainPageEntry( + ids=ids, + display_name=display_name, + dt_updated=entry["aktualisiert"], + recording_type=recording_type, +) + +# %% +pprint(main_page_entry) + + +# %% # %% @dc.dataclass(slots=True, kw_only=True) class LinkingPerson: @@ -74,13 +183,13 @@ with db.ENGINE.connect() as conn: last_name = row.Stammdaten__name location_city = row.Stammdaten__ort - display_name = f"{first_name} {last_name}" + person_name = f"{first_name} {last_name}" if location_city: - display_name += f" (Ort: {location_city})" + person_name += f" (Ort: {location_city})" persons_map[p_id] = { "pers_id": p_id, - "display_name": display_name, + "display_name": person_name, "un_ids": set(), } diff --git a/src/wce_crm/backend/backend.py b/src/wce_crm/backend/backend.py index e412388..97a0042 100644 --- a/src/wce_crm/backend/backend.py +++ b/src/wce_crm/backend/backend.py @@ -134,232 +134,6 @@ def initrec_comp_contact_person_search_get_info( # // internals -# TODO check removal -# def initrec_company_insert_initial_recording( -# data: dict[str, Any], -# ) -> RecId: -# logger.debug("[Call backend] insert_initial_recording") -# stmt = db.grunderfassung_unternehmen.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_company_update_initial_recording( -# id_: RecId, -# data: dict[str, Any], -# ) -> None: -# logger.debug("[Call backend] update_initial_recording") -# 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_company_to_db( -# auto_form_data: InitRec, -# ) -> InitRec: -# logger.debug("[AutoForm -- backend] Call database saving routine...") - -# dump_data = copy.deepcopy(auto_form_data.db_data) -# dump_data["geloescht"] = auto_form_data.geloescht - -# with db.ENGINE.begin() as conn: -# if auto_form_data.ids.un_id is None: -# logger.debug("[AutoForm -- backend] Insert...") - -# stmt = db.grunderfassung_unternehmen.insert().returning( -# db.grunderfassung_unternehmen.c.un_id, -# db.grunderfassung_unternehmen.c.Metadaten_aktualisierung, -# db.grunderfassung_unternehmen.c.geloescht, -# ) -# ret = conn.execute(stmt, dump_data) - -# from_db = ret.mappings().fetchall() -# assert len(from_db) == 1, "expected excactly one returned row" -# data_from_db = from_db[0] -# auto_form_data.ids.un_id = data_from_db["un_id"] -# auto_form_data.geloescht = data_from_db["geloescht"] -# auto_form_data.db_data["Metadaten_aktualisierung"] = data_from_db[ -# "Metadaten_aktualisierung" -# ] - -# logger.debug("[AutoForm -- backend] Inserted InitRec Company successfully") - -# else: -# logger.debug("[AutoForm -- backend] Update...") -# stmt = ( -# db.grunderfassung_unternehmen.update() -# .where(db.grunderfassung_unternehmen.c.un_id == auto_form_data.ids.un_id) -# .returning( -# db.grunderfassung_unternehmen.c.Metadaten_aktualisierung, -# db.grunderfassung_unternehmen.c.geloescht, -# ) -# ) -# ret = conn.execute(stmt, dump_data) - -# from_db = ret.mappings().fetchall() -# assert len(from_db) == 1, "expected excatly one returned row" -# data_from_db = from_db[0] - -# auto_form_data.db_data["Metadaten_aktualisierung"] = data_from_db[ -# "Metadaten_aktualisierung" -# ] - -# logger.debug( -# "[AutoForm -- backend] Updated InitRec Company with ID %d successfully", -# auto_form_data.ids.un_id, -# ) - -# return auto_form_data - - -# def initrec_company_from_db( -# id_: RecId, -# ) -> InitRec: -# logger.debug("[AutoForm -- backend] Call database loading routine...") -# stmt = db.grunderfassung_unternehmen.select().where( -# db.grunderfassung_unternehmen.c.un_id == id_ -# ) -# with db.ENGINE.connect() as conn: -# ret = conn.execute(stmt) - -# from_db = ret.mappings().all() -# assert len(from_db) == 1, "not excactly one company initial recording obtained" -# data_from_db = dict(from_db[0]) - -# geloescht = data_from_db["geloescht"] -# del data_from_db["geloescht"] - -# return InitRec( -# ids=EntityIds(un_id=id_), -# # rec_id=id_, # TODO remove -# geloescht=geloescht, -# db_data=data_from_db, -# ) - - -# def initrec_person_to_db( -# auto_form_data: InitRec, -# ) -> InitRec: -# logger.debug("[AutoForm -- backend] Call database saving routine...") - -# dump_data = copy.deepcopy(auto_form_data.db_data) -# dump_data["geloescht"] = auto_form_data.geloescht - -# with db.ENGINE.begin() as conn: -# if auto_form_data.ids.pers_id is None: -# logger.debug("[AutoForm -- backend] Insert...") - -# stmt = sa.insert(db.t_grunderfassung_personen).returning( -# db.t_grunderfassung_personen.c.pers_id, -# db.t_grunderfassung_personen.c.Metadaten_aktualisierung, -# db.t_grunderfassung_personen.c.geloescht, -# ) -# stmt_compiled = str(stmt.compile(db.ENGINE)) -# logger.debug( -# "[AutoForm -- backend] Data to insert:\nStatement: %s\n%s", -# stmt_compiled, -# pformat(dump_data), -# ) - -# ret = conn.execute(stmt, dump_data) - -# from_db = ret.mappings().fetchall() -# assert len(from_db) == 1, "expected excactly one returned row" -# data_from_db = from_db[0] -# auto_form_data.ids.pers_id = data_from_db["pers_id"] -# auto_form_data.geloescht = data_from_db["geloescht"] -# auto_form_data.db_data["Metadaten_aktualisierung"] = data_from_db[ -# "Metadaten_aktualisierung" -# ] - -# logger.debug("[AutoForm -- backend] Inserted InitRec Person successfully") - -# else: -# logger.debug("[AutoForm -- backend] Update...") -# stmt = ( -# db.t_grunderfassung_personen.update() -# .where(db.t_grunderfassung_personen.c.pers_id == auto_form_data.ids.pers_id) -# .returning( -# db.t_grunderfassung_personen.c.Metadaten_aktualisierung, -# db.t_grunderfassung_personen.c.geloescht, -# ) -# ) -# ret = conn.execute(stmt, dump_data) - -# from_db = ret.mappings().fetchall() -# assert len(from_db) == 1, "expected excatly one returned row" -# data_from_db = from_db[0] - -# auto_form_data.db_data["Metadaten_aktualisierung"] = data_from_db[ -# "Metadaten_aktualisierung" -# ] - -# logger.debug( -# "[AutoForm -- backend] Updated InitRec Person with ID %d successfully", -# auto_form_data.ids.pers_id, -# ) - -# return auto_form_data - - -# def initrec_person_from_db( -# id_: RecId, -# ) -> InitRec: -# logger.debug("[AutoForm -- backend] Call database loading routine...") -# stmt = db.t_grunderfassung_personen.select().where( -# db.t_grunderfassung_personen.c.pers_id == id_ -# ) -# with db.ENGINE.connect() as conn: -# ret = conn.execute(stmt) - -# from_db = ret.mappings().all() -# assert len(from_db) == 1, "not excactly one company initial recording obtained" -# data_from_db = dict(from_db[0]) - -# geloescht = data_from_db["geloescht"] -# del data_from_db["geloescht"] - -# return InitRec( -# ids=EntityIds(pers_id=id_), -# # rec_id=id_, # TODO remove -# geloescht=geloescht, -# db_data=data_from_db, -# ) - - -# TODO check removal -# def initrec_company_get_initial_recording( -# id_: RecId, -# ) -> dict[str, Any]: -# logger.debug("[Call backend] get_initial_recording") -# stmt = db.grunderfassung_unternehmen.select().where( -# db.grunderfassung_unternehmen.c.un_id == id_ -# ) -# with db.ENGINE.connect() as conn: -# ret = conn.execute(stmt) - -# results = ret.mappings().all() -# if not results: -# raise KeyError(f"Database ID {id_} not found") - -# assert len(results) == 1, "more than one company initial recording obtained" - -# row = results[0] -# assert row, "row was not obtained" - -# return dict(row) - - def split_dict( dic: dict[K, V], split_keys: Container[K], @@ -612,85 +386,6 @@ def page_initrec_from_db( ) -# def initrec_company_delete_initial_recording( -# id_: RecId, -# ) -> None: -# logger.debug("[Call backend] delete_initial_recording") -# 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], -# ) -> RecId: -# 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_: RecId, -# 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_: RecId, -# ) -> dict[str, Any]: -# logger.debug("[Call backend] get_initial_recording person") -# stmt = db.grunderfassung_personen.select().where( -# db.grunderfassung_personen.c.pers_id == id_ -# ) -# with db.ENGINE.connect() as conn: -# ret = conn.execute(stmt) - -# results = ret.mappings().all() -# if not results: -# raise KeyError(f"Database ID {id_} not found") - -# assert len(results) == 1, "more than one person initial recording obtained" - -# row = results[0] -# assert row, "row was not obtained" - -# return dict(row) - - -# def initrec_person_delete_initial_recording( -# id_: RecId, -# ) -> 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) - -# if ret.rowcount == 0: -# raise KeyError(f"Database ID {id_} not found for deletion") - - @wrap_result(10) def page_consulting_to_db( consultation_data: Beratungsgespraech_Vorgang, @@ -1031,7 +726,7 @@ def companyprofile_page_get_consultations( # // initial recording interaction -# TODO wrap result +# TODO wrap result, add to corresponding component def initrec_link_person_company( un_id: RecId, pers_id: RecId, @@ -1063,89 +758,71 @@ def initrec_link_person_company( # // main page interaction -# TODO change to correct logic -def _main_page_get_company_list() -> list[MainPageEntry]: - logger.debug("[Call backend] get_company_list") - - raise RuntimeError("TODOs to implement") - - stmt = sa.select( - db.grunderfassung_unternehmen.c.un_id, - db.grunderfassung_unternehmen.c.Partnersuche__un_suche, - db.grunderfassung_unternehmen.c.Metadaten_aktualisierung, - ).where(db.grunderfassung_unternehmen.c.geloescht.is_(None)) - - with db.ENGINE.connect() as conn: - res = conn.execute(stmt) - - main_page_companies: list[MainPageEntry] = [] - - for entry in res.mappings(): - rec_id = entry["un_id"] - assert rec_id, "no RecID defined" - ma_id_external = entry["Partnersuche__un_suche"] - assert ma_id_external is not None, "external MA ID is NULL" - datetime_akt = cast(datetime.datetime, entry["Metadaten_aktualisierung"]) - datetime_akt = datetime_akt.astimezone(TIMEZONE_CEST) - - comp_info = external_company_get_info(ma_id_external) - display_name = comp_info["ma_unternehmensname"] - - main_page_companies.append( - MainPageEntry( - rec_id=rec_id, - display_name=display_name, - Metadaten_aktualisierung=datetime_akt, - type=RecordingType.WITH_COMPANY, - ) - ) - - return main_page_companies - - -def _main_page_get_person_list() -> list[MainPageEntry]: - logger.debug("[Call backend] get_company_list") - stmt = sa.select( - db.t_grunderfassung_personen.c.pers_id, - db.t_grunderfassung_personen.c.Stammdaten__vorname, - db.t_grunderfassung_personen.c.Stammdaten__name, - db.t_grunderfassung_personen.c.Metadaten_aktualisierung, - ).where(db.t_grunderfassung_personen.c.geloescht.is_(None)) - - with db.ENGINE.connect() as conn: - res = conn.execute(stmt) - - main_page_persons: list[MainPageEntry] = [] - - for entry in res.mappings(): - rec_id = entry["pers_id"] - assert rec_id, "no RecID defined" - datetime_akt = cast(datetime.datetime, entry["Metadaten_aktualisierung"]) - datetime_akt = datetime_akt.astimezone() - - surname: str = entry["Stammdaten__name"] - first_name: str = entry["Stammdaten__vorname"] - names_to_join = [n for n in (first_name, surname) if n] - display_name = " ".join(names_to_join) - - main_page_persons.append( - MainPageEntry( - rec_id=rec_id, - display_name=display_name, - Metadaten_aktualisierung=datetime_akt, - type=RecordingType.WITHOUT_COMPANY, - ) - ) - - return main_page_persons - - def main_page_get_entries() -> list[MainPageEntry]: - companies = _main_page_get_company_list() - persons = _main_page_get_person_list() + with db.ENGINE.connect() as conn: + q_relevant_entries = ( + sa.select( + db.t_grunderfassung_personen.c.pers_id, + db.t_grunderfassung_personen.c.aktualisiert, + db.t_zuordnung_personen_unternehmen.c.id.label("link_id"), + db.t_zuordnung_personen_unternehmen.c.un_id, + db.t_zuordnung_personen_unternehmen.c.an_id, + db.t_grunderfassung_personen.c.Stammdaten__name, + db.t_grunderfassung_personen.c.Stammdaten__vorname, + db.t_grunderfassung_personen.c.Stammdaten__ort, + ) + .outerjoin( + db.t_zuordnung_personen_unternehmen, + db.t_grunderfassung_personen.c.pers_id + == db.t_zuordnung_personen_unternehmen.c.pers_id, + ) + .where(db.t_grunderfassung_personen.c.geloescht.is_(None)) + .order_by(db.t_grunderfassung_personen.c.Stammdaten__name) + ).subquery("relevant_entries") - all_entries = companies + persons + stmt = sa.select( + q_relevant_entries, + db.t_ext_crm_master_attach.c.ma_unternehmensname, + ).outerjoin( + db.t_ext_crm_master_attach, + q_relevant_entries.c.un_id == db.t_ext_crm_master_attach.c.ma_id, + ) - all_entries.sort(key=lambda x: x.Metadaten_aktualisierung, reverse=True) + ret = conn.execute(stmt) - return all_entries + main_page_entries: list[MainPageEntry] = [] + for entry in ret.mappings(): + ids = EntityIds( + pers_id=entry["pers_id"], + link_id=entry["link_id"], + un_id=entry["un_id"], + an_id=entry["an_id"], + ) + recording_type = RecordingType.WITHOUT_COMPANY + if ids.un_id or ids.link_id: + recording_type = RecordingType.WITH_COMPANY + + first_name = entry["Stammdaten__vorname"] + last_name = entry["Stammdaten__name"] + location_city = entry["Stammdaten__ort"] + person_name = f"{first_name} {last_name}" + if location_city: + person_name += f" (Ort: {location_city})" + + company_name = entry["ma_unternehmensname"] + + display_name = person_name + if recording_type is RecordingType.WITH_COMPANY: + display_name = company_name + + main_page_entries.append( + MainPageEntry( + ids=ids, + display_name=display_name, + dt_updated=entry["aktualisiert"], + recording_type=recording_type, + ) + ) + main_page_entries.sort(key=lambda x: x.dt_updated, reverse=True) + + return main_page_entries diff --git a/src/wce_crm/data_models.py b/src/wce_crm/data_models.py index 6ce0bf0..4a3f578 100644 --- a/src/wce_crm/data_models.py +++ b/src/wce_crm/data_models.py @@ -199,6 +199,14 @@ class Page_InitRec_Form_Data: company: dict[str, Any] +def _create_empty_initrec_form_data() -> Page_InitRec_Form_Data: + return Page_InitRec_Form_Data( + metadata={}, + person={}, + company={}, + ) + + # !! there must be exactly one InitRecForm for each recording type @dc.dataclass(slots=True, kw_only=True) class Page_InitRec_State(PageState["InitRecForm"]): @@ -208,7 +216,9 @@ class Page_InitRec_State(PageState["InitRecForm"]): # un_id: RecId | None = None locked: bool geloescht: bool = False - form_data: Page_InitRec_Form_Data + form_data: Page_InitRec_Form_Data = dc.field( + default_factory=_create_empty_initrec_form_data + ) initrec_form_states: dict[EntityType, Page_InitRec_Form_State] = dc.field( init=False, default_factory=dict ) diff --git a/src/wce_crm/gui.py b/src/wce_crm/gui.py index 572571f..2cd2a18 100644 --- a/src/wce_crm/gui.py +++ b/src/wce_crm/gui.py @@ -3510,15 +3510,14 @@ class Page_InitRec(QWidget): logger_initrecform.debug("[Page -- InitRec] Entity IDs: %s", ids) self._reset() - ids = EntityIds( - # un_id=3, - # an_id=7, - pers_id=5, - ) - # ids = EntityIds( - # link_id=7, + # # un_id=3, + # # an_id=7, + # pers_id=5, # ) + # # ids = EntityIds( + # # link_id=7, + # # ) self.STATE.ids.update(ids, ignore_none=False) if ids.empty(): @@ -6239,7 +6238,7 @@ class MainWindow(QMainWindow): self.initrec.back_main_requested.connect(self.show_main_page) self.initrec.back_requested.connect(self.simple_go_back) self.initrec.company_profile_requested.connect(self.show_page_company_profile) - # self.newinitrec.update_triggered.connect(self.update_grid) + self.initrec.update_triggered.connect(self.update_grid) self.stack.addWidget(self.initrec) # SITE: 'Unternehmensprofil' @@ -6344,70 +6343,53 @@ class MainWindow(QMainWindow): for col_idx, title in enumerate(headers): self.header_grid.addWidget(HeaderCell(title), self.current_row, col_idx) - # self.update_grid() + self.update_grid() return main_widget def _clear_layout(self) -> None: clear_layout(self.grid) - # def update_grid(self) -> None: - # clear_layout(self.grid) + def update_grid(self) -> None: + clear_layout(self.grid) - # data = backend.main_page_get_entries() + data = backend.main_page_get_entries() - # for entry in data: - # self.add_row_to_grid(entry) + for entry in data: + self.add_row_to_grid(entry) # TODO rework - # def add_row_to_grid( - # self, - # entry: types.MainPageEntry, - # ): - # row = self.current_row + def add_row_to_grid( + self, + entry: types.MainPageEntry, + ): + row = self.current_row - # # NAME - # cell_name = MainPage_ClickableCell(entry.display_name, entry) - # cell_name.clicked.connect(self.goto_initial_recording) - # self.grid.addWidget(cell_name, row, 0) - # # DATE - # cell_date_name = entry.Metadaten_aktualisierung.date().strftime(DATE_FMT) - # cell_date = MainPage_ClickableCell(cell_date_name, entry) - # cell_date.clicked.connect(self.goto_initial_recording) - # self.grid.addWidget(cell_date, row, 4) + # NAME + cell_name = MainPage_ClickableCell(entry.display_name, entry) + cell_name.clicked.connect(self.goto_initial_recording) + self.grid.addWidget(cell_name, row, 0) + # DATE + cell_date_name = entry.dt_updated.date().strftime(DATE_FMT) + cell_date = MainPage_ClickableCell(cell_date_name, entry) + cell_date.clicked.connect(self.goto_initial_recording) + self.grid.addWidget(cell_date, row, 4) - # self.current_row += 1 # Zähler für den nächsten Eintrag erhöhen + self.current_row += 1 # Zähler für den nächsten Eintrag erhöhen # TODO rework - # def goto_initial_recording( - # self, - # data: types.MainPageEntry, - # ): - # target: QWidget - # if data.type is types.RecordingType.COMPANY: - # ids = EntityIds(un_id=data.rec_id, type=RecordingType.COMPANY) - # new_state = Page_InitRecCompany_State( - # session=self.STATE.session, - # ids=ids, - # # un_id=data.rec_id, # TODO remove - # locked=True, - # ) - # self.initrec_company.load_state(new_state) - # target = self.initrec_company - # elif data.type is types.RecordingType.PERSON: - # ids = EntityIds(pers_id=data.rec_id, type=RecordingType.PERSON) - # new_state = Page_InitRecPerson_State( - # session=self.STATE.session, - # ids=ids, - # # pers_id=data.rec_id, # TODO remove - # locked=True, - # ) - # self.initrec_person.load_state(new_state) - # target = self.initrec_person - # else: - # raise TypeError(f"Unknown init rec type: {data.type}") - - # self._navigate_to_target(target) + def goto_initial_recording( + self, + data: types.MainPageEntry, + ): + new_state = Page_InitRec_State( + session=self.STATE.session, + ids=data.ids, + recording_type=data.recording_type, + locked=True, + ) + self.initrec.load_state(new_state) + self._navigate_to_target(self.initrec) def _navigate_to_target( self, diff --git a/src/wce_crm/types.py b/src/wce_crm/types.py index 8fe1d55..c96e9ff 100644 --- a/src/wce_crm/types.py +++ b/src/wce_crm/types.py @@ -244,10 +244,10 @@ class ContactPersonInfo(TypedDict): # ** internals @dc.dataclass(slots=True) class MainPageEntry: - rec_id: RecId + ids: EntityIds display_name: str - Metadaten_aktualisierung: datetime.datetime - type: RecordingType + dt_updated: datetime.datetime + recording_type: RecordingType @dc.dataclass(slots=True) -- 2.34.1