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
|
# Ä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)
|
## `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
|
- 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\Bankprotokoll;
|
||||||
use Finanzmanager\Models\Bankumsatz;
|
use Finanzmanager\Models\Bankumsatz;
|
||||||
use Finanzmanager\Models\Bankzugang;
|
use Finanzmanager\Models\Bankzugang;
|
||||||
|
use Finanzmanager\Models\Buchung;
|
||||||
use Finanzmanager\Models\Kategorie;
|
use Finanzmanager\Models\Kategorie;
|
||||||
use Finanzmanager\Models\Klasse;
|
use Finanzmanager\Models\Klasse;
|
||||||
use Finanzmanager\Models\Konto;
|
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 –
|
* Wendet die Regeln auf Buchungen ohne Kategorie an, ohne sie zu ändern –
|
||||||
* damit lässt sich eine neue Regel ausprobieren, bevor sie bucht.
|
* 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
|
public function regelnPruefen(): void
|
||||||
{
|
{
|
||||||
|
|
@ -832,15 +836,20 @@ class BankingController extends BaseController
|
||||||
$treffer = 0;
|
$treffer = 0;
|
||||||
$gesamt = 0;
|
$gesamt = 0;
|
||||||
|
|
||||||
foreach (Bankumsatz::liste(['status' => 'offen']) as $umsatz) {
|
foreach (Buchung::ohneKategorie() as $buchung) {
|
||||||
$gesamt++;
|
$gesamt++;
|
||||||
|
$umsatz = [
|
||||||
|
'empfaenger' => $buchung['empfaenger'],
|
||||||
|
'verwendungszweck' => $buchung['buchungstext'],
|
||||||
|
'betrag' => $buchung['betrag'],
|
||||||
|
];
|
||||||
if (AbgleichRegel::passendeRegel($umsatz, $regeln) !== null) {
|
if (AbgleichRegel::passendeRegel($umsatz, $regeln) !== null) {
|
||||||
$treffer++;
|
$treffer++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
View::redirect('/banking/regeln', sprintf(
|
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,
|
$gesamt,
|
||||||
$treffer
|
$treffer
|
||||||
));
|
));
|
||||||
|
|
|
||||||
|
|
@ -487,6 +487,35 @@ class Buchung
|
||||||
return $anzahlen;
|
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 jüngsten Buchungen zu einem Empfängernamen, kontenübergreifend – für
|
||||||
* die Adressverwaltung: Wer sich eine Adresse ansieht, will wissen, was
|
* die Adressverwaltung: Wer sich eine Adresse ansieht, will wissen, was
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ include __DIR__ . '/../layout/kopf.php';
|
||||||
<div class="seitenaktionen">
|
<div class="seitenaktionen">
|
||||||
<form method="post" action="/banking/regeln/pruefen" class="inline">
|
<form method="post" action="/banking/regeln/pruefen" class="inline">
|
||||||
<input type="hidden" name="csrf_token" value="<?= View::h($csrfToken) ?>">
|
<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>
|
</form>
|
||||||
<a href="/banking/umsaetze" class="knopf">Zum Abgleich</a>
|
<a href="/banking/umsaetze" class="knopf">Zum Abgleich</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ class BankingTest extends TestCase
|
||||||
$this->abgleichFindetVorhandeneBuchung();
|
$this->abgleichFindetVorhandeneBuchung();
|
||||||
$this->uebernehmenErzeugtEineBuchung();
|
$this->uebernehmenErzeugtEineBuchung();
|
||||||
$this->regelmitSplitsErzeugtEineSplitbuchung();
|
$this->regelmitSplitsErzeugtEineSplitbuchung();
|
||||||
|
$this->buchungOhneKategorieWirdGefunden();
|
||||||
$this->verwerfenUndZuruecksetzen();
|
$this->verwerfenUndZuruecksetzen();
|
||||||
$this->abrufprotokollHaeltDenVerlauf();
|
$this->abrufprotokollHaeltDenVerlauf();
|
||||||
}
|
}
|
||||||
|
|
@ -777,6 +778,52 @@ class BankingTest extends TestCase
|
||||||
AbgleichRegel::loeschen($regelId);
|
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
|
private function verwerfenUndZuruecksetzen(): void
|
||||||
{
|
{
|
||||||
$id = $this->umsatzAnlegen([
|
$id = $this->umsatzAnlegen([
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue