Files
FEDEO/backend/src/mcp/tools/accounting.ts
florianfederspiel e60188f043 Dokument-Steuertypen im FEDEO MCP ergänzen
Erweitert die Ausgangsbeleg-Tools um explizite Dokument-Steuertypen. Der MCP listet die unterstützten Steuertypen, validiert taxType als Dokumentfeld und setzt bei 13b UStG, 19 UStG und 12.3 UStG die Positions-USt analog zur Oberfläche auf 0 Prozent.
2026-05-11 17:25:10 +02:00

971 lines
38 KiB
TypeScript

import { and, desc, eq, ilike, or } from "drizzle-orm"
import {
accounts,
bankstatements,
createddocuments,
files,
incominginvoices,
statementallocations,
} from "../../../db/schema"
import { useNextNumberRangeNumber } from "../../utils/functions"
import { McpTool } from "../types"
const limitFromArgs = (args: Record<string, unknown>, fallback = 25) => {
const raw = Number(args.limit ?? fallback)
if (!Number.isFinite(raw)) return fallback
return Math.min(Math.max(Math.trunc(raw), 1), 100)
}
const stringArg = (args: Record<string, unknown>, key: string) => {
const value = args[key]
return typeof value === "string" && value.trim() ? value.trim() : null
}
const numberArg = (args: Record<string, unknown>, key: string) => {
const value = Number(args[key])
return Number.isFinite(value) ? value : null
}
const hasValue = (value: unknown) => value !== null && value !== undefined && value !== ""
const hasValidNumber = (value: unknown) => hasValue(value) && Number.isFinite(Number(value))
const allowedOutgoingDocumentTypes = new Set([
"quotes",
"costEstimates",
"confirmationOrders",
"deliveryNotes",
"packingSlips",
"invoices",
"advanceInvoices",
"cancellationInvoices",
"serialInvoices",
])
const allowedOutgoingDocumentTaxTypes = new Set([
"Standard",
"13b UStG",
"19 UStG",
"12.3 UStG",
])
const outgoingDocumentTaxableTypes = new Set([
"invoices",
"cancellationInvoices",
"advanceInvoices",
"serialInvoices",
"confirmationOrders",
"quotes",
"costEstimates",
])
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 normalizeOutgoingDocumentTaxType = (value: unknown) => {
const taxType = typeof value === "string" && value.trim() ? value.trim() : "Standard"
if (!allowedOutgoingDocumentTaxTypes.has(taxType)) {
throw new Error(`Ungültiger Dokument-Steuertyp: ${taxType}`)
}
return taxType
}
const applyOutgoingDocumentTaxType = (
payload: Record<string, unknown>,
args: Record<string, unknown>,
documentType: string,
existingTaxType?: unknown,
existingRows?: unknown
) => {
if (!outgoingDocumentTaxableTypes.has(documentType)) {
payload.taxType = null
return
}
const taxType = args.taxType !== undefined
? normalizeOutgoingDocumentTaxType(args.taxType)
: existingTaxType
? normalizeOutgoingDocumentTaxType(existingTaxType)
: "Standard"
payload.taxType = taxType
if (["13b UStG", "19 UStG", "12.3 UStG"].includes(taxType)) {
const rows = Array.isArray(payload.rows)
? payload.rows
: Array.isArray(existingRows)
? existingRows
: null
if (!rows) return
payload.rows = rows.map((row: any) => ({
...row,
taxPercent: 0,
}))
}
}
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",
"documentDate",
"deliveryDate",
"deliveryDateEnd",
"deliveryDateType",
"payment_type",
"title",
"description",
"startText",
"endText",
]
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 assertNoManualDocumentNumber = (args: Record<string, unknown>) => {
if (args.documentNumber !== undefined || args.assignDocumentNumber !== undefined) {
throw new Error("Belegnummern werden nur beim Finalisieren vergeben")
}
}
const incomingInvoiceAccountsArg = (args: Record<string, unknown>) => {
if (args.accounts === undefined) return undefined
if (!Array.isArray(args.accounts)) throw new Error("accounts muss ein Array sein")
return args.accounts
}
const buildIncomingInvoicePayload = (
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.state = "Entwurf"
payload.expense = args.expense !== undefined ? args.expense === true : true
payload.paid = false
payload.archived = false
}
for (const field of ["state", "reference", "date", "dueDate", "description", "paymentType"] as const) {
if (args[field] !== undefined) payload[field] = stringArg(args, field)
}
if (args.vendor !== undefined) payload.vendor = numberArg(args, "vendor")
if (args.document !== undefined) payload.document = numberArg(args, "document")
if (args.expense !== undefined) payload.expense = args.expense === true
if (args.paid !== undefined) payload.paid = args.paid === true
const accountsPayload = incomingInvoiceAccountsArg(args)
if (accountsPayload !== undefined) payload.accounts = accountsPayload
return payload
}
const isDepreciationBookingMode = (value: unknown) =>
["depreciation", "depreciation_bundle"].includes(String(value || ""))
const validateIncomingInvoiceData = (invoice: Record<string, any>) => {
const errors: Array<{ message: string; type: "breaking" | "warning" }> = []
if (!invoice.vendor) errors.push({ message: "Es ist kein Lieferant ausgewählt", type: "breaking" })
if (!String(invoice.reference || "").trim()) errors.push({ message: "Es ist keine Referenz angegeben", type: "breaking" })
if (!invoice.date) errors.push({ message: "Es ist kein Datum ausgewählt", type: "breaking" })
if (!Array.isArray(invoice.accounts) || invoice.accounts.length === 0) {
errors.push({ message: "Es ist keine Position vorhanden", type: "breaking" })
}
;(Array.isArray(invoice.accounts) ? invoice.accounts : []).forEach((account: any, idx: number) => {
const pos = idx + 1
if (!account?.account) errors.push({ message: `Pos ${pos}: Keine Kategorie`, type: "breaking" })
if (!hasValidNumber(account?.amountNet) && !hasValidNumber(account?.amountGross)) {
errors.push({ message: `Pos ${pos}: Kein gültiger Betrag`, type: "breaking" })
}
if (!account?.taxType) errors.push({ message: `Pos ${pos}: Kein Steuerschlüssel`, type: "breaking" })
if (hasValidNumber(account?.amountNet) && !hasValidNumber(account?.amountTax)) {
errors.push({ message: `Pos ${pos}: Steuerbetrag fehlt, bitte Steuer neu berechnen`, type: "warning" })
}
if (isDepreciationBookingMode(account?.bookingMode) && !Number(account?.depreciationMonths)) {
errors.push({ message: `Pos ${pos}: Abschreibungsdauer fehlt`, type: "breaking" })
}
if (account?.bookingMode === "depreciation_bundle" && !String(account?.depreciationGroup || "").trim()) {
errors.push({ message: `Pos ${pos}: Sammelposten benötigt einen Gruppennamen`, type: "breaking" })
}
})
const order = { breaking: 0, warning: 1 }
errors.sort((a, b) => order[a.type] - order[b.type])
return {
valid: errors.every((error) => error.type !== "breaking"),
errors,
blockingErrors: errors.filter((error) => error.type === "breaking"),
warnings: errors.filter((error) => error.type === "warning"),
}
}
export const accountingTools: McpTool[] = [
{
name: "accounting.outgoing_documents.tax_types.list",
title: "Steuertypen für Ausgangsbelege auflisten",
description: "Listet die unterstützten Dokument-Steuertypen für Ausgangsbelege.",
requiredPermissions: ["accounting.outgoing_documents.read"],
inputSchema: {
type: "object",
properties: {},
},
async handler() {
return {
rows: [
{ key: "Standard", label: "Standard", forcesZeroTaxPercent: false },
{ key: "13b UStG", label: "13b UStG", forcesZeroTaxPercent: true },
{ key: "19 UStG", label: "19 UStG Kleinunternehmer", forcesZeroTaxPercent: true },
{ key: "12.3 UStG", label: "12.3 UStG", forcesZeroTaxPercent: true },
],
}
},
},
{
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-Entwurf im aktiven Mandanten. Belegnummern werden erst beim Finalisieren vergeben.",
requiredPermissions: ["accounting.outgoing_documents.write"],
inputSchema: {
type: "object",
required: ["type"],
properties: {
type: { type: "string" },
customer: { type: "number" },
contact: { type: "number" },
contract: { type: "number" },
project: { type: "number" },
plant: { type: "number" },
documentDate: { type: "string" },
deliveryDate: { type: "string" },
deliveryDateEnd: { type: "string" },
deliveryDateType: { type: "string" },
paymentDays: { type: "number" },
payment_type: { type: "string" },
taxType: {
type: "string",
enum: ["Standard", "13b UStG", "19 UStG", "12.3 UStG"],
description: "Steuertyp des gesamten Ausgangsdokuments, nicht der einzelne USt-Satz einer Position.",
},
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) {
assertNoManualDocumentNumber(args)
const payload = buildOutgoingDocumentPayload(args, context.userId, context.tenantId, true)
payload.state = "Entwurf"
applyOutgoingDocumentTaxType(payload, args, String(payload.type))
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, solange er noch nicht finalisiert ist.",
requiredPermissions: ["accounting.outgoing_documents.write"],
inputSchema: {
type: "object",
required: ["id"],
properties: {
id: { type: "number" },
type: { type: "string" },
state: { type: "string" },
customer: { type: "number" },
contact: { type: "number" },
contract: { type: "number" },
project: { type: "number" },
plant: { type: "number" },
documentDate: { type: "string" },
deliveryDate: { type: "string" },
deliveryDateEnd: { type: "string" },
deliveryDateType: { type: "string" },
paymentDays: { type: "number" },
payment_type: { type: "string" },
taxType: {
type: "string",
enum: ["Standard", "13b UStG", "19 UStG", "12.3 UStG"],
description: "Steuertyp des gesamten Ausgangsdokuments, nicht der einzelne USt-Satz einer Position.",
},
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")
assertNoManualDocumentNumber(args)
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")
if (existing.state !== "Entwurf") throw new Error("Finalisierte Ausgangsbelege können nicht über update geändert werden")
const payload = buildOutgoingDocumentPayload(args, context.userId, context.tenantId)
payload.state = "Entwurf"
applyOutgoingDocumentTaxType(payload, args, String(payload.type || existing.type), existing.taxType, existing.rows)
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.finalize",
title: "Ausgangsbeleg finalisieren",
description: "Finalisiert einen Ausgangsbeleg, setzt den Status auf Gebucht und vergibt dabei genau einmal eine Belegnummer.",
requiredPermissions: ["accounting.outgoing_documents.write"],
inputSchema: {
type: "object",
required: ["id"],
properties: {
id: { type: "number" },
documentDate: { type: "string" },
deliveryDate: { type: "string" },
deliveryDateEnd: { type: "string" },
paymentDays: { type: "number" },
payment_type: { type: "string" },
taxType: {
type: "string",
enum: ["Standard", "13b UStG", "19 UStG", "12.3 UStG"],
description: "Steuertyp des gesamten Ausgangsdokuments, nicht der einzelne USt-Satz einer Position.",
},
rows: { type: "array" },
report: { type: "object" },
availableInPortal: { type: "boolean" },
},
},
async handler(context, args) {
const id = numberArg(args, "id")
if (!id) throw new Error("id ist erforderlich")
assertNoManualDocumentNumber(args)
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")
if (existing.documentNumber) throw new Error("Ausgangsbeleg wurde bereits finalisiert")
const payload = buildOutgoingDocumentPayload(args, context.userId, context.tenantId)
const result = await useNextNumberRangeNumber(context.server, context.tenantId, existing.type)
payload.state = "Gebucht"
payload.documentNumber = result.usedNumber
applyOutgoingDocumentTaxType(payload, args, existing.type, existing.taxType, existing.rows)
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",
title: "Konten suchen",
description: "Sucht Sachkonten im aktiven Kontenrahmen des Mandanten.",
requiredPermissions: ["accounting.accounts.read"],
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "Suchtext für Kontonummer, Bezeichnung oder Beschreibung." },
limit: { type: "number", minimum: 1, maximum: 100 },
},
},
async handler(context, args) {
const tenantRows = await context.server.db.query.tenants.findMany({
where: (tenant, { eq }) => eq(tenant.id, context.tenantId),
columns: {
accountChart: true,
},
limit: 1,
})
const accountChart = tenantRows[0]?.accountChart || "skr03"
const query = stringArg(args, "query")
const limit = limitFromArgs(args)
const whereCond = query
? and(
eq(accounts.accountChart, accountChart),
or(
ilike(accounts.number, `%${query}%`),
ilike(accounts.label, `%${query}%`),
ilike(accounts.description, `%${query}%`)
)
)
: eq(accounts.accountChart, accountChart)
const rows = await context.server.db
.select()
.from(accounts)
.where(whereCond)
.orderBy(accounts.number)
.limit(limit)
return { accountChart, rows }
},
},
{
name: "accounting.incoming_invoices.list",
title: "Eingangsrechnungen auflisten",
description: "Listet Eingangsrechnungen des aktiven Mandanten.",
requiredPermissions: ["accounting.incoming_invoices.read"],
inputSchema: {
type: "object",
properties: {
state: { type: "string", description: "Optionaler Statusfilter." },
paid: { type: "boolean", description: "Optionaler Zahlungsstatus." },
includeArchived: { type: "boolean", default: false },
limit: { type: "number", minimum: 1, maximum: 100 },
},
},
async handler(context, args) {
const conditions = [eq(incominginvoices.tenant, context.tenantId)]
const state = stringArg(args, "state")
if (state) conditions.push(eq(incominginvoices.state, state))
if (typeof args.paid === "boolean") conditions.push(eq(incominginvoices.paid, args.paid))
if (args.includeArchived !== true) conditions.push(eq(incominginvoices.archived, false))
const rows = await context.server.db
.select()
.from(incominginvoices)
.where(and(...conditions))
.orderBy(desc(incominginvoices.createdAt))
.limit(limitFromArgs(args))
return { rows }
},
},
{
name: "accounting.incoming_invoices.get",
title: "Eingangsrechnung laden",
description: "Lädt eine Eingangsrechnung des aktiven Mandanten anhand ihrer ID.",
requiredPermissions: ["accounting.incoming_invoices.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(incominginvoices)
.where(and(eq(incominginvoices.id, id), eq(incominginvoices.tenant, context.tenantId)))
.limit(1)
if (!rows[0]) throw new Error("Eingangsrechnung nicht gefunden")
return { invoice: rows[0] }
},
},
{
name: "accounting.incoming_invoices.files.list",
title: "Dateien eines Eingangsbelegs auflisten",
description: "Listet Dateien, die mit einem Eingangsbeleg im aktiven Mandanten verknüpft sind.",
requiredPermissions: ["accounting.incoming_invoices.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(files)
.where(and(
eq(files.incominginvoice, id),
eq(files.tenant, context.tenantId),
eq(files.archived, false)
))
.orderBy(desc(files.createdAt))
return { rows }
},
},
{
name: "accounting.incoming_invoices.validate",
title: "Eingangsbeleg validieren",
description: "Prüft einen Eingangsbeleg mit denselben Pflichtregeln wie die FEDEO-Oberfläche vor dem Buchen.",
requiredPermissions: ["accounting.incoming_invoices.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(incominginvoices)
.where(and(eq(incominginvoices.id, id), eq(incominginvoices.tenant, context.tenantId)))
.limit(1)
if (!rows[0]) throw new Error("Eingangsbeleg nicht gefunden")
return validateIncomingInvoiceData(rows[0] as Record<string, any>)
},
},
{
name: "accounting.incoming_invoices.create",
title: "Eingangsbeleg erstellen",
description: "Erstellt einen Eingangsbeleg-Entwurf im aktiven Mandanten.",
requiredPermissions: ["accounting.incoming_invoices.write"],
inputSchema: {
type: "object",
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 payload = buildIncomingInvoicePayload(args, context.userId, context.tenantId, true)
payload.state = "Entwurf"
const [created] = await context.server.db
.insert(incominginvoices)
.values(payload as any)
.returning()
return { invoice: created, validation: validateIncomingInvoiceData(created as Record<string, any>) }
},
},
{
name: "accounting.incoming_invoices.update",
title: "Eingangsbeleg bearbeiten",
description: "Bearbeitet einen noch nicht gebuchten Eingangsbeleg im aktiven Mandanten.",
requiredPermissions: ["accounting.incoming_invoices.write"],
inputSchema: {
type: "object",
required: ["id"],
properties: {
id: { type: "number" },
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 id = numberArg(args, "id")
if (!id) throw new Error("id ist erforderlich")
const [existing] = await context.server.db
.select()
.from(incominginvoices)
.where(and(eq(incominginvoices.id, id), eq(incominginvoices.tenant, context.tenantId)))
.limit(1)
if (!existing) throw new Error("Eingangsbeleg nicht gefunden")
if (existing.state === "Gebucht") throw new Error("Gebuchte Eingangsbelege können nicht über update geändert werden")
const payload = buildIncomingInvoicePayload(args, context.userId, context.tenantId)
payload.state = existing.state || "Entwurf"
const [updated] = await context.server.db
.update(incominginvoices)
.set(payload as any)
.where(and(eq(incominginvoices.id, id), eq(incominginvoices.tenant, context.tenantId)))
.returning()
return { invoice: updated, validation: validateIncomingInvoiceData(updated as Record<string, any>) }
},
},
{
name: "accounting.incoming_invoices.book",
title: "Eingangsbeleg buchen",
description: "Validiert und bucht einen vorbereiteten oder als Entwurf gespeicherten Eingangsbeleg.",
requiredPermissions: ["accounting.incoming_invoices.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 [existing] = await context.server.db
.select()
.from(incominginvoices)
.where(and(eq(incominginvoices.id, id), eq(incominginvoices.tenant, context.tenantId)))
.limit(1)
if (!existing) throw new Error("Eingangsbeleg nicht gefunden")
if (existing.state === "Gebucht") return { invoice: existing, validation: validateIncomingInvoiceData(existing as Record<string, any>) }
const validation = validateIncomingInvoiceData(existing as Record<string, any>)
if (!validation.valid) {
return {
booked: false,
invoice: existing,
validation,
}
}
const [updated] = await context.server.db
.update(incominginvoices)
.set({
state: "Gebucht",
updatedAt: new Date(),
updatedBy: context.userId,
})
.where(and(eq(incominginvoices.id, id), eq(incominginvoices.tenant, context.tenantId)))
.returning()
return {
booked: true,
invoice: updated,
validation: validateIncomingInvoiceData(updated as Record<string, any>),
}
},
},
{
name: "accounting.incoming_invoices.archive",
title: "Eingangsbeleg archivieren",
description: "Archiviert einen Eingangsbeleg im aktiven Mandanten.",
requiredPermissions: ["accounting.incoming_invoices.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(incominginvoices)
.set({
archived: true,
updatedAt: new Date(),
updatedBy: context.userId,
})
.where(and(eq(incominginvoices.id, id), eq(incominginvoices.tenant, context.tenantId)))
.returning()
if (!updated) throw new Error("Eingangsbeleg nicht gefunden")
return { invoice: updated }
},
},
{
name: "accounting.bank_statements.list",
title: "Bankumsätze auflisten",
description: "Listet Bankumsätze des aktiven Mandanten.",
requiredPermissions: ["accounting.bank.read"],
inputSchema: {
type: "object",
properties: {
account: { type: "number", description: "Optionale Bankkonto-ID." },
includeArchived: { type: "boolean", default: false },
limit: { type: "number", minimum: 1, maximum: 100 },
},
},
async handler(context, args) {
const conditions = [eq(bankstatements.tenant, context.tenantId)]
const account = numberArg(args, "account")
if (account) conditions.push(eq(bankstatements.account, account))
if (args.includeArchived !== true) conditions.push(eq(bankstatements.archived, false))
const rows = await context.server.db
.select()
.from(bankstatements)
.where(and(...conditions))
.orderBy(desc(bankstatements.date), desc(bankstatements.id))
.limit(limitFromArgs(args))
return { rows }
},
},
{
name: "accounting.bank_statements.get",
title: "Bankumsatz laden",
description: "Lädt einen Bankumsatz des aktiven Mandanten anhand seiner ID.",
requiredPermissions: ["accounting.bank.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(bankstatements)
.where(and(eq(bankstatements.id, id), eq(bankstatements.tenant, context.tenantId)))
.limit(1)
if (!rows[0]) throw new Error("Bankumsatz nicht gefunden")
return { bankStatement: rows[0] }
},
},
{
name: "accounting.statement_allocations.list",
title: "Buchungszuordnungen auflisten",
description: "Listet Buchungszuordnungen des aktiven Mandanten.",
requiredPermissions: ["accounting.statement_allocations.read"],
inputSchema: {
type: "object",
properties: {
bankstatement: { type: "number" },
incominginvoice: { type: "number" },
includeArchived: { type: "boolean", default: false },
limit: { type: "number", minimum: 1, maximum: 100 },
},
},
async handler(context, args) {
const conditions = [eq(statementallocations.tenant, context.tenantId)]
const bankstatement = numberArg(args, "bankstatement")
const incominginvoice = numberArg(args, "incominginvoice")
if (bankstatement) conditions.push(eq(statementallocations.bankstatement, bankstatement))
if (incominginvoice) conditions.push(eq(statementallocations.incominginvoice, incominginvoice))
if (args.includeArchived !== true) conditions.push(eq(statementallocations.archived, false))
const rows = await context.server.db
.select()
.from(statementallocations)
.where(and(...conditions))
.orderBy(desc(statementallocations.created_at))
.limit(limitFromArgs(args))
return { rows }
},
},
]