Daten visuell erfassen in 1:n- und 1:1- Beziehungen

15.06.2006 von Helma  Spona
In den allermeisten Access-Anwendungen wird eine 1:n- oder 1:1-Beziehung zwischen Tabellen oder Abfragen vorkommen. Dabei gibt es Anwendungen, bei denen es in diesem Fall nicht notwendig ist, den zugehörigen Datensatz in einem Unterformular oder separaten Formular mühsam zu erfassen. Ich zeige Ihnen, wie es wesentlich komfortabler geht – für Benutzer und Entwickler.

Das typische Beispiel für Anwendungen, bei denenm sich Datensätze, die über 1:1 oder 1:n-Beziehungen verknüpft sind, komfortabel erstellen lassen, ist eine Fakturierung. In der Regel wird es eine Tabelle mit Aufträgen geben sowie eine Tabelle mit Zahlungsein- und -ausgängen (Bild 1). Sollen nun Zahlungseingänge erfasst werden, muss dann entweder die Auftragsnummer für die Zahlung ausgewählt werden, um die Zahlung dem Auftrag zuzuordnen, oder es muss ein Formular mit Unterformular erstellt werden. Das ist jedoch überflüssiger Aufwand, wenn in der Regel ohnehin der Auftrag in einem Betrag bezahlt wird und nur ein Datensatz pro Auftrag zu erfassen ist.

Bild 1: Grundlegender Aufbau der Anwendung.
Bild 2: Aufbau des Formulars zur Anzeige der Aufträge.

Zudem gibt es dann das Problem, dass bei Eingabe des Betrags Tippfehler, etwa Zahlendreher, gemacht werden können, so dass der als gezahlt verbuchte Betrag nicht mit dem Auftragswert übereinstimmt. Viel einfacher ist es, wenn Sie dafür sorgen, dass durch Auswahl des Eintrags bezahlt oder abgeschlossen im Steuerelement für das Feld Status ein entsprechender Datensatz in die Tabelle tabZahlungen geschrieben wird.

Zahlungseingänge automatisch erfassen

Alles, was Sie dazu brauchen, ist ein Formular, in dem die Aufträge tabellarisch oder als Einzelformular angezeigt werden. Hier müssen Sie ein Kombinationslistenfeld für die Auswahl des Wertes für das Feld Status einfügen, in dem die Werte aus der Tabelle tabStatus angezeigt werden. Damit nun auch ein Zahlungseingang erfasst wird, wenn der Benutzer den Status des Auftrags ändert, müssen Sie eine Ereignisprozedur für das Change-Ereignis (Bei Änderung) des Kombinationslistenfeldes erstellen (Listing 1).

Private Sub Status_Change()
Dim strSQL As String
strSQL = ""
'Wenn Auftrag schon in tabZahlungen vorhanden ist, löschen
strSQL = ""
If Me.Status.Text = "bezahlt" Then
'Zahlungseingang in der Tabelle tabZahlungen erfassen
If Me.Auftragsart.Value = 2 Then 'Gutschrift
strSQL = "DELETE FROM tabZahlungen WHERE Auftrag=" & Me.ID.Value
Application.CurrentDb.Execute strSQL
strSQL = "INSERT INTO tabZahlungen (Betrag,Referenz,Auftrag) " & _
"VALUES(" & getWert(Me.Auftragswert.Value * (-1)) & ",""Auftrag " &
_
Me.ID.Value & """," & Me.ID.Value & ");"
ElseIf Me.Auftragsart.Value = 1 Then 'Rechnung
strSQL = "DELETE FROM tabZahlungen WHERE Auftrag=" & Me.ID.Value
Application.CurrentDb.Execute strSQL
strSQL = "INSERT INTO tabZahlungen (Betrag,Referenz,Auftrag) " & _
"VALUES(" & getWert(Me.Auftragswert.Value) & ",""Auftrag " & _
Me.ID.Value & """," & Me.ID.Value & ");"
End If
ElseIf Me.Status.Text = "offen" Then
'eventuell vorhandene Datensätze löschen
strSQL = "DELETE FROM tabZahlungen WHERE Auftrag=" & Me.ID.Value
End If
'SQL-Anweisung ausführen
If strSQL > "" Then
Application.CurrentDb.Execute strSQL
End If
End Sub

