Ausgangsbelege im FEDEO MCP ergänzen
Erweitert den Accounting-MCP um lesende und schreibende Tools für Ausgangsbelege. Belege können gelistet, geladen, als Entwurf erstellt, aktualisiert und archiviert werden; optional wird beim Schreiben eine Belegnummer aus dem passenden Nummernkreis gezogen.
This commit is contained in:
@@ -2,9 +2,11 @@ import { and, desc, eq, ilike, or } from "drizzle-orm"
|
|||||||
import {
|
import {
|
||||||
accounts,
|
accounts,
|
||||||
bankstatements,
|
bankstatements,
|
||||||
|
createddocuments,
|
||||||
incominginvoices,
|
incominginvoices,
|
||||||
statementallocations,
|
statementallocations,
|
||||||
} from "../../../db/schema"
|
} from "../../../db/schema"
|
||||||
|
import { useNextNumberRangeNumber } from "../../utils/functions"
|
||||||
import { McpTool } from "../types"
|
import { McpTool } from "../types"
|
||||||
|
|
||||||
const limitFromArgs = (args: Record<string, unknown>, fallback = 25) => {
|
const limitFromArgs = (args: Record<string, unknown>, fallback = 25) => {
|
||||||
@@ -23,7 +25,321 @@ const numberArg = (args: Record<string, unknown>, key: string) => {
|
|||||||
return Number.isFinite(value) ? value : null
|
return Number.isFinite(value) ? value : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const allowedOutgoingDocumentTypes = new Set([
|
||||||
|
"quotes",
|
||||||
|
"costEstimates",
|
||||||
|
"confirmationOrders",
|
||||||
|
"deliveryNotes",
|
||||||
|
"packingSlips",
|
||||||
|
"invoices",
|
||||||
|
"advanceInvoices",
|
||||||
|
"cancellationInvoices",
|
||||||
|
"serialInvoices",
|
||||||
|
])
|
||||||
|
|
||||||
|
const documentTypeArg = (args: Record<string, unknown>, key = "type") => {
|
||||||
|
const type = stringArg(args, key) || "invoices"
|
||||||
|
if (!allowedOutgoingDocumentTypes.has(type)) {
|
||||||
|
throw new Error(`Ungültige Belegart: ${type}`)
|
||||||
|
}
|
||||||
|
return type
|
||||||
|
}
|
||||||
|
|
||||||
|
const optionalObjectArg = (args: Record<string, unknown>, key: string) => {
|
||||||
|
const value = args[key]
|
||||||
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null
|
||||||
|
}
|
||||||
|
|
||||||
|
const optionalArrayArg = (args: Record<string, unknown>, key: string) => {
|
||||||
|
const value = args[key]
|
||||||
|
return Array.isArray(value) ? value : null
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildOutgoingDocumentPayload = (
|
||||||
|
args: Record<string, unknown>,
|
||||||
|
userId: string,
|
||||||
|
tenantId: number,
|
||||||
|
includeCreateDefaults = false
|
||||||
|
) => {
|
||||||
|
const payload: Record<string, unknown> = {
|
||||||
|
updatedAt: new Date(),
|
||||||
|
updatedBy: userId,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeCreateDefaults) {
|
||||||
|
payload.tenant = tenantId
|
||||||
|
payload.createdBy = userId
|
||||||
|
payload.created_by = userId
|
||||||
|
payload.archived = false
|
||||||
|
payload.state = stringArg(args, "state") || "Entwurf"
|
||||||
|
payload.type = documentTypeArg(args)
|
||||||
|
payload.rows = optionalArrayArg(args, "rows") || []
|
||||||
|
}
|
||||||
|
|
||||||
|
const stringFields = [
|
||||||
|
"state",
|
||||||
|
"documentNumber",
|
||||||
|
"documentDate",
|
||||||
|
"deliveryDate",
|
||||||
|
"deliveryDateEnd",
|
||||||
|
"deliveryDateType",
|
||||||
|
"payment_type",
|
||||||
|
"title",
|
||||||
|
"description",
|
||||||
|
"startText",
|
||||||
|
"endText",
|
||||||
|
"taxType",
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const field of stringFields) {
|
||||||
|
if (args[field] !== undefined) payload[field] = stringArg(args, field)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const field of ["customer", "contact", "contract", "project", "plant", "letterhead", "createddocument"] as const) {
|
||||||
|
if (args[field] !== undefined) payload[field] = numberArg(args, field)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const field of ["paymentDays"] as const) {
|
||||||
|
if (args[field] !== undefined) payload[field] = numberArg(args, field)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.type !== undefined) payload.type = documentTypeArg(args)
|
||||||
|
if (args.address !== undefined) payload.address = optionalObjectArg(args, "address")
|
||||||
|
if (args.info !== undefined) payload.info = optionalObjectArg(args, "info")
|
||||||
|
if (args.agriculture !== undefined) payload.agriculture = optionalObjectArg(args, "agriculture")
|
||||||
|
if (args.report !== undefined) payload.report = optionalObjectArg(args, "report") || {}
|
||||||
|
if (args.serialConfig !== undefined) payload.serialConfig = optionalObjectArg(args, "serialConfig") || {}
|
||||||
|
if (args.rows !== undefined) payload.rows = optionalArrayArg(args, "rows") || []
|
||||||
|
if (args.usedAdvanceInvoices !== undefined) payload.usedAdvanceInvoices = optionalArrayArg(args, "usedAdvanceInvoices") || []
|
||||||
|
if (typeof args.availableInPortal === "boolean") payload.availableInPortal = args.availableInPortal
|
||||||
|
if (typeof args.advanceInvoiceResolved === "boolean") payload.advanceInvoiceResolved = args.advanceInvoiceResolved
|
||||||
|
if (args.customSurchargePercentage !== undefined) payload.customSurchargePercentage = numberArg(args, "customSurchargePercentage") || 0
|
||||||
|
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldAssignDocumentNumber = (args: Record<string, unknown>, state?: unknown) =>
|
||||||
|
args.assignDocumentNumber === true || (state && state !== "Entwurf" && !stringArg(args, "documentNumber"))
|
||||||
|
|
||||||
export const accountingTools: McpTool[] = [
|
export const accountingTools: McpTool[] = [
|
||||||
|
{
|
||||||
|
name: "accounting.outgoing_documents.list",
|
||||||
|
title: "Ausgangsbelege auflisten",
|
||||||
|
description: "Listet Ausgangsbelege des aktiven Mandanten mit optionalen Filtern.",
|
||||||
|
requiredPermissions: ["accounting.outgoing_documents.read"],
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
type: { type: "string", description: "Belegart, z. B. invoices, quotes oder deliveryNotes." },
|
||||||
|
state: { type: "string", description: "Optionaler Statusfilter, z. B. Entwurf oder Gebucht." },
|
||||||
|
customer: { type: "number" },
|
||||||
|
project: { type: "number" },
|
||||||
|
includeArchived: { type: "boolean", default: false },
|
||||||
|
limit: { type: "number", minimum: 1, maximum: 100 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async handler(context, args) {
|
||||||
|
const conditions = [eq(createddocuments.tenant, context.tenantId)]
|
||||||
|
const type = stringArg(args, "type")
|
||||||
|
const state = stringArg(args, "state")
|
||||||
|
const customer = numberArg(args, "customer")
|
||||||
|
const project = numberArg(args, "project")
|
||||||
|
|
||||||
|
if (type) {
|
||||||
|
if (!allowedOutgoingDocumentTypes.has(type)) throw new Error(`Ungültige Belegart: ${type}`)
|
||||||
|
conditions.push(eq(createddocuments.type, type))
|
||||||
|
}
|
||||||
|
if (state) conditions.push(eq(createddocuments.state, state))
|
||||||
|
if (customer) conditions.push(eq(createddocuments.customer, customer))
|
||||||
|
if (project) conditions.push(eq(createddocuments.project, project))
|
||||||
|
if (args.includeArchived !== true) conditions.push(eq(createddocuments.archived, false))
|
||||||
|
|
||||||
|
const rows = await context.server.db
|
||||||
|
.select()
|
||||||
|
.from(createddocuments)
|
||||||
|
.where(and(...conditions))
|
||||||
|
.orderBy(desc(createddocuments.createdAt))
|
||||||
|
.limit(limitFromArgs(args))
|
||||||
|
|
||||||
|
return { rows }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "accounting.outgoing_documents.get",
|
||||||
|
title: "Ausgangsbeleg laden",
|
||||||
|
description: "Lädt einen Ausgangsbeleg des aktiven Mandanten anhand seiner ID.",
|
||||||
|
requiredPermissions: ["accounting.outgoing_documents.read"],
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
required: ["id"],
|
||||||
|
properties: {
|
||||||
|
id: { type: "number" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async handler(context, args) {
|
||||||
|
const id = numberArg(args, "id")
|
||||||
|
if (!id) throw new Error("id ist erforderlich")
|
||||||
|
|
||||||
|
const rows = await context.server.db
|
||||||
|
.select()
|
||||||
|
.from(createddocuments)
|
||||||
|
.where(and(eq(createddocuments.id, id), eq(createddocuments.tenant, context.tenantId)))
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
if (!rows[0]) throw new Error("Ausgangsbeleg nicht gefunden")
|
||||||
|
return { document: rows[0] }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "accounting.outgoing_documents.create",
|
||||||
|
title: "Ausgangsbeleg erstellen",
|
||||||
|
description: "Erstellt einen Ausgangsbeleg im aktiven Mandanten. Ohne assignDocumentNumber bleibt der Beleg als Entwurf ohne neue Nummer möglich.",
|
||||||
|
requiredPermissions: ["accounting.outgoing_documents.write"],
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
required: ["type"],
|
||||||
|
properties: {
|
||||||
|
type: { type: "string" },
|
||||||
|
state: { type: "string", default: "Entwurf" },
|
||||||
|
assignDocumentNumber: { type: "boolean", default: false },
|
||||||
|
customer: { type: "number" },
|
||||||
|
contact: { type: "number" },
|
||||||
|
contract: { type: "number" },
|
||||||
|
project: { type: "number" },
|
||||||
|
plant: { type: "number" },
|
||||||
|
documentNumber: { type: "string" },
|
||||||
|
documentDate: { type: "string" },
|
||||||
|
deliveryDate: { type: "string" },
|
||||||
|
deliveryDateEnd: { type: "string" },
|
||||||
|
deliveryDateType: { type: "string" },
|
||||||
|
paymentDays: { type: "number" },
|
||||||
|
payment_type: { type: "string" },
|
||||||
|
taxType: { type: "string" },
|
||||||
|
title: { type: "string" },
|
||||||
|
description: { type: "string" },
|
||||||
|
startText: { type: "string" },
|
||||||
|
endText: { type: "string" },
|
||||||
|
address: { type: "object" },
|
||||||
|
rows: { type: "array" },
|
||||||
|
letterhead: { type: "number" },
|
||||||
|
availableInPortal: { type: "boolean" },
|
||||||
|
customSurchargePercentage: { type: "number" },
|
||||||
|
report: { type: "object" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async handler(context, args) {
|
||||||
|
const payload = buildOutgoingDocumentPayload(args, context.userId, context.tenantId, true)
|
||||||
|
|
||||||
|
if (shouldAssignDocumentNumber(args, payload.state) && !payload.documentNumber) {
|
||||||
|
const result = await useNextNumberRangeNumber(context.server, context.tenantId, String(payload.type))
|
||||||
|
payload.documentNumber = result.usedNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
const [created] = await context.server.db
|
||||||
|
.insert(createddocuments)
|
||||||
|
.values(payload as any)
|
||||||
|
.returning()
|
||||||
|
|
||||||
|
return { document: created }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "accounting.outgoing_documents.update",
|
||||||
|
title: "Ausgangsbeleg aktualisieren",
|
||||||
|
description: "Aktualisiert einen Ausgangsbeleg im aktiven Mandanten.",
|
||||||
|
requiredPermissions: ["accounting.outgoing_documents.write"],
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
required: ["id"],
|
||||||
|
properties: {
|
||||||
|
id: { type: "number" },
|
||||||
|
type: { type: "string" },
|
||||||
|
state: { type: "string" },
|
||||||
|
assignDocumentNumber: { type: "boolean", default: false },
|
||||||
|
customer: { type: "number" },
|
||||||
|
contact: { type: "number" },
|
||||||
|
contract: { type: "number" },
|
||||||
|
project: { type: "number" },
|
||||||
|
plant: { type: "number" },
|
||||||
|
documentNumber: { type: "string" },
|
||||||
|
documentDate: { type: "string" },
|
||||||
|
deliveryDate: { type: "string" },
|
||||||
|
deliveryDateEnd: { type: "string" },
|
||||||
|
deliveryDateType: { type: "string" },
|
||||||
|
paymentDays: { type: "number" },
|
||||||
|
payment_type: { type: "string" },
|
||||||
|
taxType: { type: "string" },
|
||||||
|
title: { type: "string" },
|
||||||
|
description: { type: "string" },
|
||||||
|
startText: { type: "string" },
|
||||||
|
endText: { type: "string" },
|
||||||
|
address: { type: "object" },
|
||||||
|
rows: { type: "array" },
|
||||||
|
letterhead: { type: "number" },
|
||||||
|
availableInPortal: { type: "boolean" },
|
||||||
|
customSurchargePercentage: { type: "number" },
|
||||||
|
report: { type: "object" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async handler(context, args) {
|
||||||
|
const id = numberArg(args, "id")
|
||||||
|
if (!id) throw new Error("id ist erforderlich")
|
||||||
|
|
||||||
|
const [existing] = await context.server.db
|
||||||
|
.select()
|
||||||
|
.from(createddocuments)
|
||||||
|
.where(and(eq(createddocuments.id, id), eq(createddocuments.tenant, context.tenantId)))
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
if (!existing) throw new Error("Ausgangsbeleg nicht gefunden")
|
||||||
|
|
||||||
|
const payload = buildOutgoingDocumentPayload(args, context.userId, context.tenantId)
|
||||||
|
const nextType = String(payload.type || existing.type)
|
||||||
|
|
||||||
|
if (shouldAssignDocumentNumber(args, payload.state || existing.state) && !payload.documentNumber && !existing.documentNumber) {
|
||||||
|
const result = await useNextNumberRangeNumber(context.server, context.tenantId, nextType)
|
||||||
|
payload.documentNumber = result.usedNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
const [updated] = await context.server.db
|
||||||
|
.update(createddocuments)
|
||||||
|
.set(payload as any)
|
||||||
|
.where(and(eq(createddocuments.id, id), eq(createddocuments.tenant, context.tenantId)))
|
||||||
|
.returning()
|
||||||
|
|
||||||
|
return { document: updated }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "accounting.outgoing_documents.archive",
|
||||||
|
title: "Ausgangsbeleg archivieren",
|
||||||
|
description: "Archiviert einen Ausgangsbeleg im aktiven Mandanten.",
|
||||||
|
requiredPermissions: ["accounting.outgoing_documents.write"],
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
required: ["id"],
|
||||||
|
properties: {
|
||||||
|
id: { type: "number" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async handler(context, args) {
|
||||||
|
const id = numberArg(args, "id")
|
||||||
|
if (!id) throw new Error("id ist erforderlich")
|
||||||
|
|
||||||
|
const [updated] = await context.server.db
|
||||||
|
.update(createddocuments)
|
||||||
|
.set({
|
||||||
|
archived: true,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
updatedBy: context.userId,
|
||||||
|
})
|
||||||
|
.where(and(eq(createddocuments.id, id), eq(createddocuments.tenant, context.tenantId)))
|
||||||
|
.returning()
|
||||||
|
|
||||||
|
if (!updated) throw new Error("Ausgangsbeleg nicht gefunden")
|
||||||
|
return { document: updated }
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "accounting.accounts.search",
|
name: "accounting.accounts.search",
|
||||||
title: "Konten suchen",
|
title: "Konten suchen",
|
||||||
|
|||||||
Reference in New Issue
Block a user