(Dieser öffentliche ENTWURF wurde zuletzt am 4. März geändert, um den aktuellen Stand widerzuspiegeln. Sobald dieser Hinweis verschwindet, gilt die vorliegende Version als final und zitierbar.)

Eines meiner Software-Projekte ist TreasureDB, ein elektronisches Vereinskassenbuch auf Basis des Datenbanksystems SQLite. Es ist Open-Source, steht unter der GPL. Zwar ist die Software auf kleine, eingetragene Vereine in Deutschland zugeschnitten. Dennoch ist die verwendete Sprache des Projektes Englisch, damit das Programm prinzipiell international verwendet werden kann. Wer mag, kann mir mit der Übersetzung helfen, auch auf technischer Ebene.

TreasureDB kann auf drei Arten bedient werden:

  1. Direkt über die sqlite3-Konsole oder einer beliebigen anderen Benutzerschnittstelle für SQLite-Datenbanken. Die Datenbank, die in einer einzigen Datei vorliegt, gewährleistet dabei mit Schlüsselbeziehungen und Triggern (Routinen, die unter definierten Bedingungen automatisch angestoßen werden) selbst die Konsistenz. So kann, wer SQL beherrscht, die Datenbank direkt verwalten, ohne auf andere Tools angewiesen zu sein, und braucht nicht befürchten, dass er/sie die Daten beschädigen könnte.
  2. Auf der Datenbank baut das rein textbasierte Programm trsr auf.  Es wird mit dem Namen einer Funktion gestartet, außerdem kann das zu bearbeitende Konto angegeben werden. Es dient als Schnittstelle zum Nutzer, der kein SQL beherrscht oder verwenden möchte, aber generell die Text-Konsole etwas Graphischem vorzieht.
  3. Eine solche Funktion ist server. Damit wird ein HTTP-Server gestartet. So kann die Datenbank über einen beliebigen Webbrowser gepflegt werden.

Eine Ausbildung zum Buchhalter sollte der Kassenwart nicht benötigen,  um TreasureDB anzuwenden. Die habe ich schließlich auch nicht, von allgemeinem Wirtschaftsunterricht damals in der Oberstufe einmal abgesehen. Damit ist die Zielgruppe der Software, wie auch dieses Artikels, umrissen: Kleine Vereine mit relativer Computeraffinität, z.B. User groups wie die UUG Rhein-Neckar.

Diese kleine Serie besteht aus mehreren Teilen:

  1. 4 Grundbegriffe in ihrem Zusammenhang, Aufbau des Systems – Konten, Guthaben, Belastungen und interne Bewegungen.
  2. Ausgangslage – LibreOffice-Calc Datei vom vorigen Kassenwart (Beispiel), zusätzlich Kontoauszüge, die vollständig ab einem definierten Zeitpunkt elektronisch in tabellarischem Format vorliegen (z.B. CSV, TSV).
  3. Migration – Daten einspielen und Beziehungen zwischen Aus- und Eingängen explizieren, d.h. Kontobewegungen spezifizieren.
  4. Nichttriviale Szenarien – zum Beispiel, wenn ein Mitglied die Mitgliedsgebühr für zwei bezahlt.

Aufbau von TreasureDB

Grundbegriffe

Da ein Bild bekanntlich mehr als tausend Worte sagt:

a) Konto

Ein Konto ist in diesem System lediglich eine Art Gruppenzuordnung von einzelnen Guthaben und von Belastungen. Letztere referenzieren Konten sowohl als belastete als auch als – indirekt über Zielguthaben (s.u.) – begünstigte Konten. Ein Konto kann einen bestimmten Typ haben, zum Beispiel den Typ „Mitgliedskonto“. Es ist möglich, mehrere Konten des gleichen Typs aufeinmal zu belasten. Vereinskonten haben meist keinen Typ, diese werden ausschließlich über ihre ID, ihre Bezeichnung unterschieden, die übrigens bewusst keine Zahl ist, sondern ein frei wählbarer Name. Die Mitgliedsnummer wird bei Mitgliedskonten in einem einfachen Feld eingetragen, das für Nummern in externen Fremdtabellen gedacht ist.

Außerdem kann zu einem Konto eine IBAN gespeichert werden. Ist diese angegeben, muss sie im Verwendungszweck von Auszahlungen stets enthalten sein. Ist keine IBAN enthalten, sind Auszahlungen von diesem Konto nicht möglich. Das Feld kann auch explizit leer definiert werden, muss es sogar für das Hauptkonto des Vereins. Dann werden Auszahlungen nicht geprüft.

b) Guthaben

Mit Guthaben sind zum einen Einzahlungen gemeint, z.B. Mitgliedsbeiträge. Zum anderen gibt es Guthaben, die anfangs gar keine sind, denn sie haben bei Buchung den Betrag 0. Diese Guthaben werden mit Belastungen verknüpft und bilden das Ziel von internen Bewegungen.

Jedes Guthaben hat neben dem Betrag und dem Buchungsdatum auch einen angegebenen Zweck. Bei Einzahlungen ist dies der Verwendungszweck, wie er aus dem Kontoauszug hervorgeht. Bei Zielguthaben ist der Kassierer frei in der Bestimmung des Zwecks. Ich empfehle, eine Gruppenbezeichnung für Belastungen anzugeben, etwa „Mitgliedsbeiträge 2016“.

c) Belastung

Eine Belastung ist eine Forderung, die beglichen oder noch offen sein kann. Es handelt sich um einen Datensatz, der folgende Daten enthält:

  • Identifizierungsangabe des Belegs, z.B. Rechnungsnummer, bei Mitgliedsbeiträgen eine Zeichenfolge nach dem Schema Jahr, Monat und Mitgliedsname;
  • Buchungsdatum;
  • Name des belasteten Kontos;
  • Nummer des Zielguthabens im begünstigten Konto;
  • Grund der Belastung;
  • Betrag.

d) Bewegung

Bewegungen sind das A&O im elektronischen Kassenbuch. Es genügt nicht, einfach Ein- und Ausgänge aufzulisten. Auch ein Zusammenhang zwischen ihnen ist herzustellen und explizit zu speichern, will der Kassierer diesen Zusammenhang nicht bei Bedarf immer neu rekapitulieren müssen.

Eine Bewegung verknüpft also ein Guthaben mit einer Belastung. Sie speichert auch den Zeitpunkt und den Betrag, der im Vergleich zwischen Guthaben und Belastung der jeweils geringere ist. Dieser Betrag wird tatsächlich transferiert, um den Betrag wird die Belastung getilgt, bzw. das Quellguthaben verringert und das Zielguthaben wiederum erhöht.

Die Tilgung von Belastungen und die Verwendung von Guthaben geschieht transparent. Das heißt, die originalen Beträge bleiben zu Referenzzwecken erhalten. Der Abzug davon wird getrennt gespeichert. Wo nötig, wird einfach die Differenz gebildet.

Architektur der Datenbank

Beachte: Selbstgewählte Namen werden im Folgenden in Fettdruck geschrieben.

Tabellen

Obige Grundbegriffe korrelieren mit den Tabellen in der Datenbank, die auf Englisch bezeichnet sind.

  • Konten werden in der Tabelle Account gespeichert.
  • Guthaben in Credit
  • Belastungen in Debit
  • Bewegungen in Transfer

Views

Die Datenbank enthält neben den Tabellen zudem eine Reihe von Views. Diese sind eine Art virtuelle Tabellen. Sie enthalten selbst keine Daten, sondern fragen on demand ihrerseits die Tabellen ab und bereiten die zurückgelieferten Daten zweckorientiert auf. Sie entlasten so den Kassierer beim Überblicken und Nachvollziehen vergangener Ein- und Ausgänge sowie ihrer Zusammenhänge. Das Einfügen, Ändern und Löschen von Datensätzen ist Views per Design nicht möglich.

  • ReconstructedBankStatement (dt. rekonstruierter Kontoauszug): Dieser View ist dazu gedacht, vom Kassenprüfer mit den Papierauszügen der Bank verglichen zu werden. Er enthält alle Einzahlungen, also alle Guthaben, die nicht als Ziel von Belastungen spezifiziert wurden, sowie alle Auszahlungen, also Belastungen ohne Zielguthaben.
  • CurrentArrears (dt. aktuelle Rückstände): Belastungen, die noch nicht vollständig beglichen wurden, also noch nicht oder nicht von genug Bewegungen referenziert werden.
  • AvailableCredits (dt. verfügbare Guthaben): Guthaben, die noch nicht (vollständig) verwendet, also von (genug) Bewegungen referenziert werden.
  • Balance (dt. Abgleich): Zu jedem Account werden bestimmte Daten aggregiert. Da dies der wichtigste View ist, gehe ich detailliert auf die einzelnen Felder ein.
    • Am wichtigsten ist die Summe der offene Belastungen (Spalte arrears).
    • Nicht viel weniger wichtig sind die Guthaben, die noch nicht verwendet wurden (available). Sind beide Spalten auf derselben Zeile >0, bedeutet das schlicht, dass noch Bewegungen zu erstellen sind. Kann der Kassierer Guthaben und Belastungen nicht zusammenbringen, etwa aufgrund missverständlicher Verwendungszwecke, muss er diese Fälle mit dem Mitglied klären.
    • Außerdem enthält die Balance das Datum des Kontoausgleichs, also der letzten Belastung (even_until), die getilgt wurde und zwar in Folge, d.h. zeitlich vor dieser Belastung gibt es ebenfalls keine ungetilgten oder unvollständig getilgten Belastungen;
    • die Summe der Beträge, die aufgrund von Dienstleistungen/Waren auf dem Konto gelandet sind und ausgezahlt oder weiter verwendet wurden, bzw. noch auszuzahlen oder weiter zu verwenden sind (earned);
    • und nicht zuletzt die Summe der Beträge von offenen Belastungen, deren Zielguthaben dem Konto zugeordnet ist (promised).
  • History (dt. Verlauf): enthält alle internen Bewegungen doppelt, also jeweils in Hin- und in Gegenrichtung. So kann dieser View sowohl nach dem belasteten als auch nach dem begünstigten Konto gefiltert werden. Mit der View ReconstructedBankStatement zusammengenommen haben wir also ein komplettes Bild aller erfolgten Kassenvorgänge.
  • Report (dt. Bericht): Dieser View hilft dem Kassierer, Unklarheiten gemeinsam mit Mitgliedern zu beseitigen. Gefiltert nach einem Konto listet es alle Guthaben auf, die nach dem Datum des Kontoausgleichs eingezahlt wurden, alle Abzüge durch Bewegungen von diesen Guthaben, sowie alle offenen Forderungen. Summiert ergibt sich der Saldo, der sich in der Balance wiederfindet.

Trigger

Die folgenden Funktionen, Trigger genannt, löst die Datenbank automatisch zu bestimmten Ereignissen aus. Sie verhindern Fehleingaben und nehmen nötige Verarbeitungen vor.

  • balanceTransfer (dt. Bewegung abgleichen): Bei jedem Eintrag einer Bewegung vergleicht dieser Trigger Guthaben und Belastung, speichert den geringeren Betrag in der Bewegung und zieht ihn jeweils von Guthaben und Belastung ab.
    Er gibt folgende Fehlermeldungen aus:
    It is not the debtor who is set to pay = Es wird versucht, die Bewegung mit einem Guthaben eines anderen Kontos als das belastete zu verknüpfen.
    Target of a debit cannot be an incoming payment = Zielguthaben kann keine Einzahlung sein.
    Credit spent = Guthaben ist vollständig verbraucht und kann daher keine weiteren Belastungen begleichen.
    Debt settled = Belastung ist bereits vollständig beglichen, weshalb es keinen Sinn hat, mehr Guthaben darauf zu verwenden.
    Oops, lost _temp record before increasing spent = Bug (d.h. sollte nie passieren. Hörst du Murphy lachen? Das bildest du dir bestimmt nur ein, ts …)
    Oops, lost _temp record before increasing value = Bug (dito)
  • revokeTransfer (dt. Bewegung widerrufen): Wird eine Bewegung wieder gelöscht, sind die Verarbeitungen entsprechend wieder rückgängig zu machen. Es werden keine Fehlermeldungen ausgegeben.
  • enforceImmutableTransfer (dt. mache Bewegungen unveränderlich): Verhindert willkürliche Änderungen von Bewegungen. Fehlermeldung: Transfer cannot be updated, but needs to be replaced to make triggers run = Bewegungen können nicht geändert werden. Sie müssen ersetzt werden, damit die nötigen Trigger angestoßen werden.
  • enforceZeroPaidAtStart (dt. initialisiere Belastungen mit paid=0): Bei neu eingetragenen Belastungen muss das Feld paid 0 oder gar nicht definiert sein. Fehlermeldung: Debt must be initially unpaid = Eine eingetragene Belastung muss unbeglichen sein.
  • enforceDebtImmutableOutsideTrigger (dt. schütze Belastungen vor Veränderungen außerhalb Triggern): Mögliche Fehlermeldung: paid is set and adjusted automatically according to added Transfer records  = das Feld paid muss von Triggern gesetzt werden. Eingaben wird nicht getraut.
  • rebalanceIncreasedCredit (dt. gleiche erhöhte Guthaben neu aus): Wird eine Bewegung eingetragen, so wird ja das mit der Belastung verknüpfte Zielguthaben erhöht. Sind mit dem Zielguthaben wiederum Bewegungen verknüpft, die Belastungen nicht vollständig begleichen, so werden diese Bewegungen erneut durchgeführt.
  • rebalanceReducedCredit (dt. gleiche verringerte Guthaben neu aus): Wird eine Bewegung gelöscht, muss das Zielguthaben der Belastung entsprechend reduziert werden. Der Trigger stellt außerdem fest, ob die Bewegungen, die dieses Guthaben als Quelle verknüpft haben, auch mit dem geringeren Betrag hätten stattfinden können. Wo dies nicht der Fall ist, werden die überstehenden Bewegungen ebenfalls widerrufen,
  • enforceFixedDebit (dt. schütze Belastungen vor Veränderungen des Betrags): Mögliche Fehlermeldung: Debt is involved in transfers to revoke at first  = Belastung ist schon vollständig oder teilweise beglichen. Der Betrag, das belastete Konto oder das Zielguthaben können daher nicht korrigiert werden.
  • enforceSpentImmutableOutsideTrigger (dt. schütze Guthaben vor Veränderung außerhalb Triggern): Mögliche Fehlermeldung:  spent is set and adjusted automatically according to added Transfer records = Das Feld spent darf nicht händisch verändert werden.
  • enforceZeroSpentAtStart (dt. initialisiere Guthaben mit spent=0): Bei neu eingetragenden Guthaben muss das Feld spent 0 oder undefiniert sein. Mögliche Fehlermeldung: credit must be initially unused  = Einzahlungen dürfen anfangs nicht verwendet worden sein.
  • enforceFixedCredit (dt. schütze Guthabenbetrag gegen Veränderung): Mögliche Fehlermeldung: Credit involved in transactions to revoke at first = Eingezahltes Guthaben ist schon vollständig oder teilweise beglichen. Der Betrag oder das begünstigte Konto können daher nicht geändert werden.
  • checkIBANatTransfer (dt. prüfe IBAN bei Auszahlungen): Ist zum Konto eine IBAN gespeichert – oder stattdessen eine leere Zeichenfolge, wodurch alle IBANs gültig sind –, kann von diesem Konto an die gespeicherte IBAN ausgezahlt werden. Mögliche Fehlermeldung: IBAN used does not match IBAN currently stored in account record = Hat sich der Kassierer bei einer Empfänger-IBAN vertippt? Sollte nicht passieren. Die Bank wird Überweisungen nicht rückgängig machen. Aber der Kassierer kann ja immer noch privat für den Schaden aufkommen, indem er die Bewegung rückgängig macht und die fehlerhafte Auszahlung auf sein Konto umbucht.

Probleme

TreasureDB hat noch einige Probleme, die teilweise nicht so einfach zu lösen sind.

Dazu gehört etwa, das A eine Belastung zugunsten B zwar anzahlen kann, aber unter Umständen B wiederum nicht eine Belastung von A, wobei A das erhaltene Guthaben umgekehrt für die gleiche Belastung verwendet, und so weiter. Auch wenn diese reziproken, oder zirkulären Anzahlungen realiter sehr selten vorkommen mögen, wenn der Kassierer überhaupt einen Sinn darin sieht, derartiges aus Absicht zu definieren: Nicht ausgeschlossen ist, dass er, da er mehr oder weniger versehentlich eben doch eine definiert hat, mit einer kryptischen Fehlermeldung konfrontiert wird: Schlüsseldublette in  __INTERNAL_TRIGGER_STACK.ID oder so. Wenn auch technisch richtig, müsste die Fehlermeldung eigentlich lauten: »Zirkuläre  Anzahlungen werden nicht unterstützt.« Bevor ich Trigger allerdings mit redundanten Checks überfrachte, nur weil mir die Standardfehlermeldungen nicht gefallen, gehe ich lieber erst mal in mich.