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
+ DATEV berechnet die USt aus dem Bruttobetrag erneut. Der Export wird mit „USt prüfen“ markiert. +