Steuerkram/src/Models/Booking.php

210 lines
7.6 KiB
PHP

<?php
declare(strict_types=1);
namespace Steuerkram\Models;
use Steuerkram\Database;
class Booking
{
public static function forBeleg(int $belegId): array
{
return Database::fetchAll(
'SELECT bk.*,
k.kontonummer as kategorie_nr, k.bezeichnung as kategorie_name,
z.kontonummer as zahlungs_nr, z.bezeichnung as zahlungs_name
FROM bookings bk
JOIN accounts k ON bk.kategorie_konto_id = k.id
JOIN accounts z ON bk.zahlungskonto_id = z.id
WHERE bk.beleg_id = ?
ORDER BY bk.buchungs_datum, bk.id',
[$belegId]
);
}
public static function journal(array $filter = [], int $page = 1, int $perPage = 50): array
{
$where = ['1=1'];
$params = [];
if (!empty($filter['year'])) {
$where[] = 'fy.year = ?';
$params[] = (int)$filter['year'];
}
if (!empty($filter['month'])) {
$where[] = 'MONTH(bk.buchungs_datum) = ?';
$params[] = (int)$filter['month'];
}
if (!empty($filter['konto'])) {
$where[] = '(bk.kategorie_konto_id = ? OR bk.zahlungskonto_id = ?)';
$params[] = (int)$filter['konto'];
$params[] = (int)$filter['konto'];
}
if (!empty($filter['search'])) {
$where[] = 'bk.buchungstext LIKE ?';
$params[] = '%' . $filter['search'] . '%';
}
$offset = ($page - 1) * $perPage;
$sql = 'SELECT bk.*,
k.kontonummer as kategorie_nr, k.bezeichnung as kategorie_name,
z.kontonummer as zahlungs_nr, z.bezeichnung as zahlungs_name,
bl.beleg_nr, fy.year as fiscal_year_nr
FROM bookings bk
JOIN accounts k ON bk.kategorie_konto_id = k.id
JOIN accounts z ON bk.zahlungskonto_id = z.id
JOIN belege bl ON bk.beleg_id = bl.id
JOIN fiscal_years fy ON bk.fiscal_year_id = fy.id
WHERE ' . implode(' AND ', $where) . '
ORDER BY bk.buchungs_datum DESC, bk.id DESC
LIMIT ? OFFSET ?';
$params[] = $perPage;
$params[] = $offset;
$rows = Database::fetchAll($sql, $params);
$countSql = 'SELECT COUNT(*) as cnt FROM bookings bk
JOIN fiscal_years fy ON bk.fiscal_year_id = fy.id
WHERE ' . implode(' AND ', $where);
$countParams = array_slice($params, 0, -2);
$total = (int)(Database::fetchOne($countSql, $countParams)['cnt'] ?? 0);
return ['rows' => $rows, 'total' => $total, 'pages' => (int)ceil($total / $perPage)];
}
public static function find(int $id): array|false
{
return Database::fetchOne('SELECT * FROM bookings WHERE id = ?', [$id]);
}
public static function create(array $data): int
{
// Determine fiscal year
$year = (int)date('Y', strtotime($data['buchungs_datum']));
$fy = FiscalYear::findOrCreateByYear($year);
if ($fy['locked']) {
throw new \RuntimeException("Geschäftsjahr {$year} ist gesperrt. Neue Buchungen nicht möglich.");
}
// Calculate amounts
$netto = round((float)$data['betrag_netto'], 2);
$ust = round((float)($data['ust_satz'] ?? 0) / 100 * $netto, 2);
$brutto = round($netto + $ust, 2);
$nr = self::generateBuchungsNr();
$steuerart = $data['steuerart'] ?? 'normal';
if (!in_array($steuerart, ['normal','ig_lieferung','export','ig_erwerb_19','ig_erwerb_7'], true)) {
$steuerart = 'normal';
}
Database::execute(
'INSERT INTO bookings (buchungs_nr, beleg_id, buchungs_datum, buchungstyp,
kategorie_konto_id, zahlungskonto_id,
betrag_netto, ust_satz, steuerart, betrag_ust, betrag_brutto, buchungstext, fiscal_year_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
[
$nr,
(int)$data['beleg_id'],
$data['buchungs_datum'],
$data['buchungstyp'],
(int)$data['kategorie_konto_id'],
(int)$data['zahlungskonto_id'],
$netto,
(float)($data['ust_satz'] ?? 0),
$steuerart,
$ust,
$brutto,
$data['buchungstext'],
$fy['id'],
]
);
$id = (int)Database::lastInsertId();
// Mark Beleg as booked
Database::execute('UPDATE belege SET booked=1, updated_at=NOW() WHERE id=?', [(int)$data['beleg_id']]);
return $id;
}
public static function reverse(int $id, string $reason): int
{
$orig = self::find($id);
if (!$orig) throw new \RuntimeException('Buchung nicht gefunden.');
if ($orig['is_reversal']) throw new \RuntimeException('Stornobuchungen können nicht storniert werden.');
$reversalData = [
'beleg_id' => $orig['beleg_id'],
'buchungs_datum' => date('Y-m-d'),
'buchungstyp' => $orig['buchungstyp'],
'kategorie_konto_id' => $orig['kategorie_konto_id'],
'zahlungskonto_id' => $orig['zahlungskonto_id'],
'betrag_netto' => $orig['betrag_netto'],
'ust_satz' => $orig['ust_satz'],
'buchungstext' => 'STORNO zu ' . $orig['buchungs_nr'] . ': ' . $reason,
];
$year = (int)date('Y', strtotime($reversalData['buchungs_datum']));
$fy = FiscalYear::findOrCreateByYear($year);
$nr = self::generateBuchungsNr();
Database::execute(
'INSERT INTO bookings (buchungs_nr, beleg_id, buchungs_datum, buchungstyp,
kategorie_konto_id, zahlungskonto_id,
betrag_netto, ust_satz, betrag_ust, betrag_brutto, buchungstext, is_reversal, fiscal_year_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?)',
[
$nr,
$reversalData['beleg_id'],
$reversalData['buchungs_datum'],
$reversalData['buchungstyp'],
$reversalData['kategorie_konto_id'],
$reversalData['zahlungskonto_id'],
$orig['betrag_netto'],
$orig['ust_satz'],
$orig['betrag_ust'],
$orig['betrag_brutto'],
$reversalData['buchungstext'],
$fy['id'],
]
);
$reversalId = (int)Database::lastInsertId();
// Link original to reversal
Database::execute('UPDATE bookings SET reversed_by=? WHERE id=?', [$reversalId, $id]);
return $reversalId;
}
public static function generateBuchungsNr(): string
{
$year = date('Y');
$result = Database::fetchOne(
'SELECT COUNT(*) as cnt FROM bookings WHERE YEAR(buchungs_datum) = ?',
[$year]
);
$next = (int)($result['cnt'] ?? 0) + 1;
return sprintf('BU-%s-%06d', $year, $next);
}
public static function sumByYear(int $year): array
{
return Database::fetchAll(
'SELECT bk.buchungstyp, bk.kategorie_konto_id, bk.zahlungskonto_id,
SUM(bk.betrag_netto) as sum_netto,
SUM(bk.betrag_ust) as sum_ust,
SUM(bk.betrag_brutto) as sum_brutto
FROM bookings bk
JOIN fiscal_years fy ON bk.fiscal_year_id = fy.id
WHERE fy.year = ?
GROUP BY bk.buchungstyp, bk.kategorie_konto_id, bk.zahlungskonto_id',
[$year]
);
}
}