Fix n.n.: Regel-Testlauf prüft gegen Buchungen ohne Kategorie statt gegen offene Umsätze
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
9ae2dfea40
commit
2bf5bae3bd
5 changed files with 104 additions and 5 deletions
14
Changes.md
14
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
));
|
||||
|
|
|
|||
|
|
@ -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<array<string,mixed>>
|
||||
*/
|
||||
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
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ 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 offene Umsätze prüfen</button>
|
||||
<button type="submit" class="knopf">Gegen Buchungen ohne Kategorie prüfen</button>
|
||||
</form>
|
||||
<a href="/banking/umsaetze" class="knopf">Zum Abgleich</a>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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([
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue