generated from dopt-python/py311
Compare commits
75 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 36b7c319cc | |||
| f8b1c65221 | |||
| 37f7535693 | |||
| c91bc9022a | |||
| 68bd873838 | |||
| a6ef3d562e | |||
| ecb9a9cf13 | |||
| 1c586299fc | |||
| 2b6268e38a | |||
| 07ae18271e | |||
| b635e7eb75 | |||
| 02d94c4a80 | |||
| 07c2bd92da | |||
| 2894d3a1a9 | |||
| a515f3b820 | |||
| 449d401c04 | |||
| ba26baa225 | |||
| 1ff55038fa | |||
| dc9374f5b7 | |||
| acfc4a824a | |||
| 868db09d94 | |||
| fd25576b0e | |||
| 416a686877 | |||
| 6e04a7485a | |||
| ffe38a4ee7 | |||
| c1843702f8 | |||
| 1cab78b2b4 | |||
| 9a73f27518 | |||
| 65e3e10e13 | |||
| 9392f3fc77 | |||
| 7bfbdc731b | |||
| 482ae436eb | |||
| 0fce42b788 | |||
| 284196f127 | |||
| c1ba3cf6e7 | |||
| a40213a1ed | |||
| f4a4d2d98e | |||
| af62025558 | |||
| e6dc2644ac | |||
| 393e3ba39b | |||
| 3bbed94048 | |||
| eb66b435a0 | |||
| b41d55db7d | |||
| 0cddb58f06 | |||
| 3841b645e1 | |||
| 5563e5a6b9 | |||
| 6eb358e0aa | |||
| f84cbb21c0 | |||
| 7c79bf8e91 | |||
| 6f7593ac9d | |||
| 38125eb2ba | |||
| 14357df8cd | |||
| 39d4327e19 | |||
| 9cfe53509f | |||
| 16ae573687 | |||
| 046adfd3df | |||
| fae94d01a3 | |||
| 770ae11ad6 | |||
| cae623bc19 | |||
| 0bb6e98341 | |||
| 62e69184d7 | |||
| 7c26c1fc2e | |||
| afa8fb351d | |||
| deeb038956 | |||
| ae48f72500 | |||
| 1d353d863e | |||
| 10c8b79cb3 | |||
| 9dc26e36b5 | |||
| 04bfac8f53 | |||
| df93914942 | |||
| b9427bc756 | |||
| 76ff68cb43 | |||
| b7f45066f0 | |||
| b6087a5b21 | |||
| 27934497d5 |
@@ -16,10 +16,18 @@ Die Anwendung besteht aus einem Ordner, der alle relevanten Dateien zu ihrer Aus
|
||||
|
||||
## Verwendung
|
||||
|
||||
In der aktuellen Version ist die Grunderfassung für Unternehmen implementiert. Formularfelder und grundlegende Optik orientieren sich an den bereitgestellten Anforderungsdokumenten inkl. etwaiger Mock-Ups. Zur Anlage eines neuen Eintrags ist in der Übersicht oben die Schaltfläche "Neu" zu wählen. Anschließend kann die Grunderfassung für Unternehmen gewählt werden. Daraufhin öffnet sich eine Formularübersicht. Pflichtfelder sind mit einem Sternchen "*" gekennzeichnet. Sollten Pflichtfelder beim Speichern fehlen, so werden diese in einem Hinweisfenster benannt und im Formular selbst rot hervorgehoben. Das Formular kann mit den Knöpfen ganz unten gespeichert oder zurückgesetzt werden. Darüber hinaus sind Tastenkürzel für beide Aktionen hinterlegt: "Strg + s" für das Speichern und "Strg + z" für das Zurücksetzen.
|
||||
### Grunderfassung
|
||||
|
||||
In der aktuellen Version ist die Grunderfassung implementiert. Formularfelder und grundlegende Optik orientieren sich an den bereitgestellten Anforderungsdokumenten inkl. etwaiger Mock-Ups. Zur Anlage eines neuen Eintrags ist in der Übersicht oben die Schaltfläche "Neu" zu wählen. Anschließend kann der Typ der Grunderfassung gewählt werden. Daraufhin öffnet sich eine Formularübersicht. Pflichtfelder sind mit einem Sternchen "*" gekennzeichnet. Sollten Pflichtfelder beim Speichern fehlen, so werden diese in einem Hinweisfenster benannt und im Formular selbst rot hervorgehoben. Das Formular kann mit den Knöpfen ganz oben gespeichert oder zurückgesetzt werden. Hierfür muss vorther die Bearbeitung aktiviert werden. Darüber hinaus sind Tastenkürzel für beide Aktionen hinterlegt: "Strg + s" für das Speichern und "Strg + z" für das Zurücksetzen. Einträge können auch gelöscht werden.
|
||||
|
||||
Sind alle Felder gefüllt, werden im Hintergrund die Daten nach dem Auslösen des Speichervorgangs nochmals validiert. Beispielsweise werden erfasste E-Mail-Adressen auf Gültigkeit geprüft. Etwaige Fehler werden dem Nutzer ebenfalls angezeigt.
|
||||
|
||||
Nach der erfolgreichen Speicherung kann über die Schaltfläche im oberen Bereich "zurück zur Übersicht" auf die Hauptseite gewechselt werden. Dort sollte nun der neu angelegte Eintrag sichtbar sein. Die Übersicht ist absteigend nach dem Datum der letzten Änderung sortiert. Ein eben gespeicherter Eintrag sollte sich demzufolge ganz oben in der Übersicht befinden. Mit einem Klick auf einen Eintrag in der Übersicht wird erneut das Formular zur Grunderfassung mit den hinterlegten Daten geladen und angezeigt. Es können Änderungen vorgenommen und gespeichert werden.
|
||||
|
||||
*Die Grunderfassung für Individualpersonen ist aktuell nur als Dummy verfügbar. Bei einem Klick auf die entsprechende Schaltfläche passiert nichts.*
|
||||
### Unternehmesprofil
|
||||
|
||||
Wird eine Grunderfassung mit Unternehmen durchgeführt, so kann für dieses Unternehmen auf das Unternehmensprofil über die Schaltfläche im oberen Bereich gewechselt werden. Aktuell beherbergt dieses Profil die Stammdaten als auch die Verwaltung von Pauschal- und Indivualberatungen.
|
||||
|
||||
### Beratungen
|
||||
|
||||
Über die Startseite und die Schaltfläche "Neu" sowie die Auswahl im Unternehmesprofil kann ein Beratungsvorgang angelegt werden. Es kann zwischen Pauschal- und Individualberatungen ausgewählt werden. Je nach Kontext ist die Beratung bereits mit Entitäten verknüpft. Ausstehende Verknüpfungen werden nach dem Speichern der Beratung dem Nutzer überein Banner mitgeteilt. Er hat dann die Möglichkeit, die Verknüpfung von Unternehmen und/oder Personen vorzunehmen.
|
||||
|
||||
+6
-1
@@ -46,6 +46,7 @@ def run_migrations_offline() -> None:
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
dialect_opts={"paramstyle": "named"},
|
||||
render_as_batch=True,
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
@@ -66,7 +67,11 @@ def run_migrations_online() -> None:
|
||||
)
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(connection=connection, target_metadata=target_metadata)
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=target_metadata,
|
||||
render_as_batch=True,
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
"""add unique constraint for company person linking table
|
||||
|
||||
Revision ID: 03d4f000f445
|
||||
Revises: 85577ce9f2ab
|
||||
Create Date: 2026-07-22 09:17:46.576581
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "03d4f000f445"
|
||||
down_revision: Union[str, Sequence[str], None] = "85577ce9f2ab"
|
||||
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("zuordnung_personen_unternehmen") as batch_op:
|
||||
batch_op.create_unique_constraint("uq_un_id_pers_id", ["un_id", "pers_id"])
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("zuordnung_personen_unternehmen") as batch_op:
|
||||
batch_op.drop_constraint("uq_un_id_pers_id")
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,118 @@
|
||||
"""Baseline: data model with new approach
|
||||
|
||||
Revision ID: 1a8e957522a4
|
||||
Revises:
|
||||
Create Date: 2026-06-26 13:47:48.937123
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
import wce_crm.db
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "1a8e957522a4"
|
||||
down_revision: Union[str, Sequence[str], None] = None
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"grunderfassung_personen",
|
||||
sa.Column("pers_id", sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column("Metadaten_erstellung", wce_crm.db.UTCDateTime(), nullable=True),
|
||||
sa.Column("Metadaten_aktualisierung", wce_crm.db.UTCDateTime(), nullable=True),
|
||||
sa.Column("Metadaten_nutzer", sa.String(length=20), nullable=True),
|
||||
sa.Column("Metadaten_wiedereintrittsdatum", sa.Date(), nullable=True),
|
||||
sa.Column("Arbeitserfahrung", sa.Text(), nullable=True),
|
||||
sa.Column("Grunderfassung_fallnummer", sa.Text(), nullable=True),
|
||||
sa.Column("Grunderfassung_notiz", sa.Text(), nullable=True),
|
||||
sa.Column("HoehereBildung", sa.Text(), nullable=True),
|
||||
sa.Column("Kontaktperson__KP_adresse", sa.Text(), nullable=True),
|
||||
sa.Column("Kontaktperson__KP_anrede_anschrift", sa.Text(), nullable=True),
|
||||
sa.Column("Kontaktperson__KP_email", sa.Text(), nullable=True),
|
||||
sa.Column("Kontaktperson__KP_festnetznummer", sa.Text(), nullable=True),
|
||||
sa.Column("Kontaktperson__KP_funktion_beziehung", sa.Text(), nullable=True),
|
||||
sa.Column("Kontaktperson__KP_mobilfunknummer", sa.Text(), nullable=True),
|
||||
sa.Column("Kontaktperson__KP_name", sa.Text(), nullable=True),
|
||||
sa.Column("Kontaktperson__KP_name_partner", sa.Text(), nullable=True),
|
||||
sa.Column("Kontaktperson__KP_titel", sa.Text(), nullable=True),
|
||||
sa.Column("Kontaktperson__KP_vorname", sa.Text(), nullable=True),
|
||||
sa.Column("Projektrelevanz__relevanz", sa.Text(), nullable=True),
|
||||
sa.Column("Projektrelevanz__foerderperiode", sa.Text(), nullable=True),
|
||||
sa.Column("Schulbildung", sa.Text(), nullable=True),
|
||||
sa.Column("Sprachkenntnisse", sa.Text(), nullable=True),
|
||||
sa.Column("Stammdaten__PLZ", sa.Text(), nullable=True),
|
||||
sa.Column("Stammdaten__anrede_anschrift", sa.Text(), nullable=True),
|
||||
sa.Column("Stammdaten__anzahl_kinder__alter", sa.Text(), nullable=True),
|
||||
sa.Column("Stammdaten__anzahl_kinder__anzahl", sa.Integer(), nullable=True),
|
||||
sa.Column("Stammdaten__aufenthaltsort", sa.Text(), nullable=True),
|
||||
sa.Column("Stammdaten__bundesland", sa.Text(), nullable=True),
|
||||
sa.Column("Stammdaten__land", sa.Text(), nullable=True),
|
||||
sa.Column("Stammdaten__email", sa.Text(), nullable=True),
|
||||
sa.Column("Stammdaten__familienstand", sa.Text(), nullable=True),
|
||||
sa.Column("Stammdaten__festnetznummer", sa.Text(), nullable=True),
|
||||
sa.Column("Stammdaten__geburtsdatum", sa.Date(), nullable=True),
|
||||
sa.Column("Stammdaten__hausnummer", sa.Text(), nullable=True),
|
||||
sa.Column("Stammdaten__herkunftsland", sa.Text(), nullable=True),
|
||||
sa.Column("Stammdaten__mobilfunknummer", sa.Text(), nullable=True),
|
||||
sa.Column("Stammdaten__name", sa.Text(), nullable=True),
|
||||
sa.Column("Stammdaten__ort", sa.Text(), nullable=True),
|
||||
sa.Column("Stammdaten__rueckkehrer", sa.Boolean(), nullable=True),
|
||||
sa.Column("Stammdaten__staatsangehoerigkeit", sa.Text(), nullable=True),
|
||||
sa.Column("Stammdaten__strasse", sa.Text(), nullable=True),
|
||||
sa.Column("Stammdaten__titel", sa.Text(), nullable=True),
|
||||
sa.Column("Stammdaten__vorname", sa.Text(), nullable=True),
|
||||
sa.Column("WeitereInfos__WI_arbeitsstatus", sa.Text(), nullable=True),
|
||||
sa.Column("WeitereInfos__WI_aufenthaltstitel", sa.Text(), nullable=True),
|
||||
sa.Column("WeitereInfos__WI_deutsch_sprache", sa.Text(), nullable=True),
|
||||
sa.Column("WeitereInfos__WI_gueltigkeit_aufenthaltstitel", sa.Date(), nullable=True),
|
||||
sa.Column("WeitereInfos__WI_meldung_institution", sa.Text(), nullable=True),
|
||||
sa.PrimaryKeyConstraint("pers_id"),
|
||||
)
|
||||
op.create_table(
|
||||
"grunderfassung_unternehmen",
|
||||
sa.Column("un_id", sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column("Metadaten_erstellung", wce_crm.db.UTCDateTime(), nullable=True),
|
||||
sa.Column("Metadaten_aktualisierung", wce_crm.db.UTCDateTime(), nullable=True),
|
||||
sa.Column("Metadaten_nutzer", sa.String(length=20), nullable=True),
|
||||
sa.Column("Grunderfassung_fallnummer", sa.Text(), nullable=True),
|
||||
sa.Column("Grunderfassung_notiz", sa.Text(), nullable=True),
|
||||
sa.Column("Partnersuche__kanal_aufmerksamkeit", sa.Text(), nullable=True),
|
||||
sa.Column("Partnersuche__person_suche", sa.Integer(), nullable=True),
|
||||
sa.Column("Partnersuche__un_suche", sa.Integer(), nullable=True),
|
||||
sa.PrimaryKeyConstraint("un_id"),
|
||||
)
|
||||
op.create_table(
|
||||
"zuordnung_personen_unternehmen",
|
||||
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column("un_id", sa.Integer(), nullable=False),
|
||||
sa.Column("pers_id", sa.Integer(), nullable=False),
|
||||
sa.Column("gueltig_ab", wce_crm.db.UTCDateTime(), nullable=False),
|
||||
sa.Column("gueltig_bis", wce_crm.db.UTCDateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(
|
||||
["pers_id"],
|
||||
["grunderfassung_personen.pers_id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["un_id"],
|
||||
["grunderfassung_unternehmen.un_id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table("zuordnung_personen_unternehmen")
|
||||
op.drop_table("grunderfassung_unternehmen")
|
||||
op.drop_table("grunderfassung_personen")
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,73 @@
|
||||
"""adding consulting sessions
|
||||
|
||||
Revision ID: 2afa4e506448
|
||||
Revises: 1a8e957522a4
|
||||
Create Date: 2026-07-09 14:21:24.586706
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
import wce_crm.db
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "2afa4e506448"
|
||||
down_revision: Union[str, Sequence[str], None] = "1a8e957522a4"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"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=True),
|
||||
sa.ForeignKeyConstraint(
|
||||
["pers_id"],
|
||||
["grunderfassung_personen.pers_id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["un_id"],
|
||||
["grunderfassung_unternehmen.un_id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("vorgang_id"),
|
||||
)
|
||||
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=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")
|
||||
op.drop_table("beratung_vorgang")
|
||||
# ### end Alembic commands ###
|
||||
@@ -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 ###
|
||||
@@ -1,91 +0,0 @@
|
||||
"""added new column and renamed table for initial recording
|
||||
|
||||
Revision ID: 5f2af5179c47
|
||||
Revises:
|
||||
Create Date: 2026-06-17 16:06:46.421562
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "5f2af5179c47"
|
||||
down_revision: Union[str, Sequence[str], None] = None
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.rename_table("grunderfassung_unternehmen", "grunderfassung")
|
||||
op.add_column(
|
||||
"grunderfassung",
|
||||
sa.Column("Metadaten_wiedereintrittsdatum", sa.Date, nullable=True, default=None),
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"grunderfassung_unternehmen",
|
||||
sa.Column("erfassung_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("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("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("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("erfassung_id"),
|
||||
)
|
||||
op.drop_table("grunderfassung")
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,47 @@
|
||||
"""change unique constraint to partial index for company person linking table
|
||||
|
||||
Revision ID: 665259cdfdc0
|
||||
Revises: 03d4f000f445
|
||||
Create Date: 2026-07-22 10:14:43.889298
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "665259cdfdc0"
|
||||
down_revision: Union[str, Sequence[str], None] = "03d4f000f445"
|
||||
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("zuordnung_personen_unternehmen", schema=None) as batch_op:
|
||||
batch_op.drop_constraint(batch_op.f("uq_un_id_pers_id"), type_="unique")
|
||||
batch_op.create_index(
|
||||
"uq_aktive_zuordnung",
|
||||
["un_id", "pers_id"],
|
||||
unique=True,
|
||||
sqlite_where=sa.text("gueltig_bis IS NULL"),
|
||||
)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("zuordnung_personen_unternehmen", schema=None) as batch_op:
|
||||
batch_op.drop_index(
|
||||
"uq_aktive_zuordnung", sqlite_where=sa.text("gueltig_bis IS NULL")
|
||||
)
|
||||
batch_op.create_unique_constraint(
|
||||
batch_op.f("uq_un_id_pers_id"), ["un_id", "pers_id"]
|
||||
)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
@@ -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 ###
|
||||
@@ -0,0 +1,41 @@
|
||||
"""timestamps as deletion indicator
|
||||
|
||||
Revision ID: 830416cab29c
|
||||
Revises: c285b0606094
|
||||
Create Date: 2026-07-11 06:12:14.423902
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
import wce_crm.db
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "830416cab29c"
|
||||
down_revision: Union[str, Sequence[str], None] = "c285b0606094"
|
||||
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.add_column(
|
||||
"beratung_einzelberatung",
|
||||
sa.Column("geloescht", wce_crm.db.UTCDateTime(), nullable=True),
|
||||
)
|
||||
op.add_column(
|
||||
"beratung_vorgang", sa.Column("geloescht", wce_crm.db.UTCDateTime(), nullable=True)
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column("beratung_vorgang", "geloescht")
|
||||
op.drop_column("beratung_einzelberatung", "geloescht")
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,42 @@
|
||||
"""timestamps for deletions of initial recording
|
||||
|
||||
Revision ID: 85577ce9f2ab
|
||||
Revises: 830416cab29c
|
||||
Create Date: 2026-07-15 17:06:55.947134
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
import wce_crm.db
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "85577ce9f2ab"
|
||||
down_revision: Union[str, Sequence[str], None] = "830416cab29c"
|
||||
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.add_column(
|
||||
"grunderfassung_personen",
|
||||
sa.Column("geloescht", wce_crm.db.UTCDateTime(), nullable=True),
|
||||
)
|
||||
op.add_column(
|
||||
"grunderfassung_unternehmen",
|
||||
sa.Column("geloescht", wce_crm.db.UTCDateTime(), nullable=True),
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column("grunderfassung_unternehmen", "geloescht")
|
||||
op.drop_column("grunderfassung_personen", "geloescht")
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,38 @@
|
||||
"""documents of initial recording - fix typo in column
|
||||
|
||||
Revision ID: 8704e976f0ba
|
||||
Revises: f74114e74ad9
|
||||
Create Date: 2026-07-31 11:44:08.601177
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "8704e976f0ba"
|
||||
down_revision: Union[str, Sequence[str], None] = "f74114e74ad9"
|
||||
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_Dokumente", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("speicher_pfad", sa.Text(), nullable=False))
|
||||
batch_op.drop_column("speicherpfad")
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("Grunderfassung_Dokumente", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("speicherpfad", sa.TEXT(), nullable=False))
|
||||
batch_op.drop_column("speicher_pfad")
|
||||
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,48 @@
|
||||
"""updated timestamps nullability
|
||||
|
||||
Revision ID: c285b0606094
|
||||
Revises: 2afa4e506448
|
||||
Create Date: 2026-07-10 09:22:12.340425
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "c285b0606094"
|
||||
down_revision: Union[str, Sequence[str], None] = "2afa4e506448"
|
||||
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("beratung_einzelberatung") as batch_op:
|
||||
batch_op.alter_column(
|
||||
"aktualisiert",
|
||||
existing_type=sa.DATETIME(),
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
with op.batch_alter_table("beratung_vorgang") as batch_op:
|
||||
batch_op.alter_column("aktualisiert", existing_type=sa.DATETIME(), nullable=False)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("beratung_einzelberatung") as batch_op:
|
||||
batch_op.alter_column(
|
||||
"aktualisiert",
|
||||
existing_type=sa.DATETIME(),
|
||||
nullable=True,
|
||||
)
|
||||
|
||||
with op.batch_alter_table("beratung_vorgang") as batch_op:
|
||||
batch_op.alter_column("aktualisiert", existing_type=sa.DATETIME(), nullable=True)
|
||||
# ### end Alembic commands ###
|
||||
@@ -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 ###
|
||||
@@ -0,0 +1,52 @@
|
||||
"""update schema for documents of initial recording
|
||||
|
||||
Revision ID: f74114e74ad9
|
||||
Revises: 5dfa1fc1dfdf
|
||||
Create Date: 2026-07-31 11:02:57.484651
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
import wce_crm.db
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "f74114e74ad9"
|
||||
down_revision: Union[str, Sequence[str], None] = "5dfa1fc1dfdf"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"Grunderfassung_Dokumente",
|
||||
sa.Column("id", sa.Integer(), autoincrement=True, 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.Column("speicherpfad", sa.Text(), nullable=False),
|
||||
sa.Column("dateiname_original", sa.Text(), nullable=False),
|
||||
sa.Column("dateigroesse_bytes", sa.Integer(), nullable=True),
|
||||
sa.Column("datum_hinzugefuegt", wce_crm.db.UTCDateTime(), nullable=False),
|
||||
sa.Column("mime_type", sa.Text(), nullable=False),
|
||||
sa.Column("anmerkung", sa.Text(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["pers_id"],
|
||||
["Grunderfassung_Personen.pers_id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table("Grunderfassung_Dokumente")
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,119 @@
|
||||
Ext_Kontaktliste.Master
|
||||
ma_id int pk
|
||||
|
||||
|
||||
Ext_Kontaktliste.Ansprechpartner
|
||||
an_id int pk
|
||||
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
|
||||
|
||||
|
||||
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}
|
||||
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
|
||||
Grunderfassung_notiz text nullable
|
||||
HoehereBildung text nullable
|
||||
Kontaktperson__KP_adresse text nullable
|
||||
Kontaktperson__KP_anrede_anschrift text nullable
|
||||
Kontaktperson__KP_email text nullable
|
||||
Kontaktperson__KP_festnetznummer text nullable
|
||||
Kontaktperson__KP_funktion_beziehung text nullable
|
||||
Kontaktperson__KP_mobilfunknummer text nullable
|
||||
Kontaktperson__KP_name_partner text nullable
|
||||
Kontaktperson__KP_titel text nullable
|
||||
Kontaktperson__KP_vorname text nullable
|
||||
Kontaktperson__KP_email text nullable
|
||||
Kontaktperson__KP_email text nullable
|
||||
Kontaktperson__KP_email text nullable
|
||||
Projektrelevanz__relevanz text nullable
|
||||
Projektrelevanz__foerderperiode text nullable
|
||||
Schulbildung text nullable
|
||||
Stammdaten__kanal_aufmerksamkeit text nullable
|
||||
Stammdaten__PLZ text nullable
|
||||
Stammdaten__anrede_anschrift text nullable
|
||||
Stammdaten__anzahl_kinder__alter text nullable
|
||||
Stammdaten__anzahl_kinder__anzahl int nullable
|
||||
Stammdaten__aufenthaltsort text nullable
|
||||
Stammdaten__bundesland text nullable
|
||||
Stammdaten__land text nullable
|
||||
Stammdaten__email text nullable
|
||||
Stammdaten__familienstand text nullable
|
||||
Stammdaten__festnetznummer text nullable
|
||||
Stammdaten__geburtsdatum date nullable
|
||||
Stammdaten__hausnummer text nullable
|
||||
Stammdaten__herkunftsland text nullable
|
||||
Stammdaten__mobilfunknummer text nullable
|
||||
Stammdaten__name text nullable
|
||||
Stammdaten__ort text nullable
|
||||
Stammdaten__rueckkehrer bool nullable
|
||||
Stammdaten__staatsangehoerigkeit text nullable
|
||||
Stammdaten__strasse text nullable
|
||||
Stammdaten__titel text nullable
|
||||
Stammdaten__vorname text nullable
|
||||
WeitereInfos__WI_arbeitsstatus text nullable
|
||||
WeitereInfos__WI_aufenthaltstitel text nullable
|
||||
WeitereInfos__WI_deutsch_sprache text nullable
|
||||
WeitereInfos__WI_gueltigkeit_aufenthaltstitel text nullable
|
||||
WeitereInfos__WI_meldung_institution text nullable
|
||||
|
||||
|
||||
Grunderfassung_Dokumente
|
||||
id int pk {autoIncrement}
|
||||
pers_id int -> Grunderfassung_Personen(pers_id)
|
||||
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
|
||||
speicher_pfad text
|
||||
dateiname_original text
|
||||
dateigroesse_bytes int nullable
|
||||
datum_hinzugefuegt "timestamp with time zone"
|
||||
mime_type text
|
||||
anmerkung text
|
||||
|
||||
|
||||
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 -> 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)
|
||||
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
|
||||
|
||||
|
||||
Beratung_Einzelberatung | Änderungen müssen über einen DB-Trigger an Elterntabelle zurückgeschrieben werden
|
||||
beratung_id int pk {autoIncrement}
|
||||
vorgang_id int -> Beratung_Vorgang(vorgang_id) | Jede Beratung muss zu einem Beratungsvorgang gehören.
|
||||
erstellt "timestamp with time zone"=`datetime.datetime.now(datetime.UTC)` | gekoppelt mit Elterntabelle 'Beratung_Vorgang' über Trigger
|
||||
aktualisiert "timestamp with time zone"=`datetime.datetime.now(datetime.UTC)` {on_update: `datetime.datetime.now(datetime.UTC)`} | gekoppelt mit Elterntabelle 'Beratung_Vorgang' über Trigger
|
||||
nutzer_id int | externe Nutzerverwaltung
|
||||
nutzer_name text | externe Nutzerverwaltung
|
||||
zeitstempel "timestamp with time zone" | Nutzereingabe
|
||||
ansprechpartner text
|
||||
kommunikationsweg text
|
||||
thema_crm_matrix text nullable | Verlinkung ins CRM, genaue Gestaltung noch unklar
|
||||
anmerkungen text nullable
|
||||
rueckmeldung txt nullable
|
||||
geloescht "timestamp with time zone" nullable
|
||||
+3
-2
@@ -1,5 +1,6 @@
|
||||
DOPT_STOP_FOLDER_NAME=python
|
||||
DOPT_DB_CRM=data/wce_crm.db
|
||||
DOPT_DB_MAIN=data/wce_grunderfassung.db
|
||||
DOPT_DB_CRM=data/db/wce_crm.db
|
||||
DOPT_DB_MAIN=data/db/wce_grunderfassung.db
|
||||
DOPT_PATH_LOGGING=data/logs
|
||||
DOPT_PATH_FILE_STORAGE=data/files
|
||||
DOPT_ALEMBIC_BASE=python/alembic
|
||||
@@ -1,5 +1,34 @@
|
||||
# Versionshistorie (Changelog)
|
||||
|
||||
## 31.07.2026 (Version: v0.1.1dev) (Tag: ExtTest-20260731)
|
||||
|
||||
- Umsetzung der folgenden Anforderungen (IDs):
|
||||
- 1000
|
||||
- 1002
|
||||
- 1003
|
||||
- 1004
|
||||
- 1005
|
||||
- 1006
|
||||
- 1007
|
||||
- 1008
|
||||
- 1009
|
||||
- 1011
|
||||
- 1012
|
||||
- 1013
|
||||
- 1014
|
||||
- Grunderfassung komplett überarbeitet:
|
||||
- mehr Übersicht durch aufklappbare Bereiche
|
||||
- Hinzufügen und Verwalten von Dokumenten
|
||||
- weitere gemäß Anforderungsliste
|
||||
- Unternehmensprofil:
|
||||
- hinzugefügt
|
||||
- Möglichkeit zur Anlage von Beratungsvorgängen
|
||||
- Beratungsvorgänge:
|
||||
- Anlegen über Startseite "Neu" und über Unternehmensprofil
|
||||
- nachträgliche Verknüpfung mit Unternehmen/Personen (kontextabhängig)
|
||||
- Hinzufügen/Verwalten mehrerer Sessions
|
||||
- Platzhalter für Verlinkung in CRM-Matrix (mehrere Elemente sollen später ausgewählt werden)
|
||||
|
||||
## 17.06.2026 (Version: v0.1.1dev) (Tag: ExtTest-20260617)
|
||||
|
||||
- Grunderfassung für Individualpersonen hinzugefügt
|
||||
|
||||
@@ -3571,29 +3571,29 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.15.10"
|
||||
version = "0.15.21"
|
||||
requires_python = ">=3.7"
|
||||
summary = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
groups = ["lint"]
|
||||
files = [
|
||||
{file = "ruff-0.15.10-py3-none-linux_armv6l.whl", hash = "sha256:0744e31482f8f7d0d10a11fcbf897af272fefdfcb10f5af907b18c2813ff4d5f"},
|
||||
{file = "ruff-0.15.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b1e7c16ea0ff5a53b7c2df52d947e685973049be1cdfe2b59a9c43601897b22e"},
|
||||
{file = "ruff-0.15.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:93cc06a19e5155b4441dd72808fdf84290d84ad8a39ca3b0f994363ade4cebb1"},
|
||||
{file = "ruff-0.15.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83e1dd04312997c99ea6965df66a14fb4f03ba978564574ffc68b0d61fd3989e"},
|
||||
{file = "ruff-0.15.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8154d43684e4333360fedd11aaa40b1b08a4e37d8ffa9d95fee6fa5b37b6fab1"},
|
||||
{file = "ruff-0.15.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ab88715f3a6deb6bde6c227f3a123410bec7b855c3ae331b4c006189e895cef"},
|
||||
{file = "ruff-0.15.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a768ff5969b4f44c349d48edf4ab4f91eddb27fd9d77799598e130fb628aa158"},
|
||||
{file = "ruff-0.15.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ee3ef42dab7078bda5ff6a1bcba8539e9857deb447132ad5566a038674540d0"},
|
||||
{file = "ruff-0.15.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51cb8cc943e891ba99989dd92d61e29b1d231e14811db9be6440ecf25d5c1609"},
|
||||
{file = "ruff-0.15.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e59c9bdc056a320fb9ea1700a8d591718b8faf78af065484e801258d3a76bc3f"},
|
||||
{file = "ruff-0.15.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:136c00ca2f47b0018b073f28cb5c1506642a830ea941a60354b0e8bc8076b151"},
|
||||
{file = "ruff-0.15.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8b80a2f3c9c8a950d6237f2ca12b206bccff626139be9fa005f14feb881a1ae8"},
|
||||
{file = "ruff-0.15.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e3e53c588164dc025b671c9df2462429d60357ea91af7e92e9d56c565a9f1b07"},
|
||||
{file = "ruff-0.15.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b0c52744cf9f143a393e284125d2576140b68264a93c6716464e129a3e9adb48"},
|
||||
{file = "ruff-0.15.10-py3-none-win32.whl", hash = "sha256:d4272e87e801e9a27a2e8df7b21011c909d9ddd82f4f3281d269b6ba19789ca5"},
|
||||
{file = "ruff-0.15.10-py3-none-win_amd64.whl", hash = "sha256:28cb32d53203242d403d819fd6983152489b12e4a3ae44993543d6fe62ab42ed"},
|
||||
{file = "ruff-0.15.10-py3-none-win_arm64.whl", hash = "sha256:601d1610a9e1f1c2165a4f561eeaa2e2ea1e97f3287c5aa258d3dab8b57c6188"},
|
||||
{file = "ruff-0.15.10.tar.gz", hash = "sha256:d1f86e67ebfdef88e00faefa1552b5e510e1d35f3be7d423dc7e84e63788c94e"},
|
||||
{file = "ruff-0.15.21-py3-none-linux_armv6l.whl", hash = "sha256:63ea0e965e5d73c90e95b2434beeafc70820536717f561b32ab6e777cb9bdf5d"},
|
||||
{file = "ruff-0.15.21-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0f212c5d7d54c01bbfe6dcab02b724a39300f3e34ed7acbe995ccb320a2c58bd"},
|
||||
{file = "ruff-0.15.21-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e6312e41bc96791299614995ea3a977c5857c3b5662b1ecef6755b02b87cb646"},
|
||||
{file = "ruff-0.15.21-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01d65b4831c6b2a4ba8ee6faa84049d44d982b7a706e622c4094c509e51673be"},
|
||||
{file = "ruff-0.15.21-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2c5a913a589120ce67933d5d05fd6ddbcc2481c6a054980ee767f7414c72b4fd"},
|
||||
{file = "ruff-0.15.21-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef04b681d02ad4dc9620f00f83ac5c22f652d0e9a9cfe431d219b16ad5ccc41"},
|
||||
{file = "ruff-0.15.21-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16d090c0740916594157e75b80d666eab8e78083b39b3b0e1d698f4670a17b86"},
|
||||
{file = "ruff-0.15.21-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a10e74757dd65004d779b73e2f3c5210156d9980b41224d50d2ebcf1db51e67"},
|
||||
{file = "ruff-0.15.21-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bab0905d2f29e0d9fbc3c373ed23db0095edaa3f71f1f4f519ec15134d9e85c8"},
|
||||
{file = "ruff-0.15.21-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:00eca240af5789fec6fe7df74c088cc1f9644ed83027113468efba7c92b94075"},
|
||||
{file = "ruff-0.15.21-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:262ab31557a75141325e32d3357f3597645a7f084e732b6b054dde428ecd9341"},
|
||||
{file = "ruff-0.15.21-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:659c4e7a4212f83306045ec7c5e5a356d16d9a6ef4ae0c7a4d872914fc655d9d"},
|
||||
{file = "ruff-0.15.21-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9e866eab611a5f959d36df2d10e446973a3610bc42b0c15b31dc27977d59c233"},
|
||||
{file = "ruff-0.15.21-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e89bc93c0d3803ba870b55c29671bad9dc6d94bb1eb181b056b52eb05b52854f"},
|
||||
{file = "ruff-0.15.21-py3-none-win32.whl", hash = "sha256:01f8d5be84823c172b389e123174f781f9daf86d6c58719d603f941932195cdd"},
|
||||
{file = "ruff-0.15.21-py3-none-win_amd64.whl", hash = "sha256:d4b8d9a2f0f12b816b50447f6eccb9f4bb01a6b82c86b50fb3b5354b458dc6d3"},
|
||||
{file = "ruff-0.15.21-py3-none-win_arm64.whl", hash = "sha256:6e83115d4b9377c1cbc13abf0e051f069fab0ef815ea0504a8a008cee24dd0a8"},
|
||||
{file = "ruff-0.15.21.tar.gz", hash = "sha256:d0cfc841c572283c36548f82664a54ce6565567f1b0d5b4cf2caac693d8b7500"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -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
|
||||
# %%
|
||||
+257
-19
@@ -8,43 +8,281 @@ 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
|
||||
crm_path = constants.Config.DB_PATH_CRM
|
||||
crm_path = constants.Config.DB_PATH_KONTAKTLISTE
|
||||
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.front_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)
|
||||
|
||||
+11
-3
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "wce-crm"
|
||||
version = "0.1.1dev10"
|
||||
version = "0.1.1dev11"
|
||||
description = "GUI for CRM of NAFKA project with WCE"
|
||||
authors = [
|
||||
{name = "d-opt GmbH, resp. Florian Förster", email = "f.foerster@d-opt.com"},
|
||||
@@ -14,6 +14,14 @@ license = {text = "LicenseRef-Proprietary"}
|
||||
requires = ["pdm-backend"]
|
||||
build-backend = "pdm.backend"
|
||||
|
||||
[tool.pyright]
|
||||
typeCheckingMode = "basic"
|
||||
pythonVersion = "3.11"
|
||||
reportUnusedImport = "none"
|
||||
reportUnusedVariable = "warning"
|
||||
reportUnusedClass = "warning"
|
||||
reportUnusedFunction = "warning"
|
||||
reportPrivateUsage = "error"
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 94
|
||||
@@ -26,7 +34,7 @@ quote-style = "double"
|
||||
skip-magic-trailing-comma = false
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "I"]
|
||||
select = ["E", "F", "I", "SLF"]
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
extra-standard-library = ["typing_extensions"]
|
||||
@@ -71,7 +79,7 @@ directory = "reports/coverage"
|
||||
|
||||
|
||||
[tool.bumpversion]
|
||||
current_version = "0.1.1dev10"
|
||||
current_version = "0.1.1dev11"
|
||||
parse = """(?x)
|
||||
(?P<major>0|[1-9]\\d*)\\.
|
||||
(?P<minor>0|[1-9]\\d*)\\.
|
||||
|
||||
+16
-10
@@ -45,15 +45,10 @@ Write-Output "Deleted outstanding artifacts successfully"
|
||||
|
||||
Write-Output "Create data folder..."
|
||||
$pattern = "dopt_nafka_wce-crm_v*"
|
||||
$data_folder = Join-Path -Path $ENV_PATH -ChildPath 'data'
|
||||
|
||||
if (-not (Test-Path -Path $data_folder)){
|
||||
Write-Output "[PWSH] Data path not existing. Create..."
|
||||
New-Item -Path $data_folder -ItemType Directory
|
||||
}
|
||||
else {
|
||||
Write-Output "Data path already exists."
|
||||
}
|
||||
create_folder -base_path $ENV_PATH -folder_name 'data' -recreate
|
||||
create_folder -base_path $ENV_PATH -folder_name 'data/db'
|
||||
create_folder -base_path $ENV_PATH -folder_name 'data/files'
|
||||
|
||||
|
||||
Write-Output "Building package..."
|
||||
@@ -93,10 +88,10 @@ if ($? -eq $false){
|
||||
}
|
||||
Write-Output "Successfully installed package"
|
||||
|
||||
# copy database file
|
||||
# copy database files
|
||||
Write-Output "Copying database files..."
|
||||
$copy_file = Join-Path -Path $SRC_PATH -ChildPath 'data/db/wce_grunderfassung.db'
|
||||
$dest_file = Join-Path -Path $ENV_PATH -ChildPath 'data'
|
||||
$dest_file = Join-Path -Path $ENV_PATH -ChildPath 'data/db'
|
||||
Copy-Item -Path $copy_file -Destination $dest_file -Force
|
||||
if ($? -eq $false){
|
||||
Write-Output "[PWSH] Exiting script because there were errors while copying database 'Grunderfassung'"
|
||||
@@ -110,6 +105,17 @@ if ($? -eq $false){
|
||||
}
|
||||
Write-Output "Copied database files successfully"
|
||||
|
||||
# copy content files
|
||||
Write-Output "Copying content files (documents)..."
|
||||
$copy_file = Join-Path -Path $SRC_PATH -ChildPath 'data/files'
|
||||
$dest_file = Join-Path -Path $ENV_PATH -ChildPath 'data/'
|
||||
Copy-Item -Path $copy_file -Destination $dest_file -Force -Recurse
|
||||
if ($? -eq $false){
|
||||
Write-Output "[PWSH] Exiting script because there were errors while copying documents"
|
||||
Exit
|
||||
}
|
||||
Write-Output "Copied content files (documents) successfully"
|
||||
|
||||
# copy alembic files
|
||||
Write-Output "Copying alembic files..."
|
||||
$copy_file = Join-Path -Path $SRC_PATH -ChildPath 'alembic'
|
||||
|
||||
@@ -5,4 +5,5 @@
|
||||
- DOPT_DB_CRM: path to CRM database, relative to base path
|
||||
- DOPT_DB_MAIN: path to main database, relative to base path
|
||||
- DOPT_PATH_LOGGING: path to logging folder, relative to base path
|
||||
- DOPT_PATH_FILE_STORAGE: path to folder for saving files
|
||||
- DOPT_ALEMBIC_BASE: path to all relevant alembic file
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
def _get_leafs(data):
|
||||
if isinstance(data, dict):
|
||||
for value in data.values():
|
||||
yield from _get_leafs(value)
|
||||
elif isinstance(data, (list, tuple, set)):
|
||||
for item in data:
|
||||
yield from _get_leafs(item)
|
||||
else:
|
||||
yield data
|
||||
|
||||
|
||||
def _get_leaf_dicts(data):
|
||||
if isinstance(data, dict):
|
||||
has_inner_dicts = False
|
||||
|
||||
for value in data.values():
|
||||
for inner_dict in _get_leaf_dicts(value):
|
||||
has_inner_dicts = True
|
||||
yield inner_dict
|
||||
|
||||
if not has_inner_dicts:
|
||||
yield data
|
||||
|
||||
elif isinstance(data, (list, tuple, set)):
|
||||
for item in data:
|
||||
yield from _get_leaf_dicts(item)
|
||||
+718
-126
@@ -1,68 +1,59 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses as dc
|
||||
import copy
|
||||
import datetime
|
||||
from typing import Any, TypeAlias, TypedDict, cast
|
||||
from collections.abc import Container, Mapping
|
||||
from pprint import pformat
|
||||
from typing import TYPE_CHECKING, Any, Final, TypeVar, cast
|
||||
|
||||
import polars as pl
|
||||
import sqlalchemy as sql
|
||||
import sqlalchemy as sa
|
||||
from dopt_basics.result_pattern import wrap_result
|
||||
from sqlalchemy.dialects.sqlite import insert as sqlite_insert
|
||||
|
||||
from wce_crm import db
|
||||
from wce_crm.logging import logger_back as logger
|
||||
from wce_crm.backend.document_service import (
|
||||
DocumentRepository,
|
||||
DocumentService,
|
||||
FileStorageService,
|
||||
)
|
||||
from wce_crm.constants import TIMEZONE_CEST, Config
|
||||
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_backend as logger
|
||||
from wce_crm.types import (
|
||||
CompanyInfo,
|
||||
CompanyProfileConsultationEntry,
|
||||
CompanyProfileConsultations,
|
||||
ConsultingType,
|
||||
ContactPersonInfo,
|
||||
EntityIds,
|
||||
EntityType,
|
||||
LinkingConsultationsEntryCompany,
|
||||
LinkingConsultationsEntryPerson,
|
||||
MainPageEntry,
|
||||
RecordingType,
|
||||
)
|
||||
|
||||
InitRecId: TypeAlias = int
|
||||
if TYPE_CHECKING:
|
||||
from dopt_basics.datastructures import DualDict
|
||||
|
||||
from wce_crm.types import ConsId, ExtAnId, ExtMaId, RecId
|
||||
|
||||
class CompanyInfo(TypedDict):
|
||||
ma_id: str
|
||||
wce_id: str
|
||||
ma_unternehmensname: str
|
||||
ma_branche: str
|
||||
ma_strasse: str
|
||||
ma_hausnummer: str
|
||||
ma_plz: str
|
||||
ma_ort: str
|
||||
ma_plz_postfach: str
|
||||
ma_postfach: str
|
||||
ma_website: str
|
||||
ma_mail: str
|
||||
ma_telefonnummer: str
|
||||
ma_faxnummer: str
|
||||
ma_ersteintrag_datum: str
|
||||
ma_aktualisierung_datum: str
|
||||
ma_aktualisierung_nutzer: str
|
||||
ma_sollprozess: str
|
||||
ma_auslaendische_mitarbeiter: str
|
||||
ma_quelle_information: str
|
||||
ma_bemerkung: str
|
||||
ma_kontakt: str
|
||||
ma_schlagworte: str
|
||||
ma_archiviert: str
|
||||
K = TypeVar("K")
|
||||
V = TypeVar("V")
|
||||
|
||||
|
||||
class ContactPersonInfo(TypedDict):
|
||||
an_id: str
|
||||
ma_id: str
|
||||
wce_id: str
|
||||
st_id: str
|
||||
an_sachgebiet: str
|
||||
an_anrede: str
|
||||
an_titel: str
|
||||
an_nachname: str
|
||||
an_vorname: str
|
||||
an_position: str
|
||||
an_mail: str
|
||||
an_festnetz: str
|
||||
an_mobil: str
|
||||
an_faxnummer: str
|
||||
an_hauptansprechpartner: str
|
||||
an_anrede_anschrift: str
|
||||
an_bemerkung: str
|
||||
an_aktualisierung_datum: str
|
||||
an_aktualisierung_nutzer: str
|
||||
an_letztes_kontaktdatum: str
|
||||
an_ersteintrag_datum: str
|
||||
an_archiviert: str
|
||||
INITREC_DOC_SERVICE: Final[DocumentService] = DocumentService(
|
||||
repo=DocumentRepository(file_storage_dir=Config.PATH_FILE_STORAGE),
|
||||
storage=FileStorageService(storage_dir=Config.PATH_FILE_STORAGE),
|
||||
)
|
||||
|
||||
|
||||
def _transform_for_gui_output(
|
||||
@@ -99,8 +90,9 @@ def initrec_comp_search_choices() -> tuple[tuple[str, int], ...]:
|
||||
return tuple(zip(df["dedupl"], df["ma_id"]))
|
||||
|
||||
|
||||
def initrec_comp_search_get_info(
|
||||
ma_id: int,
|
||||
# TODO change to direct SQL interaction
|
||||
def external_company_get_info(
|
||||
ma_id: ExtMaId,
|
||||
) -> CompanyInfo:
|
||||
logger.debug("[Call backend] comp_search_get_info")
|
||||
df = db.DF_CRM_MASTER.filter(pl.col.ma_id == ma_id)
|
||||
@@ -112,7 +104,7 @@ def initrec_comp_search_get_info(
|
||||
|
||||
|
||||
def initrec_comp_contact_person_search_choices(
|
||||
ma_id: int | None,
|
||||
ma_id: ExtMaId | None,
|
||||
use_both_names: bool,
|
||||
) -> tuple[tuple[str, int], ...]:
|
||||
# TODO no reload functionality
|
||||
@@ -140,7 +132,7 @@ def initrec_comp_contact_person_search_choices(
|
||||
|
||||
|
||||
def initrec_comp_contact_person_search_get_info(
|
||||
an_id: int,
|
||||
an_id: ExtAnId,
|
||||
) -> ContactPersonInfo:
|
||||
logger.debug("[Call backend] contact_person_search_get_info")
|
||||
df = db.DF_CONTACT_PERSON.filter(pl.col.an_id == an_id)
|
||||
@@ -151,105 +143,705 @@ def initrec_comp_contact_person_search_get_info(
|
||||
return cast(ContactPersonInfo, df.row(0, named=True))
|
||||
|
||||
|
||||
def initrec_insert_initial_recording(
|
||||
data: dict[str, Any],
|
||||
) -> InitRecId:
|
||||
logger.debug("[Call backend] insert_initial_recording")
|
||||
stmt = db.grunderfassung.insert()
|
||||
with db.ENGINE.begin() as conn:
|
||||
ret = conn.execute(stmt, data)
|
||||
# // internals
|
||||
def split_dict(
|
||||
dic: dict[K, V],
|
||||
split_keys: Container[K],
|
||||
) -> tuple[dict[K, V], dict[K, V]]:
|
||||
keys_in: dict[K, V] = {}
|
||||
keys_out = copy.deepcopy(dic)
|
||||
|
||||
for check_key in dic:
|
||||
if check_key in split_keys:
|
||||
keys_in[check_key] = keys_out.pop(check_key)
|
||||
|
||||
return keys_in, keys_out
|
||||
|
||||
|
||||
def dic_deep_update(
|
||||
dic: dict[K, V],
|
||||
data: Mapping[K, V],
|
||||
) -> dict[K, V]:
|
||||
dic_copy = copy.deepcopy(dic)
|
||||
dic_copy.update(data)
|
||||
|
||||
return dic_copy
|
||||
|
||||
|
||||
def _rename_db_data(
|
||||
db_data: dict[str, Any],
|
||||
mapping: DualDict,
|
||||
) -> 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:
|
||||
|
||||
with db.ENGINE.begin() as conn:
|
||||
# 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
|
||||
|
||||
if data.ids.pers_id is None:
|
||||
logger.debug("[InitRec -- backend] Insert...")
|
||||
|
||||
stmt = sa.insert(db.t_grunderfassung_personen).returning(
|
||||
db.t_grunderfassung_personen
|
||||
)
|
||||
stmt_compiled = str(stmt.compile(db.ENGINE))
|
||||
logger.debug(
|
||||
"[InitRec -- backend] Data to insert:\nStatement: %s\n%s",
|
||||
stmt_compiled,
|
||||
pformat(dump_data),
|
||||
)
|
||||
|
||||
ret = conn.execute(stmt, dump_data)
|
||||
|
||||
from_db = ret.mappings().fetchall()
|
||||
assert len(from_db) == 1, "expected excactly one returned row"
|
||||
data_from_db = 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.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 = dict(from_db[0])
|
||||
data_from_db = _rename_db_data(
|
||||
data_from_db, FIELD_DB_MAPPING_GRUNDERERFASSUNG_PERSONEN
|
||||
)
|
||||
|
||||
# data.geloescht = data_from_db["geloescht"]
|
||||
# data.form_data.person["Metadaten_aktualisierung"] = data_from_db[
|
||||
# "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
|
||||
|
||||
# documents
|
||||
assert data.ids.pers_id
|
||||
data.docs = INITREC_DOC_SERVICE.to_db(conn, pers_id=data.ids.pers_id, docs=data.docs)
|
||||
|
||||
# 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 page_initrec_from_db(
|
||||
ids: EntityIds,
|
||||
) -> InitRec:
|
||||
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_person_table = dict(from_db[0])
|
||||
|
||||
data_person_table = _rename_db_data(
|
||||
data_person_table, FIELD_DB_MAPPING_GRUNDERERFASSUNG_PERSONEN
|
||||
)
|
||||
geloescht = data_person_table["geloescht"]
|
||||
del data_person_table["geloescht"]
|
||||
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
|
||||
)
|
||||
|
||||
# documents
|
||||
with db.ENGINE.connect() as conn:
|
||||
docs = INITREC_DOC_SERVICE.from_db(conn, pers_id=ids.pers_id)
|
||||
|
||||
return InitRec(
|
||||
ids=ids,
|
||||
recording_type=recording_type,
|
||||
geloescht=geloescht,
|
||||
form_data=form_data,
|
||||
docs=docs,
|
||||
)
|
||||
|
||||
|
||||
@wrap_result(10)
|
||||
def page_consulting_to_db(
|
||||
consultation_data: Beratungsgespraech_Vorgang,
|
||||
) -> Beratungsgespraech_Vorgang:
|
||||
logger.debug("[Consulting Page] Call database saving routine...")
|
||||
|
||||
with db.ENGINE.begin() as conn:
|
||||
if consultation_data.vorgang_id is None:
|
||||
# insert new "Vorgang"
|
||||
insert_data_process = consultation_data.model_dump(
|
||||
exclude={"vorgang_id", "beratungen"}
|
||||
)
|
||||
logger.debug(
|
||||
"[Consulting Page] Call insert 'Vorgang' with data:\n%s",
|
||||
pformat(insert_data_process),
|
||||
)
|
||||
|
||||
stmt = db.t_beratung_vorgang.insert()
|
||||
ret = conn.execute(stmt, insert_data_process)
|
||||
if ret.rowcount == 0:
|
||||
raise IOError("Entry was not inserted correctly")
|
||||
|
||||
prim_keys = ret.inserted_primary_key
|
||||
assert prim_keys
|
||||
|
||||
return prim_keys[0]
|
||||
consultation_data.vorgang_id = cast("ConsId", prim_keys[0])
|
||||
logger.debug("[Consulting Page] Inserted 'Vorgang' successfully")
|
||||
|
||||
else:
|
||||
logger.debug(
|
||||
"[Consulting Page] VorgangID already set. ID: %d. Update...",
|
||||
consultation_data.vorgang_id,
|
||||
)
|
||||
update_data_process = consultation_data.model_dump(exclude={"beratungen"})
|
||||
stmt = db.t_beratung_vorgang.update().where(
|
||||
db.t_beratung_vorgang.c.vorgang_id == consultation_data.vorgang_id
|
||||
)
|
||||
conn.execute(stmt, update_data_process)
|
||||
|
||||
rows_for_db_insert: list[dict[str, Any]] = []
|
||||
rows_for_db_update: list[dict[str, Any]] = []
|
||||
|
||||
cons_sessions_inserted: list[Beratungsgespraech_Einzelgespraech] = []
|
||||
for cons_session in consultation_data.beratungen:
|
||||
cons_session.vorgang_id = consultation_data.vorgang_id
|
||||
if cons_session.beratung_id is None:
|
||||
row_data = cons_session.model_dump(exclude={"beratung_id"})
|
||||
rows_for_db_insert.append(row_data)
|
||||
cons_sessions_inserted.append(cons_session)
|
||||
else:
|
||||
row_data = cons_session.model_dump()
|
||||
# new bind param to avoid name clashes
|
||||
row_data["b_beratung_id"] = row_data["beratung_id"]
|
||||
del row_data["beratung_id"]
|
||||
rows_for_db_update.append(row_data)
|
||||
|
||||
if rows_for_db_update:
|
||||
# ... update
|
||||
logger.debug(
|
||||
"[Consulting Page] Call update for sessions:\n%s", pformat(rows_for_db_update)
|
||||
)
|
||||
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)
|
||||
|
||||
if rows_for_db_insert:
|
||||
# ... insert
|
||||
logger.debug(
|
||||
"[Consulting Page] Call insert for sessions:\n%s", pformat(rows_for_db_insert)
|
||||
)
|
||||
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()])
|
||||
|
||||
assert len(cons_sessions_inserted) == len(new_cons_session_ids)
|
||||
for cons_session, new_id in zip(cons_sessions_inserted, new_cons_session_ids):
|
||||
cons_session.beratung_id = new_id
|
||||
|
||||
return consultation_data
|
||||
|
||||
|
||||
def initrec_update_initial_recording(
|
||||
id_: InitRecId,
|
||||
data: dict[str, Any],
|
||||
@wrap_result(10)
|
||||
def page_consulting_delete(
|
||||
cons_id: ConsId,
|
||||
) -> None:
|
||||
logger.debug("[Call backend] update_initial_recording")
|
||||
stmt = db.grunderfassung.update().where(db.grunderfassung.c.erfassung_id == id_)
|
||||
timestamp_deleted = datetime.datetime.now(datetime.UTC)
|
||||
dump_data: dict[str, Any] = {"geloescht": timestamp_deleted}
|
||||
|
||||
with db.ENGINE.begin() as conn:
|
||||
conn.execute(stmt, data)
|
||||
stmt = db.t_beratung_vorgang.update().where(
|
||||
db.t_beratung_vorgang.c.vorgang_id == cons_id
|
||||
)
|
||||
conn.execute(stmt, dump_data)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def initrec_get_initial_recording(
|
||||
id_: InitRecId,
|
||||
) -> dict[str, Any]:
|
||||
logger.debug("[Call backend] get_initial_recording")
|
||||
stmt = db.grunderfassung.select().where(db.grunderfassung.c.erfassung_id == id_)
|
||||
with db.ENGINE.begin() as conn:
|
||||
@wrap_result(11)
|
||||
def page_consulting_from_db(
|
||||
cons_id: ConsId,
|
||||
) -> Beratungsgespraech_Vorgang:
|
||||
# get "Vorgang" and all associated sessions to instantiate Pydantic data model
|
||||
logger.debug("[Consulting Page] Call database reading routine...")
|
||||
|
||||
with db.ENGINE.connect() as conn:
|
||||
# get the consultation process
|
||||
stmt = db.t_beratung_vorgang.select().where(
|
||||
db.t_beratung_vorgang.c.vorgang_id == cons_id
|
||||
)
|
||||
ret = conn.execute(stmt)
|
||||
|
||||
if ret.rowcount == 0:
|
||||
raise KeyError(f"Database ID {id_} not found")
|
||||
results = ret.mappings().all()
|
||||
|
||||
row = ret.fetchone()
|
||||
assert row, "row was not obtained"
|
||||
if not results:
|
||||
raise KeyError(f"Database ID {cons_id} not found")
|
||||
|
||||
return row._asdict()
|
||||
assert len(results) == 1, "more than one consulting process obtained"
|
||||
consultation_data_db = results[0]
|
||||
consultation_data = Beratungsgespraech_Vorgang(
|
||||
vorgang_id=consultation_data_db["vorgang_id"],
|
||||
un_id=consultation_data_db["un_id"],
|
||||
pers_id=consultation_data_db["pers_id"],
|
||||
titel=consultation_data_db["titel"],
|
||||
beratungs_typ=consultation_data_db["beratungs_typ"],
|
||||
erstellt=consultation_data_db["erstellt"],
|
||||
aktualisiert=consultation_data_db["aktualisiert"],
|
||||
geloescht=consultation_data_db["geloescht"],
|
||||
beratungen=[],
|
||||
)
|
||||
|
||||
if consultation_data.geloescht is not None:
|
||||
logger.info(
|
||||
(
|
||||
"[Consulting Page] Obtained entry for ID=%d. This entry was marked "
|
||||
"as deleted."
|
||||
),
|
||||
cons_id,
|
||||
)
|
||||
|
||||
# get all consultation sessions of this process, only the ones
|
||||
# which are not marked as deleted
|
||||
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()
|
||||
# empty results possible
|
||||
if not cons_sessions:
|
||||
logger.debug("[Consulting Page] No sessions, return directly...")
|
||||
return consultation_data
|
||||
|
||||
for session in cons_sessions:
|
||||
cons_session_pydantic = Beratungsgespraech_Einzelgespraech(
|
||||
beratung_id=session["beratung_id"],
|
||||
vorgang_id=session["vorgang_id"],
|
||||
nutzer_id=session["nutzer_id"],
|
||||
nutzer_name=session["nutzer_name"],
|
||||
zeitstempel=session["zeitstempel"],
|
||||
ansprechpartner=session["ansprechpartner"],
|
||||
kommunikationsweg=session["kommunikationsweg"],
|
||||
thema_crm_matrix=session["thema_crm_matrix"],
|
||||
anmerkungen=session["anmerkungen"],
|
||||
rueckmeldung=session["rueckmeldung"],
|
||||
erstellt=session["erstellt"],
|
||||
aktualisiert=session["aktualisiert"],
|
||||
)
|
||||
consultation_data.beratungen.append(cons_session_pydantic)
|
||||
|
||||
logger.debug("[Consulting Page] Returning with sessions attached...")
|
||||
|
||||
return consultation_data
|
||||
|
||||
|
||||
def initrec_delete_initial_recording(
|
||||
id_: InitRecId,
|
||||
) -> None:
|
||||
logger.debug("[Call backend] delete_initial_recording")
|
||||
stmt = db.grunderfassung.delete().where(db.grunderfassung.c.erfassung_id == id_)
|
||||
with db.ENGINE.begin() as conn:
|
||||
# // consulting page interaction
|
||||
def page_consulting_linking_companies(
|
||||
un_id: RecId | None,
|
||||
) -> list[LinkingConsultationsEntryCompany]:
|
||||
with db.ENGINE.connect() as conn:
|
||||
q_relevant_companies = sa.select(
|
||||
db.t_unternehmen.c.un_id,
|
||||
).where(
|
||||
db.t_unternehmen.c.geloescht.is_(None),
|
||||
)
|
||||
if un_id is not None:
|
||||
q_relevant_companies = q_relevant_companies.where(
|
||||
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.t_ext_crm_master_attach.c.ma_unternehmensname,
|
||||
).select_from(
|
||||
q_relevant_companies.join(
|
||||
db.t_ext_crm_master_attach,
|
||||
q_relevant_companies.c.un_id == db.t_ext_crm_master_attach.c.ma_id,
|
||||
)
|
||||
)
|
||||
|
||||
ret = conn.execute(stmt)
|
||||
company_names = ret.mappings().all()
|
||||
if not company_names:
|
||||
raise ValueError(f"No companies found for 'un_id': {un_id}")
|
||||
|
||||
logger.debug("[Page Consulting -- Linking] Retrieved company names:\n%s", company_names)
|
||||
|
||||
linking_entries: list[LinkingConsultationsEntryCompany] = []
|
||||
for entry in company_names:
|
||||
linking_entries.append(
|
||||
LinkingConsultationsEntryCompany(
|
||||
un_id=entry["un_id"],
|
||||
display_name=entry["ma_unternehmensname"],
|
||||
)
|
||||
)
|
||||
|
||||
return linking_entries
|
||||
|
||||
|
||||
def page_consulting_linking_persons() -> list[LinkingConsultationsEntryPerson]:
|
||||
with db.ENGINE.connect() as conn:
|
||||
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)
|
||||
)
|
||||
|
||||
ret = conn.execute(stmt)
|
||||
|
||||
if ret.rowcount == 0:
|
||||
raise KeyError(f"Database ID {id_} not found for deletion")
|
||||
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())
|
||||
|
||||
|
||||
@dc.dataclass(slots=True)
|
||||
class FrontpageCompany:
|
||||
erfassung_id: InitRecId
|
||||
# ma_id: int
|
||||
name: str
|
||||
Metadaten_aktualisierung: datetime.datetime
|
||||
is_company: bool
|
||||
# TODO wrap result
|
||||
def page_consulting_eastablish_link(
|
||||
vorgang_id: ConsId,
|
||||
un_id: RecId | None,
|
||||
pers_id: RecId | None,
|
||||
) -> None:
|
||||
stmt = (
|
||||
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)
|
||||
)
|
||||
|
||||
with db.ENGINE.begin() as conn:
|
||||
conn.execute(stmt)
|
||||
|
||||
|
||||
def front_get_company_list() -> list[FrontpageCompany]:
|
||||
logger.debug("[Call backend] get_company_list")
|
||||
stmt = sql.select(
|
||||
db.grunderfassung.c.erfassung_id,
|
||||
db.grunderfassung.c.Partnersuche__un_suche,
|
||||
db.grunderfassung.c.Metadaten_aktualisierung,
|
||||
).order_by(db.grunderfassung.c.Metadaten_aktualisierung.desc())
|
||||
# // company profile interaction
|
||||
def companyprofile_page_get_consultations(
|
||||
un_id: RecId,
|
||||
) -> CompanyProfileConsultations:
|
||||
logger.debug("[Call backend] _companyprofile_page_get_consultations")
|
||||
stmt = sa.select(
|
||||
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)
|
||||
|
||||
front_page_companies: list[FrontpageCompany] = []
|
||||
cons_entries_pauschal: list[CompanyProfileConsultationEntry] = []
|
||||
cons_entries_individual: list[CompanyProfileConsultationEntry] = []
|
||||
|
||||
for entry in res:
|
||||
erfassung_id = entry[0]
|
||||
ma_id = entry[1]
|
||||
datetime_akt = cast(datetime.datetime, entry[2])
|
||||
datetime_akt = datetime_akt.astimezone()
|
||||
for entry in res.mappings():
|
||||
cons_id = entry["vorgang_id"]
|
||||
assert cons_id, "no VorgangID defined"
|
||||
|
||||
name: str = "PLATZHALTER INDIVIDUAL"
|
||||
is_company: bool = False
|
||||
if ma_id is not None:
|
||||
comp_info = initrec_comp_search_get_info(ma_id)
|
||||
name = comp_info["ma_unternehmensname"]
|
||||
is_company = True
|
||||
datetime_updated = cast(datetime.datetime, entry["aktualisiert"])
|
||||
datetime_updated = datetime_updated.astimezone(TIMEZONE_CEST)
|
||||
base_title = entry["titel"]
|
||||
cons_type = ConsultingType(entry["beratungs_typ"])
|
||||
|
||||
# front_page_companies.append(FrontpageCompany(erfassung_id, ma_id, name, datetime_akt))
|
||||
front_page_companies.append(
|
||||
FrontpageCompany(
|
||||
erfassung_id=erfassung_id,
|
||||
name=name,
|
||||
Metadaten_aktualisierung=datetime_akt,
|
||||
is_company=is_company,
|
||||
con_entry = CompanyProfileConsultationEntry(
|
||||
cons_id=cons_id,
|
||||
title=base_title,
|
||||
date_updated=datetime_updated,
|
||||
cons_type=cons_type,
|
||||
)
|
||||
|
||||
if cons_type is ConsultingType.PAUSCHAL:
|
||||
cons_entries_pauschal.append(con_entry)
|
||||
elif cons_type is ConsultingType.INDIVIDUAL:
|
||||
cons_entries_individual.append(con_entry)
|
||||
else:
|
||||
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)
|
||||
|
||||
return CompanyProfileConsultations(
|
||||
pauschal=cons_entries_pauschal,
|
||||
individual=cons_entries_individual,
|
||||
)
|
||||
|
||||
|
||||
# // initial recording interaction
|
||||
# TODO wrap result, add to corresponding component
|
||||
def initrec_link_person_company(
|
||||
un_id: RecId,
|
||||
pers_id: RecId,
|
||||
unlink: bool,
|
||||
) -> None:
|
||||
if unlink:
|
||||
deleted_ts = datetime.datetime.now(tz=datetime.UTC)
|
||||
stmt = (
|
||||
sa.update(db.t_zuordnung_personen_unternehmen)
|
||||
.where(
|
||||
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.t_zuordnung_personen_unternehmen)
|
||||
.values(un_id=un_id, pers_id=pers_id)
|
||||
.on_conflict_do_nothing(
|
||||
index_elements=["un_id", "pers_id"],
|
||||
index_where=sa.text("gueltig_bis IS NULL"),
|
||||
)
|
||||
)
|
||||
|
||||
return front_page_companies
|
||||
with db.ENGINE.begin() as conn:
|
||||
conn.execute(stmt)
|
||||
|
||||
|
||||
# // main page interaction
|
||||
def main_page_get_entries() -> list[MainPageEntry]:
|
||||
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")
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import shutil
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from pprint import pformat
|
||||
from typing import TYPE_CHECKING, Any, Final, cast
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from wce_crm import db
|
||||
from wce_crm.constants import TIMEZONE_CEST
|
||||
from wce_crm.data_models import Grunderfassung_Dokument
|
||||
from wce_crm.logging import logger_initrec_docs as logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from wce_crm.types import RecId
|
||||
|
||||
|
||||
class FileStorageService:
|
||||
def __init__(
|
||||
self,
|
||||
storage_dir: Path,
|
||||
):
|
||||
self.storage_dir = storage_dir
|
||||
|
||||
def store_file(
|
||||
self,
|
||||
src_path: Path,
|
||||
) -> Path:
|
||||
"""Copies file to the application's file storage, assgn an UUID and gives back
|
||||
the absolute path."""
|
||||
src_path = src_path.resolve()
|
||||
assert src_path.is_file(), "provided path is not a file"
|
||||
|
||||
if not src_path.exists():
|
||||
raise FileNotFoundError(
|
||||
f"Die Datei {src_path.name} konnte nicht gefunden werden. "
|
||||
f"Existiert diese noch oder wurde sie gelöscht?"
|
||||
)
|
||||
|
||||
ts_now = datetime.datetime.now(tz=TIMEZONE_CEST)
|
||||
month = str(ts_now.month).zfill(2)
|
||||
year = str(ts_now.year)
|
||||
target_dir = self.storage_dir / year / month
|
||||
target_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
new_filename = f"{uuid.uuid4().hex}{src_path.suffix}"
|
||||
target_path = target_dir / new_filename
|
||||
|
||||
shutil.copy2(src_path, target_path)
|
||||
|
||||
return target_path
|
||||
|
||||
def delete_file(
|
||||
self,
|
||||
absolute_path: Path,
|
||||
) -> None:
|
||||
if absolute_path.is_relative_to(self.storage_dir) and absolute_path.exists():
|
||||
absolute_path.unlink()
|
||||
|
||||
|
||||
class DocumentRepository:
|
||||
def __init__(
|
||||
self,
|
||||
file_storage_dir: Path,
|
||||
):
|
||||
self.file_storage_dir = file_storage_dir
|
||||
|
||||
def from_db(
|
||||
self,
|
||||
conn: sa.Connection,
|
||||
pers_id: RecId,
|
||||
) -> list[Grunderfassung_Dokument]:
|
||||
stmt = sa.select(db.t_grunderfassung_dokumente).where(
|
||||
db.t_grunderfassung_dokumente.c.pers_id == pers_id,
|
||||
db.t_grunderfassung_dokumente.c.geloescht.is_(None),
|
||||
)
|
||||
|
||||
ret = conn.execute(stmt)
|
||||
|
||||
docs: list[Grunderfassung_Dokument] = []
|
||||
for row in ret.mappings():
|
||||
row = dict(row)
|
||||
row["speicher_pfad"] = self.file_storage_dir / row["speicher_pfad"]
|
||||
docs.append(Grunderfassung_Dokument(**row))
|
||||
|
||||
return docs
|
||||
|
||||
def to_db(
|
||||
self,
|
||||
conn: sa.Connection,
|
||||
pers_id: RecId,
|
||||
docs: list[Grunderfassung_Dokument],
|
||||
) -> list[Grunderfassung_Dokument]:
|
||||
rows_for_db_insert: list[dict[str, Any]] = []
|
||||
rows_for_db_update: list[dict[str, Any]] = []
|
||||
docs_inserted: list[Grunderfassung_Dokument] = []
|
||||
|
||||
for doc in docs:
|
||||
doc.pers_id = pers_id
|
||||
if doc.id is None:
|
||||
row_data = doc.model_dump(exclude={"id"})
|
||||
rows_for_db_insert.append(row_data)
|
||||
docs_inserted.append(doc)
|
||||
else:
|
||||
row_data = doc.model_dump()
|
||||
# new bind param to avoid name clashes
|
||||
row_data["b_id"] = row_data["id"]
|
||||
del row_data["id"]
|
||||
rows_for_db_update.append(row_data)
|
||||
|
||||
# prepare path as relative
|
||||
file_path = row_data.get("speicher_pfad")
|
||||
if isinstance(file_path, Path):
|
||||
if file_path.is_absolute():
|
||||
file_path = file_path.relative_to(self.file_storage_dir)
|
||||
row_data["speicher_pfad"] = str(file_path)
|
||||
|
||||
if rows_for_db_update:
|
||||
# ... update
|
||||
logger.debug(
|
||||
"[InitRec -- Docs] Call update for docs:\n%s", pformat(rows_for_db_update)
|
||||
)
|
||||
stmt = db.t_grunderfassung_dokumente.update().where(
|
||||
db.t_grunderfassung_dokumente.c.id == sa.bindparam("b_id")
|
||||
)
|
||||
conn.execute(stmt, rows_for_db_update)
|
||||
|
||||
if rows_for_db_insert:
|
||||
# ... insert
|
||||
logger.debug(
|
||||
"[InitRec -- Docs] Call insert for docs:\n%s", pformat(rows_for_db_insert)
|
||||
)
|
||||
stmt = db.t_grunderfassung_dokumente.insert().returning(
|
||||
db.t_grunderfassung_dokumente.c.id
|
||||
)
|
||||
res = conn.execute(stmt, rows_for_db_insert)
|
||||
new_doc_ids = cast(list[int], [row[0] for row in res.fetchall()])
|
||||
|
||||
assert len(docs_inserted) == len(new_doc_ids)
|
||||
for doc, new_id in zip(docs_inserted, new_doc_ids):
|
||||
doc.id = new_id
|
||||
|
||||
return docs
|
||||
|
||||
|
||||
class DocumentService:
|
||||
def __init__(
|
||||
self,
|
||||
repo: DocumentRepository,
|
||||
storage: FileStorageService,
|
||||
):
|
||||
self.repo = repo
|
||||
self.storage = storage
|
||||
|
||||
def to_db(
|
||||
self,
|
||||
conn: sa.Connection,
|
||||
pers_id: RecId,
|
||||
docs: list[Grunderfassung_Dokument],
|
||||
) -> list[Grunderfassung_Dokument]:
|
||||
try:
|
||||
for doc in docs:
|
||||
if doc.id is None:
|
||||
doc.speicher_pfad = self.storage.store_file(doc.speicher_pfad)
|
||||
except Exception:
|
||||
logger.error(
|
||||
"[InitRec -- Docs] Error during file saving procedure",
|
||||
exc_info=True,
|
||||
)
|
||||
raise
|
||||
|
||||
try:
|
||||
docs = self.repo.to_db(conn, pers_id=pers_id, docs=docs)
|
||||
return docs
|
||||
|
||||
except Exception:
|
||||
logger.error(
|
||||
"[InitRec -- Docs] Error during DB saving procedure",
|
||||
exc_info=True,
|
||||
)
|
||||
for doc in docs:
|
||||
# The delete method checks if the provided saving path is really
|
||||
# relative to the storage path. Otherwise, no delete is performed to
|
||||
# ensure that original files are never deleted
|
||||
self.storage.delete_file(doc.speicher_pfad)
|
||||
raise
|
||||
|
||||
def from_db(
|
||||
self,
|
||||
conn: sa.Connection,
|
||||
pers_id: RecId,
|
||||
) -> list[Grunderfassung_Dokument]:
|
||||
return self.repo.from_db(conn, pers_id=pers_id)
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import zoneinfo
|
||||
from pathlib import Path
|
||||
from typing import Final
|
||||
|
||||
@@ -18,10 +19,21 @@ assert BASE_PATH
|
||||
|
||||
class Config:
|
||||
DEVELOPMENT_STATE: bool = bool(os.getenv("DOPT_DEVELOPMENT", None))
|
||||
DB_PATH_CRM: Path = BASE_PATH / os.getenv("DOPT_DB_CRM", "data/db/wce_crm.db")
|
||||
DB_PATH_MAIN: Path = BASE_PATH / os.getenv(
|
||||
"DOPT_DB_MAIN", "data/db/wce_grunderfassung.db"
|
||||
)
|
||||
PATH_LOGGING: Path = BASE_PATH / os.getenv("DOPT_PATH_LOGGING", "data/d-opt.log")
|
||||
DB_PATH_KONTAKTLISTE: Path = (
|
||||
BASE_PATH / os.getenv("DOPT_DB_CRM", "data/db/wce_crm.db")
|
||||
).resolve()
|
||||
DB_PATH_MAIN: Path = (
|
||||
BASE_PATH / os.getenv("DOPT_DB_MAIN", "data/db/wce_grunderfassung.db")
|
||||
).resolve()
|
||||
PATH_LOGGING: Path = (BASE_PATH / os.getenv("DOPT_PATH_LOGGING", "data/logs")).resolve()
|
||||
PATH_FILE_STORAGE: Path = (
|
||||
BASE_PATH / os.getenv("DOPT_PATH_FILE_STORAGE", "data/files")
|
||||
).resolve()
|
||||
LOG_FILENAME: str = "dopt.log"
|
||||
ALEMBIC_PATH: Path = BASE_PATH / os.getenv("DOPT_ALEMBIC_BASE", "python/alembic")
|
||||
ALEMBIC_PATH: Path = (
|
||||
BASE_PATH / os.getenv("DOPT_ALEMBIC_BASE", "python/alembic")
|
||||
).resolve()
|
||||
|
||||
|
||||
TIMEZONE_CEST: Final[zoneinfo.ZoneInfo] = zoneinfo.ZoneInfo("Europe/Berlin")
|
||||
COLUMN_SEP: Final[str] = "__"
|
||||
|
||||
@@ -4,6 +4,6 @@ from typing import Final
|
||||
|
||||
CUSTOM_WIDGET_NAMES: Final[frozenset] = frozenset(
|
||||
[
|
||||
"grunderfassung_suche",
|
||||
"grunderfassung_unternehmen_suche",
|
||||
]
|
||||
)
|
||||
|
||||
+471
-6
@@ -1,9 +1,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import dataclasses as dc
|
||||
import datetime
|
||||
import json
|
||||
from typing import Annotated, Any, Final
|
||||
from collections.abc import Iterable, Sequence
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Annotated, Any, Final, Generic, Protocol, TypeVar
|
||||
|
||||
from dopt_basics.datastructures import DualDict
|
||||
from pydantic import (
|
||||
AwareDatetime,
|
||||
BaseModel,
|
||||
@@ -13,8 +18,55 @@ from pydantic import (
|
||||
field_validator,
|
||||
model_validator,
|
||||
)
|
||||
from pydantic_core import ErrorDetails
|
||||
|
||||
from wce_crm.constants import COLUMN_SEP
|
||||
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 InitRecForm, Page_Consulting_ConsultingSession # noqa: F401
|
||||
from wce_crm.gui_components.initrec_file_selection import FileItemWidget
|
||||
from wce_crm.types import ConsId, EntityType, RecId, UserId
|
||||
|
||||
ValidAge = Annotated[int, Field(ge=0, le=99)]
|
||||
M = TypeVar("M", bound="Module")
|
||||
S = TypeVar("S", bound="PageState")
|
||||
|
||||
# // validation translation
|
||||
GERMAN_ERROR_MESSAGES: Final[dict[str, str]] = {
|
||||
"missing": "Dieses Feld ist ein Pflichtfeld.",
|
||||
"int_parsing": "Bitte eine gültige Ganzzahl eingeben.",
|
||||
"float_parsing": "Bitte eine gültige Dezimalzahl eingeben.",
|
||||
"string_too_short": "Der Text ist zu kurz. Er muss mindestens {min_length} Zeichen lang sein.",
|
||||
"string_too_long": "Der Text ist zu lang. Er darf maximal {max_length} Zeichen lang sein.",
|
||||
"greater_than": "Der Wert muss größer als {gt} sein.",
|
||||
}
|
||||
|
||||
|
||||
def translate_pydantic_errors(
|
||||
errors: list[ErrorDetails],
|
||||
) -> list[ErrorDetails]:
|
||||
translated_errors: list[ErrorDetails] = []
|
||||
|
||||
for error in errors:
|
||||
err_type = error.get("type", None)
|
||||
|
||||
if err_type is None or err_type not in GERMAN_ERROR_MESSAGES:
|
||||
error["msg"] = "[fehlende Übersetzung, nur Englisch]: " + error["msg"]
|
||||
translated_errors.append(error)
|
||||
continue
|
||||
|
||||
msg_template = GERMAN_ERROR_MESSAGES[err_type]
|
||||
ctx = error.get("ctx", {})
|
||||
try:
|
||||
error["msg"] = msg_template.format(**ctx)
|
||||
except KeyError:
|
||||
error["msg"] = msg_template
|
||||
|
||||
translated_errors.append(error)
|
||||
|
||||
return translated_errors
|
||||
|
||||
|
||||
def _parse_json(value: Any) -> str:
|
||||
@@ -26,9 +78,278 @@ def _parse_json(value: Any) -> str:
|
||||
raise TypeError
|
||||
|
||||
|
||||
COLUMN_SEP: Final[str] = "__"
|
||||
# // interfaces
|
||||
class WrapperModule(Protocol):
|
||||
def _sync_state_to_GUI(self, *args, **kwargs) -> None:
|
||||
"""build states for all child modules and use their `load_state` method
|
||||
to initialise them with the new data
|
||||
"""
|
||||
...
|
||||
|
||||
def _sync_GUI_to_state(self, *args, **kwargs) -> None:
|
||||
"""call `get_state` method on all child modules and assign respective
|
||||
properties to state of parent module
|
||||
"""
|
||||
...
|
||||
|
||||
def load_state(self, new_state: Any) -> None: ...
|
||||
|
||||
def get_state(self) -> Any:
|
||||
"""should always be a simple call to `_sync_GUI_to_state` and then returning
|
||||
the module's state
|
||||
|
||||
Returns
|
||||
-------
|
||||
Any
|
||||
state of the respective module
|
||||
"""
|
||||
...
|
||||
|
||||
def lock(self) -> None: ...
|
||||
|
||||
def unlock(self) -> None: ...
|
||||
|
||||
|
||||
class Module(WrapperModule, Protocol):
|
||||
def validate(self) -> list[str]:
|
||||
"""should be a list of field names which could not be validated successfully,
|
||||
propagates through: parent components must call this on all child modules and extend
|
||||
their own error list with the return value
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[str]
|
||||
list of error fields (form field label)
|
||||
"""
|
||||
...
|
||||
|
||||
def save_data(self) -> None: ...
|
||||
|
||||
|
||||
class InitRecFormToDb(Protocol):
|
||||
def __call__(
|
||||
self,
|
||||
auto_form_data: InitRec,
|
||||
) -> InitRec: ...
|
||||
|
||||
|
||||
class InitRecFormFromDb(Protocol):
|
||||
def __call__(
|
||||
self,
|
||||
ids: EntityIds,
|
||||
) -> InitRec: ...
|
||||
|
||||
|
||||
# // GUI states and types
|
||||
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 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 in ignore_attr:
|
||||
continue
|
||||
|
||||
new_value = getattr(source, field.name)
|
||||
|
||||
if deep:
|
||||
new_value = copy.deepcopy(new_value)
|
||||
|
||||
setattr(target, field.name, new_value)
|
||||
|
||||
|
||||
@dc.dataclass(slots=True)
|
||||
class Session:
|
||||
user_id: UserId
|
||||
user_name: str
|
||||
|
||||
|
||||
@dc.dataclass(slots=True)
|
||||
class PageState(Generic[M]):
|
||||
child_modules: list[M] = dc.field(default_factory=list)
|
||||
|
||||
|
||||
@dc.dataclass(slots=True, kw_only=True)
|
||||
class Page_MainPage_State(PageState[Module]):
|
||||
session: Session
|
||||
row_count: int = 0
|
||||
|
||||
|
||||
@dc.dataclass(slots=True, kw_only=True)
|
||||
class Page_NewEntry_State(PageState[Module]):
|
||||
session: Session
|
||||
|
||||
|
||||
@dc.dataclass(slots=True, kw_only=True)
|
||||
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
|
||||
)
|
||||
docs: list[Page_InitRec_DocumentSection_Doc_State] = dc.field(default_factory=list)
|
||||
|
||||
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 = False
|
||||
id_mapping: dict[str, str]
|
||||
|
||||
|
||||
@dc.dataclass(slots=True, kw_only=True)
|
||||
class Page_InitRec_Form_State(PageState[Module]):
|
||||
session: Session
|
||||
cfg: Page_InitRec_Form_Config
|
||||
ids: EntityIds
|
||||
ent_type: EntityType
|
||||
# rec_id: RecId | 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_InitRec_DocumentSection_State(PageState["FileItemWidget"]):
|
||||
session: Session
|
||||
doc_count: int = 0
|
||||
docs: list[Page_InitRec_DocumentSection_Doc_State] = dc.field(default_factory=list)
|
||||
locked: bool
|
||||
geloescht: bool = False
|
||||
|
||||
|
||||
@dc.dataclass(slots=True, kw_only=True)
|
||||
class Page_InitRec_DocumentSection_Doc_State(PageState[Module]):
|
||||
session: Session
|
||||
id: int | None
|
||||
# pers_id: int | None
|
||||
datum_hinzugefuegt: datetime.datetime = dc.field(
|
||||
default_factory=lambda: datetime.datetime.now(datetime.UTC)
|
||||
)
|
||||
speicher_pfad: Path
|
||||
dateiname_original: str = ""
|
||||
dateigroesse_bytes: int | None = None
|
||||
mime_type: str = ""
|
||||
anmerkung: str = ""
|
||||
locked: bool
|
||||
geloescht: bool = False
|
||||
|
||||
|
||||
@dc.dataclass(slots=True, kw_only=True)
|
||||
class Page_Consulting_State(PageState[Module]):
|
||||
session: Session
|
||||
vorgang_id: ConsId | None
|
||||
un_id: RecId | None = None
|
||||
pers_id: RecId | None = None
|
||||
titel: str = "TEST-TITEL" # add title later to GUI or build it in the background
|
||||
beratungs_typ: ConsultingType
|
||||
cons_sessions: list[Page_Consulting_ConsultingSession_State] = dc.field(
|
||||
default_factory=list
|
||||
)
|
||||
geloescht: bool = False
|
||||
locked: bool
|
||||
|
||||
|
||||
@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)
|
||||
class Page_Consulting_ConsultingSession_State(PageState[Module]):
|
||||
session: Session
|
||||
nutzer_id: UserId # must be set with the session (always known)
|
||||
nutzer_name: str # must be set with the session (always known)
|
||||
beratung_id: int | None # known and not known entries
|
||||
zeitstempel: datetime.datetime | None = None
|
||||
ansprechpartner: str | None = None
|
||||
kommunikationsweg: str | None = None
|
||||
thema_crm_matrix: str | None = (
|
||||
None # TODO should be linked to something, target unclear (need specification by customer)
|
||||
)
|
||||
anmerkungen: str | None = None
|
||||
rueckmeldung: str | None = None
|
||||
geloescht: bool = False
|
||||
locked: bool
|
||||
|
||||
|
||||
@dc.dataclass(slots=True, kw_only=True)
|
||||
class Page_Consulting_Table_State(PageState["Page_Consulting_ConsultingSession"]):
|
||||
session: Session
|
||||
row_states: list[Page_Consulting_ConsultingSession_State] = dc.field(default_factory=list)
|
||||
locked: bool
|
||||
|
||||
|
||||
@dc.dataclass(slots=True, kw_only=True)
|
||||
class Page_CompanyProfile_State(PageState[Module]):
|
||||
session: Session
|
||||
un_id: RecId | None = None
|
||||
|
||||
|
||||
# // Pydantic models
|
||||
class FlatBaseModel(BaseModel):
|
||||
"""
|
||||
Optimised Pydantic base class, which parses JSON strings and column
|
||||
@@ -83,7 +404,7 @@ class FlatBaseModel(BaseModel):
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def __unflatten_input(
|
||||
def _unflatten_input(
|
||||
cls,
|
||||
data: Any,
|
||||
) -> Any: # type: ignore
|
||||
@@ -141,8 +462,58 @@ class FlatBaseModel(BaseModel):
|
||||
return dict(items)
|
||||
|
||||
|
||||
class Grunderfassung(FlatBaseModel):
|
||||
# ** InitRec
|
||||
class Grunderfassung_Dokument(BaseModel):
|
||||
model_config = ConfigDict(str_strip_whitespace=True)
|
||||
|
||||
id: int | None
|
||||
pers_id: int | None
|
||||
speicher_pfad: Path
|
||||
dateiname_original: str
|
||||
datum_hinzugefuegt: AwareDatetime
|
||||
dateigroesse_bytes: int | None = None
|
||||
mime_type: str
|
||||
anmerkung: str
|
||||
|
||||
# default in SQLAlchemy with lambda and timezone-aware datetime
|
||||
erstellt: AwareDatetime | None = Field(default=None, exclude=True)
|
||||
aktualisiert: AwareDatetime | None = Field(default=None, exclude=True) # see above
|
||||
geloescht: AwareDatetime | None = None
|
||||
|
||||
|
||||
class InitRec(BaseModel):
|
||||
ids: EntityIds
|
||||
recording_type: RecordingType
|
||||
geloescht: AwareDatetime | None = None
|
||||
form_data: Page_InitRec_Form_Data
|
||||
docs: list[Grunderfassung_Dokument]
|
||||
|
||||
|
||||
# mapping between initrec form field keys and database properties
|
||||
FIELD_DB_MAPPING_GRUNDERERFASSUNG_PERSONEN: Final[DualDict[str, str]] = DualDict(
|
||||
Metadaten_erstellung="erstellt",
|
||||
Metadaten_aktualisierung="aktualisiert",
|
||||
)
|
||||
|
||||
FIELD_DB_MAPPING_GRUNDERERFASSUNG_UNTERNEHMEN: Final[DualDict[str, str]] = DualDict(
|
||||
Partnersuche__un_suche="un_id",
|
||||
Partnersuche__person_suche="an_id",
|
||||
)
|
||||
|
||||
FIELD_DB_SPLIT_METADATEN: Final[frozenset[str]] = frozenset(
|
||||
(
|
||||
"Metadaten_erstellung",
|
||||
"Metadaten_aktualisierung",
|
||||
"Metadaten_nutzer",
|
||||
"Metadaten_wiedereintrittsdatum",
|
||||
"Grunderfassung_fallnummer",
|
||||
"Grunderfassung_notiz",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class Grunderfassung_Metadaten(FlatBaseModel):
|
||||
# displayed and handled separately, on database level included in 'Person'
|
||||
Metadaten_erstellung: AwareDatetime | None = None
|
||||
Metadaten_aktualisierung: AwareDatetime | None = None # see above
|
||||
Metadaten_nutzer: str | None
|
||||
@@ -150,7 +521,24 @@ class Grunderfassung(FlatBaseModel):
|
||||
Grunderfassung_fallnummer: str
|
||||
Grunderfassung_notiz: str | None
|
||||
|
||||
Partnersuche: Grunderfassung_PartnerSuche | None = 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
|
||||
@@ -166,7 +554,7 @@ class Grunderfassung_PartnerSuche(BaseModel):
|
||||
|
||||
un_suche: int | None
|
||||
person_suche: int | None
|
||||
kanal_aufmerksamkeit: str | None
|
||||
kontaktweg: str | None
|
||||
|
||||
|
||||
class Grunderfassung_Projektrelevanz(BaseModel):
|
||||
@@ -194,6 +582,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
|
||||
@@ -295,3 +684,79 @@ class Grunderfassung_Sprachen(BaseModel):
|
||||
SP_nachweis: str | None
|
||||
SP_art_nachweis: str | None = None
|
||||
SP_datum_nachweis: datetime.date | None = None
|
||||
|
||||
|
||||
# TODO placeholer docuemnt
|
||||
|
||||
|
||||
# ** Consulting
|
||||
class Beratungsgespraech_Vorgang(BaseModel):
|
||||
model_config = ConfigDict(str_strip_whitespace=True)
|
||||
|
||||
vorgang_id: int | None
|
||||
un_id: int | None
|
||||
pers_id: int | None
|
||||
titel: str
|
||||
beratungs_typ: ConsultingType
|
||||
|
||||
# default in SQLAlchemy with lambda and timezone-aware datetime
|
||||
erstellt: AwareDatetime | None = Field(default=None, exclude=True)
|
||||
aktualisiert: AwareDatetime | None = Field(default=None, exclude=True) # see above
|
||||
geloescht: AwareDatetime | None = None
|
||||
|
||||
beratungen: list[Beratungsgespraech_Einzelgespraech]
|
||||
|
||||
|
||||
class Beratungsgespraech_Einzelgespraech(BaseModel):
|
||||
model_config = ConfigDict(str_strip_whitespace=True)
|
||||
|
||||
vorgang_id: int | None
|
||||
beratung_id: int | None
|
||||
nutzer_id: int
|
||||
nutzer_name: str
|
||||
zeitstempel: AwareDatetime
|
||||
ansprechpartner: str
|
||||
kommunikationsweg: str
|
||||
thema_crm_matrix: str | None
|
||||
anmerkungen: str | None
|
||||
rueckmeldung: str | None
|
||||
|
||||
# default in SQLAlchemy with lambda and timezone-aware datetime
|
||||
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={},
|
||||
),
|
||||
}
|
||||
|
||||
+410
-201
@@ -6,8 +6,8 @@ import re
|
||||
from pathlib import Path
|
||||
|
||||
import polars as pl
|
||||
import sqlalchemy as sql
|
||||
from sqlalchemy import Column, String, Table, TypeDecorator
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import Column, Index, String, Table, TypeDecorator, event, sql
|
||||
|
||||
from wce_crm import constants
|
||||
from wce_crm import types as t
|
||||
@@ -37,7 +37,7 @@ class SafeDateTime(TypeDecorator):
|
||||
class UTCDateTime(TypeDecorator):
|
||||
"""Safely coerces naive datetimes from SQLite into timezone-aware UTC."""
|
||||
|
||||
impl = sql.DateTime
|
||||
impl = sa.DateTime
|
||||
cache_ok = True
|
||||
|
||||
def process_bind_param(self, value, dialect):
|
||||
@@ -45,128 +45,70 @@ class UTCDateTime(TypeDecorator):
|
||||
if value is not None:
|
||||
# Ensure it's converted to UTC before saving
|
||||
if value.tzinfo is None:
|
||||
value = value.replace(tzinfo=datetime.timezone.utc)
|
||||
value = value.replace(tzinfo=datetime.UTC)
|
||||
else:
|
||||
value = value.astimezone(datetime.timezone.utc)
|
||||
value = value.astimezone(datetime.UTC)
|
||||
return value
|
||||
|
||||
def process_result_value(self, value, dialect):
|
||||
"""Runs when fetching from the database."""
|
||||
if value is not None and value.tzinfo is None:
|
||||
# Explicitly tell Python this data *is* UTC
|
||||
value = value.replace(tzinfo=datetime.timezone.utc)
|
||||
value = value.replace(tzinfo=datetime.UTC)
|
||||
return value
|
||||
|
||||
|
||||
MD_CRM = sql.MetaData()
|
||||
MD_MAIN = sql.MetaData()
|
||||
ENGINE = sql.create_engine(f"sqlite:///{constants.Config.DB_PATH_MAIN}")
|
||||
MD_CRM = sa.MetaData()
|
||||
MD_MAIN = sa.MetaData()
|
||||
URL_MAIN = sa.engine.URL.create(
|
||||
"sqlite+pysqlite",
|
||||
database=str(constants.Config.DB_PATH_MAIN),
|
||||
)
|
||||
ENGINE = sa.create_engine(
|
||||
URL_MAIN,
|
||||
connect_args={"uri": True, "timeout": 0.5},
|
||||
)
|
||||
|
||||
# ---------- OLD "Kontaktliste" ----------
|
||||
|
||||
# md_kontaktliste = sql.MetaData()
|
||||
|
||||
# ext_kl_unternehmen: sql.Table = Table(
|
||||
# "Unternehmen",
|
||||
# md_kontaktliste,
|
||||
# Column("u_id", sql.Integer, nullable=False, unique=True),
|
||||
# Column("u_zeitstempel_eintrag", sql.DateTime, nullable=False),
|
||||
# Column("u_rechtsform", sql.Text, nullable=False),
|
||||
# Column("u_firmenname", sql.Text, nullable=False),
|
||||
# Column("u_strasse", sql.Text, nullable=False),
|
||||
# Column("u_hausnummer", sql.Text, nullable=False),
|
||||
# Column("u_adresszusatz", sql.Text, nullable=True),
|
||||
# Column("u_plz", sql.Text, nullable=False),
|
||||
# Column("u_ort", sql.Text, nullable=False),
|
||||
# Column("u_postfach", sql.Text, nullable=True),
|
||||
# Column("u_website", sql.Text, nullable=True),
|
||||
# Column("u_anrede", sql.Text, nullable=False),
|
||||
# Column("u_titel", sql.Text, nullable=True),
|
||||
# Column("u_vorname", sql.Text, nullable=False),
|
||||
# Column("u_nachname", sql.Text, nullable=False),
|
||||
# Column("u_funktion", sql.Text, nullable=False),
|
||||
# Column("u_mail", sql.Text, nullable=False),
|
||||
# Column("u_telefon", sql.Text, nullable=False),
|
||||
# Column("u_plz_postfach", sql.Text, nullable=True),
|
||||
# Column("u_einwilligung_inhaber", sql.Boolean, nullable=True),
|
||||
# Column("u_einwilligung_ansprechpartner", sql.Boolean, nullable=True),
|
||||
# Column("u_aktiv", sql.Boolean, nullable=False, default=1),
|
||||
# )
|
||||
|
||||
# ext_kl_unternehmen_schema: t.PolarsSchema = {
|
||||
# "u_id": pl.UInt64,
|
||||
# "u_zeitstempel_eintrag": pl.Datetime,
|
||||
# "u_rechtsform": pl.String,
|
||||
# "u_firmenname": pl.String,
|
||||
# "u_strasse": pl.String,
|
||||
# "u_hausnummer": pl.String,
|
||||
# "u_adresszusatz": pl.String,
|
||||
# "u_plz": pl.String,
|
||||
# "u_ort": pl.String,
|
||||
# "u_postfach": pl.String,
|
||||
# "u_website": pl.String,
|
||||
# "u_anrede": pl.String,
|
||||
# "u_titel": pl.String,
|
||||
# "u_vorname": pl.String,
|
||||
# "u_nachname": pl.String,
|
||||
# "u_funktion": pl.String,
|
||||
# "u_mail": pl.String,
|
||||
# "u_telefon": pl.String,
|
||||
# "u_plz_postfach": pl.String,
|
||||
# "u_einwilligung_inhaber": pl.Boolean,
|
||||
# "u_einwilligung_ansprechpartner": pl.Boolean,
|
||||
# "u_aktiv": pl.Boolean,
|
||||
# }
|
||||
KONTAKTLISTE_URI = constants.Config.DB_PATH_KONTAKTLISTE.as_uri() + "?mode=ro"
|
||||
|
||||
|
||||
# def get_ext_kontaktliste(
|
||||
# db_path: Path | None,
|
||||
# ) -> pl.DataFrame:
|
||||
# if db_path is None:
|
||||
# ENV_PTH = os.environ.get("DOPT_DB_KONTAKTLISTE", None)
|
||||
# if ENV_PTH is None:
|
||||
# raise ValueError("No database path provided or found as ENV var.")
|
||||
# db_path = Path(ENV_PTH)
|
||||
|
||||
# if not db_path.exists():
|
||||
# raise FileNotFoundError(f"Database not found under >{db_path}<")
|
||||
|
||||
# engine = sql.create_engine(f"sqlite:///{db_path}")
|
||||
# stmt = sql.select(ext_kl_unternehmen)
|
||||
# return pl.read_database(stmt, engine, schema_overrides=ext_kl_unternehmen_schema)
|
||||
@event.listens_for(ENGINE, "connect")
|
||||
def _db_conn_event(dbapi_connection, connection_record):
|
||||
cursor = dbapi_connection.cursor()
|
||||
# 'kontaktliste' is the alias for the second DB
|
||||
cursor.execute("ATTACH DATABASE ? AS kontaktliste", (KONTAKTLISTE_URI,))
|
||||
# enable foreign keys
|
||||
cursor.execute("PRAGMA foreign_keys=ON")
|
||||
cursor.close()
|
||||
|
||||
|
||||
# df_kontaktliste = get_ext_kontaktliste(None)
|
||||
|
||||
# ----------------------------------------------------
|
||||
|
||||
ext_crm_master: sql.Table = Table(
|
||||
ext_crm_master: Table = Table(
|
||||
"Master",
|
||||
MD_CRM,
|
||||
Column("ma_id", sql.Integer, nullable=False, unique=True),
|
||||
Column("wce_id", sql.ForeignKey("Nutzer.wce_id")),
|
||||
Column("ma_unternehmensname", sql.Text, nullable=True),
|
||||
Column("ma_branche", sql.Text, nullable=True),
|
||||
Column("ma_strasse", sql.Text, nullable=True),
|
||||
Column("ma_hausnummer", sql.Text, nullable=True),
|
||||
Column("ma_plz", sql.Text, nullable=True),
|
||||
Column("ma_ort", sql.Text, nullable=True),
|
||||
Column("ma_plz_postfach", sql.Text, nullable=True),
|
||||
Column("ma_postfach", sql.Text, nullable=True),
|
||||
Column("ma_website", sql.Text, nullable=True),
|
||||
Column("ma_mail", sql.Text, nullable=True),
|
||||
Column("ma_telefonnummer", sql.Text, nullable=True),
|
||||
Column("ma_faxnummer", sql.Text, nullable=True),
|
||||
Column("ma_id", sa.Integer, nullable=False, unique=True),
|
||||
Column("wce_id", sa.ForeignKey("Nutzer.wce_id")),
|
||||
Column("ma_unternehmensname", sa.Text, nullable=True),
|
||||
Column("ma_branche", sa.Text, nullable=True),
|
||||
Column("ma_strasse", sa.Text, nullable=True),
|
||||
Column("ma_hausnummer", sa.Text, nullable=True),
|
||||
Column("ma_plz", sa.Text, nullable=True),
|
||||
Column("ma_ort", sa.Text, nullable=True),
|
||||
Column("ma_plz_postfach", sa.Text, nullable=True),
|
||||
Column("ma_postfach", sa.Text, nullable=True),
|
||||
Column("ma_website", sa.Text, nullable=True),
|
||||
Column("ma_mail", sa.Text, nullable=True),
|
||||
Column("ma_telefonnummer", sa.Text, nullable=True),
|
||||
Column("ma_faxnummer", sa.Text, nullable=True),
|
||||
Column("ma_ersteintrag_datum", SafeDateTime, nullable=True),
|
||||
Column("ma_aktualisierung_datum", SafeDateTime, nullable=True),
|
||||
Column("ma_aktualisierung_nutzer", sql.Text, nullable=True),
|
||||
Column("ma_sollprozess", sql.Text, nullable=True),
|
||||
Column("ma_auslaendische_mitarbeiter", sql.Text, nullable=True),
|
||||
Column("ma_quelle_information", sql.Text, nullable=True),
|
||||
Column("ma_bemerkung", sql.Text, nullable=True),
|
||||
Column("ma_kontakt", sql.Boolean, nullable=True),
|
||||
Column("ma_schlagworte", sql.Text, nullable=True),
|
||||
Column("ma_archiviert", sql.Boolean, nullable=True, default=False),
|
||||
Column("ma_aktualisierung_nutzer", sa.Text, nullable=True),
|
||||
Column("ma_sollprozess", sa.Text, nullable=True),
|
||||
Column("ma_auslaendische_mitarbeiter", sa.Text, nullable=True),
|
||||
Column("ma_quelle_information", sa.Text, nullable=True),
|
||||
Column("ma_bemerkung", sa.Text, nullable=True),
|
||||
Column("ma_kontakt", sa.Boolean, nullable=True),
|
||||
Column("ma_schlagworte", sa.Text, nullable=True),
|
||||
Column("ma_archiviert", sa.Boolean, nullable=True, default=False),
|
||||
)
|
||||
|
||||
|
||||
@@ -210,26 +152,26 @@ def get_ext_crm_master(
|
||||
if not db_path.exists():
|
||||
raise FileNotFoundError(f"Database not found under >{db_path}<")
|
||||
|
||||
engine = sql.create_engine(f"sqlite:///{db_path}")
|
||||
stmt = sql.select(ext_crm_master)
|
||||
engine = sa.create_engine(f"sqlite:///{db_path}")
|
||||
stmt = sa.select(ext_crm_master)
|
||||
return pl.read_database(stmt, engine, schema_overrides=ext_crm_master_schema)
|
||||
|
||||
|
||||
DF_CRM_MASTER = get_ext_crm_master(constants.Config.DB_PATH_CRM)
|
||||
DF_CRM_MASTER = get_ext_crm_master(constants.Config.DB_PATH_KONTAKTLISTE)
|
||||
|
||||
ext_crm_nutzer: sql.Table = Table(
|
||||
ext_crm_nutzer: Table = Table(
|
||||
"Nutzer",
|
||||
MD_CRM,
|
||||
Column("wce_id", sql.Integer, nullable=False, unique=True),
|
||||
Column("wce_name", sql.Text, nullable=True),
|
||||
Column("wce_vorname", sql.Text, nullable=True),
|
||||
Column("wce_kuerzel", sql.Text, nullable=True),
|
||||
Column("wce_passwort", sql.Text, nullable=True),
|
||||
Column("wce_angelegt_am", sql.DateTime, nullable=True),
|
||||
Column("wce_rolle", sql.Text, nullable=True),
|
||||
Column("wce_angelegt_von", sql.Text, nullable=True),
|
||||
Column("wce_aktiv", sql.Boolean, nullable=True),
|
||||
Column("wce_letzter_login", sql.DateTime, nullable=True),
|
||||
Column("wce_id", sa.Integer, nullable=False, unique=True),
|
||||
Column("wce_name", sa.Text, nullable=True),
|
||||
Column("wce_vorname", sa.Text, nullable=True),
|
||||
Column("wce_kuerzel", sa.Text, nullable=True),
|
||||
Column("wce_passwort", sa.Text, nullable=True),
|
||||
Column("wce_angelegt_am", sa.DateTime, nullable=True),
|
||||
Column("wce_rolle", sa.Text, nullable=True),
|
||||
Column("wce_angelegt_von", sa.Text, nullable=True),
|
||||
Column("wce_aktiv", sa.Boolean, nullable=True),
|
||||
Column("wce_letzter_login", sa.DateTime, nullable=True),
|
||||
)
|
||||
|
||||
ext_crm_nutzer_schema: t.PolarsSchema = {
|
||||
@@ -245,31 +187,31 @@ ext_crm_nutzer_schema: t.PolarsSchema = {
|
||||
"wce_letzter_login": pl.Datetime,
|
||||
}
|
||||
|
||||
ext_crm_contact_person: sql.Table = Table(
|
||||
ext_crm_contact_person: Table = Table(
|
||||
"Ansprechpartner",
|
||||
MD_CRM,
|
||||
Column("an_id", sql.Integer, nullable=False, unique=True),
|
||||
Column("ma_id", sql.ForeignKey("Master.ma_id")),
|
||||
Column("wce_id", sql.ForeignKey("Nutzer.wce_id")),
|
||||
Column("st_id", sql.Integer, nullable=False),
|
||||
Column("an_sachgebiet", sql.Text, nullable=True),
|
||||
Column("an_anrede", sql.Text, nullable=True),
|
||||
Column("an_titel", sql.Text, nullable=True),
|
||||
Column("an_nachname", sql.Text, nullable=True),
|
||||
Column("an_vorname", sql.Text, nullable=True),
|
||||
Column("an_position", sql.Text, nullable=True),
|
||||
Column("an_mail", sql.Text, nullable=True),
|
||||
Column("an_festnetz", sql.Text, nullable=True),
|
||||
Column("an_mobil", sql.Text, nullable=True),
|
||||
Column("an_faxnummer", sql.Text, nullable=True),
|
||||
Column("an_hauptansprechpartner", sql.Text, nullable=True),
|
||||
Column("an_anrede_anschrift", sql.Text, nullable=True),
|
||||
Column("an_bemerkung", sql.Text, nullable=True),
|
||||
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", sql.Text, 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", sql.Boolean, nullable=True, default=0),
|
||||
Column("an_archiviert", sa.Boolean, nullable=True, default=0),
|
||||
)
|
||||
|
||||
|
||||
@@ -311,8 +253,8 @@ def get_ext_crm_contact_person(
|
||||
if not db_path.exists():
|
||||
raise FileNotFoundError(f"Database not found under >{db_path}<")
|
||||
|
||||
engine = sql.create_engine(f"sqlite:///{db_path}")
|
||||
stmt = sql.select(ext_crm_contact_person)
|
||||
engine = sa.create_engine(f"sqlite:///{db_path}")
|
||||
stmt = sa.select(ext_crm_contact_person)
|
||||
df = pl.read_database(stmt, engine, schema_overrides=ext_crm_contact_person_schema)
|
||||
df = df.with_columns(
|
||||
pl.col(pl.String).str.replace_all(r"[\r\t\n]", " ").str.strip_chars(" ")
|
||||
@@ -322,81 +264,348 @@ 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)
|
||||
DF_CONTACT_PERSON = get_ext_crm_contact_person(constants.Config.DB_PATH_KONTAKTLISTE)
|
||||
|
||||
grunderfassung: sql.Table = Table(
|
||||
"grunderfassung",
|
||||
t_ext_crm_master_attach: Table = Table(
|
||||
"Master",
|
||||
MD_CRM,
|
||||
Column("ma_id", sa.Integer, nullable=False, unique=True),
|
||||
Column("wce_id", sa.ForeignKey("Nutzer.wce_id")),
|
||||
Column("ma_unternehmensname", sa.Text, nullable=True),
|
||||
Column("ma_branche", sa.Text, nullable=True),
|
||||
Column("ma_strasse", sa.Text, nullable=True),
|
||||
Column("ma_hausnummer", sa.Text, nullable=True),
|
||||
Column("ma_plz", sa.Text, nullable=True),
|
||||
Column("ma_ort", sa.Text, nullable=True),
|
||||
Column("ma_plz_postfach", sa.Text, nullable=True),
|
||||
Column("ma_postfach", sa.Text, nullable=True),
|
||||
Column("ma_website", sa.Text, nullable=True),
|
||||
Column("ma_mail", sa.Text, nullable=True),
|
||||
Column("ma_telefonnummer", sa.Text, nullable=True),
|
||||
Column("ma_faxnummer", sa.Text, nullable=True),
|
||||
Column("ma_ersteintrag_datum", SafeDateTime, nullable=True),
|
||||
Column("ma_aktualisierung_datum", SafeDateTime, nullable=True),
|
||||
Column("ma_aktualisierung_nutzer", sa.Text, nullable=True),
|
||||
Column("ma_sollprozess", sa.Text, nullable=True),
|
||||
Column("ma_auslaendische_mitarbeiter", sa.Text, nullable=True),
|
||||
Column("ma_quelle_information", sa.Text, nullable=True),
|
||||
Column("ma_bemerkung", sa.Text, nullable=True),
|
||||
Column("ma_kontakt", sa.Boolean, nullable=True),
|
||||
Column("ma_schlagworte", sa.Text, nullable=True),
|
||||
Column("ma_archiviert", sa.Boolean, nullable=True, default=False),
|
||||
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",
|
||||
# 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(
|
||||
"erfassung_id",
|
||||
sql.Integer,
|
||||
"un_id",
|
||||
sa.Integer,
|
||||
primary_key=True,
|
||||
autoincrement=True,
|
||||
),
|
||||
Column(
|
||||
"Metadaten_erstellung",
|
||||
"erstellt",
|
||||
UTCDateTime,
|
||||
nullable=True,
|
||||
nullable=False,
|
||||
default=lambda: datetime.datetime.now(datetime.UTC),
|
||||
),
|
||||
Column(
|
||||
"Metadaten_aktualisierung",
|
||||
"geloescht",
|
||||
UTCDateTime,
|
||||
nullable=True,
|
||||
),
|
||||
)
|
||||
|
||||
t_grunderfassung_personen: Table = Table(
|
||||
"Grunderfassung_Personen",
|
||||
MD_MAIN,
|
||||
Column(
|
||||
"pers_id",
|
||||
sa.Integer,
|
||||
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("Metadaten_nutzer", sql.String(20), nullable=True),
|
||||
Column("Metadaten_wiedereintrittsdatum", sql.Date, nullable=True, default=None),
|
||||
Column("Arbeitserfahrung", sql.Text, nullable=True),
|
||||
Column("Grunderfassung_fallnummer", sql.Text, nullable=True),
|
||||
Column("Grunderfassung_notiz", sql.Text, nullable=True),
|
||||
Column("HoehereBildung", sql.Text, nullable=True),
|
||||
Column("Kontaktperson__KP_adresse", sql.Text, nullable=True),
|
||||
Column("Kontaktperson__KP_anrede_anschrift", sql.Text, nullable=True),
|
||||
Column("Kontaktperson__KP_email", sql.Text, nullable=True),
|
||||
Column("Kontaktperson__KP_festnetznummer", sql.Text, nullable=True),
|
||||
Column("Kontaktperson__KP_funktion_beziehung", sql.Text, nullable=True),
|
||||
Column("Kontaktperson__KP_mobilfunknummer", sql.Text, nullable=True),
|
||||
Column("Kontaktperson__KP_name", sql.Text, nullable=True),
|
||||
Column(
|
||||
"Kontaktperson__KP_name_partner", sql.Text, nullable=True
|
||||
), # TODO: check if needed when set by trigger
|
||||
Column("Kontaktperson__KP_titel", sql.Text, nullable=True),
|
||||
Column("Kontaktperson__KP_vorname", sql.Text, nullable=True),
|
||||
Column("Partnersuche__kanal_aufmerksamkeit", sql.Text, nullable=True, default=None),
|
||||
Column("Partnersuche__person_suche", sql.Integer, nullable=True, default=None),
|
||||
Column("Partnersuche__un_suche", sql.Integer, nullable=True, default=None),
|
||||
Column("Projektrelevanz__relevanz", sql.Text, nullable=True),
|
||||
Column("Projektrelevanz__foerderperiode", sql.Text, nullable=True),
|
||||
Column("Schulbildung", sql.Text, nullable=True),
|
||||
Column("Sprachkenntnisse", sql.Text, nullable=True),
|
||||
Column("Stammdaten__PLZ", sql.Text, nullable=True),
|
||||
Column("Stammdaten__anrede_anschrift", sql.Text, nullable=True),
|
||||
Column("Stammdaten__anzahl_kinder__alter", sql.Text, nullable=True),
|
||||
Column("Stammdaten__anzahl_kinder__anzahl", sql.Integer, nullable=True),
|
||||
Column("Stammdaten__aufenthaltsort", sql.Text, nullable=True),
|
||||
Column("Stammdaten__bundesland", sql.Text, nullable=True),
|
||||
Column("Stammdaten__land", sql.Text, nullable=True),
|
||||
Column("Stammdaten__email", sql.Text, nullable=True),
|
||||
Column("Stammdaten__familienstand", sql.Text, nullable=True),
|
||||
Column("Stammdaten__festnetznummer", sql.Text, nullable=True),
|
||||
Column("Stammdaten__geburtsdatum", sql.Date, nullable=True),
|
||||
Column("Stammdaten__hausnummer", sql.Text, nullable=True),
|
||||
Column("Stammdaten__herkunftsland", sql.Text, nullable=True),
|
||||
Column("Stammdaten__mobilfunknummer", sql.Text, nullable=True),
|
||||
Column("Stammdaten__name", sql.Text, nullable=True),
|
||||
Column("Stammdaten__ort", sql.Text, nullable=True),
|
||||
Column("Stammdaten__rueckkehrer", sql.Boolean, nullable=True),
|
||||
Column("Stammdaten__staatsangehoerigkeit", sql.Text, nullable=True),
|
||||
Column("Stammdaten__strasse", sql.Text, nullable=True),
|
||||
Column("Stammdaten__titel", sql.Text, nullable=True),
|
||||
Column("Stammdaten__vorname", sql.Text, nullable=True),
|
||||
Column("WeitereInfos__WI_arbeitsstatus", sql.Text, nullable=True),
|
||||
Column("WeitereInfos__WI_aufenthaltstitel", sql.Text, nullable=True),
|
||||
Column("WeitereInfos__WI_deutsch_sprache", sql.Text, nullable=True),
|
||||
Column("WeitereInfos__WI_gueltigkeit_aufenthaltstitel", sql.Date, nullable=True),
|
||||
Column("WeitereInfos__WI_meldung_institution", sql.Text, nullable=True),
|
||||
"geloescht",
|
||||
UTCDateTime,
|
||||
nullable=True,
|
||||
),
|
||||
Column("Metadaten_nutzer", sa.String(20), nullable=True),
|
||||
Column("Metadaten_wiedereintrittsdatum", sa.Date, nullable=True, default=None),
|
||||
Column("Arbeitserfahrung", sa.Text, nullable=True),
|
||||
Column("Grunderfassung_fallnummer", sa.Text, nullable=True),
|
||||
Column("Grunderfassung_notiz", sa.Text, nullable=True),
|
||||
Column("HoehereBildung", sa.Text, nullable=True),
|
||||
Column("Kontaktperson__KP_adresse", sa.Text, nullable=True),
|
||||
Column("Kontaktperson__KP_anrede_anschrift", sa.Text, nullable=True),
|
||||
Column("Kontaktperson__KP_email", sa.Text, nullable=True),
|
||||
Column("Kontaktperson__KP_festnetznummer", sa.Text, nullable=True),
|
||||
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),
|
||||
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),
|
||||
Column("Stammdaten__anzahl_kinder__anzahl", sa.Integer, nullable=True),
|
||||
Column("Stammdaten__aufenthaltsort", sa.Text, nullable=True),
|
||||
Column("Stammdaten__bundesland", sa.Text, nullable=True),
|
||||
Column("Stammdaten__land", sa.Text, nullable=True),
|
||||
Column("Stammdaten__email", sa.Text, nullable=True),
|
||||
Column("Stammdaten__familienstand", sa.Text, nullable=True),
|
||||
Column("Stammdaten__festnetznummer", sa.Text, nullable=True),
|
||||
Column("Stammdaten__geburtsdatum", sa.Date, nullable=True),
|
||||
Column("Stammdaten__hausnummer", sa.Text, nullable=True),
|
||||
Column("Stammdaten__herkunftsland", sa.Text, nullable=True),
|
||||
Column("Stammdaten__mobilfunknummer", sa.Text, nullable=True),
|
||||
Column("Stammdaten__name", sa.Text, nullable=True),
|
||||
Column("Stammdaten__ort", sa.Text, nullable=True),
|
||||
Column("Stammdaten__rueckkehrer", sa.Boolean, nullable=True),
|
||||
Column("Stammdaten__staatsangehoerigkeit", sa.Text, nullable=True),
|
||||
Column("Stammdaten__strasse", sa.Text, nullable=True),
|
||||
Column("Stammdaten__titel", sa.Text, nullable=True),
|
||||
Column("Stammdaten__vorname", sa.Text, nullable=True),
|
||||
Column("WeitereInfos__WI_arbeitsstatus", sa.Text, nullable=True),
|
||||
Column("WeitereInfos__WI_aufenthaltstitel", sa.Text, nullable=True),
|
||||
Column("WeitereInfos__WI_deutsch_sprache", sa.Text, nullable=True),
|
||||
Column("WeitereInfos__WI_gueltigkeit_aufenthaltstitel", sa.Date, nullable=True),
|
||||
Column("WeitereInfos__WI_meldung_institution", sa.Text, nullable=True),
|
||||
)
|
||||
|
||||
# MD_MAIN.create_all(ENGINE)
|
||||
t_zuordnung_personen_unternehmen: Table = Table(
|
||||
"Personen_Unternehmen_Zuordnung",
|
||||
MD_MAIN,
|
||||
Column(
|
||||
"id",
|
||||
sa.Integer,
|
||||
primary_key=True,
|
||||
autoincrement=True,
|
||||
),
|
||||
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(
|
||||
"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("Partnersuche__kontaktweg", sa.Text, nullable=True),
|
||||
Index(
|
||||
"uq_aktive_zuordnung",
|
||||
"un_id",
|
||||
"pers_id",
|
||||
unique=True,
|
||||
sqlite_where=sql.text("geloescht IS NULL"),
|
||||
),
|
||||
)
|
||||
|
||||
t_grunderfassung_dokumente: Table = Table(
|
||||
"Grunderfassung_Dokumente",
|
||||
MD_MAIN,
|
||||
Column(
|
||||
"id",
|
||||
sa.Integer,
|
||||
primary_key=True,
|
||||
autoincrement=True,
|
||||
),
|
||||
Column("pers_id", sa.ForeignKey("Grunderfassung_Personen.pers_id"), nullable=False),
|
||||
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("speicher_pfad", sa.Text, nullable=False),
|
||||
Column("dateiname_original", sa.Text, nullable=False),
|
||||
Column("dateigroesse_bytes", sa.Integer, nullable=True),
|
||||
Column("datum_hinzugefuegt", UTCDateTime, nullable=False),
|
||||
Column("mime_type", sa.Text, nullable=False),
|
||||
Column("anmerkung", sa.Text, nullable=False),
|
||||
)
|
||||
|
||||
t_beratung_vorgang: Table = Table(
|
||||
"Beratung_Vorgang",
|
||||
MD_MAIN,
|
||||
Column(
|
||||
"vorgang_id",
|
||||
sa.Integer,
|
||||
primary_key=True,
|
||||
autoincrement=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(
|
||||
"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,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
t_beratung_einzelberatung: Table = Table(
|
||||
"Beratung_Einzelberatung",
|
||||
MD_MAIN,
|
||||
Column(
|
||||
"beratung_id",
|
||||
sa.Integer,
|
||||
primary_key=True,
|
||||
autoincrement=True,
|
||||
),
|
||||
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(
|
||||
"zeitstempel",
|
||||
UTCDateTime,
|
||||
nullable=False,
|
||||
default=lambda: datetime.datetime.now(datetime.UTC),
|
||||
),
|
||||
Column("ansprechpartner", sa.Text, nullable=False),
|
||||
Column("kommunikationsweg", sa.Text, nullable=False),
|
||||
Column("thema_crm_matrix", sa.Text, nullable=True),
|
||||
Column("anmerkungen", sa.Text, nullable=True),
|
||||
Column("rueckmeldung", sa.Text, nullable=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,
|
||||
),
|
||||
)
|
||||
|
||||
+71
-182
@@ -4,19 +4,26 @@ import dataclasses as dc
|
||||
import enum
|
||||
import uuid
|
||||
from collections.abc import Sequence
|
||||
from typing import Any, Final
|
||||
from typing import TYPE_CHECKING, Any, Final, Generic, TypeVar
|
||||
|
||||
import babel
|
||||
|
||||
from wce_crm.custom_widget_registry import CUSTOM_WIDGET_NAMES
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from PySide6.QtWidgets import QWidget
|
||||
|
||||
T = TypeVar("T", bound="QWidget")
|
||||
|
||||
|
||||
class FormFieldType(enum.StrEnum):
|
||||
GROUP = enum.auto()
|
||||
TEXT = enum.auto()
|
||||
LONGTEXT = enum.auto()
|
||||
DATE = enum.auto()
|
||||
DATE_FLEXIBLE = enum.auto()
|
||||
DATETIME = enum.auto()
|
||||
DATETIME_FLEXIBLE = enum.auto()
|
||||
DROPDOWN = enum.auto()
|
||||
EXTENDED_DROPDOWN = enum.auto()
|
||||
DYNAMIC_LIST = enum.auto()
|
||||
@@ -29,7 +36,7 @@ class FormFieldType(enum.StrEnum):
|
||||
|
||||
|
||||
@dc.dataclass(slots=True)
|
||||
class FormField:
|
||||
class FormField(Generic[T]):
|
||||
label: str
|
||||
type: FormFieldType
|
||||
children: Sequence[FormField] = dc.field(default_factory=list)
|
||||
@@ -125,7 +132,7 @@ class CountryList:
|
||||
|
||||
|
||||
def get_country_list_german() -> CountryList:
|
||||
locale = babel.Locale("de", "DE")
|
||||
locale = babel.Locale("en", "GB")
|
||||
countries: list[tuple[str, str]] = []
|
||||
iso_to_country: dict[str, str] = {}
|
||||
|
||||
@@ -180,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,
|
||||
@@ -300,52 +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 = [
|
||||
_INITREC_CONTACT_PERSON = [
|
||||
FormField(
|
||||
"Name Unternehmen/Netzwerkpartner (vorausgefüllt von Suche)",
|
||||
"Name Unternehmen/Netzwerkpartner",
|
||||
FormFieldType.TEXT,
|
||||
key="KP_name_partner",
|
||||
required=False,
|
||||
placeholder="Text wird nach gewähltem Unternehmen angezeigt",
|
||||
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",
|
||||
@@ -392,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",
|
||||
@@ -419,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",
|
||||
@@ -438,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",
|
||||
@@ -447,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",
|
||||
@@ -455,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?",
|
||||
@@ -494,7 +498,6 @@ INITREC_MASTER_DATA = [
|
||||
key="bundesland",
|
||||
required=False,
|
||||
options=GERMAN_STATE_LIST.for_dropdown,
|
||||
tooltip=("nur wenn Inland angegeben"),
|
||||
),
|
||||
FormField(
|
||||
"Land",
|
||||
@@ -527,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(
|
||||
@@ -541,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"),
|
||||
@@ -552,7 +552,7 @@ INITREC_MASTER_DATA = [
|
||||
]
|
||||
|
||||
|
||||
INITREC_ADDITIONAL_DATA = [
|
||||
_INITREC_ADDITIONAL_DATA = [
|
||||
FormField(
|
||||
"Deutsch als Kommunikationssprache",
|
||||
FormFieldType.DROPDOWN,
|
||||
@@ -585,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",
|
||||
),
|
||||
@@ -627,7 +626,7 @@ INITREC_ADDITIONAL_DATA = [
|
||||
]
|
||||
|
||||
|
||||
INITREC_SCHOOL = [
|
||||
_INITREC_SCHOOL = [
|
||||
FormField("Abschluss", FormFieldType.TEXT, required=False, key="SB_abschluss"),
|
||||
FormField(
|
||||
"Abschlussgrad laut Dokument",
|
||||
@@ -665,7 +664,7 @@ INITREC_SCHOOL = [
|
||||
]
|
||||
|
||||
|
||||
INITREC_HIGHER_EDUCATION = [
|
||||
_INITREC_HIGHER_EDUCATION = [
|
||||
FormField(
|
||||
"Anerkennung",
|
||||
FormFieldType.TEXT,
|
||||
@@ -677,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",
|
||||
@@ -699,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",
|
||||
@@ -729,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,
|
||||
@@ -816,7 +806,6 @@ INITREC_WORK_EXPERIENCE = [
|
||||
("Teilzeit", None),
|
||||
("Sonstiges", None),
|
||||
],
|
||||
tooltip="Minijob, Praktikum, Wehrdienst, soziale Dienste",
|
||||
),
|
||||
FormField(
|
||||
"Bemerkungsfeld",
|
||||
@@ -827,7 +816,7 @@ INITREC_WORK_EXPERIENCE = [
|
||||
]
|
||||
|
||||
|
||||
INITREC_LANGUAGES = [
|
||||
_INITREC_LANGUAGES = [
|
||||
FormField(
|
||||
"Sprache",
|
||||
FormFieldType.TEXT,
|
||||
@@ -869,7 +858,7 @@ INITREC_LANGUAGES = [
|
||||
),
|
||||
FormField(
|
||||
"Datum des Nachweises",
|
||||
FormFieldType.DATE,
|
||||
FormFieldType.DATE_FLEXIBLE,
|
||||
required=False,
|
||||
key="SP_datum_nachweis",
|
||||
),
|
||||
@@ -881,120 +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,
|
||||
),
|
||||
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_suche",
|
||||
custom_widget="grunderfassung_unternehmen_suche",
|
||||
key="Partnersuche",
|
||||
children=INITREC_COMP_SEARCH_HEAD,
|
||||
),
|
||||
FormField(
|
||||
"Status && Projektrelevanz",
|
||||
FormFieldType.GROUP,
|
||||
key="Projektrelevanz",
|
||||
children=[
|
||||
FormField(
|
||||
"Projektrelevanz",
|
||||
FormFieldType.DYNAMIC_DROPDOWN_OPTION,
|
||||
key="",
|
||||
trigger_value="ja",
|
||||
children=[
|
||||
FormField(
|
||||
"Relevanz",
|
||||
FormFieldType.DROPDOWN,
|
||||
required=True,
|
||||
options=[("ja", None), ("nein", None)],
|
||||
key="relevanz",
|
||||
children=[
|
||||
FormField(
|
||||
"Förderperiode", FormFieldType.TEXT, key="foerderperiode"
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
FormField(
|
||||
"Daten Kontaktperson",
|
||||
FormFieldType.GROUP,
|
||||
key="Kontaktperson",
|
||||
children=INITREC_CONTACT_PERSON,
|
||||
),
|
||||
FormField(
|
||||
"Stammdaten",
|
||||
FormFieldType.GROUP,
|
||||
key="Stammdaten",
|
||||
children=INITREC_MASTER_DATA,
|
||||
),
|
||||
FormField(
|
||||
"Weitere Informationen",
|
||||
FormFieldType.GROUP,
|
||||
key="WeitereInfos",
|
||||
children=INITREC_ADDITIONAL_DATA,
|
||||
),
|
||||
FormField(
|
||||
"Schulbildung",
|
||||
FormFieldType.DYNAMIC_LIST,
|
||||
children=INITREC_SCHOOL,
|
||||
key="Schulbildung",
|
||||
),
|
||||
FormField(
|
||||
"Studium/Ausbildung",
|
||||
FormFieldType.DYNAMIC_LIST,
|
||||
children=INITREC_HIGHER_EDUCATION,
|
||||
key="HoehereBildung",
|
||||
),
|
||||
FormField(
|
||||
"Arbeitserfahrung",
|
||||
FormFieldType.DYNAMIC_LIST,
|
||||
children=INITREC_WORK_EXPERIENCE,
|
||||
key="Arbeitserfahrung",
|
||||
),
|
||||
FormField(
|
||||
"Sprachkenntnisse",
|
||||
FormFieldType.DYNAMIC_LIST,
|
||||
children=INITREC_LANGUAGES,
|
||||
key="Sprachkenntnisse",
|
||||
children=_INITREC_COMP_SEARCH_HEAD,
|
||||
),
|
||||
]
|
||||
|
||||
INITREC_PERSON = [
|
||||
INITREC_METADATEN = [
|
||||
FormField(
|
||||
"Ersteintrag Datum",
|
||||
FormFieldType.TEXT_DATETIME,
|
||||
@@ -1017,10 +902,11 @@ INITREC_PERSON = [
|
||||
required=False,
|
||||
key="Metadaten_nutzer",
|
||||
readonly=True,
|
||||
ignore_get_data=False,
|
||||
),
|
||||
FormField(
|
||||
"Wiedereintrittsdatum",
|
||||
FormFieldType.DATE,
|
||||
FormFieldType.DATE_FLEXIBLE,
|
||||
required=False,
|
||||
key="Metadaten_wiedereintrittsdatum",
|
||||
readonly=False,
|
||||
@@ -1037,6 +923,9 @@ INITREC_PERSON = [
|
||||
required=False,
|
||||
key="Grunderfassung_notiz",
|
||||
),
|
||||
]
|
||||
|
||||
INITREC_PERSON = [
|
||||
FormField(
|
||||
"Status && Projektrelevanz",
|
||||
FormFieldType.GROUP,
|
||||
@@ -1049,7 +938,7 @@ INITREC_PERSON = [
|
||||
trigger_value="ja",
|
||||
children=[
|
||||
FormField(
|
||||
"Relevanz",
|
||||
"Relevant perspektivERZ",
|
||||
FormFieldType.DROPDOWN,
|
||||
required=True,
|
||||
options=[("ja", None), ("nein", None)],
|
||||
@@ -1068,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",
|
||||
),
|
||||
]
|
||||
|
||||
+4734
-791
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,78 @@
|
||||
from pydantic import ValidationError
|
||||
from PySide6.QtWidgets import QMessageBox, QWidget
|
||||
|
||||
from wce_crm.data_models import translate_pydantic_errors
|
||||
|
||||
|
||||
def get_user_confirmation(
|
||||
parent_widget: QWidget,
|
||||
title: str,
|
||||
message: str,
|
||||
) -> bool:
|
||||
response = QMessageBox.question(
|
||||
parent_widget,
|
||||
title,
|
||||
message,
|
||||
buttons=(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No),
|
||||
defaultButton=QMessageBox.StandardButton.No,
|
||||
)
|
||||
|
||||
if response == QMessageBox.StandardButton.Yes:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def get_message_box(
|
||||
msg_type: QMessageBox.Icon,
|
||||
title: str,
|
||||
message: str,
|
||||
detailed_text: str | None = None,
|
||||
) -> QMessageBox:
|
||||
msg_box = QMessageBox()
|
||||
msg_box.setIcon(msg_type)
|
||||
msg_box.setWindowTitle(title)
|
||||
msg_box.setText(message)
|
||||
if detailed_text is not None:
|
||||
msg_box.setDetailedText(detailed_text)
|
||||
|
||||
return msg_box
|
||||
|
||||
|
||||
def pydantic_validation_error_handling(
|
||||
val_error: ValidationError,
|
||||
) -> None:
|
||||
error_texts: list[str] = []
|
||||
translated_errors = translate_pydantic_errors(val_error.errors())
|
||||
|
||||
for error in translated_errors:
|
||||
error_field = str(error["loc"][0])
|
||||
reason = error["msg"]
|
||||
path = " → ".join(error["loc"][:-1]) # type: ignore
|
||||
|
||||
error_texts.append(f"- {error_field}: {reason}, (Pfad: {path})")
|
||||
|
||||
msg_box = get_message_box(
|
||||
QMessageBox.Icon.Warning,
|
||||
"Fehler bei der Validierung der Eingabedaten",
|
||||
(
|
||||
"Bei der Validierung der Eingaben ist ein Fehler aufgetreten. Details "
|
||||
"sind unten angefügt."
|
||||
),
|
||||
"\n".join(error_texts),
|
||||
)
|
||||
msg_box.exec()
|
||||
|
||||
|
||||
def GUI_validation_error_handling(
|
||||
errors: list[str],
|
||||
) -> None:
|
||||
error_text = "Bitte füllen Sie die folgenden Pflichtfelder aus:\n\n▸ " + "\n▸ ".join(
|
||||
errors
|
||||
)
|
||||
msg_box = get_message_box(
|
||||
QMessageBox.Icon.Warning,
|
||||
"Fehlende oder fehlerhafte Angaben",
|
||||
error_text,
|
||||
)
|
||||
msg_box.exec()
|
||||
@@ -0,0 +1,771 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses as dc
|
||||
import datetime
|
||||
import os
|
||||
import sys
|
||||
from collections.abc import Iterable
|
||||
from pathlib import Path
|
||||
from pprint import pformat
|
||||
from typing import TYPE_CHECKING, Literal
|
||||
|
||||
from PySide6.QtCore import (
|
||||
QFileInfo,
|
||||
QMimeDatabase,
|
||||
QSignalBlocker,
|
||||
Qt,
|
||||
QUrl,
|
||||
Signal,
|
||||
)
|
||||
from PySide6.QtGui import QDesktopServices, QDragEnterEvent, QDropEvent
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication,
|
||||
QComboBox,
|
||||
QFileDialog,
|
||||
QFileIconProvider,
|
||||
QFrame,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QListWidget,
|
||||
QListWidgetItem,
|
||||
QMainWindow,
|
||||
QPushButton,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
import wce_crm.constants
|
||||
from wce_crm.constants import TIMEZONE_CEST
|
||||
from wce_crm.data_models import (
|
||||
Grunderfassung_Dokument,
|
||||
Page_InitRec_DocumentSection_Doc_State,
|
||||
Page_InitRec_DocumentSection_State,
|
||||
Session,
|
||||
set_page_state,
|
||||
)
|
||||
from wce_crm.gui_components.dialogs import get_user_confirmation
|
||||
from wce_crm.logging import logger_gui as logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from wce_crm.data_models import Module
|
||||
|
||||
DEBUG: bool = True
|
||||
if not wce_crm.constants.Config.DEVELOPMENT_STATE:
|
||||
DEBUG = False
|
||||
|
||||
|
||||
def get_mime_type(
|
||||
file_path: Path,
|
||||
) -> str:
|
||||
"""gets the MIME type of a file via PySide6 / QMimeDatabase"""
|
||||
db = QMimeDatabase()
|
||||
mime_type = db.mimeTypeForFile(str(file_path))
|
||||
|
||||
# .name() -> for example "application/pdf" or "image/png"
|
||||
return mime_type.name()
|
||||
|
||||
|
||||
def _format_file_size(
|
||||
size_in_bytes: int,
|
||||
) -> str:
|
||||
"""converts bytes in readable string, e.g. KB or MB"""
|
||||
if size_in_bytes < 1024:
|
||||
return f"{size_in_bytes} B"
|
||||
elif size_in_bytes < 1024 * 1024:
|
||||
return f"{size_in_bytes / 1024:.1f} KB"
|
||||
else:
|
||||
return f"{size_in_bytes / (1024 * 1024):.1f} MB"
|
||||
|
||||
|
||||
class _DropZoneWidget(QFrame):
|
||||
files_dropped = Signal(list) # sends list of pathlib.Path objects
|
||||
|
||||
STYLE_NORMAL = """
|
||||
_DropZoneWidget {
|
||||
border: 2px dashed #B0B0B0;
|
||||
border-radius: 8px;
|
||||
background-color: #F9F9F9;
|
||||
}
|
||||
_DropZoneWidget:hover {
|
||||
border-color: #0078D4;
|
||||
background-color: #F0F6FF;
|
||||
}
|
||||
/* optics in locked state */
|
||||
_DropZoneWidget:disabled {
|
||||
border: 2px dashed #D0D0D0;
|
||||
background-color: #F2F2F2;
|
||||
}
|
||||
_DropZoneWidget:disabled {
|
||||
border: 2px dashed #D0D0D0;
|
||||
background-color: #F2F2F2;
|
||||
}
|
||||
"""
|
||||
|
||||
STYLE_DRAG_ACTIVE = """
|
||||
_DropZoneWidget {
|
||||
border: 2px dashed #0078D4;
|
||||
border-radius: 8px;
|
||||
background-color: #E1EFFA;
|
||||
}
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent: QWidget | None = None,
|
||||
):
|
||||
super().__init__(parent)
|
||||
self.setAcceptDrops(True)
|
||||
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
|
||||
self.setStyleSheet(self.STYLE_NORMAL)
|
||||
self.setFixedHeight(60)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
self.label = QLabel(
|
||||
"📁 <b>Dateien hierher ziehen</b> oder <u>klicken</u> zum Auswählen"
|
||||
)
|
||||
self.label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
layout.addWidget(self.label)
|
||||
|
||||
def changeEvent(
|
||||
self,
|
||||
event,
|
||||
) -> None:
|
||||
if event.type() == event.Type.EnabledChange:
|
||||
if not self.isEnabled():
|
||||
self.label.setText("🔒 <b>Dateiupload deaktiviert</b> (Formular gesperrt)")
|
||||
self.setCursor(Qt.CursorShape.ForbiddenCursor)
|
||||
else:
|
||||
self.label.setText(
|
||||
"📁 <b>Dateien hierher ziehen</b> oder <u>klicken</u> zum Auswählen"
|
||||
)
|
||||
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
super().changeEvent(event)
|
||||
|
||||
def dragEnterEvent(
|
||||
self,
|
||||
event: QDragEnterEvent,
|
||||
) -> None:
|
||||
if event.mimeData().hasUrls():
|
||||
# check if MDB/explorer gives at least one real file (no folder)
|
||||
has_files = False
|
||||
for url in event.mimeData().urls():
|
||||
pfad = url.toLocalFile()
|
||||
if pfad and os.path.isfile(pfad):
|
||||
has_files = True
|
||||
break
|
||||
|
||||
if has_files:
|
||||
event.acceptProposedAction()
|
||||
self.setStyleSheet(self.STYLE_DRAG_ACTIVE)
|
||||
|
||||
def dragLeaveEvent(
|
||||
self,
|
||||
event,
|
||||
) -> None:
|
||||
self.setStyleSheet(self.STYLE_NORMAL)
|
||||
|
||||
def dropEvent(
|
||||
self,
|
||||
event: QDropEvent,
|
||||
) -> None:
|
||||
self.dragLeaveEvent(None)
|
||||
|
||||
valid_files: list[Path] = []
|
||||
for url in event.mimeData().urls():
|
||||
if url.isLocalFile():
|
||||
path = Path(url.toLocalFile()).resolve()
|
||||
# only add if it is a file
|
||||
if path.is_file():
|
||||
valid_files.append(path)
|
||||
|
||||
if valid_files:
|
||||
self.files_dropped.emit(valid_files)
|
||||
|
||||
def mousePressEvent(
|
||||
self,
|
||||
event,
|
||||
) -> None:
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
files, _ = QFileDialog.getOpenFileNames(self, "Dokumente auswählen")
|
||||
if files:
|
||||
files_pathlib = [Path(file).resolve() for file in files]
|
||||
self.files_dropped.emit(files_pathlib)
|
||||
|
||||
|
||||
class FileItemWidget(QFrame):
|
||||
request_remove = Signal(QWidget) # signal to parent to remove entry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
state: Page_InitRec_DocumentSection_Doc_State,
|
||||
qt_list_item: QListWidgetItem,
|
||||
parent: QWidget | None = None,
|
||||
) -> None:
|
||||
super().__init__(parent)
|
||||
self.STATE = state
|
||||
self.qt_list_item = qt_list_item
|
||||
|
||||
self.setStyleSheet("""
|
||||
FileItemWidget {
|
||||
background-color: #FFFFFF;
|
||||
border: 1px solid #E0E0E0;
|
||||
border-radius: 6px;
|
||||
padding: 6px;
|
||||
}
|
||||
FileItemWidget:hover {
|
||||
border-color: #B0B0B0;
|
||||
}
|
||||
/* optics in locked state */
|
||||
FileItemWidget:disabled {
|
||||
background-color: #F8F8F8;
|
||||
border-color: #E5E5E5;
|
||||
}
|
||||
FileItemWidget QLabel:disabled {
|
||||
color: #777777;
|
||||
}
|
||||
""")
|
||||
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(8, 6, 8, 6)
|
||||
|
||||
# file icon: use dummy to avoid icons with checkmarks (synchronised folder)
|
||||
self.icon_label = QLabel()
|
||||
self.icon_provider = QFileIconProvider()
|
||||
layout.addWidget(self.icon_label)
|
||||
|
||||
# file info: name, size, date, mime type
|
||||
info_vbox = QVBoxLayout()
|
||||
info_vbox.setSpacing(2)
|
||||
|
||||
self.file_size_display: str = ""
|
||||
|
||||
self.name_label = QLabel()
|
||||
self.meta_label = QLabel()
|
||||
self.meta_label.setStyleSheet("font-size: 11px;")
|
||||
|
||||
# additional note field for more comprehensible file descriptions
|
||||
self.note_edit = QLineEdit()
|
||||
self.note_edit.setPlaceholderText("Eigene Anmerkung / Bez. eingeben (optional)...")
|
||||
self.note_edit.setStyleSheet("""
|
||||
QLineEdit {
|
||||
font-size: 11px;
|
||||
padding: 2px 5px;
|
||||
min-height: 22px; /* avoid vertical cropping of text */
|
||||
border: 1px solid #CCCCCC;
|
||||
border-radius: 3px;
|
||||
}
|
||||
QLineEdit:focus {
|
||||
border-color: #0078D4; /* blue frame on click */
|
||||
}
|
||||
""")
|
||||
|
||||
info_vbox.addWidget(self.name_label)
|
||||
info_vbox.addWidget(self.meta_label)
|
||||
info_vbox.addWidget(self.note_edit)
|
||||
layout.addLayout(info_vbox, stretch=2)
|
||||
|
||||
# category dropdown: not needed by WCE, but still included to re-enable later
|
||||
# ?? maybe add later
|
||||
self.category_combo = QComboBox()
|
||||
# self.category_combo.addItems(
|
||||
# [
|
||||
# "Allgemein / Unsortiert",
|
||||
# "Identitätsnachweis",
|
||||
# "Vertrag",
|
||||
# "Gewerbeanmeldung",
|
||||
# "Sonstiges",
|
||||
# ]
|
||||
# )
|
||||
# self.category_combo.setFixedWidth(160)
|
||||
layout.addWidget(self.category_combo)
|
||||
self.category_combo.setVisible(False)
|
||||
|
||||
# action buttons: view and delete
|
||||
self.btn_preview = QPushButton("👁️")
|
||||
self.btn_preview.setToolTip("Datei im Standardprogramm öffnen")
|
||||
self.btn_preview.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.btn_preview.setFixedWidth(36)
|
||||
self.btn_preview.clicked.connect(self._open_preview)
|
||||
|
||||
self.btn_delete = QPushButton("🗑️")
|
||||
self.btn_delete.setToolTip("Datei entfernen")
|
||||
self.btn_delete.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.btn_delete.setFixedWidth(36)
|
||||
self.btn_delete.clicked.connect(self._on_delete_btn_clicked)
|
||||
|
||||
layout.addWidget(self.btn_preview)
|
||||
layout.addWidget(self.btn_delete)
|
||||
|
||||
if DEBUG:
|
||||
btn_state = QPushButton("STATE")
|
||||
btn_state.setToolTip("Debug call...")
|
||||
btn_state.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
btn_state.clicked.connect(self.get_state)
|
||||
layout.addWidget(btn_state)
|
||||
|
||||
self._sync_state_to_GUI(None)
|
||||
|
||||
def _on_delete_btn_clicked(self) -> None:
|
||||
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.request_remove.emit(self)
|
||||
|
||||
def _sync_state_to_GUI(
|
||||
self,
|
||||
section: Literal["locking", "content"] | None,
|
||||
) -> None:
|
||||
|
||||
if section is None or section == "content":
|
||||
created_at_string = self.STATE.datum_hinzugefuegt.astimezone(
|
||||
TIMEZONE_CEST
|
||||
).strftime("%d.%m.%Y %H:%M")
|
||||
|
||||
file_icon = self.icon_provider.icon(
|
||||
QFileInfo(f"dummy{self.STATE.speicher_pfad.suffix}")
|
||||
)
|
||||
self.icon_label.setPixmap(file_icon.pixmap(28, 28))
|
||||
|
||||
if not self.STATE.dateiname_original:
|
||||
self.STATE.dateiname_original = self.STATE.speicher_pfad.name
|
||||
self.STATE.mime_type = get_mime_type(self.STATE.speicher_pfad)
|
||||
|
||||
try:
|
||||
if not self.STATE.dateigroesse_bytes or self.STATE.dateigroesse_bytes == 0:
|
||||
self.STATE.dateigroesse_bytes = self.STATE.speicher_pfad.stat().st_size
|
||||
self.file_size_display = _format_file_size(self.STATE.dateigroesse_bytes)
|
||||
except OSError:
|
||||
self.file_size_display = "Unbekannt"
|
||||
|
||||
self.name_label.setText(f"<b>{self.STATE.dateiname_original}</b>")
|
||||
self.meta_label.setText(
|
||||
(
|
||||
f"<span style='color: #666666;'>{self.file_size_display} • Hinzugefügt: "
|
||||
f"{created_at_string}</span>"
|
||||
)
|
||||
)
|
||||
print(f"\n\n\n {self.STATE.anmerkung=} \n {self.STATE} \n\n\n")
|
||||
self.note_edit.setText(self.STATE.anmerkung)
|
||||
|
||||
if section is None or section == "locking":
|
||||
if self.STATE.locked:
|
||||
self.btn_delete.setEnabled(False)
|
||||
self.note_edit.setReadOnly(True)
|
||||
else:
|
||||
self.btn_delete.setEnabled(True)
|
||||
self.note_edit.setReadOnly(False)
|
||||
|
||||
def _sync_GUI_to_state(self) -> None:
|
||||
self.STATE.anmerkung = self.note_edit.text().strip()
|
||||
|
||||
def _open_preview(self):
|
||||
QDesktopServices.openUrl(QUrl.fromLocalFile(self.STATE.speicher_pfad))
|
||||
|
||||
def validate(self) -> list[str]:
|
||||
err_list: list[str] = []
|
||||
return err_list
|
||||
|
||||
def save_data(self) -> None:
|
||||
pass
|
||||
|
||||
def load_state(
|
||||
self,
|
||||
new_state: Page_InitRec_DocumentSection_Doc_State,
|
||||
) -> None:
|
||||
set_page_state(self.STATE, new_state)
|
||||
self._sync_state_to_GUI(None)
|
||||
|
||||
def lock(self) -> None:
|
||||
self.STATE.locked = True
|
||||
self._sync_state_to_GUI("locking")
|
||||
|
||||
def unlock(self) -> None:
|
||||
self.STATE.locked = False
|
||||
self._sync_state_to_GUI("locking")
|
||||
|
||||
def get_state(self) -> Page_InitRec_DocumentSection_Doc_State:
|
||||
self._sync_GUI_to_state()
|
||||
logger.debug(
|
||||
"[Module -- DocumentSelection] Current state of row:\n%s",
|
||||
pformat(self.STATE),
|
||||
)
|
||||
return self.STATE
|
||||
|
||||
|
||||
class DocumentSectionWidget(QWidget):
|
||||
"""main widget for file selection"""
|
||||
|
||||
doc_count_changed = Signal(int) # sends current number of files contained by the widget
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
state: Page_InitRec_DocumentSection_State,
|
||||
parent: QWidget | None = None,
|
||||
) -> None:
|
||||
super().__init__(parent)
|
||||
self.STATE = state
|
||||
|
||||
main_layout = QVBoxLayout(self)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
# Dropzone at the top
|
||||
self.drop_zone = _DropZoneWidget()
|
||||
self.drop_zone.files_dropped.connect(self.add_new_rows)
|
||||
main_layout.addWidget(self.drop_zone)
|
||||
|
||||
# list containing the file items
|
||||
self.file_list_widget = QListWidget()
|
||||
self.file_list_widget.setStyleSheet("""
|
||||
QListWidget {
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
}
|
||||
QListWidget::item {
|
||||
padding: 0px;
|
||||
margin: 0px 0px 6px 0px; /* 6px Abstand zwischen den einzelnen Datei-Zeilen */
|
||||
}
|
||||
""")
|
||||
|
||||
main_layout.addWidget(self.file_list_widget)
|
||||
|
||||
def _sync_state_to_GUI(
|
||||
self,
|
||||
section: Literal["content", "locking"] | None,
|
||||
) -> None:
|
||||
self.setUpdatesEnabled(False)
|
||||
|
||||
if section is None or section == "content":
|
||||
with QSignalBlocker(self):
|
||||
self._remove_all_rows()
|
||||
for doc in self.STATE.docs:
|
||||
if not doc.geloescht:
|
||||
self.add_row(doc, True)
|
||||
self.doc_count_changed.emit(self.STATE.doc_count)
|
||||
|
||||
if section is None or section == "locking":
|
||||
if self.STATE.locked:
|
||||
for m in self.STATE.child_modules:
|
||||
m.lock()
|
||||
self.drop_zone.setEnabled(False)
|
||||
else:
|
||||
for m in self.STATE.child_modules:
|
||||
m.unlock()
|
||||
self.drop_zone.setEnabled(True)
|
||||
|
||||
self.setUpdatesEnabled(True)
|
||||
|
||||
def _sync_GUI_to_state(self) -> None:
|
||||
logger.debug(
|
||||
(
|
||||
"[Module -- DocumentSelection] Document selection widget: Call "
|
||||
"syncing GUI to state"
|
||||
)
|
||||
)
|
||||
logger.debug(
|
||||
"[Module -- DocumentSelection] Document selection widget - Child modules: %s",
|
||||
pformat(self.STATE.child_modules),
|
||||
)
|
||||
|
||||
# Child modules are ACTIVE elements, viewable and changeable by the user.
|
||||
# The states are already part of this widget and linked by reference, but
|
||||
# the current user input must be synced with these states. Therefore, the
|
||||
# `get_state` method of these children is called, which implicitly updates
|
||||
# their state. The returned state reference is not needed.
|
||||
for m in self.STATE.child_modules:
|
||||
_ = m.get_state()
|
||||
|
||||
self.STATE.doc_count = self.file_list_widget.count()
|
||||
|
||||
def lock(self) -> None:
|
||||
self.STATE.locked = True
|
||||
self._sync_state_to_GUI("locking")
|
||||
|
||||
def unlock(self) -> None:
|
||||
self.STATE.locked = False
|
||||
self._sync_state_to_GUI("locking")
|
||||
|
||||
def validate(self) -> list[str]:
|
||||
err_list: list[str] = []
|
||||
for m in self.STATE.child_modules:
|
||||
err_list.extend(m.validate())
|
||||
|
||||
return err_list
|
||||
|
||||
def load_state(
|
||||
self,
|
||||
new_state: Page_InitRec_DocumentSection_State,
|
||||
) -> None:
|
||||
set_page_state(self.STATE, new_state)
|
||||
self._sync_state_to_GUI(None)
|
||||
|
||||
def save_data(self) -> None:
|
||||
pass
|
||||
|
||||
def _remove_all_rows(self) -> None:
|
||||
while self.STATE.child_modules:
|
||||
self._remove_row(self.STATE.child_modules[0])
|
||||
|
||||
def add_row(
|
||||
self,
|
||||
row_state: Page_InitRec_DocumentSection_Doc_State,
|
||||
state_exists: bool,
|
||||
) -> None:
|
||||
# embed in QListWidget
|
||||
list_item = QListWidgetItem(self.file_list_widget)
|
||||
item_widget = FileItemWidget(row_state, list_item)
|
||||
item_widget.request_remove.connect(self._remove_row)
|
||||
|
||||
# min size to avoid cropping of text
|
||||
size_hint = item_widget.sizeHint()
|
||||
size_hint.setHeight(size_hint.height() + 4)
|
||||
list_item.setSizeHint(size_hint)
|
||||
|
||||
self.file_list_widget.addItem(list_item)
|
||||
self.file_list_widget.setItemWidget(list_item, item_widget)
|
||||
|
||||
self.STATE.child_modules.append(item_widget)
|
||||
if not state_exists:
|
||||
self.STATE.docs.append(row_state)
|
||||
|
||||
self.STATE.doc_count = self.file_list_widget.count()
|
||||
self.doc_count_changed.emit(self.STATE.doc_count)
|
||||
|
||||
def _remove_row(
|
||||
self,
|
||||
widget: FileItemWidget,
|
||||
):
|
||||
row = self.file_list_widget.row(widget.qt_list_item)
|
||||
|
||||
if row >= 0:
|
||||
self.file_list_widget.takeItem(row)
|
||||
self.STATE.child_modules.remove(widget)
|
||||
widget.deleteLater()
|
||||
# do not remove the corresponding state since it is tracked
|
||||
# to be post-processed when the database saving operation is triggered
|
||||
self.STATE.doc_count = self.file_list_widget.count()
|
||||
self.doc_count_changed.emit(self.STATE.doc_count)
|
||||
|
||||
def get_state(self) -> Page_InitRec_DocumentSection_State:
|
||||
self._sync_GUI_to_state()
|
||||
logger.debug(
|
||||
"[Module -- DocumentSelection] Current state of document selection widget:\n%s",
|
||||
pformat(self.STATE),
|
||||
)
|
||||
|
||||
return self.STATE
|
||||
|
||||
def add_new_rows(
|
||||
self,
|
||||
filepaths: Iterable[Path],
|
||||
) -> None:
|
||||
with QSignalBlocker(self):
|
||||
for path in filepaths:
|
||||
# create custom row widget
|
||||
new_state = Page_InitRec_DocumentSection_Doc_State(
|
||||
session=self.STATE.session,
|
||||
id=None,
|
||||
speicher_pfad=path,
|
||||
locked=self.STATE.locked,
|
||||
)
|
||||
self.add_row(new_state, state_exists=False)
|
||||
|
||||
self.STATE.doc_count = self.file_list_widget.count()
|
||||
self.doc_count_changed.emit(self.STATE.doc_count)
|
||||
|
||||
|
||||
class _MainWindow(QMainWindow):
|
||||
"""main window for internal test only, not the this module's component"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.setWindowTitle("Prototyp: Dokumente & Anhänge")
|
||||
self.resize(650, 500)
|
||||
|
||||
central_widget = QWidget()
|
||||
layout = QVBoxLayout(central_widget)
|
||||
|
||||
# simulate custom widget in form
|
||||
self.header_label = QLabel("<b>📎 Dokumente & Anhänge (0)</b>")
|
||||
self.header_label.setStyleSheet("font-size: 14px; color: #333333;")
|
||||
layout.addWidget(self.header_label)
|
||||
|
||||
# custom widget
|
||||
self.SESSION = Session(user_id=42, user_name="TEST-USER")
|
||||
state = Page_InitRec_DocumentSection_State(session=self.SESSION, locked=False)
|
||||
self.doc_section = DocumentSectionWidget(state)
|
||||
self.doc_section.doc_count_changed.connect(self.update_header)
|
||||
layout.addWidget(self.doc_section)
|
||||
|
||||
# --- TEST BUTTONS ---
|
||||
# test button to trigger data gathering (simulation for saving procedure and
|
||||
# backend interaction)
|
||||
layout_save_load_btn = QHBoxLayout()
|
||||
layout_save_load_btn.setContentsMargins(0, 0, 0, 0)
|
||||
self.btn_save = QPushButton("Erfassung speichern (Daten auslesen)")
|
||||
self.btn_save.setStyleSheet(
|
||||
"background-color: #0078D4; color: white; padding: 8px; font-weight: bold;"
|
||||
)
|
||||
self.btn_save.clicked.connect(self.get_current_widget_state)
|
||||
self.btn_load = QPushButton("Zustand initialisieren (Daten laden)")
|
||||
self.btn_load.setStyleSheet(
|
||||
"background-color: #0078D4; color: white; padding: 8px; font-weight: bold;"
|
||||
)
|
||||
self.btn_load.clicked.connect(self.load_given_state)
|
||||
|
||||
layout_save_load_btn.addWidget(self.btn_save)
|
||||
layout_save_load_btn.addWidget(self.btn_load)
|
||||
layout.addLayout(layout_save_load_btn)
|
||||
|
||||
layout_lock_btn = QHBoxLayout()
|
||||
layout_lock_btn.setContentsMargins(0, 0, 0, 0)
|
||||
btn_lock = QPushButton("Form sperren")
|
||||
btn_lock.setStyleSheet(
|
||||
"background-color: #0078D4; color: white; padding: 8px; font-weight: bold;"
|
||||
)
|
||||
btn_lock.clicked.connect(lambda: self.locking_form(True))
|
||||
btn_unlock = QPushButton("Form entsperren")
|
||||
btn_unlock.setStyleSheet(
|
||||
"background-color: #0078D4; color: white; padding: 8px; font-weight: bold;"
|
||||
)
|
||||
btn_unlock.clicked.connect(lambda: self.locking_form(False))
|
||||
|
||||
layout_lock_btn.addWidget(btn_lock)
|
||||
layout_lock_btn.addWidget(btn_unlock)
|
||||
layout.addLayout(layout_lock_btn)
|
||||
|
||||
btn_pyd_val = QPushButton("Pydantic Validierung")
|
||||
btn_pyd_val.setStyleSheet(
|
||||
"background-color: #0078D4; color: white; padding: 8px; font-weight: bold;"
|
||||
)
|
||||
btn_pyd_val.clicked.connect(self.pydantic_validate_state)
|
||||
layout.addWidget(btn_pyd_val)
|
||||
|
||||
self.setCentralWidget(central_widget)
|
||||
|
||||
def update_header(
|
||||
self,
|
||||
count: int,
|
||||
) -> None:
|
||||
"""update counter in corresponding label, must be mapped to section heading in form"""
|
||||
self.header_label.setText(f"<b>📎 Dokumente & Anhänge ({count})</b>")
|
||||
|
||||
def locking_form(
|
||||
self,
|
||||
lock: bool,
|
||||
) -> None:
|
||||
if lock:
|
||||
self.doc_section.lock()
|
||||
else:
|
||||
self.doc_section.unlock()
|
||||
|
||||
def get_current_widget_state(self):
|
||||
_ = self.doc_section.get_state()
|
||||
|
||||
def pydantic_validate_state(self):
|
||||
# data = self.doc_section.get_all_documents_data()
|
||||
state = self.doc_section.get_state()
|
||||
docs: list[Grunderfassung_Dokument] = []
|
||||
for doc in state.docs:
|
||||
geloescht_dt: datetime.datetime | None = None
|
||||
if doc.geloescht:
|
||||
geloescht_dt = datetime.datetime.now(datetime.UTC)
|
||||
|
||||
dump_data = dc.asdict(doc)
|
||||
dump_data["geloescht"] = geloescht_dt
|
||||
|
||||
validated = Grunderfassung_Dokument(**dump_data)
|
||||
docs.append(validated)
|
||||
|
||||
validated_section = [m.model_dump() for m in docs]
|
||||
|
||||
logger.debug(
|
||||
("[Module -- DocumentSelection] Pydantic validation. Model:\n%s"),
|
||||
pformat(validated_section),
|
||||
)
|
||||
|
||||
def load_given_state(self) -> None:
|
||||
new_state = Page_InitRec_DocumentSection_State(
|
||||
session=self.SESSION,
|
||||
docs=[
|
||||
Page_InitRec_DocumentSection_Doc_State(
|
||||
child_modules=[],
|
||||
session=Session(user_id=42, user_name="TEST-USER"),
|
||||
id=None,
|
||||
datum_hinzugefuegt=datetime.datetime(
|
||||
2026, 7, 31, 6, 38, 7, 120949, tzinfo=datetime.timezone.utc
|
||||
),
|
||||
speicher_pfad=Path("A:/Organisation/Zeiterfassung/Gesamt.xlsx"),
|
||||
dateiname_original="Gesamt.xlsx",
|
||||
dateigroesse_bytes=182564,
|
||||
mime_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
anmerkung="",
|
||||
locked=False,
|
||||
geloescht=True,
|
||||
),
|
||||
Page_InitRec_DocumentSection_Doc_State(
|
||||
child_modules=[],
|
||||
session=Session(user_id=42, user_name="TEST-USER"),
|
||||
id=None,
|
||||
datum_hinzugefuegt=datetime.datetime(
|
||||
2026, 7, 31, 6, 39, 46, 903040, tzinfo=datetime.timezone.utc
|
||||
),
|
||||
speicher_pfad=Path(
|
||||
"B:/projects/WCE-NAFKA/01 Anforderungen/Detaillierte-Übersicht-Inhalte-Unterkategorien.xlsx"
|
||||
),
|
||||
dateiname_original="Detaillierte-Übersicht-Inhalte-Unterkategorien.xlsx",
|
||||
dateigroesse_bytes=121528,
|
||||
mime_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
anmerkung="",
|
||||
locked=False,
|
||||
geloescht=False,
|
||||
),
|
||||
Page_InitRec_DocumentSection_Doc_State(
|
||||
child_modules=[],
|
||||
session=Session(user_id=42, user_name="TEST-USER"),
|
||||
id=None,
|
||||
datum_hinzugefuegt=datetime.datetime(
|
||||
2026, 7, 31, 6, 39, 46, 909045, tzinfo=datetime.timezone.utc
|
||||
),
|
||||
speicher_pfad=Path(
|
||||
"B:/projects/WCE-NAFKA/01 Anforderungen/Mockup-CRM.pptx"
|
||||
),
|
||||
dateiname_original="Mockup-CRM.pptx",
|
||||
dateigroesse_bytes=613242,
|
||||
mime_type="application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
anmerkung="",
|
||||
locked=False,
|
||||
geloescht=False,
|
||||
),
|
||||
],
|
||||
locked=False,
|
||||
geloescht=False,
|
||||
)
|
||||
|
||||
self.doc_section.load_state(new_state)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
app = QApplication(sys.argv)
|
||||
window = _MainWindow()
|
||||
window.show()
|
||||
sys.exit(app.exec())
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# enable static type checking for the protocol
|
||||
_m1: type[Module] = DocumentSectionWidget
|
||||
_m2: type[Module] = FileItemWidget
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+24
-6
@@ -30,13 +30,31 @@ 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.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.WARNING)
|
||||
|
||||
logger_prototyping = logger_gui.getChild("prototyping")
|
||||
logger_prototyping.setLevel(logging.DEBUG)
|
||||
|
||||
# ** Backend
|
||||
logger_back = logger_base.getChild("backend")
|
||||
logger_back.setLevel(logging.DEBUG)
|
||||
logger_backend = logger_base.getChild("backend")
|
||||
logger_backend.setLevel(logging.WARNING)
|
||||
|
||||
logger_initrec_docs = logger_base.getChild("initrec_docs")
|
||||
logger_initrec_docs.setLevel(logging.DEBUG)
|
||||
|
||||
+269
-1
@@ -1,9 +1,277 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, TypeAlias
|
||||
import copy
|
||||
import dataclasses as dc
|
||||
import datetime
|
||||
import enum
|
||||
from collections.abc import Generator
|
||||
from typing import TYPE_CHECKING, Final, TypeAlias, TypedDict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import polars as pl
|
||||
|
||||
|
||||
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()
|
||||
|
||||
|
||||
class ConsultingType(enum.StrEnum):
|
||||
PAUSCHAL = enum.auto()
|
||||
INDIVIDUAL = enum.auto()
|
||||
|
||||
|
||||
@dc.dataclass(slots=True, kw_only=True)
|
||||
class EntityIds:
|
||||
un_id: ExtMaId | None = None
|
||||
an_id: ExtAnId | None = None
|
||||
pers_id: RecId | None = None
|
||||
link_id: IntLinkId | None = None
|
||||
_field_names: frozenset[str] = dc.field(init=False, repr=False, compare=False)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
field_names = set(f.name for f in dc.fields(EntityIds))
|
||||
field_names.remove("_field_names")
|
||||
self._field_names = frozenset(field_names)
|
||||
|
||||
def set_by_name(
|
||||
self,
|
||||
attr_name: str,
|
||||
value: int | None,
|
||||
) -> None:
|
||||
if attr_name not in self._field_names:
|
||||
raise KeyError(
|
||||
f"Attribute {attr_name} not member of this class. Valid "
|
||||
f"options: {self._field_names}"
|
||||
)
|
||||
|
||||
setattr(self, attr_name, value)
|
||||
|
||||
def update(
|
||||
self,
|
||||
ids: 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:
|
||||
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
|
||||
# ** externals
|
||||
class CompanyInfo(TypedDict):
|
||||
ma_id: str
|
||||
wce_id: str
|
||||
ma_unternehmensname: str
|
||||
ma_branche: str
|
||||
ma_strasse: str
|
||||
ma_hausnummer: str
|
||||
ma_plz: str
|
||||
ma_ort: str
|
||||
ma_plz_postfach: str
|
||||
ma_postfach: str
|
||||
ma_website: str
|
||||
ma_mail: str
|
||||
ma_telefonnummer: str
|
||||
ma_faxnummer: str
|
||||
ma_ersteintrag_datum: str
|
||||
ma_aktualisierung_datum: str
|
||||
ma_aktualisierung_nutzer: str
|
||||
ma_sollprozess: str
|
||||
ma_auslaendische_mitarbeiter: str
|
||||
ma_quelle_information: str
|
||||
ma_bemerkung: str
|
||||
ma_kontakt: str
|
||||
ma_schlagworte: str
|
||||
ma_archiviert: str
|
||||
|
||||
|
||||
class ContactPersonInfo(TypedDict):
|
||||
an_id: str
|
||||
ma_id: str
|
||||
wce_id: str
|
||||
st_id: str
|
||||
an_sachgebiet: str
|
||||
an_anrede: str
|
||||
an_titel: str
|
||||
an_nachname: str
|
||||
an_vorname: str
|
||||
an_position: str
|
||||
an_mail: str
|
||||
an_festnetz: str
|
||||
an_mobil: str
|
||||
an_faxnummer: str
|
||||
an_hauptansprechpartner: str
|
||||
an_anrede_anschrift: str
|
||||
an_bemerkung: str
|
||||
an_aktualisierung_datum: str
|
||||
an_aktualisierung_nutzer: str
|
||||
an_letztes_kontaktdatum: str
|
||||
an_ersteintrag_datum: str
|
||||
an_archiviert: str
|
||||
|
||||
|
||||
# ** internals
|
||||
@dc.dataclass(slots=True)
|
||||
class MainPageEntry:
|
||||
ids: EntityIds
|
||||
display_name: str
|
||||
dt_updated: datetime.datetime
|
||||
recording_type: RecordingType
|
||||
|
||||
|
||||
@dc.dataclass(slots=True)
|
||||
class CompanyProfileConsultationEntry:
|
||||
title: str
|
||||
cons_id: ConsId
|
||||
date_updated: datetime.datetime
|
||||
cons_type: ConsultingType
|
||||
|
||||
|
||||
@dc.dataclass(slots=True)
|
||||
class CompanyProfileConsultations:
|
||||
pauschal: list[CompanyProfileConsultationEntry]
|
||||
individual: list[CompanyProfileConsultationEntry]
|
||||
|
||||
|
||||
@dc.dataclass(slots=True)
|
||||
class LinkingConsultationsEntryCompany:
|
||||
un_id: RecId
|
||||
display_name: str
|
||||
|
||||
|
||||
@dc.dataclass(slots=True)
|
||||
class LinkingConsultationsEntryPerson:
|
||||
pers_id: RecId
|
||||
un_ids: set[RecId]
|
||||
display_name: str
|
||||
|
||||
Reference in New Issue
Block a user