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

7.0 KiB

3. Extraktion und Prüfung

Stufe 1 liest eine Exporttranche und erzeugt ein normalisiertes JSON-Manifest, einen Prüfbericht und eine Ausschlussliste. Paperless wird dabei nicht berührt.

Die Trennung ist Absicht: Der Lauf ist offline, beliebig wiederholbar, und das Ergebnis lässt sich ansehen, bevor irgendetwas geschrieben wird.


Aufruf

python3 ecodms_extract.py /pfad/export1/archive \
        -t export1 \
        -o export1.json \
        --skipped-csv export1-nacharbeit.csv

Als Pfad das Verzeichnis angeben, in dem export.data und export.xml liegen — also archive, nicht das Tranchenverzeichnis darüber.


Der Prüfbericht

Tranche export1
  Dokumente              1963
  Uebersprungen          34  (.html 20, .zip 11, .indd 2, .dotx 1)
  Versionen              2462
  Mehrfachklassifiziert  87
  Notizen                81
  Rollen                 FFN, r_benutzer_a, r_benutzer_b, ecoSIMSUSER
  Rechtearten            R, W

  Hinweise (12):
    [nicht_unterstuetzt] 34
    [mehrfachklassifizierung] 87
    [ohne_rechte] 1204
    [nur_archiv] 143
    …

Diesen Bericht lesen, bevor importiert wird. Er beantwortet alle Fragen, die sonst mitten im Import auftauchen.

Was die Kategorien bedeuten

Kategorie Bedeutung Handlung
nicht_unterstuetzt Dateityp, den Paperless ablehnt Nacharbeit, Liste in der CSV
mehrfachklassifizierung Dokument in mehreren Ordnern wird zusammengeführt, Konflikte protokolliert
ohne_rechte keine Zeile in docrollen_hist Regel in rollen.yml festlegen
nur_archiv keine Versionszeilen, Rückfall auf ecosimsarchive normal, kein Handlungsbedarf
datei_fehlt Version verweist auf nicht vorhandene Datei Export unvollständig, prüfen
xml_referenz_ohne_datei XML verweist auf fehlende Datei bei Office-Originalen normal
geloeschte_stammdaten Dokumentart oder Status ist gelöscht markiert meist unkritisch
papierkorb Dokument ist im ecoDMS-Papierkorb wird übersprungen
notiz_ohne_bezug Notiz zeigt auf unbekannte docs_id in anderer Tranche, oder verwaist

Rollen prüfen

Die Zeile Rollen ist die wichtigste für den nächsten Schritt. Jede genannte Rolle muss in rollen.yml stehen — entweder mit einem Ziel oder ausdrücklich als type: null.

Stufe 2 bricht sonst ab, bevor die erste Datei hochgeht.


Das Manifest

Je Dokument entsteht ein Eintrag:

{
  "ecodms_docid": 10347,
  "tranche": "export1",
  "title": "Rechnung Beispiel GmbH",
  "created": "2025-07-29",
  "document_type": "Rechnung",
  "status": "Erledigt",
  "folders": ["Allgemeines / Haushalt / Kleingeräte",
              "Allgemeines / Steuer / ESt / Anlage N"],
  "folder_parts": [["Allgemeines","Haushalt","Kleingeräte"],
                   ["Allgemeines","Steuer","ESt","Anlage N"]],
  "custom_fields": {
    "Belegnummer":   {"value": "902500503118", "type": "String"},
    "Wiedervorlage": {"value": "2026-02-02",   "type": "Date"}
  },
  "versions": [
    {"version": 1, "file": "ecodms_docid_0010347_revision_0001.pdf",
     "saved_at": "2025-07-29 14:12:03.881", "checksum": "…",
     "source": "ecosimsversions"}
  ],
  "permissions": [{"role": "r_benutzer_a", "right": "W"}],
  "notes": [{"created": "2014-07-14 00:45:00", "text": "…", "user": ""}],
  "docs_ids": [10829, 10834],
  "conflicts": {}
}