Sie müssen dazu im Prinzip nichts weiter machen, als eine entsprechende SQL-Anweisung zu formulieren und auszuführen. Zunächst sollten Sie eventuell vorhandene Datensätze löschen. Haben Sie nämlich versehentlich den Status bezahlt gewählt, müssen Sie das ja auch wieder rückgängig machen können, indem ein anderer Wert für das Feld Status ausgewählt wird.

Erzeugen der Buchung

Beim Erzeugen der Buchung müssen Sie den Auftragstyp berücksichtigen. Da Lieferscheine üblicherweise nicht bezahlt werden, sondern nur die daraus erzeugten Rechnungen und eventuell Gutschriften, dürfen Sie den neuen Datensatz nur dann erzeugen, wenn es sich um eine Rechnung oder Gutschrift handelt. Bei einer Gutschrift sind die zu buchenden Werte dann negativ (Zahlungsausgang), bei Rechnungen sind sie positiv. Vorher löschen Sie jeweils die vorhandenen Datensätze mit einer DELETE-Anweisung. Nur so ist es möglich, dass eine fehlerhafte Buchung rückgängig gemacht werden kann, indem sie durch eine neue ersetzt wird.

Entsprechend dem Auftragstyp formulieren Sie eine INSERT-Anweisung und führen diese aus. Damit die Beträge als Zahlen mit Dezimalpunkt und ohne Währungseinheit übergeben werden, müssen Sie sie jedoch entsprechend formatieren. Dafür sorgt hier der Aufruf der Funktion getWert (Listing 2).

Function getWert(curWert As Currency) As String
getWert = Replace(VBA.FormatNumber(curWert, 2, vbFalse, vbFalse, vbFalse), _
",", ".")
End Function

Mit diesen beiden Prozeduren ist das erste Problem erledigt. Wollen Sie dem Benutzer jedoch über ein weiteres Formular die Möglichkeit geben, auch Teilbeträge als Zahlungen zu erfassen, entsteht daraus ein neues Problem.

Ausblenden der Felder

Sie brauchen diese Felder natürlich nicht sichtbar anzeigen lassen, sondern können sie auch ausblenden. Wichtig ist nur, dass es die Felder mit den entsprechenden Werten gibt, da Sie dann im Code auf deren Werte zugreifen können. Allerdings ist sicherlich der noch zu zahlende Betrag für den Benutzer interessant. Die sichtbaren berechneten Textfelder sollten Sie allerdings gegen Eingaben schützen, indem Sie deren Eigenschaft gesperrt auf Ja setzen.

Werte anpassen

Die hier verwendeten Formeln setzen voraus, dass die Auftragstypen und die Werte für die Tabelle tabStatus wie in Tabelle 1 und Tabelle 2 definiert sind: Falls Sie andere Werte und Texte hinterlegt haben, müssen Sie die Formeln und auch den Code in Listing 1-6 anpassen.

Tabelle 1: Inhalt der Tabelle tabAuftragstypen.

Auftragstyp

ID

Rechnung

1

Gutschrift

2

Lieferschein

3

Tabelle 2: Inhalt der Tabelle tabStatus.

Status

ID

Bezahlt

1

Geliefert

2

Offen

3

Abgeschlossen

4

Teilzahlungen berücksichtigen

Wenn mit einem zweiten Formular Teilzahlungen erfasst werden können, könnte es natürlich sein, dass der Status des Auftrags in der Tabelle tabAuftraege nicht mehr mit den tatsächlichen Zahlungseingängen übereinstimmt. Um das beim Anzeigen der Daten zu korrigieren, müssen Sie zunächst feststellen, ob es für den Auftrag erfasste Zahlungen gibt und, wenn ja, ob deren Summe mit dem Auftragswert übereinstimmt.

Falls ja, müssen Sie den Status auf bezahlt setzen falls nicht, müssen Sie dafür sorgen, dass bei Auswahl von bezahlt aus dem Kombinationslistenfeld nicht standardmäßig der komplette Betrag, sondern nur der noch nicht bezahlte Restbetrag gebucht wird. Sie sollten dazu das Formular um drei berechnete Felder ergänzen:

