""" DB-Versionierung für die CRM-Datenbank (kbp_crm.db). Analog zu auth/model/checkdb.py: Beim Start wird checkupdates() aufgerufen und führt alle _db_Vxx_xx-Funktionen aus, deren Versionsnummer größer ist als die in der Systeminfo-Tabelle vermerkte. Neue Migrationen werden als weitere _db_Vxx_xx-Funktionen ergänzt. :author: Mistral Vibe :date: 2026-09-02 :version: 1.0 """ from datetime import datetime from sqlalchemy.exc import OperationalError from db import dbapp from crm.model import CrmSysteminfo BINDKEY = "appdb" session = dbapp.session def _updatesysteminfo(versionnr: str): """Aktualisiert Versionsnummer und Zeitstempel in der Systeminfo-Tabelle.""" s: CrmSysteminfo = CrmSysteminfo.query.get(1) s.version = versionnr s.lastupdate = datetime.now() def _db_V01_00(): """ Ersteinrichtung: Alle CRM-Tabellen anlegen und Systeminfo-Datensatz schreiben. """ dbapp.create_all(bind_key=BINDKEY) systeminfo = CrmSysteminfo() systeminfo.id = 1 systeminfo.version = "01.00" systeminfo.lastupdate = datetime.now() systeminfo.initialinstall = datetime.now() session.add(systeminfo) session.commit() def _db_V01_01(): """ Legt die Tabelle 'kontakte' an. Bei bestehenden Datenbanken, die mit Version 01.00 initialisiert wurden, fehlt diese Tabelle, da das Kontakt-Modell erst nachträglich ergänzt wurde. create_all erzeugt nur fehlende Tabellen, bestehende bleiben unangetastet. """ dbapp.create_all(bind_key=BINDKEY) _updatesysteminfo('01.01') session.commit() def _db_V01_02(): """ Legt die Tabelle 'projekte' an. Bei bestehenden Datenbanken, die mit Version 01.01 initialisiert wurden, fehlt diese Tabelle, da das Projekt-Modell erst nachträglich ergänzt wurde. create_all erzeugt nur fehlende Tabellen, bestehende bleiben unangetastet. """ dbapp.create_all(bind_key=BINDKEY) _updatesysteminfo('01.02') session.commit() def _db_V01_03(): """ Legt die Tabellen 'kontaktadressen' und 'call_logs' an. Bei bestehenden Datenbanken, die mit Version 01.02 initialisiert wurden, fehlen diese Tabellen, da die Modelle erst nachträglich ergänzt wurden. """ dbapp.create_all(bind_key=BINDKEY) _updatesysteminfo('01.03') session.commit() def _db_V01_04(): """ Fügt die Spalte 'position' zur Tabelle 'kontaktadressen' hinzu. Bei bestehenden Datenbanken, die mit Version 01.03 initialisiert wurden, fehlt diese Spalte, da das Feld erst nachträglich ergänzt wurde. """ from sqlalchemy import text session.execute(text("ALTER TABLE kontaktadressen ADD COLUMN position VARCHAR(200)")) _updatesysteminfo('01.04') session.commit() def _db_V01_05(): """ Legt Indizes auf den Fremdschlüsselspalten an, die für die serverseitige Filterung der Listen-Endpunkte genutzt werden. CREATE INDEX IF NOT EXISTS ist idempotent und überspringet bereits vorhandene Indizes. """ from sqlalchemy import text session.execute(text("CREATE INDEX IF NOT EXISTS ix_kontakte_kunden_id ON kontakte (kunden_id)")) session.execute(text("CREATE INDEX IF NOT EXISTS ix_projekte_kunden_id ON projekte (kunden_id)")) session.execute(text("CREATE INDEX IF NOT EXISTS ix_kontaktadressen_projekt_id ON kontaktadressen (projekt_id)")) session.execute(text("CREATE INDEX IF NOT EXISTS ix_call_logs_projekt_id ON call_logs (projekt_id)")) _updatesysteminfo('01.05') session.commit() def _db_V01_07(): """ Platzhalter für die fehlgeschlagene Migration V01.06. Die eigentliche Normalisierung erfolgt in V01.08, da V01.06 bereits (fehlerhaft) durchlief und die Systeminfo auf 01.06 aktualisierte. """ pass def _db_V01_08(): """ Normalisiert bestehende Datumswerte in den Spalten 'datum' und 'wiedervorlage_datum' (call_logs) sowie 'startdatum' (projekte) auf das ISO-Format yyyy-MM-dd. Diese Spalten waren bisher String-Spalten und können Werte ohne führende Nullen (z.B. '2026-08-3') enthalten, die von SQLite's date()-Funktion nicht erkannt werden. Daher wird printf('%04d-%02d-%02d', ...) zur Normalisierung verwendet, das auch einstellige Monate/Tage korrekt auffüllt. Leere Strings werden zu NULL konvertiert, da der Date-Typ keine leeren Strings akzeptiert. Alle Updates werden vor dem Update der Systeminfo committet, um 'database is locked' zu vermeiden. """ from sqlalchemy import text # Normalisierungs-Ausdruck: splittet den Datum-String am '-' und # formatiert die Komponenten mit führenden Nullen. # _norm(col) = printf('%04d-%02d-%02d', # cast(substr(col, 1, instr(col, '-')-1) as integer), # cast(substr(col, instr(col, '-')+1, instr(substr(col, instr(col, '-')+1), '-')-1) as integer), # cast(substr(col, instr(col, '-')+instr(substr(col, instr(col, '-')+1), '-')+1) as integer) # ) # Da dieser Ausdruck sehr lang ist, wird er pro Spalte inline gesetzt. # call_logs.datum session.execute(text( "UPDATE call_logs SET datum = " "printf('%04d-%02d-%02d', " " cast(substr(datum, 1, instr(datum, '-')-1) as integer), " " cast(substr(datum, instr(datum, '-')+1, " " instr(substr(datum, instr(datum, '-')+1), '-')-1) as integer), " " cast(substr(datum, " " instr(datum, '-')+instr(substr(datum, instr(datum, '-')+1), '-')+1" " ) as integer)) " "WHERE datum IS NOT NULL AND datum != '' " "AND instr(datum, '-') > 0" )) session.execute(text( "UPDATE call_logs SET datum = NULL WHERE datum = ''" )) # call_logs.wiedervorlage_datum session.execute(text( "UPDATE call_logs SET wiedervorlage_datum = " "printf('%04d-%02d-%02d', " " cast(substr(wiedervorlage_datum, 1, instr(wiedervorlage_datum, '-')-1) as integer), " " cast(substr(wiedervorlage_datum, instr(wiedervorlage_datum, '-')+1, " " instr(substr(wiedervorlage_datum, instr(wiedervorlage_datum, '-')+1), '-')-1) as integer), " " cast(substr(wiedervorlage_datum, " " instr(wiedervorlage_datum, '-')+instr(substr(wiedervorlage_datum, instr(wiedervorlage_datum, '-')+1), '-')+1" " ) as integer)) " "WHERE wiedervorlage_datum IS NOT NULL AND wiedervorlage_datum != '' " "AND instr(wiedervorlage_datum, '-') > 0" )) session.execute(text( "UPDATE call_logs SET wiedervorlage_datum = NULL WHERE wiedervorlage_datum = ''" )) # projekte.startdatum session.execute(text( "UPDATE projekte SET startdatum = " "printf('%04d-%02d-%02d', " " cast(substr(startdatum, 1, instr(startdatum, '-')-1) as integer), " " cast(substr(startdatum, instr(startdatum, '-')+1, " " instr(substr(startdatum, instr(startdatum, '-')+1), '-')-1) as integer), " " cast(substr(startdatum, " " instr(startdatum, '-')+instr(substr(startdatum, instr(startdatum, '-')+1), '-')+1" " ) as integer)) " "WHERE startdatum IS NOT NULL AND startdatum != '' " "AND instr(startdatum, '-') > 0" )) session.execute(text( "UPDATE projekte SET startdatum = NULL WHERE startdatum = ''" )) # Daten-Updates committen, bevor die Systeminfo aktualisiert wird. session.commit() # Systeminfo erst nach dem Commit der Daten-Updates aktualisieren. _updatesysteminfo('01.08') session.commit() def checkupdates(): """ Ermittelt die aktuelle DB-Version aus der Systeminfo-Tabelle und führt alle _db_Vxx_xx-Funktionen aus, deren Versionsnummer größer ist. Ist die Datenbank noch leer (Tabelle fehlt), wird die Ersteinrichtung ausgeführt. """ curr_dbversion = '_db_V01_00' try: systeminfo: CrmSysteminfo = CrmSysteminfo.query.get(1) curr_dbversion = "_db_V" + systeminfo.version.replace(".", "_") except (OperationalError, AttributeError): _db_V01_00() versionen = sorted(filter(lambda f: f.startswith("_db_V"), globals())) for v in versionen: if v > curr_dbversion: globals()[v]() print('########## crm Database updated')