folder_parts ist maßgeblich, folders nur für die Anzeige. Ordnernamen können Schrägstriche enthalten (Telefon / Internet) — ein späteres Zerlegen am Schrägstrich würde solche Namen zerschneiden.

conflicts wird bei Mehrfachklassifizierung gefüllt, wenn Titel, Dokumentart, Status, Datum oder Wiedervorlage zwischen den Instanzen abweichen. Dann gewinnt der jüngste ctimestamp, aber man sieht, dass etwas verworfen wurde.


Die Ausschlussliste

--skipped-csv schreibt alle Dokumente, deren Dateityp Paperless nicht konsumieren kann:

ecodms_docid,endungen,titel,erstellt,ordner,dateien
1247,.html,Webseite Beispiel,2019-03-12,Allgemeines | EDV,ecodms_docid_0001247_revision_0001.html
1533,.zip,Belege Sammlung,2020-11-04,Steuer,ecodms_docid_0001533_revision_0001.zip

Diese Dokumente sind nicht im Manifest. Sie scheitern deshalb nicht beim Import, sondern stehen sauber auf einer Liste.

Die Liste der akzeptierten Endungen steht als SUPPORTED_EXTENSIONS oben im Skript. Sie sollte gegen die Zielinstanz geprüft werden:

docker compose exec webserver python manage.py shell -c \
"from documents.parsers import get_supported_file_extensions as f; print(sorted(f()))"

Regeln, die Stufe 1 anwendet

Alle stammen aus der Analyse eines produktiven Bestands. Ausführlich in referenz-ecodms-exportformat.md.

  1. Nach docid gruppieren, nicht nach docs_id. Eine docid kann mehrere Klassifizierungsinstanzen haben.
  2. Ordner vereinigen, jüngster ctimestamp gewinnt bei skalaren Feldern.
  3. Immer realname verwenden, nie pdfrealname. Sonst gehen eingebettete ZUGFeRD-XML und E-Mail-Anhänge verloren.
  4. Ohne Versionszeilen auf ecosimsarchive zurückfallen. Sonst verschwinden solche Dokumente stillschweigend.
  5. docrollen_hist.docid enthält docs_id. Join über docs.id.
  6. Nur die höchste in docrollen_hist vorhandene Revision je docs_id gilt. Ältere enthalten noch das globale Importrecht.
  7. Revisionen als Zahlentupel sortieren. '1.10' ist lexikografisch kleiner als '1.9'.
  8. Base64 selektiv dekodieren. savedate und filename sind kodiert, realname und pdfrealname nicht.
  9. Wiedervorlage aus klassifizierung.defdate, nicht aus dynattribute.
  10. econotice.nid enthält docs_id, der Text ist base64-kodiertes Qt-HTML.

Wiederholbarkeit

Stufe 1 schreibt nichts außer den Ausgabedateien. Sie kann beliebig oft laufen — bei jeder Änderung an den Regeln, bei jedem neuen Export.

Nach einer Änderung am Skript alle Manifeste neu erzeugen, damit sie dasselbe Format haben.


Alle Tranchen auf einmal

for n in 1 2 3 4 5 6; do
  python3 ecodms_extract.py "export-daten/export$n/archive" \
          -t "export$n" -o "export$n.json" \
          --skipped-csv "export$n-nacharbeit.csv"
done

Danach eine Gesamtübersicht der Dateitypen:

python3 -c "
import json, glob, collections
v = collections.Counter(); d = collections.Counter()
for f in sorted(glob.glob('export*.json')):
    for doc in json.load(open(f))['documents']:
        exts = set()
        for ver in doc['versions']:
            e = '.' + ver['file'].rsplit('.',1)[-1].lower()
            v[e] += 1; exts.add(e)
        for e in exts: d[e] += 1
print(f\"{'Endung':10s} {'Versionen':>10s} {'Dokumente':>10s}\")
for k, n in v.most_common(): print(f'{k:10s} {n:10d} {d[k]:10d}')
"

Das ist die Zahl, die vor dem Import bekannt sein sollte — sonst stolpert man bei Tranche 4 über einen Dateityp, der in Tranche 1 nicht vorkam.


Weiter mit Import.