Feature: Bank-Zuordnung zeigt Ausgangsrechnungen inkl. Rechnungen ohne Beleg

- Eingabe-Spalte aus Beleg-Liste entfernt (alloc_betrag als Hidden-Field)
- Beleg-Liste nach Datum neu->alt sortiert
- Ausgangsrechnungen ohne Bank-Link in offene Belege aufgenommen
- Rechnungen mit fehlendem Beleg (beleg_id IS NULL) werden angezeigt
- Beim Verknüpfen solcher Rechnungen wird Beleg-PDF automatisch erstellt
This commit is contained in:
fds2610-bot 2026-05-15 19:09:45 +02:00
parent 700dc2952f
commit 4fb0b21a90
5 changed files with 103 additions and 17 deletions

View file

@ -72,6 +72,7 @@ $router->post('/bank/upload/confirm', fn($p) => (new BankController())->up
$router->get('/bank/{id}', fn($p) => (new BankController())->show($p)); $router->get('/bank/{id}', fn($p) => (new BankController())->show($p));
$router->post('/bank/{id}/status', fn($p) => (new BankController())->updateStatus($p)); $router->post('/bank/{id}/status', fn($p) => (new BankController())->updateStatus($p));
$router->post('/bank/{id}/link', fn($p) => (new BankController())->linkBeleg($p)); $router->post('/bank/{id}/link', fn($p) => (new BankController())->linkBeleg($p));
$router->post('/bank/{id}/link-invoice', fn($p) => (new BankController())->linkInvoice($p));
$router->post('/bank/{id}/unlink', fn($p) => (new BankController())->unlinkBeleg($p)); $router->post('/bank/{id}/unlink', fn($p) => (new BankController())->unlinkBeleg($p));
$router->post('/bank/{id}/beleg', fn($p) => (new BankController())->uploadBeleg($p)); $router->post('/bank/{id}/beleg', fn($p) => (new BankController())->uploadBeleg($p));

View file

@ -6,8 +6,10 @@ namespace Steuerkram\Controllers;
use Steuerkram\Models\BankImportFormat; use Steuerkram\Models\BankImportFormat;
use Steuerkram\Models\BankTransaction; use Steuerkram\Models\BankTransaction;
use Steuerkram\Controllers\InvoiceController;
use Steuerkram\Database; use Steuerkram\Database;
use Steuerkram\Models\Beleg; use Steuerkram\Models\Beleg;
use Steuerkram\Models\Invoice;
use Steuerkram\Services\BankImportParser; use Steuerkram\Services\BankImportParser;
use Steuerkram\View; use Steuerkram\View;
@ -161,7 +163,7 @@ class BankController
$linkedBelege = BankTransaction::getLinkedBelege($id); $linkedBelege = BankTransaction::getLinkedBelege($id);
$remainingBetrag = BankTransaction::getRemainingBetrag($id); $remainingBetrag = BankTransaction::getRemainingBetrag($id);
$offeneBelege = Beleg::all(['booked' => '0'], 1, 200); $offeneBelege = Beleg::getOffeneFuerBankzuordnung();
$view = new View(); $view = new View();
$view->render('layout/base.php', [ $view->render('layout/base.php', [
@ -170,7 +172,7 @@ class BankController
'transaction' => $transaction, 'transaction' => $transaction,
'linkedBelege' => $linkedBelege, 'linkedBelege' => $linkedBelege,
'remainingBetrag' => $remainingBetrag, 'remainingBetrag' => $remainingBetrag,
'offeneBelege' => $offeneBelege['rows'], 'offeneBelege' => $offeneBelege,
]), ]),
]); ]);
} }
@ -205,6 +207,36 @@ class BankController
View::redirect("/belege/$belegId?bank_tx=$id&alloc_betrag=$betrag"); View::redirect("/belege/$belegId?bank_tx=$id&alloc_betrag=$betrag");
} }
public function linkInvoice(array $params): void
{
$id = (int)($params['id'] ?? 0);
$invoiceId = (int)($_POST['invoice_id'] ?? 0);
$betrag = round((float)str_replace(',', '.', $_POST['alloc_betrag'] ?? ''), 2);
if (!$invoiceId) {
View::redirect("/bank/$id", 'Keine Rechnung ausgewählt.', 'error');
}
$invoice = Invoice::find($invoiceId);
if (!$invoice) {
View::redirect("/bank/$id", 'Rechnung nicht gefunden.', 'error');
}
try {
InvoiceController::createBeleg($invoice);
$invoice = Invoice::find($invoiceId);
} catch (\Exception $e) {
View::redirect("/bank/$id", 'Beleg konnte nicht erstellt werden: ' . $e->getMessage(), 'error');
}
$belegId = (int)($invoice['beleg_id'] ?? 0);
if (!$belegId) {
View::redirect("/bank/$id", 'Beleg-Erstellung fehlgeschlagen.', 'error');
}
View::redirect("/belege/$belegId?bank_tx=$id&alloc_betrag=$betrag");
}
public function unlinkBeleg(array $params): void public function unlinkBeleg(array $params): void
{ {
$id = (int)($params['id'] ?? 0); $id = (int)($params['id'] ?? 0);

View file

@ -212,7 +212,7 @@ class InvoiceController
\Steuerkram\Database::beginTransaction(); \Steuerkram\Database::beginTransaction();
$nr = Invoice::finalize($id); $nr = Invoice::finalize($id);
$invoice = Invoice::find($id); $invoice = Invoice::find($id);
$belegNr = $this->createBeleg($invoice); $belegNr = self::createBeleg($invoice);
\Steuerkram\Database::commit(); \Steuerkram\Database::commit();
View::redirect("/invoices/{$id}", "Rechnung {$nr} wurde finalisiert (Beleg {$belegNr}). Bitte Zahlungseingang erfassen."); View::redirect("/invoices/{$id}", "Rechnung {$nr} wurde finalisiert (Beleg {$belegNr}). Bitte Zahlungseingang erfassen.");
} catch (\PDOException $e) { } catch (\PDOException $e) {
@ -478,7 +478,7 @@ class InvoiceController
} }
} }
private function createBeleg(array $invoice): string public static function createBeleg(array $invoice): string
{ {
$id = (int)$invoice['id']; $id = (int)$invoice['id'];
$lines = Invoice::lines($id); $lines = Invoice::lines($id);
@ -550,7 +550,7 @@ class InvoiceController
$stornoId = Invoice::createCancellation((int)$params['id']); $stornoId = Invoice::createCancellation((int)$params['id']);
$storno = Invoice::find($stornoId); $storno = Invoice::find($stornoId);
if ($storno) { if ($storno) {
$this->createBeleg($storno); self::createBeleg($storno);
} }
View::redirect('/invoices/' . $stornoId, 'Stornorechnung erstellt.'); View::redirect('/invoices/' . $stornoId, 'Stornorechnung erstellt.');
} }

