Merge pull request 'Feature: Belegbuchung verbessert, Metadaten bereinigt (Migrationen 024–026)' (#26) from fds2610-bot/Steuerkram:main into main

Reviewed-on: https://codeberg.org/fds2610/Steuerkram/pulls/26
This commit is contained in:
fds2610 2026-04-11 11:46:09 +02:00
commit b2ec251871
14 changed files with 264 additions and 85 deletions

View file

@ -0,0 +1,10 @@
-- Migration 024: Bewirtungsfelder von bookings nach belege verschieben
-- Bestehende Daten werden nicht migriert (können gelöscht werden).
ALTER TABLE belege
ADD COLUMN bewirtungsgrund TEXT NULL AFTER notes,
ADD COLUMN teilnehmerliste TEXT NULL AFTER bewirtungsgrund;
ALTER TABLE bookings
DROP COLUMN bewirtungsgrund,
DROP COLUMN teilnehmerliste;

View file

@ -0,0 +1,2 @@
-- Migration 025: Eingangsdatum aus belege entfernen
ALTER TABLE belege DROP COLUMN eingangs_datum;

View file

@ -0,0 +1,159 @@
-- Migration 026: Kontenrahmen-Abgleich von Produktion (2026-04-11)
INSERT INTO accounts (kontonummer, bezeichnung, kontenart, steuercode, ustva_kz, euer_zeile, is_system, aktiv) VALUES
('0027', 'EDV-Software', 'Zahlungskonto', NULL, NULL, NULL, 1, 1),
('0030', 'Lizenzen an gewerblichen Schutzrechten', 'Zahlungskonto', NULL, NULL, NULL, 1, 1),
('0210', 'Maschinen', 'Zahlungskonto', NULL, NULL, NULL, 1, 1),
('0320', 'Pkw', 'Zahlungskonto', NULL, NULL, NULL, 1, 1),
('0350', 'Lkw', 'Zahlungskonto', NULL, NULL, NULL, 1, 1),
('0400', 'Betriebsausstattung', 'Zahlungskonto', NULL, NULL, NULL, 1, 1),
('0410', 'Geschäftsausstattung', 'Zahlungskonto', NULL, NULL, NULL, 1, 1),
('0420', 'Büroeinrichtung', 'Zahlungskonto', NULL, NULL, NULL, 1, 1),
('0440', 'Werkzeuge', 'Zahlungskonto', NULL, NULL, NULL, 1, 1),
('0480', 'Geringwertige Wirtschaftsgüter', 'Zahlungskonto', NULL, NULL, NULL, 1, 1),
('1000', 'Kasse', 'Zahlungskonto', NULL, NULL, NULL, 1, 1),
('1010', 'Nebenkasse 1', 'Zahlungskonto', NULL, NULL, NULL, 1, 1),
('1200', 'Bank 1 - Comdirekt', 'Zahlungskonto', '', '', NULL, 1, 1),
('1210', 'Bank 2 - Sonstige Banken und Kasse', 'Zahlungskonto', '', '', NULL, 1, 1),
('1220', 'Bank 3', 'Zahlungskonto', NULL, NULL, NULL, 1, 1),
('1230', 'Bank 4', 'Zahlungskonto', NULL, NULL, NULL, 1, 1),
('1240', 'Bank 5', 'Zahlungskonto', NULL, NULL, NULL, 1, 1),
('1390', 'Verrechnungskonto Ist-Versteuerung', 'Verbindlichkeit', '', '', NULL, 1, 1),
('1770', 'Umsatzsteuer', 'Verbindlichkeit', '', '', NULL, 1, 1),
('1771', 'Umsatzsteuer 7 %', 'Verbindlichkeit', '', '', NULL, 1, 1),
('1776', 'Umsatzsteuer 19 %', 'Verbindlichkeit', '', '', NULL, 1, 1),
('1780', 'Umsatzsteuer-Vorauszahlungen lfd. Jahr', 'Verbindlichkeit', '', '', NULL, 1, 1),
('1781', 'Umsatzsteuer-Vorauszahlungen 1/11', 'Verbindlichkeit', '', '', NULL, 1, 1),
('1800', 'Privatentnahmen', 'Privat', NULL, NULL, NULL, 1, 1),
('1810', 'Privatsteuern', 'Privat', NULL, NULL, NULL, 1, 1),
('1820', 'Sonderausgaben beschränkt abzugsfähig', 'Privat', NULL, NULL, NULL, 1, 1),
('1830', 'Sonderausgaben unbeschränkt abzugsfähig', 'Privat', NULL, NULL, NULL, 1, 1),
('1840', 'Zuwendungen, Spenden', 'Privat', NULL, NULL, NULL, 1, 1),
('1890', 'Privateinlagen', 'Privat', NULL, NULL, NULL, 1, 1),
('3106', 'Fremdleistungen 19 % Vorsteuer', 'Betriebsausgabe', 'VSt19', '61', '29', 1, 1),
('3108', 'Fremdleistungen 7 % Vorsteuer', 'Betriebsausgabe', 'VSt7', '62', '29', 1, 1),
('3109', 'Fremdleistungen ohne Vorsteuer', 'Betriebsausgabe', NULL, NULL, '29', 1, 1),
('3300', 'Wareneingang 7 % Vorsteuer', 'Betriebsausgabe', 'VSt7', '62', '28', 1, 1),
('3349', 'Wareneingang ohne Vorsteuerabzug', 'Betriebsausgabe', NULL, NULL, '26', 1, 1),
('3400', 'Wareneingang 19 % Vorsteuer', 'Betriebsausgabe', 'VSt19', '61', '27', 1, 1),
('4100', 'Löhne und Gehälter', 'Betriebsausgabe', NULL, NULL, '30', 1, 1),
('4110', 'Löhne', 'Betriebsausgabe', NULL, NULL, '30', 1, 1),
('4120', 'Gehälter', 'Betriebsausgabe', NULL, NULL, '30', 1, 1),
('4130', 'Gesetzliche soziale Aufwendungen', 'Betriebsausgabe', NULL, NULL, '31', 1, 1),
('4138', 'Beiträge zur Berufsgenossenschaft', 'Betriebsausgabe', NULL, NULL, '31', 1, 1),
('4140', 'Freiwillige soziale Aufwendungen, lohnsteuerfrei', 'Betriebsausgabe', NULL, NULL, '32', 1, 1),
('4145', 'Freiwillige soziale Aufwendungen, lohnsteuerpflichtig', 'Betriebsausgabe', NULL, NULL, '32', 1, 1),
('4180', 'Aushilfslöhne', 'Betriebsausgabe', NULL, NULL, '30', 1, 1),
('4190', 'Aushilfslöhne Minijobs', 'Betriebsausgabe', NULL, NULL, '30', 1, 1),
('4210', 'Miete (unbewegliche Wirtschaftsgüter)', 'Betriebsausgabe', NULL, NULL, '33', 1, 1),
('4211', 'Miete/Pacht unbewegliche WG (gewerbesteuerl. hinzuzur.)', 'Betriebsausgabe', NULL, NULL, '33', 1, 1),
('4220', 'Pacht (unbewegliche Wirtschaftsgüter)', 'Betriebsausgabe', NULL, NULL, '33', 1, 1),
('4230', 'Heizung', 'Betriebsausgabe', 'VSt19', '61', '34', 1, 1),
('4240', 'Gas, Strom, Wasser', 'Betriebsausgabe', 'VSt19', '61', '34', 1, 1),
('4250', 'Reinigung', 'Betriebsausgabe', 'VSt19', '61', '37', 1, 1),
('4260', 'Instandhaltung betrieblicher Räume', 'Betriebsausgabe', 'VSt19', '61', '37', 1, 1),
('4280', 'Sonstige Raumkosten', 'Betriebsausgabe', 'VSt19', '61', '33', 1, 1),
('4287', 'Tagespauschale für Tätigkeit in der häuslichen Wohnung', 'Betriebsausgabe', NULL, NULL, '66', 1, 1),
('4288', 'Häusliches Arbeitszimmer (abziehbarer Anteil)', 'Betriebsausgabe', NULL, NULL, '65', 1, 1),
('4289', 'Häusliches Arbeitszimmer (nicht abziehbarer Anteil)', 'Betriebsausgabe', NULL, NULL, '51', 1, 1),
('4300', 'Nicht abziehbare Vorsteuer', 'Betriebsausgabe', NULL, NULL, '52', 1, 1),
('4301', 'Nicht abziehbare Vorsteuer 7 %', 'Betriebsausgabe', NULL, NULL, '52', 1, 1),
('4306', 'Nicht abziehbare Vorsteuer 19 %', 'Betriebsausgabe', NULL, NULL, '52', 1, 1),
('4320', 'Gewerbesteuer', 'Betriebsausgabe', NULL, NULL, '52', 1, 1),
('4340', 'Sonstige Betriebssteuern', 'Betriebsausgabe', NULL, NULL, '52', 1, 1),
('4350', 'Verbrauchsteuer (sonstige Steuern)', 'Betriebsausgabe', NULL, NULL, '52', 1, 1),
('4360', 'Versicherungen', 'Betriebsausgabe', NULL, NULL, '52', 1, 1),
('4366', 'Versicherungen für Gebäude', 'Betriebsausgabe', NULL, NULL, '52', 1, 1),
('4380', 'Beiträge', 'Betriebsausgabe', NULL, NULL, '52', 1, 1),
('4390', 'Sonstige Abgaben', 'Betriebsausgabe', NULL, NULL, '52', 1, 1),
('4500', 'Fahrzeugkosten (allgemein)', 'Betriebsausgabe', NULL, NULL, '40', 1, 1),
('4510', 'Kfz-Steuer', 'Betriebsausgabe', NULL, NULL, '40', 1, 1),
('4520', 'Fahrzeug-Versicherungen', 'Betriebsausgabe', NULL, NULL, '40', 1, 1),
('4530', 'Laufende Fahrzeug-Betriebskosten', 'Betriebsausgabe', 'VSt19', '61', '40', 1, 1),
('4540', 'Fahrzeug-Reparaturen', 'Betriebsausgabe', 'VSt19', '61', '40', 1, 1),
('4550', 'Garagenmiete', 'Betriebsausgabe', 'VSt19', '61', '40', 1, 1),
('4560', 'Mautgebühren', 'Betriebsausgabe', NULL, NULL, '40', 1, 1),
('4570', 'Mietleasing Kfz', 'Betriebsausgabe', 'VSt19', '61', '40', 1, 1),
('4575', 'Mietleasing Elektrofahrzeuge/Fahrräder (gewerbesteuerl. hinzuzurechnen)', 'Betriebsausgabe', 'VSt19', '61', '40', 1, 1),
('4580', 'Sonstige Fahrzeugkosten', 'Betriebsausgabe', 'VSt19', '61', '40', 1, 1),
('4590', 'Fremdfahrzeugkosten', 'Betriebsausgabe', 'VSt19', '61', '40', 1, 1),
('4600', 'Werbekosten', 'Betriebsausgabe', 'VSt19', '61', '41', 1, 1),
('4605', 'Streuartikel', 'Betriebsausgabe', 'VSt19', '61', '41', 1, 1),
('4630', 'Geschenke abzugsfähig ohne § 37b EStG', 'Betriebsausgabe', 'VSt19', '61', '41', 1, 1),
('4631', 'Geschenke abzugsfähig mit § 37b EStG (§ 37b-Steuer)', 'Betriebsausgabe', 'VSt19', '61', '41', 1, 1),
('4635', 'Geschenke nicht abzugsfähig ohne § 37b EStG', 'Betriebsausgabe', 'VSt19', '61', '51', 1, 1),
('4636', 'Geschenke nicht abzugsfähig mit § 37b EStG', 'Betriebsausgabe', 'VSt19', '61', '51', 1, 1),
('4650', 'Bewirtungskosten (70 % abzugsfähig) VSt. 19%', 'Betriebsausgabe', 'VSt19', '61', '44', 1, 1),
('4651', 'Bewirtungskosten (70 % abzugsfähig) VSt. 7%', 'Betriebsausgabe', 'VSt7', '62', '44', 1, 1),
('4652', 'Bewirtungskosten (70 % abzugsfähig) VSt. 0%', 'Betriebsausgabe', '', '', NULL, 0, 1),
('4654', 'Nicht abzugsfähige Bewirtungskosten VSt. 19%', 'Betriebsausgabe', 'VSt19', '61', '51', 1, 1),
('4656', 'nicht abzugsfähige Bewirtungskosten VSt. 7%', 'Betriebsausgabe', 'VSt7', '62', '51', 1, 1),
('4657', 'nicht abzugsfähige Bewirtungskosten VSt. 0%', 'Betriebsausgabe', '', '', NULL, 0, 1),
('4660', 'Reisekosten Arbeitnehmer', 'Betriebsausgabe', NULL, NULL, '42', 1, 1),
('4663', 'Reisekosten Arbeitnehmer Fahrtkosten', 'Betriebsausgabe', NULL, NULL, '42', 1, 1),
('4664', 'Reisekosten Arbeitnehmer Verpflegungsmehraufwand', 'Betriebsausgabe', NULL, NULL, '42', 1, 1),
('4666', 'Reisekosten Arbeitnehmer Übernachtungsaufwand', 'Betriebsausgabe', 'VSt19', '61', '42', 1, 1),
('4668', 'Kilometergelderstattung Arbeitnehmer', 'Betriebsausgabe', NULL, NULL, '42', 1, 1),
('4670', 'Reisekosten Unternehmer', 'Betriebsausgabe', NULL, NULL, '43', 1, 1),
('4672', 'Reisekosten Unternehmer (nicht abziehbarer Anteil)', 'Betriebsausgabe', NULL, NULL, '51', 1, 1),
('4673', 'Reisekosten Unternehmer Fahrtkosten', 'Betriebsausgabe', NULL, NULL, '43', 1, 1),
('4674', 'Reisekosten Unternehmer Verpflegungsmehraufwand', 'Betriebsausgabe', NULL, NULL, '43', 1, 1),
('4676', 'Reisekosten Unternehmer Übernachtungsaufwand', 'Betriebsausgabe', 'VSt19', '61', '43', 1, 1),
('4780', 'Fremdarbeiten', 'Betriebsausgabe', 'VSt19', '61', '29', 1, 1),
('4800', 'Reparaturen und Instandhaltungen (Maschinen, techn. Anlagen)', 'Betriebsausgabe', 'VSt19', '61', '38', 1, 1),
('4801', 'Reparaturen und Instandhaltung von Bauten', 'Betriebsausgabe', 'VSt19', '61', '37', 1, 1),
('4805', 'Reparaturen und Instandhaltungen (andere Anlagen, BGA)', 'Betriebsausgabe', 'VSt19', '61', '38', 1, 1),
('4806', 'Wartungskosten Hard- und Software', 'Betriebsausgabe', 'VSt19', '61', '38', 1, 1),
('4809', 'Sonstige Reparaturen und Instandhaltungen', 'Betriebsausgabe', 'VSt19', '61', '38', 1, 1),
('4822', 'Abschreibungen auf immaterielle Vermögensgegenstände', 'Betriebsausgabe', NULL, NULL, '49', 1, 1),
('4830', 'Abschreibungen auf Sachanlagen (ohne AfA Fzg./Gebäude)', 'Betriebsausgabe', NULL, NULL, '49', 1, 1),
('4831', 'Abschreibungen auf Gebäude', 'Betriebsausgabe', NULL, NULL, '49', 1, 1),
('4832', 'Abschreibungen auf Fahrzeuge', 'Betriebsausgabe', NULL, NULL, '49', 1, 1),
('4855', 'Sofortabschreibung geringwertiger Wirtschaftsgüter', 'Betriebsausgabe', NULL, NULL, '50', 1, 1),
('4856', 'Sonderabschreibungen nach § 7b EStG (Mietwohnungsneubau)', 'Betriebsausgabe', NULL, NULL, '37', 1, 1),
('4900', 'Sonstige betriebliche Aufwendungen', 'Betriebsausgabe', 'VSt19', '61', '52', 1, 1),
('4909', 'Fremdleistungen / Fremdarbeiten', 'Betriebsausgabe', 'VSt19', '61', '29', 1, 1),
('4910', 'Porto', 'Betriebsausgabe', NULL, NULL, '47', 1, 1),
('4920', 'Telefon', 'Betriebsausgabe', 'VSt19', '61', '47', 1, 1),
('4925', 'Internetkosten', 'Betriebsausgabe', 'VSt19', '61', '47', 1, 1),
('4930', 'Bürobedarf', 'Betriebsausgabe', 'VSt19', '61', '47', 1, 1),
('4940', 'Zeitschriften, Bücher, digitale Medien (Fachliteratur)', 'Betriebsausgabe', 'VSt7', '62', '47', 1, 1),
('4945', 'Fortbildungskosten', 'Betriebsausgabe', 'VSt19', '61', '52', 1, 1),
('4950', 'Rechts- und Beratungskosten', 'Betriebsausgabe', 'VSt19', '61', '52', 1, 1),
('4955', 'Buchführungskosten', 'Betriebsausgabe', 'VSt19', '61', '46', 1, 1),
('4957', 'Abschluss- und Prüfungskosten', 'Betriebsausgabe', 'VSt19', '61', '46', 1, 1),
('4960', 'Mieten für Einrichtungen (bewegliche Wirtschaftsgüter)', 'Betriebsausgabe', 'VSt19', '61', '48', 1, 1),
('4964', 'Zeitlich befristete Überlassung von Rechten (Lizenzen)', 'Betriebsausgabe', 'VSt19', '61', '48', 1, 1),
('4965', 'Mietleasing (bewegliche WG / Geschäftsausstattung)', 'Betriebsausgabe', 'VSt19', '61', '48', 1, 1),
('4969', 'Abraum- und Abfallbeseitigung', 'Betriebsausgabe', 'VSt19', '61', '52', 1, 1),
('4970', 'Nebenkosten des Geldverkehrs (Bankgebühren, Zinsen)', 'Betriebsausgabe', NULL, NULL, '52', 1, 1),
('4971', 'Verwahrentgelt', 'Betriebsausgabe', NULL, NULL, '52', 1, 1),
('4980', 'Sonstiger Betriebsbedarf', 'Betriebsausgabe', 'VSt19', '61', '52', 1, 1),
('4985', 'Werkzeuge und Kleingeräte', 'Betriebsausgabe', 'VSt19', '61', '52', 1, 1),
('8100', 'Steuerfreie Umsätze § 4 Nr. 8 ff. UStG', 'Betriebseinnahme', NULL, '48', '17', 1, 1),
('8105', 'Steuerfreie Umsätze § 4 Nr. 12 UStG (Vermietung/Verpachtung)', 'Betriebseinnahme', NULL, '48', '17', 1, 1),
('8110', 'Sonstige steuerfreie Umsätze Inland', 'Betriebseinnahme', NULL, '48', '17', 1, 1),
('8120', 'Steuerfreie Umsätze § 4 Nr. 1a UStG', 'Betriebseinnahme', NULL, '43', '18', 1, 1),
('8125', 'Steuerfreie innergemeinschaftliche Lieferungen § 4 Nr. 1b UStG', 'Betriebseinnahme', NULL, '41', '18', 1, 1),
('8300', 'Erlöse 7 % USt', 'Betriebseinnahme', 'USt7', '86', '15', 1, 1),
('8400', 'Erlöse 19 % USt', 'Betriebseinnahme', 'USt19', '81', '16', 1, 1),
('8570', 'Sonstige Erträge aus Provisionen, Lizenzen und Patenten', 'Betriebseinnahme', 'USt19', '81', '16', 1, 1),
('8710', 'Erlösschmälerungen 7 % USt', 'Betriebseinnahme', 'USt7', '86', '15', 1, 1),
('8720', 'Erlösschmälerungen 19 % USt', 'Betriebseinnahme', 'USt19', '81', '16', 1, 1),
('8800', 'Erlöse aus Verkäufen Sachanlagevermögen (bei Buchverlust)', 'Betriebseinnahme', NULL, NULL, '22', 1, 1),
('8801', 'Erlöse aus Verkäufen Sachanlagevermögen 19 % USt (bei Buchverlust)', 'Betriebseinnahme', 'USt19', '81', '22', 1, 1),
('8820', 'Erlöse aus Verkäufen Sachanlagevermögen 19 % USt (bei Buchgewinn)', 'Betriebseinnahme', 'USt19', '81', '22', 1, 1),
('8900', 'Sonstige betriebliche Erträge', 'Betriebseinnahme', NULL, NULL, '23', 1, 1),
('8910', 'Sonstige betriebliche Erträge (steuerfrei)', 'Betriebseinnahme', NULL, NULL, '23', 1, 1),
('8920', 'Verwendung von Gegenständen für Zwecke außerhalb des Unternehmens', 'Betriebseinnahme', NULL, NULL, '22', 1, 1),
('8921', 'Verwendung von Gegenständen für Zwecke außerhalb des Unternehmens 7 % USt', 'Betriebseinnahme', 'USt7', '86', '22', 1, 1),
('8922', 'Verwendung von Gegenständen für Zwecke außerhalb des Unternehmens 19 % USt', 'Betriebseinnahme', 'USt19', '81', '22', 1, 1),
('8950', 'Nicht steuerbare Umsätze (Innenenumsätze)', 'Betriebseinnahme', NULL, NULL, '21', 1, 1)
ON DUPLICATE KEY UPDATE
bezeichnung=VALUES(bezeichnung),
kontenart=VALUES(kontenart),
steuercode=VALUES(steuercode),
ustva_kz=VALUES(ustva_kz),
euer_zeile=VALUES(euer_zeile),
is_system=VALUES(is_system),
aktiv=VALUES(aktiv);

View file

@ -33,11 +33,14 @@ document.addEventListener('input', (e) => {
if (opt.value === '') { opt.hidden = false; continue; }
opt.hidden = !opt.dataset.search.toLowerCase().includes(q);
}
// Auto-select first visible match
// Auto-select nur bei exaktem Kontonummer-Treffer; sonst Select leeren
// (verhindert, dass direkte Kontonummer-Eingabe durch einen Partial-Match überschrieben wird)
if (q.length > 0) {
for (const opt of select.options) {
if (!opt.hidden && opt.value !== '') { select.value = opt.value; break; }
}
const exact = [...select.options].find(
opt => opt.value !== '' && opt.dataset.search && opt.dataset.search.split(' ')[0] === q
);
select.value = exact ? exact.value : '';
select.dispatchEvent(new Event('change'));
}
});
@ -364,10 +367,11 @@ document.addEventListener('keydown', (e) => {
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.flash').forEach(el => {
if (el.closest('#ki-badge')) return; // KI-Badge nie auto-dismissen
if (el.classList.contains('flash-persistent')) return; // z.B. Bewirtungshinweis
setTimeout(() => {
el.style.transition = 'opacity .4s';
el.style.opacity = '0';
setTimeout(() => el.remove(), 400);
}, 4000);
}, 20000);
});
});

