Merge pull request 'Feature: Belegliste CSV-Export, Bewirtungsfelder, EÜR-Zeile im Kontenplan, Lade-Spinner Zuschnitt, Doppelklick-Fix Kontoauswahl, Backup-Script repariert' (#28) from fds2610-bot/Steuerkram:main into main
Reviewed-on: https://codeberg.org/fds2610/Steuerkram/pulls/28
This commit is contained in:
commit
7bd2d51181
8 changed files with 141 additions and 10 deletions
|
|
@ -45,6 +45,7 @@ $router->get('/accounts/search', fn($p) => (new AccountController())->search($p)
|
|||
|
||||
// ── Belege ───────────────────────────────────────────────────
|
||||
$router->get('/belege', fn($p) => (new BelegController())->index($p));
|
||||
$router->get('/belege/export', fn($p) => (new BelegController())->export($p));
|
||||
$router->get('/belege/upload', fn($p) => (new BelegController())->uploadForm($p));
|
||||
$router->post('/belege/upload', fn($p) => (new BelegController())->upload($p));
|
||||
$router->get('/belege/{id}', fn($p) => (new BelegController())->show($p));
|
||||
|
|
|
|||
|
|
@ -341,10 +341,23 @@ document.addEventListener('click', (e) => {
|
|||
});
|
||||
|
||||
// ── Konto-Select: Doppelklick / Enter → Kontonr. ins Filterfeld ──
|
||||
// selectedIndex ist beim click-Event garantiert korrekt; beim dblclick-Event
|
||||
// kann er in manchen Browsern bei List-Boxes zurückgesetzt sein (kein Prior-Select),
|
||||
// daher wird er beim click gecacht und als Fallback genutzt.
|
||||
const _kontoNrCache = {};
|
||||
document.addEventListener('click', (e) => {
|
||||
const sel = e.target.closest('select#kategorie_konto_id, select#zahlungskonto_id');
|
||||
if (!sel) return;
|
||||
const opt = sel.options[sel.selectedIndex];
|
||||
if (opt && opt.value) _kontoNrCache[sel.id] = opt.textContent.split(' – ')[0].trim();
|
||||
});
|
||||
|
||||
function transferKontoNr(select) {
|
||||
const opt = select.options[select.selectedIndex];
|
||||
if (!opt || !opt.value) return;
|
||||
const kontoNr = opt.textContent.split(' – ')[0].trim();
|
||||
const kontoNr = (opt && opt.value)
|
||||
? opt.textContent.split(' – ')[0].trim()
|
||||
: (_kontoNrCache[select.id] ?? null);
|
||||
if (!kontoNr) return;
|
||||
const filterInput = document.querySelector(`.konto-filter[data-target="${select.id}"]`);
|
||||
if (filterInput) filterInput.value = kontoNr;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ mkdir -p "$BACKUP_DIR"
|
|||
echo "Erstelle Backup $TIMESTAMP..."
|
||||
|
||||
# MariaDB Dump (aus dem Container)
|
||||
docker compose exec -T db mysqldump \
|
||||
docker compose exec -T db mariadb-dump \
|
||||
--user=steuerkram \
|
||||
--password=steuerkram \
|
||||
--single-transaction \
|
||||
|
|
|
|||
|
|
@ -44,10 +44,58 @@ class BelegController
|
|||
'page' => $page,
|
||||
'filter' => $filter,
|
||||
]),
|
||||
'topbarActions' => '<a href="/belege/upload" class="btn btn-primary">+ Beleg hochladen</a>',
|
||||
'topbarActions' => '<a href="/belege/export?' . http_build_query($filter) . '" class="btn btn-secondary">CSV Export</a> <a href="/belege/upload" class="btn btn-primary">+ Beleg hochladen</a>',
|
||||
]);
|
||||
}
|
||||
|
||||
public function export(array $params): void
|
||||
{
|
||||
$filter = $_GET;
|
||||
unset($filter['page']);
|
||||
$result = Beleg::all($filter, 1, 99999);
|
||||
|
||||
header('Content-Type: text/csv; charset=utf-8');
|
||||
header('Content-Disposition: attachment; filename="belegliste-' . date('Ymd') . '.csv"');
|
||||
|
||||
$out = fopen('php://output', 'w');
|
||||
fprintf($out, chr(0xEF) . chr(0xBB) . chr(0xBF)); // UTF-8 BOM for Excel
|
||||
|
||||
fputcsv($out, [
|
||||
'Belegnr.', 'Belegdatum', 'Belegart', 'Lieferant', 'Rechnungsnr.',
|
||||
'Dateiname', 'Geschäftsjahr', 'Notizen',
|
||||
'Bewirtungsgrund', 'Teilnehmerliste',
|
||||
'Brutto (€)', 'Status',
|
||||
], ';');
|
||||
|
||||
foreach ($result['rows'] as $row) {
|
||||
if ($row['locked']) {
|
||||
$status = 'Gesperrt';
|
||||
} elseif ($row['booked']) {
|
||||
$status = 'Gebucht';
|
||||
} else {
|
||||
$status = 'Offen';
|
||||
}
|
||||
|
||||
fputcsv($out, [
|
||||
$this->csvSanitize((string)($row['beleg_nr'] ?? '')),
|
||||
$row['beleg_datum'] ?? '',
|
||||
$row['belegtyp'] ?? '',
|
||||
$this->csvSanitize((string)($row['supplier_name'] ?? '')),
|
||||
$this->csvSanitize((string)($row['rechnungs_nr'] ?? '')),
|
||||
$this->csvSanitize((string)($row['filename'] ?? '')),
|
||||
(string)($row['fiscal_year_nr'] ?? ''),
|
||||
$this->csvSanitize((string)($row['notes'] ?? '')),
|
||||
$this->csvSanitize((string)($row['bewirtungsgrund'] ?? '')),
|
||||
$this->csvSanitize((string)($row['teilnehmerliste'] ?? '')),
|
||||
$row['betrag_brutto'] !== null
|
||||
? number_format((float)$row['betrag_brutto'], 2, ',', '')
|
||||
: '',
|
||||
$status,
|
||||
], ';');
|
||||
}
|
||||
fclose($out);
|
||||
}
|
||||
|
||||
public function uploadForm(array $params): void
|
||||
{
|
||||
$view = new View();
|
||||
|
|
@ -310,6 +358,14 @@ class BelegController
|
|||
echo json_encode(['ok' => true]);
|
||||
}
|
||||
|
||||
private function csvSanitize(string $value): string
|
||||
{
|
||||
if ($value !== '' && in_array($value[0], ['=', '+', '-', '@'], true)) {
|
||||
return "'" . $value;
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
private function renderView(string $template, array $data): string
|
||||
{
|
||||
extract($data, EXTR_SKIP);
|
||||
|
|
|
|||
|
|
@ -54,8 +54,8 @@ class Account
|
|||
public static function create(array $data): int
|
||||
{
|
||||
Database::execute(
|
||||
'INSERT INTO accounts (kontonummer, bezeichnung, kontenart, beschreibung, steuercode, ustva_kz)
|
||||
VALUES (?, ?, ?, ?, ?, ?)',
|
||||
'INSERT INTO accounts (kontonummer, bezeichnung, kontenart, beschreibung, steuercode, ustva_kz, euer_zeile)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)',
|
||||
[
|
||||
$data['kontonummer'],
|
||||
$data['bezeichnung'],
|
||||
|
|
@ -63,6 +63,7 @@ class Account
|
|||
$data['beschreibung'] ?? null,
|
||||
$data['steuercode'] ?? null,
|
||||
$data['ustva_kz'] ?? null,
|
||||
$data['euer_zeile'] !== '' ? ($data['euer_zeile'] ?? null) : null,
|
||||
]
|
||||
);
|
||||
return (int)Database::lastInsertId();
|
||||
|
|
@ -71,7 +72,7 @@ class Account
|
|||
public static function update(int $id, array $data): void
|
||||
{
|
||||
Database::execute(
|
||||
'UPDATE accounts SET kontonummer=?, bezeichnung=?, kontenart=?, beschreibung=?, steuercode=?, ustva_kz=?, aktiv=?
|
||||
'UPDATE accounts SET kontonummer=?, bezeichnung=?, kontenart=?, beschreibung=?, steuercode=?, ustva_kz=?, euer_zeile=?, aktiv=?
|
||||
WHERE id=?',
|
||||
[
|
||||
$data['kontonummer'],
|
||||
|
|
@ -80,6 +81,7 @@ class Account
|
|||
$data['beschreibung'] ?? null,
|
||||
$data['steuercode'] ?? null,
|
||||
$data['ustva_kz'] ?? null,
|
||||
$data['euer_zeile'] !== '' ? ($data['euer_zeile'] ?? null) : null,
|
||||
(int)($data['aktiv'] ?? 1),
|
||||
$id,
|
||||
]
|
||||
|
|
|
|||
|
|
@ -68,6 +68,44 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-grid form-grid-2">
|
||||
<div class="form-group">
|
||||
<label for="euer_zeile">EÜR-Zeile</label>
|
||||
<select id="euer_zeile" name="euer_zeile">
|
||||
<option value="">– keine –</option>
|
||||
<?php
|
||||
$euerZeilen = [
|
||||
'11' => '11 – Umsätze (Betriebseinnahmen)',
|
||||
'12' => '12 – Umsätze (7 % USt)',
|
||||
'13' => '13 – Umsätze (19 % USt)',
|
||||
'21' => '21 – Waren/Roh-/Hilfsstoffe',
|
||||
'22' => '22 – Bezogene Leistungen',
|
||||
'23' => '23 – Löhne und Gehälter',
|
||||
'26' => '26 – Miete/Pacht',
|
||||
'27' => '27 – AfA',
|
||||
'30' => '30 – Zinsen und ähnliche Aufwendungen',
|
||||
'42' => '42 – Reisekosten',
|
||||
'43' => '43 – Werbekosten',
|
||||
'44' => '44 – Bewirtungskosten (70 % abzugsfähig)',
|
||||
'45' => '45 – Versicherungen',
|
||||
'46' => '46 – Sonstige Betriebsausgaben',
|
||||
'48' => '48 – Abschreibungen GWG',
|
||||
'51' => '51 – Nicht abzugsfähige Betriebsausgaben (KZ 165)',
|
||||
'61' => '61 – Vorsteuer 19 %',
|
||||
'62' => '62 – Vorsteuer 7 %',
|
||||
'63' => '63 – Umsatzsteuer',
|
||||
];
|
||||
foreach ($euerZeilen as $val => $label):
|
||||
?>
|
||||
<option value="<?= $val ?>"<?= ($account['euer_zeile'] ?? '') === $val ? ' selected' : '' ?>>
|
||||
<?= $h($label) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<div class="form-hint">Zeile 51 = erscheint in EÜR unter KZ 165</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="beschreibung">Beschreibung</label>
|
||||
<textarea id="beschreibung" name="beschreibung"><?= $h($account['beschreibung'] ?? '') ?></textarea>
|
||||
|
|
|
|||
|
|
@ -754,7 +754,14 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||
</p>
|
||||
<div id="crop-canvas-wrap" style="position:relative;display:inline-block;cursor:crosshair;line-height:0;border:1px solid var(--color-border);border-radius:var(--radius)">
|
||||
<canvas id="crop-canvas"></canvas>
|
||||
<div id="crop-spinner" style="display:none;position:absolute;inset:0;align-items:center;justify-content:center;background:rgba(0,0,0,.35);border-radius:var(--radius)">
|
||||
<svg width="40" height="40" viewBox="0 0 40 40" style="animation:crop-spin 0.8s linear infinite">
|
||||
<circle cx="20" cy="20" r="16" fill="none" stroke="rgba(255,255,255,.25)" stroke-width="4"/>
|
||||
<path d="M20 4 A16 16 0 0 1 36 20" fill="none" stroke="#fff" stroke-width="4" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<style>@keyframes crop-spin{to{transform:rotate(360deg)}}</style>
|
||||
<div style="margin-top:.75rem;display:flex;gap:.5rem;align-items:center">
|
||||
<button type="button" class="btn btn-primary btn-sm" id="crop-confirm-btn" disabled onclick="confirmCrop()">Zuschnitt übernehmen</button>
|
||||
<button type="button" class="btn btn-secondary btn-sm" onclick="closeCropModal()">Abbrechen</button>
|
||||
|
|
@ -779,10 +786,13 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||
let dragging = false;
|
||||
let startX = 0, startY = 0;
|
||||
|
||||
const spinner = document.getElementById('crop-spinner');
|
||||
|
||||
window.openCropModal = function() {
|
||||
overlay.style.display = 'flex';
|
||||
if (img) { drawCanvas(); return; }
|
||||
|
||||
spinner.style.display = 'flex';
|
||||
img = new Image();
|
||||
img.onload = function() {
|
||||
// Canvas auf max. 90vw / 72vh skalieren
|
||||
|
|
@ -794,6 +804,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||
scaleX = img.naturalWidth / canvas.width;
|
||||
scaleY = img.naturalHeight / canvas.height;
|
||||
sel = null;
|
||||
spinner.style.display = 'none';
|
||||
drawCanvas();
|
||||
};
|
||||
img.src = FILE_URL + '?nocache=' + Date.now();
|
||||
|
|
|
|||
|
|
@ -132,11 +132,11 @@
|
|||
<thead>
|
||||
<tr>
|
||||
<th>Belegnr.</th>
|
||||
|
||||
<th>Belegdatum</th>
|
||||
<th>Lieferant</th>
|
||||
<th>Rechnungsnr.</th>
|
||||
<th>Dateiname</th>
|
||||
<th>Bewirtungsgrund / Teilnehmer</th>
|
||||
<th class="td-right">Brutto</th>
|
||||
<th>Status</th>
|
||||
<th></th>
|
||||
|
|
@ -146,11 +146,21 @@
|
|||
<?php foreach ($rows as $b): ?>
|
||||
<tr>
|
||||
<td><a href="/belege/<?= $h($b['id']) ?>" class="font-mono"><?= $h($b['beleg_nr']) ?></a></td>
|
||||
|
||||
<td class="td-muted"><?= View::formatDate($b['beleg_datum']) ?></td>
|
||||
<td class="td-muted"><?= $h($b['supplier_name'] ?? '–') ?></td>
|
||||
<td class="td-muted"><?= $h($b['rechnungs_nr'] ?? '–') ?></td>
|
||||
<td class="text-sm text-muted"><?= $h(mb_strimwidth($b['filename'], 0, 35, '…')) ?></td>
|
||||
<td style="max-width:220px">
|
||||
<?php if (!empty($b['bewirtungsgrund'])): ?>
|
||||
<div style="font-size:.8em;white-space:pre-wrap;word-break:break-word"><?= $h($b['bewirtungsgrund']) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($b['teilnehmerliste'])): ?>
|
||||
<div style="font-size:.75em;color:var(--color-muted);white-space:pre-wrap;word-break:break-word;margin-top:.15em"><?= $h($b['teilnehmerliste']) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (empty($b['bewirtungsgrund']) && empty($b['teilnehmerliste'])): ?>
|
||||
<span class="text-muted">–</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="td-right td-mono"><?= $b['betrag_brutto'] !== null ? View::formatMoney($b['betrag_brutto']) : '–' ?></td>
|
||||
<td>
|
||||
<?php if ($b['locked']): ?>
|
||||
|
|
@ -167,7 +177,7 @@
|
|||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($rows)): ?>
|
||||
<tr><td colspan="9" class="text-center text-muted" style="padding:2rem">Keine Belege gefunden.</td></tr>
|
||||
<tr><td colspan="10" class="text-center text-muted" style="padding:2rem">Keine Belege gefunden.</td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue