Serienvorlagen in Liquiditätsprognose einplanen

This commit is contained in:
2026-04-23 22:15:50 +02:00
parent f4edcc2d44
commit 50c76b67c7
2 changed files with 69 additions and 3 deletions

View File

@@ -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);

View File

@@ -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 }
}