View file

@ -126,9 +126,8 @@ class BelegController
'mime_type' => $mime,
'file_hash' => $hash,
'beleg_datum' => $_POST['beleg_datum'] ?? null,
'eingangs_datum'=> $_POST['eingangs_datum'] ?? date('Y-m-d'),
'rechnungs_nr' => $filenameWithoutExt,
'betrag_brutto' => ($_POST['betrag_brutto'] ?? '') !== '' ? $_POST['betrag_brutto'] : null,
'betrag_brutto' => null,
'belegtyp' => $_POST['belegtyp'] ?? 'Eingangsrechnung',
'notes' => $_POST['notes'] ?? null,
]);
@ -214,7 +213,6 @@ class BelegController
'rechnungs_nr' => trim($_POST['rechnungs_nr'] ?? ''),
'belegtyp' => $_POST['belegtyp'] ?? $beleg['belegtyp'],
'beleg_datum' => $_POST['beleg_datum'] ?: null,
'betrag_brutto' => ($_POST['betrag_brutto'] ?? '') !== '' ? $_POST['betrag_brutto'] : null,
'supplier_id' => $supplierId,
]);

View file

@ -89,10 +89,27 @@ class BookingController
$data = $_POST;
$errors = [];
// Direkte Kontonummer-Eingabe: nur auflösen wenn nichts per Select gewählt
foreach ([
'kategorie_kontonummer' => 'kategorie_konto_id',
'zahlungs_kontonummer' => 'zahlungskonto_id',
] as $field => $idField) {
if (!empty($data[$idField])) continue; // Select-Auswahl hat Vorrang
$nr = trim($data[$field] ?? '');
if ($nr !== '') {
$acc = \Steuerkram\Database::fetchOne('SELECT id FROM accounts WHERE kontonummer = ?', [$nr]);
if (!$acc) {
$errors[$idField] = "Kontonummer '$nr' nicht gefunden.";
} else {
$data[$idField] = $acc['id'];
}
}
}
// Validate
if (empty($data['buchungstyp'])) $errors['buchungstyp'] = 'Pflichtfeld';
if (empty($data['kategorie_konto_id'])) $errors['kategorie_konto_id'] = 'Pflichtfeld';
if (empty($data['zahlungskonto_id'])) $errors['zahlungskonto_id'] = 'Pflichtfeld';
if (empty($data['kategorie_konto_id']) && !isset($errors['kategorie_konto_id'])) $errors['kategorie_konto_id'] = 'Pflichtfeld';
if (empty($data['zahlungskonto_id']) && !isset($errors['zahlungskonto_id'])) $errors['zahlungskonto_id'] = 'Pflichtfeld';
if (empty($data['buchungs_datum'])) $errors['buchungs_datum'] = 'Pflichtfeld';
if (!isset($data['betrag_netto']) || $data['betrag_netto'] === '')
$errors['betrag_netto'] = 'Pflichtfeld';
@ -110,14 +127,24 @@ class BookingController
try {
$data['beleg_id'] = $belegId;
// Bewirtungskonto-Split: 4650→4654, 4651→4656
// Bewirtungsfelder am Beleg speichern (gehören zum Beleg, nicht zur Buchung)
\Steuerkram\Database::execute(
'UPDATE belege SET bewirtungsgrund=?, teilnehmerliste=?, updated_at=NOW() WHERE id=?',
[
!empty($data['bewirtungsgrund']) ? $data['bewirtungsgrund'] : null,
!empty($data['teilnehmerliste']) ? $data['teilnehmerliste'] : null,
$belegId,
]
);
// Bewirtungskonto-Split: 4650→4654, 4651→4656, 4652→4657
$konto = \Steuerkram\Database::fetchOne(
'SELECT kontonummer FROM accounts WHERE id = ?',
[(int)$data['kategorie_konto_id']]
);
$kontonummer = $konto['kontonummer'] ?? '';
if ($kontonummer === '4650' || $kontonummer === '4651') {
if (in_array($kontonummer, ['4650', '4651', '4652'], true)) {
$this->bucheBewirtungssplit($data, $kontonummer);
} else {
Booking::create($data);
@ -171,7 +198,7 @@ class BookingController
private function bucheBewirtungssplit(array $data, string $kontonummer): void
{
// Abzugskonto ermitteln
$abzugskontonummer = $kontonummer === '4650' ? '4654' : '4656';
$abzugskontonummer = match($kontonummer) { '4650' => '4654', '4652' => '4657', default => '4656' };
$abzugskonto = \Steuerkram\Database::fetchOne(
'SELECT id FROM accounts WHERE kontonummer = ?',
[$abzugskontonummer]
@ -189,16 +216,12 @@ class BookingController
// Buchung 1: 70 % auf Bewirtungskonto (abzugsfähig)
Booking::create(array_merge($data, [
'betrag_netto' => $netto70,
'bewirtungsgrund' => $data['bewirtungsgrund'] ?? null,
'teilnehmerliste' => $data['teilnehmerliste'] ?? null,
]));
// Buchung 2: 30 % auf Abzugskonto (nicht abzugsfähig, § 4 Abs. 5 EStG)
Booking::create(array_merge($data, [
'kategorie_konto_id' => $abzugskonto['id'],
'betrag_netto' => $netto30,
'bewirtungsgrund' => $data['bewirtungsgrund'] ?? null,
'teilnehmerliste' => $data['teilnehmerliste'] ?? null,
]));
}

View file

@ -323,7 +323,6 @@ class InvoiceController
'mime_type' => 'application/pdf',
'file_hash' => hash('sha256', $invoice['rechnungs_nr']),
'beleg_datum' => $invoice['rechnungs_datum'],
'eingangs_datum' => $invoice['rechnungs_datum'],
'rechnungs_nr' => $invoice['rechnungs_nr'],
'betrag_brutto' => $invoice['betrag_brutto'],
'belegtyp' => 'Ausgangsrechnung',

View file

@ -40,9 +40,8 @@ class Beleg
$offset = ($page - 1) * $perPage;
$sql = 'SELECT b.*,
COALESCE(b.betrag_brutto,
(SELECT SUM(bk.betrag_brutto) FROM bookings bk
WHERE bk.beleg_id = b.id AND bk.is_reversal = 0)
WHERE bk.beleg_id = b.id AND bk.is_reversal = 0
) AS betrag_brutto,
fy.year as fiscal_year_nr,
s.name as supplier_name,
@ -51,7 +50,7 @@ class Beleg
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.eingangs_datum DESC, b.id DESC
ORDER BY b.created_at DESC, b.id DESC
LIMIT ? OFFSET ?';
$params[] = $perPage;
@ -71,9 +70,8 @@ class Beleg
{
return Database::fetchOne(
'SELECT b.*,
COALESCE(b.betrag_brutto,
(SELECT SUM(bk.betrag_brutto) FROM bookings bk
WHERE bk.beleg_id = b.id AND bk.is_reversal = 0)
WHERE bk.beleg_id = b.id AND bk.is_reversal = 0
) AS betrag_brutto,
fy.year as fiscal_year_nr,
s.name as supplier_name,
@ -98,8 +96,8 @@ class Beleg
Database::execute(
'INSERT INTO belege (beleg_nr, filename, storage_path, mime_type, file_hash,
beleg_datum, eingangs_datum, rechnungs_nr, betrag_brutto, belegtyp, notes, fiscal_year_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
beleg_datum, rechnungs_nr, betrag_brutto, belegtyp, notes, fiscal_year_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
[
$data['beleg_nr'],
$data['filename'],
@ -107,7 +105,6 @@ class Beleg
$data['mime_type'],
$data['file_hash'],
$data['beleg_datum'] ?: null,
$data['eingangs_datum'],
$data['rechnungs_nr'] ?? null,
$data['betrag_brutto'] ?? null,
$data['belegtyp'] ?? 'Eingangsrechnung',
@ -134,13 +131,12 @@ class Beleg
}
Database::execute(
'UPDATE belege SET beleg_datum=?, rechnungs_nr=?, betrag_brutto=?, belegtyp=?, notes=?,
'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['betrag_brutto'] ?? null,
$data['belegtyp'] ?? 'Eingangsrechnung',
$data['notes'] ?? null,
$fyId,
@ -163,7 +159,6 @@ class Beleg
rechnungs_nr = ?,
belegtyp = ?,
beleg_datum = ?,
betrag_brutto = ?,
supplier_id = ?,
fiscal_year_id = COALESCE(?, fiscal_year_id),
updated_at = NOW()
@ -172,7 +167,6 @@ class Beleg
$data['rechnungs_nr'] ?: null,
$data['belegtyp'],
$data['beleg_datum'] ?: null,
$data['betrag_brutto'] !== null && $data['betrag_brutto'] !== '' ? (float)$data['betrag_brutto'] : null,
isset($data['supplier_id']) && $data['supplier_id'] !== '' ? (int)$data['supplier_id'] : null,
$fyId,
$id,

View file

@ -105,8 +105,8 @@ class Booking
'INSERT INTO bookings (buchungs_nr, beleg_id, buchungs_datum, buchungstyp,
kategorie_konto_id, zahlungskonto_id,
betrag_netto, ust_satz, steuerart, betrag_ust, betrag_brutto,
buchungstext, bewirtungsgrund, teilnehmerliste, fiscal_year_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
buchungstext, fiscal_year_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
[
$nr,
(int)$data['beleg_id'],
@ -120,8 +120,6 @@ class Booking
$ust,
$brutto,
$data['buchungstext'],
$data['bewirtungsgrund'] ?? null,
$data['teilnehmerliste'] ?? null,
$fy['id'],
]
);

View file

@ -269,7 +269,6 @@ class NextcloudImportService
'mime_type' => $contentType,
'file_hash' => $hash,
'beleg_datum' => null,
'eingangs_datum' => $today,
'rechnungs_nr' => null,
'betrag_brutto' => null,
'belegtyp' => 'Eingangsrechnung',

View file

@ -57,10 +57,7 @@ $suppliers = $suppliers ?? [];
<?= View::formatDate($beleg['beleg_datum']) ?>
<?php endif; ?>
</div>
<div>
<span class="text-muted text-sm">Eingangsdatum</span><br>
<?= View::formatDate($beleg['eingangs_datum']) ?>
</div>
<div>
<span class="text-muted text-sm">Rechnungsnr.<?= !$beleg['locked'] ? ' <span style="opacity:.4;font-size:.7rem">✎</span>' : '' ?></span><br>
<?php if (!$beleg['locked']): ?>
@ -75,7 +72,7 @@ $suppliers = $suppliers ?? [];
<?php endif; ?>
</div>
<div>
<span class="text-muted text-sm">Brutto lt. Beleg</span><br>
<span class="text-muted text-sm">Brutto (Buchungen)</span><br>
<?= $beleg['betrag_brutto'] ? '<strong>' . $fmt($beleg['betrag_brutto']) . '</strong>' : '<span class="text-muted">–</span>' ?>
</div>
<div>
@ -86,6 +83,17 @@ $suppliers = $suppliers ?? [];
<span class="badge badge-red">⚠ Prüfsumme abweichend</span>
<?php endif; ?>
</div>
<?php if (!empty($beleg['bewirtungsgrund']) || !empty($beleg['teilnehmerliste'])): ?>
<div>
<span class="text-muted text-sm">Bewirtung</span><br>
<?php if (!empty($beleg['bewirtungsgrund'])): ?>
<span><?= $h($beleg['bewirtungsgrund']) ?></span>
<?php endif; ?>
<?php if (!empty($beleg['teilnehmerliste'])): ?>
<span class="text-muted" style="font-size:.85em"> · <?= $h($beleg['teilnehmerliste']) ?></span>
<?php endif; ?>
</div>
<?php endif; ?>
<div style="margin-left:auto;display:flex;gap:.5rem">
<a href="/belege/<?= $h($beleg['id']) ?>/file" target="_blank" class="btn btn-secondary btn-sm">Datei öffnen</a>
<?php if (!$beleg['locked'] && in_array($beleg['mime_type'], ['image/jpeg', 'image/png'])): ?>
@ -127,11 +135,7 @@ $suppliers = $suppliers ?? [];
<label style="font-size:.8rem">Belegdatum</label>
<input type="date" name="beleg_datum" value="<?= $h($beleg['beleg_datum'] ?? '') ?>">
</div>
<div class="form-group" style="margin:0;min-width:120px">
<label style="font-size:.8rem">Brutto lt. Beleg (€)</label>
<input type="number" name="betrag_brutto" step="0.01" min="0"
value="<?= $h($beleg['betrag_brutto'] ?? '') ?>" placeholder="0.00">
</div>
<div class="form-group" style="margin:0;min-width:180px">
<label style="font-size:.8rem">Lieferant</label>
<input type="text" name="supplier_name" id="supplier_name_input"
@ -217,12 +221,6 @@ $suppliers = $suppliers ?? [];
<?php elseif ($isReversed): ?>
<span class="badge badge-red">Storniert</span>
<?php endif; ?>
<?php if (!empty($bk['bewirtungsgrund'])): ?>
<br><span class="td-muted" style="font-size:.8em">Grund: <?= $h($bk['bewirtungsgrund']) ?></span>
<?php endif; ?>
<?php if (!empty($bk['teilnehmerliste'])): ?>
<br><span class="td-muted" style="font-size:.8em">Teilnehmer: <?= $h($bk['teilnehmerliste']) ?></span>
<?php endif; ?>
</td>
<td>
<?php if (!$bk['locked'] && !$isReversal && !$isReversed): ?>
@ -378,12 +376,13 @@ $suppliers = $suppliers ?? [];
<label for="kategorie_konto_id">Kategorie *
<span id="kategorie_konto_art" class="badge badge-gray" style="margin-left:.5rem;font-size:.7rem"></span>
</label>
<input type="search" placeholder="Kontonr. oder Bezeichnung filtern…" class="konto-filter"
data-target="kategorie_konto_id" autocomplete="off" style="margin-bottom:.3rem">
<select id="kategorie_konto_id" name="kategorie_konto_id" required size="6"
<input type="search" name="kategorie_kontonummer" placeholder="Kontonr. oder Bezeichnung filtern – oder direkt eingeben" class="konto-filter"
data-target="kategorie_konto_id" autocomplete="off" style="margin-bottom:.3rem"
value="<?= $h($formData['kategorie_kontonummer'] ?? '') ?>">
<select id="kategorie_konto_id" name="kategorie_konto_id" size="6"
onchange="showKontoArt('kategorie_konto_id','kategorie_konto_art',this);updateEigenanteilState();updateBewirtungsState();updateAutoUst()">
<option value="">– Konto wählen –</option>
<?php foreach (['Betriebsausgabe', 'Betriebseinnahme', 'Privat'] as $art): ?>
<?php foreach (['Betriebsausgabe', 'Betriebseinnahme', 'Privat', 'Verbindlichkeit'] as $art): ?>
<?php if (empty($kontenGruppen[$art])) continue; ?>
<optgroup label="── <?= $h($art) ?> ──" data-kontenart="<?= $h($art) ?>">
<?php foreach ($kontenGruppen[$art] as $acc): ?>
@ -405,9 +404,10 @@ $suppliers = $suppliers ?? [];
<label for="zahlungskonto_id">Zahlungskonto *
<span id="zahlungskonto_art" class="badge badge-gray" style="margin-left:.5rem;font-size:.7rem"></span>
</label>
<input type="search" placeholder="Kontonr. oder Bezeichnung filtern…" class="konto-filter"
data-target="zahlungskonto_id" autocomplete="off" style="margin-bottom:.3rem">
<select id="zahlungskonto_id" name="zahlungskonto_id" required size="4"
<input type="search" name="zahlungs_kontonummer" placeholder="Kontonr. oder Bezeichnung filtern – oder direkt eingeben" class="konto-filter"
data-target="zahlungskonto_id" autocomplete="off" style="margin-bottom:.3rem"
value="<?= $h($formData['zahlungs_kontonummer'] ?? '') ?>">
<select id="zahlungskonto_id" name="zahlungskonto_id" size="4"
onchange="showKontoArt('zahlungskonto_id','zahlungskonto_art',this)">
<option value="">– Konto wählen –</option>
<?php foreach (['Zahlungskonto', 'Verbindlichkeit'] as $art): ?>
@ -451,7 +451,7 @@ $suppliers = $suppliers ?? [];
<!-- Bewirtungsfelder (nur bei Konto 4650/4651) -->
<div id="bewirtungsfelder" style="display:none">
<div class="flash flash-info" style="margin-bottom:.75rem;padding:.5rem .75rem;font-size:.85rem">
<div class="flash flash-info flash-persistent" style="margin-bottom:.75rem;padding:.5rem .75rem;font-size:.85rem">
<strong>Bewirtungsausgabe:</strong> 70 % werden auf das gewählte Konto gebucht, 30 % automatisch auf das Abzugskonto. Die Vorsteuer wird vollständig berücksichtigt.
</div>
<div class="form-group">
@ -459,13 +459,13 @@ $suppliers = $suppliers ?? [];
<textarea name="bewirtungsgrund" id="bewirtungsgrund_input"
style="height:2.4em;resize:none;overflow:hidden"
oninput="this.style.height='auto';this.style.height=this.scrollHeight+'px'"
placeholder="Anlass/Grund der Bewirtung"><?= $h($formData['bewirtungsgrund'] ?? '') ?></textarea>
placeholder="Anlass/Grund der Bewirtung"><?= $h($formData['bewirtungsgrund'] ?? $beleg['bewirtungsgrund'] ?? '') ?></textarea>
</div>
<div class="form-group">
<label for="teilnehmerliste_input">Teilnehmerliste *</label>
<textarea name="teilnehmerliste" id="teilnehmerliste_input"
style="height:5.4em;resize:vertical"
placeholder="Namen der Teilnehmer"><?= $h($formData['teilnehmerliste'] ?? '') ?></textarea>
placeholder="Namen der Teilnehmer"><?= $h($formData['teilnehmerliste'] ?? $beleg['teilnehmerliste'] ?? '') ?></textarea>
</div>
</div>
@ -497,8 +497,8 @@ function filterKategorieKonten(buchungstyp) {
groups.forEach(function(g) {
const art = g.getAttribute('data-kontenart');
if (!art) return;
// Privat immer anzeigen, Kategorie nach Typ filtern
g.style.display = (art === zielArt || art === 'Privat') ? '' : 'none';
// Privat und Verbindlichkeit immer anzeigen, Kategorie nach Typ filtern
g.style.display = (art === zielArt || art === 'Privat' || art === 'Verbindlichkeit') ? '' : 'none';
});
// Auswahl zurücksetzen wenn aktuell gewähltes Konto nicht mehr sichtbar
@ -518,8 +518,10 @@ function updateAutoUst() {
const selectedOpt = kategorieEl.options[kategorieEl.selectedIndex];
const steuercode = selectedOpt ? (selectedOpt.getAttribute('data-steuercode') || '') : '';
const kontonummer = selectedOpt ? (selectedOpt.getAttribute('data-kontonummer') || '') : '';
const ustMap = { 'VSt19': '19', 'USt19': '19', 'VSt7': '7', 'USt7': '7' };
const autoUst = ustMap[steuercode] || null;
// 4652/4657 immer 0 % (keine Vorsteuer bei pauschalen Bewirtungskosten ohne USt-Ausweis)
const autoUst = (['4652','4657'].includes(kontonummer)) ? '0' : (ustMap[steuercode] || null);
const hiddenEl = document.getElementById('ust_satz_hidden');
if (autoUst !== null) {
@ -543,7 +545,7 @@ function updateBewirtungsState() {
const selectedOpt = kategorieEl.options[kategorieEl.selectedIndex];
const kontonummer = selectedOpt ? (selectedOpt.getAttribute('data-kontonummer') || '') : '';
const istBewirtung = kontonummer === '4650' || kontonummer === '4651';
const istBewirtung = ['4650','4651','4652'].includes(kontonummer);
felder.style.display = istBewirtung ? '' : 'none';

View file

@ -65,7 +65,7 @@
const fd = new FormData();
fd.append('beleg_file', file);
fd.append('belegtyp', 'Eingangsrechnung');
fd.append('eingangs_datum', new Date().toISOString().slice(0,10));
fd.append('beleg_datum', new Date().toISOString().slice(0,10));
fetch('/belege/upload', {
@ -132,7 +132,7 @@
<thead>
<tr>
<th>Belegnr.</th>
<th>Eingangsdatum</th>
<th>Belegdatum</th>
<th>Lieferant</th>
<th>Rechnungsnr.</th>
@ -146,7 +146,7 @@
<?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['eingangs_datum']) ?></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>

View file

@ -25,11 +25,7 @@
</select>
</div>
<div class="form-group">
<label for="eingangs_datum">Eingangsdatum *</label>
<input type="date" id="eingangs_datum" name="eingangs_datum"
value="<?= $h($old['eingangs_datum'] ?? date('Y-m-d')) ?>" required>
</div>
</div>
<div class="form-grid form-grid-2">
@ -46,12 +42,7 @@
</div>
</div>
<div class="form-group">
<label for="betrag_brutto">Bruttobetrag (€)</label>
<input type="number" id="betrag_brutto" name="betrag_brutto"
value="<?= $h($old['betrag_brutto'] ?? '') ?>"
step="0.01" min="0" placeholder="0.00">
</div>
<div class="form-group">
<label for="notes">Notizen</label>

View file

@ -115,7 +115,7 @@ $fmt = fn($v) => View::formatMoney($v);
const fd = new FormData();
fd.append('beleg_file', file);
fd.append('belegtyp', 'Eingangsrechnung');
fd.append('eingangs_datum', new Date().toISOString().slice(0,10));
fd.append('beleg_datum', new Date().toISOString().slice(0,10));
fetch('/belege/upload', {