Ausgangsbelege um Kostenstellenzuordnung erweitern

This commit is contained in:
2026-05-26 16:21:24 +02:00
parent b59599cb92
commit cb09651d8d
11 changed files with 208 additions and 10 deletions

View File

@@ -54,6 +54,7 @@ const optionsToImport = ref({
contactPerson: true,
plant: true,
project:true,
costcentre: true,
description: true,
startText: false,
rows: true,
@@ -74,6 +75,7 @@ const mappings = ref({
contactPerson: "Ansprechpartner Mitarbeiter",
plant: "Objekt",
project: "Projekt",
costcentre: "Kostenstelle",
description: "Beschreibung",
startText: "Einleitung",
rows: "Positionen",

View File

@@ -10,6 +10,7 @@ const props = defineProps({
const loading = ref(true)
const incomingInvoices = ref([])
const createddocuments = ref([])
const costcentres = ref([])
const selectedYear = ref(String(dayjs().year()))
const selectedMonth = ref("all")
@@ -98,7 +99,7 @@ const monthItems = [
]
const reportRows = computed(() => {
return incomingInvoices.value.flatMap((invoice) => {
const incomingRows = incomingInvoices.value.flatMap((invoice) => {
const invoiceDate = invoice.date ? dayjs(invoice.date) : null
if (invoiceDate && invoiceDate.year().toString() !== selectedYear.value) {
@@ -120,7 +121,8 @@ const reportRows = computed(() => {
const accountCostCentre = costCentreMap.value.get(getCostCentreId(account.costCentre))
return {
id: `${invoice.id}-${index}`,
id: `incoming-${invoice.id}-${index}`,
sourceLabel: "Eingangsbeleg",
invoiceId: invoice.id,
reference: invoice.reference || "-",
date: invoice.date,
@@ -135,6 +137,53 @@ const reportRows = computed(() => {
}
})
})
const outgoingRows = createddocuments.value.flatMap((document) => {
const documentDate = document.documentDate ? dayjs(document.documentDate) : null
if (documentDate && documentDate.year().toString() !== selectedYear.value) {
return []
}
if (documentDate && selectedMonth.value !== "all" && documentDate.month() + 1 !== Number(selectedMonth.value)) {
return []
}
return (document.rows || [])
.filter((row) => !["pagebreak", "title", "text"].includes(row.mode))
.map((row, index) => {
const costCentreId = getCostCentreId(row.costCentre || row.costcentre || document.costcentre)
if (!relevantCostCentreIds.value.has(costCentreId)) {
return null
}
const amountNet = Number((Number(row.quantity || 0) * Number(row.price || 0) * (1 - Number(row.discountPercent || 0) / 100)).toFixed(2))
const taxPercent = Number(row.taxPercent || 0)
const amountTax = Number((amountNet * (taxPercent / 100)).toFixed(2))
const amountGross = Number((amountNet + amountTax).toFixed(2))
const accountCostCentre = costCentreMap.value.get(costCentreId)
return {
id: `outgoing-${document.id}-${index}`,
sourceLabel: "Ausgangsbeleg",
invoiceId: document.id,
reference: document.documentNumber || document.title || "-",
date: document.documentDate,
state: document.state || "-",
vendorName: document.customer?.name || "-",
accountLabel: "Umsatz",
costCentreName: accountCostCentre ? `${accountCostCentre.number} - ${accountCostCentre.name}` : "-",
description: row.text || row.description || document.description || "-",
amountNet,
amountTax,
amountGross
}
})
.filter(Boolean)
})
return [...incomingRows, ...outgoingRows]
})
const totals = computed(() => {
@@ -147,9 +196,10 @@ const totals = computed(() => {
})
const columns = [
{ accessorKey: "sourceLabel", header: "Art" },
{ accessorKey: "reference", header: "Beleg" },
{ accessorKey: "date", header: "Datum" },
{ accessorKey: "vendorName", header: "Lieferant" },
{ accessorKey: "vendorName", header: "Kontakt" },
{ accessorKey: "accountLabel", header: "Konto" },
{ accessorKey: "costCentreName", header: "Kostenstelle" },
{ accessorKey: "description", header: "Beschreibung" },
@@ -163,10 +213,16 @@ const setupPage = async () => {
costcentres.value = await useEntities("costcentres").select("*", null, false, true)
const invoices = await useEntities("incominginvoices").select("*, vendor(id,name)")
const documents = await useEntities("createddocuments").select("*, customer(id,name)")
incomingInvoices.value = invoices.filter((invoice) =>
(invoice.accounts || []).some((account) => relevantCostCentreIds.value.has(account.costCentre))
)
createddocuments.value = documents.filter((document) =>
["invoices", "advanceInvoices", "cancellationInvoices"].includes(document.type)
&& document.state === "Gebucht"
&& (document.rows || []).some((row) => relevantCostCentreIds.value.has(getCostCentreId(row.costCentre || row.costcentre || document.costcentre)))
)
const firstYear = yearItems.value[0]?.value
if (firstYear && !yearItems.value.some((item) => item.value === selectedYear.value)) {
@@ -264,7 +320,7 @@ setupPage()
<div class="text-right font-medium tabular-nums">{{ currency(row.original.amountGross) }}</div>
</template>
<template #empty>
<TableEmptyState label="Keine Eingangsbelege mit dieser Kostenstelle oder ihren Unterkostenstellen gefunden" />
<TableEmptyState label="Keine Belege mit dieser Kostenstelle oder ihren Unterkostenstellen gefunden" />
</template>
</UTable>

View File

@@ -43,6 +43,7 @@ const itemInfo = ref({
city: null,
},
project: null,
costcentre: null,
documentNumber: null,
documentNumberTitle: "Rechnungsnummer",
documentDate: dayjs(),
@@ -83,6 +84,7 @@ const letterheads = ref([])
const createddocuments = ref([])
const projects = ref([])
const plants = ref([])
const costcentres = ref([])
const products = ref([])
const productcategories = ref([])
const selectedProductcategorie = ref(null)
@@ -126,6 +128,10 @@ const getContractNumber = (id) => findById(contracts.value, id)?.contractNumber
const getLetterheadName = (id) => findById(letterheads.value, id)?.name || "Briefpapier nicht gefunden"
const getPlantName = (id) => findById(plants.value, id)?.name || "Objekt nicht gefunden"
const getProjectName = (id) => findById(projects.value, id)?.name || "Projekt nicht gefunden"
const getCostCentreName = (id) => {
const costcentre = findById(costcentres.value, id)
return costcentre ? [costcentre.number, costcentre.name].filter(Boolean).join(" - ") : "Keine Kostenstelle ausgewählt"
}
const getSelectedLetterhead = () => findById(letterheads.value, itemInfo.value.letterhead)
const normalizeExternalUrl = (value) => {
if (!value || typeof value !== "string") return null
@@ -216,6 +222,7 @@ const setupData = async () => {
createddocuments.value = await useEntities("createddocuments").select("*")
projects.value = await useEntities("projects").select("*")
plants.value = await useEntities("plants").select("*")
costcentres.value = await useEntities("costcentres").select("*")
services.value = await useEntities("services").select("*")
servicecategories.value = await useEntities("servicecategories").select("*")
products.value = await useEntities("products").select("*")
@@ -336,6 +343,8 @@ const normalizeCreatedDocumentRow = (row) => {
return {
...normalizedRow,
id: normalizedRow.id || uuidv4(),
costCentre: normalizedRow.costCentre || normalizedRow.costcentre || null,
purchasePrice: Number(normalizedRow.purchasePrice || 0),
linkedEntitys: Array.isArray(normalizedRow.linkedEntitys) ? normalizedRow.linkedEntitys : [],
}
}
@@ -554,6 +563,7 @@ const setupPage = async () => {
if (optionsToImport.contactPerson) itemInfo.value.contactPerson = linkedDocument.contactPerson
if (optionsToImport.plant) itemInfo.value.plant = linkedDocument.plant
if (optionsToImport.project) itemInfo.value.project = linkedDocument.project
if (optionsToImport.costcentre) itemInfo.value.costcentre = linkedDocument.costcentre
if (optionsToImport.title) itemInfo.value.title = linkedDocument.title
if (optionsToImport.description) itemInfo.value.description = linkedDocument.description
if (optionsToImport.startText) itemInfo.value.startText = linkedDocument.startText
@@ -580,6 +590,7 @@ const setupPage = async () => {
itemInfo.value.contactPerson = linkedDocument.contactPerson
itemInfo.value.plant = linkedDocument.plant
itemInfo.value.project = linkedDocument.project
itemInfo.value.costcentre = linkedDocument.costcentre
itemInfo.value.title = linkedDocument.title
itemInfo.value.description = linkedDocument.description
itemInfo.value.startText = linkedDocument.startText
@@ -790,6 +801,7 @@ const importPositions = () => {
price: advanceInvoiceData.value.part,
taxPercent: 19,
discountPercent: 0,
costCentre: itemInfo.value.costcentre,
advanceInvoiceData: advanceInvoiceData.value
})
setPosNumbers()
@@ -828,9 +840,11 @@ const addPosition = (mode) => {
quantity: 1,
unit: 1,
inputPrice: 0,
purchasePrice: 0,
price: 0,
taxPercent: taxPercentage,
discountPercent: 0,
costCentre: itemInfo.value.costcentre,
linkedEntitys: []
}
@@ -846,6 +860,7 @@ const addPosition = (mode) => {
taxPercent: taxPercentage,
discountPercent: 0,
unit: 1,
costCentre: itemInfo.value.costcentre,
linkedEntitys: []
})
} else if (mode === 'service') {
@@ -858,6 +873,7 @@ const addPosition = (mode) => {
taxPercent: taxPercentage,
discountPercent: 0,
unit: 1,
costCentre: itemInfo.value.costcentre,
linkedEntitys: []
}
@@ -1167,6 +1183,8 @@ const documentReport = computed(() => {
let product = products.value.find(i => i.id === row.product)
totalProductsPurchasePrice += (product?.purchase_price || 0) * row.quantity
} else if (row.mode === "free") {
totalProductsPurchasePrice += Number(row.purchasePrice || 0) * Number(row.quantity || 0)
} else if (row.service) {
let service = services.value.find(i => i.id === row.service)
@@ -1577,6 +1595,7 @@ const saveSerialInvoice = async () => {
contract: normalizeEntityId(itemInfo.value.contract),
address: itemInfo.value.address,
project: normalizeEntityId(itemInfo.value.project),
costcentre: itemInfo.value.costcentre,
paymentDays: itemInfo.value.paymentDays,
payment_type: itemInfo.value.payment_type,
outgoingsepamandate: normalizeEntityId(itemInfo.value.outgoingsepamandate),
@@ -1660,6 +1679,7 @@ const saveDocument = async (state, resetup = false) => {
contract: normalizeEntityId(itemInfo.value.contract),
address: itemInfo.value.address,
project: normalizeEntityId(itemInfo.value.project),
costcentre: itemInfo.value.costcentre,
plant: normalizeEntityId(itemInfo.value.plant),
documentNumber: itemInfo.value.documentNumber,
documentDate: itemInfo.value.documentDate,
@@ -1820,6 +1840,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
//row.unit = service.unit ? service.unit : services.value.find(i => i.id === row.service).unit
row.inputPrice = ((service.sellingPriceComposed.total || service.sellingPrice) ? (service.sellingPriceComposed.total || service.sellingPrice) : (services.value.find(i => i.id === row.service).sellingPriceComposed.total || services.value.find(i => i.id === row.service).sellingPrice))
row.description = service.description ? service.description : (services.value.find(i => i.id === row.service) ? services.value.find(i => i.id === row.service).description : "")
row.costCentre = service.costcentre || services.value.find(i => i.id === row.service)?.costcentre || itemInfo.value.costcentre
if (['13b UStG', '19 UStG'].includes(itemInfo.value.taxType)) {
row.taxPercent = 0
@@ -1832,6 +1853,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
console.log("Product Detected")
row.unit = product.unit ? product.unit : products.value.find(i => i.id === row.product).unit
row.inputPrice = (product.selling_price ? product.selling_price : products.value.find(i => i.id === row.product).selling_price)
row.costCentre = row.costCentre || itemInfo.value.costcentre
//row.price = Number((row.originalPrice * (1 + itemInfo.value.customSurchargePercentage /100)).toFixed(2))
row.description = product.description ? product.description : (products.value.find(i => i.id === row.product) ? products.value.find(i => i.id === row.product).description : "")
@@ -2512,6 +2534,41 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
/>
</InputGroup>
</UFormField>
<UFormField
label="Kostenstelle:"
>
<InputGroup>
<USelectMenu
:items="costcentres"
v-model="itemInfo.costcentre"
value-key="id"
label-key="name"
:search-input="{ placeholder: 'Suche...' }"
:filter-fields="['name', 'number']"
class="w-full"
>
<template #default>
{{ getCostCentreName(itemInfo.costcentre) }}
</template>
<template #item="{ item: costcentre }">
{{ [costcentre.number, costcentre.name].filter(Boolean).join(" - ") }}
</template>
</USelectMenu>
<UButton
variant="outline"
color="error"
v-if="itemInfo.costcentre"
icon="i-heroicons-x-mark"
@click="itemInfo.costcentre = null"
/>
<EntityModalButtons
type="costcentres"
:id="itemInfo.costcentre"
@return-data="(data) => itemInfo.costcentre = data.id"
/>
</InputGroup>
</UFormField>
<UFormField
label="Vertrag:"
@@ -2727,7 +2784,9 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
<th class="pl-2">Name</th>
<th class="pl-2">Menge</th>
<th class="pl-2">Einheit</th>
<th class="pl-2">Kostenstelle</th>
<th class="pl-2" v-if="!deliveryNoteLikeDocumentTypes.includes(itemInfo.type)">Preis</th>
<th class="pl-2" v-if="!deliveryNoteLikeDocumentTypes.includes(itemInfo.type)">EK</th>
<!-- <th class="pl-2" v-if="!deliveryNoteLikeDocumentTypes.includes(itemInfo.type)">Steuer</th>
<th class="pl-2" v-if="!deliveryNoteLikeDocumentTypes.includes(itemInfo.type)">Rabatt</th>-->
<th class="pl-2"></th>
@@ -2752,13 +2811,13 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
</td>
<td
v-if="row.mode === 'pagebreak'"
colspan="7"
colspan="9"
>
<USeparator/>
</td>
<td
v-if="row.mode === 'text'"
colspan="7"
colspan="9"
>
<!-- <UInput
v-model="row.text"
@@ -2972,6 +3031,28 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
</template>
</USelectMenu>
</td>
<td
class="w-full"
v-if="!['pagebreak','title','text'].includes(row.mode)"
>
<USelectMenu
v-model="row.costCentre"
:disabled="itemInfo.type === 'cancellationInvoices'"
:items="costcentres"
label-key="name"
value-key="id"
:search-input="{ placeholder: 'Suche...' }"
:filter-fields="['name', 'number']"
class="w-44"
>
<template #default>
<span class="truncate">{{ getCostCentreName(row.costCentre) }}</span>
</template>
<template #item="{ item: costcentre }">
{{ [costcentre.number, costcentre.name].filter(Boolean).join(" - ") }}
</template>
</USelectMenu>
</td>
<td
class="w-full"
v-if="!['pagebreak','title','text'].includes(row.mode) && !deliveryNoteLikeDocumentTypes.includes(itemInfo.type)"
@@ -3004,6 +3085,24 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
</template>
</UInput>
</td>
<td
class="w-full"
v-if="!['pagebreak','title','text'].includes(row.mode) && !deliveryNoteLikeDocumentTypes.includes(itemInfo.type)"
>
<UInput
v-if="row.mode === 'free'"
v-model="row.purchasePrice"
:disabled="itemInfo.type === 'cancellationInvoices'"
type="number"
step="0.001"
class="w-28"
>
<template #leading>
<span class="text-gray-500 dark:text-gray-400 text-xs">EUR</span>
</template>
</UInput>
<span v-else class="text-gray-400">-</span>
</td>
<!-- <td
class="w-40"
v-if="!['pagebreak','title','text'].includes(row.mode)&& !deliveryNoteLikeDocumentTypes.includes(itemInfo.type)"
@@ -3324,7 +3423,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {
</td>
<td
v-if="row.mode === 'title'"
colspan="6"
colspan="8"
>
<UInput
:disabled="itemInfo.type === 'cancellationInvoices'"

View File

@@ -2964,7 +2964,7 @@ export const useDataStore = defineStore('data', () => {
labelSingle: "Leistung",
isStandardEntity: true,
redirect: true,
selectWithInformation: "*, unit(*)",
selectWithInformation: "*, unit(*), costcentre(*)",
historyItemHolder: "service",
filters: [{
name: "Archivierte ausblenden",
@@ -3019,6 +3019,15 @@ export const useDataStore = defineStore('data', () => {
{label: "0 %", key: 0}]
//TODO: Default Value
},
{
key: "costcentre",
label: "Kostenstellen-Vorauswahl",
inputType: "select",
selectDataType: "costcentres",
selectOptionAttribute: "name",
selectSearchAttributes: ["name", "number"],
component: costcentre
},
{
key: "servicecategories",
label: "Leistungskategorien",