Zur Berechnung der gewünschten Werte legen Sie für die drei Textfelder die folgenden Werte für die Eigenschaft Steuerelementinhalt fest:

Die Funktion DomAnzahl gibt die Anzahl von Datensätzen zurück, die in der Tabelle tabZahlungen für den Auftrag vorhanden sind. Den Auftrag definieren Sie durch den Kriteriumsausdruck, den Sie als dritten Parameter angeben. Entsprechend ermittelt die Funktion DomSumme die Summe des Feldes Betrag in der Tabelle tabZahlungen.

Das Feld txtRestbetrag

Etwas komplizierter sieht es bei der Berechnung des Feldes txtRestbetrag aus. Hier müssen Sie wieder die Auftragsart berücksichtigen. Bei einer Rechnung ziehen Sie einfach den Zahlbetrag vom Auftragswert ab. Bei einer Gutschrift geht das nicht, weil hier ein negativer Wert gebucht wurde, da es ja ein Zahlungsausgang ist. Daher müssen Sie hier den Zahlbetrag wieder mit -1 multiplizieren, bevor Sie ihn vom Auftragswert abziehen. Um das zu berücksichtigen, verwenden Sie hier zwei ineinander geschachtelte Wenn-Funktionen. In der ersten prüfen Sie, ob der Auftragstyp eine Rechnung (=1) ist. Falls ja, berechnen Sie die Differenz. Falls nicht, folgt im Else-Zweig der Funktion die zweite Wenn- Funktion. Sie prüft, ob der Auftragstyp eine Gutschrift ist. Ist das der Fall, wird die Differenz für die Gutschrift berechnet. Ist der Auftrag weder eine Rechnung noch eine Gutschrift, sorgt der Else-Zweig der inneren Wenn-Funktion dafür, dass beispielsweise bei Lieferscheinen eine 0 als Restbetrag ausgegeben wird.

Nun haben Sie die Grundlagen dafür geschaffen, die widersprüchlichen Angaben zu finden und gegebenenfalls zu korrigieren. Fehler, die zu korrigieren sind, liegen dann vor,

Bild 3: Aufbau des erweiterten Formulars.

Um die Korrekturen durchführen zu können, wenn der Benutzer den Datensatz zum aktiven Datensatz macht, erstellen Sie eine Ereignisprozedur für das Ereignis OnCurrent des Formulars (Beim Anzeigen). Dort prüfen Sie dann den aktuellen Status und vergleichen ihn mit dem Wert des Feldes txtRestbetrag, um gegebenenfalls Fehler zu finden (Listing 3).

Private Sub Form_Current()
'Prüfen, ob Daten korrekt sind
If (Me.txtRestbetrag.Value > 0) And ((Me.Status.Value = 1) Or (Me.Status.Value
= 4)) Then
If Me.Status.Value = 4 Then 'Ware geliefert aber nicht bezahlt
'Status auf "geliefert" setzen
Me.Status.Value = 2
ElseIf Me.Status.Value = 1 Then 'Ware nicht bezahlt und nicht geliefert
'Status auf "offen" setzen
Me.Status.Value = 3
End If
ElseIf ((Me.txtRestbetrag.Value = 0) And ((Me.Auftragsart.Value = 1) Or _
(Me.Auftragsart.Value = 2))) And ((Me.Status.Value = 2) Or (Me.Status.Value
= 3)) Then
If Me.Status.Value = 2 Then
'Wenn Auftrag bezahlt und geliefert Status auf "abgeschlossen" setzen
Me.Status.Value = 4
ElseIf Me.Status.Value = 3 Then
'Wenn Auftrag "offen" aber bezahlt, dann Status auf "bezahlt" setzen
Me.Status.Value = 1
End If
End If
End Sub

Wichtig ist dabei, dass Sie darauf achten, beim Restbetrag 0 nur dann eine Korrektur vorzunehmen, wenn es sich um eine Rechnung oder eine Gutschrift handelt, weil ja auch bei einem Lieferschein der Restbetrag 0 ist.

Ausführen der Ereignisprozedur

