Compare commits
4 Commits
a6f55b61e4
...
a50ff256ee
| Author | SHA1 | Date | |
|---|---|---|---|
| a50ff256ee | |||
| ebbd1e0cd0 | |||
| 2ec162c0e7 | |||
| d0bc8a5e0f |
@@ -1,16 +1,42 @@
|
|||||||
import { McpToolResult } from "./types"
|
import { McpToolResult } from "./types"
|
||||||
|
|
||||||
|
const OMIT_ARCHIVED = Symbol("omit-archived")
|
||||||
|
|
||||||
|
function omitArchivedRecords(value: unknown): unknown | typeof OMIT_ARCHIVED {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value
|
||||||
|
.map(omitArchivedRecords)
|
||||||
|
.filter((item) => item !== OMIT_ARCHIVED)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!value || typeof value !== "object") return value
|
||||||
|
|
||||||
|
const prototype = Object.getPrototypeOf(value)
|
||||||
|
if (prototype !== Object.prototype && prototype !== null) return value
|
||||||
|
|
||||||
|
const record = value as Record<string, unknown>
|
||||||
|
if (record.archived === true) return OMIT_ARCHIVED
|
||||||
|
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(record)
|
||||||
|
.map(([key, item]) => [key, omitArchivedRecords(item)] as const)
|
||||||
|
.filter(([, item]) => item !== OMIT_ARCHIVED),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function asToolResult(payload: unknown): McpToolResult {
|
export function asToolResult(payload: unknown): McpToolResult {
|
||||||
|
const sanitizedPayload = omitArchivedRecords(payload)
|
||||||
|
const resultPayload = sanitizedPayload === OMIT_ARCHIVED ? {} : sanitizedPayload
|
||||||
const structuredContent =
|
const structuredContent =
|
||||||
payload && typeof payload === "object" && !Array.isArray(payload)
|
resultPayload && typeof resultPayload === "object" && !Array.isArray(resultPayload)
|
||||||
? payload as Record<string, unknown>
|
? resultPayload as Record<string, unknown>
|
||||||
: { result: payload }
|
: { result: resultPayload }
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: JSON.stringify(payload, null, 2),
|
text: JSON.stringify(resultPayload, null, 2),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
structuredContent,
|
structuredContent,
|
||||||
@@ -33,4 +59,3 @@ export function asToolError(error: unknown): McpToolResult {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,12 +10,15 @@ import {
|
|||||||
folders,
|
folders,
|
||||||
incominginvoices,
|
incominginvoices,
|
||||||
ownaccounts,
|
ownaccounts,
|
||||||
|
serialExecutions,
|
||||||
statementallocations,
|
statementallocations,
|
||||||
vendors,
|
vendors,
|
||||||
} from "../../../db/schema"
|
} from "../../../db/schema"
|
||||||
import { useNextNumberRangeNumber } from "../../utils/functions"
|
import { useNextNumberRangeNumber } from "../../utils/functions"
|
||||||
import { saveFile } from "../../utils/files"
|
import { saveFile } from "../../utils/files"
|
||||||
import { insertHistoryItem } from "../../utils/history"
|
import { insertHistoryItem } from "../../utils/history"
|
||||||
|
import { executeManualGeneration, finishManualGeneration } from "../../modules/serialexecution.service"
|
||||||
|
import { updateOutgoingDocumentCostCentres } from "../../modules/outgoing-document-cost-centres.service"
|
||||||
import {
|
import {
|
||||||
prepareStatementAllocationInput,
|
prepareStatementAllocationInput,
|
||||||
statementAllocationUuidArg,
|
statementAllocationUuidArg,
|
||||||
@@ -43,6 +46,22 @@ const numberArg = (args: Record<string, unknown>, key: string) => {
|
|||||||
const hasValue = (value: unknown) => value !== null && value !== undefined && value !== ""
|
const hasValue = (value: unknown) => value !== null && value !== undefined && value !== ""
|
||||||
const hasValidNumber = (value: unknown) => hasValue(value) && Number.isFinite(Number(value))
|
const hasValidNumber = (value: unknown) => hasValue(value) && Number.isFinite(Number(value))
|
||||||
const MAX_MCP_UPLOAD_BYTES = 20 * 1024 * 1024
|
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([
|
const allowedOutgoingDocumentTypes = new Set([
|
||||||
"quotes",
|
"quotes",
|
||||||
@@ -437,7 +456,6 @@ export const accountingTools: McpTool[] = [
|
|||||||
state: { type: "string", description: "Optionaler Statusfilter, z. B. Entwurf oder Gebucht." },
|
state: { type: "string", description: "Optionaler Statusfilter, z. B. Entwurf oder Gebucht." },
|
||||||
customer: { type: "number" },
|
customer: { type: "number" },
|
||||||
project: { type: "number" },
|
project: { type: "number" },
|
||||||
includeArchived: { type: "boolean", default: false },
|
|
||||||
limit: { type: "number", minimum: 1, maximum: 100 },
|
limit: { type: "number", minimum: 1, maximum: 100 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -455,7 +473,7 @@ export const accountingTools: McpTool[] = [
|
|||||||
if (state) conditions.push(eq(createddocuments.state, state))
|
if (state) conditions.push(eq(createddocuments.state, state))
|
||||||
if (customer) conditions.push(eq(createddocuments.customer, customer))
|
if (customer) conditions.push(eq(createddocuments.customer, customer))
|
||||||
if (project) conditions.push(eq(createddocuments.project, project))
|
if (project) conditions.push(eq(createddocuments.project, project))
|
||||||
if (args.includeArchived !== true) conditions.push(eq(createddocuments.archived, false))
|
conditions.push(eq(createddocuments.archived, false))
|
||||||
|
|
||||||
const rows = await context.server.db
|
const rows = await context.server.db
|
||||||
.select()
|
.select()
|
||||||
@@ -486,7 +504,7 @@ export const accountingTools: McpTool[] = [
|
|||||||
const rows = await context.server.db
|
const rows = await context.server.db
|
||||||
.select()
|
.select()
|
||||||
.from(createddocuments)
|
.from(createddocuments)
|
||||||
.where(and(eq(createddocuments.id, id), eq(createddocuments.tenant, context.tenantId)))
|
.where(and(eq(createddocuments.id, id), eq(createddocuments.tenant, context.tenantId), eq(createddocuments.archived, false)))
|
||||||
.limit(1)
|
.limit(1)
|
||||||
|
|
||||||
if (!rows[0]) throw new Error("Ausgangsbeleg nicht gefunden")
|
if (!rows[0]) throw new Error("Ausgangsbeleg nicht gefunden")
|
||||||
@@ -502,7 +520,7 @@ export const accountingTools: McpTool[] = [
|
|||||||
type: "object",
|
type: "object",
|
||||||
required: ["type"],
|
required: ["type"],
|
||||||
properties: {
|
properties: {
|
||||||
type: { type: "string" },
|
type: { type: "string", enum: [...allowedOutgoingDocumentTypes] },
|
||||||
customer: { type: "number" },
|
customer: { type: "number" },
|
||||||
contact: { type: "number" },
|
contact: { type: "number" },
|
||||||
contract: { type: "number" },
|
contract: { type: "number" },
|
||||||
@@ -530,6 +548,7 @@ export const accountingTools: McpTool[] = [
|
|||||||
availableInPortal: { type: "boolean" },
|
availableInPortal: { type: "boolean" },
|
||||||
customSurchargePercentage: { type: "number" },
|
customSurchargePercentage: { type: "number" },
|
||||||
report: { type: "object" },
|
report: { type: "object" },
|
||||||
|
serialConfig: serialConfigSchema,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async handler(context, args) {
|
async handler(context, args) {
|
||||||
@@ -556,7 +575,7 @@ export const accountingTools: McpTool[] = [
|
|||||||
required: ["id"],
|
required: ["id"],
|
||||||
properties: {
|
properties: {
|
||||||
id: { type: "number" },
|
id: { type: "number" },
|
||||||
type: { type: "string" },
|
type: { type: "string", enum: [...allowedOutgoingDocumentTypes] },
|
||||||
state: { type: "string" },
|
state: { type: "string" },
|
||||||
customer: { type: "number" },
|
customer: { type: "number" },
|
||||||
contact: { type: "number" },
|
contact: { type: "number" },
|
||||||
@@ -585,6 +604,7 @@ export const accountingTools: McpTool[] = [
|
|||||||
availableInPortal: { type: "boolean" },
|
availableInPortal: { type: "boolean" },
|
||||||
customSurchargePercentage: { type: "number" },
|
customSurchargePercentage: { type: "number" },
|
||||||
report: { type: "object" },
|
report: { type: "object" },
|
||||||
|
serialConfig: serialConfigSchema,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async handler(context, args) {
|
async handler(context, args) {
|
||||||
@@ -614,6 +634,72 @@ export const accountingTools: McpTool[] = [
|
|||||||
return { document: updated }
|
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",
|
name: "accounting.outgoing_documents.finalize",
|
||||||
title: "Ausgangsbeleg finalisieren",
|
title: "Ausgangsbeleg finalisieren",
|
||||||
@@ -669,6 +755,90 @@ export const accountingTools: McpTool[] = [
|
|||||||
return { document: updated }
|
return { document: updated }
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "accounting.serial_invoices.execute",
|
||||||
|
title: "Serienrechnungslauf starten",
|
||||||
|
description: "Erzeugt aus ausgewählten aktiven Serienrechnungsvorlagen einen neuen Rechnungslauf.",
|
||||||
|
requiredPermissions: ["accounting.outgoing_documents.write"],
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
required: ["executionDate", "templateIds"],
|
||||||
|
properties: {
|
||||||
|
executionDate: { type: "string", description: "Ausführungsdatum als ISO-8601-Wert." },
|
||||||
|
templateIds: {
|
||||||
|
type: "array",
|
||||||
|
minItems: 1,
|
||||||
|
uniqueItems: true,
|
||||||
|
items: { type: "number" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async handler(context, args) {
|
||||||
|
const executionDate = stringArg(args, "executionDate")
|
||||||
|
const parsedExecutionDate = executionDate ? new Date(executionDate) : null
|
||||||
|
const templateIds = Array.isArray(args.templateIds)
|
||||||
|
? [...new Set(args.templateIds.map(Number).filter((id) => Number.isFinite(id) && id > 0))]
|
||||||
|
: []
|
||||||
|
|
||||||
|
if (!parsedExecutionDate || Number.isNaN(parsedExecutionDate.getTime())) {
|
||||||
|
throw new Error("executionDate muss ein gültiger ISO-8601-Wert sein")
|
||||||
|
}
|
||||||
|
if (!templateIds.length) throw new Error("templateIds muss mindestens eine gültige ID enthalten")
|
||||||
|
|
||||||
|
return executeManualGeneration(
|
||||||
|
context.server,
|
||||||
|
parsedExecutionDate,
|
||||||
|
templateIds,
|
||||||
|
context.tenantId,
|
||||||
|
context.userId,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "accounting.serial_invoice_executions.list",
|
||||||
|
title: "Serienrechnungsläufe auflisten",
|
||||||
|
description: "Listet die zuletzt gestarteten Serienrechnungsläufe des aktiven Mandanten.",
|
||||||
|
requiredPermissions: ["accounting.outgoing_documents.read"],
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
status: { type: "string", enum: ["draft", "completed", "error"] },
|
||||||
|
limit: { type: "number", minimum: 1, maximum: 100 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async handler(context, args) {
|
||||||
|
const conditions = [eq(serialExecutions.tenant, context.tenantId)]
|
||||||
|
const status = stringArg(args, "status")
|
||||||
|
if (status) conditions.push(eq(serialExecutions.status, status))
|
||||||
|
|
||||||
|
const rows = await context.server.db
|
||||||
|
.select()
|
||||||
|
.from(serialExecutions)
|
||||||
|
.where(and(...conditions))
|
||||||
|
.orderBy(desc(serialExecutions.createdAt))
|
||||||
|
.limit(limitFromArgs(args))
|
||||||
|
|
||||||
|
return { rows }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "accounting.serial_invoice_executions.finish",
|
||||||
|
title: "Serienrechnungslauf abschließen",
|
||||||
|
description: "Finalisiert die erzeugten Rechnungen eines Serienrechnungslaufs und schließt den Lauf ab.",
|
||||||
|
requiredPermissions: ["accounting.outgoing_documents.write"],
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
required: ["id"],
|
||||||
|
properties: {
|
||||||
|
id: { type: "string", description: "UUID des Serienrechnungslaufs." },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async handler(context, args) {
|
||||||
|
const id = stringArg(args, "id")
|
||||||
|
if (!id || !UUID_PATTERN.test(id)) throw new Error("id muss eine gültige UUID sein")
|
||||||
|
return finishManualGeneration(context.server, id, context.tenantId)
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "accounting.outgoing_documents.archive",
|
name: "accounting.outgoing_documents.archive",
|
||||||
title: "Ausgangsbeleg archivieren",
|
title: "Ausgangsbeleg archivieren",
|
||||||
@@ -754,7 +924,6 @@ export const accountingTools: McpTool[] = [
|
|||||||
properties: {
|
properties: {
|
||||||
state: { type: "string", description: "Optionaler Statusfilter." },
|
state: { type: "string", description: "Optionaler Statusfilter." },
|
||||||
paid: { type: "boolean", description: "Optionaler Zahlungsstatus." },
|
paid: { type: "boolean", description: "Optionaler Zahlungsstatus." },
|
||||||
includeArchived: { type: "boolean", default: false },
|
|
||||||
limit: { type: "number", minimum: 1, maximum: 100 },
|
limit: { type: "number", minimum: 1, maximum: 100 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -764,7 +933,7 @@ export const accountingTools: McpTool[] = [
|
|||||||
|
|
||||||
if (state) conditions.push(eq(incominginvoices.state, state))
|
if (state) conditions.push(eq(incominginvoices.state, state))
|
||||||
if (typeof args.paid === "boolean") conditions.push(eq(incominginvoices.paid, args.paid))
|
if (typeof args.paid === "boolean") conditions.push(eq(incominginvoices.paid, args.paid))
|
||||||
if (args.includeArchived !== true) conditions.push(eq(incominginvoices.archived, false))
|
conditions.push(eq(incominginvoices.archived, false))
|
||||||
|
|
||||||
const rows = await context.server.db
|
const rows = await context.server.db
|
||||||
.select()
|
.select()
|
||||||
@@ -795,7 +964,7 @@ export const accountingTools: McpTool[] = [
|
|||||||
const rows = await context.server.db
|
const rows = await context.server.db
|
||||||
.select()
|
.select()
|
||||||
.from(incominginvoices)
|
.from(incominginvoices)
|
||||||
.where(and(eq(incominginvoices.id, id), eq(incominginvoices.tenant, context.tenantId)))
|
.where(and(eq(incominginvoices.id, id), eq(incominginvoices.tenant, context.tenantId), eq(incominginvoices.archived, false)))
|
||||||
.limit(1)
|
.limit(1)
|
||||||
|
|
||||||
if (!rows[0]) throw new Error("Eingangsrechnung nicht gefunden")
|
if (!rows[0]) throw new Error("Eingangsrechnung nicht gefunden")
|
||||||
@@ -1142,7 +1311,6 @@ export const accountingTools: McpTool[] = [
|
|||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
account: { type: "number", description: "Optionale Bankkonto-ID." },
|
account: { type: "number", description: "Optionale Bankkonto-ID." },
|
||||||
includeArchived: { type: "boolean", default: false },
|
|
||||||
limit: { type: "number", minimum: 1, maximum: 100 },
|
limit: { type: "number", minimum: 1, maximum: 100 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1151,7 +1319,7 @@ export const accountingTools: McpTool[] = [
|
|||||||
const account = numberArg(args, "account")
|
const account = numberArg(args, "account")
|
||||||
|
|
||||||
if (account) conditions.push(eq(bankstatements.account, account))
|
if (account) conditions.push(eq(bankstatements.account, account))
|
||||||
if (args.includeArchived !== true) conditions.push(eq(bankstatements.archived, false))
|
conditions.push(eq(bankstatements.archived, false))
|
||||||
|
|
||||||
const rows = await context.server.db
|
const rows = await context.server.db
|
||||||
.select()
|
.select()
|
||||||
@@ -1182,7 +1350,7 @@ export const accountingTools: McpTool[] = [
|
|||||||
const rows = await context.server.db
|
const rows = await context.server.db
|
||||||
.select()
|
.select()
|
||||||
.from(bankstatements)
|
.from(bankstatements)
|
||||||
.where(and(eq(bankstatements.id, id), eq(bankstatements.tenant, context.tenantId)))
|
.where(and(eq(bankstatements.id, id), eq(bankstatements.tenant, context.tenantId), eq(bankstatements.archived, false)))
|
||||||
.limit(1)
|
.limit(1)
|
||||||
|
|
||||||
if (!rows[0]) throw new Error("Bankumsatz nicht gefunden")
|
if (!rows[0]) throw new Error("Bankumsatz nicht gefunden")
|
||||||
@@ -1199,7 +1367,6 @@ export const accountingTools: McpTool[] = [
|
|||||||
properties: {
|
properties: {
|
||||||
bankstatement: { type: "number" },
|
bankstatement: { type: "number" },
|
||||||
incominginvoice: { type: "number" },
|
incominginvoice: { type: "number" },
|
||||||
includeArchived: { type: "boolean", default: false },
|
|
||||||
limit: { type: "number", minimum: 1, maximum: 100 },
|
limit: { type: "number", minimum: 1, maximum: 100 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1210,7 +1377,7 @@ export const accountingTools: McpTool[] = [
|
|||||||
|
|
||||||
if (bankstatement) conditions.push(eq(statementallocations.bankstatement, bankstatement))
|
if (bankstatement) conditions.push(eq(statementallocations.bankstatement, bankstatement))
|
||||||
if (incominginvoice) conditions.push(eq(statementallocations.incominginvoice, incominginvoice))
|
if (incominginvoice) conditions.push(eq(statementallocations.incominginvoice, incominginvoice))
|
||||||
if (args.includeArchived !== true) conditions.push(eq(statementallocations.archived, false))
|
conditions.push(eq(statementallocations.archived, false))
|
||||||
|
|
||||||
const rows = await context.server.db
|
const rows = await context.server.db
|
||||||
.select()
|
.select()
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export const masterdataTools: McpTool[] = [
|
|||||||
const rows = await context.server.db
|
const rows = await context.server.db
|
||||||
.select()
|
.select()
|
||||||
.from(customers)
|
.from(customers)
|
||||||
.where(and(eq(customers.id, id), eq(customers.tenant, context.tenantId)))
|
.where(and(eq(customers.id, id), eq(customers.tenant, context.tenantId), eq(customers.archived, false)))
|
||||||
.limit(1)
|
.limit(1)
|
||||||
|
|
||||||
if (!rows[0]) throw new Error("Kunde nicht gefunden")
|
if (!rows[0]) throw new Error("Kunde nicht gefunden")
|
||||||
@@ -71,7 +71,6 @@ export const masterdataTools: McpTool[] = [
|
|||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
query: { type: "string", description: "Suchtext für Name, Lieferantennummer oder Notizen." },
|
query: { type: "string", description: "Suchtext für Name, Lieferantennummer oder Notizen." },
|
||||||
includeArchived: { type: "boolean", default: false },
|
|
||||||
limit: { type: "number", minimum: 1, maximum: 100 },
|
limit: { type: "number", minimum: 1, maximum: 100 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -86,7 +85,7 @@ export const masterdataTools: McpTool[] = [
|
|||||||
ilike(vendors.notes, `%${query}%`)
|
ilike(vendors.notes, `%${query}%`)
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
if (args.includeArchived !== true) conditions.push(eq(vendors.archived, false))
|
conditions.push(eq(vendors.archived, false))
|
||||||
|
|
||||||
const rows = await context.server.db
|
const rows = await context.server.db
|
||||||
.select()
|
.select()
|
||||||
@@ -115,7 +114,7 @@ export const masterdataTools: McpTool[] = [
|
|||||||
const rows = await context.server.db
|
const rows = await context.server.db
|
||||||
.select()
|
.select()
|
||||||
.from(vendors)
|
.from(vendors)
|
||||||
.where(and(eq(vendors.id, id), eq(vendors.tenant, context.tenantId)))
|
.where(and(eq(vendors.id, id), eq(vendors.tenant, context.tenantId), eq(vendors.archived, false)))
|
||||||
.limit(1)
|
.limit(1)
|
||||||
|
|
||||||
if (!rows[0]) throw new Error("Lieferant nicht gefunden")
|
if (!rows[0]) throw new Error("Lieferant nicht gefunden")
|
||||||
@@ -133,7 +132,6 @@ export const masterdataTools: McpTool[] = [
|
|||||||
query: { type: "string", description: "Suchtext für Name, E-Mail, Telefon, Rolle oder Notizen." },
|
query: { type: "string", description: "Suchtext für Name, E-Mail, Telefon, Rolle oder Notizen." },
|
||||||
customer: { type: "number" },
|
customer: { type: "number" },
|
||||||
vendor: { type: "number" },
|
vendor: { type: "number" },
|
||||||
includeArchived: { type: "boolean", default: false },
|
|
||||||
limit: { type: "number", minimum: 1, maximum: 100 },
|
limit: { type: "number", minimum: 1, maximum: 100 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -157,7 +155,7 @@ export const masterdataTools: McpTool[] = [
|
|||||||
}
|
}
|
||||||
if (customer) conditions.push(eq(contacts.customer, customer))
|
if (customer) conditions.push(eq(contacts.customer, customer))
|
||||||
if (vendor) conditions.push(eq(contacts.vendor, vendor))
|
if (vendor) conditions.push(eq(contacts.vendor, vendor))
|
||||||
if (args.includeArchived !== true) conditions.push(eq(contacts.archived, false))
|
conditions.push(eq(contacts.archived, false))
|
||||||
|
|
||||||
const rows = await context.server.db
|
const rows = await context.server.db
|
||||||
.select()
|
.select()
|
||||||
@@ -178,7 +176,6 @@ export const masterdataTools: McpTool[] = [
|
|||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
query: { type: "string", description: "Suchtext für Name, Artikelnummer, Hersteller, EAN, Barcode oder Beschreibung." },
|
query: { type: "string", description: "Suchtext für Name, Artikelnummer, Hersteller, EAN, Barcode oder Beschreibung." },
|
||||||
includeArchived: { type: "boolean", default: false },
|
|
||||||
limit: { type: "number", minimum: 1, maximum: 100 },
|
limit: { type: "number", minimum: 1, maximum: 100 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -197,7 +194,7 @@ export const masterdataTools: McpTool[] = [
|
|||||||
ilike(products.description, `%${query}%`)
|
ilike(products.description, `%${query}%`)
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
if (args.includeArchived !== true) conditions.push(eq(products.archived, false))
|
conditions.push(eq(products.archived, false))
|
||||||
|
|
||||||
const rows = await context.server.db
|
const rows = await context.server.db
|
||||||
.select()
|
.select()
|
||||||
@@ -226,7 +223,7 @@ export const masterdataTools: McpTool[] = [
|
|||||||
const rows = await context.server.db
|
const rows = await context.server.db
|
||||||
.select()
|
.select()
|
||||||
.from(products)
|
.from(products)
|
||||||
.where(and(eq(products.id, id), eq(products.tenant, context.tenantId)))
|
.where(and(eq(products.id, id), eq(products.tenant, context.tenantId), eq(products.archived, false)))
|
||||||
.limit(1)
|
.limit(1)
|
||||||
|
|
||||||
if (!rows[0]) throw new Error("Artikel nicht gefunden")
|
if (!rows[0]) throw new Error("Artikel nicht gefunden")
|
||||||
@@ -242,7 +239,6 @@ export const masterdataTools: McpTool[] = [
|
|||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
query: { type: "string", description: "Suchtext für Name, Leistungsnummer oder Beschreibung." },
|
query: { type: "string", description: "Suchtext für Name, Leistungsnummer oder Beschreibung." },
|
||||||
includeArchived: { type: "boolean", default: false },
|
|
||||||
limit: { type: "number", minimum: 1, maximum: 100 },
|
limit: { type: "number", minimum: 1, maximum: 100 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -256,7 +252,7 @@ export const masterdataTools: McpTool[] = [
|
|||||||
ilike(services.description, `%${query}%`)
|
ilike(services.description, `%${query}%`)
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
if (args.includeArchived !== true) conditions.push(eq(services.archived, false))
|
conditions.push(eq(services.archived, false))
|
||||||
|
|
||||||
const rows = await context.server.db
|
const rows = await context.server.db
|
||||||
.select()
|
.select()
|
||||||
@@ -285,7 +281,7 @@ export const masterdataTools: McpTool[] = [
|
|||||||
const rows = await context.server.db
|
const rows = await context.server.db
|
||||||
.select()
|
.select()
|
||||||
.from(services)
|
.from(services)
|
||||||
.where(and(eq(services.id, id), eq(services.tenant, context.tenantId)))
|
.where(and(eq(services.id, id), eq(services.tenant, context.tenantId), eq(services.archived, false)))
|
||||||
.limit(1)
|
.limit(1)
|
||||||
|
|
||||||
if (!rows[0]) throw new Error("Leistung nicht gefunden")
|
if (!rows[0]) throw new Error("Leistung nicht gefunden")
|
||||||
@@ -303,7 +299,6 @@ export const masterdataTools: McpTool[] = [
|
|||||||
query: { type: "string", description: "Suchtext für Nummer, Name oder Beschreibung." },
|
query: { type: "string", description: "Suchtext für Nummer, Name oder Beschreibung." },
|
||||||
branch: { type: "number" },
|
branch: { type: "number" },
|
||||||
project: { type: "number" },
|
project: { type: "number" },
|
||||||
includeArchived: { type: "boolean", default: false },
|
|
||||||
limit: { type: "number", minimum: 1, maximum: 100 },
|
limit: { type: "number", minimum: 1, maximum: 100 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -322,7 +317,7 @@ export const masterdataTools: McpTool[] = [
|
|||||||
}
|
}
|
||||||
if (branch) conditions.push(eq(costcentres.branch, branch))
|
if (branch) conditions.push(eq(costcentres.branch, branch))
|
||||||
if (project) conditions.push(eq(costcentres.project, project))
|
if (project) conditions.push(eq(costcentres.project, project))
|
||||||
if (args.includeArchived !== true) conditions.push(eq(costcentres.archived, false))
|
conditions.push(eq(costcentres.archived, false))
|
||||||
|
|
||||||
const rows = await context.server.db
|
const rows = await context.server.db
|
||||||
.select()
|
.select()
|
||||||
@@ -351,7 +346,7 @@ export const masterdataTools: McpTool[] = [
|
|||||||
const rows = await context.server.db
|
const rows = await context.server.db
|
||||||
.select()
|
.select()
|
||||||
.from(costcentres)
|
.from(costcentres)
|
||||||
.where(and(eq(costcentres.id, id), eq(costcentres.tenant, context.tenantId)))
|
.where(and(eq(costcentres.id, id), eq(costcentres.tenant, context.tenantId), eq(costcentres.archived, false)))
|
||||||
.limit(1)
|
.limit(1)
|
||||||
|
|
||||||
if (!rows[0]) throw new Error("Kostenstelle nicht gefunden")
|
if (!rows[0]) throw new Error("Kostenstelle nicht gefunden")
|
||||||
@@ -367,7 +362,6 @@ export const masterdataTools: McpTool[] = [
|
|||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
query: { type: "string", description: "Suchtext für Nummer, Name oder Beschreibung." },
|
query: { type: "string", description: "Suchtext für Nummer, Name oder Beschreibung." },
|
||||||
includeArchived: { type: "boolean", default: false },
|
|
||||||
limit: { type: "number", minimum: 1, maximum: 100 },
|
limit: { type: "number", minimum: 1, maximum: 100 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -382,7 +376,7 @@ export const masterdataTools: McpTool[] = [
|
|||||||
ilike(branches.description, `%${query}%`)
|
ilike(branches.description, `%${query}%`)
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
if (args.includeArchived !== true) conditions.push(eq(branches.archived, false))
|
conditions.push(eq(branches.archived, false))
|
||||||
|
|
||||||
const rows = await context.server.db
|
const rows = await context.server.db
|
||||||
.select()
|
.select()
|
||||||
@@ -404,7 +398,6 @@ export const masterdataTools: McpTool[] = [
|
|||||||
properties: {
|
properties: {
|
||||||
query: { type: "string", description: "Suchtext für Name oder Beschreibung." },
|
query: { type: "string", description: "Suchtext für Name oder Beschreibung." },
|
||||||
branch: { type: "number" },
|
branch: { type: "number" },
|
||||||
includeArchived: { type: "boolean", default: false },
|
|
||||||
limit: { type: "number", minimum: 1, maximum: 100 },
|
limit: { type: "number", minimum: 1, maximum: 100 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -420,7 +413,7 @@ export const masterdataTools: McpTool[] = [
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
if (branch) conditions.push(eq(teams.branch, branch))
|
if (branch) conditions.push(eq(teams.branch, branch))
|
||||||
if (args.includeArchived !== true) conditions.push(eq(teams.archived, false))
|
conditions.push(eq(teams.archived, false))
|
||||||
|
|
||||||
const rows = await context.server.db
|
const rows = await context.server.db
|
||||||
.select()
|
.select()
|
||||||
@@ -441,7 +434,6 @@ export const masterdataTools: McpTool[] = [
|
|||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
query: { type: "string", description: "Suchtext für Name, Kennzeichen, FIN oder Farbe." },
|
query: { type: "string", description: "Suchtext für Name, Kennzeichen, FIN oder Farbe." },
|
||||||
includeArchived: { type: "boolean", default: false },
|
|
||||||
limit: { type: "number", minimum: 1, maximum: 100 },
|
limit: { type: "number", minimum: 1, maximum: 100 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -457,7 +449,7 @@ export const masterdataTools: McpTool[] = [
|
|||||||
ilike(vehicles.color, `%${query}%`)
|
ilike(vehicles.color, `%${query}%`)
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
if (args.includeArchived !== true) conditions.push(eq(vehicles.archived, false))
|
conditions.push(eq(vehicles.archived, false))
|
||||||
|
|
||||||
const rows = await context.server.db
|
const rows = await context.server.db
|
||||||
.select()
|
.select()
|
||||||
@@ -479,7 +471,6 @@ export const masterdataTools: McpTool[] = [
|
|||||||
properties: {
|
properties: {
|
||||||
query: { type: "string", description: "Suchtext für Name, Artikelnummer, Seriennummer, Hersteller oder Beschreibung." },
|
query: { type: "string", description: "Suchtext für Name, Artikelnummer, Seriennummer, Hersteller oder Beschreibung." },
|
||||||
vendor: { type: "number" },
|
vendor: { type: "number" },
|
||||||
includeArchived: { type: "boolean", default: false },
|
|
||||||
limit: { type: "number", minimum: 1, maximum: 100 },
|
limit: { type: "number", minimum: 1, maximum: 100 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -499,7 +490,7 @@ export const masterdataTools: McpTool[] = [
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
if (vendor) conditions.push(eq(inventoryitems.vendor, vendor))
|
if (vendor) conditions.push(eq(inventoryitems.vendor, vendor))
|
||||||
if (args.includeArchived !== true) conditions.push(eq(inventoryitems.archived, false))
|
conditions.push(eq(inventoryitems.archived, false))
|
||||||
|
|
||||||
const rows = await context.server.db
|
const rows = await context.server.db
|
||||||
.select()
|
.select()
|
||||||
@@ -528,7 +519,7 @@ export const masterdataTools: McpTool[] = [
|
|||||||
const rows = await context.server.db
|
const rows = await context.server.db
|
||||||
.select()
|
.select()
|
||||||
.from(inventoryitems)
|
.from(inventoryitems)
|
||||||
.where(and(eq(inventoryitems.id, id), eq(inventoryitems.tenant, context.tenantId)))
|
.where(and(eq(inventoryitems.id, id), eq(inventoryitems.tenant, context.tenantId), eq(inventoryitems.archived, false)))
|
||||||
.limit(1)
|
.limit(1)
|
||||||
|
|
||||||
if (!rows[0]) throw new Error("Inventar nicht gefunden")
|
if (!rows[0]) throw new Error("Inventar nicht gefunden")
|
||||||
@@ -568,4 +559,3 @@ export const masterdataTools: McpTool[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ export const organisationTools: McpTool[] = [
|
|||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
query: { type: "string", description: "Suchtext für Name, Kundennummer, Vorname, Nachname oder Notizen." },
|
query: { type: "string", description: "Suchtext für Name, Kundennummer, Vorname, Nachname oder Notizen." },
|
||||||
includeArchived: { type: "boolean", default: false },
|
|
||||||
limit: { type: "number", minimum: 1, maximum: 100 },
|
limit: { type: "number", minimum: 1, maximum: 100 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -47,7 +46,7 @@ export const organisationTools: McpTool[] = [
|
|||||||
ilike(customers.notes, `%${query}%`)
|
ilike(customers.notes, `%${query}%`)
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
if (args.includeArchived !== true) conditions.push(eq(customers.archived, false))
|
conditions.push(eq(customers.archived, false))
|
||||||
|
|
||||||
const rows = await context.server.db
|
const rows = await context.server.db
|
||||||
.select({
|
.select({
|
||||||
@@ -80,7 +79,6 @@ export const organisationTools: McpTool[] = [
|
|||||||
query: { type: "string", description: "Suchtext für Name, Projektnummer, Kundenreferenz oder Notizen." },
|
query: { type: "string", description: "Suchtext für Name, Projektnummer, Kundenreferenz oder Notizen." },
|
||||||
customer: { type: "number" },
|
customer: { type: "number" },
|
||||||
activePhase: { type: "string" },
|
activePhase: { type: "string" },
|
||||||
includeArchived: { type: "boolean", default: false },
|
|
||||||
limit: { type: "number", minimum: 1, maximum: 100 },
|
limit: { type: "number", minimum: 1, maximum: 100 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -100,7 +98,7 @@ export const organisationTools: McpTool[] = [
|
|||||||
}
|
}
|
||||||
if (customer) conditions.push(eq(projects.customer, customer))
|
if (customer) conditions.push(eq(projects.customer, customer))
|
||||||
if (activePhase) conditions.push(eq(projects.active_phase, activePhase))
|
if (activePhase) conditions.push(eq(projects.active_phase, activePhase))
|
||||||
if (args.includeArchived !== true) conditions.push(eq(projects.archived, false))
|
conditions.push(eq(projects.archived, false))
|
||||||
|
|
||||||
const rows = await context.server.db
|
const rows = await context.server.db
|
||||||
.select()
|
.select()
|
||||||
@@ -131,7 +129,7 @@ export const organisationTools: McpTool[] = [
|
|||||||
const rows = await context.server.db
|
const rows = await context.server.db
|
||||||
.select()
|
.select()
|
||||||
.from(projects)
|
.from(projects)
|
||||||
.where(and(eq(projects.id, id), eq(projects.tenant, context.tenantId)))
|
.where(and(eq(projects.id, id), eq(projects.tenant, context.tenantId), eq(projects.archived, false)))
|
||||||
.limit(1)
|
.limit(1)
|
||||||
|
|
||||||
if (!rows[0]) throw new Error("Projekt nicht gefunden")
|
if (!rows[0]) throw new Error("Projekt nicht gefunden")
|
||||||
@@ -219,7 +217,6 @@ export const organisationTools: McpTool[] = [
|
|||||||
properties: {
|
properties: {
|
||||||
query: { type: "string", description: "Suchtext für Name." },
|
query: { type: "string", description: "Suchtext für Name." },
|
||||||
customer: { type: "number" },
|
customer: { type: "number" },
|
||||||
includeArchived: { type: "boolean", default: false },
|
|
||||||
limit: { type: "number", minimum: 1, maximum: 100 },
|
limit: { type: "number", minimum: 1, maximum: 100 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -230,7 +227,7 @@ export const organisationTools: McpTool[] = [
|
|||||||
|
|
||||||
if (query) conditions.push(ilike(plants.name, `%${query}%`))
|
if (query) conditions.push(ilike(plants.name, `%${query}%`))
|
||||||
if (customer) conditions.push(eq(plants.customer, customer))
|
if (customer) conditions.push(eq(plants.customer, customer))
|
||||||
if (args.includeArchived !== true) conditions.push(eq(plants.archived, false))
|
conditions.push(eq(plants.archived, false))
|
||||||
|
|
||||||
const rows = await context.server.db
|
const rows = await context.server.db
|
||||||
.select()
|
.select()
|
||||||
@@ -254,7 +251,6 @@ export const organisationTools: McpTool[] = [
|
|||||||
project: { type: "number" },
|
project: { type: "number" },
|
||||||
customer: { type: "number" },
|
customer: { type: "number" },
|
||||||
eventtype: { type: "string" },
|
eventtype: { type: "string" },
|
||||||
includeArchived: { type: "boolean", default: false },
|
|
||||||
limit: { type: "number", minimum: 1, maximum: 100 },
|
limit: { type: "number", minimum: 1, maximum: 100 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -275,7 +271,7 @@ export const organisationTools: McpTool[] = [
|
|||||||
if (project) conditions.push(eq(events.project, project))
|
if (project) conditions.push(eq(events.project, project))
|
||||||
if (customer) conditions.push(eq(events.customer, customer))
|
if (customer) conditions.push(eq(events.customer, customer))
|
||||||
if (eventtype) conditions.push(eq(events.eventtype, eventtype))
|
if (eventtype) conditions.push(eq(events.eventtype, eventtype))
|
||||||
if (args.includeArchived !== true) conditions.push(eq(events.archived, false))
|
conditions.push(eq(events.archived, false))
|
||||||
|
|
||||||
const rows = await context.server.db
|
const rows = await context.server.db
|
||||||
.select()
|
.select()
|
||||||
@@ -298,7 +294,6 @@ export const organisationTools: McpTool[] = [
|
|||||||
query: { type: "string", description: "Suchtext für Name, Beschreibung oder Kategorie." },
|
query: { type: "string", description: "Suchtext für Name, Beschreibung oder Kategorie." },
|
||||||
project: { type: "number" },
|
project: { type: "number" },
|
||||||
customer: { type: "number" },
|
customer: { type: "number" },
|
||||||
includeArchived: { type: "boolean", default: false },
|
|
||||||
limit: { type: "number", minimum: 1, maximum: 100 },
|
limit: { type: "number", minimum: 1, maximum: 100 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -317,7 +312,7 @@ export const organisationTools: McpTool[] = [
|
|||||||
}
|
}
|
||||||
if (project) conditions.push(eq(tasks.project, project))
|
if (project) conditions.push(eq(tasks.project, project))
|
||||||
if (customer) conditions.push(eq(tasks.customer, customer))
|
if (customer) conditions.push(eq(tasks.customer, customer))
|
||||||
if (args.includeArchived !== true) conditions.push(eq(tasks.archived, false))
|
conditions.push(eq(tasks.archived, false))
|
||||||
|
|
||||||
const rows = await context.server.db
|
const rows = await context.server.db
|
||||||
.select()
|
.select()
|
||||||
@@ -348,7 +343,7 @@ export const organisationTools: McpTool[] = [
|
|||||||
const rows = await context.server.db
|
const rows = await context.server.db
|
||||||
.select()
|
.select()
|
||||||
.from(tasks)
|
.from(tasks)
|
||||||
.where(and(eq(tasks.id, id), eq(tasks.tenant, context.tenantId)))
|
.where(and(eq(tasks.id, id), eq(tasks.tenant, context.tenantId), eq(tasks.archived, false)))
|
||||||
.limit(1)
|
.limit(1)
|
||||||
|
|
||||||
if (!rows[0]) throw new Error("Aufgabe nicht gefunden")
|
if (!rows[0]) throw new Error("Aufgabe nicht gefunden")
|
||||||
|
|||||||
146
backend/src/modules/outgoing-document-cost-centres.service.ts
Normal file
146
backend/src/modules/outgoing-document-cost-centres.service.ts
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
import { and, eq, inArray } from "drizzle-orm"
|
||||||
|
import { FastifyInstance } from "fastify"
|
||||||
|
import { costcentres, createddocuments } from "../../db/schema"
|
||||||
|
import { insertHistoryItem } from "../utils/history"
|
||||||
|
|
||||||
|
export type RowCostCentreAssignment = {
|
||||||
|
rowId: string
|
||||||
|
costcentre: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OutgoingDocumentCostCentreUpdate = {
|
||||||
|
costcentre?: string | null
|
||||||
|
rowCostCentres?: RowCostCentreAssignment[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const serviceError = (message: string, statusCode = 400) =>
|
||||||
|
Object.assign(new Error(message), { statusCode })
|
||||||
|
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
|
||||||
|
|
||||||
|
export function applyRowCostCentreAssignments(
|
||||||
|
rows: unknown,
|
||||||
|
assignments: RowCostCentreAssignment[] = [],
|
||||||
|
) {
|
||||||
|
if (!Array.isArray(rows)) return []
|
||||||
|
if (!assignments.length) return rows
|
||||||
|
|
||||||
|
const assignmentMap = new Map(assignments.map((assignment) => [String(assignment.rowId), assignment.costcentre]))
|
||||||
|
const existingIds = new Set(rows
|
||||||
|
.filter((row) => row && typeof row === "object" && !Array.isArray(row))
|
||||||
|
.map((row) => (row as Record<string, unknown>).id)
|
||||||
|
.filter((id) => id !== null && id !== undefined && id !== "")
|
||||||
|
.map(String))
|
||||||
|
|
||||||
|
for (const rowId of assignmentMap.keys()) {
|
||||||
|
if (!existingIds.has(rowId)) throw serviceError(`Position ${rowId} wurde im Ausgangsbeleg nicht gefunden`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows.map((row) => {
|
||||||
|
if (!row || typeof row !== "object" || Array.isArray(row)) return row
|
||||||
|
const rowRecord = row as Record<string, unknown>
|
||||||
|
if (rowRecord.id === null || rowRecord.id === undefined || rowRecord.id === "") return row
|
||||||
|
const rowId = String(rowRecord.id)
|
||||||
|
if (!assignmentMap.has(rowId)) return row
|
||||||
|
|
||||||
|
const { costcentre: _legacyCostCentre, ...rest } = rowRecord
|
||||||
|
return { ...rest, costCentre: assignmentMap.get(rowId) ?? null }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateOutgoingDocumentCostCentres(
|
||||||
|
server: FastifyInstance,
|
||||||
|
tenantId: number,
|
||||||
|
userId: string,
|
||||||
|
documentId: number,
|
||||||
|
input: OutgoingDocumentCostCentreUpdate,
|
||||||
|
) {
|
||||||
|
const hasDocumentCostCentre = Object.prototype.hasOwnProperty.call(input, "costcentre")
|
||||||
|
if (hasDocumentCostCentre && input.costcentre !== null && !UUID_PATTERN.test(String(input.costcentre))) {
|
||||||
|
throw serviceError("costcentre muss eine gültige UUID oder null sein")
|
||||||
|
}
|
||||||
|
if (input.rowCostCentres !== undefined && !Array.isArray(input.rowCostCentres)) {
|
||||||
|
throw serviceError("rowCostCentres muss ein Array sein")
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowAssignments = (input.rowCostCentres || []).map((assignment) => {
|
||||||
|
const rowId = typeof assignment?.rowId === "string" ? assignment.rowId.trim() : ""
|
||||||
|
const costcentre = assignment?.costcentre
|
||||||
|
if (!rowId) throw serviceError("Jede Positionszuordnung benötigt eine rowId")
|
||||||
|
if (costcentre !== null && !UUID_PATTERN.test(String(costcentre))) {
|
||||||
|
throw serviceError(`Ungültige Kostenstelle für Position ${rowId}`)
|
||||||
|
}
|
||||||
|
return { rowId, costcentre }
|
||||||
|
})
|
||||||
|
if (!hasDocumentCostCentre && !rowAssignments.length) {
|
||||||
|
throw serviceError("Mindestens eine Beleg- oder Positionskostenstelle ist erforderlich")
|
||||||
|
}
|
||||||
|
|
||||||
|
const [existing] = await server.db
|
||||||
|
.select()
|
||||||
|
.from(createddocuments)
|
||||||
|
.where(and(
|
||||||
|
eq(createddocuments.id, documentId),
|
||||||
|
eq(createddocuments.tenant, tenantId),
|
||||||
|
eq(createddocuments.archived, false),
|
||||||
|
))
|
||||||
|
.limit(1)
|
||||||
|
|
||||||
|
if (!existing) throw serviceError("Ausgangsbeleg nicht gefunden", 404)
|
||||||
|
if (existing.state !== "Gebucht") {
|
||||||
|
throw serviceError("Kostenstellen können über diesen Weg nur bei fertiggestellten Ausgangsbelegen geändert werden")
|
||||||
|
}
|
||||||
|
|
||||||
|
const referencedCostCentreIds = [...new Set([
|
||||||
|
...(hasDocumentCostCentre && input.costcentre ? [input.costcentre] : []),
|
||||||
|
...rowAssignments.map((assignment) => assignment.costcentre).filter((id): id is string => Boolean(id)),
|
||||||
|
])]
|
||||||
|
|
||||||
|
if (referencedCostCentreIds.length) {
|
||||||
|
const validCostCentres = await server.db
|
||||||
|
.select({ id: costcentres.id })
|
||||||
|
.from(costcentres)
|
||||||
|
.where(and(
|
||||||
|
eq(costcentres.tenant, tenantId),
|
||||||
|
eq(costcentres.archived, false),
|
||||||
|
inArray(costcentres.id, referencedCostCentreIds),
|
||||||
|
))
|
||||||
|
|
||||||
|
if (validCostCentres.length !== referencedCostCentreIds.length) {
|
||||||
|
throw serviceError("Mindestens eine Kostenstelle wurde nicht gefunden oder ist archiviert")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedRows = applyRowCostCentreAssignments(existing.rows, rowAssignments)
|
||||||
|
const update: Record<string, unknown> = {
|
||||||
|
rows: updatedRows,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
updatedBy: userId,
|
||||||
|
}
|
||||||
|
if (hasDocumentCostCentre) update.costcentre = input.costcentre ?? null
|
||||||
|
|
||||||
|
const [updated] = await server.db
|
||||||
|
.update(createddocuments)
|
||||||
|
.set(update)
|
||||||
|
.where(and(
|
||||||
|
eq(createddocuments.id, documentId),
|
||||||
|
eq(createddocuments.tenant, tenantId),
|
||||||
|
eq(createddocuments.archived, false),
|
||||||
|
eq(createddocuments.state, "Gebucht"),
|
||||||
|
))
|
||||||
|
.returning()
|
||||||
|
|
||||||
|
if (!updated) throw serviceError("Ausgangsbeleg nicht gefunden", 404)
|
||||||
|
|
||||||
|
await insertHistoryItem(server, {
|
||||||
|
tenant_id: tenantId,
|
||||||
|
created_by: userId,
|
||||||
|
entity: "createddocuments",
|
||||||
|
entityId: documentId,
|
||||||
|
action: "updated",
|
||||||
|
oldVal: existing,
|
||||||
|
newVal: updated,
|
||||||
|
text: "Kostenstellen des fertiggestellten Ausgangsbelegs geändert",
|
||||||
|
})
|
||||||
|
|
||||||
|
return updated
|
||||||
|
}
|
||||||
@@ -14,9 +14,13 @@ import { documentTemplateHandlebars } from "../utils/handlebars";
|
|||||||
dayjs.extend(quarterOfYear);
|
dayjs.extend(quarterOfYear);
|
||||||
|
|
||||||
|
|
||||||
export const executeManualGeneration = async (server:FastifyInstance,executionDate,templateIds,tenantId,executedBy) => {
|
export const executeManualGeneration = async (
|
||||||
try {
|
server: FastifyInstance,
|
||||||
console.log(executedBy)
|
executionDate: string | Date,
|
||||||
|
templateIds: number[],
|
||||||
|
tenantId: number,
|
||||||
|
executedBy: string,
|
||||||
|
) => {
|
||||||
|
|
||||||
const executionDayjs = dayjs(executionDate);
|
const executionDayjs = dayjs(executionDate);
|
||||||
|
|
||||||
@@ -33,20 +37,22 @@ export const executeManualGeneration = async (server:FastifyInstance,executionDa
|
|||||||
if (!tenant) throw new Error(`Tenant mit ID ${tenantId} nicht gefunden.`);
|
if (!tenant) throw new Error(`Tenant mit ID ${tenantId} nicht gefunden.`);
|
||||||
|
|
||||||
// 2. Templates laden
|
// 2. Templates laden
|
||||||
const templates = await server.db
|
const uniqueTemplateIds = [...new Set(templateIds)]
|
||||||
|
const templates = (await server.db
|
||||||
.select()
|
.select()
|
||||||
.from(schema.createddocuments)
|
.from(schema.createddocuments)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(schema.createddocuments.tenant, tenantId),
|
eq(schema.createddocuments.tenant, tenantId),
|
||||||
eq(schema.createddocuments.type, "serialInvoices"),
|
eq(schema.createddocuments.type, "serialInvoices"),
|
||||||
inArray(schema.createddocuments.id, templateIds)
|
eq(schema.createddocuments.archived, false),
|
||||||
|
inArray(schema.createddocuments.id, uniqueTemplateIds)
|
||||||
)
|
)
|
||||||
);
|
))
|
||||||
|
.filter((template) => Boolean((template.serialConfig as any)?.active));
|
||||||
|
|
||||||
if (templates.length === 0) {
|
if (templates.length !== uniqueTemplateIds.length) {
|
||||||
console.warn("Keine passenden Vorlagen gefunden.");
|
throw new Error("Mindestens eine Serienrechnungsvorlage wurde nicht gefunden, ist archiviert oder inaktiv.");
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Folder & FileType IDs holen (Hilfsfunktionen unten)
|
// 3. Folder & FileType IDs holen (Hilfsfunktionen unten)
|
||||||
@@ -62,7 +68,7 @@ export const executeManualGeneration = async (server:FastifyInstance,executionDa
|
|||||||
executionDate: executionDayjs.toDate(),
|
executionDate: executionDayjs.toDate(),
|
||||||
status: "draft",
|
status: "draft",
|
||||||
createdBy: executedBy,
|
createdBy: executedBy,
|
||||||
summary: `${templateIds.length} Vorlagen verarbeitet`
|
summary: `${uniqueTemplateIds.length} Vorlagen verarbeitet`
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
@@ -88,13 +94,10 @@ export const executeManualGeneration = async (server:FastifyInstance,executionDa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return results;
|
return { execution: executionRecord, results };
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const finishManualGeneration = async (server: FastifyInstance, executionId: number) => {
|
export const finishManualGeneration = async (server: FastifyInstance, executionId: string, tenantId: number) => {
|
||||||
try {
|
try {
|
||||||
console.log(`Beende Ausführung ${executionId}...`);
|
console.log(`Beende Ausführung ${executionId}...`);
|
||||||
|
|
||||||
@@ -103,15 +106,16 @@ export const finishManualGeneration = async (server: FastifyInstance, executionI
|
|||||||
const [executionRecord] = await server.db
|
const [executionRecord] = await server.db
|
||||||
.select()
|
.select()
|
||||||
.from(schema.serialExecutions)// @ts-ignore
|
.from(schema.serialExecutions)// @ts-ignore
|
||||||
.where(eq(schema.serialExecutions.id, executionId))
|
.where(and(
|
||||||
|
eq(schema.serialExecutions.id, executionId),
|
||||||
|
eq(schema.serialExecutions.tenant, tenantId),
|
||||||
|
))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (!executionRecord) throw new Error("Execution nicht gefunden");
|
if (!executionRecord) throw new Error("Execution nicht gefunden");
|
||||||
|
|
||||||
console.log(executionRecord);
|
console.log(executionRecord);
|
||||||
|
|
||||||
const tenantId = executionRecord.tenant;
|
|
||||||
|
|
||||||
console.log(tenantId)
|
console.log(tenantId)
|
||||||
|
|
||||||
// Tenant laden (für Settings etc.)
|
// Tenant laden (für Settings etc.)
|
||||||
@@ -132,7 +136,11 @@ export const finishManualGeneration = async (server: FastifyInstance, executionI
|
|||||||
const documents = await server.db
|
const documents = await server.db
|
||||||
.select()
|
.select()
|
||||||
.from(schema.createddocuments)
|
.from(schema.createddocuments)
|
||||||
.where(eq(schema.createddocuments.serialexecution, executionId));
|
.where(and(
|
||||||
|
eq(schema.createddocuments.serialexecution, executionId),
|
||||||
|
eq(schema.createddocuments.tenant, tenantId),
|
||||||
|
eq(schema.createddocuments.archived, false),
|
||||||
|
));
|
||||||
|
|
||||||
console.log(`${documents.length} Dokumente werden finalisiert...`);
|
console.log(`${documents.length} Dokumente werden finalisiert...`);
|
||||||
|
|
||||||
@@ -228,7 +236,10 @@ export const finishManualGeneration = async (server: FastifyInstance, executionI
|
|||||||
status: finalStatus,
|
status: finalStatus,
|
||||||
summary: `Abgeschlossen: ${successCount} erfolgreich, ${errorCount} Fehler.`
|
summary: `Abgeschlossen: ${successCount} erfolgreich, ${errorCount} Fehler.`
|
||||||
})// @ts-ignore
|
})// @ts-ignore
|
||||||
.where(eq(schema.serialExecutions.id, executionId));
|
.where(and(
|
||||||
|
eq(schema.serialExecutions.id, executionId),
|
||||||
|
eq(schema.serialExecutions.tenant, tenantId),
|
||||||
|
));
|
||||||
|
|
||||||
return { success: true, processed: successCount, errors: errorCount };
|
return { success: true, processed: successCount, errors: errorCount };
|
||||||
|
|
||||||
@@ -240,7 +251,10 @@ export const finishManualGeneration = async (server: FastifyInstance, executionI
|
|||||||
.update(schema.serialExecutions)
|
.update(schema.serialExecutions)
|
||||||
.set({ status: "error", summary: "Kritischer Fehler beim Finalisieren." })
|
.set({ status: "error", summary: "Kritischer Fehler beim Finalisieren." })
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
.where(eq(schema.serialExecutions.id, executionId));
|
.where(and(
|
||||||
|
eq(schema.serialExecutions.id, executionId),
|
||||||
|
eq(schema.serialExecutions.tenant, tenantId),
|
||||||
|
));
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {generateTimesEvaluation} from "../modules/time/evaluation.service";
|
|||||||
import {citys, files} from "../../db/schema";
|
import {citys, files} from "../../db/schema";
|
||||||
import {and, eq, isNull, not} from "drizzle-orm";
|
import {and, eq, isNull, not} from "drizzle-orm";
|
||||||
import {executeManualGeneration, finishManualGeneration} from "../modules/serialexecution.service";
|
import {executeManualGeneration, finishManualGeneration} from "../modules/serialexecution.service";
|
||||||
|
import { updateOutgoingDocumentCostCentres } from "../modules/outgoing-document-cost-centres.service";
|
||||||
import { s3 } from "../utils/s3";
|
import { s3 } from "../utils/s3";
|
||||||
import { secrets } from "../utils/secrets";
|
import { secrets } from "../utils/secrets";
|
||||||
import { storeExtractedTextForFile } from "../utils/documentText";
|
import { storeExtractedTextForFile } from "../utils/documentText";
|
||||||
@@ -296,15 +297,34 @@ export default async function functionRoutes(server: FastifyInstance) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
server.post('/functions/serial/start', async (req, reply) => {
|
server.post('/functions/serial/start', async (req, reply) => {
|
||||||
console.log(req.body)
|
const {executionDate, templateIds} = req.body as {executionDate:string, templateIds:number[]}
|
||||||
const {executionDate,templateIds,tenantId} = req.body as {executionDate:string,templateIds:Number[],tenantId:Number}
|
return executeManualGeneration(server, executionDate, templateIds, req.user.tenant_id, req.user.user_id)
|
||||||
await executeManualGeneration(server,executionDate,templateIds,tenantId,req.user.user_id)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
server.post('/functions/serial/finish/:execution_id', async (req, reply) => {
|
server.post('/functions/serial/finish/:execution_id', async (req, reply) => {
|
||||||
const {execution_id} = req.params as { execution_id: string }
|
const {execution_id} = req.params as { execution_id: string }
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
await finishManualGeneration(server,execution_id)
|
return finishManualGeneration(server, execution_id, req.user.tenant_id)
|
||||||
|
})
|
||||||
|
|
||||||
|
server.put('/functions/outgoing-documents/:id/cost-centres', async (req, reply) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params as { id: string }
|
||||||
|
const documentId = Number(id)
|
||||||
|
if (!Number.isFinite(documentId)) return reply.code(400).send({ error: "Ungültige Ausgangsbeleg-ID" })
|
||||||
|
|
||||||
|
const document = await updateOutgoingDocumentCostCentres(
|
||||||
|
server,
|
||||||
|
req.user.tenant_id,
|
||||||
|
req.user.user_id,
|
||||||
|
documentId,
|
||||||
|
req.body as any,
|
||||||
|
)
|
||||||
|
return { document }
|
||||||
|
} catch (error) {
|
||||||
|
const statusCode = (error as any)?.statusCode || 500
|
||||||
|
return reply.code(statusCode).send({ error: error instanceof Error ? error.message : "Kostenstellen konnten nicht geändert werden" })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
server.post('/functions/services/bankstatementsync', async (req, reply) => {
|
server.post('/functions/services/bankstatementsync', async (req, reply) => {
|
||||||
|
|||||||
37
backend/tests/mcpArchivedResults.test.ts
Normal file
37
backend/tests/mcpArchivedResults.test.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import assert from "node:assert/strict"
|
||||||
|
import test from "node:test"
|
||||||
|
import { mcpTools } from "../src/mcp/registry"
|
||||||
|
import { asToolResult } from "../src/mcp/result"
|
||||||
|
|
||||||
|
test("removes archived records from MCP text and structured output", () => {
|
||||||
|
const result = asToolResult({
|
||||||
|
rows: [
|
||||||
|
{ id: 1, archived: false, name: "Aktiv" },
|
||||||
|
{ id: 2, archived: true, name: "Archiviert" },
|
||||||
|
],
|
||||||
|
nested: {
|
||||||
|
current: { id: 3, archived: false },
|
||||||
|
previous: { id: 4, archived: true },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.deepEqual(result.structuredContent, {
|
||||||
|
rows: [{ id: 1, archived: false, name: "Aktiv" }],
|
||||||
|
nested: { current: { id: 3, archived: false } },
|
||||||
|
})
|
||||||
|
assert.deepEqual(JSON.parse(result.content[0].text), result.structuredContent)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("does not expose a singular archived record", () => {
|
||||||
|
const result = asToolResult({ task: { id: 1, archived: true } })
|
||||||
|
|
||||||
|
assert.deepEqual(result.structuredContent, {})
|
||||||
|
assert.deepEqual(JSON.parse(result.content[0].text), {})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("does not advertise includeArchived on MCP tools", () => {
|
||||||
|
for (const tool of mcpTools) {
|
||||||
|
const properties = (tool.inputSchema.properties || {}) as Record<string, unknown>
|
||||||
|
assert.equal(properties.includeArchived, undefined, tool.name)
|
||||||
|
}
|
||||||
|
})
|
||||||
34
backend/tests/mcpSerialInvoices.test.ts
Normal file
34
backend/tests/mcpSerialInvoices.test.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import assert from "node:assert/strict"
|
||||||
|
import test from "node:test"
|
||||||
|
import { mcpToolMap } from "../src/mcp/registry"
|
||||||
|
|
||||||
|
test("publishes the complete serial invoice configuration on create and update", () => {
|
||||||
|
for (const name of ["accounting.outgoing_documents.create", "accounting.outgoing_documents.update"]) {
|
||||||
|
const tool = mcpToolMap.get(name)
|
||||||
|
const properties = tool?.inputSchema.properties as Record<string, any>
|
||||||
|
|
||||||
|
assert.ok(properties.serialConfig, name)
|
||||||
|
assert.deepEqual(properties.serialConfig.properties.intervall.enum, [
|
||||||
|
"wöchentlich",
|
||||||
|
"2 - wöchentlich",
|
||||||
|
"monatlich",
|
||||||
|
"vierteljährlich",
|
||||||
|
"halbjährlich",
|
||||||
|
"jährlich",
|
||||||
|
])
|
||||||
|
assert.deepEqual(properties.serialConfig.properties.dateDirection.enum, ["Rückwirkend", "Im Voraus"])
|
||||||
|
assert.ok(properties.type.enum.includes("serialInvoices"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test("registers tenant-scoped serial invoice execution tools", () => {
|
||||||
|
const execute = mcpToolMap.get("accounting.serial_invoices.execute")
|
||||||
|
const list = mcpToolMap.get("accounting.serial_invoice_executions.list")
|
||||||
|
const finish = mcpToolMap.get("accounting.serial_invoice_executions.finish")
|
||||||
|
|
||||||
|
assert.deepEqual(execute?.requiredPermissions, ["accounting.outgoing_documents.write"])
|
||||||
|
assert.deepEqual(execute?.inputSchema.required, ["executionDate", "templateIds"])
|
||||||
|
assert.deepEqual(list?.requiredPermissions, ["accounting.outgoing_documents.read"])
|
||||||
|
assert.deepEqual(finish?.requiredPermissions, ["accounting.outgoing_documents.write"])
|
||||||
|
assert.deepEqual(finish?.inputSchema.required, ["id"])
|
||||||
|
})
|
||||||
33
backend/tests/outgoingDocumentCostCentres.test.ts
Normal file
33
backend/tests/outgoingDocumentCostCentres.test.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import assert from "node:assert/strict"
|
||||||
|
import test from "node:test"
|
||||||
|
import { applyRowCostCentreAssignments } from "../src/modules/outgoing-document-cost-centres.service"
|
||||||
|
import { mcpToolMap } from "../src/mcp/registry"
|
||||||
|
|
||||||
|
test("changes only requested position cost centres", () => {
|
||||||
|
const rows = [
|
||||||
|
{ id: "row-1", description: "Montage", price: 100, costCentre: "old-1" },
|
||||||
|
{ id: "row-2", description: "Material", price: 50, costCentre: "old-2" },
|
||||||
|
]
|
||||||
|
|
||||||
|
assert.deepEqual(applyRowCostCentreAssignments(rows, [
|
||||||
|
{ rowId: "row-1", costcentre: "new-1" },
|
||||||
|
]), [
|
||||||
|
{ id: "row-1", description: "Montage", price: 100, costCentre: "new-1" },
|
||||||
|
rows[1],
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("rejects assignments for unknown document positions", () => {
|
||||||
|
assert.throws(
|
||||||
|
() => applyRowCostCentreAssignments([{ id: "row-1" }], [{ rowId: "missing", costcentre: null }]),
|
||||||
|
/Position missing.*nicht gefunden/,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("registers a dedicated finalized-document cost centre tool", () => {
|
||||||
|
const tool = mcpToolMap.get("accounting.outgoing_documents.cost_centres.update")
|
||||||
|
|
||||||
|
assert.deepEqual(tool?.requiredPermissions, ["accounting.outgoing_documents.write"])
|
||||||
|
assert.deepEqual(tool?.inputSchema.required, ["id"])
|
||||||
|
assert.ok(tool?.inputSchema.anyOf)
|
||||||
|
})
|
||||||
@@ -1896,6 +1896,31 @@ const saveDocument = async (state, resetup = false) => {
|
|||||||
if (resetup) await setupPage()
|
if (resetup) await setupPage()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const saveFinalizedCostCentres = async () => {
|
||||||
|
const rowCostCentres = itemInfo.value.rows
|
||||||
|
.filter(row => row?.id)
|
||||||
|
.map(row => ({
|
||||||
|
rowId: String(row.id),
|
||||||
|
costcentre: row.costCentre || null,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const result = await $api(`/api/functions/outgoing-documents/${itemInfo.value.id}/cost-centres`, {
|
||||||
|
method: "PUT",
|
||||||
|
body: {
|
||||||
|
costcentre: itemInfo.value.costcentre || null,
|
||||||
|
rowCostCentres,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
itemInfo.value.costcentre = result.document.costcentre
|
||||||
|
itemInfo.value.rows = normalizeCreatedDocumentRows(result.document.rows)
|
||||||
|
toast.add({
|
||||||
|
title: "Kostenstellen gespeichert",
|
||||||
|
description: "Die Kostenstellen des fertiggestellten Belegs wurden aktualisiert.",
|
||||||
|
color: "success",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const selectedTab = ref("0")
|
const selectedTab = ref("0")
|
||||||
|
|
||||||
const closeDocument = async () => {
|
const closeDocument = async () => {
|
||||||
@@ -2077,7 +2102,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
|
|||||||
<UButton
|
<UButton
|
||||||
icon="i-mdi-content-save"
|
icon="i-mdi-content-save"
|
||||||
@click="saveDocument('Entwurf',true)"
|
@click="saveDocument('Entwurf',true)"
|
||||||
v-if="itemInfo.type !== 'serialInvoices' && !isTemplateMode"
|
v-if="itemInfo.type !== 'serialInvoices' && !isTemplateMode && itemInfo.state === 'Entwurf'"
|
||||||
:disabled="!itemInfo.customer"
|
:disabled="!itemInfo.customer"
|
||||||
>
|
>
|
||||||
Speichern
|
Speichern
|
||||||
@@ -2092,10 +2117,17 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
|
|||||||
</UButton>
|
</UButton>
|
||||||
<UButton
|
<UButton
|
||||||
@click="closeDocument"
|
@click="closeDocument"
|
||||||
v-if="itemInfo.id && itemInfo.type !== 'serialInvoices' && !isTemplateMode"
|
v-if="itemInfo.id && itemInfo.type !== 'serialInvoices' && !isTemplateMode && itemInfo.state === 'Entwurf'"
|
||||||
>
|
>
|
||||||
{{selectedTab === '0' ? "Vorschau zeigen" : "Fertigstellen"}}
|
{{selectedTab === '0' ? "Vorschau zeigen" : "Fertigstellen"}}
|
||||||
</UButton>
|
</UButton>
|
||||||
|
<UButton
|
||||||
|
icon="i-heroicons-tag"
|
||||||
|
@click="saveFinalizedCostCentres"
|
||||||
|
v-if="itemInfo.id && itemInfo.type !== 'serialInvoices' && !isTemplateMode && itemInfo.state === 'Gebucht'"
|
||||||
|
>
|
||||||
|
Kostenstellen speichern
|
||||||
|
</UButton>
|
||||||
<UButton
|
<UButton
|
||||||
icon="i-mdi-content-save"
|
icon="i-mdi-content-save"
|
||||||
@click="saveSerialInvoice"
|
@click="saveSerialInvoice"
|
||||||
|
|||||||
@@ -652,7 +652,7 @@ const executeSerialInvoices = async () => {
|
|||||||
|
|
||||||
toast.add({
|
toast.add({
|
||||||
title: 'Ausführung gestartet',
|
title: 'Ausführung gestartet',
|
||||||
description: `${res.length} Rechnungen werden im Hintergrund generiert.`,
|
description: `${res.results.length} Rechnungen wurden für den Lauf vorbereitet.`,
|
||||||
icon: 'i-heroicons-check-circle',
|
icon: 'i-heroicons-check-circle',
|
||||||
color: 'green'
|
color: 'green'
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user