Artikelverwaltung, Teil 2

Lies diesen Artikel und viele weitere mit einem kostenlosen, einwöchigen Testzugang.

André Minhorst, Duisburg

Im ersten Teil dieser Beitragsreihe haben Sie das Datenmodell und die wesentlichen Formulare der Artikelverwaltung kennen gelernt. Nun beschäftigen Sie sich mit den Inventuren: Welche Faktoren sind dafür wichtig Wie werden bereits in einer Inventur berücksichtigte Daten bei folgenden Inventuren ausgeschlossen Außerdem erfahren Sie in dieser Folge, wie Sie Berichte für die Ausgabe von Artikel- und Inventurdaten erstellen.

Hinweis

Die Musterlösung zu diesem Beitrag heißt Artikelverwaltung00.mdb und funktioniert mit Access 2000 und höher. Sie finden die Datenbank auf der Heft-CD oder im Internet unter http://www.access-im-unternehmen.de (Shortlink 280). Der erste Teil dieses Beitrags steht an gleicher Stelle unter dem Shortlink 273 bereit.

Eine Inventur ist die Aufnahme des Bestands aller Vermögenswerte eines Unternehmens, also auch der vorhandenen Artikel. Die in die Artikelverwaltung integrierte Inventurfunktion ermöglicht dies ausschließlich für die Artikel; die Aufnahme aller für eine Inventur notwendigen Informationen würde den Umfang des Beitrags leider sprengen. Diese Inventurfunktion berücksichtigt lediglich die in der Datenbank gespeicherten Artikel. Die Menge der vorhandenen Artikel sowie deren Maßeinheiten sollten regelmäßig zum Bilanzstichtag bekannt sein, um eine ordnungsgemäße Buchhaltung zu ermöglichen. Der Artikelbestand muss allerdings nicht genau an diesem Tag gezählt werden.

Inventurarten

Bild 1: Aufruf der Inventur

