Steuerkram/src/Models/Beleg.php

294 lines
11 KiB
PHP

<?php
declare(strict_types=1);
namespace Steuerkram\Models;
use Steuerkram\Database;
class Beleg
{
public static function all(array $filter = [], int $page = 1, int $perPage = 30): array
{
$where = ['1=1'];
$params = [];
if (!empty($filter['search'])) {
$where[] = '(b.beleg_nr LIKE ? OR b.filename LIKE ? OR b.rechnungs_nr LIKE ? OR b.notes LIKE ?
OR CAST(b.beleg_datum AS CHAR) LIKE ?
OR s.name LIKE ?
OR b.bewirtungsgrund LIKE ? OR b.teilnehmerliste LIKE ?
OR (SELECT CAST(SUM(bk2s.betrag_brutto) AS CHAR) FROM bookings bk2s
WHERE bk2s.beleg_id = b.id AND bk2s.is_reversal = 0 AND bk2s.parent_booking_id IS NULL) LIKE ?)';
$s = '%' . $filter['search'] . '%';
$sBrutto = '%' . str_replace(',', '.', $filter['search']) . '%';
$params = array_merge($params, [$s, $s, $s, $s, $s, $s, $s, $s, $sBrutto]);
}
if (!empty($filter['year'])) {
$where[] = '(YEAR(b.beleg_datum) = ? OR b.beleg_datum IS NULL)';
$params[] = (int)$filter['year'];
}
if (!empty($filter['month'])) {
$where[] = '(MONTH(b.beleg_datum) = ? OR b.beleg_datum IS NULL)';
$params[] = (int)$filter['month'];
}
if (isset($filter['booked']) && $filter['booked'] !== '') {
if ((int)$filter['booked'] === 1) {
$where[] = 'EXISTS (SELECT 1 FROM bookings bk2 WHERE bk2.beleg_id = b.id)';
} else {
$where[] = 'NOT EXISTS (SELECT 1 FROM bookings bk2 WHERE bk2.beleg_id = b.id)';
}
}
if (!empty($filter['belegtyp'])) {
$where[] = 'b.belegtyp = ?';
$params[] = $filter['belegtyp'];
}
$offset = ($page - 1) * $perPage;
$sql = 'SELECT b.*,
(SELECT SUM(bk.betrag_brutto) FROM bookings bk
WHERE bk.beleg_id = b.id AND bk.is_reversal = 0 AND bk.parent_booking_id IS NULL AND bk.reversed_by IS NULL
) AS betrag_brutto,
fy.year as fiscal_year_nr,
s.name as supplier_name,
(EXISTS (SELECT 1 FROM bookings bk3 WHERE bk3.beleg_id = b.id)) AS booked
FROM belege b
LEFT JOIN fiscal_years fy ON b.fiscal_year_id = fy.id
LEFT JOIN suppliers s ON b.supplier_id = s.id
WHERE ' . implode(' AND ', $where) . '
ORDER BY b.created_at DESC, b.id DESC
LIMIT ? OFFSET ?';
$params[] = $perPage;
$params[] = $offset;
$rows = Database::fetchAll($sql, $params);
// Count total
$countSql = 'SELECT COUNT(*) as cnt FROM belege b LEFT JOIN suppliers s ON b.supplier_id = s.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 getOffeneFuerBankzuordnung(): array
{
$belege = Database::fetchAll(
'SELECT b.*,
COALESCE(
(SELECT SUM(bk.betrag_brutto) FROM bookings bk
WHERE bk.beleg_id = b.id AND bk.is_reversal = 0 AND bk.parent_booking_id IS NULL AND bk.reversed_by IS NULL),
b.betrag_brutto
) AS betrag_brutto,
fy.year as fiscal_year_nr,
s.name as supplier_name
FROM belege b
LEFT JOIN fiscal_years fy ON b.fiscal_year_id = fy.id
LEFT JOIN suppliers s ON b.supplier_id = s.id
WHERE (
(b.belegtyp = \'Eingangsrechnung\'
AND NOT EXISTS (SELECT 1 FROM bookings bk2 WHERE bk2.beleg_id = b.id))
OR
(b.belegtyp = \'Ausgangsrechnung\'
AND NOT EXISTS (SELECT 1 FROM bank_transaction_belege btb WHERE btb.beleg_id = b.id))
)',
[]
);
$rechnungenOhneBeleg = Database::fetchAll(
'SELECT NULL AS id,
i.id AS invoice_id,
i.rechnungs_nr AS beleg_nr,
i.rechnungs_datum AS beleg_datum,
\'Ausgangsrechnung\' AS belegtyp,
i.betrag_brutto,
COALESCE(NULLIF(TRIM(c.firma), \'\'), CONCAT(TRIM(c.vorname), \' \', TRIM(c.nachname))) AS supplier_name
FROM invoices i
JOIN customers c ON c.id = i.customer_id
WHERE i.status IN (\'Finalisiert\', \'Gesendet\')
AND i.beleg_id IS NULL',
[]
);
$alle = array_merge($belege, $rechnungenOhneBeleg);
usort($alle, fn($a, $b) => strcmp($b['beleg_datum'] ?? '', $a['beleg_datum'] ?? ''));
return $alle;
}
public static function find(int $id): array|false
{
return Database::fetchOne(
'SELECT b.*,
(SELECT SUM(bk.betrag_brutto) FROM bookings bk
WHERE bk.beleg_id = b.id AND bk.is_reversal = 0 AND bk.parent_booking_id IS NULL AND bk.reversed_by IS NULL
) AS betrag_brutto,
fy.year as fiscal_year_nr,
s.name as supplier_name,
(EXISTS (SELECT 1 FROM bookings bk2 WHERE bk2.beleg_id = b.id)) AS booked
FROM belege b
LEFT JOIN fiscal_years fy ON b.fiscal_year_id = fy.id
LEFT JOIN suppliers s ON b.supplier_id = s.id
WHERE b.id = ?',
[$id]
);
}
public static function create(array $data): int
{
// Auto-assign fiscal year
$fyId = null;
if (!empty($data['beleg_datum'])) {
$year = (int)date('Y', strtotime($data['beleg_datum']));
$fy = FiscalYear::findOrCreateByYear($year);
$fyId = $fy['id'];
}
$invoiceId = isset($data['invoice_id']) && $data['invoice_id'] ? (int)$data['invoice_id'] : null;
Database::execute(
'INSERT INTO belege (invoice_id, beleg_nr, filename, storage_path, mime_type, file_hash,
beleg_datum, rechnungs_nr, betrag_brutto, belegtyp, notes, fiscal_year_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
[
$invoiceId,
$data['beleg_nr'],
$data['filename'],
$data['storage_path'],
$data['mime_type'],
$data['file_hash'],
$data['beleg_datum'] ?: null,
$data['rechnungs_nr'] ?? null,
$data['betrag_brutto'] ?? null,
$data['belegtyp'] ?? 'Eingangsrechnung',
$data['notes'] ?? null,
$fyId,
]
);
return (int)Database::lastInsertId();
}
public static function update(int $id, array $data): void
{
$beleg = self::find($id);
if ($beleg && $beleg['locked']) {
throw new \RuntimeException('GoBD: Gesperrter Beleg kann nicht geändert werden.');
}
// Update fiscal year if date changed
$fyId = null;
if (!empty($data['beleg_datum'])) {
$year = (int)date('Y', strtotime($data['beleg_datum']));
$fy = FiscalYear::findOrCreateByYear($year);
$fyId = $fy['id'];
}
Database::execute(
'UPDATE belege SET beleg_datum=?, rechnungs_nr=?, belegtyp=?, notes=?,
fiscal_year_id=?, updated_at=NOW()
WHERE id=? AND locked=0',
[
$data['beleg_datum'] ?: null,
$data['rechnungs_nr'] ?? null,
$data['belegtyp'] ?? 'Eingangsrechnung',
$data['notes'] ?? null,
$fyId,
$id,
]
);
}
public static function updateMeta(int $id, array $data): void
{
$fyId = null;
if (!empty($data['beleg_datum'])) {
$year = (int)date('Y', strtotime($data['beleg_datum']));
$fy = FiscalYear::findOrCreateByYear($year);
$fyId = $fy['id'];
}
Database::execute(
'UPDATE belege SET
rechnungs_nr = ?,
belegtyp = ?,
beleg_datum = ?,
supplier_id = ?,
fiscal_year_id = COALESCE(?, fiscal_year_id),
updated_at = NOW()
WHERE id = ? AND locked = 0',
[
$data['rechnungs_nr'] ?: null,
$data['belegtyp'],
$data['beleg_datum'] ?: null,
isset($data['supplier_id']) && $data['supplier_id'] !== '' ? (int)$data['supplier_id'] : null,
$fyId,
$id,
]
);
}
public static function delete(int $id): bool
{
$beleg = self::find($id);
if (!$beleg) return false;
if ($beleg['locked']) return false;
// Check no bookings exist
$cnt = Database::fetchOne('SELECT COUNT(*) as cnt FROM bookings WHERE beleg_id=?', [$id]);
if ($cnt && $cnt['cnt'] > 0) return false;
// Delete file
$fullPath = dirname(__DIR__, 2) . '/public/' . $beleg['storage_path'];
if (file_exists($fullPath)) unlink($fullPath);
Database::execute('DELETE FROM belege WHERE id=?', [$id]);
return true;
}
public static function generateBelegNr(): string
{
$settings = Database::fetchOne('SELECT beleg_nr_prefix, beleg_nr_next FROM company_settings WHERE id=1');
$prefix = $settings['beleg_nr_prefix'] ?? 'BLG';
$next = (int)($settings['beleg_nr_next'] ?? 1);
$year = date('Y');
$nr = sprintf('%s-%s-%04d', $prefix, $year, $next);
Database::execute('UPDATE company_settings SET beleg_nr_next = beleg_nr_next + 1 WHERE id=1');
return $nr;
}
public static function saveKiVorschlag(int $id, ?array $vorschlag): void
{
Database::execute(
'UPDATE belege SET ki_vorschlag = ?, ki_vorschlag_at = NOW() WHERE id = ?',
[$vorschlag !== null ? json_encode($vorschlag) : null, $id]
);
}
public static function updateHash(int $id, string $hash): void
{
Database::execute(
'UPDATE belege SET file_hash = ?, updated_at = NOW() WHERE id = ?',
[$hash, $id]
);
}
public static function saveBelegText(int $id, string $text): void
{
Database::execute(
'UPDATE belege SET beleg_text = ? WHERE id = ?',
[$text !== '' ? $text : null, $id]
);
}
public static function verifyIntegrity(int $id): bool
{
$beleg = self::find($id);
if (!$beleg) return false;
$fullPath = dirname(__DIR__, 2) . '/public/' . $beleg['storage_path'];
if (!file_exists($fullPath)) return false;
return hash_file('sha256', $fullPath) === $beleg['file_hash'];
}
}