294 lines
11 KiB
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'];
|
|
}
|
|
}
|