Fix n.n.: Regel-Testlauf trägt Kategorien direkt nach statt nur zu zählen
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
2bf5bae3bd
commit
4bed769810
5 changed files with 173 additions and 35 deletions
20
Changes.md
20
Changes.md
|
|
@ -1,18 +1,22 @@
|
|||
# Änderungen
|
||||
|
||||
## Regel-Testlauf prüft gegen Buchungen ohne Kategorie (27.09.2026)
|
||||
## Regeln lassen sich auf Buchungen ohne Kategorie anwenden (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?
|
||||
Der Knopf unter Verwaltung → Regeln hieß „Gegen offene Umsätze prüfen“ und zählte nur,
|
||||
wie viele noch nicht abgeglichene Umsätze eine Regel träfe – das beantwortet weder, wo im
|
||||
Kontoblatt schon Lücken liegen, noch schließt es sie.
|
||||
|
||||
- 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`).
|
||||
- Neu: `Bankabgleich::regelnAufBuchungenOhneKategorieAnwenden()` trägt bei jeder Buchung
|
||||
ohne Kategorie die erste passende Regel nach (Kategorie, Klasse, Gegenkonto oder
|
||||
Split) – ohne Treffer bleibt die Buchung unverändert, ein abgeschlossener Monat oder
|
||||
sonstiger Fehler betrifft nur die eine Buchung und bricht den Lauf nicht ab.
|
||||
- Der Knopf „Regeln auf Buchungen ohne Kategorie anwenden“ (mit Rückfrage) ersetzt den
|
||||
reinen Testlauf; die Regel-Trefferzähler zählen mit.
|
||||
- Test in `tests/16_BankingTest.php` (`buchungOhneKategorieWirdGefunden`,
|
||||
`regelnKategorisierenBuchungenOhneKategorie`).
|
||||
|
||||
## `update.sh` zieht aus dem Fork (25.09.2026)
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ 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;
|
||||
|
|
@ -822,37 +821,18 @@ class BankingController extends BaseController
|
|||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Trägt bei allen Buchungen ohne Kategorie die erste passende Regel nach.
|
||||
* 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
|
||||
{
|
||||
$this->verlangeAnmeldung();
|
||||
|
||||
$regeln = AbgleichRegel::alle(true);
|
||||
$treffer = 0;
|
||||
$gesamt = 0;
|
||||
$ergebnis = Bankabgleich::regelnAufBuchungenOhneKategorieAnwenden();
|
||||
|
||||
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 Buchungen ohne Kategorie treffen die Regeln auf %d.',
|
||||
$gesamt,
|
||||
$treffer
|
||||
));
|
||||
View::redirect('/banking/regeln', Bankabgleich::berichtOhneKategorie($ergebnis));
|
||||
}
|
||||
|
||||
// ── Formulardaten ────────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ namespace Finanzmanager\Services;
|
|||
|
||||
use Finanzmanager\Models\AbgleichRegel;
|
||||
use Finanzmanager\Models\Bankumsatz;
|
||||
use Finanzmanager\Models\Buchung;
|
||||
use Finanzmanager\Models\Einstellung;
|
||||
|
||||
/**
|
||||
|
|
@ -128,4 +129,95 @@ class Bankabgleich
|
|||
|
||||
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<string>}
|
||||
*/
|
||||
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<string>} $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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,10 @@ include __DIR__ . '/../layout/kopf.php';
|
|||
<div class="seitenaktionen">
|
||||
<form method="post" action="/banking/regeln/pruefen" class="inline">
|
||||
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>">
|
||||
<button type="submit" class="knopf">Gegen Buchungen ohne Kategorie prüfen</button>
|
||||
<button type="submit" class="knopf"
|
||||
data-bestaetigen="Regeln auf alle Buchungen ohne Kategorie anwenden? Buchungen ohne Treffer bleiben unverändert.">
|
||||
Regeln auf Buchungen ohne Kategorie anwenden
|
||||
</button>
|
||||
</form>
|
||||
<a href="/banking/umsaetze" class="knopf">Zum Abgleich</a>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ use Finanzmanager\Models\Buchung;
|
|||
use Finanzmanager\Models\Einstellung;
|
||||
use Finanzmanager\Models\Kategorie;
|
||||
use Finanzmanager\Models\Konto;
|
||||
use Finanzmanager\Services\Bankabgleich;
|
||||
use Finanzmanager\Services\Crypto;
|
||||
use Finanzmanager\Services\FinTsVerfahren;
|
||||
|
||||
|
|
@ -49,6 +50,7 @@ class BankingTest extends TestCase
|
|||
$this->uebernehmenErzeugtEineBuchung();
|
||||
$this->regelmitSplitsErzeugtEineSplitbuchung();
|
||||
$this->buchungOhneKategorieWirdGefunden();
|
||||
$this->regelnKategorisierenBuchungenOhneKategorie();
|
||||
$this->verwerfenUndZuruecksetzen();
|
||||
$this->abrufprotokollHaeltDenVerlauf();
|
||||
}
|
||||
|
|
@ -824,6 +826,63 @@ class BankingTest extends TestCase
|
|||
$this->assert(!in_array($umbuchung, $ids, true), 'Eine Umbuchung braucht keine Kategorie');
|
||||
}
|
||||
|
||||
/**
|
||||
* Der Knopf soll die Lücken nicht nur zählen, sondern gleich schließen:
|
||||
* Jede Buchung ohne Kategorie, auf die eine Regel passt, bekommt deren
|
||||
* Zuordnung nachgetragen. Ohne Treffer bleibt die Buchung unverändert.
|
||||
*/
|
||||
private function regelnKategorisierenBuchungenOhneKategorie(): void
|
||||
{
|
||||
$regelId = AbgleichRegel::anlegen([
|
||||
'name' => 'Stadtwerke Nachtrag', 'feld' => 'empfaenger', 'vergleich' => 'enthaelt',
|
||||
'muster' => 'Stadtwerke Nachtrag', 'kategorie_id' => $this->strom, 'prioritaet' => 1, 'aktiv' => 1,
|
||||
]);
|
||||
$splitRegelId = AbgleichRegel::anlegen([
|
||||
'name' => 'Vermieter Nachtrag geteilt', 'feld' => 'empfaenger', 'vergleich' => 'enthaelt',
|
||||
'muster' => 'Vermieter Nachtrag',
|
||||
'splits' => [
|
||||
['kategorie_id' => $this->strom, 'anteil' => 0.5],
|
||||
['kategorie_id' => $this->miete, 'anteil' => 0.5],
|
||||
],
|
||||
'prioritaet' => 1, 'aktiv' => 1,
|
||||
]);
|
||||
$treffervorher = (int)AbgleichRegel::find($regelId)['treffer'];
|
||||
|
||||
$trifft = Buchung::anlegen([
|
||||
'konto_id' => $this->giro, 'datum' => '2026-08-11', 'betrag' => -55.00,
|
||||
'empfaenger' => 'Stadtwerke Nachtrag GmbH',
|
||||
]);
|
||||
$trifftSplit = Buchung::anlegen([
|
||||
'konto_id' => $this->giro, 'datum' => '2026-08-11', 'betrag' => -100.00,
|
||||
'empfaenger' => 'Vermieter Nachtrag GmbH',
|
||||
]);
|
||||
$trifftNicht = Buchung::anlegen([
|
||||
'konto_id' => $this->giro, 'datum' => '2026-08-11', 'betrag' => -12.00,
|
||||
'empfaenger' => 'Ohne jede Regel',
|
||||
]);
|
||||
|
||||
$ergebnis = Bankabgleich::regelnAufBuchungenOhneKategorieAnwenden();
|
||||
|
||||
$this->assertEquals($this->strom, (int)Buchung::find($trifft)['kategorie_id'], 'Die Kategorie wurde nachgetragen');
|
||||
$this->assertNull(Buchung::find($trifftNicht)['kategorie_id'], 'Ohne Regel bleibt die Buchung unverändert');
|
||||
|
||||
$splitBuchung = Buchung::find($trifftSplit);
|
||||
$this->assertEquals(1, (int)$splitBuchung['gesplittet'], 'Eine Splitregel macht die Buchung zur Splitbuchung');
|
||||
$splits = Buchung::splits($trifftSplit);
|
||||
$this->assertEquals(2, count($splits), 'Zwei Splitzeilen aus der Regel');
|
||||
$this->assertEquals(-50.00, (float)$splits[0]['betrag'], 'Je zur Hälfte aufgeteilt');
|
||||
|
||||
$luecken = array_map('intval', array_column(Buchung::ohneKategorie(), 'id'));
|
||||
$this->assert(in_array($trifftNicht, $luecken, true), 'Bleibt ohne Regeltreffer in der Lückenliste');
|
||||
$this->assert(!in_array($trifft, $luecken, true), 'Verlässt die Lückenliste, sobald sie kategorisiert ist');
|
||||
|
||||
$this->assertGreaterThan($treffervorher, (int)AbgleichRegel::find($regelId)['treffer'], 'Der Treffer wird gezählt');
|
||||
$this->assert($ergebnis['kategorisiert'] >= 2, 'Mindestens die zwei passenden Buchungen wurden kategorisiert');
|
||||
|
||||
AbgleichRegel::loeschen($regelId);
|
||||
AbgleichRegel::loeschen($splitRegelId);
|
||||
}
|
||||
|
||||
private function verwerfenUndZuruecksetzen(): void
|
||||
{
|
||||
$id = $this->umsatzAnlegen([
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue