Wie im Belegeditor werden bei mode=normal/service fehlende Beschreibung, Name, Einheit, Preis, USt-Satz und Kostenstelle aus Artikel bzw. Leistung übernommen. descriptionText wird als description gespeichert, weil der Editor nur description liest. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1657 lines
70 KiB
TypeScript
1657 lines
70 KiB
TypeScript
import { outgoingAddressSchema, outgoingRowsSchema, outgoingReportSchema } from "../documentSchemas"
|
|
import { limitFromArgs, offsetFromArgs, paginatedResult, paginationProperties } from "../pagination"
|
|
import { and, desc, eq, ilike, inArray, or } from "drizzle-orm"
|
|
import { randomUUID } from "node:crypto"
|
|
import { validateBankAllocationCostCentre } from "../../modules/bank-allocation-details"
|
|
import {
|
|
accounts,
|
|
bankstatements,
|
|
createddocuments,
|
|
customers,
|
|
filetags,
|
|
files,
|
|
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 { updateOutgoingDocumentCostCentres } from "../../modules/outgoing-document-cost-centres.service"
|
|
import { updateIncomingInvoiceCostCentres } from "../../modules/incoming-invoice-cost-centres.service"
|
|
import { loadBankBalances } from "../../modules/bank-balances.service"
|
|
import { dateField } from "../masterdataFields"
|
|
import {
|
|
prepareStatementAllocationInput,
|
|
statementAllocationUuidArg,
|
|
} from "../statementAllocations"
|
|
import { McpContext, McpTool } from "../types"
|
|
import { applyOutgoingRowCatalogDefaults } from "../outgoingRowDefaults"
|
|
import { outgoingStatusTools } from "./outgoingStatus"
|
|
|
|
export { prepareStatementAllocationInput } from "../statementAllocations"
|
|
|
|
|
|
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 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",
|
|
"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)
|
|
? normalizeOutgoingDocumentRows(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 normalizeOutgoingDocumentRow = (row: unknown, index: number) => {
|
|
let normalizedRow = row
|
|
|
|
if (typeof normalizedRow === "string") {
|
|
try {
|
|
normalizedRow = JSON.parse(normalizedRow)
|
|
} catch {
|
|
throw new Error(`Position ${index + 1} ist kein gültiges JSON-Objekt`)
|
|
}
|
|
}
|
|
|
|
if (!normalizedRow || typeof normalizedRow !== "object" || Array.isArray(normalizedRow)) {
|
|
throw new Error(`Position ${index + 1} muss ein Objekt sein`)
|
|
}
|
|
|
|
const rowPayload = { ...(normalizedRow as Record<string, any>) }
|
|
|
|
rowPayload.id = rowPayload.id || randomUUID()
|
|
rowPayload.pos = rowPayload.pos || String(index + 1)
|
|
rowPayload.mode = rowPayload.mode || "free"
|
|
rowPayload.inputPrice = hasValidNumber(rowPayload.inputPrice)
|
|
? Number(rowPayload.inputPrice)
|
|
: hasValidNumber(rowPayload.price)
|
|
? Number(rowPayload.price)
|
|
: 0
|
|
rowPayload.price = hasValidNumber(rowPayload.price) ? Number(rowPayload.price) : rowPayload.inputPrice
|
|
rowPayload.quantity = hasValidNumber(rowPayload.quantity) ? Number(rowPayload.quantity) : 1
|
|
rowPayload.discountPercent = hasValidNumber(rowPayload.discountPercent) ? Number(rowPayload.discountPercent) : 0
|
|
rowPayload.linkedEntitys = Array.isArray(rowPayload.linkedEntitys) ? rowPayload.linkedEntitys : []
|
|
|
|
return rowPayload
|
|
}
|
|
|
|
const normalizeOutgoingDocumentRows = (rows: unknown) => {
|
|
if (!Array.isArray(rows)) return []
|
|
return rows.map((row, index) => normalizeOutgoingDocumentRow(row, index))
|
|
}
|
|
|
|
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 = normalizeOutgoingDocumentRows(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)
|
|
}
|
|
|
|
if (args.costcentre !== undefined) payload.costcentre = stringArg(args, "costcentre")
|
|
|
|
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 = normalizeOutgoingDocumentRows(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 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,
|
|
}
|
|
}
|
|
|
|
export type IncomingInvoiceFileUpload = {
|
|
filename: string
|
|
buffer: Buffer
|
|
mimeType: string
|
|
invoiceId?: number | null
|
|
invoiceArgs?: Record<string, unknown> | null
|
|
prepare?: boolean
|
|
}
|
|
|
|
/** Shared by the Base64 tool and the binary MCP upload endpoint. */
|
|
export async function uploadIncomingInvoiceFile(context: McpContext, upload: IncomingInvoiceFileUpload) {
|
|
const { filename, buffer, mimeType } = upload
|
|
const invoiceId = upload.invoiceId || null
|
|
const invoiceArgs = upload.invoiceArgs || null
|
|
const shouldPrepare = upload.prepare !== false && !invoiceId && !invoiceArgs
|
|
if (!buffer.length) throw new Error("Datei ist leer")
|
|
if (buffer.length > MAX_MCP_UPLOAD_BYTES) throw new Error("Datei ist größer als 20 MB")
|
|
|
|
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) {
|
|
// Only the uploaded file; other open invoice files stay for an explicit preparation run.
|
|
await context.server.services.prepareIncomingInvoices.run(context.tenantId, { fileIds: [saved.id] })
|
|
}
|
|
|
|
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),
|
|
}
|
|
}
|
|
|
|
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"),
|
|
}
|
|
}
|
|
|
|
const assertTenantEntityExists = async (
|
|
context: any,
|
|
table: any,
|
|
idColumn: any,
|
|
tenantColumn: any | null,
|
|
id: unknown,
|
|
label: string,
|
|
) => {
|
|
if (!hasValue(id)) return
|
|
|
|
const [row] = await context.server.db
|
|
.select({ id: idColumn })
|
|
.from(table)
|
|
.where(tenantColumn
|
|
? and(eq(idColumn, id as any), eq(tenantColumn, context.tenantId))
|
|
: eq(idColumn, id as any))
|
|
.limit(1)
|
|
|
|
if (!row) throw new Error(`${label} nicht gefunden`)
|
|
}
|
|
|
|
const assertStatementAllocationReferences = async (context: any, payload: Record<string, any>) => {
|
|
await Promise.all([
|
|
validateBankAllocationCostCentre(context.server.db, context.tenantId, payload.costcentre),
|
|
assertTenantEntityExists(context, bankstatements, bankstatements.id, bankstatements.tenant, payload.bankstatement, "Bankumsatz"),
|
|
assertTenantEntityExists(context, createddocuments, createddocuments.id, createddocuments.tenant, payload.createddocument, "Ausgangsbeleg"),
|
|
assertTenantEntityExists(context, incominginvoices, incominginvoices.id, incominginvoices.tenant, payload.incominginvoice, "Eingangsbeleg"),
|
|
assertTenantEntityExists(context, accounts, accounts.id, null, payload.account, "Sachkonto"),
|
|
assertTenantEntityExists(context, accounts, accounts.id, null, payload.contraAccount, "Haben-Sachkonto"),
|
|
assertTenantEntityExists(context, ownaccounts, ownaccounts.id, ownaccounts.tenant, payload.ownaccount, "Zusätzliches Konto"),
|
|
assertTenantEntityExists(context, ownaccounts, ownaccounts.id, ownaccounts.tenant, payload.contraOwnaccount, "Zusätzliches Haben-Konto"),
|
|
assertTenantEntityExists(context, customers, customers.id, customers.tenant, payload.customer, "Debitor"),
|
|
assertTenantEntityExists(context, customers, customers.id, customers.tenant, payload.contraCustomer, "Haben-Debitor"),
|
|
assertTenantEntityExists(context, vendors, vendors.id, vendors.tenant, payload.vendor, "Kreditor"),
|
|
assertTenantEntityExists(context, vendors, vendors.id, vendors.tenant, payload.contraVendor, "Haben-Kreditor"),
|
|
])
|
|
}
|
|
|
|
export const accountingTools: McpTool[] = [
|
|
...outgoingStatusTools,
|
|
{
|
|
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" },
|
|
...paginationProperties,
|
|
},
|
|
},
|
|
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))
|
|
conditions.push(eq(createddocuments.archived, false))
|
|
|
|
const rows = await context.server.db
|
|
.select()
|
|
.from(createddocuments)
|
|
.where(and(...conditions))
|
|
.orderBy(desc(createddocuments.createdAt), createddocuments.id)
|
|
.limit(limitFromArgs(args) + 1)
|
|
.offset(offsetFromArgs(args))
|
|
|
|
return paginatedResult(rows, args)
|
|
},
|
|
},
|
|
{
|
|
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), eq(createddocuments.archived, false)))
|
|
.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", enum: [...allowedOutgoingDocumentTypes] },
|
|
customer: { type: "number" },
|
|
contact: { type: "number" },
|
|
contract: { type: "number" },
|
|
project: { type: "number" },
|
|
costcentre: { type: "string", description: "Kostenstellen-UUID als Belegvorgabe." },
|
|
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: outgoingAddressSchema,
|
|
rows: outgoingRowsSchema,
|
|
letterhead: { type: "number" },
|
|
availableInPortal: { type: "boolean" },
|
|
customSurchargePercentage: { type: "number" },
|
|
report: outgoingReportSchema,
|
|
serialConfig: serialConfigSchema,
|
|
},
|
|
},
|
|
async handler(context, args) {
|
|
assertNoManualDocumentNumber(args)
|
|
if (args.rows !== undefined) args = { ...args, rows: await applyOutgoingRowCatalogDefaults(context, args.rows) }
|
|
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", enum: [...allowedOutgoingDocumentTypes] },
|
|
state: { type: "string" },
|
|
customer: { type: "number" },
|
|
contact: { type: "number" },
|
|
contract: { type: "number" },
|
|
project: { type: "number" },
|
|
costcentre: { type: "string", description: "Kostenstellen-UUID als Belegvorgabe." },
|
|
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: outgoingAddressSchema,
|
|
rows: outgoingRowsSchema,
|
|
letterhead: { type: "number" },
|
|
availableInPortal: { type: "boolean" },
|
|
customSurchargePercentage: { type: "number" },
|
|
report: outgoingReportSchema,
|
|
serialConfig: serialConfigSchema,
|
|
},
|
|
},
|
|
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")
|
|
|
|
if (args.rows !== undefined) args = { ...args, rows: await applyOutgoingRowCatalogDefaults(context, args.rows) }
|
|
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.cost_centres.update",
|
|
title: "Kostenstellen eines Ausgangsbelegs ändern",
|
|
description: "Ändert ausschließlich die Beleg- und Positionskostenstellen eines bereits fertiggestellten Ausgangsbelegs.",
|
|
requiredPermissions: ["accounting.outgoing_documents.write"],
|
|
inputSchema: {
|
|
type: "object",
|
|
required: ["id"],
|
|
anyOf: [
|
|
{ required: ["costcentre"] },
|
|
{ required: ["rowCostCentres"] },
|
|
],
|
|
properties: {
|
|
id: { type: "number" },
|
|
costcentre: { type: ["string", "null"], description: "UUID der Beleg-Kostenstelle oder null." },
|
|
rowCostCentres: {
|
|
type: "array",
|
|
items: {
|
|
type: "object",
|
|
required: ["rowId", "costcentre"],
|
|
properties: {
|
|
rowId: { type: "string", description: "ID der Belegposition." },
|
|
costcentre: { type: ["string", "null"], description: "UUID der Positionskostenstelle oder null." },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async handler(context, args) {
|
|
const id = numberArg(args, "id")
|
|
if (!id) throw new Error("id ist erforderlich")
|
|
|
|
const input: {
|
|
costcentre?: string | null
|
|
rowCostCentres?: Array<{ rowId: string, costcentre: string | null }>
|
|
} = {}
|
|
if (Object.prototype.hasOwnProperty.call(args, "costcentre")) {
|
|
const costcentre = args.costcentre
|
|
if (costcentre !== null && (typeof costcentre !== "string" || !UUID_PATTERN.test(costcentre))) {
|
|
throw new Error("costcentre muss eine gültige UUID oder null sein")
|
|
}
|
|
input.costcentre = costcentre as string | null
|
|
}
|
|
if (args.rowCostCentres !== undefined) {
|
|
if (!Array.isArray(args.rowCostCentres)) throw new Error("rowCostCentres muss ein Array sein")
|
|
input.rowCostCentres = args.rowCostCentres.map((assignment: any) => {
|
|
const rowId = typeof assignment?.rowId === "string" ? assignment.rowId.trim() : ""
|
|
const costcentre = assignment?.costcentre
|
|
if (!rowId) throw new Error("Jede Positionszuordnung benötigt eine rowId")
|
|
if (costcentre !== null && (typeof costcentre !== "string" || !UUID_PATTERN.test(costcentre))) {
|
|
throw new Error(`Ungültige Kostenstelle für Position ${rowId}`)
|
|
}
|
|
return { rowId, costcentre }
|
|
})
|
|
}
|
|
|
|
const document = await updateOutgoingDocumentCostCentres(
|
|
context.server,
|
|
context.tenantId,
|
|
context.userId,
|
|
id,
|
|
input,
|
|
)
|
|
return { document }
|
|
},
|
|
},
|
|
{
|
|
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: outgoingRowsSchema,
|
|
report: outgoingReportSchema,
|
|
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")
|
|
|
|
if (args.rows !== undefined) args = { ...args, rows: await applyOutgoingRowCatalogDefaults(context, args.rows) }
|
|
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.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: ["generating", "draft", "processing", "cancelling", "cancelled", "completed", "error"] },
|
|
...paginationProperties,
|
|
},
|
|
},
|
|
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), serialExecutions.id)
|
|
.limit(limitFromArgs(args) + 1)
|
|
.offset(offsetFromArgs(args))
|
|
|
|
return paginatedResult(rows, args)
|
|
},
|
|
},
|
|
{
|
|
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",
|
|
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." },
|
|
...paginationProperties,
|
|
},
|
|
},
|
|
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, accounts.id)
|
|
.limit(limit + 1)
|
|
.offset(offsetFromArgs(args))
|
|
|
|
return { accountChart, ...paginatedResult(rows, args) }
|
|
},
|
|
},
|
|
{
|
|
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." },
|
|
...paginationProperties,
|
|
},
|
|
},
|
|
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))
|
|
conditions.push(eq(incominginvoices.archived, false))
|
|
|
|
const rows = await context.server.db
|
|
.select()
|
|
.from(incominginvoices)
|
|
.where(and(...conditions))
|
|
.orderBy(desc(incominginvoices.createdAt), incominginvoices.id)
|
|
.limit(limitFromArgs(args) + 1)
|
|
.offset(offsetFromArgs(args))
|
|
|
|
return paginatedResult(rows, args)
|
|
},
|
|
},
|
|
{
|
|
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), eq(incominginvoices.archived, false)))
|
|
.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" },
|
|
...paginationProperties,
|
|
},
|
|
},
|
|
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), files.id)
|
|
.limit(limitFromArgs(args) + 1)
|
|
.offset(offsetFromArgs(args))
|
|
|
|
return paginatedResult(rows, args)
|
|
},
|
|
},
|
|
{
|
|
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 dieser Datei an. Mit Dateizugriff besser ohne Base64 über die Adresse aus documents.files.upload_url (target=incoming_invoice) hochladen.",
|
|
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")
|
|
|
|
return uploadIncomingInvoiceFile(context, {
|
|
filename,
|
|
buffer: base64BufferArg(args, "contentBase64"),
|
|
mimeType: stringArg(args, "mimeType") || "application/octet-stream",
|
|
invoiceId: numberArg(args, "invoiceId"),
|
|
invoiceArgs: optionalObjectArg(args, "invoice") as Record<string, unknown> | null,
|
|
prepare: args.prepare !== false,
|
|
})
|
|
},
|
|
},
|
|
{
|
|
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.prepare",
|
|
title: "Belegvorbereitung starten",
|
|
description: "Startet die Belegvorbereitung wie der Button „Belege vorbereiten“: Dateien vom Typ Eingangsrechnung ohne verknüpften Beleg werden ausgelesen (strukturierte E-Rechnung, sonst KI) und als Eingangsbeleg-Entwurf angelegt. Optional nur für bestimmte Datei-IDs. Kann je Datei einige Sekunden dauern.",
|
|
requiredPermissions: ["accounting.incoming_invoices.write"],
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
fileIds: {
|
|
type: "array",
|
|
items: { type: "string", format: "uuid" },
|
|
minItems: 1,
|
|
maxItems: 50,
|
|
description: "Optionale Datei-IDs; ohne Angabe alle offenen Eingangsrechnungsdateien.",
|
|
},
|
|
},
|
|
additionalProperties: false,
|
|
},
|
|
async handler(context, args) {
|
|
let fileIds: string[] | undefined
|
|
if (args.fileIds !== undefined) {
|
|
if (!Array.isArray(args.fileIds) || !args.fileIds.length || args.fileIds.length > 50) {
|
|
throw new Error("fileIds muss 1 bis 50 Datei-IDs enthalten")
|
|
}
|
|
if (args.fileIds.some((id) => typeof id !== "string" || !UUID_PATTERN.test(id))) {
|
|
throw new Error("fileIds darf nur gültige UUIDs enthalten")
|
|
}
|
|
fileIds = [...new Set((args.fileIds as string[]).map((id) => id.toLowerCase()))]
|
|
}
|
|
|
|
const result = await context.server.services.prepareIncomingInvoices.run(context.tenantId, { fileIds })
|
|
if (result.error && !result.filesFound) throw new Error(result.error)
|
|
|
|
const invoiceIds = result.created.map((entry) => entry.invoiceId)
|
|
const invoices = invoiceIds.length
|
|
? await context.server.db
|
|
.select()
|
|
.from(incominginvoices)
|
|
.where(and(eq(incominginvoices.tenant, context.tenantId), inArray(incominginvoices.id, invoiceIds)))
|
|
: []
|
|
const handled = new Set([...result.created, ...result.skipped].map((entry) => entry.fileId))
|
|
|
|
return {
|
|
filesFound: result.filesFound,
|
|
created: result.created.map((entry) => {
|
|
const invoice = invoices.find((row) => row.id === entry.invoiceId) || null
|
|
return { ...entry, invoice, validation: invoice ? validateIncomingInvoiceData(invoice as Record<string, any>) : null }
|
|
}),
|
|
skipped: [
|
|
...result.skipped,
|
|
...(fileIds || []).filter((id) => !handled.has(id))
|
|
.map((fileId) => ({ fileId, reason: "Keine offene Eingangsrechnungsdatei (nicht gefunden, archiviert, anderer Dateityp oder bereits vorbereitet)" })),
|
|
],
|
|
}
|
|
},
|
|
},
|
|
{
|
|
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.cost_centres.update",
|
|
title: "Kostenstellen eines Eingangsbelegs ändern",
|
|
description: "Ändert ausschließlich die Positionskostenstellen eines Eingangsbelegs, auch wenn er gebucht oder einer Bankbuchung zugewiesen ist.",
|
|
requiredPermissions: ["accounting.incoming_invoices.write"],
|
|
inputSchema: {
|
|
type: "object",
|
|
required: ["id", "accountCostCentres"],
|
|
properties: {
|
|
id: { type: "number" },
|
|
accountCostCentres: {
|
|
type: "array",
|
|
minItems: 1,
|
|
items: {
|
|
type: "object",
|
|
required: ["index", "costcentre"],
|
|
properties: {
|
|
index: { type: "integer", minimum: 0, description: "Nullbasierter Index der Position in accounts." },
|
|
costcentre: { type: ["string", "null"], description: "UUID der Positionskostenstelle oder null." },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async handler(context, args) {
|
|
const id = numberArg(args, "id")
|
|
if (!id) throw new Error("id ist erforderlich")
|
|
|
|
const invoice = await updateIncomingInvoiceCostCentres(
|
|
context.server,
|
|
context.tenantId,
|
|
context.userId,
|
|
id,
|
|
{ accountCostCentres: args.accountCostCentres as any },
|
|
)
|
|
return { invoice }
|
|
},
|
|
},
|
|
{
|
|
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_accounts.balances",
|
|
title: "Kontostände abfragen",
|
|
description: "Liefert die Kontostände der Bankkonten und Kassenbücher des aktiven Mandanten samt Summe. Bankkonten zeigen den zuletzt synchronisierten Buchungssaldo; mit asOf wird der Stand zu einem vergangenen Datum aus den Bankumsätzen zurückgerechnet. Kassenbücher ergeben sich aus Anfangsbestand und Buchungen.",
|
|
requiredPermissions: ["accounting.bank.read"],
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
account: { type: "number", description: "Optionale Bankkonto-ID; ohne Angabe alle Konten." },
|
|
asOf: { ...dateField.schema, description: "Optionaler Stichtag YYYY-MM-DD (Stand zum Tagesende)." },
|
|
},
|
|
},
|
|
async handler(context, args) {
|
|
const account = numberArg(args, "account")
|
|
const asOf = args.asOf === undefined || args.asOf === null ? null : dateField.parse(args.asOf, "asOf")
|
|
const result = await loadBankBalances(context.server, context.tenantId, { account, asOf })
|
|
if (account && !result.accounts.length) throw new Error("Bankkonto nicht gefunden")
|
|
return result
|
|
},
|
|
},
|
|
{
|
|
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." },
|
|
...paginationProperties,
|
|
},
|
|
},
|
|
async handler(context, args) {
|
|
const conditions = [eq(bankstatements.tenant, context.tenantId)]
|
|
const account = numberArg(args, "account")
|
|
|
|
if (account) conditions.push(eq(bankstatements.account, account))
|
|
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) + 1)
|
|
.offset(offsetFromArgs(args))
|
|
|
|
return paginatedResult(rows, args)
|
|
},
|
|
},
|
|
{
|
|
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), eq(bankstatements.archived, false)))
|
|
.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" },
|
|
...paginationProperties,
|
|
},
|
|
},
|
|
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))
|
|
conditions.push(eq(statementallocations.archived, false))
|
|
|
|
const rows = await context.server.db
|
|
.select()
|
|
.from(statementallocations)
|
|
.where(and(...conditions))
|
|
.orderBy(desc(statementallocations.created_at), statementallocations.id)
|
|
.limit(limitFromArgs(args) + 1)
|
|
.offset(offsetFromArgs(args))
|
|
|
|
return paginatedResult(rows, args)
|
|
},
|
|
},
|
|
{
|
|
name: "accounting.statement_allocations.create",
|
|
title: "Bankzuweisung erstellen",
|
|
description: "Erstellt eine Bankzuweisung oder manuelle Soll/Haben-Buchung. Unterstützt Ausgangs- und Eingangsbelege, Sachkonten, zusätzliche Konten, Debitoren, Kreditoren, Teilbeträge, DATEV-Steuerschlüssel und Abschreibungen.",
|
|
requiredPermissions: ["accounting.statement_allocations.write"],
|
|
inputSchema: {
|
|
type: "object",
|
|
required: ["amount"],
|
|
properties: {
|
|
bankstatement: { type: "number", description: "Bankumsatz-ID. Weglassen für eine manuelle Soll/Haben-Buchung." },
|
|
amount: { type: "number", description: "Zuweisungsbetrag mit Vorzeichen des Bankumsatzes; bei manuellen Buchungen positiv." },
|
|
createddocument: { type: "number", description: "Ausgangsbeleg-ID." },
|
|
incominginvoice: { type: "number", description: "Eingangsbeleg-ID." },
|
|
account: { type: "number", description: "Sachkonto-ID beziehungsweise Soll-Sachkonto." },
|
|
ownaccount: { type: "string", description: "UUID eines zusätzlichen Kontos beziehungsweise Soll-Kontos." },
|
|
customer: { type: "number", description: "Debitor-ID beziehungsweise Soll-Debitor." },
|
|
vendor: { type: "number", description: "Kreditor-ID beziehungsweise Soll-Kreditor." },
|
|
contraAccount: { type: "number", description: "Haben-Sachkonto für manuelle Buchungen." },
|
|
contraOwnaccount: { type: "string", description: "UUID eines zusätzlichen Haben-Kontos." },
|
|
contraCustomer: { type: "number", description: "Haben-Debitor für manuelle Buchungen." },
|
|
contraVendor: { type: "number", description: "Haben-Kreditor für manuelle Buchungen." },
|
|
manualBookingDate: { type: "string", format: "date", description: "Buchungsdatum einer manuellen Buchung." },
|
|
manualInvoiceSide: { type: "string", enum: ["debit", "credit"], description: "Soll/Haben-Seite eines Eingangsbelegs in einer manuellen Buchung." },
|
|
description: { type: "string" },
|
|
datevTaxKey: { type: "string", description: "DATEV-Steuerschlüssel auf den Bruttobetrag: 9/8 für 19/7 % Vorsteuer, 3/2 für 19/7 % Umsatzsteuer, 40 ohne Steuer (Automatik aufheben)." },
|
|
costcentre: { type: ["string", "null"], description: "Kostenstellen-UUID für direkte Bankbuchungen auf Sachkonten oder zusätzliche Konten." },
|
|
bookingMode: { type: "string", enum: ["expense", "depreciation_single", "depreciation_bundle"], default: "expense" },
|
|
depreciationMonths: { type: "number", minimum: 1 },
|
|
depreciationStartDate: { type: "string", format: "date" },
|
|
depreciationMethod: { type: "string", enum: ["linear", "degressive"], default: "linear" },
|
|
depreciationLabel: { type: "string" },
|
|
depreciationGroup: { type: "string" },
|
|
residualValue: { type: "number", minimum: 0, default: 0 },
|
|
},
|
|
},
|
|
async handler(context, args) {
|
|
const payload = prepareStatementAllocationInput(args)
|
|
await assertStatementAllocationReferences(context, payload)
|
|
|
|
const [created] = await context.server.db
|
|
.insert(statementallocations)
|
|
.values({
|
|
...payload,
|
|
tenant: context.tenantId,
|
|
updated_at: new Date(),
|
|
updated_by: context.userId,
|
|
})
|
|
.returning()
|
|
|
|
if (created?.bankstatement) {
|
|
await insertHistoryItem(context.server, {
|
|
entity: "bankstatements",
|
|
entityId: Number(created.bankstatement),
|
|
action: "created",
|
|
created_by: context.userId,
|
|
tenant_id: context.tenantId,
|
|
oldVal: null,
|
|
newVal: created,
|
|
text: "Buchung über MCP erstellt",
|
|
})
|
|
}
|
|
|
|
return { allocation: created }
|
|
},
|
|
},
|
|
{
|
|
name: "accounting.statement_allocations.delete",
|
|
title: "Bankzuweisung löschen",
|
|
description: "Löscht eine Bankzuweisung oder manuelle Soll/Haben-Buchung im aktiven Mandanten.",
|
|
requiredPermissions: ["accounting.statement_allocations.write"],
|
|
inputSchema: {
|
|
type: "object",
|
|
required: ["id"],
|
|
properties: {
|
|
id: { type: "string", description: "UUID der Bankzuweisung." },
|
|
},
|
|
},
|
|
async handler(context, args) {
|
|
const id = statementAllocationUuidArg(args, "id")
|
|
if (!id) throw new Error("id ist erforderlich")
|
|
|
|
const [existing] = await context.server.db
|
|
.select()
|
|
.from(statementallocations)
|
|
.where(and(eq(statementallocations.id, id), eq(statementallocations.tenant, context.tenantId)))
|
|
.limit(1)
|
|
|
|
if (!existing) throw new Error("Bankzuweisung nicht gefunden")
|
|
|
|
const [deleted] = await context.server.db
|
|
.delete(statementallocations)
|
|
.where(and(eq(statementallocations.id, id), eq(statementallocations.tenant, context.tenantId)))
|
|
.returning()
|
|
|
|
if (existing.bankstatement) {
|
|
await insertHistoryItem(context.server, {
|
|
entity: "bankstatements",
|
|
entityId: Number(existing.bankstatement),
|
|
action: "deleted",
|
|
created_by: context.userId,
|
|
tenant_id: context.tenantId,
|
|
oldVal: existing,
|
|
newVal: null,
|
|
text: "Buchung über MCP gelöscht",
|
|
})
|
|
}
|
|
|
|
return { deleted: true, allocation: deleted }
|
|
},
|
|
},
|
|
]
|