feat(accounting): Skonto bei Zahlungen auf Ausgangs- und Eingangsbelege buchen
Skonto wird an der Bankzuordnung gespeichert und gleicht den Beleg zusammen mit der Zahlung aus. Der Betrag wird anteilig nach Steuersätzen auf die Skontokonten verteilt (Standard SKR03, je Mandant einstellbar), im DATEV-Export als eigene Buchung je Steuersatz ausgegeben und in der USt-Auswertung im Zeitraum der Zahlung berücksichtigt. Schlussrechnungen verteilen den Skonto auf die Restforderung nach Abzug der Abschläge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -55,6 +55,14 @@ export const prepareStatementAllocationInput = (args: Record<string, unknown>) =
|
||||
if (!bookingModes.has(bookingMode)) throw new Error(`Ungültige Aufwandsart: ${bookingMode}`)
|
||||
|
||||
payload.amount = amount
|
||||
if (hasValue(args.cashDiscountAmount)) {
|
||||
const cashDiscountAmount = Number(args.cashDiscountAmount)
|
||||
if (!Number.isFinite(cashDiscountAmount)) throw new Error("cashDiscountAmount muss eine gültige Zahl sein")
|
||||
if (cashDiscountAmount !== 0) {
|
||||
if (!hasValue(args.bankstatement)) throw new Error("Skonto ist nur zusammen mit einer Bankzahlung möglich")
|
||||
payload.cashDiscountAmount = cashDiscountAmount
|
||||
}
|
||||
}
|
||||
if (args.description !== undefined) payload.description = stringArg(args, "description")
|
||||
payload.bookingMode = bookingMode
|
||||
if (args.datevTaxKey !== undefined) payload.datevTaxKey = stringArg(args, "datevTaxKey")
|
||||
|
||||
@@ -36,6 +36,7 @@ import { withImages } from "../result"
|
||||
import { renderOutgoingDocumentPdf } from "../../modules/outgoing-document-pdf.service"
|
||||
import { renderPdfPagesAsPng } from "../../utils/pdfPreview"
|
||||
import { outgoingStatusTools } from "./outgoingStatus"
|
||||
import { prepareCashDiscountAllocation } from "../../modules/cash-discount"
|
||||
|
||||
export { prepareStatementAllocationInput } from "../statementAllocations"
|
||||
|
||||
@@ -1613,7 +1614,7 @@ export const accountingTools: McpTool[] = [
|
||||
{
|
||||
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.",
|
||||
description: "Erstellt eine Bankzuweisung oder manuelle Soll/Haben-Buchung. Unterstützt Ausgangs- und Eingangsbelege, Sachkonten, zusätzliche Konten, Debitoren, Kreditoren, Teilbeträge, Skonto, DATEV-Steuerschlüssel und Abschreibungen.",
|
||||
requiredPermissions: ["accounting.statement_allocations.write"],
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
@@ -1621,6 +1622,7 @@ export const accountingTools: McpTool[] = [
|
||||
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." },
|
||||
cashDiscountAmount: { type: "number", description: "Skonto (brutto) zu einem Ausgangs- oder Eingangsbeleg, zusätzlich zum gezahlten amount. Nur mit bankstatement; das Vorzeichen wird an amount angeglichen. Wird anteilig nach Steuersätzen auf die Skontokonten verteilt." },
|
||||
createddocument: { type: "number", description: "Ausgangsbeleg-ID." },
|
||||
incominginvoice: { type: "number", description: "Eingangsbeleg-ID." },
|
||||
account: { type: "number", description: "Sachkonto-ID beziehungsweise Soll-Sachkonto." },
|
||||
@@ -1646,8 +1648,9 @@ export const accountingTools: McpTool[] = [
|
||||
},
|
||||
},
|
||||
async handler(context, args) {
|
||||
const payload = prepareStatementAllocationInput(args)
|
||||
await assertStatementAllocationReferences(context, payload)
|
||||
const prepared = prepareStatementAllocationInput(args)
|
||||
await assertStatementAllocationReferences(context, prepared)
|
||||
const payload = await prepareCashDiscountAllocation(context.server.db, context.tenantId, prepared)
|
||||
|
||||
const [created] = await context.server.db
|
||||
.insert(statementallocations)
|
||||
|
||||
@@ -41,10 +41,10 @@ function documentAmount(document: any, advanceInvoices: any[]) {
|
||||
return money(money(net) + money(tax19) + money(tax7) - advanceAmount)
|
||||
}
|
||||
|
||||
export function outgoingDocumentStatus(document: any, paidAmount: number, cancellations: any[], advanceInvoices: any[]) {
|
||||
export function outgoingDocumentStatus(document: any, paidAmount: number, cancellations: any[], advanceInvoices: any[], cashDiscountAmount = 0) {
|
||||
const amount = documentAmount(document, advanceInvoices)
|
||||
if (![amount, paidAmount].every(Number.isFinite)) throw new Error(`Beleg ${document.id}: Betrag kann nicht berechnet werden`)
|
||||
const openAmount = money(amount - paidAmount)
|
||||
if (![amount, paidAmount, cashDiscountAmount].every(Number.isFinite)) throw new Error(`Beleg ${document.id}: Betrag kann nicht berechnet werden`)
|
||||
const openAmount = money(amount - paidAmount - cashDiscountAmount)
|
||||
const cancelled = cancellations.length > 0
|
||||
const applicable = document.state === "Gebucht" && ["invoices", "advanceInvoices"].includes(document.type) && !cancelled
|
||||
// Signed invoices and refunds use the same payment-direction comparison.
|
||||
@@ -55,7 +55,7 @@ export function outgoingDocumentStatus(document: any, paidAmount: number, cancel
|
||||
id: document.id, type: document.type, documentNumber: document.documentNumber, documentDate: document.documentDate,
|
||||
state: document.state, customer: document.customer, project: document.project,
|
||||
cancelled, cancellationDocuments: cancellations.map(({ id, documentNumber }) => ({ id, documentNumber })),
|
||||
paymentStatus, amount, paidAmount: money(paidAmount), openAmount,
|
||||
paymentStatus, amount, paidAmount: money(paidAmount), cashDiscountAmount: money(cashDiscountAmount), openAmount,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ async function statusesFor(context: McpContext, documents: Array<typeof createdd
|
||||
const advanceIds = [...new Set(documents.flatMap(document => Array.isArray(document.usedAdvanceInvoices)
|
||||
? document.usedAdvanceInvoices.filter(id => typeof id === "number" && Number.isSafeInteger(id) && id > 0) as number[] : []))]
|
||||
const [allocations, cancellations, advances] = await Promise.all([
|
||||
context.server.db.select({ createddocument: statementallocations.createddocument, amount: statementallocations.amount }).from(statementallocations)
|
||||
context.server.db.select({ createddocument: statementallocations.createddocument, amount: statementallocations.amount, cashDiscountAmount: statementallocations.cashDiscountAmount }).from(statementallocations)
|
||||
.where(and(eq(statementallocations.tenant, context.tenantId), eq(statementallocations.archived, false), inArray(statementallocations.createddocument, ids))),
|
||||
context.server.db.select({ id: createddocuments.id, createddocument: createddocuments.createddocument, documentNumber: createddocuments.documentNumber }).from(createddocuments)
|
||||
.where(and(eq(createddocuments.tenant, context.tenantId), eq(createddocuments.archived, false), eq(createddocuments.type, "cancellationInvoices"),
|
||||
@@ -73,15 +73,19 @@ async function statusesFor(context: McpContext, documents: Array<typeof createdd
|
||||
advanceIds.length ? context.server.db.select().from(createddocuments)
|
||||
.where(and(eq(createddocuments.tenant, context.tenantId), eq(createddocuments.archived, false), inArray(createddocuments.id, advanceIds))) : [],
|
||||
])
|
||||
return documents.map(document => outgoingDocumentStatus(document,
|
||||
allocations.filter(allocation => allocation.createddocument === document.id).reduce((sum, allocation) => sum + Number(allocation.amount), 0),
|
||||
cancellations.filter(cancellation => cancellation.createddocument === document.id), advances))
|
||||
return documents.map(document => {
|
||||
const documentAllocations = allocations.filter(allocation => allocation.createddocument === document.id)
|
||||
return outgoingDocumentStatus(document,
|
||||
documentAllocations.reduce((sum, allocation) => sum + Number(allocation.amount), 0),
|
||||
cancellations.filter(cancellation => cancellation.createddocument === document.id), advances,
|
||||
documentAllocations.reduce((sum, allocation) => sum + Number(allocation.cashDiscountAmount || 0), 0))
|
||||
})
|
||||
}
|
||||
|
||||
export const outgoingStatusTools: McpTool[] = [
|
||||
{
|
||||
name: "accounting.outgoing_documents.status.get", title: "Ausgangsbelegstatus laden",
|
||||
description: "Lädt Buchungs-, Zahlungs- und Stornostatus mit Gesamtbetrag, zugeordneten Zahlungen und offenem Betrag. Zahlungsstatus gilt für gebuchte, nicht stornierte Rechnungen und Abschlagsrechnungen; sonst null. Ändert keine Belege oder Buchungen.",
|
||||
description: "Lädt Buchungs-, Zahlungs- und Stornostatus mit Gesamtbetrag, zugeordneten Zahlungen, gebuchtem Skonto und offenem Betrag. Zahlungsstatus gilt für gebuchte, nicht stornierte Rechnungen und Abschlagsrechnungen; sonst null. Ändert keine Belege oder Buchungen.",
|
||||
requiredPermissions: ["accounting.outgoing_documents.read"],
|
||||
inputSchema: { ...objectField({ id: idField }).schema, required: ["id"] },
|
||||
async handler(context, args) {
|
||||
|
||||
Reference in New Issue
Block a user