From 0284ea8726a03f2ad0d9e2a95c5ea30fa4950e8e Mon Sep 17 00:00:00 2001 From: florianfederspiel Date: Thu, 23 Apr 2026 17:24:58 +0200 Subject: [PATCH] =?UTF-8?q?Manuelle=20Buchungen=20um=20DATEV-Steuerschl?= =?UTF-8?q?=C3=BCssel=20und=20getrennte=20Soll/Haben-Auswahl=20erweitern?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...31_manual_statementallocations_tax_key.sql | 1 + backend/db/migrations/meta/_journal.json | 7 + backend/db/schema/statementallocations.ts | 1 + backend/src/routes/banking.ts | 2 + backend/src/utils/export/datev.ts | 2 +- frontend/pages/accounting/manual-bookings.vue | 375 ++++++++++++------ 6 files changed, 261 insertions(+), 127 deletions(-) create mode 100644 backend/db/migrations/0031_manual_statementallocations_tax_key.sql diff --git a/backend/db/migrations/0031_manual_statementallocations_tax_key.sql b/backend/db/migrations/0031_manual_statementallocations_tax_key.sql new file mode 100644 index 0000000..de29737 --- /dev/null +++ b/backend/db/migrations/0031_manual_statementallocations_tax_key.sql @@ -0,0 +1 @@ +ALTER TABLE "statementallocations" ADD COLUMN "datev_tax_key" text; diff --git a/backend/db/migrations/meta/_journal.json b/backend/db/migrations/meta/_journal.json index f8581b6..d22350e 100644 --- a/backend/db/migrations/meta/_journal.json +++ b/backend/db/migrations/meta/_journal.json @@ -218,6 +218,13 @@ "when": 1776297600000, "tag": "0030_manual_statementallocations", "breakpoints": true + }, + { + "idx": 31, + "version": "7", + "when": 1776298200000, + "tag": "0031_manual_statementallocations_tax_key", + "breakpoints": true } ] } diff --git a/backend/db/schema/statementallocations.ts b/backend/db/schema/statementallocations.ts index dff28a9..850e0af 100644 --- a/backend/db/schema/statementallocations.ts +++ b/backend/db/schema/statementallocations.ts @@ -56,6 +56,7 @@ export const statementallocations = pgTable("statementallocations", { description: text("description"), manualBookingDate: text("manual_booking_date"), + datevTaxKey: text("datev_tax_key"), bookingMode: text("booking_mode").notNull().default("expense"), depreciationMonths: integer("depreciation_months"), diff --git a/backend/src/routes/banking.ts b/backend/src/routes/banking.ts index 311044e..c09676b 100644 --- a/backend/src/routes/banking.ts +++ b/backend/src/routes/banking.ts @@ -48,6 +48,7 @@ export default async function bankingRoutes(server: FastifyInstance) { next.contraCustomer = null next.contraVendor = null next.contraOwnaccount = null + next.datevTaxKey = next.datevTaxKey ? String(next.datevTaxKey).trim() : null return { data: next } } @@ -71,6 +72,7 @@ export default async function bankingRoutes(server: FastifyInstance) { next.amount = Math.abs(Number(next.amount)) next.bankstatement = null next.manualBookingDate = dayjs(next.manualBookingDate).format("YYYY-MM-DD") + next.datevTaxKey = next.datevTaxKey ? String(next.datevTaxKey).trim() : null return { data: next } } diff --git a/backend/src/utils/export/datev.ts b/backend/src/utils/export/datev.ts index a6c4210..2800e2b 100644 --- a/backend/src/utils/export/datev.ts +++ b/backend/src/utils/export/datev.ts @@ -357,7 +357,7 @@ export async function buildExportZip( const credit = getManualBookingSide(alloc, "credit"); const dateManual = dayjs(alloc.manualBookingDate).format("DDMM"); const dateManualFull = dayjs(alloc.manualBookingDate).format("DD.MM.YYYY"); - bookingLines.push(`${displayCurrency(alloc.amount,true)};"S";;;;;${debit.number};${credit.number};"";${dateManual};"";;;"${`MB ${debit.number} an ${credit.number} ${escapeString(alloc.description)}`.substring(0,59)}";;;;;;;"Geschäftspartner";"${escapeString(debit.name || credit.name)}";"Kundennummer";"${debit.number}";"Belegnummer";"";"Leistungsdatum";"${dateManualFull}";"Belegdatum";"${dateManualFull}";;;;;;;;;;"";;;;;;;;Manuelle-Buchung;${alloc.id};;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;`); + bookingLines.push(`${displayCurrency(alloc.amount,true)};"S";;;;;${debit.number};${credit.number};"${alloc.datevTaxKey || ""}";${dateManual};"";;;"${`MB ${debit.number} an ${credit.number} ${escapeString(alloc.description)}`.substring(0,59)}";;;;;;;"Geschäftspartner";"${escapeString(debit.name || credit.name)}";"Kundennummer";"${debit.number}";"Belegnummer";"";"Leistungsdatum";"${dateManualFull}";"Belegdatum";"${dateManualFull}";;;;;;;;;;"";;;;;;;;Manuelle-Buchung;${alloc.id};;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;`); return; } diff --git a/frontend/pages/accounting/manual-bookings.vue b/frontend/pages/accounting/manual-bookings.vue index 11d8e98..8e1d149 100644 --- a/frontend/pages/accounting/manual-bookings.vue +++ b/frontend/pages/accounting/manual-bookings.vue @@ -10,50 +10,99 @@ const customers = ref([]) const vendors = ref([]) const ownaccounts = ref([]) const bookings = ref([]) +const debitSearch = ref("") +const creditSearch = ref("") + +const DATEV_TAX_KEY_ITEMS = [ + { value: "", label: "Ohne Steuerschlüssel" }, + { value: "9", label: "9 - Vorsteuer 19 %" }, + { value: "8", label: "8 - Vorsteuer 7 %" }, + { value: "19", label: "19 - EU Vorsteuer 19 %" }, + { value: "18", label: "18 - EU Vorsteuer 7 %" } +] const form = reactive({ manualBookingDate: dayjs().format("YYYY-MM-DD"), amount: null, debit: "", credit: "", + datevTaxKey: "", description: "" }) -const entryOptions = computed(() => [ - ...accounts.value.map((item) => ({ - key: `account:${item.id}`, - label: `${item.number} - ${item.label}`, - number: item.number, - name: item.label, - type: "Sachkonto" - })), - ...vendors.value.map((item) => ({ - key: `vendor:${item.id}`, - label: `${item.vendorNumber || "ohne Nr."} - ${item.name}`, - number: item.vendorNumber, - name: item.name, - type: "Kreditor" - })), - ...customers.value.map((item) => ({ - key: `customer:${item.id}`, - label: `${item.customerNumber || "ohne Nr."} - ${item.name}`, - number: item.customerNumber, - name: item.name, - type: "Debitor" - })), - ...ownaccounts.value.map((item) => ({ - key: `ownaccount:${item.id}`, - label: `${item.number} - ${item.name}`, - number: item.number, - name: item.name, - type: "Zusätzliches Konto" - })) -]) - -const selectedDebit = computed(() => entryOptions.value.find((item) => item.key === form.debit)) -const selectedCredit = computed(() => entryOptions.value.find((item) => item.key === form.credit)) - const displayCurrency = (value) => `${Number(value || 0).toFixed(2).replace(".", ",")} €` +const normalizeSearch = (value) => String(value || "").toLowerCase().trim() +const matchesSearch = (entry, query) => { + const search = normalizeSearch(query) + if (!search) return true + + return [ + entry.number, + entry.name, + entry.label, + entry.typeLabel + ].some((value) => normalizeSearch(value).includes(search)) +} + +const buildEntries = (rows, type, labelBuilder) => + (rows || []).map((item) => ({ + key: `${type}:${item.id}`, + id: item.id, + type, + number: item.number || item.vendorNumber || item.customerNumber || "", + name: item.label || item.name || "", + label: labelBuilder(item), + typeLabel: + type === "account" + ? "Sachkonten" + : type === "vendor" + ? "Kreditoren" + : type === "customer" + ? "Debitoren" + : "Zusätzliche Konten" + })) + +const entryGroups = computed(() => ([ + { + key: "account", + label: "Sachkonten", + entries: buildEntries(accounts.value, "account", (item) => `${item.number} - ${item.label}`) + }, + { + key: "vendor", + label: "Kreditoren", + entries: buildEntries(vendors.value, "vendor", (item) => `${item.vendorNumber || "ohne Nr."} - ${item.name}`) + }, + { + key: "customer", + label: "Debitoren", + entries: buildEntries(customers.value, "customer", (item) => `${item.customerNumber || "ohne Nr."} - ${item.name}`) + }, + { + key: "ownaccount", + label: "Zusätzliche Konten", + entries: buildEntries(ownaccounts.value, "ownaccount", (item) => `${item.number} - ${item.name}`) + } +])) + +const groupedDebitEntries = computed(() => + entryGroups.value.map((group) => ({ + ...group, + entries: group.entries.filter((entry) => matchesSearch(entry, debitSearch.value)) + })).filter((group) => group.entries.length > 0) +) + +const groupedCreditEntries = computed(() => + entryGroups.value.map((group) => ({ + ...group, + entries: group.entries.filter((entry) => matchesSearch(entry, creditSearch.value)) + })).filter((group) => group.entries.length > 0) +) + +const allEntries = computed(() => entryGroups.value.flatMap((group) => group.entries)) +const selectedDebit = computed(() => allEntries.value.find((item) => item.key === form.debit)) +const selectedCredit = computed(() => allEntries.value.find((item) => item.key === form.credit)) +const selectedTaxKey = computed(() => DATEV_TAX_KEY_ITEMS.find((item) => item.value === form.datevTaxKey)) const getBookingSide = (booking, side) => { const map = side === "credit" @@ -123,7 +172,10 @@ const resetForm = () => { form.amount = null form.debit = "" form.credit = "" + form.datevTaxKey = "" form.description = "" + debitSearch.value = "" + creditSearch.value = "" } const saveBooking = async () => { @@ -133,23 +185,27 @@ const saveBooking = async () => { } saving.value = true - const payload = { - manualBookingDate: form.manualBookingDate, - amount: Number(form.amount), - description: form.description || "Manuelle Buchung", - ...buildSidePayload(form.debit, "debit"), - ...buildSidePayload(form.credit, "credit") + try { + const payload = { + manualBookingDate: form.manualBookingDate, + amount: Number(form.amount), + datevTaxKey: form.datevTaxKey || null, + description: form.description || "Manuelle Buchung", + ...buildSidePayload(form.debit, "debit"), + ...buildSidePayload(form.credit, "credit") + } + + await useNuxtApp().$api("/api/banking/statements", { + method: "POST", + body: { data: payload } + }) + + toast.add({ title: "Manuelle Buchung erstellt." }) + resetForm() + await loadData() + } finally { + saving.value = false } - - await useNuxtApp().$api("/api/banking/statements", { - method: "POST", - body: { data: payload } - }) - - toast.add({ title: "Manuelle Buchung erstellt." }) - resetForm() - await loadData() - saving.value = false } const deleteBooking = async (booking) => { @@ -165,74 +221,125 @@ onMounted(loadData) -
+
-
- - - +
+
+
+
+

Soll

+

Linke Buchungsseite

+
+ +
- - - +
+
+
{{ group.label }}
+
+ +
+
+
+
- - - - - +
+
+
+

Haben

+

Rechte Buchungsseite

+
+ +
- - - - - +
+
+
{{ group.label }}
+
+ +
+
+
+
+
- +
+
+ - - - + + + +
- - Manuelle Buchung erstellen - +
+ + Steuerschlüssel: {{ selectedTaxKey.label }} + + + Manuelle Buchung erstellen + +
@@ -240,7 +347,7 @@ onMounted(loadData) @@ -249,31 +356,47 @@ onMounted(loadData) Noch keine manuellen Buchungen erfasst.
-
-
+
+
{{ dayjs(booking.manualBookingDate).format("DD.MM.YYYY") }} {{ displayCurrency(booking.amount) }} + + St.-Schlüssel {{ booking.datevTaxKey }} +
-
- Soll: - {{ getBookingSide(booking, "debit").number }} - {{ getBookingSide(booking, "debit").name }} + +
+ +
+
+
Soll
+
+ {{ getBookingSide(booking, 'debit').number }} + {{ getBookingSide(booking, "debit").name }} +
+
{{ getBookingSide(booking, "debit").type }}
-
- Haben: - {{ getBookingSide(booking, "credit").number }} - {{ getBookingSide(booking, "credit").name }} -
-
- {{ booking.description }} + +
+
Haben
+
+ {{ getBookingSide(booking, 'credit').number }} + {{ getBookingSide(booking, "credit").name }} +
+
{{ getBookingSide(booking, "credit").type }}
- + +
+ {{ booking.description }} +