Unter den Kategorien stehen überall die aktiven Umbuchungskonten, je Kontengruppe gruppiert und in deren Reihenfolge (Konto::umbuchungsgruppen()): Regeln samt Splitzeilen, Umsatzabgleich, Buchungsvorlagen, Regelmäßige Buchungen, Zahlungsverkehr, Verträge sowie die Filter von Buchungsbericht und Export. Die Masken nutzen dafür die gemeinsame Liste Kategorie / Gegenkonto der Buchungsmaske; das getrennte Gegenkonto-Feld der Regel entfällt. Regel-Splitzeilen können auf ein Umbuchungskonto zeigen. Zahlungsaufträge und Verträge bekommen ein Gegenkonto (Migration 030). Ein schon gewähltes inaktives Konto bleibt sichtbar, damit keine Zuordnung verloren geht. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
326 lines
17 KiB
Markdown
326 lines
17 KiB
Markdown
# 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.
|