diff --git a/backend/db/migrations/0050_outgoing_document_costcentres.sql b/backend/db/migrations/0050_outgoing_document_costcentres.sql new file mode 100644 index 0000000..ee9bf30 --- /dev/null +++ b/backend/db/migrations/0050_outgoing_document_costcentres.sql @@ -0,0 +1,7 @@ +ALTER TABLE "createddocuments" ADD COLUMN "costcentre" uuid; +--> statement-breakpoint +ALTER TABLE "services" ADD COLUMN "costcentre" uuid; +--> statement-breakpoint +ALTER TABLE "createddocuments" ADD CONSTRAINT "createddocuments_costcentre_costcentres_id_fk" FOREIGN KEY ("costcentre") REFERENCES "public"."costcentres"("id") ON DELETE no action ON UPDATE no action; +--> statement-breakpoint +ALTER TABLE "services" ADD CONSTRAINT "services_costcentre_costcentres_id_fk" FOREIGN KEY ("costcentre") REFERENCES "public"."costcentres"("id") ON DELETE no action ON UPDATE no action; diff --git a/backend/db/migrations/meta/_journal.json b/backend/db/migrations/meta/_journal.json index 7d702c2..c1bfbdf 100644 --- a/backend/db/migrations/meta/_journal.json +++ b/backend/db/migrations/meta/_journal.json @@ -344,6 +344,20 @@ "when": 1780174800000, "tag": "0048_mobile_push_devices", "breakpoints": true + }, + { + "idx": 49, + "version": "7", + "when": 1780178400000, + "tag": "0049_email_cache", + "breakpoints": true + }, + { + "idx": 50, + "version": "7", + "when": 1780261200000, + "tag": "0050_outgoing_document_costcentres", + "breakpoints": true } ] } diff --git a/backend/db/schema/createddocuments.ts b/backend/db/schema/createddocuments.ts index e93359b..40b74fa 100644 --- a/backend/db/schema/createddocuments.ts +++ b/backend/db/schema/createddocuments.ts @@ -20,6 +20,7 @@ import { plants } from "./plants" import { authUsers } from "./auth_users" import {serialExecutions} from "./serialexecutions"; import { outgoingsepamandates } from "./outgoingsepamandates" +import { costcentres } from "./costcentres" export const createddocuments = pgTable("createddocuments", { id: bigint("id", { mode: "number" }) @@ -49,6 +50,8 @@ export const createddocuments = pgTable("createddocuments", { () => projects.id ), + costcentre: uuid("costcentre").references(() => costcentres.id), + documentNumber: text("documentNumber"), documentDate: text("documentDate"), diff --git a/backend/db/schema/services.ts b/backend/db/schema/services.ts index c9491af..708d942 100644 --- a/backend/db/schema/services.ts +++ b/backend/db/schema/services.ts @@ -13,6 +13,7 @@ import { import { tenants } from "./tenants" import { units } from "./units" import { authUsers } from "./auth_users" +import { costcentres } from "./costcentres" export const services = pgTable("services", { id: bigint("id", { mode: "number" }) @@ -35,6 +36,8 @@ export const services = pgTable("services", { unit: bigint("unit", { mode: "number" }).references(() => units.id), + costcentre: uuid("costcentre").references(() => costcentres.id), + serviceNumber: bigint("serviceNumber", { mode: "number" }), tags: jsonb("tags").default([]), diff --git a/backend/src/mcp/tools/accounting.ts b/backend/src/mcp/tools/accounting.ts index 7e1e4f7..91c4d16 100644 --- a/backend/src/mcp/tools/accounting.ts +++ b/backend/src/mcp/tools/accounting.ts @@ -205,6 +205,8 @@ const buildOutgoingDocumentPayload = ( if (args[field] !== undefined) payload[field] = numberArg(args, field) } + if (args.costcentre !== undefined) payload.costcentre = stringArg(args, "costcentre") + for (const field of ["paymentDays"] as const) { if (args[field] !== undefined) payload[field] = numberArg(args, field) } @@ -458,6 +460,7 @@ export const accountingTools: McpTool[] = [ contact: { type: "number" }, contract: { type: "number" }, project: { type: "number" }, + costcentre: { type: "string", description: "Kostenstellen-UUID als Belegvorgabe." }, plant: { type: "number" }, documentDate: { type: "string" }, deliveryDate: { type: "string" }, @@ -512,6 +515,7 @@ export const accountingTools: McpTool[] = [ contact: { type: "number" }, contract: { type: "number" }, project: { type: "number" }, + costcentre: { type: "string", description: "Kostenstellen-UUID als Belegvorgabe." }, plant: { type: "number" }, documentDate: { type: "string" }, deliveryDate: { type: "string" }, diff --git a/backend/src/modules/serialexecution.service.ts b/backend/src/modules/serialexecution.service.ts index df3188a..14e5287 100644 --- a/backend/src/modules/serialexecution.service.ts +++ b/backend/src/modules/serialexecution.service.ts @@ -378,6 +378,7 @@ async function getSaveData(item: any, tenant: any, firstDate: string, lastDate: contract: item.contract, address: item.address, project: item.project, + costcentre: item.costcentre, documentDate: executionDate, deliveryDate: firstDate, deliveryDateEnd: lastDate, diff --git a/backend/src/utils/resource.config.ts b/backend/src/utils/resource.config.ts index 7c59fee..bf7e486 100644 --- a/backend/src/utils/resource.config.ts +++ b/backend/src/utils/resource.config.ts @@ -78,7 +78,7 @@ export const resourceConfig = { table: contracts, searchColumns: ["name", "notes", "contractNumber", "paymentType", "billingInterval", "sepaRef", "bankingName"], numberRangeHolder: "contractNumber", - mtoLoad: ["customer", "contracttype", "outgoingsepamandate"], + mtoLoad: ["customer", "contracttype", "contact", "outgoingsepamandate"], }, outgoingsepamandates: { table: outgoingsepamandates, @@ -230,7 +230,7 @@ export const resourceConfig = { }, createddocuments: { table: createddocuments, - mtoLoad: ["customer", "project", "contact", "contract", "plant","letterhead","createddocument", "outgoingsepamandate"], + mtoLoad: ["customer", "project", "costcentre", "contact", "contract", "plant","letterhead","createddocument", "outgoingsepamandate"], mtmLoad: ["statementallocations","files","createddocuments"], mtmListLoad: ["statementallocations", "files"], }, diff --git a/frontend/components/copyCreatedDocumentModal.vue b/frontend/components/copyCreatedDocumentModal.vue index 613e774..9ce1fba 100644 --- a/frontend/components/copyCreatedDocumentModal.vue +++ b/frontend/components/copyCreatedDocumentModal.vue @@ -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", diff --git a/frontend/components/costcentreDisplay.vue b/frontend/components/costcentreDisplay.vue index 2eb3113..09a4e06 100644 --- a/frontend/components/costcentreDisplay.vue +++ b/frontend/components/costcentreDisplay.vue @@ -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()
{{ currency(row.original.amountGross) }}
diff --git a/frontend/pages/createDocument/edit/[[id]].vue b/frontend/pages/createDocument/edit/[[id]].vue index e5df08d..30b0b53 100644 --- a/frontend/pages/createDocument/edit/[[id]].vue +++ b/frontend/pages/createDocument/edit/[[id]].vue @@ -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 = { /> + + + + + + + + + + + Name Menge Einheit + Kostenstelle Preis + EK @@ -2752,13 +2811,13 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = {