MCP Upload für Eingangsbelege ergänzen
This commit is contained in:
@@ -3,11 +3,14 @@ import {
|
||||
accounts,
|
||||
bankstatements,
|
||||
createddocuments,
|
||||
filetags,
|
||||
files,
|
||||
folders,
|
||||
incominginvoices,
|
||||
statementallocations,
|
||||
} from "../../../db/schema"
|
||||
import { useNextNumberRangeNumber } from "../../utils/functions"
|
||||
import { saveFile } from "../../utils/files"
|
||||
import { McpTool } from "../types"
|
||||
|
||||
const limitFromArgs = (args: Record<string, unknown>, fallback = 25) => {
|
||||
@@ -28,6 +31,7 @@ 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 allowedOutgoingDocumentTypes = new Set([
|
||||
"quotes",
|
||||
@@ -193,6 +197,49 @@ const incomingInvoiceAccountsArg = (args: Record<string, unknown>) => {
|
||||
return args.accounts
|
||||
}
|
||||
|
||||
const base64BufferArg = (args: Record<string, unknown>, key: string) => {
|
||||
const value = stringArg(args, key)
|
||||
if (!value) throw new Error(`${key} ist erforderlich`)
|
||||
|
||||
const base64 = value.includes(",") ? value.split(",").pop() || "" : value
|
||||
const buffer = Buffer.from(base64, "base64")
|
||||
|
||||
if (!buffer.length) throw new Error(`${key} enthält keine Datei`)
|
||||
if (buffer.length > MAX_MCP_UPLOAD_BYTES) throw new Error("Datei ist größer als 20 MB")
|
||||
|
||||
return buffer
|
||||
}
|
||||
|
||||
const loadIncomingInvoiceFileDefaults = async (context: { server: any; tenantId: number }) => {
|
||||
const currentYear = new Date().getFullYear()
|
||||
|
||||
const [tag] = await context.server.db
|
||||
.select({ id: filetags.id })
|
||||
.from(filetags)
|
||||
.where(and(
|
||||
eq(filetags.tenant, context.tenantId),
|
||||
eq(filetags.incomingDocumentType, "invoices"),
|
||||
eq(filetags.archived, false)
|
||||
))
|
||||
.limit(1)
|
||||
|
||||
const [folder] = await context.server.db
|
||||
.select({ id: folders.id })
|
||||
.from(folders)
|
||||
.where(and(
|
||||
eq(folders.tenant, context.tenantId),
|
||||
eq(folders.function, "incomingInvoices"),
|
||||
eq(folders.year, currentYear),
|
||||
eq(folders.archived, false)
|
||||
))
|
||||
.limit(1)
|
||||
|
||||
return {
|
||||
folderId: folder?.id || null,
|
||||
fileTagId: tag?.id || null,
|
||||
}
|
||||
}
|
||||
|
||||
const buildIncomingInvoicePayload = (
|
||||
args: Record<string, unknown>,
|
||||
userId: string,
|
||||
@@ -694,6 +741,124 @@ export const accountingTools: McpTool[] = [
|
||||
return { rows }
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "accounting.incoming_invoices.files.upload",
|
||||
title: "Datei für Eingangsbeleg hochladen",
|
||||
description: "Lädt eine PDF- oder Bilddatei als Base64 hoch, verknüpft sie optional mit einem Eingangsbeleg oder stößt die automatische Vorbereitung an.",
|
||||
requiredPermissions: ["accounting.incoming_invoices.write"],
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
required: ["filename", "contentBase64"],
|
||||
properties: {
|
||||
filename: { type: "string", description: "Dateiname, z. B. rechnung.pdf." },
|
||||
contentBase64: { type: "string", description: "Dateiinhalt als Base64 oder Data-URL." },
|
||||
mimeType: { type: "string", description: "MIME-Type, z. B. application/pdf oder image/jpeg." },
|
||||
invoiceId: { type: "number", description: "Optional: vorhandener Eingangsbeleg, mit dem die Datei verknüpft wird." },
|
||||
prepare: {
|
||||
type: "boolean",
|
||||
default: true,
|
||||
description: "Wenn kein invoiceId und keine invoice-Daten übergeben werden, wird nach dem Upload die automatische Eingangsbeleg-Vorbereitung ausgeführt.",
|
||||
},
|
||||
invoice: {
|
||||
type: "object",
|
||||
description: "Optional: Daten für einen neuen Eingangsbeleg-Entwurf, der direkt mit der Datei verknüpft wird.",
|
||||
properties: {
|
||||
vendor: { type: "number" },
|
||||
reference: { type: "string" },
|
||||
date: { type: "string" },
|
||||
dueDate: { type: "string" },
|
||||
document: { type: "number" },
|
||||
description: { type: "string" },
|
||||
paymentType: { type: "string" },
|
||||
accounts: { type: "array" },
|
||||
expense: { type: "boolean" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async handler(context, args) {
|
||||
const filename = stringArg(args, "filename")
|
||||
if (!filename) throw new Error("filename ist erforderlich")
|
||||
|
||||
const buffer = base64BufferArg(args, "contentBase64")
|
||||
const mimeType = stringArg(args, "mimeType") || "application/octet-stream"
|
||||
const invoiceId = numberArg(args, "invoiceId")
|
||||
const invoiceArgs = optionalObjectArg(args, "invoice") as Record<string, unknown> | null
|
||||
const shouldPrepare = args.prepare !== false && !invoiceId && !invoiceArgs
|
||||
|
||||
let linkedInvoice: any = null
|
||||
|
||||
if (invoiceId) {
|
||||
const [existing] = await context.server.db
|
||||
.select()
|
||||
.from(incominginvoices)
|
||||
.where(and(eq(incominginvoices.id, invoiceId), eq(incominginvoices.tenant, context.tenantId)))
|
||||
.limit(1)
|
||||
|
||||
if (!existing) throw new Error("Eingangsbeleg nicht gefunden")
|
||||
linkedInvoice = existing
|
||||
} else if (invoiceArgs) {
|
||||
const payload = buildIncomingInvoicePayload(invoiceArgs, context.userId, context.tenantId, true)
|
||||
payload.state = "Entwurf"
|
||||
|
||||
const [createdInvoice] = await context.server.db
|
||||
.insert(incominginvoices)
|
||||
.values(payload as any)
|
||||
.returning()
|
||||
|
||||
linkedInvoice = createdInvoice
|
||||
}
|
||||
|
||||
const defaults = await loadIncomingInvoiceFileDefaults(context)
|
||||
const saved = await saveFile(
|
||||
context.server,
|
||||
context.tenantId,
|
||||
null,
|
||||
{
|
||||
filename,
|
||||
content: buffer,
|
||||
contentType: mimeType,
|
||||
},
|
||||
defaults.folderId,
|
||||
defaults.fileTagId,
|
||||
{
|
||||
incominginvoice: linkedInvoice?.id || null,
|
||||
createdBy: context.userId,
|
||||
updatedBy: context.userId,
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
)
|
||||
|
||||
if (!saved) throw new Error("Datei konnte nicht gespeichert werden")
|
||||
|
||||
if (shouldPrepare) {
|
||||
await context.server.services.prepareIncomingInvoices.run(context.tenantId)
|
||||
}
|
||||
|
||||
const [file] = await context.server.db
|
||||
.select()
|
||||
.from(files)
|
||||
.where(and(eq(files.id, saved.id), eq(files.tenant, context.tenantId)))
|
||||
.limit(1)
|
||||
|
||||
if (file?.incominginvoice && !linkedInvoice) {
|
||||
const [preparedInvoice] = await context.server.db
|
||||
.select()
|
||||
.from(incominginvoices)
|
||||
.where(and(eq(incominginvoices.id, file.incominginvoice), eq(incominginvoices.tenant, context.tenantId)))
|
||||
.limit(1)
|
||||
|
||||
linkedInvoice = preparedInvoice || null
|
||||
}
|
||||
|
||||
return {
|
||||
file,
|
||||
invoice: linkedInvoice,
|
||||
validation: linkedInvoice ? validateIncomingInvoiceData(linkedInvoice as Record<string, any>) : null,
|
||||
prepared: Boolean(shouldPrepare && linkedInvoice),
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "accounting.incoming_invoices.validate",
|
||||
title: "Eingangsbeleg validieren",
|
||||
|
||||
Reference in New Issue
Block a user