View file

@ -72,6 +72,50 @@ class Beleg
return ['rows' => $rows, 'total' => $total, 'pages' => (int)ceil($total / $perPage)]; 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),
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 public static function find(int $id): array|false
{ {
return Database::fetchOne( return Database::fetchOne(

View file

@ -222,18 +222,22 @@ $restFormatted = number_format($remaining, 2, ',', '.');
<th>Typ</th> <th>Typ</th>
<th>Lieferant</th> <th>Lieferant</th>
<th class="td-right">Brutto</th> <th class="td-right">Brutto</th>
<th class="td-right" style="width:130px">Zuzuordnen (€)</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<?php foreach ($offeneBelege as $b): ?> <?php foreach ($offeneBelege as $b): ?>
<?php $hatBeleg = $b['id'] !== null; ?>
<tr class="beleg-row" <tr class="beleg-row"
data-beleg-id="<?= (int)$b['id'] ?>" data-beleg-id="<?= (int)$b['id'] ?>"
style="cursor:pointer" style="cursor:<?= $hatBeleg ? 'pointer' : 'default' ?>"
title="Klicken zum Vorschau anzeigen"> <?= $hatBeleg ? 'title="Klicken zum Vorschau anzeigen"' : '' ?>>
<td class="td-mono" onclick="event.stopPropagation()"> <td class="td-mono" onclick="event.stopPropagation()">
<?php if ($hatBeleg): ?>
<a href="/belege/<?= (int)$b['id'] ?>" target="_blank"><?= $h($b['beleg_nr']) ?></a> <a href="/belege/<?= (int)$b['id'] ?>" target="_blank"><?= $h($b['beleg_nr']) ?></a>
<?php else: ?>
<a href="/invoices/<?= (int)$b['invoice_id'] ?>" target="_blank"><?= $h($b['beleg_nr']) ?></a>
<?php endif; ?>
</td> </td>
<td class="td-mono"><?= $h(View::formatDate($b['beleg_datum'])) ?></td> <td class="td-mono"><?= $h(View::formatDate($b['beleg_datum'])) ?></td>
<td><span class="badge badge-gray"><?= $h($b['belegtyp']) ?></span></td> <td><span class="badge badge-gray"><?= $h($b['belegtyp']) ?></span></td>
@ -243,17 +247,22 @@ $restFormatted = number_format($remaining, 2, ',', '.');
<td class="td-right td-mono"> <td class="td-right td-mono">
<?= $b['betrag_brutto'] ? $fmt($b['betrag_brutto']) . ' €' : '<span class="text-muted">–</span>' ?> <?= $b['betrag_brutto'] ? $fmt($b['betrag_brutto']) . ' €' : '<span class="text-muted">–</span>' ?>
</td> </td>
<td class="td-right" onclick="event.stopPropagation()"> <td onclick="event.stopPropagation()">
<form method="post" action="/bank/<?= (int)$transaction['id'] ?>/link" <?php if ($hatBeleg): ?>
style="display:flex;gap:.4rem;align-items:center;justify-content:flex-end"> <form method="post" action="/bank/<?= (int)$transaction['id'] ?>/link">
<input type="hidden" name="beleg_id" value="<?= (int)$b['id'] ?>"> <input type="hidden" name="beleg_id" value="<?= (int)$b['id'] ?>">
<input type="number" name="alloc_betrag" <input type="hidden" name="alloc_betrag"
value="<?= $h(number_format($remaining, 2, '.', '')) ?>" value="<?= $h(number_format($remaining, 2, '.', '')) ?>">
min="0.01" step="0.01"
style="width:90px;text-align:right"
required>
<button type="submit" class="btn btn-primary btn-sm">Verknüpfen</button> <button type="submit" class="btn btn-primary btn-sm">Verknüpfen</button>
</form> </form>
<?php else: ?>
<form method="post" action="/bank/<?= (int)$transaction['id'] ?>/link-invoice">
<input type="hidden" name="invoice_id" value="<?= (int)$b['invoice_id'] ?>">
<input type="hidden" name="alloc_betrag"
value="<?= $h(number_format($remaining, 2, '.', '')) ?>">
<button type="submit" class="btn btn-primary btn-sm">Verknüpfen</button>
</form>
<?php endif; ?>
</td> </td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>