diff --git a/alembic/versions/5dfa1fc1dfdf_update_schema_to_meet_customer_.py b/alembic/versions/5dfa1fc1dfdf_update_schema_to_meet_customer_.py new file mode 100644 index 0000000..24e62ec --- /dev/null +++ b/alembic/versions/5dfa1fc1dfdf_update_schema_to_meet_customer_.py @@ -0,0 +1,48 @@ +"""update schema to meet customer requirements + +Revision ID: 5dfa1fc1dfdf +Revises: e290fe18132d +Create Date: 2026-07-28 08:39:44.168872 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "5dfa1fc1dfdf" +down_revision: Union[str, Sequence[str], None] = "e290fe18132d" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("Grunderfassung_Personen", schema=None) as batch_op: + batch_op.add_column( + sa.Column("Stammdaten__kanal_aufmerksamkeit", sa.Text(), nullable=True) + ) + + with op.batch_alter_table("Personen_Unternehmen_Zuordnung", schema=None) as batch_op: + batch_op.add_column(sa.Column("Partnersuche__kontaktweg", sa.Text(), nullable=True)) + batch_op.drop_column("Partnersuche__kanal_aufmerksamkeit") + + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("Personen_Unternehmen_Zuordnung", schema=None) as batch_op: + batch_op.add_column( + sa.Column("Partnersuche__kanal_aufmerksamkeit", sa.TEXT(), nullable=True) + ) + batch_op.drop_column("Partnersuche__kontaktweg") + + with op.batch_alter_table("Grunderfassung_Personen", schema=None) as batch_op: + batch_op.drop_column("Stammdaten__kanal_aufmerksamkeit") + + # ### end Alembic commands ### diff --git a/alembic/versions/8000d15faf03_major_change_in_data_model.py b/alembic/versions/8000d15faf03_major_change_in_data_model.py new file mode 100644 index 0000000..5242e0a --- /dev/null +++ b/alembic/versions/8000d15faf03_major_change_in_data_model.py @@ -0,0 +1,317 @@ +"""major change in data model + +Revision ID: 8000d15faf03 +Revises: 665259cdfdc0 +Create Date: 2026-07-24 15:13:57.443684 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +import wce_crm.db + +# revision identifiers, used by Alembic. +revision: str = "8000d15faf03" +down_revision: Union[str, Sequence[str], None] = "665259cdfdc0" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("grunderfassung_personen") + op.drop_table("beratung_vorgang") + op.drop_table("grunderfassung_unternehmen") + with op.batch_alter_table("zuordnung_personen_unternehmen", schema=None) as batch_op: + batch_op.drop_index( + batch_op.f("uq_aktive_zuordnung"), sqlite_where=sa.text("gueltig_bis IS NULL") + ) + + op.drop_table("zuordnung_personen_unternehmen") + op.drop_table("beratung_einzelberatung") + + op.create_table( + "Grunderfassung_Personen", + sa.Column("pers_id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("erstellt", wce_crm.db.UTCDateTime(), nullable=False), + sa.Column("aktualisiert", wce_crm.db.UTCDateTime(), nullable=False), + sa.Column("geloescht", wce_crm.db.UTCDateTime(), nullable=True), + sa.Column("Metadaten_nutzer", sa.String(length=20), nullable=True), + sa.Column("Metadaten_wiedereintrittsdatum", sa.Date(), nullable=True), + sa.Column("Arbeitserfahrung", sa.Text(), nullable=True), + sa.Column("Grunderfassung_fallnummer", sa.Text(), nullable=True), + sa.Column("Grunderfassung_notiz", sa.Text(), nullable=True), + sa.Column("HoehereBildung", sa.Text(), nullable=True), + sa.Column("Kontaktperson__KP_adresse", sa.Text(), nullable=True), + sa.Column("Kontaktperson__KP_anrede_anschrift", sa.Text(), nullable=True), + sa.Column("Kontaktperson__KP_email", sa.Text(), nullable=True), + sa.Column("Kontaktperson__KP_festnetznummer", sa.Text(), nullable=True), + sa.Column("Kontaktperson__KP_funktion_beziehung", sa.Text(), nullable=True), + sa.Column("Kontaktperson__KP_mobilfunknummer", sa.Text(), nullable=True), + sa.Column("Kontaktperson__KP_name", sa.Text(), nullable=True), + sa.Column("Kontaktperson__KP_name_partner", sa.Text(), nullable=True), + sa.Column("Kontaktperson__KP_titel", sa.Text(), nullable=True), + sa.Column("Kontaktperson__KP_vorname", sa.Text(), nullable=True), + sa.Column("Projektrelevanz__relevanz", sa.Text(), nullable=True), + sa.Column("Projektrelevanz__foerderperiode", sa.Text(), nullable=True), + sa.Column("Schulbildung", sa.Text(), nullable=True), + sa.Column("Sprachkenntnisse", sa.Text(), nullable=True), + sa.Column("Stammdaten__PLZ", sa.Text(), nullable=True), + sa.Column("Stammdaten__anrede_anschrift", sa.Text(), nullable=True), + sa.Column("Stammdaten__anzahl_kinder__alter", sa.Text(), nullable=True), + sa.Column("Stammdaten__anzahl_kinder__anzahl", sa.Integer(), nullable=True), + sa.Column("Stammdaten__aufenthaltsort", sa.Text(), nullable=True), + sa.Column("Stammdaten__bundesland", sa.Text(), nullable=True), + sa.Column("Stammdaten__land", sa.Text(), nullable=True), + sa.Column("Stammdaten__email", sa.Text(), nullable=True), + sa.Column("Stammdaten__familienstand", sa.Text(), nullable=True), + sa.Column("Stammdaten__festnetznummer", sa.Text(), nullable=True), + sa.Column("Stammdaten__geburtsdatum", sa.Date(), nullable=True), + sa.Column("Stammdaten__hausnummer", sa.Text(), nullable=True), + sa.Column("Stammdaten__herkunftsland", sa.Text(), nullable=True), + sa.Column("Stammdaten__mobilfunknummer", sa.Text(), nullable=True), + sa.Column("Stammdaten__name", sa.Text(), nullable=True), + sa.Column("Stammdaten__ort", sa.Text(), nullable=True), + sa.Column("Stammdaten__rueckkehrer", sa.Boolean(), nullable=True), + sa.Column("Stammdaten__staatsangehoerigkeit", sa.Text(), nullable=True), + sa.Column("Stammdaten__strasse", sa.Text(), nullable=True), + sa.Column("Stammdaten__titel", sa.Text(), nullable=True), + sa.Column("Stammdaten__vorname", sa.Text(), nullable=True), + sa.Column("WeitereInfos__WI_arbeitsstatus", sa.Text(), nullable=True), + sa.Column("WeitereInfos__WI_aufenthaltstitel", sa.Text(), nullable=True), + sa.Column("WeitereInfos__WI_deutsch_sprache", sa.Text(), nullable=True), + sa.Column("WeitereInfos__WI_gueltigkeit_aufenthaltstitel", sa.Date(), nullable=True), + sa.Column("WeitereInfos__WI_meldung_institution", sa.Text(), nullable=True), + sa.PrimaryKeyConstraint("pers_id"), + ) + op.create_table( + "Unternehmen", + sa.Column("un_id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("erstellt", wce_crm.db.UTCDateTime(), nullable=False), + sa.Column("geloescht", wce_crm.db.UTCDateTime(), nullable=True), + sa.PrimaryKeyConstraint("un_id"), + ) + op.create_table( + "Beratung_Vorgang", + sa.Column("vorgang_id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("un_id", sa.Integer(), nullable=True), + sa.Column("pers_id", sa.Integer(), nullable=True), + sa.Column("titel", sa.Text(), nullable=False), + sa.Column("beratungs_typ", sa.String(length=20), nullable=False), + sa.Column("erstellt", wce_crm.db.UTCDateTime(), nullable=False), + sa.Column("aktualisiert", wce_crm.db.UTCDateTime(), nullable=False), + sa.Column("geloescht", wce_crm.db.UTCDateTime(), nullable=True), + sa.ForeignKeyConstraint( + ["pers_id"], + ["Grunderfassung_Personen.pers_id"], + ), + sa.ForeignKeyConstraint( + ["un_id"], + ["Unternehmen.un_id"], + ), + sa.PrimaryKeyConstraint("vorgang_id"), + ) + op.create_table( + "Personen_Unternehmen_Zuordnung", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("un_id", sa.Integer(), nullable=False), + sa.Column("an_id", sa.Integer(), nullable=False), + sa.Column("pers_id", sa.Integer(), nullable=False), + sa.Column("erstellt", wce_crm.db.UTCDateTime(), nullable=False), + sa.Column("aktualisiert", wce_crm.db.UTCDateTime(), nullable=False), + sa.Column("geloescht", wce_crm.db.UTCDateTime(), nullable=True), + sa.ForeignKeyConstraint( + ["pers_id"], + ["Grunderfassung_Personen.pers_id"], + ), + sa.ForeignKeyConstraint( + ["un_id"], + ["Unternehmen.un_id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + with op.batch_alter_table("Personen_Unternehmen_Zuordnung", schema=None) as batch_op: + batch_op.create_index( + "uq_aktive_zuordnung", + ["un_id", "pers_id"], + unique=True, + sqlite_where=sa.text("geloescht IS NULL"), + ) + + op.create_table( + "Beratung_Einzelberatung", + sa.Column("beratung_id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("vorgang_id", sa.Integer(), nullable=False), + sa.Column("nutzer_id", sa.Integer(), nullable=False), + sa.Column("nutzer_name", sa.String(length=20), nullable=False), + sa.Column("zeitstempel", wce_crm.db.UTCDateTime(), nullable=False), + sa.Column("ansprechpartner", sa.Text(), nullable=False), + sa.Column("kommunikationsweg", sa.Text(), nullable=False), + sa.Column("thema_crm_matrix", sa.Text(), nullable=True), + sa.Column("anmerkungen", sa.Text(), nullable=True), + sa.Column("rueckmeldung", sa.Text(), nullable=True), + sa.Column("erstellt", wce_crm.db.UTCDateTime(), nullable=False), + sa.Column("aktualisiert", wce_crm.db.UTCDateTime(), nullable=False), + sa.Column("geloescht", wce_crm.db.UTCDateTime(), nullable=True), + sa.ForeignKeyConstraint( + ["vorgang_id"], + ["Beratung_Vorgang.vorgang_id"], + ), + sa.PrimaryKeyConstraint("beratung_id"), + ) + + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("Beratung_Einzelberatung") + with op.batch_alter_table("Personen_Unternehmen_Zuordnung", schema=None) as batch_op: + batch_op.drop_index("uq_aktive_zuordnung", sqlite_where=sa.text("geloescht IS NULL")) + + op.drop_table("Personen_Unternehmen_Zuordnung") + op.drop_table("Beratung_Vorgang") + op.drop_table("Unternehmen") + op.drop_table("Grunderfassung_Personen") + + op.create_table( + "beratung_einzelberatung", + sa.Column("beratung_id", sa.INTEGER(), nullable=False), + sa.Column("vorgang_id", sa.INTEGER(), nullable=False), + sa.Column("nutzer_id", sa.INTEGER(), nullable=False), + sa.Column("nutzer_name", sa.VARCHAR(length=20), nullable=False), + sa.Column("zeitstempel", sa.DATETIME(), nullable=False), + sa.Column("ansprechpartner", sa.TEXT(), nullable=False), + sa.Column("kommunikationsweg", sa.TEXT(), nullable=False), + sa.Column("thema_crm_matrix", sa.TEXT(), nullable=True), + sa.Column("anmerkungen", sa.TEXT(), nullable=True), + sa.Column("rueckmeldung", sa.TEXT(), nullable=True), + sa.Column("erstellt", sa.DATETIME(), nullable=False), + sa.Column("aktualisiert", sa.DATETIME(), nullable=False), + sa.Column("geloescht", sa.DATETIME(), nullable=True), + sa.ForeignKeyConstraint( + ["vorgang_id"], + ["beratung_vorgang.vorgang_id"], + ), + sa.PrimaryKeyConstraint("beratung_id"), + ) + op.create_table( + "zuordnung_personen_unternehmen", + sa.Column("id", sa.INTEGER(), nullable=False), + sa.Column("un_id", sa.INTEGER(), nullable=False), + sa.Column("pers_id", sa.INTEGER(), nullable=False), + sa.Column("gueltig_ab", sa.DATETIME(), nullable=False), + sa.Column("gueltig_bis", sa.DATETIME(), nullable=True), + sa.ForeignKeyConstraint( + ["pers_id"], + ["grunderfassung_personen.pers_id"], + ), + sa.ForeignKeyConstraint( + ["un_id"], + ["grunderfassung_unternehmen.un_id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + with op.batch_alter_table("zuordnung_personen_unternehmen", schema=None) as batch_op: + batch_op.create_index( + batch_op.f("uq_aktive_zuordnung"), + ["un_id", "pers_id"], + unique=1, + sqlite_where=sa.text("gueltig_bis IS NULL"), + ) + + op.create_table( + "grunderfassung_unternehmen", + sa.Column("un_id", sa.INTEGER(), nullable=False), + sa.Column("Metadaten_erstellung", sa.DATETIME(), nullable=True), + sa.Column("Metadaten_aktualisierung", sa.DATETIME(), nullable=True), + sa.Column("Metadaten_nutzer", sa.VARCHAR(length=20), nullable=True), + sa.Column("Grunderfassung_fallnummer", sa.TEXT(), nullable=True), + sa.Column("Grunderfassung_notiz", sa.TEXT(), nullable=True), + sa.Column("Partnersuche__kanal_aufmerksamkeit", sa.TEXT(), nullable=True), + sa.Column("Partnersuche__person_suche", sa.INTEGER(), nullable=True), + sa.Column("Partnersuche__un_suche", sa.INTEGER(), nullable=True), + sa.Column("geloescht", sa.DATETIME(), nullable=True), + sa.PrimaryKeyConstraint("un_id"), + ) + op.create_table( + "beratung_vorgang", + sa.Column("vorgang_id", sa.INTEGER(), nullable=False), + sa.Column("un_id", sa.INTEGER(), nullable=True), + sa.Column("pers_id", sa.INTEGER(), nullable=True), + sa.Column("titel", sa.TEXT(), nullable=False), + sa.Column("beratungs_typ", sa.VARCHAR(length=20), nullable=False), + sa.Column("erstellt", sa.DATETIME(), nullable=False), + sa.Column("aktualisiert", sa.DATETIME(), nullable=False), + sa.Column("geloescht", sa.DATETIME(), nullable=True), + sa.ForeignKeyConstraint( + ["pers_id"], + ["grunderfassung_personen.pers_id"], + ), + sa.ForeignKeyConstraint( + ["un_id"], + ["grunderfassung_unternehmen.un_id"], + ), + sa.PrimaryKeyConstraint("vorgang_id"), + ) + op.create_table( + "grunderfassung_personen", + sa.Column("pers_id", sa.INTEGER(), nullable=False), + sa.Column("Metadaten_erstellung", sa.DATETIME(), nullable=True), + sa.Column("Metadaten_aktualisierung", sa.DATETIME(), nullable=True), + sa.Column("Metadaten_nutzer", sa.VARCHAR(length=20), nullable=True), + sa.Column("Metadaten_wiedereintrittsdatum", sa.DATE(), nullable=True), + sa.Column("Arbeitserfahrung", sa.TEXT(), nullable=True), + sa.Column("Grunderfassung_fallnummer", sa.TEXT(), nullable=True), + sa.Column("Grunderfassung_notiz", sa.TEXT(), nullable=True), + sa.Column("HoehereBildung", sa.TEXT(), nullable=True), + sa.Column("Kontaktperson__KP_adresse", sa.TEXT(), nullable=True), + sa.Column("Kontaktperson__KP_anrede_anschrift", sa.TEXT(), nullable=True), + sa.Column("Kontaktperson__KP_email", sa.TEXT(), nullable=True), + sa.Column("Kontaktperson__KP_festnetznummer", sa.TEXT(), nullable=True), + sa.Column("Kontaktperson__KP_funktion_beziehung", sa.TEXT(), nullable=True), + sa.Column("Kontaktperson__KP_mobilfunknummer", sa.TEXT(), nullable=True), + sa.Column("Kontaktperson__KP_name", sa.TEXT(), nullable=True), + sa.Column("Kontaktperson__KP_name_partner", sa.TEXT(), nullable=True), + sa.Column("Kontaktperson__KP_titel", sa.TEXT(), nullable=True), + sa.Column("Kontaktperson__KP_vorname", sa.TEXT(), nullable=True), + sa.Column("Projektrelevanz__relevanz", sa.TEXT(), nullable=True), + sa.Column("Projektrelevanz__foerderperiode", sa.TEXT(), nullable=True), + sa.Column("Schulbildung", sa.TEXT(), nullable=True), + sa.Column("Sprachkenntnisse", sa.TEXT(), nullable=True), + sa.Column("Stammdaten__PLZ", sa.TEXT(), nullable=True), + sa.Column("Stammdaten__anrede_anschrift", sa.TEXT(), nullable=True), + sa.Column("Stammdaten__anzahl_kinder__alter", sa.TEXT(), nullable=True), + sa.Column("Stammdaten__anzahl_kinder__anzahl", sa.INTEGER(), nullable=True), + sa.Column("Stammdaten__aufenthaltsort", sa.TEXT(), nullable=True), + sa.Column("Stammdaten__bundesland", sa.TEXT(), nullable=True), + sa.Column("Stammdaten__land", sa.TEXT(), nullable=True), + sa.Column("Stammdaten__email", sa.TEXT(), nullable=True), + sa.Column("Stammdaten__familienstand", sa.TEXT(), nullable=True), + sa.Column("Stammdaten__festnetznummer", sa.TEXT(), nullable=True), + sa.Column("Stammdaten__geburtsdatum", sa.DATE(), nullable=True), + sa.Column("Stammdaten__hausnummer", sa.TEXT(), nullable=True), + sa.Column("Stammdaten__herkunftsland", sa.TEXT(), nullable=True), + sa.Column("Stammdaten__mobilfunknummer", sa.TEXT(), nullable=True), + sa.Column("Stammdaten__name", sa.TEXT(), nullable=True), + sa.Column("Stammdaten__ort", sa.TEXT(), nullable=True), + sa.Column("Stammdaten__rueckkehrer", sa.BOOLEAN(), nullable=True), + sa.Column("Stammdaten__staatsangehoerigkeit", sa.TEXT(), nullable=True), + sa.Column("Stammdaten__strasse", sa.TEXT(), nullable=True), + sa.Column("Stammdaten__titel", sa.TEXT(), nullable=True), + sa.Column("Stammdaten__vorname", sa.TEXT(), nullable=True), + sa.Column("WeitereInfos__WI_arbeitsstatus", sa.TEXT(), nullable=True), + sa.Column("WeitereInfos__WI_aufenthaltstitel", sa.TEXT(), nullable=True), + sa.Column("WeitereInfos__WI_deutsch_sprache", sa.TEXT(), nullable=True), + sa.Column("WeitereInfos__WI_gueltigkeit_aufenthaltstitel", sa.DATE(), nullable=True), + sa.Column("WeitereInfos__WI_meldung_institution", sa.TEXT(), nullable=True), + sa.Column("geloescht", sa.DATETIME(), nullable=True), + sa.PrimaryKeyConstraint("pers_id"), + ) + + # ### end Alembic commands ### diff --git a/alembic/versions/e290fe18132d_update_outstanding_table_changes.py b/alembic/versions/e290fe18132d_update_outstanding_table_changes.py new file mode 100644 index 0000000..ab4f5ea --- /dev/null +++ b/alembic/versions/e290fe18132d_update_outstanding_table_changes.py @@ -0,0 +1,38 @@ +"""update outstanding table changes + +Revision ID: e290fe18132d +Revises: 8000d15faf03 +Create Date: 2026-07-27 09:05:06.301043 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "e290fe18132d" +down_revision: Union[str, Sequence[str], None] = "8000d15faf03" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("Personen_Unternehmen_Zuordnung", schema=None) as batch_op: + batch_op.add_column( + sa.Column("Partnersuche__kanal_aufmerksamkeit", sa.Text(), nullable=True) + ) + + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("Personen_Unternehmen_Zuordnung", schema=None) as batch_op: + batch_op.drop_column("Partnersuche__kanal_aufmerksamkeit") + + # ### end Alembic commands ### diff --git a/data_model/project_db_schema.aml b/data_model/project_db_schema.aml index 2738be5..7b10d53 100644 --- a/data_model/project_db_schema.aml +++ b/data_model/project_db_schema.aml @@ -1,30 +1,30 @@ -Ext_CRM.Master +Ext_Kontaktliste.Master ma_id int pk - -Ext_CRM.Ansprechpartner +Ext_Kontaktliste.Ansprechpartner an_id int pk - ma_id int -> Ext_CRM.Master(ma_id) + ma_id int -> Ext_Kontaktliste.Master(ma_id) +Unternehmen | Es wird nur die ID für die Kontaktliste referenziert. Alle relevanten Infos werden live aus der Kontaktliste geladen via 'ATTACH'. + un_id int pk -> Ext_Kontaktliste.Master(ma_id) + erstellt "timestamp with time zone" + geloescht "timestamp with time zone" nullable -Grunderfassung_Unternehmen - un_id int pk {autoIncrement} - geloescht "timestamp with time zone" nullable - Metadaten_erstellung "timestamp with time zone"=`datetime.datetime.now(datetime.UTC)` nullable - Metadaten_aktualisierung "timestamp with time zone"=`datetime.datetime.now(datetime.UTC)` nullable {on_update: `datetime.datetime.now(datetime.UTC)`} - Metadaten_nutzer varchar(20) nullable - Grunderfassung_fallnummer text nullable - Grunderfassung_notiz text nullable - Partnersuche__un_suche int nullable -> Ext_CRM.Master(ma_id) - Partnersuche__person_suche int nullable -> Ext_CRM.Ansprechpartner(an_id) - Partnersuche__kanal_aufmerksamkeit text nullable - +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__kontaktweg text nullable + erstellt "timestamp with time zone"=`datetime.datetime.now(datetime.UTC)` + aktualisiert "timestamp with time zone"=`datetime.datetime.now(datetime.UTC)` {on_update: `datetime.datetime.now(datetime.UTC)`} + geloescht "timestamp with time zone" nullable | Soft-Delete Marker (partieller Unique-Index: WHERE geloescht IS NULL) 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 @@ -45,6 +45,7 @@ Grunderfassung_Personen Projektrelevanz__relevanz text nullable Projektrelevanz__foerderperiode text nullable Schulbildung text nullable + Stammdaten__kanal_aufmerksamkeit text nullable Stammdaten__PLZ text nullable Stammdaten__anrede_anschrift text nullable Stammdaten__anzahl_kinder__alter text nullable @@ -71,19 +72,21 @@ 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. - id int pk {autoIncrement} - un_id int -> Grunderfassung_Unternehmen(un_id) - pers_id int -> Grunderfassung_Personen(pers_id) - gueltig_ab "timestamp with time zone"=`datetime.datetime.now(datetime.UTC)` - gueltig_bis "timestamp with time zone" nullable +# Zuordnung_Personen_Unternehmen | Eine Person kann zu mehreren Unternehmen gehören, ein Unternehmen kann mehrere Personen betreuen. +# id int pk {autoIncrement} +# un_id int -> Grunderfassung_Unternehmen(un_id) +# pers_id int -> Grunderfassung_Personen(pers_id) +# gueltig_ab "timestamp with time zone"=`datetime.datetime.now(datetime.UTC)` +# gueltig_bis "timestamp with time zone" nullable Beratung_Vorgang | Beratung kann an Unternehmen allein (pauschal), Person allein (pauschal) oder beide (individual) gebunden sein vorgang_id int pk {autoIncrement} - un_id int nullable -> Grunderfassung_Unternehmen(un_id) | Pauschalberatungen sind sowohl für Unternehmen als auch Personen zulässig + un_id int nullable -> Unternehmen(un_id) | Pauschalberatungen sind sowohl für Unternehmen als auch Personen zulässig pers_id int nullable -> Grunderfassung_Personen(pers_id) | sowohl Pauschal- als auch Individualberatung bei Personenbindung möglich titel text beratungs_typ berat_typ(pauschal, individual) @@ -92,7 +95,6 @@ Beratung_Vorgang | Beratung kann an Unternehmen allein (pauschal), Person allein geloescht "timestamp with time zone" nullable # temporaer bool | Gibt an, ob der Vorgang losgelöst von der Grunderfassung gestartet wurde (Daten müssen nachträglich zugeordnet werden); theoretisch auch erfüllt, wenn `un_id` und `pers_id` beide NULL sind - Beratung_Einzelberatung | Änderungen müssen über einen DB-Trigger an Elterntabelle zurückgeschrieben werden beratung_id int pk {autoIncrement} vorgang_id int -> Beratung_Vorgang(vorgang_id) | Jede Beratung muss zu einem Beratungsvorgang gehören. diff --git a/prototypes/dual_dict.py b/prototypes/dual_dict.py new file mode 100644 index 0000000..17fc7b3 --- /dev/null +++ b/prototypes/dual_dict.py @@ -0,0 +1,54 @@ +# %% +from __future__ import annotations + +import copy +from typing import Any + +from dopt_basics.datastructures import DualDict + +from wce_crm.data_models import FIELD_DB_MAPPING_GRUNDERERFASSUNG_PERSONEN + +# %% +dd: DualDict[str, str] = DualDict(peter="test") + + +# %% +def rename_db_data( + db_data: dict[str, Any], + mapping: DualDict, +) -> dict[str, Any]: + new_db_data = copy.deepcopy(db_data) + + for k, v in db_data.items(): + if k in mapping: + new_db_data[mapping[k]] = v + del new_db_data[k] + + if k in mapping.inverted: + new_db_data[mapping.inverted[k]] = v + del new_db_data[k] + + return new_db_data + + +form_data = { + "Metadaten_erstellung": "t1", + "Metadaten_aktualisierung": "t2", + "Metadaten_nutzer": "t3", +} +db_data = rename_db_data(form_data, FIELD_DB_MAPPING_GRUNDERERFASSUNG_PERSONEN) +# %% +db_data + +# %% +cvt_form_data = rename_db_data(db_data, FIELD_DB_MAPPING_GRUNDERERFASSUNG_PERSONEN) +cvt_form_data +# %% +dd["test"] = "peter" +# %% +dd["test"] +# %% +dd.inverted["peter"] +# %% +dd +# %% diff --git a/prototypes/tests.py b/prototypes/tests.py index 4e61039..46d3b72 100644 --- a/prototypes/tests.py +++ b/prototypes/tests.py @@ -8,20 +8,270 @@ import json import re from collections import defaultdict from collections.abc import Sequence -from pprint import pprint +from pprint import pformat, pprint from typing import Annotated, Any import babel import polars as pl -import sqlalchemy as sql +import sqlalchemy as sa from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator, model_validator from PySide6.QtCore import QDate, Qt from wce_crm import constants, db from wce_crm.backend import backend +from wce_crm.types import EntityIds, MainPageEntry, RecordingType # %% -int("test") +# backend.page_consulting_linking_companies(None) +# backend.page_consulting_linking_persons(3) +ret = backend.main_page_get_entries() +pprint(ret) +# %% +with db.ENGINE.connect() as conn: + q_relevant_entries = ( + sa.select( + db.t_grunderfassung_personen.c.pers_id, + db.t_grunderfassung_personen.c.aktualisiert, + db.t_zuordnung_personen_unternehmen.c.id.label("link_id"), + db.t_zuordnung_personen_unternehmen.c.un_id, + db.t_zuordnung_personen_unternehmen.c.an_id, + db.t_grunderfassung_personen.c.Stammdaten__name, + db.t_grunderfassung_personen.c.Stammdaten__vorname, + db.t_grunderfassung_personen.c.Stammdaten__ort, + ) + .outerjoin( + db.t_zuordnung_personen_unternehmen, + db.t_grunderfassung_personen.c.pers_id + == db.t_zuordnung_personen_unternehmen.c.pers_id, + ) + .order_by(db.t_grunderfassung_personen.c.Stammdaten__name) + ).subquery("relevant_entries") + + stmt = sa.select( + q_relevant_entries, + db.t_ext_crm_master_attach.c.ma_unternehmensname, + ).outerjoin( + db.t_ext_crm_master_attach, + q_relevant_entries.c.un_id == db.t_ext_crm_master_attach.c.ma_id, + ) + + ret = conn.execute(stmt) + + # results = list(ret.mappings()) + # print(f"Results are:\n{pformat(results)}") + + main_page_entries: list[MainPageEntry] = [] + for entry in ret.mappings(): + ids = EntityIds( + pers_id=entry["pers_id"], + link_id=entry["link_id"], + un_id=entry["un_id"], + an_id=entry["an_id"], + ) + recording_type = RecordingType.WITHOUT_COMPANY + if ids.un_id or ids.link_id: + recording_type = RecordingType.WITH_COMPANY + + first_name = entry["Stammdaten__vorname"] + last_name = entry["Stammdaten__name"] + location_city = entry["Stammdaten__ort"] + person_name = f"{first_name} {last_name}" + if location_city: + person_name += f" (Ort: {location_city})" + + company_name = entry["ma_unternehmensname"] + + display_name = person_name + if recording_type is RecordingType.WITH_COMPANY: + display_name = company_name + + main_page_entries.append( + MainPageEntry( + ids=ids, + display_name=display_name, + dt_updated=entry["aktualisiert"], + recording_type=recording_type, + ) + ) + main_page_entries.sort(key=lambda x: x.dt_updated, reverse=True) + pprint(main_page_entries) +# %% + + +# %% +entry = results[0] +ids = EntityIds( + pers_id=entry["pers_id"], + link_id=entry["link_id"], + un_id=entry["un_id"], + an_id=entry["an_id"], +) +recording_type = RecordingType.WITHOUT_COMPANY +if ids.un_id or ids.link_id: + recording_type = RecordingType.WITH_COMPANY + +first_name = entry["Stammdaten__vorname"] +last_name = entry["Stammdaten__name"] +location_city = entry["Stammdaten__ort"] +person_name = f"{first_name} {last_name}" +if location_city: + person_name += f" (Ort: {location_city})" + +company_name = entry["ma_unternehmensname"] + +display_name = person_name +if recording_type is RecordingType.WITH_COMPANY: + display_name = company_name + +main_page_entry = MainPageEntry( + ids=ids, + display_name=display_name, + dt_updated=entry["aktualisiert"], + recording_type=recording_type, +) + +# %% +pprint(main_page_entry) + + +# %% +# %% +@dc.dataclass(slots=True, kw_only=True) +class LinkingPerson: + pers_id: int + display_name: str + un_ids: set[int] + + +with db.ENGINE.connect() as conn: + # if un_id None: list all persons + # else: list just the persons associated with the company + + stmt = ( + sa.select( + db.t_grunderfassung_personen.c.pers_id, + db.t_zuordnung_personen_unternehmen.c.un_id, + db.t_grunderfassung_personen.c.Stammdaten__name, + db.t_grunderfassung_personen.c.Stammdaten__vorname, + db.t_grunderfassung_personen.c.Stammdaten__ort, + ) + .outerjoin( + db.t_zuordnung_personen_unternehmen, + db.t_grunderfassung_personen.c.pers_id + == db.t_zuordnung_personen_unternehmen.c.pers_id, + ) + .order_by(db.t_grunderfassung_personen.c.Stammdaten__name) + ) + + # if un_id is not None: + # person_select = person_select.select_from( + # db.t_grunderfassung_personen.join( + # db.t_zuordnung_personen_unternehmen, + # db.t_grunderfassung_personen.c.pers_id + # == db.t_zuordnung_personen_unternehmen.c.pers_id, + # ) + # ).where(db.t_zuordnung_personen_unternehmen.c.un_id == un_id) + + ret = conn.execute(stmt) + + persons_map: dict[str, Any] = {} + for row in ret: + p_id = row.pers_id + + if p_id not in persons_map: + first_name = row.Stammdaten__vorname + last_name = row.Stammdaten__name + location_city = row.Stammdaten__ort + + person_name = f"{first_name} {last_name}" + if location_city: + person_name += f" (Ort: {location_city})" + + persons_map[p_id] = { + "pers_id": p_id, + "display_name": person_name, + "un_ids": set(), + } + + # Falls verknüpft, ID zur Menge hinzufügen + if row.un_id is not None: + persons_map[p_id]["un_ids"].add(row.un_id) + + person_names = [LinkingPerson(**data) for data in persons_map.values()] + + +# person_names = ret.mappings().all() +# if not person_names: +# print(f"No persons found for 'un_id': {un_id}") + +print(f"[Page Consulting -- Linking] Retrieved person names:\n{pformat(person_names)}") + +# %% +dic = { + "t1": "test1", + "t2": "test2", + "t3": "test3", + "t4": "test4", +} +filter_keys = frozenset(("t3", "t2")) + +dic_in, dic_out = backend.split_dict(dic, filter_keys) + +print(f"{dic_in=}") +print(f"{dic_out=}") + + +# %% +dic_update = { + "t5": "new", + "t6": "new_2", +} + +dic_updated = backend.dic_deep_update(dic, dic_update) +print(f"{dic=}") +print(f"{dic_update=}") +print(f"{dic_updated=}") + +# %% +raw_ent_ids = EntityIds() +db_ent_ids = EntityIds(un_id=3, an_id=2) + +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 = EntityIds(un_id=None, an_id=2, link_id=1) +raw_ent_ids.update(db_ent_ids) +print(f"{raw_ent_ids=}") +print(f"{raw_ent_ids.empty()=}") +print(f"{raw_ent_ids.valid()=}") +raw_ent_ids.update(EntityIds()) +print(f"{raw_ent_ids=}") +print(f"{raw_ent_ids.empty()=}") +print(f"{raw_ent_ids.valid()=}") + +# %% +raw_ent_ids.update(EntityIds(un_id=None, pers_id=2)) +print(f"{raw_ent_ids=}") +print(f"{raw_ent_ids.empty()=}") +print(f"{raw_ent_ids.empty(RecordingType.WITH_COMPANY)=}") +print(f"{raw_ent_ids.empty(RecordingType.WITHOUT_COMPANY)=}") +print(f"{raw_ent_ids.valid(RecordingType.WITH_COMPANY)=}") +print(f"{raw_ent_ids.valid(RecordingType.WITHOUT_COMPANY)=}") +raw_ent_ids.update(EntityIds(un_id=None, an_id=2, link_id=3, pers_id=2)) +print(f"{raw_ent_ids=}") +print(f"{raw_ent_ids.empty(RecordingType.WITH_COMPANY)=}") +print(f"{raw_ent_ids.empty(RecordingType.WITHOUT_COMPANY)=}") +print(f"{raw_ent_ids.valid(RecordingType.WITH_COMPANY)=}") +print(f"{raw_ent_ids.valid(RecordingType.WITHOUT_COMPANY)=}") +raw_ent_ids.update(EntityIds(un_id=2, pers_id=None, an_id=2, link_id=3)) +print(f"{raw_ent_ids=}") +print(f"{raw_ent_ids.empty(RecordingType.WITH_COMPANY)=}") +print(f"{raw_ent_ids.empty(RecordingType.WITHOUT_COMPANY)=}") +print(f"{raw_ent_ids.valid(RecordingType.WITH_COMPANY)=}") +print(f"{raw_ent_ids.valid(RecordingType.WITHOUT_COMPANY)=}") # %% db_path = constants.Config.DB_PATH_MAIN @@ -29,22 +279,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 +456,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..97a0042 100644 --- a/src/wce_crm/backend/backend.py +++ b/src/wce_crm/backend/backend.py @@ -2,8 +2,9 @@ from __future__ import annotations import copy import datetime +from collections.abc import Container, Mapping from pprint import pformat -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any, TypeVar, cast import polars as pl import sqlalchemy as sa @@ -13,9 +14,13 @@ from sqlalchemy.dialects.sqlite import insert as sqlite_insert from wce_crm import db from wce_crm.constants import TIMEZONE_CEST from wce_crm.data_models import ( + FIELD_DB_MAPPING_GRUNDERERFASSUNG_PERSONEN, + FIELD_DB_MAPPING_GRUNDERERFASSUNG_UNTERNEHMEN, + FIELD_DB_SPLIT_METADATEN, Beratungsgespraech_Einzelgespraech, Beratungsgespraech_Vorgang, InitRec, + Page_InitRec_Form_Data, ) from wce_crm.logging import logger_back as logger from wce_crm.types import ( @@ -29,11 +34,17 @@ from wce_crm.types import ( LinkingConsultationsEntryCompany, LinkingConsultationsEntryPerson, MainPageEntry, + RecordingType, ) if TYPE_CHECKING: + from dopt_basics.datastructures import DualDict + from wce_crm.types import ConsId, ExtAnId, ExtMaId, RecId +K = TypeVar("K") +V = TypeVar("V") + def _transform_for_gui_output( data: pl.DataFrame, @@ -69,7 +80,8 @@ def initrec_comp_search_choices() -> tuple[tuple[str, int], ...]: return tuple(zip(df["dedupl"], df["ma_id"])) -def initrec_comp_search_get_info( +# TODO change to direct SQL interaction +def external_company_get_info( ma_id: ExtMaId, ) -> CompanyInfo: logger.debug("[Call backend] comp_search_get_info") @@ -122,139 +134,67 @@ def initrec_comp_contact_person_search_get_info( # // internals -# TODO check removal -# def initrec_company_insert_initial_recording( -# data: dict[str, Any], -# ) -> RecId: -# logger.debug("[Call backend] insert_initial_recording") -# stmt = db.grunderfassung_unternehmen.insert() -# with db.ENGINE.begin() as conn: -# ret = conn.execute(stmt, data) +def split_dict( + dic: dict[K, V], + split_keys: Container[K], +) -> tuple[dict[K, V], dict[K, V]]: + keys_in: dict[K, V] = {} + keys_out = copy.deepcopy(dic) -# if ret.rowcount == 0: -# raise IOError("Entry was not inserted correctly") + for check_key in dic: + if check_key in split_keys: + keys_in[check_key] = keys_out.pop(check_key) -# prim_keys = ret.inserted_primary_key -# assert prim_keys - -# return prim_keys[0] + return keys_in, keys_out -# def initrec_company_update_initial_recording( -# id_: RecId, -# data: dict[str, Any], -# ) -> None: -# logger.debug("[Call backend] update_initial_recording") -# stmt = db.grunderfassung_unternehmen.update().where( -# db.grunderfassung_unternehmen.c.un_id == id_ -# ) -# with db.ENGINE.begin() as conn: -# conn.execute(stmt, data) +def dic_deep_update( + dic: dict[K, V], + data: Mapping[K, V], +) -> dict[K, V]: + dic_copy = copy.deepcopy(dic) + dic_copy.update(data) + + return dic_copy -def initrec_company_to_db( - auto_form_data: InitRec, +def _rename_db_data( + db_data: dict[str, Any], + mapping: DualDict, +) -> dict[str, Any]: + new_db_data = copy.deepcopy(db_data) + + for k, v in db_data.items(): + if k in mapping: + new_db_data[mapping[k]] = v + del new_db_data[k] + + if k in mapping.inverted: + new_db_data[mapping.inverted[k]] = v + del new_db_data[k] + + return new_db_data + + +def page_initrec_to_db( + data: InitRec, ) -> InitRec: - logger.debug("[AutoForm -- backend] Call database saving routine...") - - dump_data = copy.deepcopy(auto_form_data.db_data) - dump_data["geloescht"] = auto_form_data.geloescht with db.ENGINE.begin() as conn: - if auto_form_data.ids.un_id is None: - logger.debug("[AutoForm -- backend] Insert...") + # person + dump_data = dic_deep_update(data.form_data.person, data.form_data.metadata) + dump_data = _rename_db_data(dump_data, FIELD_DB_MAPPING_GRUNDERERFASSUNG_PERSONEN) + dump_data["geloescht"] = data.geloescht - 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) + if data.ids.pers_id is None: + logger.debug("[InitRec -- backend] Insert...") - from_db = ret.mappings().fetchall() - assert len(from_db) == 1, "expected excactly one returned row" - data_from_db = from_db[0] - auto_form_data.ids.un_id = data_from_db["un_id"] - auto_form_data.geloescht = data_from_db["geloescht"] - auto_form_data.db_data["Metadaten_aktualisierung"] = data_from_db[ - "Metadaten_aktualisierung" - ] - - logger.debug("[AutoForm -- backend] Inserted InitRec Company successfully") - - else: - logger.debug("[AutoForm -- backend] Update...") - stmt = ( - db.grunderfassung_unternehmen.update() - .where(db.grunderfassung_unternehmen.c.un_id == auto_form_data.ids.un_id) - .returning( - db.grunderfassung_unternehmen.c.Metadaten_aktualisierung, - db.grunderfassung_unternehmen.c.geloescht, - ) - ) - ret = conn.execute(stmt, dump_data) - - from_db = ret.mappings().fetchall() - assert len(from_db) == 1, "expected excatly one returned row" - data_from_db = from_db[0] - - auto_form_data.db_data["Metadaten_aktualisierung"] = data_from_db[ - "Metadaten_aktualisierung" - ] - - logger.debug( - "[AutoForm -- backend] Updated InitRec Company with ID %d successfully", - auto_form_data.ids.un_id, - ) - - return auto_form_data - - -def initrec_company_from_db( - id_: RecId, -) -> InitRec: - logger.debug("[AutoForm -- backend] Call database loading routine...") - stmt = db.grunderfassung_unternehmen.select().where( - db.grunderfassung_unternehmen.c.un_id == id_ - ) - with db.ENGINE.connect() as conn: - ret = conn.execute(stmt) - - from_db = ret.mappings().all() - assert len(from_db) == 1, "not excactly one company initial recording obtained" - data_from_db = dict(from_db[0]) - - geloescht = data_from_db["geloescht"] - del data_from_db["geloescht"] - - return InitRec( - ids=EntityIds(un_id=id_), - # rec_id=id_, # TODO remove - geloescht=geloescht, - db_data=data_from_db, - ) - - -def initrec_person_to_db( - auto_form_data: InitRec, -) -> InitRec: - logger.debug("[AutoForm -- backend] Call database saving routine...") - - dump_data = copy.deepcopy(auto_form_data.db_data) - dump_data["geloescht"] = auto_form_data.geloescht - - with db.ENGINE.begin() as conn: - if auto_form_data.ids.pers_id is None: - logger.debug("[AutoForm -- backend] Insert...") - - stmt = sa.insert(db.grunderfassung_personen).returning( - db.grunderfassung_personen.c.pers_id, - db.grunderfassung_personen.c.Metadaten_aktualisierung, - db.grunderfassung_personen.c.geloescht, + stmt = sa.insert(db.t_grunderfassung_personen).returning( + db.t_grunderfassung_personen ) stmt_compiled = str(stmt.compile(db.ENGINE)) logger.debug( - "[AutoForm -- backend] Data to insert:\nStatement: %s\n%s", + "[InitRec -- backend] Data to insert:\nStatement: %s\n%s", stmt_compiled, pformat(dump_data), ) @@ -263,170 +203,189 @@ def initrec_person_to_db( 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" - ] + data_from_db = dict(from_db[0]) + data_from_db = _rename_db_data( + data_from_db, FIELD_DB_MAPPING_GRUNDERERFASSUNG_PERSONEN + ) + + data.ids.pers_id = data_from_db["pers_id"] 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, - ) + db.t_grunderfassung_personen.update() + .where(db.t_grunderfassung_personen.c.pers_id == data.ids.pers_id) + .returning(db.t_grunderfassung_personen) ) ret = conn.execute(stmt, dump_data) from_db = ret.mappings().fetchall() assert len(from_db) == 1, "expected excatly one returned row" - data_from_db = from_db[0] - - auto_form_data.db_data["Metadaten_aktualisierung"] = data_from_db[ - "Metadaten_aktualisierung" - ] - - logger.debug( - "[AutoForm -- backend] Updated InitRec Person with ID %d successfully", - auto_form_data.ids.pers_id, + data_from_db = dict(from_db[0]) + data_from_db = _rename_db_data( + data_from_db, FIELD_DB_MAPPING_GRUNDERERFASSUNG_PERSONEN ) - return auto_form_data + # data.geloescht = data_from_db["geloescht"] + # data.form_data.person["Metadaten_aktualisierung"] = data_from_db[ + # "Metadaten_aktualisierung" + # ] + + logger.debug( + "[InitRec -- backend] Updated InitRec Person with ID %d successfully", + data.ids.pers_id, + ) + + data.geloescht = data_from_db["geloescht"] + del data_from_db["geloescht"] + in_filter, out_filter = split_dict(data_from_db, FIELD_DB_SPLIT_METADATEN) + data.form_data.metadata = in_filter + data.form_data.person = out_filter + + # company + if data.recording_type is RecordingType.WITH_COMPANY: + # insert into company table and add additional information + assert data.ids.un_id, "company ID not set" + assert data.ids.an_id, "contact person (an) ID not set" + assert data.ids.pers_id, "person ID not set" + + stmt = ( + sqlite_insert(db.t_unternehmen) + .values(un_id=data.ids.un_id, geloescht=None) + .on_conflict_do_nothing(index_elements=["un_id"]) + ) + conn.execute(stmt) + + # additional information --> linking table + dump_data = _rename_db_data( + data.form_data.company, FIELD_DB_MAPPING_GRUNDERERFASSUNG_UNTERNEHMEN + ) + logger.debug( + "[InitRec -- backend] Company - additional info: Dump Data = \n%s", + pformat(dump_data), + ) + if "un_id" not in dump_data: + # only case when updated + dump_data["un_id"] = data.ids.un_id + dump_data["an_id"] = data.ids.an_id + + stmt = sqlite_insert(db.t_zuordnung_personen_unternehmen).values( + **dump_data, pers_id=data.ids.pers_id + ) + + stmt = stmt.on_conflict_do_update( + index_elements=["un_id", "pers_id"], + index_where=sa.text("geloescht IS NULL"), + set_={ + "an_id": stmt.excluded.an_id, + "Partnersuche__kontaktweg": stmt.excluded.Partnersuche__kontaktweg, + "aktualisiert": datetime.datetime.now(datetime.UTC), + "geloescht": data.geloescht, + }, + ).returning(db.t_zuordnung_personen_unternehmen.c.id) + ret = conn.execute(stmt) + + from_db = ret.mappings().fetchall() + assert len(from_db) == 1, "expected excatly one returned row" + data_from_db = dict(from_db[0]) + data_from_db = _rename_db_data( + data_from_db, FIELD_DB_MAPPING_GRUNDERERFASSUNG_UNTERNEHMEN + ) + data.ids.link_id = data_from_db["id"] + + return data -def initrec_person_from_db( - id_: RecId, +def page_initrec_from_db( + ids: EntityIds, ) -> InitRec: - logger.debug("[AutoForm -- backend] Call database loading routine...") - stmt = db.grunderfassung_personen.select().where( - db.grunderfassung_personen.c.pers_id == id_ + logger.debug("[InitRec -- backend] Call database loading routine...") + + recording_type = RecordingType.WITHOUT_COMPANY + if ids.link_id is not None or ids.valid(EntityType.COMPANY): + recording_type = RecordingType.WITH_COMPANY + + data_company_table: dict[str, Any] = {} + if recording_type is RecordingType.WITH_COMPANY: + logger.debug("[InitRec -- backend] With company. Link ID: %s", ids.link_id) + if ids.link_id: + stmt = sa.select( + db.t_zuordnung_personen_unternehmen.c.pers_id, + db.t_zuordnung_personen_unternehmen.c.un_id, + db.t_zuordnung_personen_unternehmen.c.an_id, + db.t_zuordnung_personen_unternehmen.c.Partnersuche__kontaktweg, + ).where( + db.t_zuordnung_personen_unternehmen.c.id == ids.link_id, + ) + else: + assert ids.un_id, "company ID not set" + assert ids.pers_id, "person ID not set" + stmt = sa.select( + db.t_zuordnung_personen_unternehmen.c.pers_id, + db.t_zuordnung_personen_unternehmen.c.un_id, + db.t_zuordnung_personen_unternehmen.c.an_id, + db.t_zuordnung_personen_unternehmen.c.Partnersuche__kontaktweg, + ).where( + db.t_zuordnung_personen_unternehmen.c.pers_id == ids.pers_id, + db.t_zuordnung_personen_unternehmen.c.un_id == ids.un_id, + db.t_zuordnung_personen_unternehmen.c.geloescht.is_(None), + ) + + with db.ENGINE.connect() as conn: + ret = conn.execute(stmt) + + from_db = ret.mappings().all() + assert len(from_db) == 1, "not excactly one company initial recording obtained" + data_company_table = dict(from_db[0]) + + ids.pers_id = data_company_table.pop("pers_id") + ids.un_id = data_company_table["un_id"] + ids.an_id = data_company_table["an_id"] + data_company_table = _rename_db_data( + data_company_table, FIELD_DB_MAPPING_GRUNDERERFASSUNG_UNTERNEHMEN + ) + + # person + assert ids.pers_id, "person ID not set" + logger.debug("[InitRec -- backend] Person... Person ID: %s", ids.pers_id) + stmt = db.t_grunderfassung_personen.select().where( + db.t_grunderfassung_personen.c.pers_id == ids.pers_id ) with db.ENGINE.connect() as conn: ret = conn.execute(stmt) from_db = ret.mappings().all() assert len(from_db) == 1, "not excactly one company initial recording obtained" - data_from_db = dict(from_db[0]) + data_person_table = dict(from_db[0]) - geloescht = data_from_db["geloescht"] - del data_from_db["geloescht"] + data_person_table = _rename_db_data( + data_person_table, FIELD_DB_MAPPING_GRUNDERERFASSUNG_PERSONEN + ) + geloescht = data_person_table["geloescht"] + del data_person_table["geloescht"] + in_filter, out_filter = split_dict(data_person_table, FIELD_DB_SPLIT_METADATEN) + + form_data = Page_InitRec_Form_Data( + metadata=in_filter, + person=out_filter, + company=data_company_table, + ) + + if geloescht is not None: + logger.warning( + "[InitRec -- backend] Loaded entries which seem to be deleted - IDs: %s", ids + ) return InitRec( - ids=EntityIds(pers_id=id_), - # rec_id=id_, # TODO remove + ids=ids, + recording_type=recording_type, geloescht=geloescht, - db_data=data_from_db, + form_data=form_data, ) -# TODO check removal -def initrec_company_get_initial_recording( - id_: RecId, -) -> dict[str, Any]: - logger.debug("[Call backend] get_initial_recording") - stmt = db.grunderfassung_unternehmen.select().where( - db.grunderfassung_unternehmen.c.un_id == id_ - ) - with db.ENGINE.connect() as conn: - ret = conn.execute(stmt) - - results = ret.mappings().all() - if not results: - raise KeyError(f"Database ID {id_} not found") - - assert len(results) == 1, "more than one company initial recording obtained" - - row = results[0] - assert row, "row was not obtained" - - return dict(row) - - -# def initrec_company_delete_initial_recording( -# id_: RecId, -# ) -> None: -# logger.debug("[Call backend] delete_initial_recording") -# stmt = db.grunderfassung_unternehmen.delete().where( -# db.grunderfassung_unternehmen.c.un_id == id_ -# ) -# with db.ENGINE.begin() as conn: -# ret = conn.execute(stmt) - -# if ret.rowcount == 0: -# raise KeyError(f"Database ID {id_} not found for deletion") - - -# def initrec_person_insert_initial_recording( -# data: dict[str, Any], -# ) -> RecId: -# logger.debug("[Call backend] insert_initial_recording") -# stmt = db.grunderfassung_personen.insert() -# with db.ENGINE.begin() as conn: -# ret = conn.execute(stmt, data) - -# if ret.rowcount == 0: -# raise IOError("Entry was not inserted correctly") - -# prim_keys = ret.inserted_primary_key -# assert prim_keys - -# return prim_keys[0] - - -# def initrec_person_update_initial_recording( -# id_: RecId, -# data: dict[str, Any], -# ) -> None: -# logger.debug("[Call backend] update_initial_recording") -# stmt = db.grunderfassung_personen.update().where( -# db.grunderfassung_personen.c.pers_id == id_ -# ) -# with db.ENGINE.begin() as conn: -# conn.execute(stmt, data) - - -# def initrec_person_get_initial_recording( -# id_: RecId, -# ) -> dict[str, Any]: -# logger.debug("[Call backend] get_initial_recording person") -# stmt = db.grunderfassung_personen.select().where( -# db.grunderfassung_personen.c.pers_id == id_ -# ) -# with db.ENGINE.connect() as conn: -# ret = conn.execute(stmt) - -# results = ret.mappings().all() -# if not results: -# raise KeyError(f"Database ID {id_} not found") - -# assert len(results) == 1, "more than one person initial recording obtained" - -# row = results[0] -# assert row, "row was not obtained" - -# return dict(row) - - -# def initrec_person_delete_initial_recording( -# id_: RecId, -# ) -> None: -# logger.debug("[Call backend] delete_initial_recording") -# stmt = db.grunderfassung_personen.delete().where( -# db.grunderfassung_personen.c.pers_id == id_ -# ) -# with db.ENGINE.begin() as conn: -# ret = conn.execute(stmt) - -# if ret.rowcount == 0: -# raise KeyError(f"Database ID {id_} not found for deletion") - - @wrap_result(10) def page_consulting_to_db( consultation_data: Beratungsgespraech_Vorgang, @@ -444,7 +403,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 +419,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 +446,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 +456,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 +477,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 +498,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 +533,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() @@ -609,27 +572,24 @@ def page_consulting_linking_companies( ) -> list[LinkingConsultationsEntryCompany]: with db.ENGINE.connect() as conn: q_relevant_companies = sa.select( - db.grunderfassung_unternehmen.c.un_id, - db.grunderfassung_unternehmen.c.Partnersuche__un_suche, + db.t_unternehmen.c.un_id, ).where( - db.grunderfassung_unternehmen.c.geloescht.is_(None), - db.grunderfassung_unternehmen.c.Partnersuche__un_suche.is_not(None), + db.t_unternehmen.c.geloescht.is_(None), ) if un_id is not None: q_relevant_companies = q_relevant_companies.where( - db.grunderfassung_unternehmen.c.un_id == un_id + db.t_unternehmen.c.un_id == un_id ) q_relevant_companies = q_relevant_companies.subquery("relevant_companies") 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, - q_relevant_companies.c.Partnersuche__un_suche - == db.ext_crm_master_attach.c.ma_id, + db.t_ext_crm_master_attach, + q_relevant_companies.c.un_id == db.t_ext_crm_master_attach.c.ma_id, ) ) @@ -652,55 +612,49 @@ def page_consulting_linking_companies( return linking_entries -def page_consulting_linking_persons( - un_id: RecId | None, -) -> list[LinkingConsultationsEntryPerson]: +def page_consulting_linking_persons() -> list[LinkingConsultationsEntryPerson]: with db.ENGINE.connect() as conn: - # if un_id None: list all persons - # else: list just the persons associated with the company - - person_select = sa.select( - db.grunderfassung_personen.c.pers_id, - db.grunderfassung_personen.c.Stammdaten__name, - db.grunderfassung_personen.c.Stammdaten__vorname, - db.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, - ) - ).where(db.zuordnung_personen_unternehmen.c.un_id == un_id) - - ret = conn.execute(person_select) - person_names = ret.mappings().all() - if not person_names: - logger.warning(f"No persons found for 'un_id': {un_id}") - return [] - - logger.debug("[Page Consulting -- Linking] Retrieved person names:\n%s", person_names) - - linking_entries: list[LinkingConsultationsEntryPerson] = [] - for entry in person_names: - first_name = entry["Stammdaten__vorname"] - last_name = entry["Stammdaten__name"] - location_city = entry["Stammdaten__ort"] - - display_name = f"{first_name} {last_name}" - if location_city: - display_name += f" (Ort: {location_city})" - - linking_entries.append( - LinkingConsultationsEntryPerson( - pers_id=entry["pers_id"], - display_name=display_name, + stmt = ( + sa.select( + db.t_grunderfassung_personen.c.pers_id, + db.t_zuordnung_personen_unternehmen.c.un_id, + db.t_grunderfassung_personen.c.Stammdaten__name, + db.t_grunderfassung_personen.c.Stammdaten__vorname, + db.t_grunderfassung_personen.c.Stammdaten__ort, ) + .outerjoin( + db.t_zuordnung_personen_unternehmen, + db.t_grunderfassung_personen.c.pers_id + == db.t_zuordnung_personen_unternehmen.c.pers_id, + ) + .order_by(db.t_grunderfassung_personen.c.Stammdaten__name) ) - return linking_entries + ret = conn.execute(stmt) + + persons_map: dict[str, LinkingConsultationsEntryPerson] = {} + for row in ret: + p_id = row.pers_id + + if p_id not in persons_map: + first_name = row.Stammdaten__vorname + last_name = row.Stammdaten__name + location_city = row.Stammdaten__ort + + display_name = f"{first_name} {last_name}" + if location_city: + display_name += f" (Ort: {location_city})" + + persons_map[p_id] = LinkingConsultationsEntryPerson( + pers_id=p_id, + un_ids=set(), + display_name=display_name, + ) + + if row.un_id is not None: + persons_map[p_id].un_ids.add(row.un_id) + + return list(persons_map.values()) # TODO wrap result @@ -709,10 +663,9 @@ def page_consulting_eastablish_link( un_id: RecId | None, pers_id: RecId | None, ) -> 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) ) @@ -720,16 +673,19 @@ def page_consulting_eastablish_link( conn.execute(stmt) +# // company profile interaction def companyprofile_page_get_consultations( un_id: RecId, ) -> 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) @@ -743,11 +699,8 @@ def companyprofile_page_get_consultations( datetime_updated = cast(datetime.datetime, entry["aktualisiert"]) datetime_updated = datetime_updated.astimezone(TIMEZONE_CEST) - base_title = entry["titel"] - - _cons_type = entry["beratungs_typ"] - cons_type = ConsultingType(_cons_type) + cons_type = ConsultingType(entry["beratungs_typ"]) con_entry = CompanyProfileConsultationEntry( cons_id=cons_id, @@ -761,7 +714,7 @@ def companyprofile_page_get_consultations( elif cons_type is ConsultingType.INDIVIDUAL: cons_entries_individual.append(con_entry) else: - raise TypeError(f"Unknown consulting type: {_cons_type}") + raise TypeError(f"Unknown consulting type: {cons_type}") cons_entries_pauschal.sort(key=lambda x: x.date_updated, reverse=True) cons_entries_individual.sort(key=lambda x: x.date_updated, reverse=True) @@ -773,7 +726,7 @@ def companyprofile_page_get_consultations( # // initial recording interaction -# TODO wrap result +# TODO wrap result, add to corresponding component def initrec_link_person_company( un_id: RecId, pers_id: RecId, @@ -782,17 +735,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,85 +758,71 @@ def initrec_link_person_company( # // main page interaction -def _main_page_get_company_list() -> list[MainPageEntry]: - logger.debug("[Call backend] get_company_list") - stmt = sa.select( - db.grunderfassung_unternehmen.c.un_id, - db.grunderfassung_unternehmen.c.Partnersuche__un_suche, - db.grunderfassung_unternehmen.c.Metadaten_aktualisierung, - ).where(db.grunderfassung_unternehmen.c.geloescht.is_(None)) - - with db.ENGINE.connect() as conn: - res = conn.execute(stmt) - - main_page_companies: list[MainPageEntry] = [] - - for entry in res.mappings(): - rec_id = entry["un_id"] - assert rec_id, "no RecID defined" - ma_id_external = entry["Partnersuche__un_suche"] - assert ma_id_external is not None, "external MA ID is NULL" - datetime_akt = cast(datetime.datetime, entry["Metadaten_aktualisierung"]) - datetime_akt = datetime_akt.astimezone(TIMEZONE_CEST) - - comp_info = initrec_comp_search_get_info(ma_id_external) - display_name = comp_info["ma_unternehmensname"] - - main_page_companies.append( - MainPageEntry( - rec_id=rec_id, - display_name=display_name, - Metadaten_aktualisierung=datetime_akt, - type=EntityType.COMPANY, - ) - ) - - return main_page_companies - - -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)) - - with db.ENGINE.connect() as conn: - res = conn.execute(stmt) - - main_page_persons: list[MainPageEntry] = [] - - for entry in res.mappings(): - rec_id = entry["pers_id"] - assert rec_id, "no RecID defined" - datetime_akt = cast(datetime.datetime, entry["Metadaten_aktualisierung"]) - datetime_akt = datetime_akt.astimezone() - - surname: str = entry["Stammdaten__name"] - first_name: str = entry["Stammdaten__vorname"] - names_to_join = [n for n in (first_name, surname) if n] - display_name = " ".join(names_to_join) - - main_page_persons.append( - MainPageEntry( - rec_id=rec_id, - display_name=display_name, - Metadaten_aktualisierung=datetime_akt, - type=EntityType.PERSON, - ) - ) - - return main_page_persons - - def main_page_get_entries() -> list[MainPageEntry]: - companies = _main_page_get_company_list() - persons = _main_page_get_person_list() + with db.ENGINE.connect() as conn: + q_relevant_entries = ( + sa.select( + db.t_grunderfassung_personen.c.pers_id, + db.t_grunderfassung_personen.c.aktualisiert, + db.t_zuordnung_personen_unternehmen.c.id.label("link_id"), + db.t_zuordnung_personen_unternehmen.c.un_id, + db.t_zuordnung_personen_unternehmen.c.an_id, + db.t_grunderfassung_personen.c.Stammdaten__name, + db.t_grunderfassung_personen.c.Stammdaten__vorname, + db.t_grunderfassung_personen.c.Stammdaten__ort, + ) + .outerjoin( + db.t_zuordnung_personen_unternehmen, + db.t_grunderfassung_personen.c.pers_id + == db.t_zuordnung_personen_unternehmen.c.pers_id, + ) + .where(db.t_grunderfassung_personen.c.geloescht.is_(None)) + .order_by(db.t_grunderfassung_personen.c.Stammdaten__name) + ).subquery("relevant_entries") - all_entries = companies + persons + stmt = sa.select( + q_relevant_entries, + db.t_ext_crm_master_attach.c.ma_unternehmensname, + ).outerjoin( + db.t_ext_crm_master_attach, + q_relevant_entries.c.un_id == db.t_ext_crm_master_attach.c.ma_id, + ) - all_entries.sort(key=lambda x: x.Metadaten_aktualisierung, reverse=True) + ret = conn.execute(stmt) - return all_entries + main_page_entries: list[MainPageEntry] = [] + for entry in ret.mappings(): + ids = EntityIds( + pers_id=entry["pers_id"], + link_id=entry["link_id"], + un_id=entry["un_id"], + an_id=entry["an_id"], + ) + recording_type = RecordingType.WITHOUT_COMPANY + if ids.un_id or ids.link_id: + recording_type = RecordingType.WITH_COMPANY + + first_name = entry["Stammdaten__vorname"] + last_name = entry["Stammdaten__name"] + location_city = entry["Stammdaten__ort"] + person_name = f"{first_name} {last_name}" + if location_city: + person_name += f" (Ort: {location_city})" + + company_name = entry["ma_unternehmensname"] + + display_name = person_name + if recording_type is RecordingType.WITH_COMPANY: + display_name = company_name + + main_page_entries.append( + MainPageEntry( + ids=ids, + display_name=display_name, + dt_updated=entry["aktualisiert"], + recording_type=recording_type, + ) + ) + main_page_entries.sort(key=lambda x: x.dt_updated, reverse=True) + + return main_page_entries diff --git a/src/wce_crm/data_models.py b/src/wce_crm/data_models.py index 72a6c8a..4a3f578 100644 --- a/src/wce_crm/data_models.py +++ b/src/wce_crm/data_models.py @@ -7,6 +7,7 @@ import json from collections.abc import Iterable, Sequence from typing import TYPE_CHECKING, Annotated, Any, Final, Generic, Protocol, TypeVar +from dopt_basics.datastructures import DualDict from pydantic import ( AwareDatetime, BaseModel, @@ -18,12 +19,12 @@ from pydantic import ( ) from pydantic_core import ErrorDetails -from wce_crm.form_defs import FormField -from wce_crm.types import EntityIds +from wce_crm.form_defs import INITREC_COMP, INITREC_METADATEN, INITREC_PERSON, FormField +from wce_crm.types import ConsultingType, EntityIds, EntityType, RecordingType if TYPE_CHECKING: - 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, EntityType, RecId, UserId ValidAge = Annotated[int, Field(ge=0, le=99)] COLUMN_SEP: Final[str] = "__" @@ -123,36 +124,6 @@ class Module(WrapperModule, Protocol): def save_data(self) -> None: ... -# TODO check removal -# class AutoFormInsert(Protocol): -# def __call__( -# self, -# data: dict[str, Any], -# ) -> RecId: ... - - -# class AutoFormUpdate(Protocol): -# def __call__( -# self, -# id_: int, -# data: dict[str, Any], -# ) -> None: ... - - -# class AutoFormGet(Protocol): -# def __call__( -# self, -# id_: int, -# ) -> dict[str, Any]: ... - - -# class AutoFormDelete(Protocol): -# def __call__( -# self, -# id_: int, -# ) -> None: ... - - class InitRecFormToDb(Protocol): def __call__( self, @@ -163,7 +134,7 @@ class InitRecFormToDb(Protocol): class InitRecFormFromDb(Protocol): def __call__( self, - id_: int, + ids: EntityIds, ) -> InitRec: ... @@ -171,19 +142,24 @@ class InitRecFormFromDb(Protocol): def set_page_state( target: S, source: S, + ignore_attr: set[str] | None = None, deep: bool = False, ) -> None: """ 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") + if ignore_attr is None: + ignore_attr = set() + ignore_attr.add("child_modules") + for field in dc.fields(source): - if field.name == "child_modules": + if field.name in ignore_attr: continue new_value = getattr(source, field.name) @@ -212,50 +188,83 @@ 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 @dc.dataclass(slots=True, kw_only=True) -class InitRecFormConfig: - model: type[FlatBaseModel] - to_db: InitRecFormToDb - from_db: InitRecFormFromDb +class Page_InitRec_Form_Data: + metadata: dict[str, Any] + person: dict[str, Any] + company: dict[str, Any] + + +def _create_empty_initrec_form_data() -> Page_InitRec_Form_Data: + return Page_InitRec_Form_Data( + metadata={}, + person={}, + company={}, + ) + + +# !! there must be exactly one InitRecForm for each recording type +@dc.dataclass(slots=True, kw_only=True) +class Page_InitRec_State(PageState["InitRecForm"]): + session: Session + ids: EntityIds = dc.field(default_factory=EntityIds) + recording_type: RecordingType + # un_id: RecId | None = None + locked: bool + geloescht: bool = False + form_data: Page_InitRec_Form_Data = dc.field( + default_factory=_create_empty_initrec_form_data + ) + initrec_form_states: dict[EntityType, Page_InitRec_Form_State] = dc.field( + init=False, default_factory=dict + ) + + def __post_init__(self) -> None: + relevant_entities = frozenset( + ( + EntityType.METADATA, + EntityType.COMPANY, + EntityType.PERSON, + ) + ) + + for ent_type in relevant_entities: + initrec_form_state = Page_InitRec_Form_State( + session=self.session, + cfg=INITREC_CONFIGS[ent_type], + ids=self.ids, + ent_type=ent_type, + locked=self.locked, + geloescht=self.geloescht, + ) + self.initrec_form_states[ent_type] = initrec_form_state + + +@dc.dataclass(slots=True, kw_only=True) +class Page_InitRec_Form_Config: + pydantic_model: type[FlatBaseModel] form_fields: Sequence[FormField] ignored_keys: Iterable[str] = tuple() - add_buttons: bool = True + add_buttons: bool = False + id_mapping: dict[str, str] @dc.dataclass(slots=True, kw_only=True) -class InitRecForm_State(PageState[Module]): +class Page_InitRec_Form_State(PageState[Module]): session: Session - cfg: InitRecFormConfig + cfg: Page_InitRec_Form_Config ids: EntityIds ent_type: EntityType # 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 - - -@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 @dc.dataclass(slots=True, kw_only=True) class Page_Consulting_State(PageState[Module]): session: Session @@ -271,13 +280,13 @@ class Page_Consulting_State(PageState[Module]): locked: bool -# TODO change to entity IDs @dc.dataclass(slots=True, kw_only=True) class Page_Consulting_Linking_State(PageState[Module]): session: Session un_id: RecId | None = None pers_id: RecId | None = None beratungs_typ: ConsultingType + filter_un_ids: set[RecId] | None = None @dc.dataclass(slots=True, kw_only=True) @@ -308,8 +317,7 @@ class Page_Consulting_Table_State(PageState["Page_Consulting_ConsultingSession"] @dc.dataclass(slots=True, kw_only=True) class Page_CompanyProfile_State(PageState[Module]): session: Session - ids: EntityIds - # rec_id: RecId | None = None + un_id: RecId | None = None # // Pydantic models @@ -428,24 +436,36 @@ class FlatBaseModel(BaseModel): # ** InitRec class InitRec(BaseModel): ids: EntityIds - # rec_id: RecId | None + recording_type: RecordingType geloescht: AwareDatetime | None = None - db_data: dict[str, Any] + form_data: Page_InitRec_Form_Data -class Grunderfassung_Unternehmen(FlatBaseModel): - # default in SQLAlchemy with lambda and timezone-aware datetime - Metadaten_erstellung: AwareDatetime | None = None - Metadaten_aktualisierung: AwareDatetime | None = None # see above - Metadaten_nutzer: str | None - Grunderfassung_fallnummer: str - Grunderfassung_notiz: str | None +# mapping between initrec form field keys and database properties +FIELD_DB_MAPPING_GRUNDERERFASSUNG_PERSONEN: Final[DualDict[str, str]] = DualDict( + Metadaten_erstellung="erstellt", + Metadaten_aktualisierung="aktualisiert", +) - Partnersuche: Grunderfassung_PartnerSuche +FIELD_DB_MAPPING_GRUNDERERFASSUNG_UNTERNEHMEN: Final[DualDict[str, str]] = DualDict( + Partnersuche__un_suche="un_id", + Partnersuche__person_suche="an_id", +) + +FIELD_DB_SPLIT_METADATEN: Final[frozenset[str]] = frozenset( + ( + "Metadaten_erstellung", + "Metadaten_aktualisierung", + "Metadaten_nutzer", + "Metadaten_wiedereintrittsdatum", + "Grunderfassung_fallnummer", + "Grunderfassung_notiz", + ) +) -class Grunderfassung_Personen(FlatBaseModel): - # default in SQLAlchemy with lambda and timezone-aware datetime +class Grunderfassung_Metadaten(FlatBaseModel): + # displayed and handled separately, on database level included in 'Person' Metadaten_erstellung: AwareDatetime | None = None Metadaten_aktualisierung: AwareDatetime | None = None # see above Metadaten_nutzer: str | None @@ -453,6 +473,24 @@ class Grunderfassung_Personen(FlatBaseModel): Grunderfassung_fallnummer: str Grunderfassung_notiz: str | None + +class Grunderfassung_Unternehmen(FlatBaseModel): + # default in SQLAlchemy with lambda and timezone-aware datetime + # Metadaten_erstellung: AwareDatetime | None = None + # Metadaten_aktualisierung: AwareDatetime | None = None # see above + # Metadaten_nutzer: str | None + # Grunderfassung_fallnummer: str + # Grunderfassung_notiz: str | None + + Partnersuche: Grunderfassung_PartnerSuche + + +class Grunderfassung_Personen(FlatBaseModel): + # default in SQLAlchemy with lambda and timezone-aware datetime + # Metadaten_erstellung: AwareDatetime | None = None + # Metadaten_aktualisierung: AwareDatetime | None = None # see above + # Metadaten_nutzer: str | None + Projektrelevanz: Grunderfassung_Projektrelevanz Kontaktperson: Grunderfassung_Kontaktperson Stammdaten: Grunderfassung_Stammdaten @@ -468,7 +506,7 @@ class Grunderfassung_PartnerSuche(BaseModel): un_suche: int | None person_suche: int | None - kanal_aufmerksamkeit: str | None + kontaktweg: str | None class Grunderfassung_Projektrelevanz(BaseModel): @@ -496,6 +534,7 @@ class Grunderfassung_Kontaktperson(BaseModel): class Grunderfassung_Stammdaten(BaseModel): model_config = ConfigDict(str_strip_whitespace=True) + kanal_aufmerksamkeit: str | None titel: str | None anrede_anschrift: str name: str @@ -635,3 +674,38 @@ class Beratungsgespraech_Einzelgespraech(BaseModel): erstellt: AwareDatetime | None = Field(default=None, exclude=True) aktualisiert: AwareDatetime | None = Field(default=None, exclude=True) # see above geloescht: AwareDatetime | None = None + + +INITREC_CONFIGS: Final[dict[EntityType, Page_InitRec_Form_Config]] = { + EntityType.METADATA: Page_InitRec_Form_Config( + pydantic_model=Grunderfassung_Metadaten, + ignored_keys=( + "Metadaten_erstellung", + "Metadaten_aktualisierung", + ), + form_fields=INITREC_METADATEN, + add_buttons=False, + id_mapping={}, + ), + EntityType.COMPANY: Page_InitRec_Form_Config( + pydantic_model=Grunderfassung_Unternehmen, + ignored_keys=tuple(), + form_fields=INITREC_COMP, + add_buttons=False, + id_mapping={ + "Partnersuche__un_suche": "un_id", + "Partnersuche__person_suche": "an_id", + }, + ), + EntityType.PERSON: Page_InitRec_Form_Config( + pydantic_model=Grunderfassung_Personen, + ignored_keys=tuple(), + # ignored_keys=( + # "Metadaten_erstellung", + # "Metadaten_aktualisierung", + # ), + form_fields=INITREC_PERSON, + add_buttons=False, + id_mapping={}, + ), +} diff --git a/src/wce_crm/db.py b/src/wce_crm/db.py index af0734e..2b4b231 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,24 @@ 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_nutzer", sa.String(20), nullable=True), Column("Metadaten_wiedereintrittsdatum", sa.Date, nullable=True, default=None), Column("Arbeitserfahrung", sa.Text, nullable=True), @@ -361,15 +410,14 @@ grunderfassung_personen: Table = Table( Column("Kontaktperson__KP_funktion_beziehung", sa.Text, nullable=True), Column("Kontaktperson__KP_mobilfunknummer", sa.Text, nullable=True), Column("Kontaktperson__KP_name", sa.Text, nullable=True), - Column( - "Kontaktperson__KP_name_partner", sa.Text, nullable=True - ), # TODO: check if needed when set by trigger + Column("Kontaktperson__KP_name_partner", sa.Text, nullable=True), Column("Kontaktperson__KP_titel", sa.Text, nullable=True), Column("Kontaktperson__KP_vorname", sa.Text, nullable=True), Column("Projektrelevanz__relevanz", sa.Text, nullable=True), Column("Projektrelevanz__foerderperiode", sa.Text, nullable=True), Column("Schulbildung", sa.Text, nullable=True), Column("Sprachkenntnisse", sa.Text, nullable=True), + Column("Stammdaten__kanal_aufmerksamkeit", sa.Text, nullable=True), Column("Stammdaten__PLZ", sa.Text, nullable=True), Column("Stammdaten__anrede_anschrift", sa.Text, nullable=True), Column("Stammdaten__anzahl_kinder__alter", sa.Text, nullable=True), @@ -398,8 +446,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 +455,40 @@ 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("Partnersuche__kontaktweg", sa.Text, 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", @@ -439,8 +496,8 @@ beratung_vorgang: Table = Table( primary_key=True, autoincrement=True, ), - Column("un_id", sa.ForeignKey("grunderfassung_unternehmen.un_id"), nullable=True), - Column("pers_id", sa.ForeignKey("grunderfassung_personen.pers_id"), nullable=True), + Column("un_id", sa.ForeignKey("Unternehmen.un_id"), nullable=True), + Column("pers_id", sa.ForeignKey("Grunderfassung_Personen.pers_id"), nullable=True), Column("titel", sa.Text, nullable=False), Column("beratungs_typ", sa.String(20), nullable=False), Column( @@ -464,8 +521,8 @@ beratung_vorgang: Table = Table( ) -beratung_einzelberatung: Table = Table( - "beratung_einzelberatung", +t_beratung_einzelberatung: Table = Table( + "Beratung_Einzelberatung", MD_MAIN, Column( "beratung_id", @@ -473,7 +530,7 @@ beratung_einzelberatung: Table = Table( primary_key=True, autoincrement=True, ), - Column("vorgang_id", sa.ForeignKey("beratung_vorgang.vorgang_id"), nullable=False), + Column("vorgang_id", sa.ForeignKey("Beratung_Vorgang.vorgang_id"), nullable=False), Column("nutzer_id", sa.Integer, nullable=False), Column("nutzer_name", sa.String(20), nullable=False), Column( diff --git a/src/wce_crm/form_defs.py b/src/wce_crm/form_defs.py index 765f6e4..c0da687 100644 --- a/src/wce_crm/form_defs.py +++ b/src/wce_crm/form_defs.py @@ -187,7 +187,7 @@ COUNTRY_LIST: Final[CountryList] = get_country_list_german() GERMAN_STATE_LIST: Final[CountryList] = get_list_germany_states() -INITREC_COMP_SEARCH_HEAD = [ +_INITREC_COMP_SEARCH_HEAD = [ FormField( "Suche", FormFieldType.EXTENDED_DROPDOWN, @@ -307,53 +307,38 @@ INITREC_COMP_SEARCH_HEAD = [ info="an_position", ), FormField( - "Wie sind Sie auf uns aufmerksam geworden?", + "Kontaktweg", FormFieldType.DROPDOWN, required=False, - key="kanal_aufmerksamkeit", + key="kontaktweg", options=[ - ("Agentur für Arbeit", None), - ("Ausländerbehörde", None), - ("Jobcenter", None), - ("Freunde/Familie", None), - ("Anerkennungsstelle", None), - ("Beratungsstelle", None), - ("Internet", None), - ("Arbeitgeber", None), - ("Bildungsdienstleister", None), - ("Welcome-Mappe", None), - ("Newsletter WFE", None), - ("Newsletter RM", None), - ("Sonstiges", None), + ("Telefon", None), + ("Mail", None), + ("Persönliches Gespräch", None), ], ), ] -INITREC_CONTACT_PERSON = [ - # TODO check removal +_INITREC_CONTACT_PERSON = [ FormField( - "Name Unternehmen/Netzwerkpartner (vorausgefüllt von Suche)", + "Name Unternehmen/Netzwerkpartner", FormFieldType.TEXT, key="KP_name_partner", required=False, - placeholder="[TODO: Logik in Verbindung mit Unternehmen unklar]", - readonly=True, + readonly=False, ), FormField( "Titel", FormFieldType.TEXT, key="KP_titel", required=False, - tooltip=( - "* nur wenn anrufende Person oder kontaktaufnehmende Person " - "nicht die zu beratende Person ist" - ), ), FormField( - "Anrede_Anschrift", - FormFieldType.TEXT, + "Anrede", + FormFieldType.DROPDOWN, key="KP_anrede_anschrift", required=True, + options=[("Sehr geehrte Frau", None), ("Sehr geehrter Herr", None)], ), FormField( "Name", @@ -400,22 +385,40 @@ INITREC_CONTACT_PERSON = [ ] -INITREC_MASTER_DATA = [ +_INITREC_MASTER_DATA = [ + FormField( + "Wie sind Sie auf uns aufmerksam geworden?", + FormFieldType.DROPDOWN, + required=False, + key="kanal_aufmerksamkeit", + options=[ + ("Agentur für Arbeit", None), + ("Ausländerbehörde", None), + ("Jobcenter", None), + ("Freunde/Familie", None), + ("Anerkennungsstelle", None), + ("Beratungsstelle", None), + ("Internet", None), + ("Arbeitgeber", None), + ("Bildungsdienstleister", None), + ("Welcome-Mappe", None), + ("Newsletter WFE", None), + ("Newsletter RM", None), + ("Sonstiges", None), + ], + ), FormField( "Titel", FormFieldType.TEXT, key="titel", required=False, - tooltip=( - "* nur wenn anrufende Person oder kontaktaufnehmende Person " - "nicht die zu beratende Person ist" - ), ), FormField( "Anrede", - FormFieldType.TEXT, + FormFieldType.DROPDOWN, key="anrede_anschrift", required=True, + options=[("Sehr geehrte Frau", None), ("Sehr geehrter Herr", None)], ), FormField( "Name", @@ -427,17 +430,13 @@ INITREC_MASTER_DATA = [ "Vorname", FormFieldType.TEXT, key="vorname", - required=False, + required=True, ), FormField( "Geburtsdatum", - FormFieldType.DATE, + FormFieldType.DATE_FLEXIBLE, key="geburtsdatum", required=False, - tooltip=( - "* Wichtig zu erfragen, da u.a. Mindestgehaltsschwelle davon abhängt " - "(Regelung bei Ü45 Jahre)" - ), ), FormField( "Herkunftsland", @@ -446,7 +445,6 @@ INITREC_MASTER_DATA = [ required=True, placeholder="Suche...", options=COUNTRY_LIST.for_dropdown, - tooltip=("* Wichtig zu erfragen aufgrund eventueller EU-Freizügigkeitsregelung"), ), FormField( "Staatsangehörigkeit", @@ -455,7 +453,6 @@ INITREC_MASTER_DATA = [ required=False, placeholder="Suche...", options=COUNTRY_LIST.for_dropdown, - tooltip=("* Wichtig zu erfragen aufgrund eventueller EU-Freizügigkeitsregelung"), ), FormField( "Rückkehrer", @@ -463,7 +460,6 @@ INITREC_MASTER_DATA = [ key="rueckkehrer", required=False, options=[("ja", None), ("nein", None)], - tooltip=("* Wichtig zu erfragen aufgrund eventueller EU-Freizügigkeitsregelung"), ), FormField( "Wo befindet sich die Person?", @@ -502,7 +498,6 @@ INITREC_MASTER_DATA = [ key="bundesland", required=False, options=GERMAN_STATE_LIST.for_dropdown, - tooltip=("nur wenn Inland angegeben"), ), FormField( "Land", @@ -535,13 +530,11 @@ INITREC_MASTER_DATA = [ FormFieldType.TEXT, key="familienstand", required=False, - tooltip="* Wichtig zu erfragen aufgrund Lebensunterhaltssicherung", ), FormField( "Anzahl Kinder", FormFieldType.DYNAMIC_DROPDOWN_NUMERIC, required=False, - tooltip="* Wichtig zu erfragen aufgrund Lebensunterhaltssicherung", key="anzahl_kinder", children=[ FormField( @@ -549,7 +542,6 @@ INITREC_MASTER_DATA = [ FormFieldType.DROPDOWN, required=False, options=[(str(x), None) for x in range(11)], - tooltip="* Wichtig zu erfragen aufgrund Lebensunterhaltssicherung", key="anzahl", children=[ FormField("Alter Kind", FormFieldType.TEXT, key="alter"), @@ -560,7 +552,7 @@ INITREC_MASTER_DATA = [ ] -INITREC_ADDITIONAL_DATA = [ +_INITREC_ADDITIONAL_DATA = [ FormField( "Deutsch als Kommunikationssprache", FormFieldType.DROPDOWN, @@ -593,11 +585,10 @@ INITREC_ADDITIONAL_DATA = [ ("Chancenaufenthaltsrecht §104c AufenthG", None), ("Sonstiges", None), ], - tooltip="sofern nicht bekannt, unbedingt einfordern", ), FormField( "Gültigkeit Aufenthaltsstatus", - FormFieldType.DATE, + FormFieldType.DATE_FLEXIBLE, required=False, key="WI_gueltigkeit_aufenthaltstitel", ), @@ -635,7 +626,7 @@ INITREC_ADDITIONAL_DATA = [ ] -INITREC_SCHOOL = [ +_INITREC_SCHOOL = [ FormField("Abschluss", FormFieldType.TEXT, required=False, key="SB_abschluss"), FormField( "Abschlussgrad laut Dokument", @@ -673,7 +664,7 @@ INITREC_SCHOOL = [ ] -INITREC_HIGHER_EDUCATION = [ +_INITREC_HIGHER_EDUCATION = [ FormField( "Anerkennung", FormFieldType.TEXT, @@ -685,10 +676,6 @@ INITREC_HIGHER_EDUCATION = [ FormFieldType.TEXT, required=False, key="HB_abschlussgrad", - tooltip=( - "bitte den Titel eingeben z.B. Doktor, Diplom oder " - "Betriebswirt (Fachschulabschluss)" - ), ), FormField( "Abschlussgrad laut Dokument", @@ -707,10 +694,6 @@ INITREC_HIGHER_EDUCATION = [ FormFieldType.TEXT, required=False, key="HB_beruf", - tooltip=( - "bitte spezifizieren z.B. Allgemeinmedizin, Ingenieur Maschinenbau, " - "technischer Betriebswirt Datenverarbeitung" - ), ), FormField( "Land", @@ -737,12 +720,11 @@ INITREC_HIGHER_EDUCATION = [ FormFieldType.TEXT, required=False, key="HB_bemerkungsfeld", - tooltip="z.B. Promotionen oder den Studiengang angeben", ), ] -INITREC_WORK_EXPERIENCE = [ +_INITREC_WORK_EXPERIENCE = [ FormField( "Branche", FormFieldType.DROPDOWN, @@ -824,7 +806,6 @@ INITREC_WORK_EXPERIENCE = [ ("Teilzeit", None), ("Sonstiges", None), ], - tooltip="Minijob, Praktikum, Wehrdienst, soziale Dienste", ), FormField( "Bemerkungsfeld", @@ -835,7 +816,7 @@ INITREC_WORK_EXPERIENCE = [ ] -INITREC_LANGUAGES = [ +_INITREC_LANGUAGES = [ FormField( "Sprache", FormFieldType.TEXT, @@ -877,7 +858,7 @@ INITREC_LANGUAGES = [ ), FormField( "Datum des Nachweises", - FormFieldType.DATE, + FormFieldType.DATE_FLEXIBLE, required=False, key="SP_datum_nachweis", ), @@ -889,52 +870,16 @@ INITREC_LANGUAGES = [ INITREC_COMP = [ - FormField( - "Ersteintrag Datum", - FormFieldType.TEXT_DATETIME, - required=False, - key="Metadaten_erstellung", - readonly=True, - ignore_get_data=True, - ), - FormField( - "Aktualisierung Datum", - FormFieldType.TEXT_DATETIME, - required=False, - key="Metadaten_aktualisierung", - readonly=True, - ignore_get_data=True, - ), - FormField( - "Aktualisierung Nutzer", - FormFieldType.TEXT, - required=False, - key="Metadaten_nutzer", - readonly=True, - ignore_get_data=True, - ), - FormField( - "Fallnummer", - FormFieldType.TEXT, - required=True, - key="Grunderfassung_fallnummer", - ), - FormField( - "Notizen", - FormFieldType.LONGTEXT, - required=False, - key="Grunderfassung_notiz", - ), FormField( "Suche", FormFieldType.CUSTOM, custom_widget="grunderfassung_unternehmen_suche", key="Partnersuche", - children=INITREC_COMP_SEARCH_HEAD, + children=_INITREC_COMP_SEARCH_HEAD, ), ] -INITREC_PERSON = [ +INITREC_METADATEN = [ FormField( "Ersteintrag Datum", FormFieldType.TEXT_DATETIME, @@ -957,11 +902,11 @@ INITREC_PERSON = [ required=False, key="Metadaten_nutzer", readonly=True, - ignore_get_data=True, + ignore_get_data=False, ), FormField( "Wiedereintrittsdatum", - FormFieldType.DATE, + FormFieldType.DATE_FLEXIBLE, required=False, key="Metadaten_wiedereintrittsdatum", readonly=False, @@ -978,6 +923,9 @@ INITREC_PERSON = [ required=False, key="Grunderfassung_notiz", ), +] + +INITREC_PERSON = [ FormField( "Status && Projektrelevanz", FormFieldType.GROUP, @@ -1009,42 +957,42 @@ INITREC_PERSON = [ "Daten Kontaktperson", FormFieldType.GROUP, key="Kontaktperson", - children=INITREC_CONTACT_PERSON, + children=_INITREC_CONTACT_PERSON, ), FormField( "Stammdaten", FormFieldType.GROUP, key="Stammdaten", - children=INITREC_MASTER_DATA, + children=_INITREC_MASTER_DATA, ), FormField( "Weitere Informationen", FormFieldType.GROUP, key="WeitereInfos", - children=INITREC_ADDITIONAL_DATA, + children=_INITREC_ADDITIONAL_DATA, ), FormField( "Schulbildung", FormFieldType.DYNAMIC_LIST, - children=INITREC_SCHOOL, + children=_INITREC_SCHOOL, key="Schulbildung", ), FormField( "Studium/Ausbildung", FormFieldType.DYNAMIC_LIST, - children=INITREC_HIGHER_EDUCATION, + children=_INITREC_HIGHER_EDUCATION, key="HoehereBildung", ), FormField( "Arbeitserfahrung", FormFieldType.DYNAMIC_LIST, - children=INITREC_WORK_EXPERIENCE, + children=_INITREC_WORK_EXPERIENCE, key="Arbeitserfahrung", ), FormField( "Sprachkenntnisse", FormFieldType.DYNAMIC_LIST, - children=INITREC_LANGUAGES, + children=_INITREC_LANGUAGES, key="Sprachkenntnisse", ), ] diff --git a/src/wce_crm/gui.py b/src/wce_crm/gui.py index 878f6ea..2cd2a18 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, ) @@ -86,36 +87,32 @@ from wce_crm.data_models import ( COLUMN_SEP, Beratungsgespraech_Einzelgespraech, Beratungsgespraech_Vorgang, - 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_Data, + Page_InitRec_Form_State, + Page_InitRec_State, Page_MainPage_State, - Page_NewInitRec_State, + Page_NewEntry_State, Session, set_page_state, translate_pydantic_errors, ) from wce_crm.form_defs import ( - INITREC_COMP, - INITREC_PERSON, FormField, 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, @@ -123,13 +120,13 @@ from wce_crm.types import ( EntityIds, EntityType, RecId, + RecordingType, ) if TYPE_CHECKING: from wce_crm.data_models import ( - InitRecForm_State, Module, - WrapperModule, + Page_InitRec_Form_State, ) from wce_crm.types import ( ConsId, @@ -865,13 +862,13 @@ def set_widget_value( raise ValueError(f"Could not parse date field value >{value}<") widget.setDate(set_date) elif isinstance(widget, FlexibleDateInput): - assert isinstance(value, datetime.datetime) + assert isinstance(value, (datetime.datetime, datetime.date)) or value is None try: widget.set_pydate(value, raise_error=True) except ValueError: raise ValueError(f"Could not parse date field value >{value}<") elif isinstance(widget, FlexibleDateTimeInput): - assert isinstance(value, datetime.datetime) + assert isinstance(value, datetime.datetime) or value is None try: widget.set_pydatetime(value, raise_error=True) except ValueError: @@ -1145,15 +1142,15 @@ class Grunderfassung_SuchWidget(CustomFormWidget): self.person_search_input = cast(QComboBox, lookup[0]["widget"]) self.person_search_input.currentIndexChanged.connect(self._selected_person) - self.CHANNEL_AWARENESS_KEY = "kanal_aufmerksamkeit" - lookup = search_widgets_by_key(self.widget_registry, self.CHANNEL_AWARENESS_KEY) + self.CHANNEL_CONTACT_KEY = "kontaktweg" + lookup = search_widgets_by_key(self.widget_registry, self.CHANNEL_CONTACT_KEY) assert len(lookup) == 1 - self.channel_awareness = cast(QComboBox, lookup[0]["widget"]) + self.channel_contact = cast(QComboBox, lookup[0]["widget"]) self.DATA_EXPORT_FIELDS = ( self.COMPANY_SEARCH_INPUT_KEY, self.PERSON_SEARCH_INPUT_KEY, - self.CHANNEL_AWARENESS_KEY, + self.CHANNEL_CONTACT_KEY, ) self.company_widgets: dict[str, QWidget] = {} @@ -1237,7 +1234,7 @@ class Grunderfassung_SuchWidget(CustomFormWidget): def _clear_company_fields(self) -> None: self.person_search_input.clear() - self.channel_awareness.setCurrentIndex(0) + self.channel_contact.setCurrentIndex(0) for widget in self.company_widgets.values(): widget = cast(QLineEdit, widget) widget.clear() @@ -1274,7 +1271,7 @@ class Grunderfassung_SuchWidget(CustomFormWidget): if ma_id is None or index == (-1): self._clear_company_fields() return - data = backend.initrec_comp_search_get_info( + data = backend.external_company_get_info( ma_id=ma_id, ) self.fill_out_company(data) @@ -1379,11 +1376,11 @@ def search_widgets_by_key( class InitRecForm(QWidget): """a widget, which is managed by a code-defined field definition collection""" - update_triggered = Signal() # formular saved (data changed for front page) + # update_triggered = Signal() # formular saved (data changed for front page) def __init__( self, - state: InitRecForm_State, + state: Page_InitRec_Form_State, ) -> None: super().__init__() self.STATE = state @@ -1426,54 +1423,54 @@ class InitRecForm(QWidget): self.main_layout = QVBoxLayout(self) self.main_layout.setContentsMargins(0, 0, 0, 0) - if DEBUG: - separator1 = QFrame() - separator1.setFrameShape(QFrame.Shape.HLine) - separator1.setFrameShadow(QFrame.Shadow.Sunken) - self.main_layout.addWidget(separator1) + # if DEBUG: + # separator1 = QFrame() + # separator1.setFrameShape(QFrame.Shape.HLine) + # separator1.setFrameShadow(QFrame.Shadow.Sunken) + # self.main_layout.addWidget(separator1) - self.registry_button = QPushButton("Ausgabe Widget Registry") - self.registry_button.clicked.connect(self._print_registry) - self.registry_button.setFixedHeight(35) - self.main_layout.addWidget(self.registry_button) + # self.registry_button = QPushButton("Ausgabe Widget Registry") + # self.registry_button.clicked.connect(self._print_registry) + # self.registry_button.setFixedHeight(35) + # self.main_layout.addWidget(self.registry_button) - self.test_button = QPushButton("Initialisiere Laden") - self.test_button.clicked.connect(self._load_from_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) - self.main_layout.addWidget(button_get) - button_set = QPushButton("SET DATA") - button_set.setFixedHeight(35) - button_set.clicked.connect(self._set_form_data) - self.main_layout.addWidget(button_set) + # button_get = QPushButton("GET DATA") + # button_get.setFixedHeight(35) + # button_get.clicked.connect(self._get_data) + # self.main_layout.addWidget(button_get) + # button_set = QPushButton("SET DATA") + # button_set.setFixedHeight(35) + # button_set.clicked.connect(self._set_data) + # self.main_layout.addWidget(button_set) - id_field_layout = QHBoxLayout() - id_field_layout.setContentsMargins(0, 0, 0, 0) - id_field_layout.setSpacing(5) - id_field_label = QLabel("ID Datenbank:") - self.id_field_input = QLineEdit() - id_field_layout.addWidget(id_field_label) - id_field_layout.addWidget(self.id_field_input) - self.main_layout.addLayout(id_field_layout) - button_db_index = QPushButton("Setze DB Index") - button_db_index.setFixedHeight(35) - button_db_index.clicked.connect(self._set_db_index) - self.main_layout.addWidget(button_db_index) + # id_field_layout = QHBoxLayout() + # id_field_layout.setContentsMargins(0, 0, 0, 0) + # id_field_layout.setSpacing(5) + # id_field_label = QLabel("ID Datenbank:") + # self.id_field_input = QLineEdit() + # id_field_layout.addWidget(id_field_label) + # id_field_layout.addWidget(self.id_field_input) + # self.main_layout.addLayout(id_field_layout) + # button_db_index = QPushButton("Setze DB Index") + # button_db_index.setFixedHeight(35) + # button_db_index.clicked.connect(self._set_db_index) + # self.main_layout.addWidget(button_db_index) - separator2 = QFrame() - separator2.setFrameShape(QFrame.Shape.HLine) - separator2.setFrameShadow(QFrame.Shadow.Sunken) - self.main_layout.addWidget(separator2) - self.main_layout.addSpacing(5) + # separator2 = QFrame() + # separator2.setFrameShape(QFrame.Shape.HLine) + # separator2.setFrameShadow(QFrame.Shadow.Sunken) + # self.main_layout.addWidget(separator2) + # self.main_layout.addSpacing(5) - self.main_layout.addSpacing(10) + # self.main_layout.addSpacing(10) # buttons (save and reset) self.add_buttons = self.cfg.add_buttons @@ -1489,11 +1486,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,134 +1509,153 @@ 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), ) def _print_registry(self) -> None: pprint_registry(self.widget_registry) - def _set_db_index(self) -> None: - try: - index = int(self.id_field_input.text()) - except ValueError: - index = None - self.STATE.ids.un_id = index - self.STATE.ids.pers_id = index - logger_gui.debug("[Auto-Form] Set index to %s, new state: %s", index, self.STATE) + # def _set_db_index(self) -> None: + # try: + # index = int(self.id_field_input.text()) + # except ValueError: + # index = None + # self.STATE.ids.un_id = index + # self.STATE.ids.pers_id = index + # logger_gui.debug("[InitRecForm] Set index to %s, new state: %s", index, self.STATE) - def _disable_save(self) -> None: - assert self.save_btn is not None - 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.ent_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")), + # # 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 _load_state_form_data(self) -> None: + logger_initrecform.info("[InitRecForm] Load data method...") + logger_initrecform.debug( + "[InitRecForm] Loaded data dict:\n%s Passing to Pydantic...", pformat(self.STATE.form_data), ) + # TODO error catching? + logger_initrecform.info( + "[InitRecForm] Pydantic model name: %s", + self.STATE.cfg.pydantic_model.__name__, + ) + + model = self.cfg.pydantic_model(**self.STATE.form_data) + logger_initrecform.debug("[InitRecForm] Loaded to Pydantic.") + logger_initrecform.debug("[InitRecForm] Convert to GUI structure...") + form_data_gui = model.to_gui() + self._set_data(form_data_gui) def reset_form(self) -> None: reset_form(self.widget_registry) - self.STATE.ids.set_id(None, self.STATE.ent_type) - # TODO remove - # self.STATE.rec_id = None - self.STATE.form_data = None + self.STATE.ids.update(EntityIds()) + 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 +1665,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_state_form_data() else: self.reset_form() @@ -1664,12 +1682,17 @@ 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 + + for form_key, id_key in self.STATE.cfg.id_mapping.items(): + self.STATE.ids.set_by_name(id_key, form_data[form_key]) + self.STATE.form_data = form_data def validate(self) -> list[str]: @@ -1677,135 +1700,139 @@ 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 - def _load_from_pydantic( - self, - data: InitRec, - ) -> None: - ent_type = self.STATE.ent_type - self.STATE.ids.set_id(data.ids.get_id(ent_type), ent_type) - # self.STATE.rec_id = data.rec_id - self.STATE.form_data = None - self.STATE.geloescht = True if data.geloescht else False + # def _load_from_pydantic( + # self, + # data: InitRec, + # ) -> None: + # # ent_type = self.STATE.recording_type + # # self.STATE.ids.set_id(data.ids.get_id(ent_type), ent_type) + # self.STATE.ids.update(data.ids) + # # self.STATE.rec_id = data.rec_id + # self.STATE.form_data.clear() + # self.STATE.geloescht = True if data.geloescht else False - self._sync_state_to_GUI(None) # TODO change to correct loading + # self._sync_state_to_GUI(None) # TODO change to correct loading + # !! must be part of the managing page def save_data(self) -> None: - 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): @@ -1978,9 +2005,13 @@ class FlexibleDateInput(QWidget): def set_pydate( self, - date: datetime.datetime, + date: datetime.datetime | datetime.date | None, raise_error: bool = False, ) -> None: + if date is None: + self.reset() + return + date_to_set = QDate(date.year, date.month, date.day) if raise_error and not date_to_set.isValid(): raise ValueError("Could not parse date object correctly") @@ -2197,9 +2228,13 @@ class FlexibleDateTimeInput(QWidget): def set_pydatetime( self, - dt: datetime.datetime, + dt: datetime.datetime | None, raise_error: bool = False, ) -> None: + if dt is None: + self.reset() + return + dt_to_set = QDateTime(dt.year, dt.month, dt.day, dt.hour, dt.minute, 0) if raise_error and not dt_to_set.isValid(): raise ValueError("Could not parse datetime object correctly") @@ -2867,79 +2902,118 @@ 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" - consulting_requested = Signal(Page_Consulting_State) # Signal "Beratungsgespräch" + # TODO re-enable with correct settings + # company_requested = Signal(Page_InitRec_State) # Signal "Unternehmen" + # person_requested = Signal(Page_InitRec_State) # Signal "Individualperson" + new_consulting_requested = Signal(Page_Consulting_State) # Signal "Beratungsgespräch" + new_initrec_requested = Signal(Page_InitRec_State) # TODO: rename def __init__( self, - state: Page_NewInitRec_State, + state: Page_NewEntry_State, ): super().__init__() self.STATE = state - layout = QVBoxLayout(self) - layout.setAlignment(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft) + outer_layout = QHBoxLayout(self) + layout_container = QWidget() + layout_container.setMinimumWidth(700) + layout_container.setMaximumWidth(1000) + layout = QVBoxLayout(layout_container) + + outer_layout.addStretch(1) + outer_layout.addWidget(layout_container, stretch=100) + # outer_layout.addWidget(scroll_area, stretch=100) + outer_layout.addStretch(1) + outer_layout.setAlignment(Qt.AlignmentFlag.AlignTop) - # Zurück-Button back_btn = QPushButton("← Zurück zur Übersicht") back_btn.setFixedWidth(200) back_btn.clicked.connect(lambda: self.back_requested.emit()) - # Platzhalter für die Details self.title_label = QLabel("Wählen Sie den Typ der Grunderfassung") self.title_label.setStyleSheet( "font-size: 24px; font-weight: bold; margin-top: 20px;" ) + title_initrec = QLabel("Grunderfassung anlegen") + title_initrec.setStyleSheet( + "font-size: 16px; font-weight: bold; margin-top: 20px; margin-bottom: 5px;" + ) btn_company = QPushButton("Unternehmen →") btn_company.setFixedWidth(300) btn_company.setFixedHeight(40) btn_company.clicked.connect(self._request_initrec_company) - btn_person = QPushButton("Individualperson →") btn_person.setFixedWidth(300) btn_person.setFixedHeight(40) btn_person.clicked.connect(self._request_initrec_person) + title_consulting = QLabel("Beratung durchführen") + title_consulting.setStyleSheet( + "font-size: 16px; font-weight: bold; margin-top: 20px; margin-bottom: 5px;" + ) btn_consulting = QPushButton("Beratungsgespräch →") btn_consulting.setFixedWidth(300) btn_consulting.setFixedHeight(40) btn_consulting.clicked.connect(self._request_consulting) + # TODO check removal + # btn_newinitrec = QPushButton("NEUE GRUNDERFASSUNG →") + # btn_newinitrec.setFixedWidth(300) + # btn_newinitrec.setFixedHeight(40) + # btn_newinitrec.clicked.connect(self._debug_request_new_initrec) + layout.addWidget(back_btn) layout.addSpacing(15) layout.addWidget(self.title_label) + # layout.addSpacing(20) + # layout.addWidget(btn_newinitrec) + # layout.addSpacing(20) + layout.addWidget(title_initrec) layout.addWidget(btn_company) layout.addWidget(btn_person) layout.addSpacing(10) + layout.addWidget(title_consulting) layout.addWidget(btn_consulting) + # TODO configure correctly def _request_initrec_company(self) -> None: - ids = EntityIds(type=EntityType.COMPANY) - req_state = Page_InitRecCompany_State( + logger_gui.debug("[Page -- NewEntry] Call for company...") + logger_gui.debug("[Page -- NewEntry] Call for person...") + req_state = Page_InitRec_State( session=self.STATE.session, - ids=ids, - # un_id=None, # TODO remove + recording_type=RecordingType.WITH_COMPANY, + form_data=Page_InitRec_Form_Data(metadata={}, person={}, company={}), locked=False, ) - logger_gui.debug("[Page -- InitRec] State to call: %s", req_state) - self.company_requested.emit(req_state) + logger_gui.debug("[Page -- NewEntry] State to call (IDs): %s", req_state.ids) + self.new_initrec_requested.emit(req_state) def _request_initrec_person(self) -> None: - ids = EntityIds(type=EntityType.PERSON) - req_state = Page_InitRecPerson_State( + logger_gui.debug("[Page -- NewEntry] Call for person...") + req_state = Page_InitRec_State( session=self.STATE.session, - ids=ids, - # pers_id=None, # TODO remove + recording_type=RecordingType.WITHOUT_COMPANY, + form_data=Page_InitRec_Form_Data(metadata={}, person={}, company={}), locked=False, ) - logger_gui.debug("[Page -- InitRec] State to call: %s", req_state) - self.person_requested.emit(req_state) + logger_gui.debug("[Page -- NewEntry] State to call (IDs): %s", req_state.ids) + self.new_initrec_requested.emit(req_state) + + def _debug_request_new_initrec(self) -> None: + req_state = Page_InitRec_State( + session=self.STATE.session, + recording_type=RecordingType.WITH_COMPANY, + form_data=Page_InitRec_Form_Data(metadata={}, person={}, company={}), + locked=False, + ) + logger_gui.debug("[Page -- NewEntry] State to call (IDs): %s", req_state.ids) + self.new_initrec_requested.emit(req_state) def _request_consulting(self) -> None: req_state = Page_Consulting_State( @@ -2949,7 +3023,7 @@ class Page_NewInitRec(QWidget): locked=False, ) logger_gui.debug("[Page -- InitRec] State to call: %s", req_state) - self.consulting_requested.emit(req_state) + self.new_consulting_requested.emit(req_state) def _sync_state_to_GUI(self) -> None: pass @@ -2962,12 +3036,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,47 +3049,102 @@ 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, - ignored_keys=( - "Metadaten_erstellung", - "Metadaten_aktualisierung", - ), - form_fields=INITREC_COMP, -) +# 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, - ignored_keys=( - "Metadaten_erstellung", - "Metadaten_aktualisierung", - ), - form_fields=INITREC_PERSON, -) +# CONFIG_GRUNDERFASSUNG_PERSONEN: Final[Page_InitRec_Form_Config] = Page_InitRec_Form_Config( +# pydantic_model=Grunderfassung_Personen, +# # to_db=backend.initrec_person_to_db, # TODO remove +# # from_db=backend.initrec_person_from_db, # TODO remove +# ignored_keys=( +# "Metadaten_erstellung", +# "Metadaten_aktualisierung", +# ), +# form_fields=INITREC_PERSON, +# add_buttons=False, +# ) CUSTOM_WIDGETS: Final[dict[str, type[CustomFormWidget]]] = { "grunderfassung_unternehmen_suche": Grunderfassung_SuchWidget, } -class Page_InitRecCompany(QWidget): +class CollapsibleBox(QWidget): + def __init__(self, title="", parent=None): + super().__init__(parent) + + # 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, + layout: QLayout, + ): + self.content_area.setLayout(layout) + + def setContentWidget( + self, + widget: QWidget, + ): + layout = QVBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) # Keine zusätzlichen Ränder + layout.addWidget(widget) + self.setContentLayout(layout) + + def set_open( + self, + open: bool, + ) -> None: + self._on_toggle(open) + + +class Page_InitRec(QWidget): back_main_requested = Signal() # back to main page back_requested = Signal() # back button - update_main_page = Signal() # form saved (data changed for front page) + update_triggered = 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 def __init__( self, - state: Page_InitRecCompany_State, + state: Page_InitRec_State, ): super().__init__() self.STATE = state - # Hauptlayout der Seite outer_layout = QHBoxLayout(self) vert_layout = QVBoxLayout() @@ -3046,32 +3175,65 @@ class Page_InitRecCompany(QWidget): back_btn_step.setMinimumWidth(200) back_btn_step.setMaximumWidth(200) - title = QLabel("Grunderfassung Unternehmen") + if self.STATE.recording_type is RecordingType.WITH_COMPANY: + title = QLabel("Grunderfassung Unternehmen") + elif self.STATE.recording_type is RecordingType.WITHOUT_COMPANY: + title = QLabel("Grunderfassung Individualperson") + else: + raise RuntimeError(f"Unspecified recording type {self.STATE.recording_type}") title.setStyleSheet("font-size: 20px; font-weight: bold;") + header_layout.setSpacing(5) + header_layout.addWidget(back_btn_step) + header_layout.addWidget(back_btn_main) + header_layout.addWidget(title) + self.comp_profile_btn = QPushButton("Unternehmensprofil") self.comp_profile_btn.clicked.connect(self._request_company_profile) self.comp_profile_btn.setSizePolicy( QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed ) self.comp_profile_btn.setFixedHeight(50) - - 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) + if DEBUG: + separator1 = QFrame() + separator1.setFrameShape(QFrame.Shape.HLine) + separator1.setFrameShadow(QFrame.Shadow.Sunken) + header_layout.addWidget(separator1) + + btn_gui2state = QPushButton("Sync GUI to state") + btn_gui2state.clicked.connect(self._sync_GUI_to_state) + btn_gui2state.setFixedHeight(35) + btn_gui2state.setSizePolicy( + QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed + ) + header_layout.addWidget(btn_gui2state) + + btn_state2gui = QPushButton("Sync state to GUI") + btn_state2gui.clicked.connect(self._sync_state_to_GUI) + btn_state2gui.setFixedHeight(35) + btn_state2gui.setSizePolicy( + QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed + ) + header_layout.addWidget(btn_state2gui) + + btn_print_state = QPushButton("Print State") + btn_print_state.clicked.connect(self._debug_print_state) + btn_print_state.setFixedHeight(35) + btn_print_state.setSizePolicy( + QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed + ) + header_layout.addWidget(btn_print_state) + + separator2 = QFrame() + separator2.setFrameShape(QFrame.Shape.HLine) + separator2.setFrameShadow(QFrame.Shadow.Sunken) + header_layout.addWidget(separator2) + header_layout.addSpacing(5) + # --- BANNER --- self.info_banners_container = QWidget() self.info_banners: list[InfoBanner] = [] @@ -3080,6 +3242,25 @@ class Page_InitRecCompany(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 @@ -3094,259 +3275,51 @@ class Page_InitRecCompany(QWidget): vert_layout.addWidget(scroll_area) - container_layout = QVBoxLayout(container) - container_layout.setContentsMargins(0, 0, 0, 0) + self.container_layout = QVBoxLayout(container) + self.container_layout.setContentsMargins(0, 0, 0, 0) # --- AUTO FORM LAYOUT --- - 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) + # now we try to render the layout within the collapsable box + self.container_layout.addSpacing(20) - container_layout.addSpacing(15) + # ** metadata + self.initrec_form_metadata_box = CollapsibleBox("Metadaten") + ent_type = EntityType.METADATA + self.initrec_form_metadata = InitRecForm(self.STATE.initrec_form_states[ent_type]) + + assert self.STATE.initrec_form_states[ent_type] is self.initrec_form_metadata.STATE + + self.STATE.child_modules.append(self.initrec_form_metadata) + self.initrec_form_metadata_box.setContentWidget(self.initrec_form_metadata) + self.container_layout.addWidget(self.initrec_form_metadata_box) # --- CUSTOM LOGIC --- - # ** fill 'Kontaktperson -> Namen Unternehmen' - search_res = search_widgets_by_key(self.auto_form.widget_registry, "Partnersuche") + # ** 'Wiedereintrittsdatum' -> only relevant without company + search_res = search_widgets_by_key( + self.initrec_form_metadata.widget_registry, "wiedereintrittsdatum" + ) + print(f">>>> {pformat(search_res)}") assert len(search_res) == 1 - search_widget = cast(Grunderfassung_SuchWidget, search_res[0]["widget"]) - self.search_widget_trigger = cast( - QLineEdit, search_widget.company_widgets["ma_unternehmensname"] - ) + field_wiedereintrittsdatum = search_res[0]["widget"] + assert isinstance(field_wiedereintrittsdatum, FlexibleDateInput) + self.field_wiedereintrittsdatum = field_wiedereintrittsdatum - 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() + # ** person + self.initrec_form_person_box = CollapsibleBox("Angaben zur Person") + ent_type = EntityType.PERSON + self.initrec_form_person = InitRecForm(self.STATE.initrec_form_states[ent_type]) - def _add_info_banner( - 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") + assert self.STATE.initrec_form_states[ent_type] is self.initrec_form_person.STATE - 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 - - -class Page_InitRecPerson(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, - ): - super().__init__() - self.STATE = state - # 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 Individualperson") - title.setStyleSheet("font-size: 20px; font-weight: bold;") - - header_layout.setSpacing(5) - header_layout.addWidget(back_btn_step) - header_layout.addWidget(back_btn_main) - header_layout.addWidget(title) - 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.AUTO_FORM_CFG = CONFIG_GRUNDERFASSUNG_PERSONEN - auto_form_state = InitRecForm_State( - session=self.STATE.session, - cfg=self.AUTO_FORM_CFG, - ids=self.STATE.ids, - ent_type=EntityType.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.STATE.child_modules.append(self.initrec_form_person) + self.initrec_form_person_box.setContentWidget(self.initrec_form_person) + self.container_layout.addWidget(self.initrec_form_person_box) # --- 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 +3327,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 +3335,71 @@ class Page_InitRecPerson(QWidget): self.selection_county.setProperty("styleClass", "stempel") self.selection_county.setEnabled(False) + # ** company + ent_type = EntityType.COMPANY + self.initrec_form_company_box = CollapsibleBox("Angaben zum Unternehmen") + self.initrec_form_company = InitRecForm(self.STATE.initrec_form_states[ent_type]) + self.STATE.child_modules.append(self.initrec_form_company) + + assert self.STATE.initrec_form_states[ent_type] is self.initrec_form_company.STATE + + target_idx = self.container_layout.indexOf(self.initrec_form_person_box) + self.initrec_form_company_box.setContentWidget(self.initrec_form_company) + self.container_layout.insertWidget(target_idx, self.initrec_form_company_box) + + # --- CUSTOM LOGIC --- + # ** fill 'Kontaktperson -> Namen Unternehmen' + search_res = search_widgets_by_key( + self.initrec_form_company.widget_registry, "Partnersuche" + ) + assert len(search_res) == 1 + search_widget = cast(Grunderfassung_SuchWidget, search_res[0]["widget"]) + self.search_widget_trigger = cast( + QLineEdit, search_widget.company_widgets["ma_unternehmensname"] + ) + + self.container_layout.addStretch() + self._update_layout() + + def _debug_print_state(self) -> None: + logger_initrecform.info( + "[Page -- InitRec] Printing state IDs:\n%s", pformat(self.STATE.ids) + ) + + def _request_company_profile(self) -> None: + assert self.STATE.ids.un_id is not None, ( + "requested company profile for a company which is not known (no ID)" + ) + req_state = Page_CompanyProfile_State( + session=self.STATE.session, + un_id=self.STATE.ids.un_id, + ) + logger_gui.debug("[Page -- InitRec Company] State to call: %s", req_state) + self.company_profile_requested.emit(req_state) + + def _update_layout(self) -> None: + self.initrec_form_metadata_box.set_open(True) + if self.STATE.recording_type is RecordingType.WITH_COMPANY: + # add company + self.initrec_form_company_box.setVisible(True) + self.initrec_form_metadata.top_level_form_layout.setRowVisible( + self.field_wiedereintrittsdatum, False + ) + self.comp_profile_btn.setVisible(True) + if self.STATE.ids.valid(EntityType.COMPANY): + self.comp_profile_btn.setEnabled(True) + else: + self.comp_profile_btn.setEnabled(False) + + else: + self.initrec_form_company_box.setVisible(False) + self.initrec_form_metadata.top_level_form_layout.setRowVisible( + self.field_wiedereintrittsdatum, True + ) + self.comp_profile_btn.setVisible(False) + self.comp_profile_btn.setEnabled(False) + + # ** custom logic def _custom_county_selection(self, idx: int) -> None: value = self.person_location.itemData(idx) if value == "Inland": @@ -3378,6 +3416,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 +3442,175 @@ 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: list[bool] = [] + relevant_ent_types: set[EntityType] = {EntityType.METADATA, EntityType.PERSON} + if self.STATE.recording_type is RecordingType.WITH_COMPANY: + relevant_ent_types.add(EntityType.COMPANY) + + valid_id_states = ( + form.relevant_ids_set() + for form in self.STATE.child_modules + if form.STATE.ent_type in relevant_ent_types + ) + return all(valid_id_states) + + def _delete_data(self) -> None: + 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: + for form in self.STATE.child_modules: + form.reset_form() + self.STATE.ids = EntityIds() + self.STATE.geloescht = False + + def _load_database( + self, + ids: EntityIds, + ) -> None: + logger_initrecform.info("[Page -- InitRec] Load data method...") + logger_initrecform.debug("[Page -- InitRec] Entity IDs: %s", ids) + self._reset() + + # ids = EntityIds( + # # un_id=3, + # # an_id=7, + # pers_id=5, + # ) + # # ids = EntityIds( + # # link_id=7, + # # ) + self.STATE.ids.update(ids, ignore_none=False) + + if ids.empty(): + logger_initrecform.debug("[Page -- InitRec] Loading: No ID set. Do nothing!") + return + + # TODO add loading logic with backend call + # !! just a dummy + initrec = backend.page_initrec_from_db(ids) + + # TODO re-enable later + # if res.status != STATUS_HANDLER.SUCCESS: + # logger_page_consulting.error( + # ( + # "[Consulting Page] There were errors during the database " + # "loading procedure." + # ), + # stack_info=True, + # ) + + # exc_formatted = ( + # f"Exception: {res.status.ExceptionType}\nMessage: {res.status.message}" + # ) + # msg_box = get_message_box( + # QMessageBox.Icon.Warning, + # "Laden fehlgeschlagen", + # ( + # "Beim Laden der Daten ist ein Fehler aufgetreten. Details sind " + # "unten angefügt." + # ), + # detailed_text=exc_formatted, + # ) + # msg_box.exec() + # return + + self._load_from_pydantic(initrec, sync_to_GUI=False) + + def _load_from_pydantic( + self, + data: InitRec, + update_only: bool = False, + sync_to_GUI: bool = True, + ) -> None: + logger_initrecform.info("[Page -- InitRec] Load from Pydantic model...") + self.STATE.ids.update(data.ids) + self.STATE.recording_type = data.recording_type + self.STATE.form_data = data.form_data + self.STATE.geloescht = True if data.geloescht else False + + # set form data to correct initrec form states + for ent_type, state in self.STATE.initrec_form_states.items(): + state.ids.update(self.STATE.ids) + state.geloescht = self.STATE.geloescht + + match ent_type: + case EntityType.COMPANY: + state.form_data = self.STATE.form_data.company + if ( + not update_only + and self.STATE.recording_type is RecordingType.WITH_COMPANY + ): + self.initrec_form_company.load_state(state) + case EntityType.PERSON: + state.form_data = self.STATE.form_data.person + if not update_only: + self.initrec_form_person.load_state(state) + case EntityType.METADATA: + state.form_data = self.STATE.form_data.metadata + logger_initrecform.info( + "[Page -- InitRec] Load METADATA from Pydantic model...\n%s", + pformat(self.STATE.form_data.metadata), + ) + self.initrec_form_metadata.load_state(state) + case _: + raise RuntimeError(f"Unspecified entity type {ent_type}") + + 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( - session=self.STATE.session, - cfg=self.AUTO_FORM_CFG, - ids=self.STATE.ids, - ent_type=EntityType.PERSON, - # rec_id=self.STATE.pers_id, # TODO remove - form_data=None, - locked=self.STATE.locked, - ) - self.auto_form.load_state(auto_form_state) + logger_page_initrec.debug( + "[Page -- InitRec] Syncing state to GUI: State IDs: %s", self.STATE.ids + ) + + if section is None or section == "content": + if not self.STATE.geloescht: + self._load_database(self.STATE.ids) + else: + self._reset() + self._update_layout() if section is None or section == "banners": self._clear_info_banners() @@ -3435,30 +3620,240 @@ class Page_InitRecPerson(QWidget): button_text="", ) + if section is None or section == "locking": + if self.STATE.locked: + for form in self.STATE.child_modules: + form.lock() + self.edit_buttons.set_modify_state(False, set_delete_btn=True) + else: + for form in self.STATE.child_modules: + form.unlock() + self.edit_buttons.set_modify_state(True, set_delete_btn=False) + self._activate_delete() + def _sync_GUI_to_state(self) -> None: - 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 + logger_page_initrec.debug("[Page -- InitRec] Syncing GUI to state...") + + logger_page_initrec.debug( + "[Page -- InitRec] Syncing GUI to state: Child modules...\n%s", + pformat(self.STATE.child_modules), + ) + + logger_page_initrec.debug( + "[Page -- InitRec] Syncing GUI to state: Existing form states keys...\n%s", + pformat(self.STATE.initrec_form_states.keys()), + ) + + for form in self.STATE.child_modules: + # ?? is this the same reference to use a similar pattern as + # ?? the consulting page (initialise once with the same state + # ?? as reference which are then automatically updated) + # !! check if this is working as expected + # new entries' states are linked to this instance by the page state + form_state = form.get_state() + logger_page_initrec.debug( + "[Page -- InitRec] Syncing GUI to state: Form state ent_type %s", + form_state.ent_type, + ) + + logger_page_initrec.debug( + "[Page -- InitRec] Syncing GUI to state: identity of form states: %s", + (self.STATE.initrec_form_states[form_state.ent_type] is form_state), + ) + assert self.STATE.initrec_form_states[form_state.ent_type] is form_state, ( + "passing by reference not working" + ) + self.STATE.ids.update(form_state.ids, ignore_none=True) + # self.STATE.form_data[form_state.ent_type] = form_state.form_data + match form_state.ent_type: + case EntityType.COMPANY: + self.STATE.form_data.company = form_state.form_data + case EntityType.PERSON: + self.STATE.form_data.person = form_state.form_data + case EntityType.METADATA: + self.STATE.form_data.metadata = form_state.form_data + case _: + raise RuntimeError(f"Unspecified entity type {form_state.ent_type}") + + logger_page_initrec.debug( + "[Page -- InitRec] Syncing GUI to state: IDs after...\n%s", + pformat(self.STATE.ids), + ) + logger_page_initrec.debug( + "[Page -- InitRec] Syncing GUI to state: Form data after...\n%s", + pformat(self.STATE.form_data), + ) def load_state( self, - new_state: Page_InitRecPerson_State, + new_state: Page_InitRec_State, ) -> None: - set_page_state(self.STATE, new_state) + set_page_state( + self.STATE, + new_state, + ignore_attr={"form_data", "initrec_form_states"}, + ) self._sync_state_to_GUI(None) - def get_state(self) -> Page_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: + if ( + m.STATE.ent_type is EntityType.COMPANY + and self.STATE.recording_type is not RecordingType.WITH_COMPANY + ): + continue + + errors.extend(m.validate()) + + return errors + 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...") + + logger_page_initrec.info("[Page -- InitRec] Syncing current state...") + # state = self.get_state() + self._sync_GUI_to_state() + form_data = self.STATE.form_data + assert form_data, "form data not set" + logger_page_initrec.debug("[Page -- InitRec] Get form data\n%s", pformat(form_data)) + db_data = Page_InitRec_Form_Data(metadata={}, person={}, company={}) + + if self.STATE.ids.empty() and self.STATE.geloescht: + logger_page_initrec.debug( + "[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" + data_to_db = InitRec( + ids=self.STATE.ids, + recording_type=self.STATE.recording_type, + geloescht=deleted_datetime, + form_data=db_data, + ) + returned = backend.page_initrec_to_db(data_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_validation_error_handling(errors) + self._enable_save() + return + + logger_page_initrec.info("[Page -- InitRec] Success! All data is valid") + + try: + logger_page_initrec.debug("[Page -- InitRec] Call Pydantic...") + + for ent_type, state in self.STATE.initrec_form_states.items(): + logger_page_initrec.debug( + "[Page -- InitRec] Ent type: %s, Current state CFG model: %s", + ent_type, + state.cfg.pydantic_model, + ) + match ent_type: + case EntityType.COMPANY: + val_data = state.cfg.pydantic_model(**self.STATE.form_data.company) + db_data.company = val_data.to_db(exclude=state.cfg.ignored_keys) + case EntityType.PERSON: + val_data = state.cfg.pydantic_model(**self.STATE.form_data.person) + db_data.person = val_data.to_db(exclude=state.cfg.ignored_keys) + case EntityType.METADATA: + val_data = state.cfg.pydantic_model(**self.STATE.form_data.metadata) + db_data.metadata = val_data.to_db(exclude=state.cfg.ignored_keys) + case _: + raise RuntimeError(f"Unspecified entity type {ent_type}") + + 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 + data_to_db = InitRec( + ids=self.STATE.ids, + recording_type=self.STATE.recording_type, + geloescht=deleted_datetime, + form_data=db_data, + ) + returned = backend.page_initrec_to_db(data_to_db) + + self._load_from_pydantic(returned, update_only=True, sync_to_GUI=False) + self.update_triggered.emit() + except Exception as err: + msg_box = get_message_box( + 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): @@ -3743,13 +4138,13 @@ class Page_CompanyProfile(QWidget): self, cons_type: ConsultingType, ) -> None: - assert self.STATE.ids.un_id is not None, ( + assert self.STATE.un_id is not None, ( "tried to call consulting from comp profile without set ID" ) new_state = Page_Consulting_State( session=self.STATE.session, vorgang_id=None, - un_id=self.STATE.ids.un_id, + un_id=self.STATE.un_id, beratungs_typ=cons_type, locked=False, ) @@ -3759,13 +4154,13 @@ class Page_CompanyProfile(QWidget): self, data: CompanyProfileConsultationEntry, ) -> None: - assert self.STATE.ids.un_id is not None, ( + assert self.STATE.un_id is not None, ( "tried to call consulting from comp profile without set ID" ) new_state = Page_Consulting_State( session=self.STATE.session, vorgang_id=data.cons_id, - un_id=self.STATE.ids.un_id, + un_id=self.STATE.un_id, beratungs_typ=data.cons_type, locked=True, ) @@ -3787,11 +4182,9 @@ class Page_CompanyProfile(QWidget): widget.deleteLater() def _update_consultations(self) -> None: - if self.STATE.ids.un_id is None: + if self.STATE.un_id is None: return - consultations = backend.companyprofile_page_get_consultations( - un_id=self.STATE.ids.un_id - ) + consultations = backend.companyprofile_page_get_consultations(un_id=self.STATE.un_id) logger_gui.debug( "[Page -- Company Profile] Consultations: %s", pformat(consultations) @@ -3829,7 +4222,7 @@ class Page_CompanyProfile(QWidget): self.add_btn_individual.setEnabled(True) def _sync_state_to_GUI(self) -> None: - if self.STATE.ids.un_id is None: + if self.STATE.un_id is None: # just clear and do nothing else logger_gui.debug("just reset, ID is None") self.table_master_data.reset() @@ -3838,10 +4231,12 @@ class Page_CompanyProfile(QWidget): return self._enable_add_btns() - init_rec = backend.initrec_company_get_initial_recording(self.STATE.ids.un_id) - ma_id = cast("ExtMaId", init_rec["Partnersuche__un_suche"]) - assert ma_id is not None, "no ma_id obtained" - comp_info = backend.initrec_comp_search_get_info(ma_id) + # TODO change to correct backend function + # raise RuntimeError("TODOs to implement") + # init_rec = backend.initrec_company_get_initial_recording(self.STATE.un_id) + # ma_id = cast("ExtMaId", init_rec["Partnersuche__un_suche"]) + # assert ma_id is not None, "no ma_id obtained" + comp_info = backend.external_company_get_info(self.STATE.un_id) data: dict[str, Any] = {} @@ -3863,8 +4258,8 @@ class Page_CompanyProfile(QWidget): self, new_state: Page_CompanyProfile_State, ) -> None: - logger_gui.debug("Called function to load with IDs: %s", pformat(new_state.ids)) - self.STATE.ids = new_state.ids + logger_gui.debug("Called function to load with ID: %s", pformat(new_state.un_id)) + self.STATE.un_id = new_state.un_id self._sync_state_to_GUI() @@ -4521,12 +4916,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 +5008,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 +5049,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." ) @@ -4665,7 +5062,7 @@ class Page_Consulting(QWidget): logger_page_consulting.error( "[Consulting Page] Error during GUI validation phase:\n%s", pformat(errors) ) - GUI_pydantic_validation_error_handling(errors) + GUI_validation_error_handling(errors) self._enable_save() return @@ -4721,7 +5118,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()), ) @@ -4795,46 +5192,14 @@ class Page_Consulting_LinkingDialog(QDialog): parent: QWidget | None = None, ): super().__init__(parent) + # State and data self.STATE = state + self.all_companies = backend.page_consulting_linking_companies(None) + self.all_persons = backend.page_consulting_linking_persons() self.setWindowTitle("Beziehung verknüpfen") self.resize(700, 450) - # firmen_aus_db = [{"id": 1, "name": "Siemens AG"}, {"id": 2, "name": "Bosch GmbH"}] - # personen_aus_db = [ - # { - # "id": 50, - # "name": "Max Mustermann", - # "unternehmen_id": 1, - # "firma_name": "Siemens AG", - # }, - # { - # "id": 51, - # "name": "Erika Muster", - # "unternehmen_id": 1, - # "firma_name": "Siemens AG", - # }, - # { - # "id": 52, - # "name": "Peter Müller", - # "unternehmen_id": 2, - # "firma_name": "Bosch GmbH", - # }, - # { - # "id": 53, - # "name": "Freiberufler Frank", - # "unternehmen_id": None, - # "firma_name": None, - # }, - # ] - - # self.all_unternehmen = firmen_aus_db - # self.all_personen = personen_aus_db - - # # Rückgabewerte - # self.gewaehltes_unternehmen = None - # self.gewaehlte_person = None - # --- UI LAYOUT --- main_layout = QVBoxLayout(self) column_layout = QHBoxLayout() @@ -4859,7 +5224,7 @@ class Page_Consulting_LinkingDialog(QDialog): column_layout.addLayout(self.vbox_persons) main_layout.addLayout(column_layout) - # Unten: OK / Abbrechen Buttons + # OK / Cancel buttons self.button_box = QDialogButtonBox( QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel ) @@ -4869,10 +5234,7 @@ class Page_Consulting_LinkingDialog(QDialog): self.button_box.button(QDialogButtonBox.StandardButton.Cancel).setText("Abbrechen") main_layout.addWidget(self.button_box) - # initial loading - # self._load_companies() - # self._load_persons() - + # TODO check removal # list_style = """ # QListWidget::item:selected { # background-color: #0078D4; /* Blau, wenn die Liste aktiv fokussiert ist */ @@ -4923,20 +5285,28 @@ class Page_Consulting_LinkingDialog(QDialog): def _sync_state_to_GUI( self, reset: bool, - section: Literal["person_selection", "validation"] | None = None, + section: Literal["person_selection", "company_selection", "validation"] | None = None, ) -> None: if reset: self._reset() - self._load_companies() + self._load_companies_to_list(None) + self._load_persons_to_list(None) if section is None or section == "person_selection": if self.STATE.beratungs_typ is ConsultingType.PAUSCHAL: self._enable_person_selection(False) elif self.STATE.beratungs_typ is ConsultingType.INDIVIDUAL: self._enable_person_selection(True) - self.list_persons.blockSignals(True) - self._load_persons(self.STATE.un_id) - self.list_persons.blockSignals(False) + if not self.list_persons.selectedItems(): + self.list_persons.blockSignals(True) + self._load_persons_to_list(self.STATE.un_id) + self.list_persons.blockSignals(False) + + if section is None or section == "company_selection": + if not self.list_companies.selectedItems(): + self.list_companies.blockSignals(True) + self._load_companies_to_list(self.STATE.filter_un_ids) + self.list_companies.blockSignals(False) if section is None or section == "validation": self._validate() @@ -4959,31 +5329,49 @@ class Page_Consulting_LinkingDialog(QDialog): else: self.ok_btn.setEnabled(False) - def _load_companies(self) -> None: + def _reload_all_companies(self) -> None: + self.all_companies = backend.page_consulting_linking_companies(None) + + def _reload_all_persons(self) -> None: + self.all_persons = backend.page_consulting_linking_persons() + + def _load_companies_to_list( + self, + un_ids: set[RecId] | None, + ) -> None: + # search_text = self.suche_firma.text().strip().lower() self.list_companies.clear() - companies = backend.page_consulting_linking_companies(un_id=self.STATE.un_id) + # TODO check reload of companies + # companies = backend.page_consulting_linking_companies(None) logger_page_consulting.debug( - "[Page Consulting -- Linking] Retrieved company entries:\n%s", companies + "[Page Consulting -- Linking] Retrieved company entries:\n%s", self.all_companies ) - for company in companies: + for company in self.all_companies: + if un_ids and company.un_id not in un_ids: + continue + item = QListWidgetItem(company.display_name) item.setData(Qt.ItemDataRole.UserRole, company) self.list_companies.addItem(item) - def _load_persons( + def _load_persons_to_list( self, un_id: RecId | None, ) -> None: """Lädt Personen. Wenn firma_id übergeben wird, wird gefiltert.""" self.list_persons.clear() - persons = backend.page_consulting_linking_persons(un_id) + # TODO check reload of persons + # persons = backend.page_consulting_linking_persons() logger_page_consulting.debug( - "[Page Consulting -- Linking] Retrieved person entries:\n%s", persons + "[Page Consulting -- Linking] Retrieved person entries:\n%s", self.all_persons ) - for person in persons: + for person in self.all_persons: + if un_id and un_id not in person.un_ids: + continue + item = QListWidgetItem(person.display_name) item.setData(Qt.ItemDataRole.UserRole, person) self.list_persons.addItem(item) @@ -5004,10 +5392,6 @@ class Page_Consulting_LinkingDialog(QDialog): pformat(self.STATE), ) self._sync_state_to_GUI(reset=False) - # if self.STATE.beratungs_typ is ConsultingType.INDIVIDUAL: - # self.list_persons.blockSignals(True) - # self._load_persons(un_id=self.STATE.un_id) - # self.list_persons.blockSignals(False) def _person_selected(self) -> None: items = self.list_persons.selectedItems() @@ -5018,29 +5402,29 @@ class Page_Consulting_LinkingDialog(QDialog): "LinkingConsultationsEntryPerson", items[0].data(Qt.ItemDataRole.UserRole) ) self.STATE.pers_id = person.pers_id - self._sync_state_to_GUI(reset=False, section="validation") - # TODO this backlink is a really good idea and should be implemented - # if person["unternehmen_id"]: - # self.list_companies.blockSignals(True) - # - # for i in range(self.list_companies.count()): - # item = self.list_companies.item(i) - # firma_data = item.data(Qt.ItemDataRole.UserRole) - # if firma_data["id"] == person["unternehmen_id"]: - # item.setSelected(True) - # self.gewaehltes_unternehmen = firma_data - # break - # self.list_companies.blockSignals(False) + if person.un_ids: + self.STATE.filter_un_ids = person.un_ids + else: + self.STATE.filter_un_ids = None + + self._sync_state_to_GUI(reset=False) def _clear_company_selection(self) -> None: + logger_page_consulting.debug( + "[Page Consulting -- Linking] Clear company selection...", + ) self.list_companies.clearSelection() self.STATE.un_id = None self._sync_state_to_GUI(reset=False) def _clear_person_selection(self) -> None: + logger_page_consulting.debug( + "[Page Consulting -- Linking] Clear person selection...", + ) self.list_persons.clearSelection() self.STATE.pers_id = None + self.STATE.filter_un_ids = None self._sync_state_to_GUI(reset=False) def get_state(self) -> Page_Consulting_Linking_State: @@ -5586,6 +5970,9 @@ class InfoBanner(QFrame): self.button.clicked.connect(self.btn_clicked.emit) +# // test components + + #################################################################### @@ -5668,7 +6055,7 @@ def pydantic_validation_error_handling( msg_box.exec() -def GUI_pydantic_validation_error_handling( +def GUI_validation_error_handling( errors: list[str], ) -> None: error_text = "Bitte füllen Sie die folgenden Pflichtfelder aus:\n\n▸ " + "\n▸ ".join( @@ -5830,39 +6217,32 @@ 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) - # SITE: 'Grunderfassung Unternehmen' - ids = EntityIds(type=EntityType.COMPANY) - initrec_company_state = Page_InitRecCompany_State( - session=self.STATE.session, ids=ids, locked=False + 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.new_consulting_requested.connect(self.show_page_consulting) + self.new_entry_select.new_initrec_requested.connect(self.show_page_initrec) + self.stack.addWidget(self.new_entry_select) + + # SITE: 'Grunderfassung' + newinitrec_state = Page_InitRec_State( + session=session, + ids=EntityIds(), + recording_type=RecordingType.WITH_COMPANY, + form_data=Page_InitRec_Form_Data(metadata={}, person={}, company={}), + 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) + self.initrec = Page_InitRec(newinitrec_state) + self.initrec.back_main_requested.connect(self.show_main_page) + self.initrec.back_requested.connect(self.simple_go_back) + self.initrec.company_profile_requested.connect(self.show_page_company_profile) + self.initrec.update_triggered.connect(self.update_grid) + self.stack.addWidget(self.initrec) + # SITE: 'Unternehmensprofil' - comp_profile_state = Page_CompanyProfile_State( - session=self.STATE.session, ids=EntityIds() - ) + comp_profile_state = Page_CompanyProfile_State(session=self.STATE.session) self.company_profile = Page_CompanyProfile(comp_profile_state) self.company_profile.back_main_requested.connect(self.show_main_page) # TODO remove @@ -5870,6 +6250,7 @@ class MainWindow(QMainWindow): self.company_profile.back_requested.connect(self.simple_go_back) self.company_profile.consulting_requested.connect(self.show_page_consulting) self.stack.addWidget(self.company_profile) + # SITE: 'Beratungsgespräch' self.consulting_page = Page_Consulting(self.STATE.session) self.consulting_page.back_main_requested.connect(self.show_main_page) @@ -5891,16 +6272,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) @@ -5976,6 +6358,7 @@ class MainWindow(QMainWindow): for entry in data: self.add_row_to_grid(entry) + # TODO rework def add_row_to_grid( self, entry: types.MainPageEntry, @@ -5987,42 +6370,26 @@ class MainWindow(QMainWindow): 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_name = entry.dt_updated.date().strftime(DATE_FMT) cell_date = MainPage_ClickableCell(cell_date_name, entry) cell_date.clicked.connect(self.goto_initial_recording) self.grid.addWidget(cell_date, row, 4) self.current_row += 1 # Zähler für den nächsten Eintrag erhöhen + # TODO rework 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}") - - self._navigate_to_target(target) + new_state = Page_InitRec_State( + session=self.STATE.session, + ids=data.ids, + recording_type=data.recording_type, + locked=True, + ) + self.initrec.load_state(new_state) + self._navigate_to_target(self.initrec) def _navigate_to_target( self, @@ -6047,21 +6414,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_initrec( 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.initrec.load_state(new_state) + self._navigate_to_target(self.initrec) - 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 +6509,8 @@ 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 _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..7c12017 100644 --- a/src/wce_crm/logging.py +++ b/src/wce_crm/logging.py @@ -30,19 +30,28 @@ 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_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") logger_back.setLevel(logging.DEBUG) diff --git a/src/wce_crm/types.py b/src/wce_crm/types.py index 9d4fc34..c96e9ff 100644 --- a/src/wce_crm/types.py +++ b/src/wce_crm/types.py @@ -1,9 +1,11 @@ from __future__ import annotations +import copy import dataclasses as dc import datetime import enum -from typing import TYPE_CHECKING, TypeAlias, TypedDict +from collections.abc import Generator +from typing import TYPE_CHECKING, Final, TypeAlias, TypedDict if TYPE_CHECKING: import polars as pl @@ -12,13 +14,22 @@ 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"]] +# TODO rename: these are not entity types or entities but different forms of +# initial recordings +class RecordingType(enum.IntEnum): + WITH_COMPANY = enum.auto() + WITHOUT_COMPANY = enum.auto() + + class EntityType(enum.IntEnum): + METADATA = enum.auto() COMPANY = enum.auto() PERSON = enum.auto() @@ -30,54 +41,150 @@ class ConsultingType(enum.StrEnum): @dc.dataclass(slots=True, kw_only=True) class EntityIds: - un_id: RecId | None = None + 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 + _field_names: frozenset[str] = dc.field(init=False, repr=False, compare=False) - def __post_init__(self, type: EntityType | None) -> None: - self._ent_type = type + def __post_init__(self) -> None: + field_names = set(f.name for f in dc.fields(EntityIds)) + field_names.remove("_field_names") + self._field_names = frozenset(field_names) - @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_by_name( + self, + attr_name: str, + value: int | None, + ) -> None: + if attr_name not in self._field_names: + raise KeyError( + f"Attribute {attr_name} not member of this class. Valid " + f"options: {self._field_names}" ) - def set_id( - self, - id_: RecId | None, - ent_type: EntityType, - ) -> None: - if ent_type is EntityType.COMPANY: - self.un_id = id_ - elif ent_type is EntityType.PERSON: - self.pers_id = id_ + setattr(self, attr_name, value) - def get_id( + def update( self, - ent_type: EntityType, - ) -> RecId | None: - if ent_type is EntityType.COMPANY: - return self.un_id + ids: EntityIds, + ignore_none: bool = False, + deep: bool = False, + ) -> None: + if not isinstance(ids, EntityIds): + raise TypeError("New IDs must be have the same type as this object") + + 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 | None = None, + ) -> Generator[int | None, None, None]: + if ent_type is None: + relevant_fields = (getattr(self, field_name) for field_name in self._field_names) + elif ent_type is EntityType.COMPANY: + # RELEVANT_ATTRIBUTES = {"un_id", "an_id", "link_id"} + RELEVANT_ATTRIBUTES = {"un_id", "an_id"} + relevant_fields = ( + getattr(self, field_name) + for field_name in self._field_names + if field_name in RELEVANT_ATTRIBUTES + ) elif ent_type is EntityType.PERSON: - return self.pers_id + RELEVANT_ATTRIBUTES = {"pers_id"} + relevant_fields = ( + getattr(self, field_name) + for field_name in self._field_names + if field_name in RELEVANT_ATTRIBUTES + ) + elif ent_type is EntityType.METADATA: + RELEVANT_ATTRIBUTES = set() + relevant_fields = ( + getattr(self, field_name) + for field_name in self._field_names + if field_name in RELEVANT_ATTRIBUTES + ) + else: + raise RuntimeError(f"Unspecified entity type {ent_type}") + + return relevant_fields + + def empty( + self, + ent_type: EntityType | None = None, + ) -> bool: + relevant_fields = self._get_relevant_fields(ent_type=ent_type) + + return not any(relevant_fields) + + def valid( + self, + ent_type: EntityType | None = None, + ) -> bool: + relevant_fields = self._get_relevant_fields(ent_type=ent_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 @@ -137,10 +244,10 @@ class ContactPersonInfo(TypedDict): # ** internals @dc.dataclass(slots=True) class MainPageEntry: - rec_id: RecId + ids: EntityIds display_name: str - Metadaten_aktualisierung: datetime.datetime - type: EntityType + dt_updated: datetime.datetime + recording_type: RecordingType @dc.dataclass(slots=True) @@ -166,4 +273,5 @@ class LinkingConsultationsEntryCompany: @dc.dataclass(slots=True) class LinkingConsultationsEntryPerson: pers_id: RecId + un_ids: set[RecId] display_name: str