diff --git a/backend/src/utils/liquidityForecast.ts b/backend/src/utils/liquidityForecast.ts index d7e1320..b10dcd3 100644 --- a/backend/src/utils/liquidityForecast.ts +++ b/backend/src/utils/liquidityForecast.ts @@ -16,7 +16,7 @@ import { } from "../../db/schema"; import { secrets } from "./secrets"; -type ForecastEventSource = "open_createddocument" | "open_incominginvoice" | "recurring_bankstatement" | "draft_createddocument" | "tax_settlement"; +type ForecastEventSource = "open_createddocument" | "open_incominginvoice" | "recurring_bankstatement" | "draft_createddocument" | "tax_settlement" | "serial_template"; type TaxEvaluationPeriod = "monthly" | "quarterly" | "yearly"; @@ -186,6 +186,15 @@ const addInterval = (date: dayjs.Dayjs, interval: RecurringCandidate["interval"] return date.add(1, "month"); }; +const addSerialInterval = (date: dayjs.Dayjs, interval: string) => { + if (interval === "wöchentlich") return date.add(1, "week"); + if (interval === "2 - wöchentlich") return date.add(2, "week"); + if (interval === "vierteljährlich") return date.add(3, "month"); + if (interval === "halbjährlich") return date.add(6, "month"); + if (interval === "jährlich") return date.add(1, "year"); + return date.add(1, "month"); +}; + const getStatementPartner = (statement: any) => { return statement.amount < 0 ? statement.credName || statement.debName || statement.text || "Regelmäßige Ausgabe" @@ -538,6 +547,58 @@ const buildTaxForecastPeriods = ( return periods; }; +const buildSerialTemplateEvents = ( + documents: any[], + today: dayjs.Dayjs, + endDate: dayjs.Dayjs +) => { + const events: ForecastEvent[] = []; + + documents + .filter((document) => document.type === "serialInvoices") + .filter((document) => document.serialConfig?.active) + .forEach((document) => { + const firstExecution = dayjs(document.serialConfig?.firstExecution); + const executionUntil = dayjs(document.serialConfig?.executionUntil); + + if (!firstExecution.isValid() || !executionUntil.isValid()) return; + + const amount = getCreatedDocumentGrossAmount(document, documents); + if (amount <= 0.01) return; + + let executionDate = firstExecution.startOf("day"); + let guard = 0; + + while (executionDate.isBefore(today, "day") && guard < 240) { + executionDate = addSerialInterval(executionDate, String(document.serialConfig?.intervall || "")); + guard += 1; + } + + while ( + executionDate.isValid() + && !executionDate.isAfter(executionUntil, "day") + && !executionDate.isAfter(endDate, "day") + && guard < 400 + ) { + const dueDate = executionDate.add(Number(document.paymentDays || 0), "day"); + + events.push({ + date: dueDate.isBefore(today, "day") ? today.format("YYYY-MM-DD") : dueDate.format("YYYY-MM-DD"), + amount, + label: document.documentNumber || document.title || `Serienvorlage ${document.id}`, + source: "serial_template", + sourceId: document.id, + confidence: 0.75, + }); + + executionDate = addSerialInterval(executionDate, String(document.serialConfig?.intervall || "")); + guard += 1; + } + }); + + return events.sort((a, b) => a.date.localeCompare(b.date)); +}; + export const generateLiquidityForecast = async ( server: FastifyInstance, tenantId: number, @@ -701,9 +762,11 @@ export const generateLiquidityForecast = async ( sourceId: period.key, confidence: 0.95, })); + const serialTemplateEvents = buildSerialTemplateEvents(activeDocuments, today, endDate); const events = [ ...openEvents, ...taxEvents, + ...serialTemplateEvents, ...expandRecurringEvents(recurring, endDate), ].filter((event) => { const date = dayjs(event.date); diff --git a/frontend/pages/accounting/liquidity.vue b/frontend/pages/accounting/liquidity.vue index 7af30e0..3417af1 100644 --- a/frontend/pages/accounting/liquidity.vue +++ b/frontend/pages/accounting/liquidity.vue @@ -11,7 +11,7 @@ const loading = ref(false) const error = ref("") const cacheLoaded = ref(false) -const CACHE_KEY = "fedeo:liquidityForecast:v2" +const CACHE_KEY = "fedeo:liquidityForecast:v3" const dismissedRecurringKeys = computed(() => { return tempStore.settings?.liquidityForecast?.dismissedRecurringKeys || [] @@ -39,7 +39,8 @@ const sourceLabels = { open_incominginvoice: "Offener Eingangsbeleg", recurring_bankstatement: "Regelmäßige Bankbewegung", draft_createddocument: "Rechnungsentwurf", - tax_settlement: "USt-Zahlung" + tax_settlement: "USt-Zahlung", + serial_template: "Serienvorlage" } const intervalLabels = { @@ -171,6 +172,7 @@ const getEventRoute = (event) => { 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 === "tax_settlement") return "/accounting/tax" + if (event.source === "serial_template" && event.sourceId) return `/createDocument/edit/${event.sourceId}` return null } @@ -262,6 +264,7 @@ const groupedEventSummary = computed(() => { open_incominginvoice: { label: sourceLabels.open_incominginvoice, amount: 0, count: 0 }, recurring_bankstatement: { label: sourceLabels.recurring_bankstatement, amount: 0, count: 0 }, draft_createddocument: { label: "Rechnungsentwürfe als Rechnungen", amount: 0, count: 0 }, + serial_template: { label: sourceLabels.serial_template, amount: 0, count: 0 }, tax_settlement: { label: sourceLabels.tax_settlement, amount: 0, count: 0 } }