Ausgeführt wird die Ereignisprozedur, wenn der Benutzer den Datensatz aktiviert, indem er beispielsweise zwecks Eingabe den Cursor in einSteuerelement des Datensatzes setzt. Verlässt der Benutzer den Datensatz wieder, weil er einen anderen Datensatz aktiviert, wird die Korrektur automatisch gespeichert.

Allerdings müssen Sie nun noch dafür sorgen, dass Teilbuchungen nicht gelöscht werden, wenn der Benutzer den Status bezahlt setzt. Würden Sie weiterhin die Ereignisprozedur aus Listing 1 verwenden, hätten Sie damit zwei Probleme.

In beiden Fällen ist es sehr viel sinnvoller, nur den Restbetrag oder eine Gegenbuchung zu machen, weil sich so auch später noch die einzelnen Buchungen nachvollziehen lassen.

Bis auf die Tatsache, dass hier prinzipiell die gleichen SQL-INSERT-Anweisungen erzeugt werden, nur mit anderen Werten und Texten, ist die Prozedur nicht mehr wieder zu erkennen.

Zunächst einmal benötigen Sie den alten Wert des Feldes Status, da Sie daran erkennen können, ob überhaupt eine Änderung des Wertes stattgefunden ist. Zudem ist der alte Status wichtig. Stand er nämlich vorher auf geliefert und wird nun auf bezahlt gesetzt, sollte der Code dafür sorgen, dass in diesem Fall der Status auf abgeschlossen gesetzt wird. Sonst ist nicht mehr ersichtlich,dass die Lieferung auch erfolgt ist. Den alten Wert können Sie über die OldValue-Eigenschaft ermitteln und einfach am Anfang der Prozedur einer Variablen zuweisen.

Außerdem sollten Sie über die Bookmark-Eigenschaft des Formulars den Datensatz ermitteln, der der aktive Datensatz ist. Damit können Sie diesen Datensatz auch nach der Aktualisierung des Formulars wieder aktivieren. Dazu weisen Sie den Rückgabewert der Bookmark-Eigenschaft einer Variant-Variablen zu.

Prüfen, welcher Fall vorliegt

Nun müssen Sie prüfen, welcher Fall vorliegt. Es könnte sein, dass es noch gar keinen Zahlungseingang für den Auftrag gibt. In diesem Fall hat das Feld txtRestbetrag den Wert Null. Sie sollten daher auf jeden Fall zuerst den Wert des Feldes txtAnzahl prüfen. Hat es den Wert 0, und wurde der Status auf 1 gesetzt, buchen Sie den Auftragswert.

Bild 4: Das Formular zur Laufzeit.

