feat(mcp): support serial invoice workflows

This commit is contained in:
2026-09-09 14:30:42 +02:00
parent d0bc8a5e0f
commit 2ec162c0e7
5 changed files with 172 additions and 22 deletions

View File

@@ -10,12 +10,14 @@ import {
folders,
incominginvoices,
ownaccounts,
serialExecutions,
statementallocations,
vendors,
} from "../../../db/schema"
import { useNextNumberRangeNumber } from "../../utils/functions"
import { saveFile } from "../../utils/files"
import { insertHistoryItem } from "../../utils/history"
import { executeManualGeneration, finishManualGeneration } from "../../modules/serialexecution.service"
import {
prepareStatementAllocationInput,
statementAllocationUuidArg,
@@ -43,6 +45,22 @@ const numberArg = (args: Record<string, unknown>, key: string) => {
const hasValue = (value: unknown) => value !== null && value !== undefined && value !== ""
const hasValidNumber = (value: unknown) => hasValue(value) && Number.isFinite(Number(value))
const MAX_MCP_UPLOAD_BYTES = 20 * 1024 * 1024
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
const serialConfigSchema = {
type: "object",
description: "Ausführungsplan einer Serienrechnungsvorlage.",
properties: {
firstExecution: { type: "string", description: "Datum der ersten Ausführung als ISO-8601-Wert." },
executionUntil: { type: ["string", "null"], description: "Optionales Datum der letzten Ausführung als ISO-8601-Wert." },
intervall: {
type: "string",
enum: ["wöchentlich", "2 - wöchentlich", "monatlich", "vierteljährlich", "halbjährlich", "jährlich"],
},
active: { type: "boolean" },
dateDirection: { type: "string", enum: ["Rückwirkend", "Im Voraus"] },
},
}
const allowedOutgoingDocumentTypes = new Set([
"quotes",
@@ -501,7 +519,7 @@ export const accountingTools: McpTool[] = [
type: "object",
required: ["type"],
properties: {
type: { type: "string" },
type: { type: "string", enum: [...allowedOutgoingDocumentTypes] },
customer: { type: "number" },
contact: { type: "number" },
contract: { type: "number" },
@@ -529,6 +547,7 @@ export const accountingTools: McpTool[] = [
availableInPortal: { type: "boolean" },
customSurchargePercentage: { type: "number" },
report: { type: "object" },
serialConfig: serialConfigSchema,
},
},
async handler(context, args) {
@@ -555,7 +574,7 @@ export const accountingTools: McpTool[] = [
required: ["id"],
properties: {
id: { type: "number" },
type: { type: "string" },
type: { type: "string", enum: [...allowedOutgoingDocumentTypes] },
state: { type: "string" },
customer: { type: "number" },
contact: { type: "number" },
@@ -584,6 +603,7 @@ export const accountingTools: McpTool[] = [
availableInPortal: { type: "boolean" },
customSurchargePercentage: { type: "number" },
report: { type: "object" },
serialConfig: serialConfigSchema,
},
},
async handler(context, args) {
@@ -668,6 +688,90 @@ export const accountingTools: McpTool[] = [
return { document: updated }
},
},
{
name: "accounting.serial_invoices.execute",
title: "Serienrechnungslauf starten",
description: "Erzeugt aus ausgewählten aktiven Serienrechnungsvorlagen einen neuen Rechnungslauf.",
requiredPermissions: ["accounting.outgoing_documents.write"],
inputSchema: {
type: "object",
required: ["executionDate", "templateIds"],
properties: {
executionDate: { type: "string", description: "Ausführungsdatum als ISO-8601-Wert." },
templateIds: {
type: "array",
minItems: 1,
uniqueItems: true,
items: { type: "number" },
},
},
},
async handler(context, args) {
const executionDate = stringArg(args, "executionDate")
const parsedExecutionDate = executionDate ? new Date(executionDate) : null
const templateIds = Array.isArray(args.templateIds)
? [...new Set(args.templateIds.map(Number).filter((id) => Number.isFinite(id) && id > 0))]
: []
if (!parsedExecutionDate || Number.isNaN(parsedExecutionDate.getTime())) {
throw new Error("executionDate muss ein gültiger ISO-8601-Wert sein")
}
if (!templateIds.length) throw new Error("templateIds muss mindestens eine gültige ID enthalten")
return executeManualGeneration(
context.server,
parsedExecutionDate,
templateIds,
context.tenantId,
context.userId,
)
},
},
{
name: "accounting.serial_invoice_executions.list",
title: "Serienrechnungsläufe auflisten",
description: "Listet die zuletzt gestarteten Serienrechnungsläufe des aktiven Mandanten.",
requiredPermissions: ["accounting.outgoing_documents.read"],
inputSchema: {
type: "object",
properties: {
status: { type: "string", enum: ["draft", "completed", "error"] },
limit: { type: "number", minimum: 1, maximum: 100 },
},
},
async handler(context, args) {
const conditions = [eq(serialExecutions.tenant, context.tenantId)]
const status = stringArg(args, "status")
if (status) conditions.push(eq(serialExecutions.status, status))
const rows = await context.server.db
.select()
.from(serialExecutions)
.where(and(...conditions))
.orderBy(desc(serialExecutions.createdAt))
.limit(limitFromArgs(args))
return { rows }
},
},
{
name: "accounting.serial_invoice_executions.finish",
title: "Serienrechnungslauf abschließen",
description: "Finalisiert die erzeugten Rechnungen eines Serienrechnungslaufs und schließt den Lauf ab.",
requiredPermissions: ["accounting.outgoing_documents.write"],
inputSchema: {
type: "object",
required: ["id"],
properties: {
id: { type: "string", description: "UUID des Serienrechnungslaufs." },
},
},
async handler(context, args) {
const id = stringArg(args, "id")
if (!id || !UUID_PATTERN.test(id)) throw new Error("id muss eine gültige UUID sein")
return finishManualGeneration(context.server, id, context.tenantId)
},
},
{
name: "accounting.outgoing_documents.archive",
title: "Ausgangsbeleg archivieren",