From 868db09d9438a72b542e5b3bbb907b91850925cb Mon Sep 17 00:00:00 2001 From: foefl Date: Thu, 23 Jul 2026 19:17:49 +0200 Subject: [PATCH] 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)