USt-Auswertung in Liquiditätsprognose integrieren
This commit is contained in:
@@ -12,10 +12,13 @@ import {
|
|||||||
createddocuments,
|
createddocuments,
|
||||||
incominginvoices,
|
incominginvoices,
|
||||||
statementallocations,
|
statementallocations,
|
||||||
|
tenants,
|
||||||
} from "../../db/schema";
|
} from "../../db/schema";
|
||||||
import { secrets } from "./secrets";
|
import { secrets } from "./secrets";
|
||||||
|
|
||||||
type ForecastEventSource = "open_createddocument" | "open_incominginvoice" | "recurring_bankstatement" | "draft_createddocument";
|
type ForecastEventSource = "open_createddocument" | "open_incominginvoice" | "recurring_bankstatement" | "draft_createddocument" | "tax_settlement";
|
||||||
|
|
||||||
|
type TaxEvaluationPeriod = "monthly" | "quarterly" | "yearly";
|
||||||
|
|
||||||
type ForecastEvent = {
|
type ForecastEvent = {
|
||||||
date: string;
|
date: string;
|
||||||
@@ -27,6 +30,26 @@ type ForecastEvent = {
|
|||||||
confidence?: number;
|
confidence?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type TaxBreakdown = {
|
||||||
|
net19: number;
|
||||||
|
tax19: number;
|
||||||
|
net7: number;
|
||||||
|
tax7: number;
|
||||||
|
net0: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TaxForecastPeriod = {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
range: string;
|
||||||
|
dueDate: string;
|
||||||
|
outputTax: number;
|
||||||
|
inputTax: number;
|
||||||
|
balance: number;
|
||||||
|
outputCount: number;
|
||||||
|
inputCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
type RecurringCandidate = {
|
type RecurringCandidate = {
|
||||||
key?: string;
|
key?: string;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -52,6 +75,13 @@ const FORECAST_DAYS = 90;
|
|||||||
const HISTORY_MONTHS = 12;
|
const HISTORY_MONTHS = 12;
|
||||||
|
|
||||||
const roundMoney = (value: number) => Number(Number(value || 0).toFixed(2));
|
const roundMoney = (value: number) => Number(Number(value || 0).toFixed(2));
|
||||||
|
const createZeroTaxBreakdown = (): TaxBreakdown => ({
|
||||||
|
net19: 0,
|
||||||
|
tax19: 0,
|
||||||
|
net7: 0,
|
||||||
|
tax7: 0,
|
||||||
|
net0: 0,
|
||||||
|
});
|
||||||
|
|
||||||
const normalizeText = (value: unknown) => String(value || "")
|
const normalizeText = (value: unknown) => String(value || "")
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
@@ -59,6 +89,85 @@ const normalizeText = (value: unknown) => String(value || "")
|
|||||||
.replace(/\s+/g, " ")
|
.replace(/\s+/g, " ")
|
||||||
.trim();
|
.trim();
|
||||||
|
|
||||||
|
const normalizeTaxEvaluationPeriod = (value?: string | null): TaxEvaluationPeriod => {
|
||||||
|
if (value === "quarterly" || value === "yearly") return value;
|
||||||
|
return "monthly";
|
||||||
|
};
|
||||||
|
|
||||||
|
const isTaxFreeDocument = (taxType?: string | null) => {
|
||||||
|
return ["13b UStG", "19 UStG", "12.3 UStG"].includes(String(taxType || ""));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTaxEvaluationPeriodBounds = (
|
||||||
|
referenceDate: dayjs.ConfigType,
|
||||||
|
period: TaxEvaluationPeriod
|
||||||
|
) => {
|
||||||
|
const base = dayjs(referenceDate);
|
||||||
|
|
||||||
|
if (period === "yearly") {
|
||||||
|
return {
|
||||||
|
start: base.startOf("year"),
|
||||||
|
end: base.endOf("year"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (period === "quarterly") {
|
||||||
|
const quarterStartMonth = Math.floor(base.month() / 3) * 3;
|
||||||
|
const start = base.month(quarterStartMonth).startOf("month");
|
||||||
|
|
||||||
|
return {
|
||||||
|
start,
|
||||||
|
end: start.add(2, "month").endOf("month"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
start: base.startOf("month"),
|
||||||
|
end: base.endOf("month"),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const shiftTaxEvaluationPeriodStart = (
|
||||||
|
periodStart: dayjs.ConfigType,
|
||||||
|
period: TaxEvaluationPeriod,
|
||||||
|
offset: number
|
||||||
|
) => {
|
||||||
|
const base = dayjs(periodStart);
|
||||||
|
|
||||||
|
if (period === "yearly") return base.add(offset, "year").startOf("year");
|
||||||
|
if (period === "quarterly") return base.add(offset * 3, "month").startOf("month");
|
||||||
|
return base.add(offset, "month").startOf("month");
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTaxEvaluationPeriodLabel = (
|
||||||
|
periodStart: dayjs.ConfigType,
|
||||||
|
period: TaxEvaluationPeriod
|
||||||
|
) => {
|
||||||
|
const { start } = getTaxEvaluationPeriodBounds(periodStart, period);
|
||||||
|
|
||||||
|
if (period === "yearly") {
|
||||||
|
return start.format("YYYY");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (period === "quarterly") {
|
||||||
|
return `Q${Math.floor(start.month() / 3) + 1} ${start.format("YYYY")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return start.format("MMMM YYYY");
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTaxEvaluationPeriodRange = (
|
||||||
|
periodStart: dayjs.ConfigType,
|
||||||
|
period: TaxEvaluationPeriod
|
||||||
|
) => {
|
||||||
|
const { start, end } = getTaxEvaluationPeriodBounds(periodStart, period);
|
||||||
|
return `${start.format("DD.MM.YYYY")} - ${end.format("DD.MM.YYYY")}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTaxSettlementDate = (periodEnd: dayjs.Dayjs) => {
|
||||||
|
return periodEnd.add(1, "month").date(10).startOf("day");
|
||||||
|
};
|
||||||
|
|
||||||
const getRecurringKey = (candidate: RecurringCandidate) => {
|
const getRecurringKey = (candidate: RecurringCandidate) => {
|
||||||
const rawKey = [
|
const rawKey = [
|
||||||
Math.sign(candidate.amount),
|
Math.sign(candidate.amount),
|
||||||
@@ -111,6 +220,72 @@ const getCreatedDocumentGrossAmount = (document: any, allDocuments: any[] = [])
|
|||||||
return roundMoney(Number(totalNet.toFixed(2)) + Number(totalTax.toFixed(2)) - advancePayments);
|
return roundMoney(Number(totalNet.toFixed(2)) + Number(totalTax.toFixed(2)) - advancePayments);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getCreatedDocumentTaxBreakdown = (document: any): TaxBreakdown => {
|
||||||
|
const breakdown = createZeroTaxBreakdown();
|
||||||
|
|
||||||
|
if (!document || isTaxFreeDocument(document.taxType)) {
|
||||||
|
return breakdown;
|
||||||
|
}
|
||||||
|
|
||||||
|
(document.rows || []).forEach((row: any) => {
|
||||||
|
if (!row || ["pagebreak", "title", "text"].includes(row.mode)) return;
|
||||||
|
|
||||||
|
const quantity = Number(row.quantity || 0);
|
||||||
|
const price = Number(row.price || 0);
|
||||||
|
const discountPercent = Number(row.discountPercent || 0);
|
||||||
|
const taxPercent = Number(row.taxPercent || 0);
|
||||||
|
const net = Number((quantity * price * (1 - discountPercent / 100)).toFixed(2));
|
||||||
|
|
||||||
|
if (!Number.isFinite(net) || net === 0) return;
|
||||||
|
|
||||||
|
if (taxPercent === 19) {
|
||||||
|
breakdown.net19 += net;
|
||||||
|
breakdown.tax19 += Number((net * 0.19).toFixed(2));
|
||||||
|
} else if (taxPercent === 7) {
|
||||||
|
breakdown.net7 += net;
|
||||||
|
breakdown.tax7 += Number((net * 0.07).toFixed(2));
|
||||||
|
} else {
|
||||||
|
breakdown.net0 += net;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
net19: roundMoney(breakdown.net19),
|
||||||
|
tax19: roundMoney(breakdown.tax19),
|
||||||
|
net7: roundMoney(breakdown.net7),
|
||||||
|
tax7: roundMoney(breakdown.tax7),
|
||||||
|
net0: roundMoney(breakdown.net0),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getIncomingInvoiceTaxBreakdown = (invoice: any): TaxBreakdown => {
|
||||||
|
const breakdown = createZeroTaxBreakdown();
|
||||||
|
|
||||||
|
(invoice?.accounts || []).forEach((account: any) => {
|
||||||
|
const taxType = String(account?.taxType || "");
|
||||||
|
const amountNet = Number(account?.amountNet || 0);
|
||||||
|
const amountTax = Number(account?.amountTax || 0);
|
||||||
|
|
||||||
|
if (taxType === "19") {
|
||||||
|
breakdown.net19 += amountNet;
|
||||||
|
breakdown.tax19 += amountTax;
|
||||||
|
} else if (taxType === "7") {
|
||||||
|
breakdown.net7 += amountNet;
|
||||||
|
breakdown.tax7 += amountTax;
|
||||||
|
} else {
|
||||||
|
breakdown.net0 += amountNet;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
net19: roundMoney(breakdown.net19),
|
||||||
|
tax19: roundMoney(breakdown.tax19),
|
||||||
|
net7: roundMoney(breakdown.net7),
|
||||||
|
tax7: roundMoney(breakdown.tax7),
|
||||||
|
net0: roundMoney(breakdown.net0),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const getIncomingInvoiceSignedAmount = (invoice: any) => {
|
const getIncomingInvoiceSignedAmount = (invoice: any) => {
|
||||||
const amount = (invoice.accounts || []).reduce((sum: number, account: any) => {
|
const amount = (invoice.accounts || []).reduce((sum: number, account: any) => {
|
||||||
return sum + Number(account.amountNet || 0) + Number(account.amountTax || 0);
|
return sum + Number(account.amountNet || 0) + Number(account.amountTax || 0);
|
||||||
@@ -300,6 +475,69 @@ const expandRecurringEvents = (recurring: RecurringCandidate[], endDate: dayjs.D
|
|||||||
return events;
|
return events;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const buildTaxForecastPeriods = (
|
||||||
|
documents: any[],
|
||||||
|
incomingInvoices: any[],
|
||||||
|
periodType: TaxEvaluationPeriod,
|
||||||
|
today: dayjs.Dayjs,
|
||||||
|
endDate: dayjs.Dayjs
|
||||||
|
) => {
|
||||||
|
const currentBounds = getTaxEvaluationPeriodBounds(today, periodType);
|
||||||
|
const periods: TaxForecastPeriod[] = [];
|
||||||
|
let offset = 0;
|
||||||
|
|
||||||
|
while (offset < 12) {
|
||||||
|
const periodStart = shiftTaxEvaluationPeriodStart(currentBounds.start, periodType, offset);
|
||||||
|
const bounds = getTaxEvaluationPeriodBounds(periodStart, periodType);
|
||||||
|
const dueDate = getTaxSettlementDate(bounds.end);
|
||||||
|
|
||||||
|
if (dueDate.isAfter(endDate, "day")) break;
|
||||||
|
|
||||||
|
const outputDocs = documents.filter((document) => {
|
||||||
|
if (document?.state !== "Gebucht") return false;
|
||||||
|
if (!["invoices", "advanceInvoices", "cancellationInvoices"].includes(document?.type)) return false;
|
||||||
|
|
||||||
|
const date = dayjs(document.documentDate);
|
||||||
|
return date.isValid() && !date.isBefore(bounds.start, "day") && !date.isAfter(bounds.end, "day");
|
||||||
|
});
|
||||||
|
|
||||||
|
const inputDocs = incomingInvoices.filter((invoice) => {
|
||||||
|
if (invoice?.state !== "Gebucht" || !invoice?.date) return false;
|
||||||
|
|
||||||
|
const date = dayjs(invoice.date);
|
||||||
|
return date.isValid() && !date.isBefore(bounds.start, "day") && !date.isAfter(bounds.end, "day");
|
||||||
|
});
|
||||||
|
|
||||||
|
const outputTax = roundMoney(outputDocs.reduce((sum, document) => {
|
||||||
|
const breakdown = getCreatedDocumentTaxBreakdown(document);
|
||||||
|
return sum + breakdown.tax19 + breakdown.tax7;
|
||||||
|
}, 0));
|
||||||
|
|
||||||
|
const inputTax = roundMoney(inputDocs.reduce((sum, invoice) => {
|
||||||
|
const breakdown = getIncomingInvoiceTaxBreakdown(invoice);
|
||||||
|
return sum + breakdown.tax19 + breakdown.tax7;
|
||||||
|
}, 0));
|
||||||
|
|
||||||
|
const balance = roundMoney(outputTax - inputTax);
|
||||||
|
|
||||||
|
periods.push({
|
||||||
|
key: bounds.start.format("YYYY-MM-DD"),
|
||||||
|
label: formatTaxEvaluationPeriodLabel(bounds.start, periodType),
|
||||||
|
range: formatTaxEvaluationPeriodRange(bounds.start, periodType),
|
||||||
|
dueDate: dueDate.format("YYYY-MM-DD"),
|
||||||
|
outputTax,
|
||||||
|
inputTax,
|
||||||
|
balance,
|
||||||
|
outputCount: outputDocs.length,
|
||||||
|
inputCount: inputDocs.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
offset += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return periods;
|
||||||
|
};
|
||||||
|
|
||||||
export const generateLiquidityForecast = async (
|
export const generateLiquidityForecast = async (
|
||||||
server: FastifyInstance,
|
server: FastifyInstance,
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
@@ -309,7 +547,7 @@ export const generateLiquidityForecast = async (
|
|||||||
const endDate = today.add(FORECAST_DAYS, "day");
|
const endDate = today.add(FORECAST_DAYS, "day");
|
||||||
const historyStart = today.subtract(HISTORY_MONTHS, "month").format("YYYY-MM-DD");
|
const historyStart = today.subtract(HISTORY_MONTHS, "month").format("YYYY-MM-DD");
|
||||||
|
|
||||||
const [accounts, statements, documents, incomingInvoices, allocations] = await Promise.all([
|
const [accounts, statements, documents, incomingInvoices, allocations, tenantSettings] = await Promise.all([
|
||||||
server.db
|
server.db
|
||||||
.select()
|
.select()
|
||||||
.from(bankaccounts)
|
.from(bankaccounts)
|
||||||
@@ -331,6 +569,13 @@ export const generateLiquidityForecast = async (
|
|||||||
.select()
|
.select()
|
||||||
.from(statementallocations)
|
.from(statementallocations)
|
||||||
.where(and(eq(statementallocations.tenant, tenantId), eq(statementallocations.archived, false))),
|
.where(and(eq(statementallocations.tenant, tenantId), eq(statementallocations.archived, false))),
|
||||||
|
server.db
|
||||||
|
.select({
|
||||||
|
taxEvaluationPeriod: tenants.taxEvaluationPeriod,
|
||||||
|
})
|
||||||
|
.from(tenants)
|
||||||
|
.where(eq(tenants.id, tenantId))
|
||||||
|
.limit(1),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const activeAccounts = accounts.filter((account) => !account.archived);
|
const activeAccounts = accounts.filter((account) => !account.archived);
|
||||||
@@ -339,6 +584,7 @@ export const generateLiquidityForecast = async (
|
|||||||
const activeIncomingInvoices = incomingInvoices.filter((invoice) => !invoice.archived);
|
const activeIncomingInvoices = incomingInvoices.filter((invoice) => !invoice.archived);
|
||||||
const activeDocumentIds = new Set(activeDocuments.map((document) => document.id));
|
const activeDocumentIds = new Set(activeDocuments.map((document) => document.id));
|
||||||
const activeIncomingInvoiceIds = new Set(activeIncomingInvoices.map((invoice) => invoice.id));
|
const activeIncomingInvoiceIds = new Set(activeIncomingInvoices.map((invoice) => invoice.id));
|
||||||
|
const taxPeriodType = normalizeTaxEvaluationPeriod(tenantSettings[0]?.taxEvaluationPeriod);
|
||||||
const activeAllocations = allocations.filter((allocation) => {
|
const activeAllocations = allocations.filter((allocation) => {
|
||||||
if (allocation.archived) return false;
|
if (allocation.archived) return false;
|
||||||
if (allocation.createddocument && !activeDocumentIds.has(allocation.createddocument)) return false;
|
if (allocation.createddocument && !activeDocumentIds.has(allocation.createddocument)) return false;
|
||||||
@@ -444,8 +690,20 @@ export const generateLiquidityForecast = async (
|
|||||||
const ignoredRecurringKeySet = new Set(ignoredRecurringKeys.filter(Boolean));
|
const ignoredRecurringKeySet = new Set(ignoredRecurringKeys.filter(Boolean));
|
||||||
const recurring = mergeRecurringCandidates(heuristicRecurring, aiRecurring)
|
const recurring = mergeRecurringCandidates(heuristicRecurring, aiRecurring)
|
||||||
.filter((candidate) => !candidate.key || !ignoredRecurringKeySet.has(candidate.key));
|
.filter((candidate) => !candidate.key || !ignoredRecurringKeySet.has(candidate.key));
|
||||||
|
const taxPeriods = buildTaxForecastPeriods(activeDocuments, activeIncomingInvoices, taxPeriodType, today, endDate);
|
||||||
|
const taxEvents: ForecastEvent[] = taxPeriods
|
||||||
|
.filter((period) => Math.abs(period.balance) > 0.01)
|
||||||
|
.map((period) => ({
|
||||||
|
date: period.dueDate,
|
||||||
|
amount: roundMoney(period.balance * -1),
|
||||||
|
label: `USt ${period.label}`,
|
||||||
|
source: "tax_settlement" as const,
|
||||||
|
sourceId: period.key,
|
||||||
|
confidence: 0.95,
|
||||||
|
}));
|
||||||
const events = [
|
const events = [
|
||||||
...openEvents,
|
...openEvents,
|
||||||
|
...taxEvents,
|
||||||
...expandRecurringEvents(recurring, endDate),
|
...expandRecurringEvents(recurring, endDate),
|
||||||
].filter((event) => {
|
].filter((event) => {
|
||||||
const date = dayjs(event.date);
|
const date = dayjs(event.date);
|
||||||
@@ -504,6 +762,11 @@ export const generateLiquidityForecast = async (
|
|||||||
events: events.sort((a, b) => a.date.localeCompare(b.date)),
|
events: events.sort((a, b) => a.date.localeCompare(b.date)),
|
||||||
draftEvents: draftEvents.sort((a, b) => a.date.localeCompare(b.date)),
|
draftEvents: draftEvents.sort((a, b) => a.date.localeCompare(b.date)),
|
||||||
draftIncome,
|
draftIncome,
|
||||||
|
tax: {
|
||||||
|
periodType: taxPeriodType,
|
||||||
|
periods: taxPeriods,
|
||||||
|
totalBalance: roundMoney(taxPeriods.reduce((sum, period) => sum + period.balance, 0)),
|
||||||
|
},
|
||||||
points,
|
points,
|
||||||
ai: {
|
ai: {
|
||||||
enabled: Boolean(secrets.OPENAI_API_KEY),
|
enabled: Boolean(secrets.OPENAI_API_KEY),
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const loading = ref(false)
|
|||||||
const error = ref("")
|
const error = ref("")
|
||||||
const cacheLoaded = ref(false)
|
const cacheLoaded = ref(false)
|
||||||
|
|
||||||
const CACHE_KEY = "fedeo:liquidityForecast:v1"
|
const CACHE_KEY = "fedeo:liquidityForecast:v2"
|
||||||
|
|
||||||
const dismissedRecurringKeys = computed(() => {
|
const dismissedRecurringKeys = computed(() => {
|
||||||
return tempStore.settings?.liquidityForecast?.dismissedRecurringKeys || []
|
return tempStore.settings?.liquidityForecast?.dismissedRecurringKeys || []
|
||||||
@@ -28,7 +28,8 @@ const sourceLabels = {
|
|||||||
open_createddocument: "Offene Ausgangsrechnung",
|
open_createddocument: "Offene Ausgangsrechnung",
|
||||||
open_incominginvoice: "Offener Eingangsbeleg",
|
open_incominginvoice: "Offener Eingangsbeleg",
|
||||||
recurring_bankstatement: "Regelmäßige Bankbewegung",
|
recurring_bankstatement: "Regelmäßige Bankbewegung",
|
||||||
draft_createddocument: "Rechnungsentwurf"
|
draft_createddocument: "Rechnungsentwurf",
|
||||||
|
tax_settlement: "USt-Zahlung"
|
||||||
}
|
}
|
||||||
|
|
||||||
const intervalLabels = {
|
const intervalLabels = {
|
||||||
@@ -156,6 +157,7 @@ const formatDate = (value) => dayjs(value).format("DD.MM.YYYY")
|
|||||||
const getEventRoute = (event) => {
|
const getEventRoute = (event) => {
|
||||||
if (event.source === "open_createddocument" && event.sourceId) return `/createDocument/show/${event.sourceId}`
|
if (event.source === "open_createddocument" && event.sourceId) return `/createDocument/show/${event.sourceId}`
|
||||||
if (event.source === "open_incominginvoice" && event.sourceId) return `/incomingInvoices/show/${event.sourceId}`
|
if (event.source === "open_incominginvoice" && event.sourceId) return `/incomingInvoices/show/${event.sourceId}`
|
||||||
|
if (event.source === "tax_settlement") return "/accounting/tax"
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,7 +247,8 @@ const groupedEventSummary = computed(() => {
|
|||||||
const summary = {
|
const summary = {
|
||||||
open_createddocument: { label: sourceLabels.open_createddocument, amount: 0, count: 0 },
|
open_createddocument: { label: sourceLabels.open_createddocument, amount: 0, count: 0 },
|
||||||
open_incominginvoice: { label: sourceLabels.open_incominginvoice, amount: 0, count: 0 },
|
open_incominginvoice: { label: sourceLabels.open_incominginvoice, amount: 0, count: 0 },
|
||||||
recurring_bankstatement: { label: sourceLabels.recurring_bankstatement, amount: 0, count: 0 }
|
recurring_bankstatement: { label: sourceLabels.recurring_bankstatement, amount: 0, count: 0 },
|
||||||
|
tax_settlement: { label: sourceLabels.tax_settlement, amount: 0, count: 0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
;(forecast.value?.events || []).forEach((event) => {
|
;(forecast.value?.events || []).forEach((event) => {
|
||||||
@@ -277,6 +280,24 @@ const draftIncomeDrivers = computed(() => {
|
|||||||
.slice(0, 12)
|
.slice(0, 12)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const taxPeriodTypeLabel = computed(() => {
|
||||||
|
if (forecast.value?.tax?.periodType === "quarterly") return "quartalsweise"
|
||||||
|
if (forecast.value?.tax?.periodType === "yearly") return "jährlich"
|
||||||
|
return "monatlich"
|
||||||
|
})
|
||||||
|
|
||||||
|
const taxForecastPeriods = computed(() => {
|
||||||
|
return [...(forecast.value?.tax?.periods || [])]
|
||||||
|
.filter((period) => Math.abs(Number(period.balance || 0)) > 0.01)
|
||||||
|
.slice(0, 6)
|
||||||
|
})
|
||||||
|
|
||||||
|
const taxEventDrivers = computed(() => {
|
||||||
|
return [...(forecast.value?.events || [])]
|
||||||
|
.filter((event) => event.source === "tax_settlement")
|
||||||
|
.sort((a, b) => a.date.localeCompare(b.date))
|
||||||
|
})
|
||||||
|
|
||||||
const detailedDays = computed(() => {
|
const detailedDays = computed(() => {
|
||||||
let previousBalance = Number(forecast.value?.startingBalance || 0)
|
let previousBalance = Number(forecast.value?.startingBalance || 0)
|
||||||
|
|
||||||
@@ -478,6 +499,79 @@ onMounted(() => {
|
|||||||
</UCard>
|
</UCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<UCard v-if="taxForecastPeriods.length">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h2 class="font-semibold">USt in der Prognose</h2>
|
||||||
|
<p class="text-sm text-gray-500">Aus der USt-Auswertung mit {{ taxPeriodTypeLabel }} Fälligkeit abgeleitet</p>
|
||||||
|
</div>
|
||||||
|
<UButton
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
icon="i-heroicons-arrow-top-right-on-square"
|
||||||
|
@click="navigateTo('/accounting/tax')"
|
||||||
|
>
|
||||||
|
USt-Auswertung öffnen
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="mb-4 rounded-lg border border-dashed border-gray-300 p-4 dark:border-gray-700">
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm text-gray-500">Im Prognosezeitraum berücksichtigte USt-Salden</p>
|
||||||
|
<p class="text-xs text-gray-500">Positive Salden werden als Auszahlung an das Finanzamt eingeplant, negative als Erstattung.</p>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="text-lg font-semibold"
|
||||||
|
:class="Number(forecast.tax?.totalBalance || 0) > 0 ? 'text-rose-600' : 'text-primary-600'"
|
||||||
|
>
|
||||||
|
{{ useCurrency(Number(forecast.tax?.totalBalance || 0) * -1) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div
|
||||||
|
v-for="period in taxForecastPeriods"
|
||||||
|
:key="period.key"
|
||||||
|
class="grid gap-3 rounded-lg border border-gray-200 p-4 lg:grid-cols-[minmax(0,1.1fr)_minmax(0,1fr)_auto] dark:border-gray-800"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p class="font-medium">{{ period.label }}</p>
|
||||||
|
<p class="text-xs text-gray-500">{{ period.range }} · Fällig am {{ formatDate(period.dueDate) }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-1 text-sm text-gray-500">
|
||||||
|
<div class="flex items-center justify-between gap-3">
|
||||||
|
<span>USt Rechnungen</span>
|
||||||
|
<span>{{ useCurrency(period.outputTax) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between gap-3">
|
||||||
|
<span>Vorsteuer</span>
|
||||||
|
<span>{{ useCurrency(period.inputTax) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between gap-3 font-medium">
|
||||||
|
<span>Saldo</span>
|
||||||
|
<span :class="period.balance > 0 ? 'text-rose-600' : 'text-primary-600'">
|
||||||
|
{{ useCurrency(period.balance) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-right">
|
||||||
|
<p class="text-xs text-gray-500">{{ period.outputCount }} Ausgangsbelege · {{ period.inputCount }} Eingangsbelege</p>
|
||||||
|
<p
|
||||||
|
class="mt-2 text-lg font-semibold"
|
||||||
|
:class="period.balance > 0 ? 'text-rose-600' : 'text-primary-600'"
|
||||||
|
>
|
||||||
|
{{ useCurrency(period.balance * -1) }}
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-gray-500">Liquiditätseffekt</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
|
||||||
<UCard v-if="draftIncomeDrivers.length">
|
<UCard v-if="draftIncomeDrivers.length">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div>
|
<div>
|
||||||
@@ -634,6 +728,43 @@ onMounted(() => {
|
|||||||
</UCard>
|
</UCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<UCard v-if="taxEventDrivers.length">
|
||||||
|
<template #header>
|
||||||
|
<div>
|
||||||
|
<h2 class="font-semibold">Geplante USt-Zahlungen und Erstattungen</h2>
|
||||||
|
<p class="text-sm text-gray-500">Direkt aus den berücksichtigten USt-Zeiträumen abgeleitet</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div
|
||||||
|
v-for="event in taxEventDrivers"
|
||||||
|
:key="`tax-${event.date}-${event.sourceId}`"
|
||||||
|
class="grid gap-3 rounded-lg border border-gray-200 p-3 sm:grid-cols-[110px_minmax(0,1fr)_auto_auto] dark:border-gray-800"
|
||||||
|
>
|
||||||
|
<span class="text-sm text-gray-500">{{ formatDate(event.date) }}</span>
|
||||||
|
<div class="min-w-0">
|
||||||
|
<p class="truncate font-medium">{{ event.label }}</p>
|
||||||
|
<p class="text-xs text-gray-500">{{ sourceLabels[event.source] }}</p>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="text-right font-semibold"
|
||||||
|
:class="event.amount < 0 ? 'text-rose-600' : 'text-primary-600'"
|
||||||
|
>
|
||||||
|
{{ useCurrency(event.amount) }}
|
||||||
|
</span>
|
||||||
|
<UButton
|
||||||
|
size="xs"
|
||||||
|
variant="ghost"
|
||||||
|
icon="i-heroicons-arrow-top-right-on-square"
|
||||||
|
@click="openEvent(event)"
|
||||||
|
>
|
||||||
|
Öffnen
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
|
||||||
<div class="grid gap-4 xl:grid-cols-[minmax(0,1.2fr)_minmax(320px,0.8fr)]">
|
<div class="grid gap-4 xl:grid-cols-[minmax(0,1.2fr)_minmax(320px,0.8fr)]">
|
||||||
<UCard>
|
<UCard>
|
||||||
<template #header>
|
<template #header>
|
||||||
|
|||||||
Reference in New Issue
Block a user