Private Sub Status_Change()
Dim strSQL As String
Dim varPos As Variant
strSQL = ""
varPos = Me.Bookmark
Dim bytStatusAlt As Byte
'Prüfen, welchen Status der Auftrag vorher hatte
If Not (IsNull(Me.Status.OldValue)) Then
bytStatusAlt = Me.Status.OldValue
End If
'wenn alter und neuer Wert identisch
'Prozedur verlassen
If bytStatusAlt = Me.Status.Value Then
Me.Requery
Me.Bookmark = varPos
Exit Sub
End If
If (Me.txtAnzahl.Value = 0) Then
If Me.Status.Value = 1 Then 'bezahlt
'Restbetrag buchen
If Me.Auftragsart.Value = 2 Then 'gutschrift
strSQL = "INSERT INTO tabZahlungen (Betrag,Referenz,Auftrag) " & _
"VALUES(" & getWert(Me.Auftragswert.Value * (-1)) & ",""Auftrag
" & _
Me.ID.Value & """," & Me.ID.Value & ");"
ElseIf Me.Auftragsart.Value = 1 Then 'Rechnung
strSQL = "INSERT INTO tabZahlungen (Betrag,Referenz,Auftrag) " & _
"VALUES(" & getWert(Me.Auftragswert.Value) & ",""Auftrag " & _
Me.ID.Value & """," & Me.ID.Value & ");"
End If
'Prüfen ob der alte Wert "geliefert" war, dann
'Status auf "abgeschlossen" setzen
If bytStatusAlt = 2 Then
Me.Status.Value = 4
End If
End If
ElseIf (Me.txtRestbetrag.Value <> 0) Then
If Me.Status.Value = 1 Then 'bezahlt
'Restbetrag buchen
If Me.Auftragsart.Value = 2 Then 'gutschrift
strSQL = "INSERT INTO tabZahlungen (Betrag,Referenz,Auftrag) " & _
"VALUES(" & getWert(Me.txtRestbetrag.Value * (-1)) & ",""Auftrag
" & _
Me.ID.Value & """," & Me.ID.Value & ");"
ElseIf Me.Auftragsart.Value = 1 Then 'Rechnung
strSQL = "INSERT INTO tabZahlungen (Betrag,Referenz,Auftrag) " & _
"VALUES(" & getWert(Me.txtRestbetrag.Value) & ",""Auftrag " &
_
Me.ID.Value & """," & Me.ID.Value & ");"
End If
'Prüfen ob der alte Wert "geliefert" war, dann
'Status auf "abgeschlossen" setzen
If bytStatusAlt = 2 Then
Me.Status.Value = 4
End If
End If
ElseIf Me.txtRestbetrag.Value = 0 Then
'Wenn Status="geliefert" oder "offen", dann Rückbuchung
'ansonsten keine Buchung
If (Me.Status.Value = 2) Or (Me.Status.Value = 3) Then
'Rückbuchung
If Me.Auftragsart.Value = 2 Then 'gutschrift
strSQL = "INSERT INTO tabZahlungen (Betrag,Referenz,Auftrag) " & _
"VALUES(" & getWert(Me.txtRestbetrag.Value) & ",""Storno Auftrag "
& _
Me.ID.Value & """," & Me.ID.Value & ");"
ElseIf Me.Auftragsart.Value = 1 Then 'Rechnung
strSQL = "INSERT INTO tabZahlungen (Betrag,Referenz,Auftrag) " & _
"VALUES(" & getWert(Me.txtRestbetrag.Value * (-1)) & ",""Storno
Auftrag " & _
Me.ID.Value & """," & Me.ID.Value & ");"
End If
'Prüfen ob der alte Wert "abgeschlossen" war, dann
'Status auf "geliefert" setzen
If bytStatusAlt = 4 Then
Me.Status.Value = 2
End If
End If
End If
'SQL-Anweisung ausführen
If strSQL > "" Then
Application.CurrentDb.Execute strSQL
End If
Me.Requery
On Error GoTo FEHLER
Me.Bookmark = varPos
Exit Sub
FEHLER:
If Err.Number = 3259 Then
DoCmd.GoToRecord acDataForm, Me.Name, acLast
End If
Exit Sub
End Sub

Der erste Else-If-Zweig prüft nun, ob der Restbetrag ungleich 0 ist. Falls in diesem Fall als Status bezahlt gewählt wurde, erzeugen Sie eine Buchung mit dem Restbetrag. Im nächsten und letzten Else-If-Zweig prüfen Sie, ob der Restbetrag gleich 0 ist. Wurde gleichzeitig der Status auf offen oder geliefert gesetzt, müssen Sie den Gesamtbetrag rückbuchen. Dazu multiplizieren Sieden Betrag bei Rechnungen mit -1, bei Gutschriften verwenden Sie den gespeicherten Wert.

Zum Schluss führen Sie die erzeugte SQL-Anweisung mit der Execute-Methode aus. Damit dann auch die neuen Werte angezeigt werden, rufen Sie die Requery-Methode auf und setzen dann die Bookmark-Eigenschaft auf den zwischengespeicherten Wert, um den Datensatz wieder zu aktivieren.

Mit diesem Code geht Ihren Benutzern die Buchung von Zahlungseingängen so leicht von der Hand, das sie gar nichts anderes mehr machen möchten. Aber noch schöner wäre es natürlich, Sie würden ihnen auch ein Formular anbieten, mit dem Teilbeträge gebucht werden können.

Teilbeträge buchen