Es gibt mehrere Varianten von Inventuren, unter anderem die folgenden:

  • Stichtagsinventur: Findet innerhalb von zehn Tagen vor und zehn Tagen nach dem Abschluss-Stichtag statt. Das Ergebnis wird auf den Abschluss-Stichtag hochgerechnet.
  • Verlegte Inventur: Erfolgt zwischen drei Monaten vor und zwei Monaten nach dem Abschluss-Stichtag. Die Zählung der unterschiedlichen Artikel muss nicht an einem Tag erfolgen. Die Werte für jeden einzelnen Artikel werden auf den Abschluss-Stichtag hochgerechnet.
  • Permanente Inventur: Die Inventur erfolgt fast ausschließlich über die Bewertung der Ein- und Ausgänge eines Artikels. Der Soll- und der Ist-Zustand müssen einmal im Jahr abgeglichen werden.
  • Die Anwendung unterstützt alle genannten Inventurarten. Der einzige Unterschied zwischen den Arten ist der Zeitpunkt, an dem Soll- und Istwert des Bestands eines Artikels ermittelt werden müssen. Mit der Artikelverwaltung können Sie den Zeitpunkt der Zählung für jeden einzelnen Artikel festlegen.

    Wie arbeitet die Inventurfunktion

    Die Inventurfunktion kann für jeden Artikel einzeln aufgerufen werden. Der Aufruf erfolgt über die Schaltfläche Inventur starten der Detailansicht eines Artikels im Formular frmArtikel (siehe Bild 1).

    Die dadurch ausgelöste Ereignisprozedur öffnet das Formular frmInventur für den aktuell im Artikelformular angezeigten Artikel (siehe Bild 2).

    Bild 2: Eingabe des gezählten Bestands

    Dieses Formular zeigt folgende Informationen an:

  • Datum der letzten Inventur
  • Bestand bei der letzten Inventur
  • Datum der aktuellen Inventur
  • Berechneter Bestand (aktuelle Inventur)
  • Tatsächlicher Bestand (aktuelle Inventur)
  • Differenz zwischen tatsächlichem und berechnetem Bestand
  • Die mit einem roten Rahmen versehenen Informationen kann der Benutzer selbst ändern. Das Datum wird automatisch mit dem aktuellen Datum vorgelegt, es kann aber nachträglich geändert werden.

    Bei der Programmierung des Formulars frmInventur und der enthaltenen Funktionen sind einige Punkte zu beachten, die im nachfolgenden Abschnitt geklärt werden. Anschließend finden Sie eine Beschreibung der Funktion selbst.

    Wichtige Voraussetzungen

    Die Inventurfunktion startet mit dem Mausklick auf die Schaltfläche Inventur starten des Artikelformulars. Das Formular frmInventur zur Eingabe der Inventurdaten bezieht seine Daten über diverse Zugriffe auf die in der Datenbank enthaltenen Tabellen.

    So durchsucht das Formular die Tabelle tblInventuren, ob es bereits eine Inventur zu dem zu untersuchenden Artikel gibt, und liest gegebenenfalls das Datum der Inventur und den Bestand in die entsprechenden Felder des Formulars ein.

    Ist noch kein Inventurdatensatz vorhanden und dementsprechend noch keine Zählung des Artikels erfolgt, verwendet das Formular Standardwerte: als Datum den 1.1.1900 und als Bestand den Wert 0.

    Der im Feld Berechneter Bestand angezeigte Wert ist die Anzahl des Artikels, die sich aus dem Stand bei der letzten Inventur und den seitdem erfolgten Zu- und Abgängen berechnet.

    Dabei spielt natürlich das Datum der aktuellen Inventur eine wichtige Rolle: Beim öffnen des Formulars zeigt dieses das aktuelle Datum und den Bestand zu diesem Datum an. Wenn der Benutzer das Datum verändert, wird auch der berechnete Bestand angepasst. Neben dem Inventurdatum muss der Benutzer auch die tatsächliche Anzahl des Artikels angeben. Erst dann wird die Differenz berechnet und die Inventurdaten können gespeichert werden.

    Private Sub Form_Current()
        Dim datLetzteInventur As Date
        Dim lngBestandLetzteInventur As Long
        Me!txtArtikel = Artikelbezeichnung(lngArtikelID)
        Call LetzteInventur(lngArtikelID, datLetzteInventur, lngBestandLetzteInventur)
        Me!txtDatumLetzteInventur = datLetzteInventur
        Me!txtBestandLetzteInventur = lngBestandLetzteInventur
        Me!txtDatumAktuelleInventur = Date
        Me!txtBestandAktuelleInventurBerechnet = lngBestandLetzteInventur + _        BestandsaenderungSumme(lngArtikelID, Date)
    End Sub

    Quellcode 1

    Private Function Artikelbezeichnung(lngArtikelID As Long) As String
        Dim db As DAO.Database
        Dim rstArtikel As DAO.Recordset
        Set db = CurrentDb
        Set rstArtikel = db.OpenRecordset("SELECT Bezeichnung FROM tblArtikel " _        & "WHERE ArtikelID = " & lngArtikelID, dbOpenDynaset)
        Artikelbezeichnung = rstArtikel!Bezeichnung
        Set db = Nothing
    End Function

    Quellcode 2

    Technische Umsetzung

    Die erste Prozedur, die bei der Verwendung des Formulars frmInventuren zum Zuge kommt, ist die Ereignisprozedur, die beim öffnen des Formulars ausgelöst wird. Sie weist der modulweit deklarierten Variablen lngArtikel die per öffnungsargument von der aufrufenden Instanz übergebene Artikelnummer zu.

    Private Sub Form_Open(Cancel As Integer)
        lngArtikelID = Me.OpenArgs
    End Sub

    Die Ermittlung der im Formular angezeigten Werte erfolgt in der Ereignisprozedur, die durch das Ereignis Beim Anzeigen ausgelöst wird (s. Quellcode 1). Die Prozedur ermittelt zunächst anhand des Werts des Feldes ArtikelID die Artikelbezeichnung und trägt diese in das Textfeld txtArtikel ein. Dazu verwendet sie die Prozedur aus Quellcode 2. Diese Prozedur liefert die Artikelbezeichnung für die als Parameter übergebene Artikel-ID. Dabei instanziert sie ein Database-Objekt und ein Recordset-Objekt und ermittelt über eine entsprechende Abfrage eine Datensatzgruppe mit dem gesuchten Artikeldatensatz.

    Performance statt Dlookup

    Statt dieser Funktion hätte man auch einen entsprechenden Aufruf der Dlookup-Funktion verwenden können (in einer Zeile):

    Me!txtArtikel = DLookup("Bezeichnung", "tblArtikel", "ArtikelID = " & lngArtikelID)

    Das wäre zwar wesentlich platzsparender und damit auch übersichtlicher gewesen, allerdings ist die Performance der Domänenfunktionen relativ schlecht. Ein konkreter Test mit je 1000 Aufrufen jeder Funktion in einer Schleife brachte ein eindeutiges Ergebnis: über mehrere Läufe hinweg war die DAO-Variante circa 50-100% schneller als die Dlookup-Funktion.

    Private Sub LetzteInventur(lngArtikelID As Long, datLetzteInventur As Date, _    lngBestandLetzteInventur As Long)
        Dim db As DAO.Database
        Dim rstInventuren As DAO.Recordset
        Dim rstBestandsaenderungen As DAO.Recordset
        Set db = CurrentDb
        Set rstInventuren = db.OpenRecordset("SELECT TOP 1 Inventurdatum, " _        & "Lagerbestand FROM tblInventuren WHERE ArtikelID = " & lngArtikelID _        & " ORDER BY Inventurdatum DESC", dbOpenDynaset)
        If Not rstInventuren.EOF Then
            datLetzteInventur = rstInventuren!Inventurdatum
            lngBestandLetzteInventur = rstInventuren!Lagerbestand
        Else
            Set rstBestandsaenderungen = db.OpenRecordset("SELECT Datum " _            & "FROM tblBestandsaenderungen WHERE ArtikelID = " & lngArtikelID _            & " ORDER BY Datum DESC", dbOpenDynaset)
            If Not rstBestandsaenderungen.EOF Then
                datLetzteInventur = rstBestandsaenderungen!Datum
            Else
                datLetzteInventur = "1.1.1900"
            End If
            lngBestandLetzteInventur = 0
            rstBestandsaenderungen.Close
        End If
        rstInventuren.Close
        Set rstInventuren = Nothing
        Set rstBestandsaenderungen = Nothing
    End Sub

    Quellcode 3

    Daten der letzten Inventur ermitteln

    In einer weiteren externen Routine ermittelt die Prozedur Form_Current das Datum der letzten Inventur und den Bestand bei der letzten Inventur.

    Da die beiden zu ermittelnden Werte aus der gleichen Tabelle stammen und gleichzeitig mit einer Routine ermittelt werden sollen, können nicht beide per Rückgabewert an die aufrufende Prozedur übermittelt werden.

    Die betroffene Routine heißt LetzteInventur und hat drei Parameter, von denen einer als Eingangs- und zwei als Ausgangsparameter verwendet werden. Die Ausgangsparameter werden bereits in der aufrufenden Prozedur Form_Current deklariert und als Variable übergeben (in einer Zeile):

    Call LetzteInventur(lngArtikelID, datLetzteInventur, lngBestandLetzteInventur)

    Die beiden letzten Parameter dieses Aufrufs werden in der Prozedur LetzteInventur mit den gewünschten Daten gefüllt (s. Quellcode 3).

    Um diese Daten zu ermitteln, greift die Prozedur auf zwei Tabellen zu. Zunächst sucht sie in der Tabelle tblInventuren nach der aktuellsten Inventur für diesen Artikel. Die dazu verwendete Abfrage ist nach dem Inventurdatum sortiert und gibt durch die Option TOP 1 nur den ersten Eintrag der Datensatzgruppe zurück. Ist dieser Datensatz vorhanden, liest die Prozedur das Inventurdatum und den Lagerbestand aus der Tabelle aus und schreibt sie in die entsprechenden Rückgabeparameter.

    Private Function BestandsaenderungSumme(lngArtikelID As Long, _    datAktuelleInventur As Date) As Long
        Dim db As DAO.Database
        Dim rstBestandsaenderungSumme As DAO.Recordset
        Set db = CurrentDb
        Set rstBestandsaenderungSumme = _        db.OpenRecordset("SELECT Sum(Anzahl * Vorzeichen) AS LagerbestandGesamt " _        & "FROM tblBestandsaenderungen WHERE ArtikelID = " & lngArtikelID _        & " AND Inventur = False AND Datum <= " _        & ISODatum(Me.txtDatumAktuelleInventur), dbOpenDynaset)
        BestandsaenderungSumme = Nz(rstBestandsaenderungSumme!LagerbestandGesamt, 0)
        rstBestandsaenderungSumme.Close
        Set rstBestandsaenderungSumme = Nothing
        Set db = Nothing
    End Function

    Quellcode 4

    Wenn die Datensatzgruppe leer ist, wurde noch keine Inventur für diesen Artikel durchgeführt. In dem Fall sind weitere Untersuchungen erforderlich, um halbwegs sinnvolle Werte zurückzugeben. Wenn noch keine Inventur stattgefunden hat, wäre der Stand vor der ersten Bestandsänderung der letzte bestätigte Stand – und der ist immer 0. Als Datum stellt die Prozedur in dem Fall das Datum der ersten Bestandsänderung ein. Wenn weder eine Bestandsänderung noch eine Inventur für einen Artikel stattgefunden hat, ist Fantasie gefragt: Als Datum wird dann der 1.1.1900 und als Bestand der Wert 0 zurückgegeben.

    Einstellen des Datums der aktuellen Inventur

    Als Datum für die aktuelle Inventur trägt die Prozedur Form_Current das aktuelle Datum ein.

    Bestandsänderung berechnen

    Fehlt noch die berechnete Bestandsänderung seit der letzten Inventur. Für deren Ermittlung ist wiederum eine Funktion zuständig. Diese heißt BestandsaenderungSumme und erwartet die Artikel-ID und das Datum der aktuellen Inventur, das auch in diesem Fall mit der Date-Funktion ermittelt wird (s. Quellcode 4).

    Diese Funktion berechnet mit der Sum-Funktion in einer Abfrage über die Datensätze der Tabelle tblBestandsaenderungen die Größe der änderung am Bestand des betroffenen Artikels.

    Dabei offenbart sich die Technik, mit der eine Bestandsveränderung als inventarisiert gekennzeichnet wird: Die Tabelle tblBestandsaenderungen enthält ein Feld namens Inventur, das nach der Berücksichtigung in einer Inventur den Wert True erhält.

    Die Abfrage zum Summieren der neuen Bestandsänderungen berücksichtigt dementsprechend nur jene Einträge, bei denen das Feld Inventur den Wert False hat und deren Datum kleiner oder gleich dem Inventurdatum ist. Das Füllen der Felder des Formulars ist somit abgeschlossen, was den automatischen Part angeht. Nun folgen noch die Verarbeitung der Benutzereingaben und das Speichern der Inventurdaten.

    Datum der aktuellen Inventur ändern

    Das Datum der aktuellen Inventur wird standardmäßig auf das aktuelle Datum eingestellt. Unter Umständen sollen aber Daten von einer Inventur eingetragen werden, deren Datum in der Vergangenheit liegt. Daher kann der Benutzer das Datum der aktuellen Inventur nachträglich ändern.

    Private Sub txtDatumAktuelleInventur_BeforeUpdate(Cancel As Integer)
        If CDate(Me.txtDatumAktuelleInventur) <= CDate(Me.txtDatumLetzteInventur) Then
            MsgBox "Das Datum der aktuellen Inventur muss nach dem Datum der " _            & "letzten Inventur liegen.", vbExclamation + vbOKOnly, "Ungültiges Datum"
            Cancel = True
            Me!txtDatumAktuelleInventur = Date
        End If
    End Sub

    Quellcode 5

    Private Sub txtDatumAktuelleInventur_AfterUpdate()
        Dim lngBestandLetzteInventur As Long
        Dim datAktuelleInventur As Date
        lngBestandLetzteInventur = Me.txtBestandLetzteInventur
        datAktuelleInventur = Me.txtDatumAktuelleInventur
        Me!txtBestandAktuelleInventurBerechnet = lngBestandLetzteInventur + _        BestandsaenderungSumme(lngArtikelID, datAktuelleInventur)
    End Sub

    Quellcode 6

    Bevor sich die änderung auswirkt, überprüft die Vor Aktualisierung-Ereignisprozedur des Textfeldes txtDatumAktuelleInventur, ob das Datum mindestens einen Tag später als der Tag der letzten Inventur liegt. Die entsprechende Prozedur finden Sie in Quellcode 5.

    Angezeigte Werte im Inventurformular an neues Inventurdatum anpassen

    Wenn das neue Inventurdatum gültig ist, muss der aktuell berechnete Bestand neu ermittelt werden. Die notwendigen Anweisungen befinden sich in der Prozedur, die durch das Ereignis Nach Aktualisierung des Textfeldes txtDatumAktuelleInventur ausgelöst wird (s. Quellcode 6).

    Inventurdaten speichern

    Wenn der Benutzer das Datum der aktuellen Inventur sowie den gezählten Lagerbestand in die entsprechenden Textfelder eingegeben hat, können die Inventurdaten gespeichert werden. Diesen Vorgang kann der Benutzer mit der Schaltfläche Inventur speichern starten.

    Die dadurch ausgelöste Ereignisprozedur (s. Quellcode 7) prüft zunächst, ob die beiden Felder txtDatumAktuelleInventur und txtBestandAktuelleInventurGezaehlt gültige Werte enthalten, und bricht die Prozedur gegebenenfalls mit einer entsprechenden Fehlermeldung ab.

    Anderenfalls steht dem Speichern der neuen Inventur nichts mehr im Wege: Eine weitere If…Then-Abfrage überprüft, ob zwischen dem berechneten und dem gezählten Bestand des betroffenen Artikels eine Differenz besteht.

    Falls ja, wird ein entsprechender Umbuchungsdatensatz in den Tabellen tblUmbuchungen und tblBestandsaenderungen angelegt (siehe Bild 3). Von dem Vorgang sind zwei Tabellen betroffen, weil es mehrere Ausprägungen einer Bestandsveränderung gibt – Eingänge, Ausgänge und Umbuchungen. Alle haben spezielle Eigenschaften, die jeweils in unterschiedlichen Tabellen gespeichert und mit der Tabelle tblBestandsaenderungen per 1:1-Beziehung verbunden werden. Weitere Informationen zum Datenmodell und zu dieser Vorgehensweise finden Sie im ersten Teil dieser Beitragsreihe in Ausgabe 2/2005.

    Für das Anlegen eines Datensatzes eignet sich eine Abfrage bestens – die hier verwendete finden Sie in der Entwurfsansicht in Bild 4.

    Bild 3: Beispiel für einen Umbuchungsdatensatz

    Anschließend führt die Prozedur noch zwei Aktionsabfragen aus: Die erste fügt den Inventurdatensatz mit der Artikel-ID, dem Inventurdatum und dem Lagerbestand zur Tabelle tblInventuren hinzu.

    Bild 4: Abfrage zum Hinzufügen von Umbuchungsdatensätzen

    Die zweite sorgt dafür, dass alle Bestandsänderungen, deren Datum kleiner oder gleich dem aktuellen Inventurdatum ist und die noch nicht per Inventur erfasst wurden, als inventarisiert gekennzeichnet werden.

    Damit werden auch solche Bestandsänderungen erfasst, die vor dem Datum der letzten Inventur erfolgt sind – es kann unter Umständen vorkommen, dass eine Bestandsänderung zu spät im System gebucht und nicht mehr von einer zeitnah stattfindenden Inventur berücksichtigt wird.

    Es ist nicht einfach, bei Wareneingängen, Warenausgängen und Umbuchungen mit verschiedenen Ursachen den überblick zu behalten – schon gar nicht, wenn der Monitor die Sicht dabei einschränkt.

    Daher finden Sie nachfolgend die Beschreibung eines Berichtes, mit dem Sie alle Bestandsänderungen übersichtlich ausgeben können.

    Dabei sollen zwei Sortierungen beziehungsweise Gruppierungen möglich sein: Zunächst soll der Bericht die Daten natürlich in der chronologischen Reihenfolge ausgeben können – und zwar gemischt und nicht nach Ein- und Ausgängen oder Umbuchungen sortiert. Die zweite Variante sieht dann eine Gruppierung nach der Art der Bestandsänderung vor.

    Erstellen der Datenherkunft des Berichts

    Die Anzeige der Wareneingänge, Warenausgänge und Umbuchungen in einem Bericht ist nicht trivial, da die Daten aus einer Haupttabelle stammen, deren Datensätze mit je einer weiteren Tabelle per 1:1-Beziehung verknüpft sind.

    Um diese Tabellen als Datenherkunft für einen Bericht zu verwenden, ist die Verwendung einer UNION-Abfrage angezeigt (s. Quellcode 8).

    Die UNION-Abfrage namens qryBestandsaenderungen fasst die drei Abfragen qryUmbuchungen, qryWareneingang und qryWarenausgang zusammen und fügt ein weiteres Feld namens Bestandsaenderungsart hinzu, das einen der drei Werte Umbuchung, Wareneingang oder Warenausgang annehmen kann.

    Private Sub cmdInventurSpeichern_Click()
        Dim db As DAO.Database
        Set db = CurrentDb
        If Not IsDate(Nz(Me.txtDatumAktuelleInventur, "")) Then
            MsgBox "Bitte geben Sie ein gültiges Datum für die Inventur ein."
            Exit Sub
            Me.txtDatumAktuelleInventur.SetFocus
        End If
        If Not IsNumeric(Nz(Me.txtBestandAktuelleInventurGezaehlt, "")) Then
            MsgBox "Bitte geben Sie den gezählten Bestand für diesen Artikel ein."
            Me.txtBestandAktuelleInventurGezaehlt.SetFocus
            Exit Sub
        End If
        If Me.txtDifferenz <> 0 Then
            db.Execute "INSERT INTO qryUmbuchungen(ArtikelID, Anzahl, Datum, " _            & "UmbuchungsartID, Bemerkungen) VALUES(" & lngArtikelID & ", " _            & Me.txtDifferenz & ", ''" & Me.txtDatumAktuelleInventur _            & "'', 1, ''Anpassung des berechneten an den gezählten Bestand'')"
        End If
        db.Execute "INSERT INTO tblInventuren(ArtikelID, Inventurdatum, Lagerbestand) " _        & " VALUES(" & lngArtikelID & ", ''" & Me.txtDatumAktuelleInventur & "'', " _        & Me.txtBestandAktuelleInventurGezaehlt & ")"
        db.Execute "UPDATE tblBestandsaenderungen SET Inventur = True " _        & "WHERE ArtikelID = " & lngArtikelID & " AND Datum > " _        & ISODatum(Me.txtBestandLetzteInventur) & " AND Datum <= " _        & ISODatum(Me.txtDatumAktuelleInventur)
        Set db = Nothing
        DoCmd.Close acForm, Me.Name
    End Sub

    Quellcode 7

    SELECT qryUmbuchungen.BestandsaenderungID, qryUmbuchungen.ArtikelID, qryUmbuchungen.AnzahlMitVorzeichen, qryUmbuchungen.Datum, 0 AS Preis, ''Umbuchung'' AS Bestandsaenderungsart 
    FROM qryUmbuchungen
    UNION
    SELECT qryWareneingang.BestandsaenderungID, qryWareneingang.ArtikelID, qryWareneingang.AnzahlMitVorzeichen, qryWareneingang.Datum, qryWareneingang.Einkaufspreis AS Preis,  ''Wareneingang'' AS Bestandsaenderungsart 
    FROM qryWareneingang
    UNION 
    SELECT qryWarenausgang.BestandsaenderungID, qryWarenausgang.ArtikelID, qryWarenausgang.AnzahlMitVorzeichen, qryWarenausgang.Datum, qryWarenausgang.Verkaufspreis AS Preis, ''Warenausgang'' AS Bestandsaenderungsart 
    FROM qryWarenausgang
    ORDER BY Datum;

    Quellcode 8

    Damit das Ergebnis der Abfrage nach dem Datum sortiert wird, legen Sie die entsprechende ORDER BY-Klausel nicht für jede Teilabfrage, sondern nur für die letzte Abfrage an. Bild 5 zeigt das Ergebnis der Abfrage mit einigen Beispieldaten an.

    Ende des frei verfügbaren Teil. Wenn Du mehr lesen möchtest, hole Dir ...

    Testzugang

    eine Woche kostenlosen Zugriff auf diesen und mehr als 1.000 weitere Artikel

    diesen und alle anderen Artikel mit dem Jahresabo

    Schreibe einen Kommentar