Manuelle Buchungen in Statementallocations integrieren

This commit is contained in:
2026-04-23 16:28:44 +02:00
parent df4b591be4
commit 743bf0660c
8 changed files with 518 additions and 39 deletions

View File

@@ -11,10 +11,12 @@ import { DE_BANK_CODE_TO_BIC } from "../utils/deBankBics"
import {
bankrequisitions,
bankstatements,
accounts,
createddocuments,
customers,
entitybankaccounts,
incominginvoices,
ownaccounts,
statementallocations,
vendors,
} from "../../db/schema"
@@ -22,10 +24,57 @@ import {
import {
eq,
and,
isNull,
aliasedTable,
} from "drizzle-orm"
export default async function bankingRoutes(server: FastifyInstance) {
const ContraAccounts = aliasedTable(accounts, "contra_accounts")
const ContraCustomers = aliasedTable(customers, "contra_customers")
const ContraVendors = aliasedTable(vendors, "contra_vendors")
const ContraOwnaccounts = aliasedTable(ownaccounts, "contra_ownaccounts")
const normalizeManualSide = (payload: any, keys: string[]) =>
keys.filter((key) => payload[key] !== null && payload[key] !== undefined && payload[key] !== "")
const prepareStatementAllocationPayload = (payload: any) => {
const next = { ...payload }
const isManualBooking = !next.bankstatement
if (!isManualBooking) {
next.manualBookingDate = null
next.contraAccount = null
next.contraCustomer = null
next.contraVendor = null
next.contraOwnaccount = null
return { data: next }
}
const debitKeys = ["account", "customer", "vendor", "ownaccount"]
const creditKeys = ["contraAccount", "contraCustomer", "contraVendor", "contraOwnaccount"]
const debitSide = normalizeManualSide(next, debitKeys)
const creditSide = normalizeManualSide(next, creditKeys)
if (!next.manualBookingDate || !dayjs(next.manualBookingDate).isValid()) {
return { error: "Für manuelle Buchungen ist ein gültiges Buchungsdatum erforderlich." }
}
if (!Number.isFinite(Number(next.amount)) || Number(next.amount) <= 0) {
return { error: "Für manuelle Buchungen muss der Betrag größer als 0 sein." }
}
if (debitSide.length !== 1 || creditSide.length !== 1) {
return { error: "Für manuelle Buchungen muss genau ein Soll- und ein Haben-Konto ausgewählt werden." }
}
next.amount = Math.abs(Number(next.amount))
next.bankstatement = null
next.manualBookingDate = dayjs(next.manualBookingDate).format("YYYY-MM-DD")
return { data: next }
}
const normalizeIban = (value?: string | null) =>
String(value || "").replace(/\s+/g, "").toUpperCase()
@@ -677,6 +726,56 @@ export default async function bankingRoutes(server: FastifyInstance) {
}
})
// ------------------------------------------------------------------
// 📒 List Manual Statement Allocations
// ------------------------------------------------------------------
server.get("/banking/manual-bookings", async (req, reply) => {
try {
if (!req.user) return reply.code(401).send({ error: "Unauthorized" })
const rows = await server.db.select({
allocation: statementallocations,
account: accounts,
customer: customers,
vendor: vendors,
ownaccount: ownaccounts,
contraAccount: ContraAccounts,
contraCustomer: ContraCustomers,
contraVendor: ContraVendors,
contraOwnaccount: ContraOwnaccounts,
})
.from(statementallocations)
.leftJoin(accounts, eq(statementallocations.account, accounts.id))
.leftJoin(customers, eq(statementallocations.customer, customers.id))
.leftJoin(vendors, eq(statementallocations.vendor, vendors.id))
.leftJoin(ownaccounts, eq(statementallocations.ownaccount, ownaccounts.id))
.leftJoin(ContraAccounts, eq(statementallocations.contraAccount, ContraAccounts.id))
.leftJoin(ContraCustomers, eq(statementallocations.contraCustomer, ContraCustomers.id))
.leftJoin(ContraVendors, eq(statementallocations.contraVendor, ContraVendors.id))
.leftJoin(ContraOwnaccounts, eq(statementallocations.contraOwnaccount, ContraOwnaccounts.id))
.where(and(
eq(statementallocations.tenant, req.user.tenant_id),
eq(statementallocations.archived, false),
isNull(statementallocations.bankstatement)
))
return reply.send(rows.map((row) => ({
...row.allocation,
account: row.account,
customer: row.customer,
vendor: row.vendor,
ownaccount: row.ownaccount,
contraAccount: row.contraAccount,
contraCustomer: row.contraCustomer,
contraVendor: row.contraVendor,
contraOwnaccount: row.contraOwnaccount,
})))
} catch (err) {
console.error(err)
return reply.code(500).send({ error: "Failed to load manual bookings" })
}
})
// ------------------------------------------------------------------
// 💰 Create Statement Allocation
@@ -686,9 +785,11 @@ export default async function bankingRoutes(server: FastifyInstance) {
if (!req.user) return reply.code(401).send({ error: "Unauthorized" })
const { data: payload } = req.body as { data: any }
const prepared = prepareStatementAllocationPayload(payload)
if (prepared.error) return reply.code(400).send({ error: prepared.error })
const inserted = await server.db.insert(statementallocations).values({
...payload,
...prepared.data,
tenant: req.user.tenant_id
}).returning()
@@ -720,16 +821,18 @@ export default async function bankingRoutes(server: FastifyInstance) {
}
}
await insertHistoryItem(server, {
entity: "bankstatements",
entityId: Number(createdRecord.bankstatement),
action: "created",
created_by: req.user.user_id,
tenant_id: req.user.tenant_id,
oldVal: null,
newVal: createdRecord,
text: "Buchung erstellt",
})
if (createdRecord.bankstatement) {
await insertHistoryItem(server, {
entity: "bankstatements",
entityId: Number(createdRecord.bankstatement),
action: "created",
created_by: req.user.user_id,
tenant_id: req.user.tenant_id,
oldVal: null,
newVal: createdRecord,
text: "Buchung erstellt",
})
}
return reply.send(createdRecord)
@@ -763,16 +866,18 @@ export default async function bankingRoutes(server: FastifyInstance) {
.delete(statementallocations)
.where(eq(statementallocations.id, id))
await insertHistoryItem(server, {
entity: "bankstatements",
entityId: Number(old.bankstatement),
action: "deleted",
created_by: req.user.user_id,
tenant_id: req.user.tenant_id,
oldVal: old,
newVal: null,
text: "Buchung gelöscht",
})
if (old.bankstatement) {
await insertHistoryItem(server, {
entity: "bankstatements",
entityId: Number(old.bankstatement),
action: "deleted",
created_by: req.user.user_id,
tenant_id: req.user.tenant_id,
oldVal: old,
newVal: null,
text: "Buchung gelöscht",
})
}
return reply.send({ success: true })