} */ public static function laufenLassen(bool $ohneKategorie): array { $regeln = AbgleichRegel::alle(true); $verbunden = 0; $uebernommen = 0; $offen = 0; $fehler = []; foreach (Bankumsatz::liste(['status' => 'offen']) as $umsatz) { // Ohne Konto gibt es kein Kontoblatt, in das die Buchung gehörte. if ((int)$umsatz['konto_id'] === 0) { $offen++; continue; } $vorschlag = Bankumsatz::vorschlag($umsatz, $regeln); if ($vorschlag['art'] === 'verbinden' && $vorschlag['buchung'] !== null) { // Ein unsicherer Treffer (mehrere Kandidaten ohne IBAN-Bestätigung) // bleibt für die Vorschlagsliste liegen. if (!$vorschlag['sicher']) { $offen++; continue; } try { Bankumsatz::verbinden((int)$umsatz['id'], (int)$vorschlag['buchung']['id']); $verbunden++; } catch (\Throwable $e) { $offen++; $fehler[] = sprintf( '%s, %s: %s', date('d.m.Y', strtotime((string)$umsatz['buchungstag'])), number_format((float)$umsatz['betrag'], 2, ',', '.') . ' €', $e->getMessage() ); } continue; } if ($vorschlag['zuordnung'] === null && !$ohneKategorie) { $offen++; continue; } try { Bankumsatz::uebernehmen((int)$umsatz['id'], $vorschlag['zuordnung'] ?? []); $uebernommen++; } catch (\Throwable $e) { // Ein einzelner Umsatz darf den Lauf nicht abbrechen: Ein // abgeschlossener Monat oder eine Buchung mit gleichem // Fingerabdruck betrifft nur ihn. Er bleibt offen und steht // weiter in der Vorschlagsliste. $offen++; $fehler[] = sprintf( '%s, %s: %s', date('d.m.Y', strtotime((string)$umsatz['buchungstag'])), number_format((float)$umsatz['betrag'], 2, ',', '.') . ' €', $e->getMessage() ); DebugLog::schreibe('abgleich', 'Umsatz ' . (int)$umsatz['id'] . ': ' . $e->getMessage()); } } // Umbuchungen, die beide Seiten schon als eigene Buchung haben. $zusammen = Bankumsatz::umbuchungenZusammenfuehren(); $verbunden += $zusammen['zusammengefuehrt']; $fehler = array_merge($fehler, $zusammen['fehler']); return [ 'verbunden' => $verbunden, 'uebernommen' => $uebernommen, 'offen' => $offen, 'fehler' => $fehler, ]; } /** * Der Lauf nach einem Abruf – nur, wenn er eingeschaltet ist. * * @return array{verbunden: int, uebernommen: int, offen: int, fehler: list}|null */ public static function automatisch(): ?array { if (!Einstellung::getBool('banking_automatisch_buchen', true)) { return null; } return self::laufenLassen(true); } /** * Eine Zeile für Meldung und Abrufprotokoll. * * @param array{verbunden: int, uebernommen: int, offen: int, fehler: list} $ergebnis */ public static function bericht(array $ergebnis): string { $text = sprintf( '%d gebucht, %d mit vorhandenen Buchungen verbunden, %d bleiben offen.', $ergebnis['uebernommen'], $ergebnis['verbunden'], $ergebnis['offen'] ); if ($ergebnis['fehler'] !== []) { $text .= ' Nicht gebucht: ' . implode('; ', array_slice($ergebnis['fehler'], 0, 3)); if (count($ergebnis['fehler']) > 3) { $text .= sprintf(' (und %d weitere)', count($ergebnis['fehler']) - 3); } } return $text; } /** * Trägt bei allen Buchungen ohne Kategorie die erste passende Regel nach – * der Testlauf unter „Regeln“ soll die gefundenen Lücken gleich schließen, * nicht nur zählen, wie viele Regeln träfen. * * Buchungen ohne Treffer bleiben unangetastet, ebenso ein abgeschlossener * Monat: Der einzelne Fehler landet im Bericht, der Lauf macht mit den * übrigen Buchungen weiter. * * @return array{gesamt: int, kategorisiert: int, fehler: list} */ public static function regelnAufBuchungenOhneKategorieAnwenden(): array { $regeln = AbgleichRegel::alle(true); $gesamt = 0; $kategorisiert = 0; $fehler = []; foreach (Buchung::ohneKategorie() as $buchung) { $gesamt++; $umsatz = [ 'empfaenger' => $buchung['empfaenger'], 'verwendungszweck' => $buchung['buchungstext'], 'betrag' => $buchung['betrag'], ]; $regel = AbgleichRegel::passendeRegel($umsatz, $regeln); if ($regel === null) { continue; } $zuordnung = AbgleichRegel::anwenden($regel, $umsatz); try { Buchung::speichern( (int)$buchung['id'], [ 'konto_id' => $buchung['konto_id'], 'datum' => $buchung['datum'], 'valuta' => $buchung['valuta'], 'betrag' => $buchung['betrag'], 'empfaenger' => $buchung['empfaenger'], 'buchungstext' => $buchung['buchungstext'], 'nummer' => $buchung['nummer'], 'abgeglichen' => $buchung['abgeglichen'], 'kategorie_id' => $zuordnung['kategorie_id'], 'klasse_id' => $zuordnung['klasse_id'], 'gegenkonto_id' => $zuordnung['gegenkonto_id'], ], $zuordnung['splits'] ); AbgleichRegel::zaehleTreffer((int)$regel['id']); $kategorisiert++; } catch (\Throwable $e) { $fehler[] = sprintf( '%s, %s: %s', date('d.m.Y', strtotime((string)$buchung['datum'])), number_format((float)$buchung['betrag'], 2, ',', '.') . ' €', $e->getMessage() ); DebugLog::schreibe('abgleich', 'Buchung ' . (int)$buchung['id'] . ': ' . $e->getMessage()); } } return ['gesamt' => $gesamt, 'kategorisiert' => $kategorisiert, 'fehler' => $fehler]; } /** * Eine Zeile für die Meldung nach dem Anwenden der Regeln. * * @param array{gesamt: int, kategorisiert: int, fehler: list} $ergebnis */ public static function berichtOhneKategorie(array $ergebnis): string { $text = sprintf( 'Von %d Buchungen ohne Kategorie wurden %d anhand der Regeln kategorisiert.', $ergebnis['gesamt'], $ergebnis['kategorisiert'] ); if ($ergebnis['fehler'] !== []) { $text .= ' Nicht kategorisiert: ' . implode('; ', array_slice($ergebnis['fehler'], 0, 3)); if (count($ergebnis['fehler']) > 3) { $text .= sprintf(' (und %d weitere)', count($ergebnis['fehler']) - 3); } } return $text; } }