From 41e5a4021b7486c92bd67eca6ae09752272b9249 Mon Sep 17 00:00:00 2001 From: florianfederspiel Date: Thu, 23 Apr 2026 17:52:56 +0200 Subject: [PATCH] Manuelle Buchungen um zuweisbare Eingangsbelege erweitern --- ...nual_statementallocations_invoice_side.sql | 1 + backend/db/migrations/meta/_journal.json | 7 +++ backend/db/schema/statementallocations.ts | 1 + backend/src/routes/banking.ts | 20 ++++++++ backend/src/utils/export/datev.ts | 12 ++++- frontend/pages/accounting/manual-bookings.vue | 46 +++++++++++++++++-- 6 files changed, 82 insertions(+), 5 deletions(-) create mode 100644 backend/db/migrations/0032_manual_statementallocations_invoice_side.sql diff --git a/backend/db/migrations/0032_manual_statementallocations_invoice_side.sql b/backend/db/migrations/0032_manual_statementallocations_invoice_side.sql new file mode 100644 index 0000000..972cc2d --- /dev/null +++ b/backend/db/migrations/0032_manual_statementallocations_invoice_side.sql @@ -0,0 +1 @@ +ALTER TABLE "statementallocations" ADD COLUMN "manual_invoice_side" text; diff --git a/backend/db/migrations/meta/_journal.json b/backend/db/migrations/meta/_journal.json index d22350e..5e0b36e 100644 --- a/backend/db/migrations/meta/_journal.json +++ b/backend/db/migrations/meta/_journal.json @@ -225,6 +225,13 @@ "when": 1776298200000, "tag": "0031_manual_statementallocations_tax_key", "breakpoints": true + }, + { + "idx": 32, + "version": "7", + "when": 1776298800000, + "tag": "0032_manual_statementallocations_invoice_side", + "breakpoints": true } ] } diff --git a/backend/db/schema/statementallocations.ts b/backend/db/schema/statementallocations.ts index 850e0af..8f674b4 100644 --- a/backend/db/schema/statementallocations.ts +++ b/backend/db/schema/statementallocations.ts @@ -32,6 +32,7 @@ export const statementallocations = pgTable("statementallocations", { incominginvoice: bigint("ii_id", { mode: "number" }).references( () => incominginvoices.id ), + manualInvoiceSide: text("manual_invoice_side"), tenant: bigint("tenant", { mode: "number" }) .notNull() diff --git a/backend/src/routes/banking.ts b/backend/src/routes/banking.ts index c09676b..deffc1d 100644 --- a/backend/src/routes/banking.ts +++ b/backend/src/routes/banking.ts @@ -34,6 +34,8 @@ export default async function bankingRoutes(server: FastifyInstance) { const ContraCustomers = aliasedTable(customers, "contra_customers") const ContraVendors = aliasedTable(vendors, "contra_vendors") const ContraOwnaccounts = aliasedTable(ownaccounts, "contra_ownaccounts") + const ManualInvoices = aliasedTable(incominginvoices, "manual_invoices") + const ManualInvoiceVendors = aliasedTable(vendors, "manual_invoice_vendors") const normalizeManualSide = (payload: any, keys: string[]) => keys.filter((key) => payload[key] !== null && payload[key] !== undefined && payload[key] !== "") @@ -49,14 +51,24 @@ export default async function bankingRoutes(server: FastifyInstance) { next.contraVendor = null next.contraOwnaccount = null next.datevTaxKey = next.datevTaxKey ? String(next.datevTaxKey).trim() : null + next.manualInvoiceSide = null return { data: next } } const debitKeys = ["account", "customer", "vendor", "ownaccount"] const creditKeys = ["contraAccount", "contraCustomer", "contraVendor", "contraOwnaccount"] + const hasManualInvoice = next.incominginvoice !== null && next.incominginvoice !== undefined && next.incominginvoice !== "" const debitSide = normalizeManualSide(next, debitKeys) const creditSide = normalizeManualSide(next, creditKeys) + if (hasManualInvoice) { + if (next.manualInvoiceSide === "debit") debitSide.push("incominginvoice") + else if (next.manualInvoiceSide === "credit") creditSide.push("incominginvoice") + else return { error: "Für zugewiesene Eingangsbelege muss Soll oder Haben ausgewählt sein." } + } else { + next.manualInvoiceSide = null + } + if (!next.manualBookingDate || !dayjs(next.manualBookingDate).isValid()) { return { error: "Für manuelle Buchungen ist ein gültiges Buchungsdatum erforderlich." } } @@ -745,6 +757,8 @@ export default async function bankingRoutes(server: FastifyInstance) { contraCustomer: ContraCustomers, contraVendor: ContraVendors, contraOwnaccount: ContraOwnaccounts, + incominginvoice: ManualInvoices, + incominginvoiceVendor: ManualInvoiceVendors, }) .from(statementallocations) .leftJoin(accounts, eq(statementallocations.account, accounts.id)) @@ -755,6 +769,8 @@ export default async function bankingRoutes(server: FastifyInstance) { .leftJoin(ContraCustomers, eq(statementallocations.contraCustomer, ContraCustomers.id)) .leftJoin(ContraVendors, eq(statementallocations.contraVendor, ContraVendors.id)) .leftJoin(ContraOwnaccounts, eq(statementallocations.contraOwnaccount, ContraOwnaccounts.id)) + .leftJoin(ManualInvoices, eq(statementallocations.incominginvoice, ManualInvoices.id)) + .leftJoin(ManualInvoiceVendors, eq(ManualInvoices.vendor, ManualInvoiceVendors.id)) .where(and( eq(statementallocations.tenant, req.user.tenant_id), eq(statementallocations.archived, false), @@ -771,6 +787,10 @@ export default async function bankingRoutes(server: FastifyInstance) { contraCustomer: row.contraCustomer, contraVendor: row.contraVendor, contraOwnaccount: row.contraOwnaccount, + incominginvoice: row.incominginvoice ? { + ...row.incominginvoice, + vendor: row.incominginvoiceVendor, + } : null, }))) } catch (err) { console.error(err) diff --git a/backend/src/utils/export/datev.ts b/backend/src/utils/export/datev.ts index 2800e2b..26a9a00 100644 --- a/backend/src/utils/export/datev.ts +++ b/backend/src/utils/export/datev.ts @@ -341,11 +341,20 @@ export async function buildExportZip( const vendor = side === "credit" ? alloc.contraVendor : alloc.vendor; const customer = side === "credit" ? alloc.contraCustomer : alloc.customer; const ownaccount = side === "credit" ? alloc.contraOwnaccount : alloc.ownaccount; + const incominginvoice = alloc.manualInvoiceSide === side ? alloc.incominginvoice : null; if (account) return { number: account.number, name: account.label, type: "Sachkonto" }; if (vendor) return { number: vendor.vendorNumber, name: vendor.name, type: "Kreditor" }; if (customer) return { number: customer.customerNumber, name: customer.name, type: "Debitor" }; if (ownaccount) return { number: ownaccount.number, name: ownaccount.name, type: "Eigenes Konto" }; + if (incominginvoice) { + return { + number: incominginvoice.vendor?.vendorNumber || "", + name: `${incominginvoice.reference || "Eingangsbeleg"} ${incominginvoice.vendor?.name || ""}`.trim(), + type: "Eingangsbeleg", + reference: incominginvoice.reference || "", + }; + } return { number: "", name: "", type: prefix }; }; @@ -357,7 +366,8 @@ 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};"${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;;;;"";;;;;;`); + const belegnummer = debit.reference || credit.reference || ""; + bookingLines.push(`${displayCurrency(alloc.amount,true)};"S";;;;;${debit.number};${credit.number};"${alloc.datevTaxKey || ""}";${dateManual};"${belegnummer}";;;"${`MB ${debit.number} an ${credit.number} ${escapeString(alloc.description)}`.substring(0,59)}";;;;;;;"Geschäftspartner";"${escapeString(debit.name || credit.name)}";"Kundennummer";"${debit.number}";"Belegnummer";"${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 83633c0..42ded0e 100644 --- a/frontend/pages/accounting/manual-bookings.vue +++ b/frontend/pages/accounting/manual-bookings.vue @@ -9,6 +9,7 @@ const accounts = ref([]) const customers = ref([]) const vendors = ref([]) const ownaccounts = ref([]) +const incomingInvoices = ref([]) const bookings = ref([]) const debitSearch = ref("") const creditSearch = ref("") @@ -51,8 +52,8 @@ const buildEntries = (rows, type, labelBuilder) => key: `${type}:${item.id}`, id: item.id, type, - number: item.number || item.vendorNumber || item.customerNumber || "", - name: item.label || item.name || "", + number: item.number || item.vendorNumber || item.customerNumber || item.reference || "", + name: item.label || item.name || item.vendor?.name || "", label: labelBuilder(item), typeLabel: type === "account" @@ -61,9 +62,23 @@ const buildEntries = (rows, type, labelBuilder) => ? "Kreditoren" : type === "customer" ? "Debitoren" - : "Zusätzliche Konten" + : type === "incominginvoice" + ? "Eingangsbelege" + : "Zusätzliche Konten" })) +const getIncomingInvoiceGross = (invoice) => { + return Number((invoice.accounts || []).reduce((sum, account) => { + return sum + Number(account.amountNet || 0) + Number(account.amountTax || 0) + }, 0)) +} + +const getIncomingInvoiceOpenAmount = (invoice) => { + const gross = getIncomingInvoiceGross(invoice) + const allocated = Number((invoice.statementallocations || []).reduce((sum, allocation) => sum + Number(allocation.amount || 0), 0)) + return Math.abs(gross) - Math.abs(allocated) +} + const entryGroups = computed(() => ([ { key: "account", @@ -84,6 +99,11 @@ const entryGroups = computed(() => ([ key: "ownaccount", label: "Zusätzliche Konten", entries: buildEntries(ownaccounts.value, "ownaccount", (item) => `${item.number} - ${item.name}`) + }, + { + key: "incominginvoice", + label: "Eingangsbelege", + entries: buildEntries(incomingInvoices.value, "incominginvoice", (item) => `${item.reference || "Ohne Referenz"} - ${item.vendor?.name || "Ohne Lieferant"} - Offen ${displayCurrency(getIncomingInvoiceOpenAmount(item))}`) } ])) @@ -129,6 +149,14 @@ const selectedCredit = computed(() => allEntries.value.find((item) => item.key = const selectedTaxKey = computed(() => DATEV_TAX_KEY_ITEMS.find((item) => item.value === form.datevTaxKey)) const getBookingSide = (booking, side) => { + if (booking.incominginvoice && booking.manualInvoiceSide === side) { + return { + type: "Eingangsbeleg", + number: booking.incominginvoice?.reference || "", + name: booking.incominginvoice?.vendor?.name || booking.incominginvoice?.description || "" + } + } + const map = side === "credit" ? [ ["contraAccount", "Sachkonto"], @@ -161,6 +189,12 @@ const buildSidePayload = (sideKey, target) => { if (!type || !id) return const numericId = type === "ownaccount" ? id : Number(id) + if (type === "incominginvoice") { + return { + incominginvoice: numericId, + manualInvoiceSide: target + } + } if (target === "debit") { if (type === "account") return { account: numericId } if (type === "customer") return { customer: numericId } @@ -176,11 +210,12 @@ const buildSidePayload = (sideKey, target) => { const loadData = async () => { loading.value = true - const [accountRows, customerRows, vendorRows, ownaccountRows, bookingRows] = await Promise.all([ + const [accountRows, customerRows, vendorRows, ownaccountRows, incomingInvoiceRows, bookingRows] = await Promise.all([ useEntities("accounts").selectSpecial("*", "number", true), useEntities("customers").select(), useEntities("vendors").select(), useEntities("ownaccounts").select(), + useEntities("incominginvoices").select("*, vendor(*), statementallocations(id,amount)"), useNuxtApp().$api("/api/banking/manual-bookings") ]) @@ -188,6 +223,9 @@ const loadData = async () => { customers.value = customerRows || [] vendors.value = vendorRows || [] ownaccounts.value = ownaccountRows || [] + incomingInvoices.value = (incomingInvoiceRows || []) + .filter((invoice) => invoice.state === "Gebucht" && !invoice.archived) + .filter((invoice) => getIncomingInvoiceOpenAmount(invoice) > 0.004) bookings.value = (bookingRows || []).sort((a, b) => String(b.manualBookingDate || "").localeCompare(String(a.manualBookingDate || ""))) loading.value = false }