diff --git a/Changes.md b/Changes.md index c67a256..44684a7 100644 --- a/Changes.md +++ b/Changes.md @@ -1,5 +1,19 @@ # Änderungen +## Regel-Testlauf prüft gegen Buchungen ohne Kategorie (27.09.2026) + +Der Knopf „Gegen Buchungen ohne Kategorie prüfen“ unter Verwaltung → Regeln lief bisher +gegen noch nicht abgeglichene Umsätze – das beantwortet aber eine andere Frage als die, +die beim Anlegen oder Ändern einer Regel interessiert: Wie viele bereits vorhandene +Lücken würde diese Regel schließen? + +- Neu: `Buchung::ohneKategorie()` liefert Buchungen ganz ohne Kategorie – weder direkt + (`kategorie_id`) noch in einer Splitzeile –, ausgenommen Umbuchungen (die haben per + Definition ein Gegenkonto statt einer Kategorie). +- `BankingController::regelnPruefen()` testet die aktiven Regeln jetzt gegen diese Liste + statt gegen `Bankumsatz::liste(['status' => 'offen'])`. +- Test in `tests/16_BankingTest.php` (`buchungOhneKategorieWirdGefunden`). + ## `update.sh` zieht aus dem Fork (25.09.2026) - Das Update-Skript holt den Code ausdrücklich per `git pull --ff-only origin main`. Vorher diff --git a/src/Controllers/BankingController.php b/src/Controllers/BankingController.php index a236fcc..66cbb66 100644 --- a/src/Controllers/BankingController.php +++ b/src/Controllers/BankingController.php @@ -12,6 +12,7 @@ use Finanzmanager\Models\Bankinstitut; use Finanzmanager\Models\Bankprotokoll; use Finanzmanager\Models\Bankumsatz; use Finanzmanager\Models\Bankzugang; +use Finanzmanager\Models\Buchung; use Finanzmanager\Models\Kategorie; use Finanzmanager\Models\Klasse; use Finanzmanager\Models\Konto; @@ -821,8 +822,11 @@ class BankingController extends BaseController } /** - * Wendet die Regeln auf die offenen Umsätze an, ohne sie zu übernehmen – - * damit lässt sich eine neue Regel ausprobieren, bevor sie bucht. + * Wendet die Regeln auf Buchungen ohne Kategorie an, ohne sie zu ändern – + * damit lässt sich eine neue oder geänderte Regel ausprobieren, bevor sie + * bucht. Maßgeblich ist die Kategorie, nicht der Abgleichsstatus: Eine + * bereits übernommene Buchung ohne Kategorie soll die Regel genauso + * finden wie ein frisch abgerufener Umsatz. */ public function regelnPruefen(): void { @@ -832,15 +836,20 @@ class BankingController extends BaseController $treffer = 0; $gesamt = 0; - foreach (Bankumsatz::liste(['status' => 'offen']) as $umsatz) { + foreach (Buchung::ohneKategorie() as $buchung) { $gesamt++; + $umsatz = [ + 'empfaenger' => $buchung['empfaenger'], + 'verwendungszweck' => $buchung['buchungstext'], + 'betrag' => $buchung['betrag'], + ]; if (AbgleichRegel::passendeRegel($umsatz, $regeln) !== null) { $treffer++; } } View::redirect('/banking/regeln', sprintf( - 'Von %d offenen Umsätzen treffen die Regeln auf %d.', + 'Von %d Buchungen ohne Kategorie treffen die Regeln auf %d.', $gesamt, $treffer )); diff --git a/src/Models/Buchung.php b/src/Models/Buchung.php index 0f4e3c3..89a4054 100644 --- a/src/Models/Buchung.php +++ b/src/Models/Buchung.php @@ -487,6 +487,35 @@ class Buchung return $anzahlen; } + /** + * Buchungen ganz ohne Kategorie – weder direkt noch in einer Splitzeile. + * Grundlage für den Regel-Testlauf: Er soll zeigen, wie viele Lücken eine + * (neue oder geänderte) Regel schließen würde, nicht wie viele Umsätze + * noch nicht abgeglichen sind – das sind zwei verschiedene Fragen. + * + * Kein Gegenkonto und nicht gesplittet ohne jede kategorisierte Zeile: + * dieselbe Definition wie bei {@see unkategorisierteJeKonto()}, nur als + * Liste statt als Zählung je Konto. + * + * @return list> + */ + public static function ohneKategorie(): array + { + return Database::fetchAll( + 'SELECT b.* FROM buchungen b + WHERE b.benutzer_id = ? AND b.gegenkonto_id IS NULL + AND ( + (b.gesplittet = 0 AND b.kategorie_id IS NULL) + OR (b.gesplittet = 1 AND NOT EXISTS ( + SELECT 1 FROM buchung_splits s + WHERE s.buchung_id = b.id AND s.kategorie_id IS NOT NULL + )) + ) + ORDER BY b.datum', + [Mandant::id()] + ); + } + /** * Die jüngsten Buchungen zu einem Empfängernamen, kontenübergreifend – für * die Adressverwaltung: Wer sich eine Adresse ansieht, will wissen, was diff --git a/src/Views/banking/regeln.php b/src/Views/banking/regeln.php index 98a8938..cf9930a 100644 --- a/src/Views/banking/regeln.php +++ b/src/Views/banking/regeln.php @@ -29,7 +29,7 @@ include __DIR__ . '/../layout/kopf.php';
- +
Zum Abgleich
diff --git a/tests/16_BankingTest.php b/tests/16_BankingTest.php index fdb4627..4c01552 100644 --- a/tests/16_BankingTest.php +++ b/tests/16_BankingTest.php @@ -48,6 +48,7 @@ class BankingTest extends TestCase $this->abgleichFindetVorhandeneBuchung(); $this->uebernehmenErzeugtEineBuchung(); $this->regelmitSplitsErzeugtEineSplitbuchung(); + $this->buchungOhneKategorieWirdGefunden(); $this->verwerfenUndZuruecksetzen(); $this->abrufprotokollHaeltDenVerlauf(); } @@ -777,6 +778,52 @@ class BankingTest extends TestCase AbgleichRegel::loeschen($regelId); } + /** + * Der Regel-Testlauf soll Lücken zeigen, die eine Regel schließen könnte – + * nicht den Abgleichsstatus. Deshalb zählt „ohne Kategorie“ unabhängig + * davon, ob eine Buchung aus einem Umsatz entstand oder manuell erfasst + * wurde, und eine Splitbuchung zählt nur, wenn wirklich keine Zeile eine + * Kategorie trägt. + */ + private function buchungOhneKategorieWirdGefunden(): void + { + $ohneKategorie = Buchung::anlegen([ + 'konto_id' => $this->giro, 'datum' => '2026-08-10', 'betrag' => -19.99, + 'empfaenger' => 'Unbekannter Laden', + ]); + $mitKategorie = Buchung::anlegen([ + 'konto_id' => $this->giro, 'datum' => '2026-08-10', 'betrag' => -30.00, + 'empfaenger' => 'Bekannter Laden', 'kategorie_id' => $this->strom, + ]); + $splitOhneKategorie = Buchung::anlegen( + ['konto_id' => $this->giro, 'datum' => '2026-08-10', 'betrag' => -50.00, 'empfaenger' => 'Split ohne'], + [ + ['kategorie_id' => null, 'betrag' => -30.00], + ['kategorie_id' => null, 'betrag' => -20.00], + ] + ); + $splitMitKategorie = Buchung::anlegen( + ['konto_id' => $this->giro, 'datum' => '2026-08-10', 'betrag' => -50.00, 'empfaenger' => 'Split mit'], + [ + ['kategorie_id' => $this->strom, 'betrag' => -30.00], + ['kategorie_id' => null, 'betrag' => -20.00], + ] + ); + $umbuchung = Buchung::anlegen([ + 'konto_id' => $this->giro, 'datum' => '2026-08-10', 'betrag' => -40.00, + 'empfaenger' => 'Umbuchung', 'gegenkonto_id' => $this->sparkonto, + ]); + + $ids = array_column(Buchung::ohneKategorie(), 'id'); + $ids = array_map('intval', $ids); + + $this->assert(in_array($ohneKategorie, $ids, true), 'Buchung ohne jede Kategorie taucht auf'); + $this->assert(in_array($splitOhneKategorie, $ids, true), 'Splitbuchung ohne kategorisierte Zeile taucht auf'); + $this->assert(!in_array($mitKategorie, $ids, true), 'Kategorisierte Buchung fehlt in der Liste'); + $this->assert(!in_array($splitMitKategorie, $ids, true), 'Teilweise kategorisierte Splitbuchung fehlt in der Liste'); + $this->assert(!in_array($umbuchung, $ids, true), 'Eine Umbuchung braucht keine Kategorie'); + } + private function verwerfenUndZuruecksetzen(): void { $id = $this->umsatzAnlegen([