Damit der Benutzer Teilbeträge zu einem Auftrag buchen kann, stehen zahlreiche Möglichkeiten zur Verfügung. Sie erstellen ein separates Formular, das über einen Button geöffnet werden kann oder das Sie als Unterformular in das vorhandene Formular einfügen. Sie können aber natürlich auch die notwendigen Eingabefelder in das bisherige Formular integrieren und sie nur dann einblenden, wenn Teilzahlungen möglich sind, also wenn der Auftrag noch nicht bezahlt ist. Diese Möglichkeit wird nachfolgend realisiert, da sie gerade bei nur wenigen notwendigen Eingaben den meisten Komfort für den Benutzer bietet.

Ergänzen Sie dazu das Formular im Detailbereich um ein Gruppenfeld und fügen Sie dort ein Feld txtDatum für die Datumseingabe ein, ein Feld txtBetrag für den zu buchenden Betrag und einen Button bttSpeichern mit der Beschriftung buchen, über den die Buchung erfolgen kann. Das Ergebnis sollte dann wie in Bild 5 aussehen. Möchten Sie Labelfelder zur Beschriftung einfügen, sollten Sie auch diese sinnvoll benennen, weil Sie zum Ein- und Ausblenden darauf zugreifen müssen. Legen Sie außerdem für alle Steuerelemente die Eigenschaft Sichtbar auf Nein fest, da sie erst dann eingeblendet werden sollen, wenn sie benötigt werden.

Bild 5: Die Steuerelemente zum Erfassen von Teilbeträgen.

Für den Button erstellen Sie dann eine Click- Ereignisprozedur, in der Sie wieder das Lesezeichen
des Formulars sichern, die SQL-Anweisung generieren und ausführen, das Formular aktualisieren und dann das Lesezeichen wieder zuweisen (Listing 5).

Private Sub bttSpeichern_Click()
Dim strSQL As String
Dim varPos As Variant
strSQL = ""
varPos = Me.Bookmark
If Me.Auftragsart.Value = 2 Then 'Gutschrift
strSQL = "INSERT INTO tabZahlungen (Betrag,Referenz,Auftrag) " & _
"VALUES(" & getWert(Me.txtBetrag.Value * (-1)) & ",""Auftrag " & _
Me.ID.Value & """," & Me.ID.Value & ");"
ElseIf Me.Auftragsart.Value = 1 Then 'Rechnung
strSQL = "INSERT INTO tabZahlungen (Betrag,Referenz,Auftrag) " & _
"VALUES(" & getWert(Me.txtBetrag.Value) & ",""Auftrag " & _
Me.ID.Value & """," & Me.ID.Value & ");"
End If
If strSQL > "" Then
Application.CurrentDb.Execute strSQL
End If
Me.Requery
Me.Bookmark = varPos
End Sub

Nun müssen Sie aber noch dafür sorgen, dass die Steuerelemente eingeblendet und bei Bedarf initialisiert werden. Im Feld txtDatum soll das aktuelle Datum eingetragen werden, im Feld txtBetrag sollten Sie den Restbetrag eintragen, wenn er vom Auftragswert abweicht, andernfalls den Auftragswert. Dazu ergänzen Sie die Current-Ereignisprozedur des Formulars wie in Listing 6.

Private Sub Form_Current()
...
End If
If Me.Status.Value = 2 Or Me.Status.Value = 3 Then
Me.Rahmen.Visible = True
Me.lblBetrag.Visible = True
Me.lblDatum.Visible = True
Me.txtBetrag.Visible = True
Me.txtDatum.Visible = True
Me.bttSpeichern.Visible = True
Me.txtDatum.Value = Date
If Me.txtAnzahl > 0 Then
Me.txtBetrag.Value = Me.txtRestbetrag.Value
Else
Me.txtBetrag.Value = Me.Auftragswert.Value
End If
Else
Me.Rahmen.Visible = False
Me.lblBetrag.Visible = False
Me.lblDatum.Visible = False
Me.txtBetrag.Visible = False
Me.txtDatum.Visible = False
Me.bttSpeichern.Visible = False
End If
End Sub

Da bei einem Endlosformular die eingeblendeten Steuerelemente jedoch für alle Datensätze und nicht nur für den aktuellen Datensatz angezeigt werden, sollten Sie das Formular als einzelnes Formular anzeigen lassen und die Eigenschaft Standardansicht auf Einzelnes Formular.