Feature: Bankbuchung auf mehrere Belege aufteilen mit Restsaldo und Status teilweise
This commit is contained in:
parent
ce7a64534b
commit
702b0e4664
10 changed files with 273 additions and 52 deletions
12
Changes.md
12
Changes.md
|
|
@ -2,6 +2,18 @@
|
|||
|
||||
---
|
||||
|
||||
## 2026-05-15 – Feature: Bankbuchung auf mehrere Belege aufteilen
|
||||
|
||||
- Migration 053: Junction-Tabelle `bank_transaction_belege` (bank_tx_id, beleg_id, betrag); `bank_transactions.beleg_id` entfernt; Status-ENUM um `teilweise` erweitert; bestehende Verknüpfungen automatisch migriert
|
||||
- Neuer Status `teilweise`: wird gesetzt wenn mind. ein Beleg verknüpft ist, aber noch Restbetrag offen bleibt
|
||||
- Standardfilter in der Bankübersicht zeigt jetzt „Offen & Teilweise"
|
||||
- `bank/show.php`: Sektion „Verknüpfte Belege" mit Betrag je Beleg und Entfernen-Button; Restsaldo-Anzeige; Betrag-Eingabefeld pro Beleg in der Offene-Belege-Liste
|
||||
- Beim Verknüpfen wird der zugeordnete Betrag an die Buchungsmaske weitergegeben (`alloc_betrag`)
|
||||
- `BankTransaction::linkBeleg()` schreibt jetzt in Junction-Tabelle mit Betrag; `recalcStatus()` setzt Status automatisch; `unlinkBeleg()` entfernt Verknüpfung; `getLinkedBelege()` / `getRemainingBetrag()` neu
|
||||
- Neue Route `POST /bank/{id}/unlink`
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-15 – Feature: Dublettenprüfung beim Bank-Import
|
||||
|
||||
- Nach dem Upload wird geprüft, welche Buchungen bereits in der DB vorhanden sind (Abgleich über Buchungstag, Wertstellung, Betrag, Buchungstext)
|
||||
|
|
|
|||
24
migrations/053_bank_transaction_belege.sql
Normal file
24
migrations/053_bank_transaction_belege.sql
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
-- n:m-Verknüpfung Bankbuchung <-> Belege mit zugewiesenem Betrag
|
||||
CREATE TABLE IF NOT EXISTS bank_transaction_belege (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
bank_tx_id INT UNSIGNED NOT NULL,
|
||||
beleg_id INT UNSIGNED NOT NULL,
|
||||
betrag DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uq_tx_beleg (bank_tx_id, beleg_id),
|
||||
FOREIGN KEY (bank_tx_id) REFERENCES bank_transactions(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (beleg_id) REFERENCES belege(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Bestehende 1:1-Verknüpfungen migrieren
|
||||
INSERT IGNORE INTO bank_transaction_belege (bank_tx_id, beleg_id, betrag)
|
||||
SELECT bt.id, bt.beleg_id, ABS(bt.wert)
|
||||
FROM bank_transactions bt
|
||||
WHERE bt.beleg_id IS NOT NULL;
|
||||
|
||||
-- Neuer Status 'teilweise'
|
||||
ALTER TABLE bank_transactions
|
||||
MODIFY COLUMN status ENUM('offen','teilweise','gebucht','ignoriert') NOT NULL DEFAULT 'offen';
|
||||
|
||||
-- Altes Einzelfeld entfernen
|
||||
ALTER TABLE bank_transactions DROP COLUMN beleg_id;
|
||||
|
|
@ -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->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}/unlink', fn($p) => (new BankController())->unlinkBeleg($p));
|
||||
$router->post('/bank/{id}/beleg', fn($p) => (new BankController())->uploadBeleg($p));
|
||||
|
||||
// ── Buchungen ────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class BankController
|
|||
{
|
||||
$filter = $_GET;
|
||||
if (!isset($filter['status'])) {
|
||||
$filter['status'] = 'offen';
|
||||
$filter['status'] = 'pending';
|
||||
}
|
||||
|
||||
$page = max(1, (int)($_GET['page'] ?? 1));
|
||||
|
|
@ -159,15 +159,18 @@ class BankController
|
|||
View::redirect('/bank', 'Buchung nicht gefunden.', 'error');
|
||||
}
|
||||
|
||||
// Offene Belege (noch nicht gebucht)
|
||||
$offeneBelege = Beleg::all(['booked' => '0'], 1, 200);
|
||||
$linkedBelege = BankTransaction::getLinkedBelege($id);
|
||||
$remainingBetrag = BankTransaction::getRemainingBetrag($id);
|
||||
$offeneBelege = Beleg::all(['booked' => '0'], 1, 200);
|
||||
|
||||
$view = new View();
|
||||
$view->render('layout/base.php', [
|
||||
'pageTitle' => 'Bankbuchung zuordnen',
|
||||
'content' => $this->renderView('bank/show', [
|
||||
'transaction' => $transaction,
|
||||
'offeneBelege' => $offeneBelege['rows'],
|
||||
'transaction' => $transaction,
|
||||
'linkedBelege' => $linkedBelege,
|
||||
'remainingBetrag' => $remainingBetrag,
|
||||
'offeneBelege' => $offeneBelege['rows'],
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
|
@ -177,7 +180,7 @@ class BankController
|
|||
$id = (int)($params['id'] ?? 0);
|
||||
$status = $_POST['status'] ?? '';
|
||||
|
||||
if (!in_array($status, ['offen', 'gebucht', 'ignoriert'], true)) {
|
||||
if (!in_array($status, ['offen', 'teilweise', 'gebucht', 'ignoriert'], true)) {
|
||||
View::redirect('/bank', 'Ungültiger Status.', 'error');
|
||||
}
|
||||
|
||||
|
|
@ -190,13 +193,25 @@ class BankController
|
|||
{
|
||||
$id = (int)($params['id'] ?? 0);
|
||||
$belegId = (int)($_POST['beleg_id'] ?? 0);
|
||||
$betrag = round((float)str_replace(',', '.', $_POST['alloc_betrag'] ?? ''), 2);
|
||||
|
||||
if (!$belegId) {
|
||||
View::redirect("/bank/$id", 'Kein Beleg ausgewählt.', 'error');
|
||||
}
|
||||
if ($betrag <= 0) {
|
||||
View::redirect("/bank/$id", 'Bitte einen gültigen Betrag eingeben.', 'error');
|
||||
}
|
||||
|
||||
BankTransaction::linkBeleg($id, $belegId);
|
||||
View::redirect('/bank', 'Beleg verknüpft und Buchung als gebucht markiert.');
|
||||
View::redirect("/belege/$belegId?bank_tx=$id&alloc_betrag=$betrag");
|
||||
}
|
||||
|
||||
public function unlinkBeleg(array $params): void
|
||||
{
|
||||
$id = (int)($params['id'] ?? 0);
|
||||
$belegId = (int)($_POST['beleg_id'] ?? 0);
|
||||
|
||||
BankTransaction::unlinkBeleg($id, $belegId);
|
||||
View::redirect("/bank/$id", 'Beleg-Verknüpfung entfernt.');
|
||||
}
|
||||
|
||||
public function uploadBeleg(array $params): void
|
||||
|
|
@ -248,8 +263,9 @@ class BankController
|
|||
'belegtyp' => (float)$transaction['wert'] >= 0 ? 'Ausgangsrechnung' : 'Eingangsrechnung',
|
||||
]);
|
||||
|
||||
BankTransaction::linkBeleg($id, $belegId);
|
||||
View::redirect('/bank', 'Beleg hochgeladen und Buchung als gebucht markiert.');
|
||||
$betrag = BankTransaction::getRemainingBetrag($id) ?: abs((float)$transaction['wert']);
|
||||
BankTransaction::linkBeleg($id, $belegId, $betrag);
|
||||
View::redirect('/bank', 'Beleg hochgeladen und Buchung verknüpft.');
|
||||
}
|
||||
|
||||
private function renderView(string $name, array $data = []): string
|
||||
|
|
|
|||
|
|
@ -223,12 +223,15 @@ class BelegController
|
|||
if (preg_match('/Auftraggeber:\s*(.*?)(?:\s*Buchungstext|\s*$)/si', $bankTx['buchungstext'], $m)) {
|
||||
$bankTxSupplier = trim($m[1]);
|
||||
}
|
||||
$bankTxBrutto = abs((float)$bankTx['wert']);
|
||||
// alloc_betrag aus URL hat Vorrang (Teilbetrag bei Aufteilung), sonst voller Betrag
|
||||
$allocBetrag = isset($_GET['alloc_betrag']) ? round((float)$_GET['alloc_betrag'], 2) : 0.0;
|
||||
$bankTxBrutto = $allocBetrag > 0 ? $allocBetrag : abs((float)$bankTx['wert']);
|
||||
$netto = round($bankTxBrutto / 1.19, 2);
|
||||
|
||||
$bankFormData = [
|
||||
'buchungs_datum' => $bankTx['wertstellung'],
|
||||
'betrag_netto' => number_format($netto, 2, '.', ''),
|
||||
'alloc_betrag' => number_format($bankTxBrutto, 2, '.', ''),
|
||||
'zahlungs_kontonummer'=> '1200',
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -184,9 +184,13 @@ class BookingController
|
|||
}
|
||||
|
||||
// Bankbuchung verknüpfen wenn aus Bank-Kontext
|
||||
$bankTxId = (int)($data['bank_tx_id'] ?? 0);
|
||||
$bankTxId = (int)($data['bank_tx_id'] ?? 0);
|
||||
$allocBetrag = round((float)str_replace(',', '.', $data['alloc_betrag'] ?? ''), 2);
|
||||
if ($bankTxId > 0) {
|
||||
BankTransaction::linkBeleg($bankTxId, $belegId);
|
||||
if ($allocBetrag <= 0) {
|
||||
$allocBetrag = BankTransaction::getRemainingBetrag($bankTxId) ?: abs((float)$data['betrag_netto'] ?? 0);
|
||||
}
|
||||
BankTransaction::linkBeleg($bankTxId, $belegId, $allocBetrag);
|
||||
}
|
||||
|
||||
if ($newAssetId !== null) {
|
||||
|
|
|
|||
|
|
@ -13,8 +13,10 @@ class BankTransaction
|
|||
$where = ['1=1'];
|
||||
$params = [];
|
||||
|
||||
$status = $filter['status'] ?? 'offen';
|
||||
if ($status !== '') {
|
||||
$status = $filter['status'] ?? 'pending';
|
||||
if ($status === 'pending') {
|
||||
$where[] = "bt.status IN ('offen','teilweise')";
|
||||
} elseif ($status !== '') {
|
||||
$where[] = 'bt.status = ?';
|
||||
$params[] = $status;
|
||||
}
|
||||
|
|
@ -51,11 +53,17 @@ class BankTransaction
|
|||
|
||||
$rows = Database::fetchAll(
|
||||
"SELECT bt.*, bib.filename AS batch_filename, bif.name AS format_name,
|
||||
b.id AS beleg_id, b.beleg_nr
|
||||
btb_agg.beleg_id AS beleg_id, btb_agg.beleg_count,
|
||||
b.beleg_nr
|
||||
FROM bank_transactions bt
|
||||
JOIN bank_import_batches bib ON bt.batch_id = bib.id
|
||||
JOIN bank_import_formats bif ON bib.format_id = bif.id
|
||||
LEFT JOIN belege b ON b.id = bt.beleg_id
|
||||
LEFT JOIN (
|
||||
SELECT bank_tx_id, MIN(beleg_id) AS beleg_id, COUNT(*) AS beleg_count
|
||||
FROM bank_transaction_belege
|
||||
GROUP BY bank_tx_id
|
||||
) btb_agg ON btb_agg.bank_tx_id = bt.id
|
||||
LEFT JOIN belege b ON b.id = btb_agg.beleg_id
|
||||
WHERE $whereStr
|
||||
ORDER BY bt.wertstellung DESC, bt.id DESC
|
||||
LIMIT ? OFFSET ?",
|
||||
|
|
@ -90,17 +98,105 @@ class BankTransaction
|
|||
);
|
||||
}
|
||||
|
||||
public static function linkBeleg(int $id, int $belegId): void
|
||||
/** Verknüpft einen Beleg mit zugeordnetem Betrag und aktualisiert den Status. */
|
||||
public static function linkBeleg(int $txId, int $belegId, float $betrag): void
|
||||
{
|
||||
Database::execute(
|
||||
"UPDATE bank_transactions SET beleg_id = ?, status = 'gebucht' WHERE id = ?",
|
||||
[$belegId, $id]
|
||||
'INSERT INTO bank_transaction_belege (bank_tx_id, beleg_id, betrag)
|
||||
VALUES (?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE betrag = VALUES(betrag)',
|
||||
[$txId, $belegId, $betrag]
|
||||
);
|
||||
self::recalcStatus($txId);
|
||||
}
|
||||
|
||||
/** Entfernt eine Beleg-Verknüpfung und aktualisiert den Status. */
|
||||
public static function unlinkBeleg(int $txId, int $belegId): void
|
||||
{
|
||||
Database::execute(
|
||||
'DELETE FROM bank_transaction_belege WHERE bank_tx_id = ? AND beleg_id = ?',
|
||||
[$txId, $belegId]
|
||||
);
|
||||
self::recalcStatus($txId);
|
||||
}
|
||||
|
||||
/** Berechnet Status neu: 0 Links→offen, Restsaldo>0→teilweise, Restsaldo=0→gebucht. */
|
||||
public static function recalcStatus(int $txId): void
|
||||
{
|
||||
$row = Database::fetchOne(
|
||||
'SELECT ABS(bt.wert) AS brutto, COALESCE(SUM(btb.betrag), 0) AS allocated
|
||||
FROM bank_transactions bt
|
||||
LEFT JOIN bank_transaction_belege btb ON btb.bank_tx_id = bt.id
|
||||
WHERE bt.id = ?
|
||||
GROUP BY bt.id',
|
||||
[$txId]
|
||||
);
|
||||
if (!$row) return;
|
||||
|
||||
$remaining = round((float)$row['brutto'] - (float)$row['allocated'], 2);
|
||||
$allocated = (float)$row['allocated'];
|
||||
|
||||
if ($allocated <= 0) {
|
||||
$status = 'offen';
|
||||
} elseif ($remaining > 0.01) {
|
||||
$status = 'teilweise';
|
||||
} else {
|
||||
$status = 'gebucht';
|
||||
}
|
||||
|
||||
Database::execute(
|
||||
"UPDATE bank_transactions SET status = ? WHERE id = ?",
|
||||
[$status, $txId]
|
||||
);
|
||||
}
|
||||
|
||||
/** Gibt alle verknüpften Belege mit ihren Beträgen zurück. */
|
||||
public static function getLinkedBelege(int $txId): array
|
||||
{
|
||||
return Database::fetchAll(
|
||||
'SELECT btb.betrag, b.id AS beleg_id, b.beleg_nr, b.beleg_datum,
|
||||
b.belegtyp, b.betrag_brutto, s.name AS supplier_name
|
||||
FROM bank_transaction_belege btb
|
||||
JOIN belege b ON b.id = btb.beleg_id
|
||||
LEFT JOIN suppliers s ON s.id = b.supplier_id
|
||||
WHERE btb.bank_tx_id = ?
|
||||
ORDER BY b.beleg_nr',
|
||||
[$txId]
|
||||
);
|
||||
}
|
||||
|
||||
/** Gibt den noch nicht zugeordneten Restbetrag zurück. */
|
||||
public static function getRemainingBetrag(int $txId): float
|
||||
{
|
||||
$row = Database::fetchOne(
|
||||
'SELECT ABS(bt.wert) AS brutto, COALESCE(SUM(btb.betrag), 0) AS allocated
|
||||
FROM bank_transactions bt
|
||||
LEFT JOIN bank_transaction_belege btb ON btb.bank_tx_id = bt.id
|
||||
WHERE bt.id = ?
|
||||
GROUP BY bt.id',
|
||||
[$txId]
|
||||
);
|
||||
if (!$row) return 0.0;
|
||||
return round(max(0.0, (float)$row['brutto'] - (float)$row['allocated']), 2);
|
||||
}
|
||||
|
||||
/** Sucht die Bankbuchung, die mit einem bestimmten Beleg verknüpft ist. */
|
||||
public static function findByBelegId(int $belegId): ?array
|
||||
{
|
||||
$row = Database::fetchOne(
|
||||
'SELECT bt.*, bib.filename AS batch_filename, btb.betrag AS alloc_betrag
|
||||
FROM bank_transaction_belege btb
|
||||
JOIN bank_transactions bt ON bt.id = btb.bank_tx_id
|
||||
JOIN bank_import_batches bib ON bib.id = bt.batch_id
|
||||
WHERE btb.beleg_id = ?
|
||||
LIMIT 1',
|
||||
[$belegId]
|
||||
);
|
||||
return $row ?: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Teilt $rows in ['new' => [...], 'dup' => [...]] auf.
|
||||
* Eine Zeile gilt als Dublette, wenn buchungstag + wertstellung + wert + buchungstext(200) bereits existieren.
|
||||
*/
|
||||
public static function findDuplicates(array $rows): array
|
||||
{
|
||||
|
|
@ -108,7 +204,6 @@ class BankTransaction
|
|||
return ['new' => [], 'dup' => []];
|
||||
}
|
||||
|
||||
// Kandidaten-Abfrage: alle DB-Zeilen, deren (buchungstag, wertstellung, wert) in den Upload-Daten vorkommen
|
||||
$placeholders = implode(',', array_fill(0, count($rows), '(?,?,?)'));
|
||||
$params = [];
|
||||
foreach ($rows as $r) {
|
||||
|
|
@ -124,7 +219,6 @@ class BankTransaction
|
|||
$params
|
||||
);
|
||||
|
||||
// Lookup-Set für schnellen Vergleich
|
||||
$lookup = [];
|
||||
foreach ($existing as $e) {
|
||||
$lookup[$e['buchungstag'] . '|' . $e['wertstellung'] . '|' . $e['wert'] . '|' . $e['btext']] = true;
|
||||
|
|
@ -147,19 +241,6 @@ class BankTransaction
|
|||
return ['new' => $new, 'dup' => $dup];
|
||||
}
|
||||
|
||||
public static function findByBelegId(int $belegId): ?array
|
||||
{
|
||||
$row = Database::fetchOne(
|
||||
'SELECT bt.*, bib.filename AS batch_filename
|
||||
FROM bank_transactions bt
|
||||
JOIN bank_import_batches bib ON bib.id = bt.batch_id
|
||||
WHERE bt.beleg_id = ?
|
||||
LIMIT 1',
|
||||
[$belegId]
|
||||
);
|
||||
return $row ?: null;
|
||||
}
|
||||
|
||||
public static function createBatch(int $formatId, string $filename, int $rowCount): int
|
||||
{
|
||||
Database::execute(
|
||||
|
|
|
|||
|
|
@ -36,11 +36,13 @@ $months = ['Jan','Feb','Mär','Apr','Mai','Jun','Jul','Aug','Sep','Okt','Nov','D
|
|||
<option value="ausgabe"<?= ($filter['typ'] ?? '') === 'ausgabe' ? ' selected' : '' ?>>Ausgaben</option>
|
||||
</select>
|
||||
|
||||
<select name="status" style="width:110px">
|
||||
<option value="offen"<?= ($filter['status'] ?? 'offen') === 'offen' ? ' selected' : '' ?>>Offen</option>
|
||||
<select name="status" style="width:140px">
|
||||
<option value="pending"<?= ($filter['status'] ?? 'pending') === 'pending' ? ' selected' : '' ?>>Offen & Teilweise</option>
|
||||
<option value="offen"<?= ($filter['status'] ?? '') === 'offen' ? ' selected' : '' ?>>Nur Offen</option>
|
||||
<option value="teilweise"<?= ($filter['status'] ?? '') === 'teilweise' ? ' selected' : '' ?>>Teilweise</option>
|
||||
<option value="gebucht"<?= ($filter['status'] ?? '') === 'gebucht' ? ' selected' : '' ?>>Gebucht</option>
|
||||
<option value="ignoriert"<?= ($filter['status'] ?? '') === 'ignoriert' ? ' selected' : '' ?>>Ignoriert</option>
|
||||
<option value=""<?= ($filter['status'] ?? 'offen') === '' ? ' selected' : '' ?>>Alle</option>
|
||||
<option value=""<?= ($filter['status'] ?? 'pending') === '' ? ' selected' : '' ?>>Alle</option>
|
||||
</select>
|
||||
|
||||
<button type="submit" class="btn btn-secondary btn-sm">Filtern</button>
|
||||
|
|
@ -82,9 +84,10 @@ $months = ['Jan','Feb','Mär','Apr','Mai','Jun','Jul','Aug','Sep','Okt','Nov','D
|
|||
$wert = (float)$row['wert'];
|
||||
$isPlus = $wert >= 0;
|
||||
$color = $isPlus ? 'var(--color-success)' : 'var(--color-danger)';
|
||||
$ziel = ($row['status'] === 'gebucht' && !empty($row['beleg_id']))
|
||||
? '/belege/' . (int)$row['beleg_id']
|
||||
: '/bank/' . (int)$row['id'];
|
||||
$ziel = '/bank/' . (int)$row['id'];
|
||||
if ($row['status'] === 'gebucht' && !empty($row['beleg_id']) && ($row['beleg_count'] ?? 1) == 1) {
|
||||
$ziel = '/belege/' . (int)$row['beleg_id'];
|
||||
}
|
||||
?>
|
||||
<tr style="cursor:pointer" onclick="window.location='<?= $ziel ?>'">
|
||||
<td class="td-mono"><?= $h(View::formatDate($row['wertstellung'])) ?></td>
|
||||
|
|
@ -101,13 +104,20 @@ $months = ['Jan','Feb','Mär','Apr','Mai','Jun','Jul','Aug','Sep','Okt','Nov','D
|
|||
<?php
|
||||
$badgeClass = match($row['status']) {
|
||||
'gebucht' => 'badge-green',
|
||||
'teilweise' => 'badge-warning',
|
||||
'ignoriert' => 'badge-gray',
|
||||
default => 'badge-blue',
|
||||
};
|
||||
?>
|
||||
<span class="badge <?= $badgeClass ?>"><?= $h(ucfirst($row['status'])) ?></span>
|
||||
<?php if ($row['status'] === 'gebucht' && !empty($row['beleg_nr'])): ?>
|
||||
<div class="text-muted text-sm" style="margin-top:.25rem"><?= $h($row['beleg_nr']) ?></div>
|
||||
<?php if (in_array($row['status'], ['gebucht','teilweise']) && !empty($row['beleg_nr'])): ?>
|
||||
<div class="text-muted text-sm" style="margin-top:.25rem">
|
||||
<?php if (($row['beleg_count'] ?? 1) > 1): ?>
|
||||
<?= (int)$row['beleg_count'] ?> Belege
|
||||
<?php else: ?>
|
||||
<?= $h($row['beleg_nr']) ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td onclick="event.stopPropagation()">
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@ $color = $isPlus ? 'var(--color-success)' : 'var(--color-danger)';
|
|||
$settings = Setting::get();
|
||||
$vorschauBreite = (int)($settings['beleg_vorschau_breite'] ?? 200);
|
||||
$vorschauHoehe = (int)($settings['beleg_vorschau_hoehe'] ?? 300);
|
||||
|
||||
$remaining = (float)$remainingBetrag;
|
||||
$totalBrutto = abs($wert);
|
||||
$restFormatted = number_format($remaining, 2, ',', '.');
|
||||
?>
|
||||
|
||||
<!-- Bankbuchung Info-Bar -->
|
||||
|
|
@ -33,16 +37,23 @@ $vorschauHoehe = (int)($settings['beleg_vorschau_hoehe'] ?? 300);
|
|||
<span><?= $h($transaction['buchungstext']) ?></span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-muted text-sm">Wert</span><br>
|
||||
<span class="text-muted text-sm">Gesamt</span><br>
|
||||
<strong style="color:<?= $color ?>;font-size:1.1rem">
|
||||
<?= $isPlus ? '+' : '' ?><?= $h(number_format($wert, 2, ',', '.')) ?> €
|
||||
</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-muted text-sm">Verbleibend</span><br>
|
||||
<strong style="color:<?= $remaining <= 0.01 ? 'var(--color-success)' : 'var(--color-warning,#d97706)' ?>;font-size:1.1rem">
|
||||
<?= $h($restFormatted) ?> €
|
||||
</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-muted text-sm">Status</span><br>
|
||||
<?php
|
||||
$badgeClass = match($transaction['status']) {
|
||||
'gebucht' => 'badge-green',
|
||||
'teilweise' => 'badge-warning',
|
||||
'ignoriert' => 'badge-gray',
|
||||
default => 'badge-blue',
|
||||
};
|
||||
|
|
@ -63,9 +74,59 @@ $vorschauHoehe = (int)($settings['beleg_vorschau_hoehe'] ?? 300);
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($linkedBelege)): ?>
|
||||
<!-- Bereits verknüpfte Belege -->
|
||||
<div class="card mb-2">
|
||||
<div class="card-header">
|
||||
<div class="card-title">Verknüpfte Belege</div>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Belegnr.</th>
|
||||
<th>Datum</th>
|
||||
<th>Typ</th>
|
||||
<th>Lieferant</th>
|
||||
<th class="td-right">Belegbetrag</th>
|
||||
<th class="td-right">Zugeordnet</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($linkedBelege as $lb): ?>
|
||||
<tr>
|
||||
<td class="td-mono">
|
||||
<a href="/belege/<?= (int)$lb['beleg_id'] ?>"><?= $h($lb['beleg_nr']) ?></a>
|
||||
</td>
|
||||
<td class="td-mono"><?= $h(View::formatDate($lb['beleg_datum'])) ?></td>
|
||||
<td><span class="badge badge-gray"><?= $h($lb['belegtyp']) ?></span></td>
|
||||
<td><?= $lb['supplier_name'] ? $h($lb['supplier_name']) : '<span class="text-muted">–</span>' ?></td>
|
||||
<td class="td-right td-mono">
|
||||
<?= $lb['betrag_brutto'] ? $fmt($lb['betrag_brutto']) . ' €' : '<span class="text-muted">–</span>' ?>
|
||||
</td>
|
||||
<td class="td-right td-mono" style="font-weight:600">
|
||||
<?= $h(number_format((float)$lb['betrag'], 2, ',', '.')) ?> €
|
||||
</td>
|
||||
<td>
|
||||
<form method="post" action="/bank/<?= (int)$transaction['id'] ?>/unlink"
|
||||
data-confirm="Verknüpfung mit <?= $h($lb['beleg_nr']) ?> wirklich entfernen?">
|
||||
<input type="hidden" name="beleg_id" value="<?= (int)$lb['beleg_id'] ?>">
|
||||
<button type="submit" class="btn btn-danger btn-sm">Entfernen</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($remaining > 0.01): ?>
|
||||
<!-- Beleg hochladen -->
|
||||
<div class="card mb-2">
|
||||
<div class="card-header"><div class="card-title">Beleg hochladen</div></div>
|
||||
<div class="card-header"><div class="card-title">Neuen Beleg hochladen & verknüpfen</div></div>
|
||||
<div class="card-body">
|
||||
<form method="post" action="/bank/<?= (int)$transaction['id'] ?>/beleg" enctype="multipart/form-data">
|
||||
<div style="display:flex;gap:.75rem;align-items:flex-end;flex-wrap:wrap">
|
||||
|
|
@ -102,6 +163,7 @@ $vorschauHoehe = (int)($settings['beleg_vorschau_hoehe'] ?? 300);
|
|||
<th>Typ</th>
|
||||
<th>Lieferant</th>
|
||||
<th class="td-right">Brutto</th>
|
||||
<th class="td-right" style="width:130px">Zuzuordnen (€)</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
|
@ -120,11 +182,19 @@ $vorschauHoehe = (int)($settings['beleg_vorschau_hoehe'] ?? 300);
|
|||
<?= $b['supplier_name'] ? $h($b['supplier_name']) : '<span class="text-muted">–</span>' ?>
|
||||
</td>
|
||||
<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 onclick="event.stopPropagation()">
|
||||
<a href="/belege/<?= (int)$b['id'] ?>?bank_tx=<?= (int)$transaction['id'] ?>"
|
||||
class="btn btn-primary btn-sm">Verknüpfen</a>
|
||||
<td class="td-right" onclick="event.stopPropagation()">
|
||||
<form method="post" action="/bank/<?= (int)$transaction['id'] ?>/link"
|
||||
style="display:flex;gap:.4rem;align-items:center;justify-content:flex-end">
|
||||
<input type="hidden" name="beleg_id" value="<?= (int)$b['id'] ?>">
|
||||
<input type="number" name="alloc_betrag"
|
||||
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>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
|
|
@ -168,12 +238,10 @@ $vorschauHoehe = (int)($settings['beleg_vorschau_hoehe'] ?? 300);
|
|||
const id = row.dataset.belegId;
|
||||
if (!id) return;
|
||||
|
||||
// Hervorhebung wechseln
|
||||
if (activeRow) activeRow.style.background = '';
|
||||
activeRow = row;
|
||||
row.style.background = 'var(--color-primary-light, #eff6ff)';
|
||||
|
||||
// Vorschau laden
|
||||
hint.style.display = 'none';
|
||||
frame.style.display = 'block';
|
||||
frame.src = '/belege/' + id + '/file';
|
||||
|
|
@ -181,3 +249,4 @@ $vorschauHoehe = (int)($settings['beleg_vorschau_hoehe'] ?? 300);
|
|||
});
|
||||
})();
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
|
|
|
|||
|
|
@ -377,6 +377,7 @@ $suppliers = $suppliers ?? [];
|
|||
<form method="post" action="/belege/<?= $h($beleg['id']) ?>/bookings">
|
||||
<?php if ($bankTx ?? null): ?>
|
||||
<input type="hidden" name="bank_tx_id" value="<?= (int)$bankTx['id'] ?>">
|
||||
<input type="hidden" name="alloc_betrag" value="<?= $h($formData['alloc_betrag'] ?? '') ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Buchungstyp -->
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue