feat(calllog): kontaktadresse_id als Pflicht-FK + is_erledigt()-Methoden

- Migration V01_13: Spalte kontaktadresse_id auf call_logs mit Backfill
  über die erste Kontaktadresse des Projekts; Commit vor dem
  Systeminfo-Update (Muster wie V01_08) gegen 'database is locked'.
- CallLog gehört zu genau einer Kontaktadresse (FK, Pflichtfeld);
  create/update validieren Existenz und Projekt-Zugehörigkeit.
- is_erledigt() auf Kontaktadresse (mind. ein erledigend-Tag reicht)
  und Projekt (False ohne Kontakte, sonst alle erledigt); beide in
  to_dict() serialisiert.
- Client: kontaktadresse_id in Notizmaske/Submit (required), Interfaces
  um is_erledigt erweitert.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
gg committed 2026-09-22 20:12:02 +02:00
1 parent 24b492c5c1
commit 977b64292c
9 files changed
+174 -18

No files matched your search

+57 -8
View File
@@ -148,15 +148,33 @@ class Projekt(dbapp.Model):
startdatum = Column(Date, nullable=True)
kunde = relationship('Kunde')
# Kontaktadressen dieses Projekts (Akquise-Ziele). Die Rückrichtung
# ist Kontaktadresse.projekt (back_populates).
kontaktadressen = relationship('Kontaktadresse', back_populates='projekt')
def is_erledigt(self):
"""
Prüft dynamisch, ob das Projekt als erledigt gilt.
True, wenn das Projekt Kontaktadressen hat und alle davon erledigt
sind. Ein Projekt ohne Kontaktadressen ist offen (False), da
Kontaktadressen erst nach dem Anlegen des Projekts erfasst werden
können — ein neues Projekt ist somit zunächst offen.
"""
if not self.kontaktadressen:
return False
return all(k.is_erledigt() for k in self.kontaktadressen)
def to_dict(self):
"""
Liefert die Projektdaten als Dictionary. Die Tabellenspalten werden
über columns2dict serialisiert; kunden_name wird zusätzlich aus der
Kunde-Relationship eingefügt.
Kunde-Relationship eingefügt. is_erledigt wird dynamisch aus den
zugeordneten Kontaktadressen berechnet.
"""
result = columns2dict(self)
result['kunden_name'] = self.kunde.name if self.kunde else None
result['is_erledigt'] = self.is_erledigt()
return result
@@ -220,34 +238,64 @@ class Kontaktadresse(dbapp.Model):
land = Column(String(80), nullable=True)
notiz = Column(String(2000), nullable=True)
projekt = relationship('Projekt')
projekt = relationship('Projekt', back_populates='kontaktadressen')
# Gesprächsnotizen, die zu diesem Kontakt erfasst wurden. Die
# Rückrichtung ist CallLog.kontaktadresse (back_populates); die
# Join-Bedingung ergibt sich aus dem Fremdschlüssel
# call_logs.kontaktadresse_id -> kontaktadressen.id.
call_logs = relationship('CallLog', back_populates='kontaktadresse')
def is_erledigt(self):
"""
Prüft dynamisch, ob der Kontakt als erledigt gilt.
True, sobald mindestens eine Gesprächsnotiz des Kontakts einen Tag
mit is_erledigend=True hat. Spätere Notizen ohne erledigend-Tag
setzen den Status nicht zurück. False bei keinen Notizen oder wenn
keine Notiz einen erledigend-Tag hat.
"""
return any(
clt.tag.is_erledigend
for notiz in self.call_logs
for clt in notiz.tags_rel
)
def to_dict(self):
"""
Liefert die Kontaktdaten als Dictionary. Die Tabellenspalten werden
über columns2dict serialisiert; projekt_name wird zusätzlich aus der
Projekt-Relationship eingefügt.
Projekt-Relationship eingefügt. is_erledigt wird dynamisch aus den
Gesprächsnotizen des Kontakts berechnet.
"""
result = columns2dict(self)
result['projekt_name'] = self.projekt.projektname if self.projekt else None
result['is_erledigt'] = self.is_erledigt()
return result
class CallLog(dbapp.Model):
"""
Gesprächsnotiz für ein Projekt. Jede Gesprächsnotiz ist genau einem
Projekt zugeordnet (projekt_id). Der Projektname (projekt_name) und
der Kundenname (kunden_name) sind keine eigenen Spalten, sondern
werden über die Projekt-Relationship abgeleitet und nur in to_dict()
für die JSON-Serialisierung ergänzt.
Gesprächsnotiz für einen Kontakt. Jede Gesprächsnotiz ist genau einem
Projekt (projekt_id) und genau einer Kontaktadresse (kontaktadresse_id)
zugeordnet. Der Projektname (projekt_name) und der Kundenname
(kunden_name) sind keine eigenen Spalten, sondern werden über die
Projekt-Relationship abgeleitet und nur in to_dict() für die
JSON-Serialisierung ergänzt.
"""
__tablename__ = 'call_logs'
__bind_key__ = BINDKEY
__table_args__ = (
Index('ix_call_logs_projekt_id', 'projekt_id'),
Index('ix_call_logs_kontaktadresse_id', 'kontaktadresse_id'),
)
id = Column(Integer, primary_key=True)
projekt_id = Column(Integer, ForeignKey('projekte.id'), nullable=False)
# Jede Gesprächsnotiz gehört zu genau einer Kontaktadresse
# (Pflichtfeld). nullable=False im Modell; in bestehenden Datenbanken
# ist die physische Spalte nullable (SQLite kann kein nachträgliches
# NOT NULL) — die Pflicht wird über die Server-Validierung in
# crm.model.call_log durchgesetzt.
kontaktadresse_id = Column(Integer, ForeignKey('kontaktadressen.id'), nullable=False)
datum = Column(Date, nullable=True)
uhrzeit_beginn = Column(String(10), nullable=True)
dauer = Column(String(10), nullable=True)
@@ -259,6 +307,7 @@ class CallLog(dbapp.Model):
erledigt = Column(Boolean, nullable=False, default=False)
projekt = relationship('Projekt')
kontaktadresse = relationship('Kontaktadresse', back_populates='call_logs')
# n:m-Beziehung zu Tags über die Verknüpfungstabelle call_logs_tags.
# cascade='all, delete-orphan' stellt sicher, dass die Zuordnungen
# automatisch gelöscht werden, wenn die Gesprächsnotiz gelöscht wird.