# Import und Export Unter *Verwaltung → Import/Export* führen zwei Knöpfe hinein. Beide fragen zuerst nach dem **Dateiformat** (QIF oder CSV) und öffnen danach den passenden Dialog – der Import weiß dann, welche Dateien er annimmt, der Export, was er schreiben soll. | | QIF | CSV | |---|---|---| | Import | Klassen, Kategorien, Konten, Buchungen, Buchungsvorlagen | Buchungen samt Splitzeilen | | Export | Stammdaten und Buchungen | Buchungen samt Splitzeilen | Die Formate stehen an einer Stelle: `Dateiformat::ALLE`. Kommt ein drittes hinzu, ist das die einzige Liste, die es zu ergänzen gilt – Dialoge, Auswahlkarten und die Prüfung des Dateinamens hängen daran. ## QIF-Import Der Lexware FinanzManager exportiert erweitertes Quicken-QIF. Die `.QDF`-Datei ist verschlüsselt und damit keine Migrationsquelle – der QIF-Export ist der einzige Weg. Stand: Klassen, Kategorien, Konten (Phase 1) sowie Buchungen samt Splitzeilen und Umbuchungen (Phase 2) werden übernommen. Wertpapierbestände folgen in Phase 6, Buchungsvorlagen in Phase 7. ### Aufbau der Datei ``` !Option:MDY Datumsformat für die gesamte Datei !Type:Class Klassen !Type:Cat Kategorien !Option:AutoSwitch Beginn der Kontenliste !Account ein Konto je Eintrag !Clear:AutoSwitch Ende der Kontenliste !Account / !Type:… ab hier abwechselnd Konto und dessen Buchungen ``` Ein Eintrag endet mit `^`. Jede Zeile beginnt mit einem Feldbuchstaben, der Rest ist der Wert. Felder dürfen mehrfach auftreten (Splitzeilen, Adresszeilen), deshalb hält `QifParser` jeden Eintrag als Liste von Paaren und bietet `feld()`, `felder()` und `hatFeld()`. **Die Datei ist zustandsbehaftet.** Der Kontobezug einer Buchung steckt allein darin, welcher `!Account`-Abschnitt zuletzt kam – nach Sektionstypen gruppiert gelesen wäre er verloren. Der Parser liefert daher eine flache Liste in Dateireihenfolge, jeder Eintrag mit Sektionstyp und aktuellem Konto. Jeder Kontoeintrag setzt zugleich das aktuelle Konto. In der Kontenliste folgen mehrere aufeinander, ohne dass Buchungen dazwischenstehen; das schadet nicht und deckt zugleich die zweite Schreibweise ab, bei der ein einzelner `!Account` den Kontowechsel bedeutet. ### Felder der Stammdaten | Sektion | Feld | Bedeutung | |---|---|---| | `!Type:Class` | `N` / `D` | Name, Beschreibung | | `!Type:Cat` | `N` | vollständiger Pfad mit `:` | | | `D` | Beschreibung | | | `I` / `E` | Einnahme bzw. Ausgabe (Marker ohne Wert) | | | `T` | steuerrelevant (Marker ohne Wert) | | | `R` | Kennziffer des Steuerformulars | | `!Account` | `N` / `T` | Name, Kontotyp (siehe `docs/stammdaten.md`) | | | `D` | Beschreibung; `Kontonr. 12345` wird als Kontonummer übernommen | | | `L` | Kreditlimit im US-Format | **Kontengruppen fehlen im Export.** Ein `!Account`-Eintrag führt nur die vier Felder oben – kein Feld nennt die Gruppe, und auch die `QSD`-Datei enthält weder Gruppennamen noch Zuordnung. Der Import kann sie deshalb nicht anlegen: nach dem Einlesen stehen alle Konten unter „Nicht gruppiert“. Die Gruppen werden einmalig unter *Verwaltung → Kontengruppen* angelegt; dort lassen sich die Konten einer Gruppe in einem Zug ankreuzen. ### Fallstricke 1. **Zeichensatz ISO-8859-1, Zeilenenden CRLF.** `QifParser::nachUtf8()` prüft erst, ob schon UTF-8 vorliegt – eine zweite Konvertierung zerstörte die Umlaute. 2. **Beträge im US-Format**: `-1,241.04`. Das Komma ist Tausendertrenner. 3. **Datum laut Kopfzeile**, meist `MDY` mit zweistelligem Jahr: `D3.18.26` ist der 18.03.2026. Jahreszahlen ab 70 gelten als 19xx. 4. **Kategorie und Klasse in einer Zeile**: `L400 - Leben:B-Essen alleine/Privat`. 5. **Unterkategorien ohne genannte Elternkategorie.** Der Import legt fehlende Zwischenstufen selbst an (`Kategorie::findenOderAnlegenNachPfad()`). 6. **Konten stehen zweimal in der Datei** – einmal in der Kontenliste, einmal als Kopf ihrer Buchungen. Der zweite Treffer ist keine Dublette. 7. **Umbuchungen stehen nur einmal in der Datei** – in dem Konto, in dem sie erfasst wurden. Die Gegenseite ist allein durch `L[Zielkonto]` benannt und wird beim Import erzeugt; ohne sie fehlte dem Gegenkonto der Betrag. Auch Splitzeilen können ein Umbuchungsziel tragen (`S[Konto]`). Eine Buchung, die auf ihr eigenes Konto verweist, ist die Eröffnungsbuchung und bekommt keine Gegenseite. 8. **`C` ist der Abgleichstatus** (`X` = abgeglichen), kein Kennzeichen für vorausberechnete Buchungen – Wertpapierbuchungen führen es nie. Geplante Buchungen sortiert man über das Datum aus. 9. **Anfangssalden stehen nicht als eigenes Feld im Export.** Sie ergeben sich aus den Buchungen vor dem Stichtag, Gegenbuchungen eingerechnet. ### Arbeitsweise der Importeure Beide sind **idempotent**: Was es schon gibt, wird gezählt und übersprungen, nie doppelt angelegt. Ein zweiter Lauf derselben Datei ändert nichts. Der echte Lauf liegt jeweils in einer Transaktion – ein Abbruch mitten in der Kategorieliste hinterließe sonst einen halben Baum. `QifStammdatenImport` (Klassen, Kategorien, Konten): Der **Trockenlauf** schreibt gar nicht, sondern zählt mit einer Menge bereits bekannter Pfade mit; auch die Zwischenstufen, die der Import selbst anlegen würde, erscheinen in der Vorschau. `QifBuchungsImport` arbeitet mit einem **Stichtag**: * Buchungen **vor** dem Stichtag werden nicht gespeichert, sondern je Konto zum Anfangssaldo aufsummiert – einschließlich der Gegenbuchungen aus Umbuchungen. Die Salden stimmen dadurch exakt, ohne die ganze Historie mitzuschleppen. * Buchungen **im Zeitraum** kommen einzeln in die Datenbank, mit Splitzeilen, Klasse, Abgleichstatus und Adresszeilen. * Buchungen **nach dem Enddatum** (Vorgabe: heute) sind Vorausberechnungen des FinanzManagers und bleiben draußen. * **Wertpapiergeschäfte** schlagen vorerst nur mit ihrem Geldfluss auf dem Verrechnungskonto zu Buche (`L|[Konto]` und `$`). Ein Betrag auf dem Depot ohne Gegenwert würde die Summe der Kontostände verfälschen; Bestände folgen in Phase 6. ### Nur einzelne Konten übernehmen Der Import lässt sich auf einzelne Konten begrenzen (Option `konten`, im Dialog eine Ankreuzliste, auf der Kommandozeile `--konto=NAME`). Drei Dinge sind dabei entschieden: 1. **Maßgeblich ist jedes berührte Konto, nicht nur der `!Account`-Abschnitt.** Eine Umbuchung steht nur einmal in der Datei, beim erfassenden Konto. Zählte allein dieser Abschnitt, fehlte dem gewählten Konto die Gegenseite. Deshalb kommt ein Eintrag mit, sobald sein Konto, ein Umbuchungsziel aus `L` oder `S` oder – beim Wertpapiergeschäft – das Verrechnungskonto in der Auswahl steht. Umgekehrt heißt das: Wer nur das Zielkonto wählt, bekommt die Buchung auch auf dem erfassenden Konto zu sehen; sie gehört dort hin und lässt sich nicht halbieren. 2. **Anfangssalden werden weiterhin über die ganze Datei gerechnet, aber nur für die gewählten Konten geschrieben.** Der Saldo eines Kontos steckt auch in Buchungen, die anderswo erfasst wurden – ohne den vollen Durchlauf wäre er falsch. Und ein Teilimport darf die Salden der übrigen Konten nicht überschreiben. 3. **Die Fingerabdrücke entstehen vorab in einem eigenen Durchgang.** Der laufende Zähler für gleiche Fingerabdrücke (siehe unten) hinge sonst an der Auswahl: Dieselbe Buchung bekäme im Teilimport einen anderen Hash als im Vollimport und käme ein zweites Mal in die Datenbank. So lassen sich die Konten nacheinander einlesen, ohne dass die Umbuchungen zwischen ihnen doppelt entstehen. Die auswählbaren Konten liefert `QifBuchungsImport::kontenDerDatei()` – erst nach dem Lesen steht fest, was die Datei führt. Enthalten sind auch Konten, die selbst keine Buchung tragen und nur Umbuchungsziel sind; sonst ließe sich genau deren Gegenseite nicht übernehmen. Die Dublettenerkennung nutzt einen Fingerabdruck aus Konto, Datum, Betrag, Nummer und Text. Weil es echte Doppelbuchungen am selben Tag gibt (zweimal dasselbe im selben Laden), zählt ein laufender Index gleiche Fingerabdrücke durch – stabil über Läufe hinweg, weil die Datei immer dieselbe Reihenfolge hat. ### Bedienung Im Dialog unter *Verwaltung → Import/Export → Import → QIF*: Datei hochladen, Trockenlauf, dann übernehmen. Hochgeladene Dateien bleiben im Importverzeichnis (`data/import`), damit derselbe Export erst geprüft und dann übernommen werden kann; das [Housekeeping](#housekeeping) räumt sie später weg. Nach dem Trockenlauf steht unter *Konten* die Ankreuzliste der Konten, die in der Datei vorkommen – vorher gibt es nichts zu wählen, weil die Datei noch nicht gelesen wurde. Von dort führen zwei Wege weiter: ein erneuter Trockenlauf mit der Auswahl oder das Übernehmen. Ohne Häkchen zählt die ganze Datei. Auf der Kommandozeile: ```bash docker compose exec -u www-data php php scripts/qif-import.php data/import/Export.QIF docker compose exec -u www-data php php scripts/qif-import.php --uebernehmen data/import/Export.QIF ``` Ohne `--uebernehmen` läuft nur der Trockenlauf. `--benutzer=NAME` wählt den Mandanten, sonst gilt der erste aktive Benutzer. `--stichtag=` und `--bis=` überschreiben die Vorgaben; ohne Angabe gilt der Stichtag aus den Einstellungen (Reiter *Import/Export*) und als Enddatum der heutige Tag. `--nur-stammdaten` lässt die Buchungen aus. ```bash docker compose exec -u www-data php php scripts/qif-import.php --konten data/import/Export.QIF docker compose exec -u www-data php php scripts/qif-import.php \ --konto="DKB Girokonto" --konto="Tagesgeld" --uebernehmen data/import/Export.QIF ``` `--konten` nennt die Konten der Datei und beendet sich, ohne etwas anzufassen. `--konto=` grenzt den Import ein und ist mehrfach angebbar; ein Name, den die Datei nicht führt, bricht ab – sonst liefe der Import durch und legte nichts an. ### Prüfung gegen den echten Export Der Vollexport (8780 Buchungen, 2017–2026) ergibt mit Stichtag 01.01.2026: ``` Klassen 17 angelegt Kategorien 105 angelegt Konten 37 angelegt, 36 vorhanden (die Zweitnennung je Konto) Buchungen 833 angelegt vor dem Stichtag verrechnet: 7946 Buchungen auf 35 Konten nach dem Enddatum verworfen: 1 vorausberechnete Buchung darunter Wertpapiergeschäfte: 16 (nur Geldfluss) ``` Der Härtetest sind die Salden: Acht Konten, deren Werte aus der Kontenübersicht des FinanzManagers vorliegen, stimmen danach **auf den Cent** – von „Whg Besigheim“ (210.601,99 €) bis „Auslagen Eltern Wohnungskauf“ (−22.410,94 €). ## CSV-Import Das Gegenstück zum CSV-Export: Buchungen samt Splitzeilen, sonst nichts. Konten, Kontengruppen und Buchungsvorlagen bleiben dem QIF vorbehalten. **Die Kopfzeile bestimmt die Spalten, nicht ihre Reihenfolge.** Eine Datei aus einem anderen Programm darf sie umsortieren oder eigene ergänzen; verglichen wird kleingeschrieben und ohne Umlaute (`CsvTabelle::schluessel()`), „Empfänger“ und „empfaenger“ meinen dasselbe. Unverzichtbar sind `Datum`, `Konto` und `Betrag` – fehlt eine davon, bricht der Lauf ab, statt eine halbe Datei einzulesen. | Spalte | Inhalt | |---|---| | `Art` | `Buchung` oder `Splitzeile` | | `Datum` / `Valuta` | `TT.MM.JJJJ` oder `JJJJ-MM-TT` | | `Konto` | Name eines vorhandenen Kontos | | `Nummer`, `Empfänger`, `Buchungstext` | wie im Kontoblatt; bei Splitzeilen ist `Buchungstext` die Notiz | | `Kategorie` | vollständiger Pfad mit `:` | | `Klasse` | Name | | `Gegenkonto` | statt einer Kategorie: macht die Zeile zur Umbuchung | | `Betrag` | deutsch (`-84,00`) oder englisch (`-84.00`) | | `Abgeglichen` | `ja`/`nein` | Entschieden ist dabei: 1. **Splitzeilen folgen unmittelbar auf ihre Buchung** und tragen in `Art` das Wort `Splitzeile`. Alles bis zur nächsten Buchungszeile gehört dazu. Eine Splitzeile ohne Buchung davor ist ein Fehler und wird gemeldet – stillschweigend fallen zu lassen wäre ein unbemerkter Verlust. Die Beträge der Zeilen müssen den Buchungsbetrag ergeben; das prüft `Buchung::anlegen()`. 2. **Kategorien und Klassen legt der Import bei Bedarf an, Konten nicht.** Eine Kategorie ist ein Pfad und mehr nicht; ein Konto trägt Kontoart, Bankverbindung und Anfangssaldo, und nichts davon steht in der Zeile. Ein unbekanntes Konto ist deshalb ein Fehler. 3. **Dieselbe Dublettenerkennung wie beim QIF**: Fingerabdruck aus Konto, Datum, Betrag, Nummer und Text, mit laufendem Index für echte Doppelbuchungen am selben Tag. Ein zweiter Lauf derselben Datei ändert nichts. 4. **Zahlen und Trenner werden erkannt, nicht vorausgesetzt.** `CsvTabelle` sieht auf der Kopfzeile nach, ob Semikolon oder Komma trennt, und liest Beträge in beiden Schreibweisen: Maßgeblich ist, welches Zeichen zuletzt steht – es trennt die Nachkommastellen, das andere ist Tausendertrenner. Gelesen wird über einen Speicherstrom und `fgetcsv()`, damit ein maskiertes Feld auch einen Zeilenumbruch enthalten darf. ## Export *Verwaltung → Import/Export → Export*, dann das Format, dann der Filter: **Zeitraum, Konto, Kategorie und Klasse**. Ohne Häkchen zählt jeweils alles. Über dem Formular steht, wie viele Buchungen der Filter trifft – sonst lädt man eine leere Datei herunter und merkt es erst in der Tabellenkalkulation. Zwei Eigenheiten des Datenmodells schlagen auf den Filter durch: * **Der Kontofilter greift auf beide Seiten.** Eine Umbuchung ist EINE Buchung mit `gegenkonto_id` und steht nur beim erfassenden Konto. Zählte allein dieses, fehlte dem Auszug des Gegenkontos seine einzige Bewegung. * **Kategorie und Klasse zählen auch in den Splitzeilen.** Sonst fiele jede Splitbuchung aus dem Filter, obwohl sie die gesuchte Kategorie führt. * **Unter den Kategorien stehen die aktiven Umbuchungskonten** (#124, `gegenkonten[]`). Ein gewähltes Konto nimmt jede Buchung oder Splitzeile mit diesem Gegenkonto mit; Kategorien und Umbuchungskonten verbinden sich als „oder“, wie in einer Auswahlliste. Die Kontenliste der Datei enthält zusätzlich jedes Konto, das nur Umbuchungsziel ist. Ohne seinen Eintrag bezöge sich die Datei auf ein Konto, das der Import nicht kennt, und die Umbuchung ginge verloren. **Die Datei wird nicht vorgehalten.** Sie entsteht im Speicher und geht mit `Content-Disposition: attachment` unmittelbar an den Browser; ein Exportverzeichnis gibt es bewusst nicht. Ein Auszug der eigenen Finanzen, der irgendwo im Dateisystem liegen bleibt, ist eine Altlast, die niemand aufräumt. Das Filterformular sendet als **POST**. Bei vielen angehakten Kategorien wächst eine GET-Adresse über das, was nginx annimmt – die Antwort wäre dann „414 Request-URI Too Large“ statt der Datei. Die GET-Route bleibt für kurze Adressen bestehen, die man sich merken mag. ### QIF-Export Derselbe Dialekt, den der Import liest: erst `!Type:Class`, dann `!Type:Cat`, dann die Kontenliste zwischen `!Option:AutoSwitch` und `!Clear:AutoSwitch`, danach je Konto ein `!Account`-Kopf und dessen Buchungen. Datumsformat ist `DMY` mit vierstelligem Jahr (`!Option:DMY`), Beträge stehen im US-Format. Klassen und Kategorien kommen **vollständig** mit, auch wenn der Filter enger ist: Ohne sie ließe sich die Datei auf einer leeren Installation nicht einspielen. Die Konten dagegen nur, soweit der Filter sie zulässt – sonst wäre es kein Auszug mehr. Wertpapiergeschäfte (`!Type:Invst`) stehen nicht darin; sie hängen am Depot und werden über die Wertpapierverwaltung gepflegt. ### CSV-Export Deutsches Tabellenkalkulations-CSV: Semikolon als Trenner, Komma als Dezimalzeichen, kein Tausenderpunkt (der macht aus der Zahl einen Text), CRLF am Zeilenende und ein BOM vorweg – sonst zeigen Excel und LibreOffice beim Doppelklick „Übrige“ statt „Übrige“. Die Spalten sind die des CSV-Imports; dieselbe Datei liest er wieder ein. `CsvTabelle` hält das Format an einer Stelle; `BerichtExport` (Auswertungen) nutzt dieselben Zeilen- und Zahlenregeln. ## Housekeeping Exportdateien entstehen im Speicher und hinterlassen nichts. Die hochgeladenen Importdateien bleiben dagegen absichtlich liegen – erst Trockenlauf, dann Übernahme, ohne zweiten Upload. Genau dadurch sammeln sich im Importverzeichnis mit der Zeit Kontoauszüge an, die niemand mehr braucht. `Housekeeping` löscht deshalb, was älter ist als die eingestellte Frist (*Einstellungen → Import/Export*, Vorgabe 14 Tage; `0` schaltet es ab). Überwacht werden das Importverzeichnis und `data/tmp`, der Platz für Arbeitsdateien; gelöscht werden nur gewöhnliche Dateien unmittelbar darin – ein Symlink zeigte womöglich aus dem überwachten Bereich heraus. Angestoßen wird der Lauf **beiläufig beim Öffnen von Import und Export**, höchstens einmal am Tag (`housekeeping_letzter_lauf`). Ein Fehler dabei reißt die Seite nicht mit: Wer den Import öffnet, will importieren, nicht eine Fehlermeldung übers Aufräumen lesen. Für Installationen, die wochenlang nicht geöffnet werden, gibt es denselben Lauf als Skript: ```bash docker compose exec -u www-data php php scripts/housekeeping.php --probe docker compose exec -u www-data php php scripts/housekeeping.php --still ``` `--probe` zählt nur auf, was wegfiele.