*/ private array $meldungen = []; public function __construct(QifParser $parser) { $this->parser = $parser; } public static function ausDatei(string $pfad): self { return new self(QifParser::ausDatei($pfad)); } /** * @return array{klassen: array, kategorien: array, * konten: array, meldungen: list, * trockenlauf: bool} */ public function ausfuehren(bool $trockenlauf = false): array { $this->meldungen = []; $lauf = function () use ($trockenlauf): array { return [ 'klassen' => $this->klassen($trockenlauf), 'kategorien' => $this->kategorien($trockenlauf), 'konten' => $this->konten($trockenlauf), 'trockenlauf' => $trockenlauf, ]; }; // Beim echten Lauf gilt alles oder nichts – ein Abbruch mitten in der // Kategorieliste hinterließe sonst einen halben Baum. $ergebnis = $trockenlauf ? $lauf() : Database::transaktion($lauf); $ergebnis['meldungen'] = $this->meldungen; return $ergebnis; } /** @return array */ private function klassen(bool $trockenlauf): array { $zaehler = ['neu' => 0, 'vorhanden' => 0, 'fehler' => 0]; $bekannte = array_column(Klasse::alle(), 'name'); foreach ($this->parser->eintraege(QifParser::TYP_KLASSE) as $eintrag) { $name = trim((string)QifParser::feld($eintrag, 'N')); if ($name === '') { $zaehler['fehler']++; $this->melde('fehler', 'Klasse ohne Namen übersprungen.'); continue; } if (in_array($name, $bekannte, true)) { $zaehler['vorhanden']++; continue; } try { if (!$trockenlauf) { Klasse::anlegen($name, (string)QifParser::feld($eintrag, 'D')); } $bekannte[] = $name; $zaehler['neu']++; } catch (\Throwable $e) { $zaehler['fehler']++; $this->melde('fehler', 'Klasse „' . $name . '“: ' . $e->getMessage()); } } return $zaehler; } /** @return array */ private function kategorien(bool $trockenlauf): array { $zaehler = ['neu' => 0, 'vorhanden' => 0, 'fehler' => 0]; // Im Trockenlauf werden auch die Elternstufen mitgezählt, die der // Import selbst anlegen würde – sonst stimmte die Vorschau nicht. $bekannte = array_flip(array_column(Kategorie::alle(), 'pfad')); foreach ($this->parser->eintraege(QifParser::TYP_KATEGORIE) as $eintrag) { $pfad = Kategorie::normalisierePfad((string)QifParser::feld($eintrag, 'N')); if ($pfad === '') { $zaehler['fehler']++; $this->melde('fehler', 'Kategorie ohne Namen übersprungen.'); continue; } if (isset($bekannte[$pfad])) { $zaehler['vorhanden']++; continue; } $art = QifParser::hatFeld($eintrag, 'I') ? 'einnahme' : 'ausgabe'; $daten = [ 'beschreibung' => (string)QifParser::feld($eintrag, 'D'), 'steuerrelevant' => QifParser::hatFeld($eintrag, 'T'), 'steuerformular' => (string)QifParser::feld($eintrag, 'R'), ]; try { if (!$trockenlauf) { Kategorie::findenOderAnlegenNachPfad($pfad, $art, $daten); } // Fehlende Zwischenstufen zählen als eigene neue Kategorien. $teile = explode(Kategorie::TRENNER, $pfad); $bisher = ''; foreach ($teile as $teil) { $bisher = $bisher === '' ? $teil : $bisher . Kategorie::TRENNER . $teil; if (!isset($bekannte[$bisher])) { $bekannte[$bisher] = true; $zaehler['neu']++; } } } catch (\Throwable $e) { $zaehler['fehler']++; $this->melde('fehler', 'Kategorie „' . $pfad . '“: ' . $e->getMessage()); } } return $zaehler; } /** @return array */ private function konten(bool $trockenlauf): array { $zaehler = ['neu' => 0, 'vorhanden' => 0, 'fehler' => 0]; $bekannte = array_flip(array_column(Konto::alle(), 'name')); foreach ($this->parser->eintraege(QifParser::TYP_KONTO) as $eintrag) { $name = trim((string)QifParser::feld($eintrag, 'N')); if ($name === '') { $zaehler['fehler']++; $this->melde('fehler', 'Konto ohne Namen übersprungen.'); continue; } // In der Kontenliste steht jedes Konto einmal, danach leitet // derselbe Eintrag noch einmal die Buchungen ein – der zweite // Treffer ist keine Dublette, sondern derselbe Datensatz. if (isset($bekannte[$name])) { $zaehler['vorhanden']++; continue; } try { if (!$trockenlauf) { Konto::anlegen($this->kontoDaten($eintrag, $name)); } $bekannte[$name] = true; $zaehler['neu']++; } catch (\Throwable $e) { $zaehler['fehler']++; $this->melde('fehler', 'Konto „' . $name . '“: ' . $e->getMessage()); } } return $zaehler; } /** * @param array{zeilen: list} $eintrag * @return array */ private function kontoDaten(array $eintrag, string $name): array { $qifTyp = (string)QifParser::feld($eintrag, 'T'); $beschreibung = trim((string)QifParser::feld($eintrag, 'D')); $kontonummer = ''; if ($beschreibung !== '' && preg_match(self::KONTONUMMER_MUSTER, $beschreibung, $treffer) === 1) { $kontonummer = $treffer[1]; $beschreibung = ''; } $limit = QifParser::feld($eintrag, 'L'); return [ 'name' => $name, 'kontoart' => Konto::artAusQif($qifTyp), 'beschreibung' => $beschreibung, 'kontonummer' => $kontonummer, 'kreditlimit' => $limit === null ? null : QifParser::betrag($limit), // Anfangssalden stehen nicht im Export; sie werden nach dem Import // je Konto nachgetragen (PLAN.md, Kapitel 4). 'anfangssaldo' => 0, 'aktiv' => 1, ]; } private function melde(string $typ, string $text): void { $this->meldungen[] = ['typ' => $typ, 'text' => $text]; } /** Vorschau ohne Schreibzugriff. */ public function vorschau(): array { return $this->ausfuehren(true); } }