Gedanken über Dokumente
Dokumente in iDempiere sind Datensätze aus Tabellen mit besonderen Eigenschaften. In der deutschen Übersetzung ist zumeist von "Beleg" die Rede. Beispiele für Dokumente sind Angebote, Rechnungen, Journale, etc. Allerdings ist der Begriff "Dokument" nicht ganz klar abgegrenz, weil es einen Strauß von Eigenschaften gibt, die eine konkrete Tabelle nicht alle nutzen muss aber kann. Diese möchte ich hier näher beleuchten; dabei richtet sich dieser Artikel an Entwikckler und Implementoren, die diese Eigenschaften in einer eigenen Tabelle nutzen wollen.
Arten von Dokumenteigenschaften
Dokumentnummer
Hat eine Tabelle eine Spalte mit Namen DocumentNo, so wird diese automatisch besonders behandelt. Bei der ersten Benutzung der Tabelle wird eine Sequenz angelegt, die dfür sorgt, das eine fortlaufende Belegnummer vergeben wird. Diese Belegnummer kann wie andere auch dann in Belegnummernkreis (Fenster_ID-112) genauer eingestellt werden. Sie kann auf einen bestimmten Startwert gestellt werden oder ein Präfix oder ein Suffix erhalten. Belegnummern können übrigens auch Kontextvariablen enthalten, so das man z.B. ein Datum einfügen kann für so etwas wie "Rechnung 00001/2014" (siehe z.B. en:NF001_Document_Sequence_Improved).
Modell
Für einen Dokumenttyp, der eigene Logik implementiert, muss man ein Model schreiben. Das bedeutet, das man ein eigenes Plugin erzeugt und dort eine Java-Klasse schreibt, die sogenannte Model-Klasse (M*). Diese muss von einer vorgegebenen Basisklasse abgeleitet sein (X_*). Diese wiederum wird mit Hilfe des Tools GenerateModel aus der Tabellendefinition der Datenbank erzeugt.
DocAction
Viele Dokumenteigenschaften sind davon abhängig, das die Model-Klasse das Interface DocAction implementiert. Diese Klasse erlaubt, das Status- und Workflow-Framework von iDempiere zu nutzen. Das bedeutet, das ein solches Dokument dann z.B. "fertiggestellt" und "geschlossen" werden kann. Die Implementierung dieses Interface erfordert eine ganze Menge Klassen. Um das etwas zu erleichtern, kann man sich als Beispiel die Klasse [org.compiere.process.DocActionTemplate.java] ansehen. Ich selber habe versucht, die allgemeinen Eigenschaften einer solchen Klasse in eine abstrakte Basisklasse namens [AbstractMBAYInterestCalculation] auszulagern. Diese ist nicht perfekt, aber kann dem interessierten Leser vielleicht auch als Beispiel dienen.
Spalten
Am besten fängt man an, indem man die folgenden Spalten einrichtet:
- DocumentNo (mandatory; 30 Zeichen)
- DateDoc (mandatory)
- DateAcct (mandatory)
- Doc_User_ID (entweder eine Spalte oder eine getter-Methode, die dann eine Spalte wie SalesRep_ID oder sowas ausliest; Standardlogik z.B. "@#AD_User_ID@").
- C_Currency_ID (mandatory; Standardlogik z.B. "@$C_Currency_ID@")
- DocAction (mandatory; 2 Zeichen; default 'CO'; Schaltfläche; Referenz "_Document Action"; hier wird ein Prozeß angegeben, der den entsprechenden Workflow unseres Dokumenttyps angibt)
- DocStatus (mandatory; 2 Zeichen; default 'DR'; Listnreferenz; Referenz "_Document Status" )
- processed (mandatory; default 'N')
- processing (mandatory; default 'N') - laut Dantam's Artikel ein varchar(1), aber wahrscheinlich eine Schaltfläche. Hier ist z.B. in C_Invoice der gleiche Prozeß wie bei DocAction eingetragen. Ich habe bisher keine Ahnung, wozu man da zwei Spalten zu braucht.
- isApproved (mandatory; default 'Y')
Was macht dabei DocumentEngine?
Die Dokument-Aktionen werden von der Klasse DocEngine aus ausgeführt. Wer eine Statusoperation ausführen möchte, kann das am besten mit der odel-Methode processIt() machen, die dann die entsprechende Methode von DocEngine aufrufen sollte, die dann das Management der verschiedenen Statusmöglichkeiten übernimmt.
- http://www.adempiere.com/Document_Engine - u.a. Links zu Sourcecode der Doc_*-Klassen
- http://www.adempiere.com/Development_Guidelines_in_German#Klasse_DocumentEngine - deutsche Einführung, recht gut geschrieben
Workflow
Man kann einen Beleg-Workflow anlegen, der den Wechsel zwischen verschiedenen Statuswerten definiert. Man definiert einen Workflow mit den Knoten Start, Prepare und Complete. Die beiden letzteren werden mit der entsprechenden Belegaktion verbunden, damit diese sodann ausgeführt wird. Dann definiert man einen Übergang (Transition) jeweils von Start zu Prepare und von Prepare zu Complete.
Der so definierte Workflow sorgt dafür, das Dokumente ordentlich fertiggestellt werden können. Er kann als Basis für weitere Experimente dienen.
stornieren von Dokumenten
Wer Dokumente stornieren möchte, benötigt dazu noch folgende Spalte:
- Reversal_ID (Referenztyp Tabelle, als Referenz muss eine neu erzeugte Tabellenreferenz eingetragen werden, die meine ganze Tabelle umschliesst und nach DocumentNo sortiert sein kann)
Ich habe das Stornieren bisher nicht selber ausprobiert. Wahrscheinlich muss der Workflow entsprechend konfiguriert werden. Außerdem muss man natürlich mit grosser Sorgfalt vorgehen, wenn man verbuchte Dokumente stornieren möchte.
Reaktivieren von Dokumenten
Ich möchte mein Dokument gerne raktivieren, nachdem es bereits fertiggestellt wurde. Dazu habe ich im Workflow einen weiteren Knoten "Reactivate" erstellt und diesen mit der entsprechenden Belegaktion verknüpft. Dann habe ich einen zweiten Übergang von Start zu Reactivate definiert. Der erste Übergang, der eine kleinere Reihenfolge-Nummer erhalten hat (also zuerst ausgeführt wird), hat dann eine Bedingung erhalten, die ihn nur ausführt, wenn "DocAction = 'CO'" ist. Damit funktioniert auch der Aufruf der Modellmethode reactivateIt() durch den Benutzer.
Prinzipiell setzt das Reaktivieren voraus, das man die Auswirkungen des Dokumentes storniert. Man muss das zwar nicht zwingend so implementieren, ich würde es aber empfehlen.
Approval / Freigabe
Workflows erlauben auch, die Bearbeitung eines Dokumentes von einer vorherigen Freigabe abhängig zu machen. So können z.B. Rechnungen über 1.000,- € immer erst dem Vorgesetzten vorgelegt werden oder dergleichen.
Mehr über Workflows
von http://en.wikiversity.org/wiki/Adempiere_Technical_Training#Document_Process_Workflow:
Every document has a defined process – the workflow is started with the Process button:
These workflows have a defined start context (the document) and a responsible.
- Start (Draft)
- Auto
- Prepare (In Progress)
- Complete (Completed)
If you want to customize a workflow for a document:
- for all clients
- add customized node/transitions and/or inactivate standard transitions
- for a client or organization
- make changes on System workflow with entity type <> Dictionary/Adempiere
- execute process “Workflow to Client”
Buchender Beleg
Ein Beleg, der eigene Buchungszeilen erzeugen kann, muss die folgenden Felder enthalten:
- Posted (mandatory; boolean; Default 'N')
- ProcessedOn (integer)
- C_DocType_ID (mandatory; Table; Referenz C_DocType)
Die entsprechende Logik wird durch die Anwesenheit des Feldes "Posted" aktiviert. In diesem Fall wird nach dem Abschliessen eines Dokumentes eine zum Modell passende Verbuchungsklasse benutzt, um die Verbuchung vorzunehmen. Hierfür gibt es die Konventione, das diese auf "_Doc" endet. Innerhalb des iDempiere-Kerns wird der Name dieser Klasse (basierend auf dieser Konvention) automatisch ermittelt. In ADempiere ging das auch für eigene Klassen so. In iDempiere, das durch die OSGi-Pakete den Zugriff des Kerns auf unsere Plugin-Klassen nicht mehr erlaubt, gibt es eine Factory zu diesem Zweck.
Dokument-Basisbelegtyp
Unser neuer Belegtyp muss auch einen Eintrag in der Datenbank erhalten. Dazu müssen wir uns allerdings zuerst überlegen, welchem Basistyp er zugehört. Dieser Basistyp dient der Sortierung und Gruppierung von Belegtypen insbesondere für die Belegtyp-Eingabefelder. Außerdem kann/muss in der Periodenkontrolle (im Kalender) für jeden einzelnen Basisbelegtyp die Periode geöffnet oder geschlossen werden. Obwohl es interessanterweise nicht unbedingt zwingend vorgeschrieben ist, bietet es sich dennoch an, für eine eigene verbuchende Dokumentenklasse auch einen Dokument-Basistyp einzurichten.
Der Basisbelegtyp besteht aus einem Eintrag in der Referenzliste "C_DocType DocBaseType" und hat immer drei Buchstaben. Wer möchte, kann hier prinzipiell für alle seine Dokumente einen Typ "DOC" einrichten. Allerdings ist es sinnvoller, für jeden Belegtyp, den man erzeugt, einen eigenen Basistypen zu erzeugen. Dann kann ein Auswahlfeld im Fenster per Validierung nach diesem Basistyp filtern und dem Benutzer bleibt so später immer die Möglichkeit, eigene Ableitungen zu generieren.
Bei einigen Basistypen des Standard-iDempiere ist es so, das mehrere Basistypen mit Objekten derselben Tabelle arbeiten. So ist z.B. durch die Aufteilung "API Accounts Payable Invoice" und "ARI Accounts Receivable Invoice" sichergestellt, das Eingangs- und Ausgangsrechnungen niemals gemischt werden. Beide stehen zwar in derselben Tabelle und benutzen weitgehend dieselbe Geschäftslogik, sie haben jedoch jeweils eigene Fenster und werden auch durch das Feld "isSOTrx" (und eben den unterschiedlichen Basisbelegtyp) unterschieden. Durch die zwei Basis-Belegtypen reicht eine einfache Validierung auf dem Eingabefeld für den Belegtyp, so das man für Kunden auch wirklich nur Verkaufs-Belegtypen auswählen kann und umgekehrt.
Nachdem man die Basis-Belegtypen erstellt hat (im System-Mandant), sollte man nun im Mandanten den Prozeß "Belegarten überprüfen" aufrufen. Dieser legt zu jeder Basisbelegart eine Belegart an und erzeugt auch direkt entsprechende Perioden-Einträge im Kalender.
Die so automatisch erzeugten Belegarten kann man im Fenster "Belegart" ansehen und ggf. anpassen. Es empfiehlt sich, die Nummernverwaltung einzuschalten und einen Belegnummernkreis anzugeben (oder neu zu erzeugen).
In diesem Fenster kann man nun noch weitere Belegarten von seinem Basisbelegtyp ableiten. Das hat verschiedene Zwecke:
- Zuerst einmal kann das der betriebsinternen Gruppierung von Belegen dienen. Wer in seinem Betrieb meint, das eine Warenrechnung anders ist als eine Dienstleistungsrechnung und eine Auslandsrechnung wiederum anders und eine Frachtrechnung auch, kann das so ohne grosse Probleme aufteilen.
- Man kann (in diesem Fenster) ein eigenes Druckformat angeben und so den gedruckten Beleg ganz anders aussehen lassen.
- Man kann die Direktbuchungen (Charge) über die Direktbuchungsart (Gebührenart je nach Übersetzung) gruppieren und für bestimmte Belegarten nur bestimmte Buchungen erlauben. So kann also die Frachtrechnung auf das Konto für Fracht buchen, die Auslandsrechnung auf das Konto für Zoll, etc.
Als nächstes sollte man eine Validierungsregel erstellen. Diese bekommt den Namen "C_DocType MeinBelegtypname" und als SQL-Code sowas wie die folgende Zeile:
C_DocType.DocBaseType IN ('ARZ', 'APZ') AND C_DocType.IsSOTrx='@IsSOTrx@' AND C_DocType.AD_Client_ID=@#AD_Client_ID@
Den Teil mit IsSOTrx kann man natürlich weglassen, wenn wir keinen getrennten Belegtyp für Eingangs- und Ausgangsfenster haben. Dieser Teil der Formel sorgt dafür, das wir identische Felddefinitionen für Eingangs- und Ausgangsbelege nehmen können. Die beiden Fenster unterscheiden sich dann nur dadurch, das beim Öffnen des Fensters die Kontextvariable "IsSOTrx" gesetzt wird. Hiermit wird dann alles andere gesteuert. Das dieser Wert gesetzt ist, kann man in der Fensterdefinition einstellen.
Einkaufs- und Verkaufsbelege
Wer einen Belegtyp hat, der Einkaufs- und Verkaufsbelege (AP und AR) unterschiedlich verbuchen soll, braucht auch noch dieses Feld. Bei dieser Art Belegen bietet es sich im Allgemeinen an, auch zwei getrennte Basisbelegtypen und ggf. zwei getrennte Fenster zu erzeugen. Die Fensterdefinitionen können weitgehend identisch sein bis auf den Eintrag "Verkaufstransaktion", der die Kontextvariable "IsSOTrx" setzt und sie somit unterscheidbar macht.
- isSOTrx (mandatory; boolean; im Fenster am besten gar nciht angezeigt oder mindestens schreibgeschützt)
Fenster, Tab & Felder
Das Fenster sollte auf den Fenstertyp "Transaktion" gesetzt werden. Dadurch wird die [History-Funktion] aktiviert. Diese sorgt dafür, das immer nur die aktuellen und nicht abgeschlossenen Dokumente angezeigt werden.
Bei der Konfiguration des Fensters sollten übrigens die folgenden Felder angezeigt werden (laut Dantam):
- Processed (set to "Read Only")
- DocStatus (set to "Read Only")
- DocAction
- Currency (wenn benutzt sollte es u.U. schreibgeschützt sein)
Für Dokumente, die Einkaufs- und Verkaufsbelege unterscheiden, bietet es sich an, diese zu kopieren und eine WHERE-Klasel einzuführen, so das man zwei Fenster für diese eigentlich unterschiedlichen Belegtypen hat. Man kann in der Fensterdefinition ein Fenster als Verkaufstransaktion definieren und somit eine Kontextvariable setzen, die in vielen Abfragen und Validierungen benutzt wird, um sicherzustellen, das alle Felder entsprechend angepasst funktionieren. Das erfordert natürlich bei der Erzeugung eigener Abfragen etwas Sorgfalt, aber es gibt ja genug bestehende Beispiele wie z.B. "Rechnung (Kunde)" und "Rechnung (Lieferant)".
Grnudsätzlich kann man einen Datensatz ausdrucken, indem man in der Toolbar auf den Button für einen entsprechenden Bericht klickt. Durch diesen wird dann ein Druckformat erzeugt, das man entsprechend anpassen kann. Will man oft eher einzelne Datensätze z.B. als Formular drucken als eine Liste von Sätzen, so bietet es sich an, den Druck-Button zu aktivieren, mit dem man einzelne Datensätze drucken kann. Das macht man, indem man im Fenster "Prozeß & Bericht" einen Eintrag macht. Diesen setzte man auf "Bericht" und gibt ggf. ein vorbereitetes Druckformat an. Dieses kann dann mit mit den Buttons für "Druck" und "Druckvorschau" gestartet werden, indem man es im Datensatz des Fensterregisters einträgt.
Hier gibt es übrigens ein kleines Problem. Das Fenster "Prozeß & Bericht" muss man im Systemmandanten einrichten, während man sein Druckformat ja zumeist vorher im normalen Mandanten vorbereitet hat. Man kann in diesem Fall das Druckformat freilassen. Es wird automatisch das gewählt, das das Feld "Standard" gesetzt hat.
Berichtsansicht benutzen, um mehr Spalten auszudrucken als in der Tabelle stehen
Eine Erweiterung gibt es noch, indem man in "Prozeß & Bericht" im Feld "Berichtsansicht" eine ebensolche einträgt. In diesem Fall wird ein Druckformat ausgewählt, das ebendiese Berichtsansicht ausgewählt hat. Hierdurch kann man die Auswahl des Druckformats noch weiter eingrenzen.
Insbesondere kann man hier auch eine Berichtsansicht angeben, die auf einer ganz anderen Tabelle basiert. Diese Tabelle sollte lediglich das Primärschlüsselfeld unseres Dokumentes beinhalten, weil hierdurch der ausgedruckte Datensatz heruasgefiltert wird. Ich habe das beispielsweise benutzt, um anstatt der im aktuellen Fenster bearbeiteten Tabelle ein Datenbank-View für den Ausdruck zu benutzen, das zwar auf ebendieser Tabelle basiert, ihnr per JOIN aber jede Menge weitere Daten hinzufügt. Diese grosse Menge an Informationen kann man dann (wenn man möchte) durch eine Berichtsansicht wieder einschränken und dann im Druckformat verarbeiten. Ein derartiges Druckformat erzeugt man im Fenster "Druckformat", wo man die Tabelle und die Ansicht bei der Neuanlage des Datensatzes einstellen kann. Dann erzeugt man die Felder mittels des automatischen Proßess-Knopfes hierfür und kann dann das Druckformat schon im Zielfenster einstellen und dort in der Druckansicht benutzen. Ist diese das erste Mal zu sehen, kann man es dann wie gewöhnlich entstandene Druckformate auch anpassen.
Drucken per Programm
Mit der folgenden Methode kann ich in meinem Model für die Tabelle BAY_InterestCalculation einen Ausdruck veranlassen, der auf einem View basiert, das RV_BAY_InterestCalculation heisst. Man kann diese Methode z.B. in completeIt() aufrufen, um dafür zu sorgen, das ein Dokument immer gedruckt wird, wenn es abgeschlossen wird. Man kann übrigens im Prozeßfenster einstellen, ob der Ausdruck sofort im Hintergrund oder erst mit Rückfrage geschieht.
public void print() { int viewTable = MTable.getTable_ID("RV_" + Table_Name); MPrintFormat format = new Query(getCtx(), MPrintFormat.Table_Name, MPrintFormat.COLUMNNAME_AD_Table_ID + "=? AND " + MPrintFormat.COLUMNNAME_IsDefault + "=? ", get_TrxName()).setParameters(viewTable, "Y") .first(); MQuery query = new MQuery(get_Table_ID()); query.addRestriction(get_KeyColumns()[0], MQuery.EQUAL, get_ID()); PrintInfo info = new PrintInfo(getDocumentInfo(), get_Table_ID(), get_ID()); ReportEngine re = new ReportEngine(getCtx(), format, query, info, get_TrxName()); re.print(); }
Links
Artikel, die Dokumente erklären
Basiserklärungen
- http://wiki.idempiere.org/en/Create_a_custom_document_class - recht neuer Artikel von Daniel Tamm im iDempiere Wiki, hat noch viele Lücken
- http://www.adempiere.com/How_to_create_a_new_document_with_specific_accounting
weiterführende Erklärungen
- http://www.adempiere.com/How_to_Activate_Document_Approval_Workflow
- http://www.adempiere.com/Document_Action_Dialog - Erklärung zum Action Button und dem davon aufgehenden Fenster
- http://www.adempiere.com/Document_Sequence - Document Sequence
- Erweiterungen
- http://www.adempiere.com/Enhance_Document_No_Formatting - Verbesserung der Dokumentnummerierung (hierdurch sind Kontextvariablen erlaubt)
- http://wiki.idempiere.org/en/NF001_Document_Sequence_Improved - Verbesserung bzgl. Organisationen und Neustart von Nummernkreisen
- http://www.adempiere.com/Sponsored_Development:_Document_Signing - Document Signing Erweiterung, augenscheinlich nie verwirklicht worden
Artikel, die ich als Quellen verwendet, aber bereits weitgehend hier eingearbeitet habe:
- http://www.adempiere.com/HOWTO_Process_Documents - erklärt recht gut die Benutzung der vorhandenen Dokumenttypen; weniger die Entwicklung neuer
- http://en.wikiversity.org/wiki/Adempiere_Technical_Training#Document_Process_Workflow - enthält kurze, aber aufschlussreiche Teile zum Thema, insbesondere "Document Process Workflow"
- http://www.adempiere.com/Document_Engine
- https://groups.google.com/forum/#!topic/idempiere/WtTlVL1ZjWw - kurzed Forumsthread darüber, wie man einen Workflow implementieren kann
Fenster, die mit Dokumenten zusammenhängen:
- http://wiki.idempiere.org/en/Document_Type_%28Window_ID-135%29
- http://wiki.idempiere.org/en/Document_Sequence_%28Window_ID-112%29
- http://wiki.idempiere.org/en/Unprocessed_Documents_%28All%29_%28Window_ID-53087%29
- http://wiki.idempiere.org/en/UnPosted_Documents_%28Window_ID-294%29
- http://wiki.idempiere.org/en/My_Unprocessed_Documents_%28Window_ID-53086%29
- http://wiki.idempiere.org/de/Workflow_%28Fenster_ID-113%29
(Was ist der Unterschied zwischen "unprocessed" und "unposted" und woher wissen diese Fenster, welche Tabellen sie absuchen sollen?)
- http://wiki.idempiere.org/en/Verify_Document_Types_%28Process_ID-233%29 - was macht dieser Prozeß? Er erzeugt wohl Open-Einträge für neue Dokumenttypen. Sonst noch was?
weitere Informationen
involvierte Klassen und Interfaces
MSetup
Create the list of document type for new Client ( Method createAccounting ).
org.compiere.server.AcctProcessor
Background process running on the JBoss server that perform the accounting document posting process.
org.compiere.grid.ed.VDocAction
Define hardcoded in dynInit method the list of possible transitions, and transitions by table
Beispiele für die Implementierung einiger DocAction-Methoden
getDocumentInfo()
Ergibt eine kurze aber eindeutige Bezeichnung des Datensatzes, die vor allem in Logzeilen und sonstigen Programm-Mitteilungen verwendet wird. Dieses Beispiel nutzt einen vorhandenen Documenttyp.
public String getDocumentInfo(){ MDocType dt = MDocType.get(getCtx(), getC_DocType_ID()); String msgreturn = dt.getNameTrl()+" "+getDocumentNo(); return msgreturn.toString(); }
createPDF()
Es gibt eine Methode, um das Dokument als PDF auszugeben. Diese wird - soweit ich gesehen habe - nur benutzt, um das Dokument per EMail zu versenden. Da man aber nie weiss, kann man das z.B. so wie folgend implementieren. Dabei kann man bei der Auswähl des Printformats natürlich auch genauer auswählen.
public File createPDF (){ try{ StringBuilder msgfile = new StringBuilder().append(get_TableName()).append(get_ID()).append("_"); File temp = File.createTempFile(msgfile.toString(), ".pdf"); return createPDF (temp); }catch (Exception e){ log.severe("Could not create PDF - " + e.getMessage()); } return null; } public File createPDF (File file){ MPrintFormat format = MPrintFormat.get(getCtx(), 0, get_Table_ID()); MQuery query=new MQuery(get_Table_ID()); query.addRestriction(get_KeyColumns()[0],MQuery.EQUAL,get_ID()); PrintInfo info = new PrintInfo(getDocumentNo(),get_Table_ID(),0); ReportEngine re = new ReportEngine(getCtx(), format, query, info, get_TrxName()); if(format.getJasperProcess_ID() > 0){ // JasperReports Print Format ProcessInfo pi = new ProcessInfo ("", format.getJasperProcess_ID()); pi.setRecord_ID(get_ID()); pi.setIsBatch(true); ServerProcessCtl.process(pi, null); return pi.getPDFReport(); }else{ // Standard Print Format (Non-Jasper) return re.getPDF(file); } }