KI-AGENT: Kassenbucheintrag vereinfacht anlegen
This commit is contained in:
@@ -260,7 +260,7 @@ export default async function bankingRoutes(server: FastifyInstance) {
|
||||
}
|
||||
|
||||
if (!Number.isFinite(cashbookId)) return reply.code(400).send({ error: "Ungültige Kasse." })
|
||||
if (!body.date || !dayjs(body.date).isValid()) return reply.code(400).send({ error: "Bitte ein gültiges Buchungsdatum angeben." })
|
||||
const bookingDate = body.date && dayjs(body.date).isValid() ? dayjs(body.date) : dayjs()
|
||||
if (!Number.isFinite(Number(body.amount)) || Number(body.amount) <= 0) return reply.code(400).send({ error: "Der Betrag muss größer als 0 sein." })
|
||||
if (body.direction !== "income" && body.direction !== "expense") return reply.code(400).send({ error: "Bitte Einnahme oder Ausgabe auswählen." })
|
||||
|
||||
@@ -273,8 +273,11 @@ export default async function bankingRoutes(server: FastifyInstance) {
|
||||
|
||||
if (!cashbook[0]) return reply.code(404).send({ error: "Kasse nicht gefunden." })
|
||||
|
||||
const counterPayload = buildCashbookCounterPayload(String(body.counterType || ""), body.counterId)
|
||||
if (!counterPayload) return reply.code(400).send({ error: "Bitte ein Gegenkonto auswählen." })
|
||||
const hasCounterInput = Boolean(body.counterType || body.counterId)
|
||||
const counterPayload = hasCounterInput
|
||||
? buildCashbookCounterPayload(String(body.counterType || ""), body.counterId)
|
||||
: null
|
||||
if (hasCounterInput && !counterPayload) return reply.code(400).send({ error: "Bitte ein gültiges Gegenkonto auswählen." })
|
||||
|
||||
const signedAmount = body.direction === "income"
|
||||
? Math.abs(Number(body.amount))
|
||||
@@ -284,8 +287,8 @@ export default async function bankingRoutes(server: FastifyInstance) {
|
||||
const created = await server.db.transaction(async (tx) => {
|
||||
const insertedStatements = await tx.insert(bankstatements).values({
|
||||
account: cashbookId,
|
||||
date: dayjs(body.date).format("YYYY-MM-DD"),
|
||||
valueDate: dayjs(body.date).format("YYYY-MM-DD"),
|
||||
date: bookingDate.format("YYYY-MM-DD"),
|
||||
valueDate: bookingDate.format("YYYY-MM-DD"),
|
||||
amount: signedAmount,
|
||||
tenant: req.user.tenant_id,
|
||||
text: description,
|
||||
@@ -296,7 +299,8 @@ export default async function bankingRoutes(server: FastifyInstance) {
|
||||
}).returning()
|
||||
|
||||
const statement = insertedStatements[0]
|
||||
const insertedAllocations = await tx.insert(statementallocations).values({
|
||||
const insertedAllocations = counterPayload
|
||||
? await tx.insert(statementallocations).values({
|
||||
bankstatement: statement.id,
|
||||
amount: signedAmount,
|
||||
tenant: req.user.tenant_id,
|
||||
@@ -304,10 +308,11 @@ export default async function bankingRoutes(server: FastifyInstance) {
|
||||
datevTaxKey: body.datevTaxKey ? String(body.datevTaxKey).trim() : null,
|
||||
...counterPayload,
|
||||
}).returning()
|
||||
: []
|
||||
|
||||
return {
|
||||
statement,
|
||||
allocation: insertedAllocations[0],
|
||||
allocation: insertedAllocations[0] || null,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -14,10 +14,8 @@ const route = useRoute()
|
||||
|
||||
const bankstatements = ref([])
|
||||
const bankaccounts = ref([])
|
||||
const accounts = ref([])
|
||||
const customers = ref([])
|
||||
const vendors = ref([])
|
||||
const ownaccounts = ref([])
|
||||
const entitybankaccounts = ref([])
|
||||
const createddocuments = ref([])
|
||||
const incominginvoices = ref([])
|
||||
@@ -31,8 +29,6 @@ const savingCashbookBooking = ref(false)
|
||||
const loadingDocs = ref(true) // Startet im Ladezustand
|
||||
const suggestionsModalOpen = ref(false)
|
||||
const selectedSuggestionRowId = ref(null)
|
||||
const counterSearch = ref("")
|
||||
const expandedGroups = ref([])
|
||||
|
||||
const CASHBOOK_BANK_ID = "fedeo-cashbook"
|
||||
|
||||
@@ -62,22 +58,10 @@ const dateRange = ref({
|
||||
})
|
||||
|
||||
const cashbookForm = reactive({
|
||||
date: $dayjs().format("YYYY-MM-DD"),
|
||||
direction: "expense",
|
||||
amount: null,
|
||||
counter: "",
|
||||
datevTaxKey: "__none__",
|
||||
description: ""
|
||||
amount: null
|
||||
})
|
||||
|
||||
const DATEV_TAX_KEY_ITEMS = [
|
||||
{ value: "__none__", label: "Ohne Steuerschlüssel" },
|
||||
{ value: "9", label: "9 - Vorsteuer 19 %" },
|
||||
{ value: "8", label: "8 - Vorsteuer 7 %" },
|
||||
{ value: "19", label: "19 - EU Vorsteuer 19 %" },
|
||||
{ value: "18", label: "18 - EU Vorsteuer 7 %" }
|
||||
]
|
||||
|
||||
const getCalendarValue = (value) => {
|
||||
if (!value) return undefined
|
||||
|
||||
@@ -98,13 +82,11 @@ const setDateRangeFieldToToday = (field) => {
|
||||
const setupPage = async () => {
|
||||
loadingDocs.value = true
|
||||
try {
|
||||
const [statements, bankAccountItems, accountItems, customerItems, vendorItems, ownAccountItems, entityBankItems, documentItems, invoiceItems] = await Promise.all([
|
||||
const [statements, bankAccountItems, customerItems, vendorItems, entityBankItems, documentItems, invoiceItems] = await Promise.all([
|
||||
useEntities("bankstatements").select("*, statementallocations(*)", "valueDate", false),
|
||||
useEntities("bankaccounts").select(),
|
||||
useEntities("accounts").selectSpecial("*", "number", true),
|
||||
useEntities("customers").select(),
|
||||
useEntities("vendors").select(),
|
||||
useEntities("ownaccounts").select(),
|
||||
useEntities("entitybankaccounts").select(),
|
||||
useEntities("createddocuments").select("*, statementallocations(*), customer(id,name)"),
|
||||
useEntities("incominginvoices").select("*, statementallocations(*), vendor(*)")
|
||||
@@ -115,10 +97,8 @@ const setupPage = async () => {
|
||||
...account,
|
||||
displayLabel: getBankAccountLabel(account)
|
||||
}))
|
||||
accounts.value = accountItems
|
||||
customers.value = customerItems
|
||||
vendors.value = vendorItems
|
||||
ownaccounts.value = ownAccountItems
|
||||
entitybankaccounts.value = entityBankItems
|
||||
createddocuments.value = documentItems
|
||||
incominginvoices.value = invoiceItems.filter(i => i.state === "Gebucht")
|
||||
@@ -276,118 +256,25 @@ const currentCashbookBalance = computed(() => {
|
||||
.reduce((sum, statement) => sum + Number(statement.amount || 0), 0)
|
||||
return openingBalance + movementSum
|
||||
})
|
||||
const getIncomingInvoiceGross = (invoice) => Number((invoice.accounts || []).reduce((sum, account) => {
|
||||
return sum + Number(account.amountNet || 0) + Number(account.amountTax || 0)
|
||||
}, 0))
|
||||
const getIncomingInvoiceOpenAmount = (invoice) => {
|
||||
const gross = getIncomingInvoiceGross(invoice)
|
||||
const allocated = Number((invoice.statementallocations || []).reduce((sum, allocation) => sum + Number(allocation.amount || 0), 0))
|
||||
return Math.abs(gross) - Math.abs(allocated)
|
||||
}
|
||||
const buildCashbookEntries = (rows, type, labelBuilder) =>
|
||||
(rows || []).map((item) => ({
|
||||
key: `${type}:${item.id}`,
|
||||
id: item.id,
|
||||
type,
|
||||
number: item.number || item.vendorNumber || item.customerNumber || item.reference || "",
|
||||
name: item.label || item.name || item.vendor?.name || "",
|
||||
label: labelBuilder(item),
|
||||
typeLabel:
|
||||
type === "account"
|
||||
? "Sachkonten"
|
||||
: type === "vendor"
|
||||
? "Kreditoren"
|
||||
: type === "customer"
|
||||
? "Debitoren"
|
||||
: type === "incominginvoice"
|
||||
? "Eingangsbelege"
|
||||
: "Zusätzliche Konten"
|
||||
}))
|
||||
const cashbookEntryGroups = computed(() => ([
|
||||
{
|
||||
key: "account",
|
||||
label: "Sachkonten",
|
||||
entries: buildCashbookEntries(accounts.value, "account", (item) => `${item.number} - ${item.label}`)
|
||||
},
|
||||
{
|
||||
key: "vendor",
|
||||
label: "Kreditoren",
|
||||
entries: buildCashbookEntries(vendors.value, "vendor", (item) => `${item.vendorNumber || "ohne Nr."} - ${item.name}`)
|
||||
},
|
||||
{
|
||||
key: "customer",
|
||||
label: "Debitoren",
|
||||
entries: buildCashbookEntries(customers.value, "customer", (item) => `${item.customerNumber || "ohne Nr."} - ${item.name}`)
|
||||
},
|
||||
{
|
||||
key: "ownaccount",
|
||||
label: "Zusätzliche Konten",
|
||||
entries: buildCashbookEntries(ownaccounts.value, "ownaccount", (item) => `${item.number} - ${item.name}`)
|
||||
},
|
||||
{
|
||||
key: "incominginvoice",
|
||||
label: "Eingangsbelege",
|
||||
entries: buildCashbookEntries(
|
||||
incominginvoices.value.filter((invoice) => !invoice.archived && getIncomingInvoiceOpenAmount(invoice) > 0.004),
|
||||
"incominginvoice",
|
||||
(item) => `${item.reference || "Ohne Referenz"} - ${item.vendor?.name || "Ohne Lieferant"} - Offen ${displayCurrency(getIncomingInvoiceOpenAmount(item))}`
|
||||
)
|
||||
}
|
||||
]))
|
||||
const groupedCashbookCounterEntries = computed(() =>
|
||||
cashbookEntryGroups.value.map((group) => ({
|
||||
...group,
|
||||
entries: group.entries.filter((entry) => {
|
||||
const search = normalizeSuggestionText(counterSearch.value)
|
||||
if (!search) return true
|
||||
return [entry.number, entry.name, entry.label, entry.typeLabel]
|
||||
.some((value) => normalizeSuggestionText(value).includes(search))
|
||||
})
|
||||
})).filter((group) => group.entries.length > 0)
|
||||
)
|
||||
const selectedCashbookCounter = computed(() => cashbookEntryGroups.value.flatMap((group) => group.entries).find((entry) => entry.key === cashbookForm.counter))
|
||||
const isCashbookGroupExpanded = (groupKey) => expandedGroups.value.includes(groupKey)
|
||||
const toggleCashbookGroupExpanded = (groupKey) => {
|
||||
if (expandedGroups.value.includes(groupKey)) {
|
||||
expandedGroups.value = expandedGroups.value.filter((item) => item !== groupKey)
|
||||
return
|
||||
}
|
||||
expandedGroups.value = [...expandedGroups.value, groupKey]
|
||||
}
|
||||
const visibleCashbookEntries = (group) => {
|
||||
if (group.entries.length <= 5 || isCashbookGroupExpanded(group.key)) return group.entries
|
||||
return group.entries.slice(0, 5)
|
||||
}
|
||||
const saveCashbookBooking = async () => {
|
||||
if (!selectedCashbookAccount.value || !cashbookForm.date || !cashbookForm.amount || !cashbookForm.counter) {
|
||||
toast.add({ title: "Bitte Kasse, Datum, Betrag und Gegenkonto auswählen.", color: "warning" })
|
||||
if (!selectedCashbookAccount.value || !cashbookForm.amount) {
|
||||
toast.add({ title: "Bitte Kasse und Betrag auswählen.", color: "warning" })
|
||||
return
|
||||
}
|
||||
|
||||
const [counterType, counterId] = String(cashbookForm.counter).split(":")
|
||||
savingCashbookBooking.value = true
|
||||
try {
|
||||
await $api(`/api/banking/cashbooks/${selectedCashbookAccount.value.id}/bookings`, {
|
||||
const created = await $api(`/api/banking/cashbooks/${selectedCashbookAccount.value.id}/bookings`, {
|
||||
method: "POST",
|
||||
body: {
|
||||
date: cashbookForm.date,
|
||||
direction: cashbookForm.direction,
|
||||
amount: Number(cashbookForm.amount),
|
||||
counterType,
|
||||
counterId,
|
||||
datevTaxKey: cashbookForm.datevTaxKey === "__none__" ? null : cashbookForm.datevTaxKey,
|
||||
description: cashbookForm.description
|
||||
amount: Number(cashbookForm.amount)
|
||||
}
|
||||
})
|
||||
toast.add({ title: "Kassenbuchung erstellt." })
|
||||
cashbookForm.amount = null
|
||||
cashbookForm.counter = ""
|
||||
cashbookForm.datevTaxKey = "__none__"
|
||||
cashbookForm.description = ""
|
||||
counterSearch.value = ""
|
||||
expandedGroups.value = []
|
||||
cashbookBookingModalOpen.value = false
|
||||
await setupPage()
|
||||
await router.push(`/banking/statements/edit/${created.statement.id}`)
|
||||
} finally {
|
||||
savingCashbookBooking.value = false
|
||||
}
|
||||
@@ -876,7 +763,7 @@ onMounted(() => {
|
||||
</div>
|
||||
<PageLeaveGuard :when="isSyncing"/>
|
||||
|
||||
<UModal v-model:open="cashbookBookingModalOpen" :ui="{ width: 'sm:max-w-5xl' }">
|
||||
<UModal v-model:open="cashbookBookingModalOpen" :ui="{ width: 'sm:max-w-lg' }">
|
||||
<template #content>
|
||||
<UCard>
|
||||
<template #header>
|
||||
@@ -896,27 +783,12 @@ onMounted(() => {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="grid gap-3 sm:grid-cols-3">
|
||||
<UFormField label="Buchungsdatum">
|
||||
<UInput v-model="cashbookForm.date" type="date" />
|
||||
</UFormField>
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-3">
|
||||
<UFormField label="Betrag">
|
||||
<UInput v-model="cashbookForm.amount" type="number" min="0" step="0.01" placeholder="0,00" />
|
||||
</UFormField>
|
||||
<UFormField label="DATEV-Steuerschlüssel">
|
||||
<USelect
|
||||
v-model="cashbookForm.datevTaxKey"
|
||||
:items="DATEV_TAX_KEY_ITEMS"
|
||||
value-key="value"
|
||||
label-key="label"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6 xl:grid-cols-[220px_minmax(0,1fr)]">
|
||||
<div class="space-y-3">
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
<button
|
||||
type="button"
|
||||
class="w-full rounded-lg border px-4 py-3 text-left transition"
|
||||
@@ -940,60 +812,6 @@ onMounted(() => {
|
||||
<div class="text-sm text-gray-500">Bargeld wird aus der Kasse entnommen.</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-white">Gegenkonto</h3>
|
||||
<p class="text-sm text-gray-500">Sachkonto, Kreditor, Debitor, Eingangsbeleg oder zusätzliches Konto.</p>
|
||||
</div>
|
||||
<UInput v-model="counterSearch" icon="i-heroicons-magnifying-glass" placeholder="Gegenkonto durchsuchen..." class="max-w-sm" />
|
||||
</div>
|
||||
|
||||
<div class="max-h-[360px] space-y-4 overflow-y-auto pr-1">
|
||||
<div v-for="group in groupedCashbookCounterEntries" :key="group.key" class="space-y-2">
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500">{{ group.label }}</div>
|
||||
<div class="grid gap-2 md:grid-cols-2">
|
||||
<button
|
||||
v-for="entry in visibleCashbookEntries(group)"
|
||||
:key="entry.key"
|
||||
type="button"
|
||||
class="rounded-lg border px-3 py-2 text-left transition"
|
||||
:class="cashbookForm.counter === entry.key
|
||||
? 'border-primary-500 bg-primary-50 dark:bg-primary-950/30'
|
||||
: 'border-gray-200 bg-white hover:border-gray-300 dark:border-gray-800 dark:bg-gray-900'"
|
||||
@click="cashbookForm.counter = entry.key"
|
||||
>
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<span class="font-mono text-sm">{{ entry.number }}</span>
|
||||
<span class="truncate text-sm">{{ entry.name }}</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<UButton
|
||||
v-if="group.entries.length > 5"
|
||||
size="xs"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
:label="isCashbookGroupExpanded(group.key) ? 'Weniger anzeigen' : `${group.entries.length - 5} weitere anzeigen`"
|
||||
@click="toggleCashbookGroupExpanded(group.key)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UAlert
|
||||
v-if="selectedCashbookCounter"
|
||||
color="primary"
|
||||
variant="soft"
|
||||
icon="i-heroicons-arrows-right-left"
|
||||
:title="cashbookForm.direction === 'income' ? `${selectedCashbookAccount?.datevNumber} an ${selectedCashbookCounter.number}` : `${selectedCashbookCounter.number} an ${selectedCashbookAccount?.datevNumber}`"
|
||||
:description="`${selectedCashbookCounter.typeLabel.slice(0, -1)} ${selectedCashbookCounter.name} wird als Gegenkonto verwendet.`"
|
||||
/>
|
||||
|
||||
<UFormField label="Beschreibung">
|
||||
<UTextarea v-model="cashbookForm.description" placeholder="z. B. Bareinkauf Büromaterial" autoresize class="w-full" />
|
||||
</UFormField>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1003,7 +821,7 @@ onMounted(() => {
|
||||
Abbrechen
|
||||
</UButton>
|
||||
<UButton color="primary" icon="i-heroicons-check" :loading="savingCashbookBooking" @click="saveCashbookBooking">
|
||||
Eintrag speichern
|
||||
Anlegen und bearbeiten
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user