Files
kbcrm/App_Server/root/crm/model/checkdb.py
T
gg 461953a7e9 Datumsspalten auf Date-Typ umgestellt und Normalisierungsmigration
Modelle: startdatum (Projekt), datum und wiedervorlage_datum (CallLog)
von String(20) auf Date umgestellt. columns2dict serialisiert diese nun
über isoformat() als yyyy-MM-dd. Die String-Normalisierung in
columns2dict wurde entfernt, da sie nicht mehr benötigt wird.

Migration V01.08: Normalisiert bestehende String-Datumswerte (z.B.
'2026-08-3') auf ISO-Format yyyy-MM-dd mittels printf/substr, da
SQLite's date() einstellige Komponenten nicht erkennt. Leere Strings
werden zu NULL. V01.07 ist Platzhalter für die fehlgeschlagene V01.06.

Client: toDateInputValue normalisiert ebenfalls String-Datumswerte
als Fallback für Route-Params (Neuanlage).

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-09-04 16:57:17 +02:00

223 lines
8.1 KiB
Python

"""
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 <vibe@mistral.ai>
: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')