# 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 ```bash 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: ```json { "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: ```csv 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: ```bash 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](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 ```bash 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: ```bash 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](04-import.md).