Merge pull request 'Bank-Import: Dublettenprüfung, Fixes & Beleg-Verknüpfung' (#70) from fds2610-bot/Steuerkram:main into main
Reviewed-on: https://codeberg.org/fds2610/Steuerkram/pulls/70
This commit is contained in:
commit
267fa7cd1e
11 changed files with 372 additions and 12 deletions
27
Changes.md
27
Changes.md
|
|
@ -2,6 +2,33 @@
|
|||
|
||||
---
|
||||
|
||||
## 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)
|
||||
- Werden Dubletten gefunden, erscheint eine Prüfseite mit zwei Tabellen: erkannte Dubletten (per Checkbox auswählbar) und neue Buchungen (werden immer importiert)
|
||||
- „Alle auswählen / abwählen"-Buttons + Gesamtzähler zeigen an, wie viele Buchungen importiert werden
|
||||
- Erst nach Bestätigung wird importiert; ohne Auswahl wird der Import übersprungen
|
||||
- Migration 052: UNIQUE-Constraint auf `bank_transactions` entfernt (UI-Prüfung ist die primäre Schutzschicht)
|
||||
- `BankTransaction::findDuplicates()`: neue Methode mit Batch-Abfrage gegen die DB
|
||||
- `BankController::doImport()`: gemeinsame Import-Logik extrahiert (wird von `upload()` und `uploadConfirm()` genutzt)
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-15 – Fix: Bank-Import-Duplikate verhindern
|
||||
|
||||
- Migration 051: UNIQUE-Constraint `(buchungstag, wertstellung, wert, buchungstext(200))` auf `bank_transactions`; bestehende Duplikate wurden dabei bereinigt
|
||||
- `BankController::upload()`: Import läuft jetzt in einer DB-Transaktion – bei Fehler wird alles zurückgerollt statt einen unvollständigen Batch zu hinterlassen
|
||||
- `BankController::upload()`: Dateiname-Prüfung vor dem Import – erneuter Upload derselben Datei wird mit Fehlermeldung abgelehnt
|
||||
- `BankTransaction::filenameExists()`: neue Hilfsmethode zur Duplikat-Prüfung anhand Dateiname
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-15 – Fix: Netto-Neuberechnung bei Kategorie-Wechsel in Bank-Buchungsmaske
|
||||
|
||||
- IIFE-Wrapper für `updateAutoUst` in einen `DOMContentLoaded`-Listener verschoben, damit er erst nach der Funktionsdefinition (späteres `<script>`-Tag) gesetzt wird und nicht sofort von ihr überschrieben wird.
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-14 – Feature: Bank buchen
|
||||
|
||||
- Navigation: neuer Eintrag „Bank buchen" unter Belege & Buchungen
|
||||
|
|
|
|||
22
migrations/051_bank_transactions_unique.sql
Normal file
22
migrations/051_bank_transactions_unique.sql
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
-- Duplikate entfernen: pro Gruppe (buchungstag, wertstellung, wert, buchungstext) die niedrigste id behalten
|
||||
DELETE bt
|
||||
FROM bank_transactions bt
|
||||
INNER JOIN (
|
||||
SELECT MIN(id) AS keep_id, buchungstag, wertstellung, wert, buchungstext
|
||||
FROM bank_transactions
|
||||
GROUP BY buchungstag, wertstellung, wert, buchungstext
|
||||
HAVING COUNT(*) > 1
|
||||
) dup ON bt.buchungstag = dup.buchungstag
|
||||
AND bt.wertstellung = dup.wertstellung
|
||||
AND bt.wert = dup.wert
|
||||
AND bt.buchungstext = dup.buchungstext
|
||||
AND bt.id > dup.keep_id;
|
||||
|
||||
-- Unvollständige Batches entfernen (row_count stimmt nicht mit tatsächlichen Zeilen überein)
|
||||
DELETE FROM bank_import_batches
|
||||
WHERE id NOT IN (SELECT DISTINCT batch_id FROM bank_transactions)
|
||||
AND row_count > 0;
|
||||
|
||||
-- UNIQUE-Constraint gegen Doppel-Importe
|
||||
ALTER TABLE bank_transactions
|
||||
ADD CONSTRAINT uq_bank_tx UNIQUE (buchungstag, wertstellung, wert, buchungstext(200));
|
||||
3
migrations/052_drop_bank_tx_unique.sql
Normal file
3
migrations/052_drop_bank_tx_unique.sql
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
-- UNIQUE-Constraint entfernt: Dublettenprüfung erfolgt jetzt über die UI vor dem Import.
|
||||
-- Force-Import durch den Nutzer wäre mit einem UNIQUE-Constraint nicht möglich.
|
||||
ALTER TABLE bank_transactions DROP INDEX IF EXISTS uq_bank_tx;
|
||||
|
|
@ -68,6 +68,7 @@ $router->post('/belege/{id}/vorschlag/reset', fn($p) => (new BelegController(
|
|||
$router->get('/bank', fn($p) => (new BankController())->index($p));
|
||||
$router->get('/bank/upload', fn($p) => (new BankController())->uploadForm($p));
|
||||
$router->post('/bank/upload', fn($p) => (new BankController())->upload($p));
|
||||
$router->post('/bank/upload/confirm', fn($p) => (new BankController())->uploadConfirm($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}/link', fn($p) => (new BankController())->linkBeleg($p));
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ namespace Steuerkram\Controllers;
|
|||
|
||||
use Steuerkram\Models\BankImportFormat;
|
||||
use Steuerkram\Models\BankTransaction;
|
||||
use Steuerkram\Database;
|
||||
use Steuerkram\Models\Beleg;
|
||||
use Steuerkram\Services\BankImportParser;
|
||||
use Steuerkram\View;
|
||||
|
|
@ -83,9 +84,67 @@ class BankController
|
|||
View::redirect('/bank/upload', 'Keine Buchungszeilen in der Datei gefunden.', 'error');
|
||||
}
|
||||
|
||||
$batchId = BankTransaction::createBatch($formatId, $origName, count($rows));
|
||||
foreach ($rows as $row) {
|
||||
BankTransaction::insertRow($batchId, $row);
|
||||
$split = BankTransaction::findDuplicates($rows);
|
||||
|
||||
if (!empty($split['dup'])) {
|
||||
$_SESSION['bank_import_pending'] = [
|
||||
'format_id' => $formatId,
|
||||
'filename' => $origName,
|
||||
'new_rows' => $split['new'],
|
||||
'dup_rows' => $split['dup'],
|
||||
];
|
||||
|
||||
$view = new View();
|
||||
$view->render('layout/base.php', [
|
||||
'pageTitle' => 'Dublettenprüfung',
|
||||
'content' => $this->renderView('bank/upload_confirm', [
|
||||
'new_rows' => $split['new'],
|
||||
'dup_rows' => $split['dup'],
|
||||
'filename' => $origName,
|
||||
]),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->doImport($formatId, $origName, $rows);
|
||||
}
|
||||
|
||||
public function uploadConfirm(array $params): void
|
||||
{
|
||||
$pending = $_SESSION['bank_import_pending'] ?? null;
|
||||
if (!$pending) {
|
||||
View::redirect('/bank/upload', 'Keine ausstehenden Import-Daten gefunden.', 'error');
|
||||
}
|
||||
unset($_SESSION['bank_import_pending']);
|
||||
|
||||
$selectedIdx = array_map('intval', (array)($_POST['dup_idx'] ?? []));
|
||||
|
||||
$rows = $pending['new_rows'];
|
||||
foreach ($selectedIdx as $idx) {
|
||||
if (isset($pending['dup_rows'][$idx])) {
|
||||
$rows[] = $pending['dup_rows'][$idx];
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($rows)) {
|
||||
View::redirect('/bank', 'Import abgebrochen – keine Buchungen ausgewählt.');
|
||||
}
|
||||
|
||||
$this->doImport($pending['format_id'], $pending['filename'], $rows);
|
||||
}
|
||||
|
||||
private function doImport(int $formatId, string $filename, array $rows): void
|
||||
{
|
||||
Database::beginTransaction();
|
||||
try {
|
||||
$batchId = BankTransaction::createBatch($formatId, $filename, count($rows));
|
||||
foreach ($rows as $row) {
|
||||
BankTransaction::insertRow($batchId, $row);
|
||||
}
|
||||
Database::commit();
|
||||
} catch (\Throwable $e) {
|
||||
Database::rollBack();
|
||||
View::redirect('/bank/upload', 'Import fehlgeschlagen: ' . $e->getMessage(), 'error');
|
||||
}
|
||||
|
||||
View::redirect('/bank', count($rows) . ' Buchungen importiert.');
|
||||
|
|
|
|||
|
|
@ -243,6 +243,8 @@ class BelegController
|
|||
}
|
||||
}
|
||||
|
||||
$linkedBankTx = BankTransaction::findByBelegId((int)$beleg['id']);
|
||||
|
||||
$view = new View();
|
||||
$view->render('layout/base.php', [
|
||||
'pageTitle' => 'Beleg ' . $beleg['beleg_nr'],
|
||||
|
|
@ -260,6 +262,7 @@ class BelegController
|
|||
'bankTxSupplier' => $bankTxSupplier,
|
||||
'bankTxBrutto' => $bankTxBrutto,
|
||||
'formData' => $bankFormData,
|
||||
'linkedBankTx' => $linkedBankTx,
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,10 +50,12 @@ class BankTransaction
|
|||
)['n'] ?? 0);
|
||||
|
||||
$rows = Database::fetchAll(
|
||||
"SELECT bt.*, bib.filename AS batch_filename, bif.name AS format_name
|
||||
"SELECT bt.*, bib.filename AS batch_filename, bif.name AS format_name,
|
||||
b.id AS beleg_id, 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
|
||||
WHERE $whereStr
|
||||
ORDER BY bt.wertstellung DESC, bt.id DESC
|
||||
LIMIT ? OFFSET ?",
|
||||
|
|
@ -96,6 +98,68 @@ class BankTransaction
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
if (empty($rows)) {
|
||||
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) {
|
||||
$params[] = $r['buchungstag'];
|
||||
$params[] = $r['wertstellung'];
|
||||
$params[] = (string)$r['wert'];
|
||||
}
|
||||
|
||||
$existing = Database::fetchAll(
|
||||
"SELECT buchungstag, wertstellung, CAST(wert AS CHAR) AS wert, LEFT(buchungstext,200) AS btext
|
||||
FROM bank_transactions
|
||||
WHERE (buchungstag, wertstellung, CAST(wert AS CHAR)) IN ($placeholders)",
|
||||
$params
|
||||
);
|
||||
|
||||
// Lookup-Set für schnellen Vergleich
|
||||
$lookup = [];
|
||||
foreach ($existing as $e) {
|
||||
$lookup[$e['buchungstag'] . '|' . $e['wertstellung'] . '|' . $e['wert'] . '|' . $e['btext']] = true;
|
||||
}
|
||||
|
||||
$new = [];
|
||||
$dup = [];
|
||||
foreach ($rows as $row) {
|
||||
$key = $row['buchungstag'] . '|'
|
||||
. $row['wertstellung'] . '|'
|
||||
. (string)$row['wert'] . '|'
|
||||
. mb_substr($row['buchungstext'], 0, 200);
|
||||
if (isset($lookup[$key])) {
|
||||
$dup[] = $row;
|
||||
} else {
|
||||
$new[] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -82,8 +82,11 @@ $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'];
|
||||
?>
|
||||
<tr style="cursor:pointer" onclick="window.location='/bank/<?= (int)$row['id'] ?>'">
|
||||
<tr style="cursor:pointer" onclick="window.location='<?= $ziel ?>'">
|
||||
<td class="td-mono"><?= $h(View::formatDate($row['wertstellung'])) ?></td>
|
||||
<td>
|
||||
<div><?= $h($row['buchungstext']) ?></div>
|
||||
|
|
@ -103,6 +106,9 @@ $months = ['Jan','Feb','Mär','Apr','Mai','Jun','Jul','Aug','Sep','Okt','Nov','D
|
|||
};
|
||||
?>
|
||||
<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 endif; ?>
|
||||
</td>
|
||||
<td onclick="event.stopPropagation()">
|
||||
<?php if ($row['status'] !== 'ignoriert'): ?>
|
||||
|
|
|
|||
150
src/Views/bank/upload_confirm.php
Normal file
150
src/Views/bank/upload_confirm.php
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
<?php use Steuerkram\View; $h = fn($v) => View::h($v); ?>
|
||||
<?php
|
||||
$fmtDate = fn($d) => $d ? date('d.m.Y', strtotime($d)) : '–';
|
||||
$fmtMoney = function($v) {
|
||||
$v = (float)$v;
|
||||
$cls = $v >= 0 ? 'color:var(--color-success)' : 'color:var(--color-danger)';
|
||||
return '<span style="' . $cls . '">' . number_format($v, 2, ',', '.') . ' €</span>';
|
||||
};
|
||||
?>
|
||||
|
||||
<div style="max-width:900px">
|
||||
|
||||
<div class="flash flash-warning" style="margin-bottom:1.5rem">
|
||||
<strong>Dublettenprüfung:</strong> Beim Import von <em><?= $h($filename) ?></em> wurden
|
||||
<strong><?= count($dup_rows) ?></strong> bereits vorhandene Buchung<?= count($dup_rows) !== 1 ? 'en' : '' ?> gefunden.
|
||||
<?php if (count($new_rows) > 0): ?>
|
||||
<strong><?= count($new_rows) ?></strong> neue Buchung<?= count($new_rows) !== 1 ? 'en' : '' ?> werden in jedem Fall importiert.
|
||||
<?php else: ?>
|
||||
Es gibt keine neuen Buchungen.
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<form method="post" action="/bank/upload/confirm" id="confirm-form">
|
||||
|
||||
<div class="card" style="margin-bottom:1.5rem">
|
||||
<div class="card-body" style="padding-bottom:.75rem">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:.75rem">
|
||||
<h3 style="margin:0;font-size:1rem">
|
||||
Bereits vorhandene Buchungen
|
||||
<span class="badge badge-warning" style="margin-left:.5rem"><?= count($dup_rows) ?></span>
|
||||
</h3>
|
||||
<div style="display:flex;gap:.5rem">
|
||||
<button type="button" class="btn btn-secondary btn-sm" id="btn-all">Alle auswählen</button>
|
||||
<button type="button" class="btn btn-secondary btn-sm" id="btn-none">Alle abwählen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table" style="margin-bottom:0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:2.5rem;text-align:center">
|
||||
<input type="checkbox" id="cb-all" title="Alle umschalten">
|
||||
</th>
|
||||
<th>Datum</th>
|
||||
<th>Buchungstext</th>
|
||||
<th>Vorgang</th>
|
||||
<th class="td-right">Betrag</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($dup_rows as $idx => $row): ?>
|
||||
<tr class="dup-row">
|
||||
<td style="text-align:center">
|
||||
<input type="checkbox" name="dup_idx[]" value="<?= $idx ?>" class="dup-cb">
|
||||
</td>
|
||||
<td class="td-mono" style="white-space:nowrap"><?= $fmtDate($row['wertstellung']) ?></td>
|
||||
<td style="font-size:.85rem;max-width:400px">
|
||||
<?= $h(mb_substr($row['buchungstext'], 0, 120)) ?>
|
||||
<?php if (mb_strlen($row['buchungstext']) > 120): ?>…<?php endif; ?>
|
||||
</td>
|
||||
<td style="font-size:.85rem;color:var(--color-text-muted)"><?= $h($row['vorgang']) ?></td>
|
||||
<td class="td-right td-mono"><?= $fmtMoney($row['wert']) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (count($new_rows) > 0): ?>
|
||||
<div class="card" style="margin-bottom:1.5rem">
|
||||
<div class="card-body" style="padding-bottom:.75rem">
|
||||
<h3 style="margin:0 0 .75rem;font-size:1rem">
|
||||
Neue Buchungen (werden importiert)
|
||||
<span class="badge badge-success" style="margin-left:.5rem"><?= count($new_rows) ?></span>
|
||||
</h3>
|
||||
<table class="table" style="margin-bottom:0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Datum</th>
|
||||
<th>Buchungstext</th>
|
||||
<th>Vorgang</th>
|
||||
<th class="td-right">Betrag</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($new_rows as $row): ?>
|
||||
<tr>
|
||||
<td class="td-mono" style="white-space:nowrap"><?= $fmtDate($row['wertstellung']) ?></td>
|
||||
<td style="font-size:.85rem;max-width:400px">
|
||||
<?= $h(mb_substr($row['buchungstext'], 0, 120)) ?>
|
||||
<?php if (mb_strlen($row['buchungstext']) > 120): ?>…<?php endif; ?>
|
||||
</td>
|
||||
<td style="font-size:.85rem;color:var(--color-text-muted)"><?= $h($row['vorgang']) ?></td>
|
||||
<td class="td-right td-mono"><?= $fmtMoney($row['wert']) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div style="display:flex;gap:.75rem;align-items:center">
|
||||
<button type="submit" class="btn btn-primary" id="btn-submit">
|
||||
Import bestätigen
|
||||
</button>
|
||||
<a href="/bank/upload" class="btn btn-secondary">Abbrechen</a>
|
||||
<span id="submit-hint" class="text-muted text-sm" style="margin-left:.25rem"></span>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const cbs = () => document.querySelectorAll('.dup-cb');
|
||||
const cbAll = document.getElementById('cb-all');
|
||||
const hint = document.getElementById('submit-hint');
|
||||
const btnAll = document.getElementById('btn-all');
|
||||
const btnNone = document.getElementById('btn-none');
|
||||
const newCount = <?= count($new_rows) ?>;
|
||||
|
||||
function updateHint() {
|
||||
const checked = document.querySelectorAll('.dup-cb:checked').length;
|
||||
const total = checked + newCount;
|
||||
hint.textContent = total > 0
|
||||
? total + ' Buchung' + (total !== 1 ? 'en' : '') + ' werden importiert'
|
||||
: 'Keine Buchungen ausgewählt – Import wird übersprungen';
|
||||
}
|
||||
|
||||
function setAll(val) {
|
||||
cbs().forEach(cb => { cb.checked = val; });
|
||||
cbAll.checked = val;
|
||||
updateHint();
|
||||
}
|
||||
|
||||
cbAll.addEventListener('change', () => setAll(cbAll.checked));
|
||||
btnAll.addEventListener('click', () => setAll(true));
|
||||
btnNone.addEventListener('click', () => setAll(false));
|
||||
document.addEventListener('change', e => {
|
||||
if (e.target.classList.contains('dup-cb')) {
|
||||
cbAll.checked = document.querySelectorAll('.dup-cb:not(:checked)').length === 0;
|
||||
updateHint();
|
||||
}
|
||||
});
|
||||
|
||||
updateHint();
|
||||
})();
|
||||
</script>
|
||||
|
|
@ -295,6 +295,28 @@ $suppliers = $suppliers ?? [];
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gespeicherte Bankbuchungs-Verknüpfung -->
|
||||
<?php if ($linkedBankTx ?? null): ?>
|
||||
<?php $lbt = $linkedBankTx; $lbwert = (float)$lbt['wert']; ?>
|
||||
<a href="/bank/<?= (int)$lbt['id'] ?>" class="card mb-2"
|
||||
style="display:block;text-decoration:none;color:inherit;border-left:4px solid var(--color-success)">
|
||||
<div class="card-body" style="padding:.6rem 1.25rem">
|
||||
<div style="display:flex;align-items:center;gap:.75rem;flex-wrap:wrap">
|
||||
<span class="badge badge-green" style="font-size:.8rem">Bankbuchung</span>
|
||||
<span class="td-mono text-sm"><?= View::formatDate($lbt['wertstellung']) ?></span>
|
||||
<?php if ($lbt['vorgang']): ?>
|
||||
<span class="text-sm text-muted"><?= $h($lbt['vorgang']) ?></span>
|
||||
<?php endif; ?>
|
||||
<span class="text-sm" style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"
|
||||
title="<?= $h($lbt['buchungstext']) ?>"><?= $h($lbt['buchungstext']) ?></span>
|
||||
<strong style="white-space:nowrap;color:<?= $lbwert >= 0 ? 'var(--color-success)' : 'var(--color-danger)' ?>">
|
||||
<?= $lbwert >= 0 ? '+' : '' ?><?= number_format($lbwert, 2, ',', '.') ?> €
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- BankBuchung-Badge (ersetzt KI-Badge wenn aus Bank-Kontext) -->
|
||||
<?php if (!$beleg['locked'] && ($bankTx ?? null)): ?>
|
||||
<div class="card mb-2" style="border-left:4px solid var(--color-primary,#3b82f6)">
|
||||
|
|
@ -550,12 +572,15 @@ $suppliers = $suppliers ?? [];
|
|||
nettoInput.addEventListener('input', function () { manualNetto = true; });
|
||||
ustSelect.addEventListener('change', recalcNetto);
|
||||
|
||||
// updateAutoUst setzt den USt-Satz per JS ohne change-Event → ummanteln
|
||||
const _origAutoUst = window.updateAutoUst;
|
||||
window.updateAutoUst = function () {
|
||||
_origAutoUst.apply(this, arguments);
|
||||
recalcNetto();
|
||||
};
|
||||
// updateAutoUst ist in einem späteren <script>-Block definiert und zum jetzigen
|
||||
// Zeitpunkt noch nicht vorhanden – Wrapper daher erst in DOMContentLoaded setzen.
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const _origAutoUst = window.updateAutoUst;
|
||||
window.updateAutoUst = function () {
|
||||
_origAutoUst.apply(this, arguments);
|
||||
recalcNetto();
|
||||
};
|
||||
});
|
||||
|
||||
// Zahlungskonto 1200 vorauswählen
|
||||
const zkSelect = document.getElementById('zahlungskonto_id');
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
1-260514
|
||||
1-260515
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue