From 3d829c3bc0af11d468b679a4f1ca6a5f94d46be8 Mon Sep 17 00:00:00 2001 From: flfeders Date: Sat, 8 Aug 2026 09:18:17 +0200 Subject: [PATCH 01/14] Banknotizen automatisch speichern --- .../banking/statements/[mode]/[[id]].vue | 51 ++++++++++++++----- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/frontend/pages/banking/statements/[mode]/[[id]].vue b/frontend/pages/banking/statements/[mode]/[[id]].vue index de61cd1..a65c8c8 100644 --- a/frontend/pages/banking/statements/[mode]/[[id]].vue +++ b/frontend/pages/banking/statements/[mode]/[[id]].vue @@ -40,6 +40,9 @@ const ownaccounts = ref([]) const loading = ref(true) const loadingDocuments = ref(true) const savingNotes = ref(false) +const notesSaveDelay = 800 +let notesSaveTimer = null +let notesSaveQueued = false const rebuildDocumentLists = () => { const documents = createddocuments.value.filter(i => i.type === "invoices" || i.type === "advanceInvoices") @@ -262,14 +265,26 @@ const removeAllocation = async (allocationId) => { } const saveNotes = async () => { + if (!itemInfo.value?.id || !notesChanged.value) return + if (savingNotes.value) { + notesSaveQueued = true + return + } + + const notes = String(itemInfo.value.notes || "").trim() || null savingNotes.value = true try { - const notes = String(itemInfo.value.notes || "").trim() || null const updated = await useEntities("bankstatements").update(itemInfo.value.id, {notes}, true) - itemInfo.value.notes = updated.notes oldItemInfo.value.notes = updated.notes + if (String(itemInfo.value.notes || "").trim() === String(notes || "")) { + itemInfo.value.notes = updated.notes + } } finally { savingNotes.value = false + if (notesSaveQueued || notesChanged.value) { + notesSaveQueued = false + scheduleNotesSave() + } } } @@ -277,6 +292,26 @@ const notesChanged = computed(() => String(itemInfo.value?.notes || "").trim() !== String(oldItemInfo.value?.notes || "").trim() ) +const scheduleNotesSave = () => { + if (notesSaveTimer) clearTimeout(notesSaveTimer) + notesSaveTimer = setTimeout(() => { + notesSaveTimer = null + void saveNotes() + }, notesSaveDelay) +} + +watch(() => itemInfo.value?.notes, () => { + if (!loading.value && notesChanged.value) scheduleNotesSave() +}) + +onBeforeRouteLeave(async () => { + if (notesSaveTimer) { + clearTimeout(notesSaveTimer) + notesSaveTimer = null + } + if (notesChanged.value) await saveNotes() +}) + const searchString = ref(tempStore.searchStrings["bankstatementsedit"] || '') const clearSearchString = () => { @@ -616,22 +651,12 @@ setup() -
- - Notiz speichern - -
From 09431a87c79130951f2b7488be6ad91c10f67965 Mon Sep 17 00:00:00 2001 From: flfeders Date: Sun, 9 Aug 2026 13:48:45 +0200 Subject: [PATCH 02/14] Fix document import account form submission --- frontend/pages/settings/document-imports/index.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/pages/settings/document-imports/index.vue b/frontend/pages/settings/document-imports/index.vue index 7fc51ad..b60e270 100644 --- a/frontend/pages/settings/document-imports/index.vue +++ b/frontend/pages/settings/document-imports/index.vue @@ -184,7 +184,7 @@ onMounted(load) From 8301d8777e7ead79d96bc6d4086806995fa00154 Mon Sep 17 00:00:00 2001 From: flfeders Date: Sun, 9 Aug 2026 16:35:47 +0200 Subject: [PATCH 07/14] feat: correct total tax on incoming invoices --- .../0062_incoming_invoice_tax_override.sql | 1 + backend/db/migrations/meta/_journal.json | 7 ++ backend/db/schema/incominginvoices.ts | 3 + backend/src/utils/export/datev.ts | 35 ++++--- backend/src/utils/liquidityForecast.ts | 17 +++- frontend/composables/useDepreciation.ts | 19 +++- frontend/composables/useSum.js | 18 ++-- frontend/composables/useTaxEvaluation.ts | 6 ++ frontend/pages/accounting/cashbooks/[id].vue | 4 +- frontend/pages/accounting/manual-bookings.vue | 4 +- frontend/pages/banking/index.vue | 8 +- .../banking/statements/[mode]/[[id]].vue | 8 +- .../pages/incomingInvoices/[mode]/[id].vue | 95 ++++++++++--------- frontend/pages/incomingInvoices/index.vue | 7 +- 14 files changed, 129 insertions(+), 103 deletions(-) create mode 100644 backend/db/migrations/0062_incoming_invoice_tax_override.sql diff --git a/backend/db/migrations/0062_incoming_invoice_tax_override.sql b/backend/db/migrations/0062_incoming_invoice_tax_override.sql new file mode 100644 index 0000000..d6a92bb --- /dev/null +++ b/backend/db/migrations/0062_incoming_invoice_tax_override.sql @@ -0,0 +1 @@ +ALTER TABLE "incominginvoices" ADD COLUMN "tax_amount_override" numeric(12, 2); diff --git a/backend/db/migrations/meta/_journal.json b/backend/db/migrations/meta/_journal.json index 5a99836..596952c 100644 --- a/backend/db/migrations/meta/_journal.json +++ b/backend/db/migrations/meta/_journal.json @@ -414,6 +414,13 @@ "when": 1786280400000, "tag": "0061_additional_skr03_accounts", "breakpoints": true + }, + { + "idx": 59, + "version": "7", + "when": 1786284000000, + "tag": "0062_incoming_invoice_tax_override", + "breakpoints": true } ] } diff --git a/backend/db/schema/incominginvoices.ts b/backend/db/schema/incominginvoices.ts index caa1ffb..ef43f31 100644 --- a/backend/db/schema/incominginvoices.ts +++ b/backend/db/schema/incominginvoices.ts @@ -5,6 +5,7 @@ import { text, boolean, jsonb, + numeric, uuid, } from "drizzle-orm/pg-core" @@ -55,6 +56,8 @@ export const incominginvoices = pgTable("incominginvoices", { }, ]), + taxAmountOverride: numeric("tax_amount_override", { precision: 12, scale: 2 }), + paid: boolean("paid").notNull().default(false), expense: boolean("expense").notNull().default(true), diff --git a/backend/src/utils/export/datev.ts b/backend/src/utils/export/datev.ts index 7b176ce..7b8ab81 100644 --- a/backend/src/utils/export/datev.ts +++ b/backend/src/utils/export/datev.ts @@ -80,18 +80,17 @@ const formatDatevDate = (date: dayjs.ConfigType, format: string) => { return parsed.isValid() ? parsed.tz(DATEV_TIMEZONE).format(format) : ""; }; -const getIncomingInvoiceTaxReview = (account: any) => { - const taxRate = account.taxType === "19" ? 19 : account.taxType === "7" ? 7 : null; - if (taxRate === null) return null; +const getIncomingInvoiceTaxOverride = (invoice: any) => { + if (invoice.taxAmountOverride === null || invoice.taxAmountOverride === undefined || invoice.taxAmountOverride === "") return null; - const amountNet = Number(account.amountNet); - const amountTax = Number(account.amountTax); - if (!Number.isFinite(amountNet) || !Number.isFinite(amountTax)) return null; + const calculatedTax = (invoice.accounts as any[] || []).reduce((sum, account) => sum + Number(account.amountTax || 0), 0); + const overriddenTax = Number(invoice.taxAmountOverride); + if (!Number.isFinite(overriddenTax) || Math.abs(overriddenTax - calculatedTax) < 0.005) return null; - const calculatedTax = Number((amountNet * (taxRate / 100)).toFixed(2)); - if (Math.abs(amountTax - calculatedTax) < 0.005) return null; - - return `USt pruefen ${displayCurrency(amountTax, true)}`; + return { + amount: overriddenTax, + correction: Number((overriddenTax - calculatedTax).toFixed(2)), + }; }; const getCreatedDocumentRevenueLines = (document: any) => { @@ -368,7 +367,12 @@ export async function buildExportZip( // ER incominginvoicesList.forEach(ii => { const accs = ii.accounts as any[] || []; - accs.forEach(account => { + const taxOverride = getIncomingInvoiceTaxOverride(ii); + const correctionAccountIndex = taxOverride + ? Math.max(0, accs.findIndex(account => account.taxType === "19" || account.taxType === "7")) + : -1; + + accs.forEach((account, accountIndex) => { let file = filesIncomingInvoices.find(i => i.incominginvoice === ii.id); let accountData = accountsList.find(i => i.id === account.account); if (!accountData) return; @@ -381,10 +385,13 @@ export async function buildExportZip( else if(account.taxType === '7I') buschluessel = "18"; else buschluessel = "-"; - let amountGross =/* account.amountGross ? account.amountGross : */(account.amountNet || 0) + (account.amountTax || 0); + let amountGross = Number(account.amountNet || 0) + Number(account.amountTax || 0); + if (taxOverride && accountIndex === correctionAccountIndex) amountGross += taxOverride.correction; let shSelector = Math.sign(amountGross) === -1 ? "H" : "S"; - const taxReview = getIncomingInvoiceTaxReview(account); - let text = `${taxReview ? `${taxReview} - ` : ""}ER ${ii.reference}: ${escapeString(ii.description)}`.substring(0,59); + const taxReview = taxOverride && accountIndex === correctionAccountIndex + ? `USt pruefen ${displayCurrency(taxOverride.amount, true)} - ` + : ""; + let text = `${taxReview}ER ${ii.reference}: ${escapeString(ii.description)}`.substring(0,59); const vend = ii.vendor; // durch Mapping verfügbar bookingLines.push(`${Math.abs(amountGross).toFixed(2).replace(".",",")};"${shSelector}";;;;;${accountData.number};${vend?.vendorNumber || ""};"${buschluessel}";${formatDatevDate(ii.date, "DDMM")};"${ii.reference}";;;"${text}";;;;;;${file ? `"BEDI ""${file.id}"""` : ""};"Geschäftspartner";"${vend?.name || ""}";"Kundennummer";"${vend?.vendorNumber || ""}";"Belegnummer";"${ii.reference}";"Leistungsdatum";"${formatDatevDate(ii.date, "DD.MM.YYYY")}";"Belegdatum";"${formatDatevDate(ii.date, "DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`); diff --git a/backend/src/utils/liquidityForecast.ts b/backend/src/utils/liquidityForecast.ts index 21d10a1..a2567cd 100644 --- a/backend/src/utils/liquidityForecast.ts +++ b/backend/src/utils/liquidityForecast.ts @@ -287,6 +287,12 @@ const getIncomingInvoiceTaxBreakdown = (invoice: any): TaxBreakdown => { } }); + if (invoice?.taxAmountOverride !== null && invoice?.taxAmountOverride !== undefined && invoice?.taxAmountOverride !== "") { + const correction = Number(invoice.taxAmountOverride) - breakdown.tax19 - breakdown.tax7; + if (breakdown.tax19 !== 0 || breakdown.net19 !== 0) breakdown.tax19 += correction; + else if (breakdown.tax7 !== 0 || breakdown.net7 !== 0) breakdown.tax7 += correction; + } + return { net19: roundMoney(breakdown.net19), tax19: roundMoney(breakdown.tax19), @@ -297,9 +303,14 @@ const getIncomingInvoiceTaxBreakdown = (invoice: any): TaxBreakdown => { }; const getIncomingInvoiceSignedAmount = (invoice: any) => { - const amount = (invoice.accounts || []).reduce((sum: number, account: any) => { - return sum + Number(account.amountNet || 0) + Number(account.amountTax || 0); - }, 0); + const totals = (invoice.accounts || []).reduce((result: { net: number, tax: number }, account: any) => ({ + net: result.net + Number(account.amountNet || 0), + tax: result.tax + Number(account.amountTax || 0), + }), { net: 0, tax: 0 }); + const tax = invoice.taxAmountOverride !== null && invoice.taxAmountOverride !== undefined && invoice.taxAmountOverride !== "" + ? Number(invoice.taxAmountOverride) + : totals.tax; + const amount = totals.net + tax; return roundMoney(invoice.expense === false ? amount : amount * -1); }; diff --git a/frontend/composables/useDepreciation.ts b/frontend/composables/useDepreciation.ts index bc870b2..538392e 100644 --- a/frontend/composables/useDepreciation.ts +++ b/frontend/composables/useDepreciation.ts @@ -222,13 +222,24 @@ export const getIncomingInvoiceImmediateExpenseNet = (invoice: any) => { } export const getIncomingInvoiceImmediateExpenseGross = (invoice: any) => { - return Number(((invoice?.accounts || []).reduce((sum: number, account: any) => { + const immediateAccounts = (invoice?.accounts || []).filter((account: any) => { + const normalized = normalizeIncomingInvoiceAccount(account, invoice?.date) + return !isDepreciationBookingMode(normalized.bookingMode) + }) + const gross = immediateAccounts.reduce((sum: number, account: any) => { const normalized = normalizeIncomingInvoiceAccount(account, invoice?.date) - if (isDepreciationBookingMode(normalized.bookingMode)) return sum - const amountGross = Number(normalized.amountGross) return sum + (Number.isFinite(amountGross) ? amountGross : Number(normalized.amountNet || 0) + Number(normalized.amountTax || 0)) - }, 0)).toFixed(2)) + }, 0) + const calculatedTax = (invoice?.accounts || []).reduce((sum: number, account: any) => sum + Number(account.amountTax || 0), 0) + const correction = immediateAccounts.length > 0 + && invoice?.taxAmountOverride !== null + && invoice?.taxAmountOverride !== undefined + && invoice?.taxAmountOverride !== "" + ? Number(invoice.taxAmountOverride) - calculatedTax + : 0 + + return Number((gross + correction).toFixed(2)) } export const getIncomingInvoiceDepreciationRows = (invoice: any, rangeStart: any, rangeEnd: any) => { diff --git a/frontend/composables/useSum.js b/frontend/composables/useSum.js index 5fdd07d..abdbd44 100644 --- a/frontend/composables/useSum.js +++ b/frontend/composables/useSum.js @@ -12,17 +12,15 @@ export const useSum = () => { } const getIncomingInvoiceSum = (invoice) => { - let sum = 0 - invoice.accounts.forEach(account => { + const totals = (invoice.accounts || []).reduce((result, account) => ({ + net: result.net + Number(account.amountNet || 0), + tax: result.tax + Number(account.amountTax || 0) + }), { net: 0, tax: 0 }) + const tax = invoice.taxAmountOverride !== null && invoice.taxAmountOverride !== undefined && invoice.taxAmountOverride !== "" + ? Number(invoice.taxAmountOverride) + : totals.tax - - sum += account.amountTax - sum += account.amountNet - - - - }) - return sum.toFixed(2) + return (totals.net + tax).toFixed(2) } const getCreatedDocumentSum = (createddocument,createddocuments = []) => { diff --git a/frontend/composables/useTaxEvaluation.ts b/frontend/composables/useTaxEvaluation.ts index 189f276..c141804 100644 --- a/frontend/composables/useTaxEvaluation.ts +++ b/frontend/composables/useTaxEvaluation.ts @@ -152,6 +152,12 @@ export const getIncomingInvoiceTaxBreakdown = (invoice: any) => { } }) + if (invoice?.taxAmountOverride !== null && invoice?.taxAmountOverride !== undefined && invoice?.taxAmountOverride !== "") { + const correction = Number(invoice.taxAmountOverride) - breakdown.tax19 - breakdown.tax7 + if (breakdown.tax19 !== 0 || breakdown.net19 !== 0) breakdown.tax19 += correction + else if (breakdown.tax7 !== 0 || breakdown.net7 !== 0) breakdown.tax7 += correction + } + return { net19: Number(breakdown.net19.toFixed(2)), tax19: Number(breakdown.tax19.toFixed(2)), diff --git a/frontend/pages/accounting/cashbooks/[id].vue b/frontend/pages/accounting/cashbooks/[id].vue index d15415d..a6fb4c6 100644 --- a/frontend/pages/accounting/cashbooks/[id].vue +++ b/frontend/pages/accounting/cashbooks/[id].vue @@ -46,9 +46,7 @@ const currentBalance = computed(() => { }) const getIncomingInvoiceGross = (invoice) => { - return Number((invoice.accounts || []).reduce((sum, account) => { - return sum + Number(account.amountNet || 0) + Number(account.amountTax || 0) - }, 0)) + return Number(useSum().getIncomingInvoiceSum(invoice)) } const getIncomingInvoiceOpenAmount = (invoice) => { diff --git a/frontend/pages/accounting/manual-bookings.vue b/frontend/pages/accounting/manual-bookings.vue index 42ded0e..c7f14f3 100644 --- a/frontend/pages/accounting/manual-bookings.vue +++ b/frontend/pages/accounting/manual-bookings.vue @@ -68,9 +68,7 @@ const buildEntries = (rows, type, labelBuilder) => })) const getIncomingInvoiceGross = (invoice) => { - return Number((invoice.accounts || []).reduce((sum, account) => { - return sum + Number(account.amountNet || 0) + Number(account.amountTax || 0) - }, 0)) + return Number(useSum().getIncomingInvoiceSum(invoice)) } const getIncomingInvoiceOpenAmount = (invoice) => { diff --git a/frontend/pages/banking/index.vue b/frontend/pages/banking/index.vue index 8ca1fd0..b4f79d7 100644 --- a/frontend/pages/banking/index.vue +++ b/frontend/pages/banking/index.vue @@ -230,13 +230,7 @@ const calculateOpenSum = (statement) => { } const getInvoiceSum = (invoice, onlyOpenSum) => { - let sum = 0 - if (invoice.accounts) { - invoice.accounts.forEach(account => { - sum += (account.amountTax || 0) - sum += (account.amountNet || 0) - }) - } + let sum = Number(useSum().getIncomingInvoiceSum(invoice)) if (onlyOpenSum) sum = sum + Number(invoice.statementallocations.reduce((n, {amount}) => n + amount, 0)) diff --git a/frontend/pages/banking/statements/[mode]/[[id]].vue b/frontend/pages/banking/statements/[mode]/[[id]].vue index a65c8c8..8dd006c 100644 --- a/frontend/pages/banking/statements/[mode]/[[id]].vue +++ b/frontend/pages/banking/statements/[mode]/[[id]].vue @@ -130,13 +130,7 @@ const separateIBAN = (input) => { } const getInvoiceSum = (invoice, onlyOpenSum) => { - let sum = 0 - if (invoice.accounts) { - invoice.accounts.forEach(account => { - sum += (account.amountTax || 0) - sum += (account.amountNet || 0) - }) - } + let sum = Number(useSum().getIncomingInvoiceSum(invoice)) if (onlyOpenSum) sum = sum + Number(invoice.statementallocations.reduce((n, {amount}) => n + amount, 0)) diff --git a/frontend/pages/incomingInvoices/[mode]/[id].vue b/frontend/pages/incomingInvoices/[mode]/[id].vue index 1efd87d..ae05fb0 100644 --- a/frontend/pages/incomingInvoices/[mode]/[id].vue +++ b/frontend/pages/incomingInvoices/[mode]/[id].vue @@ -103,6 +103,8 @@ watch(() => itemInfo.value.date, (value) => { // --- Berechnungslogik --- const useNetMode = ref(false) +const editingTotalTax = ref(false) +const totalTaxDraft = ref(null) const taxOptions = ref([ { label: "19% USt", percentage: 19, key: "19" }, @@ -186,24 +188,17 @@ const totalCalculated = computed(() => { } }) - totalGross = Number(totalNet + totalAmount19Tax + totalAmount7Tax) + const calculatedTax = Number(totalAmount19Tax + totalAmount7Tax) + const hasTaxOverride = itemInfo.value.taxAmountOverride !== null && itemInfo.value.taxAmountOverride !== undefined && itemInfo.value.taxAmountOverride !== "" + const totalTax = hasTaxOverride ? Number(itemInfo.value.taxAmountOverride) : calculatedTax + totalGross = Number(totalNet + totalTax) - return { totalNet, totalAmount19Tax, totalAmount7Tax, totalGross } + return { totalNet, totalAmount19Tax, totalAmount7Tax, calculatedTax, totalTax, totalGross, hasTaxOverride } }) const hasAmount = (value) => value !== null && value !== undefined && value !== "" const hasValidNumber = (value) => hasAmount(value) && Number.isFinite(Number(value)) const isDepreciationItem = (item) => isDepreciationBookingMode(item?.bookingMode) -const getCalculatedTax = (item) => { - const taxRate = Number(taxOptions.value.find((tax) => tax.key === item.taxType)?.percentage || 0) - - return Number((Number(item.amountNet || 0) * (taxRate / 100)).toFixed(2)) -} -const hasManualTaxDifference = (item) => { - if (!hasValidNumber(item.amountTax) || !hasValidNumber(item.amountNet)) return false - - return Math.abs(Number(item.amountTax) - getCalculatedTax(item)) >= 0.005 -} const updateBookingMode = (item) => { ensureDepreciationDefaults(item, itemInfo.value.date) @@ -226,14 +221,6 @@ const recalculateItem = (item, source) => { calculateFromNet() } else if (source === 'gross') { calculateFromGross() - } else if (source === 'tax') { - if(!hasValidNumber(item.amountTax)) return - - if((useNetMode.value || !hasAmount(item.amountGross)) && hasAmount(item.amountNet)) { - item.amountGross = Number((Number(item.amountNet) + Number(item.amountTax)).toFixed(2)) - } else if(hasAmount(item.amountGross)) { - item.amountNet = Number((Number(item.amountGross) - Number(item.amountTax)).toFixed(2)) - } } else if (source === 'taxType' || source === 'manual') { if(hasAmount(item.amountNet)) calculateFromNet() else if(hasAmount(item.amountGross)) calculateFromGross() @@ -246,6 +233,19 @@ const moveGrossToNet = (item) => { recalculateItem(item, 'net') } +const startEditingTotalTax = () => { + totalTaxDraft.value = totalCalculated.value.totalTax.toFixed(2) + editingTotalTax.value = true +} + +const applyTotalTaxOverride = () => { + if (!hasValidNumber(totalTaxDraft.value)) return + + const value = Number(Number(totalTaxDraft.value).toFixed(2)) + itemInfo.value.taxAmountOverride = Math.abs(value - totalCalculated.value.calculatedTax) >= 0.005 ? value : null + editingTotalTax.value = false +} + // --- Saving --- const updateIncomingInvoice = async (setBooked = false) => { if (setBooked && hasBlockingIncomingInvoiceErrors.value) { @@ -889,30 +889,13 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE
- - + +
-
- -
-
blockingIncomingInvoiceE Netto Gesamt {{ totalCalculated.totalNet.toFixed(2) }} €
-
- + 7% USt - {{ totalCalculated.totalAmount7Tax.toFixed(2) }} € -
-
- + 19% USt - {{ totalCalculated.totalAmount19Tax.toFixed(2) }} € +
+ + USt gesamt + Manuell + +
+ + + + + +
+
+ {{ totalCalculated.totalTax.toFixed(2) }} € + +
+

+ DATEV berechnet die USt aus dem Bruttobetrag erneut. Der Export wird mit „USt prüfen“ markiert. +

Rechnungsbetrag {{ totalCalculated.totalGross.toFixed(2) }} € diff --git a/frontend/pages/incomingInvoices/index.vue b/frontend/pages/incomingInvoices/index.vue index 72872be..939fd48 100644 --- a/frontend/pages/incomingInvoices/index.vue +++ b/frontend/pages/incomingInvoices/index.vue @@ -141,12 +141,7 @@ const displayCurrency = (value, currency = "€") => { return `${Number(value).toFixed(2).replace(".",",")} ${currency}` } const getInvoiceSum = (invoice) => { - let sum = 0 - invoice.accounts.forEach(account => { - sum += account.amountTax - sum += account.amountNet - }) - return sum.toFixed(2) + return useSum().getIncomingInvoiceSum(invoice) } const getPaidAmount = (item) => { From 7ad55aaa0bf51a529df3a1977d80e6d8b7d01977 Mon Sep 17 00:00:00 2001 From: flfeders Date: Sun, 9 Aug 2026 16:38:43 +0200 Subject: [PATCH 08/14] Include manual tax accounts in VAT evaluation --- .../0063_skr03_output_tax_account.sql | 20 +++++++++++++ backend/db/migrations/meta/_journal.json | 7 +++++ frontend/components/displayTaxSummary.vue | 28 ++++++++++++++--- frontend/composables/useTaxEvaluation.ts | 30 +++++++++++++++++++ frontend/pages/accounting/tax.vue | 26 ++++++++++++++-- 5 files changed, 104 insertions(+), 7 deletions(-) create mode 100644 backend/db/migrations/0063_skr03_output_tax_account.sql diff --git a/backend/db/migrations/0063_skr03_output_tax_account.sql b/backend/db/migrations/0063_skr03_output_tax_account.sql new file mode 100644 index 0000000..4515a2d --- /dev/null +++ b/backend/db/migrations/0063_skr03_output_tax_account.sql @@ -0,0 +1,20 @@ +UPDATE "accounts" +SET "label" = 'Umsatzsteuer 19 %', + "description" = NULL +WHERE "accountChart" = 'skr03' + AND "number" = '1776'; +--> statement-breakpoint +INSERT INTO "accounts" ("number", "label", "description", "accountChart") +SELECT '1776', 'Umsatzsteuer 19 %', NULL, 'skr03' +WHERE NOT EXISTS ( + SELECT 1 + FROM "accounts" + WHERE "accountChart" = 'skr03' + AND "number" = '1776' +); +--> statement-breakpoint +SELECT setval( + pg_get_serial_sequence('accounts', 'id'), + GREATEST(COALESCE((SELECT MAX("id") FROM "accounts"), 1), 1), + true +); diff --git a/backend/db/migrations/meta/_journal.json b/backend/db/migrations/meta/_journal.json index 596952c..c707a25 100644 --- a/backend/db/migrations/meta/_journal.json +++ b/backend/db/migrations/meta/_journal.json @@ -421,6 +421,13 @@ "when": 1786284000000, "tag": "0062_incoming_invoice_tax_override", "breakpoints": true + }, + { + "idx": 60, + "version": "7", + "when": 1786287600000, + "tag": "0063_skr03_output_tax_account", + "breakpoints": true } ] } diff --git a/frontend/components/displayTaxSummary.vue b/frontend/components/displayTaxSummary.vue index db99e5b..add4274 100644 --- a/frontend/components/displayTaxSummary.vue +++ b/frontend/components/displayTaxSummary.vue @@ -6,6 +6,7 @@ import { formatTaxEvaluationPeriodRange, getCreatedDocumentTaxBreakdown, getIncomingInvoiceTaxBreakdown, + getManualBookingTaxBreakdown, getTaxEvaluationPeriodBounds, normalizeTaxEvaluationPeriod } from "~/composables/useTaxEvaluation" @@ -40,7 +41,10 @@ const loadSummary = async () => { const periodType = normalizeTaxEvaluationPeriod(auth.activeTenantData?.taxEvaluationPeriod) const bounds = getTaxEvaluationPeriodBounds(dayjs(), periodType) - const { createdDocuments: docs, incomingInvoices: incoming } = await loadCoreData() + const [{ createdDocuments: docs, incomingInvoices: incoming }, manualBookings] = await Promise.all([ + loadCoreData(), + useNuxtApp().$api("/api/banking/manual-bookings") as Promise + ]) const outputDocs = (docs || []).filter((doc: any) => { if (doc?.state !== "Gebucht") return false @@ -67,12 +71,28 @@ const loadSummary = async () => { return sum + breakdown.tax19 + breakdown.tax7 }, 0) + const manualTax = (manualBookings || []) + .filter((booking: any) => { + const date = dayjs(booking.manualBookingDate) + return date.isValid() && !date.isBefore(bounds.start, "day") && !date.isAfter(bounds.end, "day") + }) + .reduce((sum: { outputTax19: number; inputTax19: number }, booking: any) => { + const breakdown = getManualBookingTaxBreakdown(booking) + return { + outputTax19: sum.outputTax19 + breakdown.outputTax19, + inputTax19: sum.inputTax19 + breakdown.inputTax19, + } + }, { outputTax19: 0, inputTax19: 0 }) + + const totalOutputTax = outputTax + manualTax.outputTax19 + const totalInputTax = inputTax + manualTax.inputTax19 + summary.value = { label: formatTaxEvaluationPeriodLabel(bounds.start, periodType), range: formatTaxEvaluationPeriodRange(bounds.start, periodType), - outputTax: Number(outputTax.toFixed(2)), - inputTax: Number(inputTax.toFixed(2)), - balance: Number((outputTax - inputTax).toFixed(2)), + outputTax: Number(totalOutputTax.toFixed(2)), + inputTax: Number(totalInputTax.toFixed(2)), + balance: Number((totalOutputTax - totalInputTax).toFixed(2)), outputCount: outputDocs.length, inputCount: inputDocs.length, } diff --git a/frontend/composables/useTaxEvaluation.ts b/frontend/composables/useTaxEvaluation.ts index c141804..d5d26a7 100644 --- a/frontend/composables/useTaxEvaluation.ts +++ b/frontend/composables/useTaxEvaluation.ts @@ -166,3 +166,33 @@ export const getIncomingInvoiceTaxBreakdown = (invoice: any) => { net0: Number(breakdown.net0.toFixed(2)), } } + +const getSkr03AccountNumber = (account: any) => { + if (!account || String(account.accountChart || "").toLowerCase() !== "skr03") return null + return String(account.number || "") +} + +export const getManualBookingTaxBreakdown = (booking: any) => { + const amount = Number(booking?.amount || 0) + const breakdown = { outputTax19: 0, inputTax19: 0 } + + if (!Number.isFinite(amount) || amount === 0) return breakdown + + const applySide = (account: any, side: "debit" | "credit") => { + const accountNumber = getSkr03AccountNumber(account) + + if (accountNumber === "1576") { + breakdown.inputTax19 += side === "debit" ? amount : -amount + } else if (accountNumber === "1776") { + breakdown.outputTax19 += side === "credit" ? amount : -amount + } + } + + applySide(booking.account, "debit") + applySide(booking.contraAccount, "credit") + + return { + outputTax19: Number(breakdown.outputTax19.toFixed(2)), + inputTax19: Number(breakdown.inputTax19.toFixed(2)), + } +} diff --git a/frontend/pages/accounting/tax.vue b/frontend/pages/accounting/tax.vue index 6aa9713..47f06a7 100644 --- a/frontend/pages/accounting/tax.vue +++ b/frontend/pages/accounting/tax.vue @@ -6,6 +6,7 @@ import { formatTaxEvaluationPeriodRange, getCreatedDocumentTaxBreakdown, getIncomingInvoiceTaxBreakdown, + getManualBookingTaxBreakdown, getTaxEvaluationPeriodBounds, normalizeTaxEvaluationPeriod, shiftTaxEvaluationPeriodStart @@ -18,6 +19,7 @@ const auth = useAuthStore() const loading = ref(true) const createdDocuments = ref([]) const incomingInvoices = ref([]) +const manualBookings = ref([]) const periodType = computed(() => normalizeTaxEvaluationPeriod(auth.activeTenantData?.taxEvaluationPeriod)) @@ -40,13 +42,15 @@ const loadData = async () => { loading.value = true try { - const [docs, incoming] = await Promise.all([ + const [docs, incoming, manual] = await Promise.all([ useEntities("createddocuments").select(), - useEntities("incominginvoices").select() + useEntities("incominginvoices").select(), + useNuxtApp().$api("/api/banking/manual-bookings") ]) createdDocuments.value = (docs || []).filter(isRelevantOutputDocument) incomingInvoices.value = (incoming || []).filter(isRelevantInputInvoice) + manualBookings.value = (manual as any[]) || [] } finally { loading.value = false } @@ -69,6 +73,19 @@ const periods = computed(() => { return date.isValid() && !date.isBefore(bounds.start, "day") && !date.isAfter(bounds.end, "day") }) + const manualTax = manualBookings.value + .filter((booking) => { + const date = dayjs(booking.manualBookingDate) + return date.isValid() && !date.isBefore(bounds.start, "day") && !date.isAfter(bounds.end, "day") + }) + .reduce((sum, booking) => { + const breakdown = getManualBookingTaxBreakdown(booking) + return { + outputTax19: sum.outputTax19 + breakdown.outputTax19, + inputTax19: sum.inputTax19 + breakdown.inputTax19, + } + }, { outputTax19: 0, inputTax19: 0 }) + const output = outputDocs.reduce((sum, doc) => { const breakdown = getCreatedDocumentTaxBreakdown(doc) return { @@ -91,6 +108,9 @@ const periods = computed(() => { } }, { net19: 0, tax19: 0, net7: 0, tax7: 0, net0: 0 }) + output.tax19 = Number((output.tax19 + manualTax.outputTax19).toFixed(2)) + input.tax19 = Number((input.tax19 + manualTax.inputTax19).toFixed(2)) + const outputTax = Number((output.tax19 + output.tax7).toFixed(2)) const inputTax = Number((input.tax19 + input.tax7).toFixed(2)) const balance = Number((outputTax - inputTax).toFixed(2)) @@ -146,7 +166,7 @@ onMounted(loadData)

Intervall: {{ periodType === "monthly" ? "monatlich" : periodType === "quarterly" ? "quartalsweise" : "jährlich" }}. - Berücksichtigt werden gebuchte Ausgangsrechnungen, Abschlags- und Stornorechnungen sowie gebuchte Eingangsbelege mit Datum. + Berücksichtigt werden gebuchte Ausgangsrechnungen, Abschlags- und Stornorechnungen, gebuchte Eingangsbelege sowie manuelle Buchungen auf SKR03 1576 und 1776.

{{ currentPeriod.range }} From 7d3e9a383f7ba3b122d890e1e8bd8886b1f78cd7 Mon Sep 17 00:00:00 2001 From: flfeders Date: Sun, 9 Aug 2026 17:14:53 +0200 Subject: [PATCH 09/14] fix: use corrected invoice total for bank allocation --- backend/src/routes/banking.ts | 55 +++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/backend/src/routes/banking.ts b/backend/src/routes/banking.ts index 9fb60bb..ce25ef0 100644 --- a/backend/src/routes/banking.ts +++ b/backend/src/routes/banking.ts @@ -41,6 +41,60 @@ export default async function bankingRoutes(server: FastifyInstance) { const ManualInvoices = aliasedTable(incominginvoices, "manual_invoices") const ManualInvoiceVendors = aliasedTable(vendors, "manual_invoice_vendors") + const useCurrentIncomingInvoiceAmount = async (tenantId: number, allocation: any) => { + const invoiceId = Number(allocation.incominginvoice) + const statementId = Number(allocation.bankstatement) + if (!invoiceId || !statementId) return allocation + + const [[invoice], [statement], invoiceAllocations, statementAllocations] = await Promise.all([ + server.db.select().from(incominginvoices).where(and( + eq(incominginvoices.id, invoiceId), + eq(incominginvoices.tenant, tenantId) + )).limit(1), + server.db.select().from(bankstatements).where(and( + eq(bankstatements.id, statementId), + eq(bankstatements.tenant, tenantId) + )).limit(1), + server.db.select({ amount: statementallocations.amount }).from(statementallocations).where(and( + eq(statementallocations.incominginvoice, invoiceId), + eq(statementallocations.tenant, tenantId), + eq(statementallocations.archived, false) + )), + server.db.select({ amount: statementallocations.amount }).from(statementallocations).where(and( + eq(statementallocations.bankstatement, statementId), + eq(statementallocations.tenant, tenantId), + eq(statementallocations.archived, false) + )), + ]) + + if (!invoice || !statement) return allocation + + const accountTotals = (invoice.accounts as any[] || []).reduce((totals, account) => ({ + net: totals.net + Number(account.amountNet || 0), + tax: totals.tax + Number(account.amountTax || 0), + }), { net: 0, tax: 0 }) + const correctedTax = invoice.taxAmountOverride !== null && invoice.taxAmountOverride !== undefined && invoice.taxAmountOverride !== "" + ? Number(invoice.taxAmountOverride) + : accountTotals.tax + const legacyRemaining = Math.max(0, Math.abs(accountTotals.net + accountTotals.tax) + - invoiceAllocations.reduce((sum, item) => sum + Math.abs(Number(item.amount || 0)), 0)) + const correctedRemaining = Math.max(0, Math.abs(accountTotals.net + correctedTax) + - invoiceAllocations.reduce((sum, item) => sum + Math.abs(Number(item.amount || 0)), 0)) + const statementRemaining = Math.max(0, Math.abs(Number(statement.amount || 0)) + - statementAllocations.reduce((sum, item) => sum + Math.abs(Number(item.amount || 0)), 0)) + const requestedAmount = Number(allocation.amount || 0) + const legacyAssignment = Math.min(legacyRemaining, statementRemaining) + + // Nur den automatisch vorgeschlagenen Altbetrag ersetzen. Bewusst eingegebene Teilbeträge bleiben erhalten. + if (Math.abs(Math.abs(requestedAmount) - legacyAssignment) >= 0.005) return allocation + + const currentAssignment = Math.min(correctedRemaining, statementRemaining) + return { + ...allocation, + amount: Number((Math.sign(requestedAmount || (invoice.expense ? -1 : 1)) * currentAssignment).toFixed(2)), + } + } + const normalizeManualSide = (payload: any, keys: string[]) => keys.filter((key) => payload[key] !== null && payload[key] !== undefined && payload[key] !== "") @@ -1108,6 +1162,7 @@ export default async function bankingRoutes(server: FastifyInstance) { const { data: payload } = req.body as { data: any } const prepared = prepareStatementAllocationPayload(payload) if (prepared.error) return reply.code(400).send({ error: prepared.error }) + prepared.data = await useCurrentIncomingInvoiceAmount(req.user.tenant_id, prepared.data) const inserted = await server.db.insert(statementallocations).values({ ...prepared.data, From 17717e8a1b2a34721198b54a8e69862b63576b45 Mon Sep 17 00:00:00 2001 From: flfeders Date: Sun, 9 Aug 2026 17:38:17 +0200 Subject: [PATCH 10/14] Speed up initial file listing --- frontend/composables/useFiles.ts | 5 ++++- frontend/pages/files/index.vue | 33 +++++++++++++++++++++++--------- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/frontend/composables/useFiles.ts b/frontend/composables/useFiles.ts index edc4fc0..cd61936 100644 --- a/frontend/composables/useFiles.ts +++ b/frontend/composables/useFiles.ts @@ -77,7 +77,10 @@ export const useFiles = () => { const selectDocument = async (id) => { let documentIds = [id] if(documentIds.length === 0) return [] - const fileData = await useEntities("files").selectSingle(id) + const fileData = await useEntities("files").selectSingle( + id, + "*, incominginvoice(*), project(*), vendor(*), customer(*), contract(*), plant(*), createddocument(*), vehicle(*), product(*), profile(*), check(*), inventoryitem(*)" + ) const res = await useNuxtApp().$api("/api/files/presigned",{ method: "POST", body: { diff --git a/frontend/pages/files/index.vue b/frontend/pages/files/index.vue index ee60d5a..b17aeb3 100644 --- a/frontend/pages/files/index.vue +++ b/frontend/pages/files/index.vue @@ -63,20 +63,24 @@ watch(searchString, (val) => { const setupPage = async () => { loadingDocs.value = true try { - const [fRes, dRes, tRes] = await Promise.all([ + const [fRes, dRes] = await Promise.all([ useEntities("folders").select(), - files.selectDocuments(), - useEntities("filetags").select() + useEntities("files").select("id, path, folder, type, createdAt") ]) folders.value = fRes || [] documents.value = dRes || [] - filetags.value = tRes || [] syncCurrentFolderFromRoute() } finally { loadingDocs.value = false loaded.value = true } + + useEntities("filetags").select() + .then((items) => { + filetags.value = items || [] + }) + .catch((error) => console.error("Dateitypen konnten nicht geladen werden:", error)) } // --- Global Drag & Drop (Auto-Open Upload Modal) --- @@ -323,11 +327,22 @@ const updateName = async () => { } } -const showFile = (fileId) => { - modal.open(DocumentDisplayModal, { - documentData: documents.value.find(i => i.id === fileId), - onUpdatedNeeded: () => setupPage() - }) +const showFile = async (fileId) => { + loadingDocs.value = true + try { + const documentData = await files.selectDocument(fileId) + if (!documentData) throw new Error("Datei nicht gefunden") + + modal.open(DocumentDisplayModal, { + documentData, + onUpdatedNeeded: () => setupPage() + }) + } catch (error) { + console.error(error) + toast.add({title: 'Datei konnte nicht geöffnet werden', color: 'red'}) + } finally { + loadingDocs.value = false + } } const openScanModal = () => { From e12ec4557dad2455a6595edad2fff0998a0c30be Mon Sep 17 00:00:00 2001 From: flfeders Date: Sun, 9 Aug 2026 17:39:41 +0200 Subject: [PATCH 11/14] Reduce duplicate dashboard data loading --- frontend/components/displayBankaccounts.vue | 10 +++++++--- frontend/components/displayOpenBalances.vue | 6 ++++-- frontend/composables/useDashboardData.ts | 2 +- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/frontend/components/displayBankaccounts.vue b/frontend/components/displayBankaccounts.vue index 07978a7..5fe2512 100644 --- a/frontend/components/displayBankaccounts.vue +++ b/frontend/components/displayBankaccounts.vue @@ -6,9 +6,13 @@ let unallocatedStatements = ref(0) let bankaccounts = ref([]) const setupPage = async () => { - let bankstatements = (await useEntities("bankstatements").select("*, statementallocations(*)","date",true)).filter(i => !i.archived) + const [statementItems, accountItems] = await Promise.all([ + useEntities("bankstatements").select("*, statementallocations(*)", "date", true), + useEntities("bankaccounts").select() + ]) + let bankstatements = statementItems.filter(i => !i.archived) unallocatedStatements.value = bankstatements.filter(i => Number(calculateOpenSum(i)) !== 0).length - bankaccounts.value = await useEntities("bankaccounts").select() + bankaccounts.value = accountItems } setupPage() @@ -51,4 +55,4 @@ const calculateOpenSum = (statement) => { \ No newline at end of file + diff --git a/frontend/components/displayOpenBalances.vue b/frontend/components/displayOpenBalances.vue index b23082f..fe8e9c5 100644 --- a/frontend/components/displayOpenBalances.vue +++ b/frontend/components/displayOpenBalances.vue @@ -10,9 +10,11 @@ let draftInvoicesSum = ref(0) let draftInvoicesCount = ref(0) let countPreparedOpenIncomingInvoices = ref(0) +const { loadCoreData } = useDashboardData() const setupPage = async () => { - let items = (await useEntities("createddocuments").select("*, statementallocations(*), customer(id,name), linkedDocument(*)")).filter(i => !i.archived) + const { createdDocuments, incomingInvoices } = await loadCoreData() + let items = createdDocuments.filter(i => !i.archived) let documents = items.filter(i => i.type === "invoices" ||i.type === "advanceInvoices") let draftDocuments = documents.filter(i => i.state === "Entwurf") @@ -36,7 +38,7 @@ const setupPage = async () => { }) draftInvoicesCount.value = draftDocuments.length - countPreparedOpenIncomingInvoices.value = (await useEntities("incominginvoices").select("id, state")).filter(i => i.state === "Vorbereitet" && !i.archived).length + countPreparedOpenIncomingInvoices.value = incomingInvoices.filter(i => i.state === "Vorbereitet" && !i.archived).length } setupPage() diff --git a/frontend/composables/useDashboardData.ts b/frontend/composables/useDashboardData.ts index d92acf8..61c4a40 100644 --- a/frontend/composables/useDashboardData.ts +++ b/frontend/composables/useDashboardData.ts @@ -32,7 +32,7 @@ export const useDashboardData = () => { if (!force && cache.pendingRequest) return cache.pendingRequest cache.pendingRequest = Promise.all([ - useEntities("createddocuments").select(), + useEntities("createddocuments").select("*, statementallocations(*), customer(id,name), linkedDocument(*)"), useEntities("incominginvoices").select() ]) .then(([createdDocuments, incomingInvoices]) => { From e7fe98da563bac617dc471ccf6e68fc4e1236761 Mon Sep 17 00:00:00 2001 From: flfeders Date: Sun, 9 Aug 2026 17:42:44 +0200 Subject: [PATCH 12/14] Eingangsbelege ohne Bankzuweisung archivieren --- frontend/pages/incomingInvoices/[mode]/[id].vue | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/frontend/pages/incomingInvoices/[mode]/[id].vue b/frontend/pages/incomingInvoices/[mode]/[id].vue index ae05fb0..7e336b3 100644 --- a/frontend/pages/incomingInvoices/[mode]/[id].vue +++ b/frontend/pages/incomingInvoices/[mode]/[id].vue @@ -150,6 +150,17 @@ const bankBookingDateLabel = computed(() => { return bankBookingDates.value.map(formatDate).join(", ") }) +const hasActiveBankAssignment = computed(() => (itemInfo.value.statementallocations || []).some((allocation) => { + if (allocation?.archived) return false + + return Boolean(allocation?.bankstatement || allocation?.bs_id) +})) +const canArchiveIncomingInvoice = computed(() => { + if (itemInfo.value.archived) return false + if (mode.value !== "show") return true + + return itemInfo.value.state === "Gebucht" && !hasActiveBankAssignment.value +}) const vendorName = computed(() => vendors.value.find((vendor) => vendor.id === itemInfo.value.vendor)?.name || "-") const eInvoiceValidation = computed(() => itemInfo.value.eInvoiceValidation || null) const eInvoiceSourceLabel = computed(() => { @@ -322,7 +333,7 @@ const hasBlockingIncomingInvoiceErrors = computed(() => blockingIncomingInvoiceE