Fix #155: AfA-Buchungen laufen jetzt über Booking::createAfa()/reverseAfa()
AssetController setzte INSERT INTO bookings bislang direkt ab und umging damit die Sperre des Geschäftsjahres, den transaktionalen Schutz der Nummernvergabe und ordnete Stornos dem falschen Geschäftsjahr zu.
This commit is contained in:
parent
e86e5f29ec
commit
88d9cc53ff
6 changed files with 344 additions and 78 deletions
|
|
@ -2,6 +2,14 @@
|
|||
|
||||
---
|
||||
|
||||
## 2026-08-03 – Fix: AfA-Buchungen umgehen das Booking-Modell (#155)
|
||||
|
||||
- `AssetController::bookAfa()` und `reverseAfaBooking()` setzten ihre `INSERT INTO bookings`-Statements bislang direkt ab statt über `Booking::create()`/`reverse()`. Vier Folgen: Die Sperre eines abgeschlossenen Geschäftsjahres griff nicht, ein zweiter, unabhängiger Nummernkreis (`AFA-NNNNN`) lief neben `BU-YYYY-NNNNNN` auf derselben Spalte, die Nummernvergabe stand außerhalb der Transaktion, und das Storno einer AfA-Buchung wurde immer dem *aktuellen* Kalenderjahr statt dem Jahr der Originalbuchung zugeordnet.
|
||||
- **`src/Models/Booking.php`:** Neue Methoden `createAfa()` und `reverseAfa()` bündeln die komplette Schreiblogik für AfA-Buchungen – inklusive Sperrprüfung des Geschäftsjahres, Nummernvergabe (`generateAfaBuchungsNr()`, eigener `AFA-NNNNN`-Kreis, bewusst beibehalten statt vereinheitlicht) innerhalb der Transaktion, und – beim Storno – Übernahme der `fiscal_year_id` der Originalbuchung. Das Buchungsdatum des Storno bleibt das tatsächliche Stornodatum (Ist-Versteuerung).
|
||||
- **`src/Controllers/AssetController.php`:** `bookAfa()` und `reverseAfaBooking()` rufen nur noch die Modellmethoden auf, die bisherigen Vorprüfungen bleiben erhalten. `RuntimeException` aus dem Modell wird als HTTP 409 mit `{"error": "..."}` beantwortet. Kein direkter `Database`-Zugriff auf `bookings` mehr im Controller.
|
||||
- **`migrations/092_afa_storno_fiscal_year_korrektur.sql`** (neu): Korrigiert die `fiscal_year_id` bereits vorhandener AfA-Stornobuchungen rückwirkend auf das Jahr ihrer Originalbuchung.
|
||||
- **`tests/82_AfaBuchungModellTest.php`** (neu): Sperre verhindert AfA-Buchung und -Storno, Buchung im offenen Jahr landet korrekt (Jahr, 31.12.-Datum, `betrag_ust = 0`), Storno übernimmt die `fiscal_year_id` der Originalbuchung, `reversed_by`/`is_reversal` werden gesetzt, Buchungsnummer folgt dem Schema `AFA-NNNNN`.
|
||||
|
||||
## 2026-08-02 – Testsuite als echtes Gate in update.sh (#169)
|
||||
|
||||
- Schritt 9/9 von `scripts/update.sh` gab bei rotem Testergebnis nur eine Warnung aus – Exit-Code 0, kein Abbruch. Ein Update mit fehlgeschlagenen Tests galt damit als erfolgreich, obwohl die neue Version zu diesem Zeitpunkt bereits live war.
|
||||
|
|
|
|||
|
|
@ -100,3 +100,11 @@ Die Tabelle besitzt ein Flag `automatisch_berechnet`:
|
|||
3. Im Dialog: Toggle „Manuell bearbeiten" aktiviert editierbare Zellen.
|
||||
4. **„Neu berechnen"** überschreibt manuelle Änderungen (nach Bestätigung).
|
||||
5. **„Speichern"** sichert die aktuelle Tabelle (inklusive manueller Änderungen) in der Datenbank.
|
||||
|
||||
### AfA-Buchung
|
||||
|
||||
Für eine Zeile der Abschreibungstabelle lässt sich per Klick eine AfA-Buchung anlegen (`Booking::createAfa()`). Sie bucht den Abschreibungsbetrag zum 31.12. des jeweiligen Jahres vom hinterlegten Anlagekonto auf ein gewähltes Ausgabenkonto, ohne Umsatzsteuer.
|
||||
|
||||
- **Sperre des Geschäftsjahres:** Ist das Geschäftsjahr des AfA-Jahres bereits gesperrt, wird die Buchung mit einer Fehlermeldung abgelehnt (HTTP 409) – wie bei jeder anderen Buchung auch.
|
||||
- **Nummernkreis:** AfA-Buchungen erhalten eine eigene, global fortlaufende Nummer im Format `AFA-NNNNN`, getrennt vom regulären Nummernkreis `BU-YYYY-NNNNNN`. Erkennbar sind sie zusätzlich am `buchungstyp = 'AfA'`.
|
||||
- **Storno:** Beim Stornieren (`Booking::reverseAfa()`) erhält die Stornobuchung die `fiscal_year_id` der Originalbuchung, nicht die des aktuellen Kalenderjahres – eine 2026 stornierte AfA-Buchung für 2024 verändert die Jahressumme 2024, nicht 2026. Das Buchungsdatum des Storno bleibt bewusst das tatsächliche Stornodatum (Ist-Versteuerung). Ist das Geschäftsjahr der Originalbuchung gesperrt, wird auch das Storno abgelehnt.
|
||||
|
|
|
|||
11
migrations/092_afa_storno_fiscal_year_korrektur.sql
Normal file
11
migrations/092_afa_storno_fiscal_year_korrektur.sql
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
-- Migration 092: Bestandskorrektur – AfA-Stornobuchungen erhalten die
|
||||
-- fiscal_year_id ihrer Originalbuchung statt des Kalenderjahres der Stornierung.
|
||||
-- (Fix #155: reverseAfaBooking() verwendete bislang date('Y') statt afa_jahr/
|
||||
-- fiscal_year_id der stornierten Buchung.)
|
||||
|
||||
UPDATE bookings storno
|
||||
JOIN bookings orig ON orig.reversed_by = storno.id
|
||||
SET storno.fiscal_year_id = orig.fiscal_year_id
|
||||
WHERE storno.buchungstyp = 'AfA'
|
||||
AND storno.is_reversal = 1
|
||||
AND storno.fiscal_year_id <> orig.fiscal_year_id;
|
||||
|
|
@ -9,7 +9,6 @@ use Steuerkram\Models\Account;
|
|||
use Steuerkram\Models\Asset;
|
||||
use Steuerkram\Models\AssetDepreciation;
|
||||
use Steuerkram\Models\Booking;
|
||||
use Steuerkram\Models\FiscalYear;
|
||||
use Steuerkram\Models\Setting;
|
||||
use Steuerkram\View;
|
||||
|
||||
|
|
@ -329,43 +328,26 @@ class AssetController extends BaseController
|
|||
|
||||
$jahr = (int)$scheduleRow['jahr'];
|
||||
$betrag = round((float)$scheduleRow['abschreibungsbetrag'], 2);
|
||||
$datum = $jahr . '-12-31';
|
||||
$text = 'AfA ' . $asset['bezeichnung'] . ' ' . $jahr;
|
||||
|
||||
$fy = FiscalYear::findOrCreateByYear($jahr);
|
||||
try {
|
||||
$bookingId = Booking::createAfa([
|
||||
'asset_id' => $assetId,
|
||||
'afa_jahr' => $jahr,
|
||||
'kategorie_konto_id' => $ausgabekontoId,
|
||||
'zahlungskonto_id' => (int)$asset['account_id'],
|
||||
'betrag' => $betrag,
|
||||
'buchungstext' => $text,
|
||||
'schedule_row_id' => $rowId,
|
||||
]);
|
||||
} catch (\RuntimeException $e) {
|
||||
http_response_code(409);
|
||||
echo json_encode(['error' => $e->getMessage()]);
|
||||
return;
|
||||
}
|
||||
|
||||
$buchungsNr = \Steuerkram\Database::fetchOne(
|
||||
"SELECT CONCAT('AFA-', LPAD(COALESCE(MAX(CAST(SUBSTRING(buchungs_nr,5) AS UNSIGNED)),0)+1,5,'0')) AS nr
|
||||
FROM bookings WHERE buchungs_nr LIKE 'AFA-%'"
|
||||
)['nr'] ?? ('AFA-' . str_pad('1', 5, '0', STR_PAD_LEFT));
|
||||
|
||||
\Steuerkram\Database::execute(
|
||||
'INSERT INTO bookings
|
||||
(buchungs_nr, beleg_id, asset_id, afa_jahr, buchungs_datum, buchungstyp,
|
||||
kategorie_konto_id, zahlungskonto_id,
|
||||
betrag_netto, ust_satz, steuerart, betrag_ust, betrag_brutto,
|
||||
buchungstext, fiscal_year_id)
|
||||
VALUES (?, NULL, ?, ?, ?, ?, ?, ?, ?, 0, ?, 0, ?, ?, ?)',
|
||||
[
|
||||
$buchungsNr,
|
||||
$assetId,
|
||||
$jahr,
|
||||
$datum,
|
||||
Constants::BUCHUNGSTYP_AFA,
|
||||
$ausgabekontoId,
|
||||
(int)$asset['account_id'],
|
||||
$betrag,
|
||||
'normal',
|
||||
$betrag,
|
||||
$text,
|
||||
(int)$fy['id'],
|
||||
]
|
||||
);
|
||||
|
||||
$bookingId = (int)\Steuerkram\Database::lastInsertId();
|
||||
AssetDepreciation::setBookingId($rowId, $bookingId);
|
||||
|
||||
echo json_encode(['ok' => true, 'buchungs_nr' => $buchungsNr]);
|
||||
$buchung = Booking::find($bookingId);
|
||||
echo json_encode(['ok' => true, 'buchungs_nr' => $buchung['buchungs_nr']]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -411,51 +393,16 @@ class AssetController extends BaseController
|
|||
return;
|
||||
}
|
||||
|
||||
$fy = FiscalYear::findOrCreateByYear((int)date('Y'));
|
||||
$nr = \Steuerkram\Database::fetchOne(
|
||||
"SELECT CONCAT('AFA-', LPAD(COALESCE(MAX(CAST(SUBSTRING(buchungs_nr,5) AS UNSIGNED)),0)+1,5,'0')) AS nr
|
||||
FROM bookings WHERE buchungs_nr LIKE 'AFA-%'"
|
||||
)['nr'] ?? 'AFA-00001';
|
||||
|
||||
\Steuerkram\Database::beginTransaction();
|
||||
try {
|
||||
\Steuerkram\Database::execute(
|
||||
'INSERT INTO bookings
|
||||
(buchungs_nr, beleg_id, asset_id, afa_jahr, buchungs_datum, buchungstyp,
|
||||
kategorie_konto_id, zahlungskonto_id,
|
||||
betrag_netto, ust_satz, steuerart, betrag_ust, betrag_brutto,
|
||||
buchungstext, is_reversal, fiscal_year_id)
|
||||
VALUES (?, NULL, ?, ?, ?, ?, ?, ?, ?, 0, ?, 0, ?, ?, 1, ?)',
|
||||
[
|
||||
$nr,
|
||||
$orig['asset_id'],
|
||||
$orig['afa_jahr'],
|
||||
date('Y-m-d'),
|
||||
Constants::BUCHUNGSTYP_AFA,
|
||||
$orig['kategorie_konto_id'],
|
||||
$orig['zahlungskonto_id'],
|
||||
-1 * (float)$orig['betrag_netto'],
|
||||
'normal',
|
||||
-1 * (float)$orig['betrag_brutto'],
|
||||
'STORNO zu ' . $orig['buchungs_nr'],
|
||||
$fy['id'],
|
||||
]
|
||||
);
|
||||
$reversalId = (int)\Steuerkram\Database::lastInsertId();
|
||||
\Steuerkram\Database::execute(
|
||||
'UPDATE bookings SET reversed_by = ? WHERE id = ?',
|
||||
[$reversalId, $bookingId]
|
||||
);
|
||||
AssetDepreciation::setBookingId($rowId, 0);
|
||||
\Steuerkram\Database::commit();
|
||||
} catch (\Throwable $e) {
|
||||
\Steuerkram\Database::rollBack();
|
||||
http_response_code(500);
|
||||
$reversalId = Booking::reverseAfa($bookingId, $rowId);
|
||||
} catch (\RuntimeException $e) {
|
||||
http_response_code(409);
|
||||
echo json_encode(['error' => $e->getMessage()]);
|
||||
return;
|
||||
}
|
||||
|
||||
echo json_encode(['ok' => true, 'storno_nr' => $nr]);
|
||||
$storno = Booking::find($reversalId);
|
||||
echo json_encode(['ok' => true, 'storno_nr' => $storno['buchungs_nr']]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -335,6 +335,124 @@ class Booking
|
|||
return $reversalId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Legt eine AfA-Buchung im eigenen AFA-Nummernkreis an. Referenz für Sperrprüfung,
|
||||
* Nummernvergabe innerhalb der Transaktion und Schreibumfang: self::create().
|
||||
*/
|
||||
public static function createAfa(array $data): int
|
||||
{
|
||||
$jahr = (int)$data['afa_jahr'];
|
||||
$fy = FiscalYear::findOrCreateByYear($jahr);
|
||||
|
||||
if ($fy['locked']) {
|
||||
throw new \RuntimeException("Geschäftsjahr {$jahr} ist gesperrt. Neue Buchungen nicht möglich.");
|
||||
}
|
||||
|
||||
$betrag = round((float)$data['betrag'], 2);
|
||||
$datum = $jahr . '-12-31';
|
||||
|
||||
Database::beginTransaction();
|
||||
try {
|
||||
$nr = self::generateAfaBuchungsNr();
|
||||
Database::execute(
|
||||
'INSERT INTO bookings
|
||||
(buchungs_nr, beleg_id, asset_id, afa_jahr, buchungs_datum, buchungstyp,
|
||||
kategorie_konto_id, zahlungskonto_id,
|
||||
betrag_netto, ust_satz, steuerart, betrag_ust, betrag_brutto,
|
||||
buchungstext, fiscal_year_id)
|
||||
VALUES (?, NULL, ?, ?, ?, ?, ?, ?, ?, 0, ?, 0, ?, ?, ?)',
|
||||
[
|
||||
$nr,
|
||||
(int)$data['asset_id'],
|
||||
$jahr,
|
||||
$datum,
|
||||
Constants::BUCHUNGSTYP_AFA,
|
||||
(int)$data['kategorie_konto_id'],
|
||||
(int)$data['zahlungskonto_id'],
|
||||
$betrag,
|
||||
'normal',
|
||||
$betrag,
|
||||
$data['buchungstext'],
|
||||
$fy['id'],
|
||||
]
|
||||
);
|
||||
|
||||
$id = (int)Database::lastInsertId();
|
||||
|
||||
if (!empty($data['schedule_row_id'])) {
|
||||
AssetDepreciation::setBookingId((int)$data['schedule_row_id'], $id);
|
||||
}
|
||||
|
||||
Database::commit();
|
||||
} catch (\Throwable $e) {
|
||||
Database::rollBack();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Storniert eine AfA-Buchung im eigenen AFA-Nummernkreis. Das Storno erhält die
|
||||
* fiscal_year_id der Originalbuchung (nicht das laufende Kalenderjahr) – wer 2026
|
||||
* eine AfA-Buchung für 2024 storniert, darf die Jahressumme 2026 nicht verfälschen.
|
||||
* Das Buchungsdatum bleibt bewusst das tatsächliche Stornodatum (Ist-Versteuerung).
|
||||
*/
|
||||
public static function reverseAfa(int $bookingId, ?int $scheduleRowId = null): int
|
||||
{
|
||||
$orig = self::find($bookingId);
|
||||
if (!$orig) throw new \RuntimeException('Buchung nicht gefunden.');
|
||||
if ($orig['is_reversal']) throw new \RuntimeException('Stornobuchungen können nicht storniert werden.');
|
||||
|
||||
$fy = FiscalYear::find((int)$orig['fiscal_year_id']);
|
||||
if (!$fy) throw new \RuntimeException('Geschäftsjahr der Originalbuchung nicht gefunden.');
|
||||
if ($fy['locked']) {
|
||||
throw new \RuntimeException("Geschäftsjahr {$fy['year']} ist gesperrt. Neue Buchungen nicht möglich.");
|
||||
}
|
||||
|
||||
Database::beginTransaction();
|
||||
try {
|
||||
$nr = self::generateAfaBuchungsNr();
|
||||
Database::execute(
|
||||
'INSERT INTO bookings
|
||||
(buchungs_nr, beleg_id, asset_id, afa_jahr, buchungs_datum, buchungstyp,
|
||||
kategorie_konto_id, zahlungskonto_id,
|
||||
betrag_netto, ust_satz, steuerart, betrag_ust, betrag_brutto,
|
||||
buchungstext, is_reversal, fiscal_year_id)
|
||||
VALUES (?, NULL, ?, ?, ?, ?, ?, ?, ?, 0, ?, 0, ?, ?, 1, ?)',
|
||||
[
|
||||
$nr,
|
||||
$orig['asset_id'],
|
||||
$orig['afa_jahr'],
|
||||
date('Y-m-d'),
|
||||
Constants::BUCHUNGSTYP_AFA,
|
||||
$orig['kategorie_konto_id'],
|
||||
$orig['zahlungskonto_id'],
|
||||
-1 * (float)$orig['betrag_netto'],
|
||||
'normal',
|
||||
-1 * (float)$orig['betrag_brutto'],
|
||||
'STORNO zu ' . $orig['buchungs_nr'],
|
||||
$fy['id'],
|
||||
]
|
||||
);
|
||||
|
||||
$reversalId = (int)Database::lastInsertId();
|
||||
|
||||
Database::execute('UPDATE bookings SET reversed_by=? WHERE id=?', [$reversalId, $bookingId]);
|
||||
|
||||
if ($scheduleRowId !== null) {
|
||||
AssetDepreciation::setBookingId($scheduleRowId, 0);
|
||||
}
|
||||
|
||||
Database::commit();
|
||||
} catch (\Throwable $e) {
|
||||
Database::rollBack();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return $reversalId;
|
||||
}
|
||||
|
||||
public static function upsertEigenanteil30(
|
||||
int $belegId,
|
||||
float $betragNetto,
|
||||
|
|
@ -437,6 +555,17 @@ class Booking
|
|||
return sprintf('BU-%s-%06d', $year, $next);
|
||||
}
|
||||
|
||||
/** Eigener, global fortlaufender Nummernkreis für AfA-Buchungen (AFA-NNNNN). */
|
||||
private static function generateAfaBuchungsNr(): string
|
||||
{
|
||||
$result = Database::fetchOne(
|
||||
"SELECT MAX(CAST(SUBSTRING(buchungs_nr,5) AS UNSIGNED)) as maxnr
|
||||
FROM bookings WHERE buchungs_nr LIKE 'AFA-%'"
|
||||
);
|
||||
$next = (int)($result['maxnr'] ?? 0) + 1;
|
||||
return sprintf('AFA-%05d', $next);
|
||||
}
|
||||
|
||||
/** Summiert betrag_brutto der aktiven Einnahme-Hauptbuchungen einer bereits geladenen Buchungsliste. */
|
||||
public static function sumZahlungen(array $bookings): float
|
||||
{
|
||||
|
|
|
|||
163
tests/82_AfaBuchungModellTest.php
Normal file
163
tests/82_AfaBuchungModellTest.php
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Steuerkram\Models\Asset;
|
||||
use Steuerkram\Models\Booking;
|
||||
use Steuerkram\Models\FiscalYear;
|
||||
|
||||
/**
|
||||
* Test 82: AfA-Buchungen laufen über Booking::createAfa()/reverseAfa().
|
||||
*
|
||||
* Fix #155: AssetController setzte INSERT INTO bookings bislang direkt ab und
|
||||
* umging damit die Sperre des Geschäftsjahres, den Transaktionsschutz der
|
||||
* Nummernvergabe und – beim Storno – die richtige Jahreszuordnung.
|
||||
*/
|
||||
class AfaBuchungModellTest extends TestCase
|
||||
{
|
||||
protected string $testName = 'Test 82: AfA-Buchungen über Booking-Modell';
|
||||
|
||||
private int $assetId;
|
||||
private int $kategorieId;
|
||||
private int $zahlungsId;
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$this->kategorieId = (int)$this->findAccountId('4855');
|
||||
$this->zahlungsId = (int)$this->findAccountId('0420');
|
||||
|
||||
$this->assetId = Asset::create([
|
||||
'bezeichnung' => 'Testgerät AfA-Modell',
|
||||
'anschaffungsdatum' => '2024-01-15',
|
||||
'anschaffungskosten' => 1200.00,
|
||||
'zugaenge' => 0,
|
||||
'abgaenge' => 0,
|
||||
'abschreibung_jahr' => 400.00,
|
||||
'nutzungsdauer' => 3,
|
||||
'account_id' => $this->zahlungsId,
|
||||
]);
|
||||
|
||||
$this->gesperrtesJahrLehntAfaBuchungAb();
|
||||
$this->afaBuchungInOffenemJahr();
|
||||
$this->stornoUebernimmtJahrDerOriginalbuchung();
|
||||
$this->gesperrtesJahrLehntStornoAb();
|
||||
}
|
||||
|
||||
private function gesperrtesJahrLehntAfaBuchungAb(): void
|
||||
{
|
||||
$jahr = 2019;
|
||||
$fy = FiscalYear::findOrCreateByYear($jahr);
|
||||
\Steuerkram\Database::execute('UPDATE fiscal_years SET locked = 1 WHERE id = ?', [$fy['id']]);
|
||||
|
||||
$vorher = $this->countBookings();
|
||||
$exception = null;
|
||||
try {
|
||||
Booking::createAfa($this->afaData($jahr));
|
||||
} catch (\RuntimeException $e) {
|
||||
$exception = $e;
|
||||
}
|
||||
|
||||
$this->assert($exception !== null, 'AfA-Buchung in gesperrtem Geschäftsjahr wirft eine RuntimeException');
|
||||
$this->assertEquals(
|
||||
$vorher,
|
||||
$this->countBookings(),
|
||||
'Bei Sperre wird kein Datensatz in bookings angelegt'
|
||||
);
|
||||
}
|
||||
|
||||
private function afaBuchungInOffenemJahr(): void
|
||||
{
|
||||
$jahr = 2024;
|
||||
$bookingId = Booking::createAfa($this->afaData($jahr));
|
||||
$this->assertGreaterThan(0, $bookingId, 'AfA-Buchung in offenem Jahr wird angelegt');
|
||||
|
||||
$booking = Booking::find($bookingId);
|
||||
$this->assertEquals($jahr, (int)$booking['afa_jahr'], 'afa_jahr ist gesetzt');
|
||||
$this->assertEquals($jahr . '-12-31', $booking['buchungs_datum'], 'buchungs_datum liegt auf dem 31.12. des AfA-Jahres');
|
||||
$this->assertEquals(0.00, (float)$booking['betrag_ust'], 'betrag_ust ist 0');
|
||||
$this->assertEquals(400.00, (float)$booking['betrag_netto'], 'betrag_netto entspricht dem AfA-Betrag');
|
||||
$this->assert(
|
||||
(bool)preg_match('/^AFA-\d{5}$/', $booking['buchungs_nr']),
|
||||
'Buchungsnummer folgt dem Schema AFA-NNNNN (erhalten: ' . $booking['buchungs_nr'] . ')'
|
||||
);
|
||||
}
|
||||
|
||||
private function stornoUebernimmtJahrDerOriginalbuchung(): void
|
||||
{
|
||||
// Vorjahr: 2022 – das laufende Kalenderjahr ist ein anderes.
|
||||
$jahr = 2022;
|
||||
$bookingId = Booking::createAfa($this->afaData($jahr));
|
||||
$original = Booking::find($bookingId);
|
||||
|
||||
$reversalId = Booking::reverseAfa($bookingId);
|
||||
$reversal = Booking::find($reversalId);
|
||||
|
||||
$this->assertEquals(
|
||||
$original['fiscal_year_id'],
|
||||
$reversal['fiscal_year_id'],
|
||||
'Storno erhält die fiscal_year_id der Originalbuchung, nicht die des aktuellen Jahres'
|
||||
);
|
||||
$aktuellesJahr = FiscalYear::findOrCreateByYear((int)date('Y'));
|
||||
$this->assert(
|
||||
(int)$reversal['fiscal_year_id'] !== (int)$aktuellesJahr['id'],
|
||||
'fiscal_year_id des Storno ist nicht die des laufenden Kalenderjahres'
|
||||
);
|
||||
$this->assertEquals(1, (int)$reversal['is_reversal'], 'Stornobuchung ist als is_reversal=1 markiert');
|
||||
|
||||
$updatedOriginal = Booking::find($bookingId);
|
||||
$this->assertEquals(
|
||||
$reversalId,
|
||||
(int)$updatedOriginal['reversed_by'],
|
||||
'reversed_by der Originalbuchung zeigt auf die Stornobuchung'
|
||||
);
|
||||
$this->assert(
|
||||
(bool)preg_match('/^AFA-\d{5}$/', $reversal['buchungs_nr']),
|
||||
'Storno-Buchungsnummer folgt ebenfalls dem Schema AFA-NNNNN'
|
||||
);
|
||||
}
|
||||
|
||||
private function gesperrtesJahrLehntStornoAb(): void
|
||||
{
|
||||
$jahr = 2020;
|
||||
$bookingId = Booking::createAfa($this->afaData($jahr));
|
||||
|
||||
$fy = FiscalYear::findByYear($jahr);
|
||||
\Steuerkram\Database::execute('UPDATE fiscal_years SET locked = 1 WHERE id = ?', [$fy['id']]);
|
||||
|
||||
$vorher = $this->countBookings();
|
||||
$exception = null;
|
||||
try {
|
||||
Booking::reverseAfa($bookingId);
|
||||
} catch (\RuntimeException $e) {
|
||||
$exception = $e;
|
||||
}
|
||||
|
||||
$this->assert($exception !== null, 'Storno einer AfA-Buchung im gesperrten Jahr wirft eine RuntimeException');
|
||||
$this->assertEquals(
|
||||
$vorher,
|
||||
$this->countBookings(),
|
||||
'Bei Sperre wird kein zusätzlicher Storno-Datensatz angelegt'
|
||||
);
|
||||
|
||||
$unverändert = Booking::find($bookingId);
|
||||
$this->assert(empty($unverändert['reversed_by']), 'Originalbuchung bleibt ohne reversed_by, wenn das Storno abgelehnt wird');
|
||||
}
|
||||
|
||||
private function afaData(int $jahr): array
|
||||
{
|
||||
return [
|
||||
'asset_id' => $this->assetId,
|
||||
'afa_jahr' => $jahr,
|
||||
'kategorie_konto_id' => $this->kategorieId,
|
||||
'zahlungskonto_id' => $this->zahlungsId,
|
||||
'betrag' => 400.00,
|
||||
'buchungstext' => 'AfA Test ' . $jahr,
|
||||
];
|
||||
}
|
||||
|
||||
private function countBookings(): int
|
||||
{
|
||||
$row = \Steuerkram\Database::fetchOne('SELECT COUNT(*) as cnt FROM bookings');
|
||||
return (int)($row['cnt'] ?? 0);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue