Files
migrate-ecodms-to-paperless/docs/referenz-ecodms-exportformat.md
2026-09-04 19:51:02 +02:00

13 KiB

Das ecoDMS-Exportformat

Nachschlagewerk zu den Eigenheiten des Exports. Der größte Teil davon steht in keiner Dokumentation und wurde bei der Migration eines gewachsenen Bestands aufgedeckt.

Wer nur ein Dokument aus diesem Repository liest, sollte es dieses sein. Fast jeder der hier beschriebenen Punkte kann zu stiller Fehlzuordnung führen — zu Dokumenten am falschen Platz, zu Rechten an der falschen Stelle, zu verlorenen Versionen.


Aufbau eines Exports

Das ecoDMS-Exportwerkzeug erzeugt ein ZIP mit dieser Struktur:

offline_export/
├── archive/
│   ├── export.data                 SQLite-Datenbank mit Metadaten
│   ├── export.xml                  dieselben Daten plus Historie
│   ├── lucene/                     Suchindex des Offline-Clients
│   ├── ecodms_docid_0010325.pdf              Kopfversion als PDF/A
│   ├── ecodms_docid_0010325_revision_0001.odt   Original je Version
│   └── …
└── (Programmdateien des Offline-Clients)

Die Dateien liegen im Dateisystem, nicht in der Datenbank. Die BLOB-Spalten data, pdf und ocrtext in export.data sind durchgehend NULL. Das ist gut so — eine SQLite mit dem gesamten Dateibestand wäre unhandlich.


Fallstrick 1: docid gegen docs_id

Der wichtigste Punkt des ganzen Dokuments.

ecoDMS führt zwei Nummern, die leicht verwechselt werden:

Nummer Bedeutung Wo
docid das Dokument klassifizierung.docid, ecosimsversions.docid
docs_id die Klassifizierungsinstanz docs.id

Bei niedrigen Nummern stimmen beide zufällig überein. Bei höheren laufen sie auseinander:

docs.id 718  → docid 697
docs.id 2022 → docid 1960

Ein Join über die falsche Spalte liefert deshalb bei alten Dokumenten korrekte Ergebnisse und bei neuen falsche. Das fällt beim Testen mit wenigen Datensätzen nicht auf.

Verschärfung: falsch benannte Spalten

Zwei Tabellen haben eine Spalte namens docid, die in Wahrheit die docs_id enthält:

CREATE TABLE docrollen_hist(docid bigint, role varchar, doc_right char(1), revision varchar);
--                          ^^^^^ enthält docs_id
CREATE TABLE econotice(id bigint, text vcharacter, tdate timestamp, nid vcharacter, username varchar);
--                                                                  ^^^ enthält docs_id

Der korrekte Join ist in beiden Fällen … = docs.id.

So prüft man es

Nicht an Datensätzen mit niedrigen Nummern — dort sind beide Werte gleich. Such gezielt Fälle mit Abweichung und prüf inhaltlich, welche Auflösung zum Datensatz passt:

SELECT n.id, n.nid,
       (SELECT docid FROM docs WHERE id = CAST(n.nid AS INTEGER)) AS als_docs_id,
       (SELECT id    FROM docs WHERE docid = CAST(n.nid AS INTEGER)) AS als_docid
FROM econotice n
WHERE als_docs_id IS NOT als_docid;

Dann die Bemerkung der beiden Kandidaten ansehen. Eine Notiz „an Herrn X gesendet" gehört zum Dienstplan, nicht zur Zinsmitteilung der Bank.


Fallstrick 2: Mehrfachklassifizierung

Ein Dokument kann in ecoDMS mehrfach abgelegt sein — dieselbe Datei in zwei Ordnern, mit je eigener Klassifizierung.

docs.id 10829 (kid 22325) → Haushalt/Kleingeräte,  Zeitraum leer
docs.id 10834 (kid 22540) → Steuer/ESt/Anlage N,   Zeitraum 2025
                            beide: docid 10347

Daraus folgt: Eine docid entspricht nicht einer Zeile in docs. Wer nach docs_id gruppiert, legt das Dokument zweimal an.

Paperless bildet das sogar besser ab, weil Tags many-to-many sind: Aus zwei Ablageorten wird ein Dokument mit den Tags beider Pfade. Man braucht aber eine Zusammenführungsregel für die skalaren Felder — bewährt hat sich: der jüngste ctimestamp gewinnt, Ordner werden vereinigt, Abweichungen bei Titel, Dokumentart, Status und Datum werden protokolliert.

Häufigkeit im Testbestand: eines von vier Dokumenten einer Stichprobe. Nicht als Randfall behandeln.


Fallstrick 3: Revision ist nicht Version

Zwei völlig unabhängige Zähler:

Begriff Wo Zählt
klassifizierung.revision 1.0, 1.1, 1.2 Klassifizierungsänderungen
ecosimsversions.version 1, 2, 3 Dateiversionen

Beispiele aus einem echten Bestand:

docid Dateiversionen Revision
10325 4 1.3 (= 4. Revision)
10422 3 1.1 (= 2. Revision)
11072 0 1.5 (= 6. Revision)
11141 1 1.2 (= 3. Revision)

Es gibt keine Umrechnung. Für die Versionskette in Paperless zählt ausschließlich ecosimsversions.version.

Und beachte docid 11072: null Dateiversionen. Solche Dokumente haben keine Zeile in ecosimsversions und müssen auf ecosimsarchive zurückfallen, sonst verschwinden sie stillschweigend.


Fallstrick 4: Immer das Original importieren

ecosimsversions führt zwei Dateinamen:

Spalte Inhalt
realname Originaldatei
pdfrealname PDF/A-Fassung

Immer realname verwenden. Der Unterschied ist nicht kosmetisch:

  • Bei ZUGFeRD enthält das Original die eingebettete XML. Eine neu gerenderte Fassung womöglich nicht.
  • Bei E-Mails enthält die .eml die Anhänge. Das gerenderte PDF nicht. In einem Testfall: 34,0 KB Original gegen 25,8 KB PDF — die Differenz war der Rechnungsanhang.

Bei reinen PDF-Dokumenten sind beide Spalten identisch, die Regel kostet dort also nichts.

Was fehlt

Bei Nicht-PDF-Originalen exportiert ecoDMS pro Version nur das Original, nicht die PDF/A-Fassung — obwohl pdfrealname sie referenziert. Die XML verweist dann auf Dateien, die im Archiv nicht existieren.

Für Paperless unkritisch, es erzeugt Archivdateien selbst. Aber ein Skript, das blind allen Referenzen folgt, läuft ins Leere.


Fallstrick 5: Rechte sind historisiert

docrollen_hist ist eine Historientabelle. Sie enthält alle jemals gesetzten Rechte, nicht nur die aktuellen:

docs_id 10829  ecoSIMSUSER      W  1.0
docs_id 10829  r_benutzer_a     W  1.1
docs_id 10829  r_benutzer_b     W  1.1

ecoSIMSUSER ist das globale Importrecht — jeder hat Zugriff. Es wird beim Import gesetzt und bei der ersten Klassifizierung durch die echten Berechtigten ersetzt.

Wer alle Zeilen übernimmt, öffnet jedes Dokument für alle.

Maßgeblich sind nur die Zeilen mit der höchsten in docrollen_hist vorhandenen Revision je docs_id. Nicht der Revisionswert aus klassifizierung — der kann höher sein, wenn zuletzt nur die Klassifizierung geändert wurde.

Revisionen richtig sortieren

revision ist ein String. Lexikografisch ist '1.10' kleiner als '1.9'. Bei einem Bestand mit mehr als neun Revisionen führt das zu falschen Ergebnissen. Als Tupel aus Integern parsen:

def revtuple(r):
    return tuple(int(p) for p in str(r).split("."))

Dokumente ohne Rechteeintrag

Viele Dokumente haben gar keine Zeile in docrollen_hist — in einer Stichprobe sechs von acht. Die Tabelle speichert offenbar nur ausdrücklich vergebene Berechtigungen.

Das ist der gefährlichste Punkt der ganzen Migration, denn:

In Paperless bedeutet „keine Rechte gesetzt" nicht gesperrt, sondern unbeschränkt.

Ein Dokument ohne Eigentümer und ohne Objektrechte ist für jeden angelegten Benutzer sichtbar. Die Migration muss deshalb jedem Dokument einen Eigentümer geben, auch wenn aus ecoDMS nichts überliefert ist.


Fallstrick 6: Base64 an unregelmäßigen Stellen

Manche Felder sind base64-kodiert, benachbarte nicht:

Kodiert Klartext
ecosimsversions.savedate ecosimsversions.realname
ecosimsversions.filename ecosimsversions.pdfrealname
ecosimsarchive.version ecosimsarchive.filename
econotice.text econotice.tdate
XML <date> XML letzte-änderung
ecodmsexporter.exp_query

Es gibt kein erkennbares System. Im Zweifel dekodieren und bei einem Fehlschlag den Originalwert nehmen.

Notiztexte

econotice.text ist base64-kodiertes Qt-HTML mit CSS-Block im Kopf. Zum Auspacken:

s = base64.b64decode(raw).decode("utf-8", "replace")
s = re.sub(r"<[^>]+>", " ", s)                       # Tags
s = html.unescape(re.sub(r"\s+", " ", s))            # Entities, Leerraum
s = re.sub(r"^\s*p,\s*li\s*\{[^}]*\}\s*", "", s)     # CSS-Rest

Der letzte Schritt ist nötig, weil der CSS-Block nach dem Entfernen der Tags als Text übrigbleibt: p, li { white-space: pre-wrap; }


Fallstrick 7: Ordnernamen enthalten Schrägstriche

Ordner heißen in der Praxis auch Telefon / Internet oder TV / Rundfunk. Wer den Ordnerpfad zu einem String verkettet und später am Schrägstrich zerlegt, zerschneidet solche Namen in Telefon und Internet.

Die Folge sind Tags mit Leerzeichen am Rand — und wenn das Zielsystem die beim Speichern entfernt, kollidieren sie mit einer echten Ebene gleichen Namens.

Ordnerpfade als Liste von Segmenten führen, nicht als verketteten String.


Wo die Wiedervorlage steckt

Nicht in dynattribute, sondern in klassifizierung.defdate. Es ist eine eingebaute Funktion, kein dynamisches Attribut. Passend dazu gibt es in status den Wert Wiedervorlage.

econotice ist nicht die Wiedervorlage, sondern die Notizfunktion.


Die Live-Datenbank ist etwas anderes

Wer die Klassifizierungsdaten in der laufenden ecoDMS-PostgreSQL sucht, sucht vergeblich. Dort gibt es weder klassifizierung noch docs — die inhaltliche Ebene erzeugt erst der Exporter aus dem Container-Speichersystem.

In der Live-Datenbank liegt nur die Verwaltungsebene: Archive, Dateien, Versionen, Benutzer, Rollen, Suchindex.

Das erklärt auch, warum Dokumentverknüpfungen nicht auffindbar sind. Vier unabhängige Prüfungen an einem produktiven Bestand:

  • keine Tabelle mit zwei Dokumentspalten
  • beim Anlegen einer Verknüpfung entsteht keine neue Zeile in irgendeiner Tabelle
  • die Partner-ID kommt in keiner Textspalte vor
  • ebenso in keiner Zahlenspalte außer den eigenen Datensätzen

Verknüpfungen sind damit weder im Export noch in der Datenbank greifbar. Sie müssen von Hand nachgetragen werden.


Der Exporter selbst

AND statt OR

Bei manueller Mehrfachauswahl kann der Exporter eine unerfüllbare Abfrage erzeugen:

docid = '11072' AND docid = '11094' AND docid = '11143' AND 

Das Ergebnis ist ein Export mit vollständigen Stammdaten und null Dokumenten, ohne Fehlermeldung. Die Abfrage steht base64-kodiert in ecodmsexporter.exp_query:

base64.b64decode(row["exp_query"]).decode()

Nach jedem Export prüfen. Und Exporte besser über ein Suchkriterium oder einen ID-Bereich definieren als über eine Liste einzeln markierter Dokumente.

Tabellen im Export

Tabelle Inhalt
docs id (= docs_id), docid, kid, archiv, trashed, attachments
klassifizierung kid, docid, mainfolder, folder, bemerkung, status, revision, docart, ctimestamp, cdate, defdate, docs_id, dyn_*
ecosimsversions docid, version, savedate, realname, pdfrealname, checksum, comment, fixiert
ecosimsarchive Kopfdokument je docid, Rückfall wenn keine Versionen
docrollen_hist docid (= docs_id!), role, doc_right, revision
econotice id, text (base64 Qt-HTML), tdate, nid (= docs_id!), username
systemordner oid, name, parentid — Hierarchie über parentid
documentenart, status Stammdaten, mit trashed-Kennzeichen
dynattribute Spaltenname → Anzeigename und Typ, mit deleted-Kennzeichen
dup_items, duplicatesindexer Duplikaterkennung, für die Migration irrelevant

attachments bedeutet etwas anderes

docs.attachments zählt eingebettete Anhänge, nicht separate Dateien:

  • bei ZUGFeRD die im PDF eingebettete XML
  • bei E-Mails die Anhänge innerhalb der .eml

Beide reisen mit dem Original mit. Es fehlt nichts — solange man realname importiert.

lucene ist wertlos

Der Ordner enthält den Suchindex des Offline-Clients. Gespeichert sind nur Klassifizierungsdaten, die ohnehin vorliegen. Der Dokumenttext existiert lediglich als kleingeschriebene Termliste im invertierten Index und ist nicht rekonstruierbar.

Schade — sonst hätte man sich das erneute OCR sparen können.


Prüfliste vor jedem Import

  • exp_query dekodiert und plausibel?
  • Anzahl Dokumente im Export gegen die Erwartung?
  • Alle in der XML referenzierten Dateien im Archiv vorhanden?
  • Rollen im Export vollständig in der Übersetzung abgebildet?
  • Vorkommende doc_right-Werte bekannt?
  • Dateitypen bekannt und von Paperless unterstützt?
  • Häufigkeit der Mehrfachklassifizierung geprüft?
  • Dokumente ohne Rechteeintrag — Regel festgelegt?

ecodms_extract.py beantwortet all das im Prüfbericht.