diff --git a/backend/db/schema/tenants.ts b/backend/db/schema/tenants.ts index ab4b5e4..190069d 100644 --- a/backend/db/schema/tenants.ts +++ b/backend/db/schema/tenants.ts @@ -128,8 +128,11 @@ export const tenants = pgTable( customers: { prefix: "", suffix: "", nextNumber: 10000 }, products: { prefix: "AT-", suffix: "", nextNumber: 1000 }, quotes: { prefix: "AN-", suffix: "", nextNumber: 1000 }, + costEstimates: { prefix: "KS-", suffix: "", nextNumber: 1000 }, confirmationOrders: { prefix: "AB-", suffix: "", nextNumber: 1000 }, invoices: { prefix: "RE-", suffix: "", nextNumber: 1000 }, + deliveryNotes: { prefix: "LS-", suffix: "", nextNumber: 1000 }, + packingSlips: { prefix: "PS-", suffix: "", nextNumber: 1000 }, spaces: { prefix: "LP-", suffix: "", nextNumber: 1000 }, customerspaces: { prefix: "KLP-", suffix: "", nextNumber: 1000 }, inventoryitems: { prefix: "IA-", suffix: "", nextNumber: 1000 }, diff --git a/backend/src/utils/functions.ts b/backend/src/utils/functions.ts index eaa4ab6..3fa2e59 100644 --- a/backend/src/utils/functions.ts +++ b/backend/src/utils/functions.ts @@ -12,6 +12,13 @@ export const useNextNumberRangeNumber = async ( tenantId: number, numberRange: string ) => { + const numberRangeFallbacks: Record = { + costEstimates: "quotes", + packingSlips: "deliveryNotes", + advanceInvoices: "invoices", + cancellationInvoices: "invoices", + } + const [tenant] = await server.db .select() .from(tenants) @@ -23,11 +30,15 @@ export const useNextNumberRangeNumber = async ( const numberRanges = tenant.numberRanges || {} - if (!numberRanges[numberRange]) { + const resolvedNumberRange = numberRanges[numberRange] + ? numberRange + : numberRangeFallbacks[numberRange] + + if (!resolvedNumberRange || !numberRanges[resolvedNumberRange]) { throw new Error(`Number range '${numberRange}' not found`) } - const current = numberRanges[numberRange] + const current = numberRanges[resolvedNumberRange] const usedNumber = (current.prefix || "") + @@ -37,7 +48,7 @@ export const useNextNumberRangeNumber = async ( const updatedRanges = { // @ts-ignore ...numberRanges, - [numberRange]: { + [resolvedNumberRange]: { ...current, nextNumber: current.nextNumber + 1, }, diff --git a/backend/src/utils/pdf.ts b/backend/src/utils/pdf.ts index c997cd2..3ba9248 100644 --- a/backend/src/utils/pdf.ts +++ b/backend/src/utils/pdf.ts @@ -58,6 +58,8 @@ const getDuration = (time) => { export const createInvoicePDF = async (server:FastifyInstance, returnMode, invoiceData, backgroundPath:string) => { + const deliveryNoteLikeDocumentTypes = ["deliveryNotes", "packingSlips"] + const isPackingSlip = invoiceData?.type === "packingSlips" const genPDF = async (invoiceData, backgroundSourceBuffer) => { const pdfDoc = await PDFDocument.create() @@ -347,7 +349,19 @@ export const createInvoicePDF = async (server:FastifyInstance, returnMode, invoi font: fontBold }) - if (invoiceData.type !== "deliveryNotes") { + if (isPackingSlip) { + pages[pageCounter - 1].drawText("Check", { + ...getCoordinatesForPDFLib(180, 137, page1), + size: 12, + color: rgb(0, 0, 0), + lineHeight: 12, + opacity: 1, + maxWidth: 240, + font: fontBold + }) + } + + if (!deliveryNoteLikeDocumentTypes.includes(invoiceData.type)) { pages[pageCounter - 1].drawText("Steuer", { ...getCoordinatesForPDFLib(135, 137, page1), size: 12, @@ -414,9 +428,21 @@ export const createInvoicePDF = async (server:FastifyInstance, returnMode, invoi maxWidth: 240 }) + if (isPackingSlip) { + pages[pageCounter - 1].drawRectangle({ + ...getCoordinatesForPDFLib(182, rowHeight + 1, page1), + width: 12, + height: 12, + borderColor: rgb(0, 0, 0), + borderWidth: 0.8, + opacity: 1, + borderOpacity: 1, + }) + } + let rowTextLines = 0 - if (invoiceData.type !== "deliveryNotes") { + if (!deliveryNoteLikeDocumentTypes.includes(invoiceData.type)) { pages[pageCounter - 1].drawText(splitStringBySpace(row.text, 35).join("\n"), { ...getCoordinatesForPDFLib(52, rowHeight, page1), size: 10, @@ -428,7 +454,7 @@ export const createInvoicePDF = async (server:FastifyInstance, returnMode, invoi rowTextLines = splitStringBySpace(row.text, 35).length } else { - pages[pageCounter - 1].drawText(splitStringBySpace(row.text, 80).join("\n"), { + pages[pageCounter - 1].drawText(splitStringBySpace(row.text, isPackingSlip ? 68 : 80).join("\n"), { ...getCoordinatesForPDFLib(52, rowHeight, page1), size: 10, color: rgb(0, 0, 0), @@ -437,13 +463,13 @@ export const createInvoicePDF = async (server:FastifyInstance, returnMode, invoi font: fontBold }) - rowTextLines = splitStringBySpace(row.text, 80).length + rowTextLines = splitStringBySpace(row.text, isPackingSlip ? 68 : 80).length } let rowDescriptionLines = 0 if (row.descriptionText) { - if (invoiceData.type !== "deliveryNotes") { + if (!deliveryNoteLikeDocumentTypes.includes(invoiceData.type)) { rowDescriptionLines = splitStringBySpace(row.descriptionText, 60).length pages[pageCounter - 1].drawText(splitStringBySpace(row.descriptionText, 60).join("\n"), { ...getCoordinatesForPDFLib(52, rowHeight + (rowTextLines * 5), page1), @@ -454,8 +480,8 @@ export const createInvoicePDF = async (server:FastifyInstance, returnMode, invoi }) } else { - rowDescriptionLines = splitStringBySpace(row.descriptionText, 80).length - pages[pageCounter - 1].drawText(splitStringBySpace(row.descriptionText, 80).join("\n"), { + rowDescriptionLines = splitStringBySpace(row.descriptionText, isPackingSlip ? 68 : 80).length + pages[pageCounter - 1].drawText(splitStringBySpace(row.descriptionText, isPackingSlip ? 68 : 80).join("\n"), { ...getCoordinatesForPDFLib(52, rowHeight + (rowTextLines * 5), page1), size: 10, color: rgb(0, 0, 0), @@ -466,7 +492,7 @@ export const createInvoicePDF = async (server:FastifyInstance, returnMode, invoi } - if (invoiceData.type !== "deliveryNotes") { + if (!deliveryNoteLikeDocumentTypes.includes(invoiceData.type)) { pages[pageCounter - 1].drawText(`${row.taxPercent} %`, { ...getCoordinatesForPDFLib(135, rowHeight, page1), size: 10, @@ -632,7 +658,19 @@ export const createInvoicePDF = async (server:FastifyInstance, returnMode, invoi font: fontBold }) - if (invoiceData.type !== "deliveryNotes") { + if (isPackingSlip) { + page.drawText("Check", { + ...getCoordinatesForPDFLib(180, 22, page1), + size: 12, + color: rgb(0, 0, 0), + lineHeight: 12, + opacity: 1, + maxWidth: 240, + font: fontBold + }) + } + + if (!deliveryNoteLikeDocumentTypes.includes(invoiceData.type)) { page.drawText("Steuer", { ...getCoordinatesForPDFLib(135, 22, page1), size: 12, @@ -742,7 +780,7 @@ export const createInvoicePDF = async (server:FastifyInstance, returnMode, invoi let endTextDiff = 35 - if (invoiceData.type !== "deliveryNotes") { + if (!deliveryNoteLikeDocumentTypes.includes(invoiceData.type)) { pages[pageCounter - 1].drawLine({ start: getCoordinatesForPDFLib(20, rowHeight, page1), end: getCoordinatesForPDFLib(198, rowHeight, page1), @@ -864,10 +902,10 @@ export const createInvoicePDF = async (server:FastifyInstance, returnMode, invoi opacity: 1, maxWidth: 500 }) - - return await pdfDoc.saveAsBase64() } + return await pdfDoc.saveAsBase64() + } const pdfBytes = await genPDF(invoiceData, await getBackgroundSourceBuffer(server,backgroundPath)) @@ -1138,4 +1176,4 @@ export const createTimeSheetPDF = async (server: FastifyInstance, returnMode, da console.log(error) throw error; // Fehler weiterwerfen, damit er oben ankommt } -} \ No newline at end of file +} diff --git a/frontend/components/EntityShowSubCreatedDocuments.vue b/frontend/components/EntityShowSubCreatedDocuments.vue index 6593399..5ceeccc 100644 --- a/frontend/components/EntityShowSubCreatedDocuments.vue +++ b/frontend/components/EntityShowSubCreatedDocuments.vue @@ -56,6 +56,7 @@ const dataStore = useDataStore() const tempStore = useTempStore() const router = useRouter() +const deliveryNoteLikeDocumentTypes = ['deliveryNotes', 'packingSlips'] const createddocuments = ref([]) @@ -117,7 +118,7 @@ const getAvailableQueryStringData = (keys) => { } const invoiceDeliveryNotes = () => { - router.push(`/createDocument/edit?type=invoices&loadMode=deliveryNotes&linkedDocuments=[${props.item.createddocuments.filter(i => i.type === "deliveryNotes").map(i => i.id)}]`) + router.push(`/createDocument/edit?type=invoices&loadMode=deliveryNotes&linkedDocuments=[${props.item.createddocuments.filter(i => deliveryNoteLikeDocumentTypes.includes(i.type)).map(i => i.id)}]`) } const showFinalInvoiceConfig = ref(false) @@ -150,13 +151,18 @@ const selectItem = (item) => { @click="invoiceDeliveryNotes" v-if="props.topLevelType === 'projects'" > - Lieferscheine abrechnen + Lieferscheine/Packscheine abrechnen + Angebot + + + Kostenschätzung + @@ -167,6 +173,11 @@ const selectItem = (item) => { > + Lieferschein + + + Packschein + @@ -198,7 +209,7 @@ const selectItem = (item) => { label="Rechnungsvorlage" > { {{ row.original.documentDate ? dayjs(row.original.documentDate).add(row.original.paymentDays,'day').format("DD.MM.YY") : '' }} diff --git a/frontend/numberRanges.json b/frontend/numberRanges.json index 5f40bd0..046bd2a 100644 --- a/frontend/numberRanges.json +++ b/frontend/numberRanges.json @@ -19,11 +19,26 @@ "suffix": "", "nextNumber": 1000 }, + "costEstimates": { + "prefix": "KS-", + "suffix": "", + "nextNumber": 1000 + }, "confirmationOrders": { "prefix": "AB-", "suffix": "", "nextNumber": 1000 }, + "deliveryNotes": { + "prefix": "LS-", + "suffix": "", + "nextNumber": 1000 + }, + "packingSlips": { + "prefix": "PS-", + "suffix": "", + "nextNumber": 1000 + }, "invoices": { "prefix": "RE-", "suffix": "", @@ -39,4 +54,4 @@ "suffix": "", "nextNumber": 1000 } -} \ No newline at end of file +} diff --git a/frontend/pages/createDocument/edit/[[id]].vue b/frontend/pages/createDocument/edit/[[id]].vue index 85330c7..44a63a5 100644 --- a/frontend/pages/createDocument/edit/[[id]].vue +++ b/frontend/pages/createDocument/edit/[[id]].vue @@ -12,6 +12,19 @@ const route = useRoute() const router = useRouter() const modal = useModal() const auth = useAuthStore() +const quoteLikeDocumentTypes = ["quotes", "costEstimates"] +const deliveryNoteLikeDocumentTypes = ["deliveryNotes", "packingSlips"] +const documentStorageFallbackTypes = { + costEstimates: "quotes", + packingSlips: "deliveryNotes", + advanceInvoices: "invoices", + cancellationInvoices: "invoices" +} +const textTemplateFallbackTypes = { + serialInvoices: "invoices", + costEstimates: "quotes", + packingSlips: "deliveryNotes" +} const guard = ref(true) @@ -303,7 +316,7 @@ const setupPage = async () => { itemInfo.value.rows.push(...linkedDocument.rows) } - for await (const doc of linkedDocuments.filter(i => i.type === "quotes")) { + for await (const doc of linkedDocuments.filter(i => quoteLikeDocumentTypes.includes(i.type))) { let linkedDocument = await useEntities("createddocuments").selectSingle(doc.id) itemInfo.value.rows.push({ @@ -471,20 +484,24 @@ const setDocumentTypeConfig = (withTexts = false) => { if (itemInfo.value.type === "invoices" || itemInfo.value.type === "advanceInvoices" || itemInfo.value.type === "serialInvoices" || itemInfo.value.type === "cancellationInvoices") { itemInfo.value.documentNumberTitle = "Rechnungsnummer" itemInfo.value.title = `Rechnung-Nr. ${itemInfo.value.documentNumber ? itemInfo.value.documentNumber : "XXXX"}` - } else if (itemInfo.value.type === "quotes") { - itemInfo.value.documentNumberTitle = "Angebotsnummer" - itemInfo.value.title = `Angebot-Nr. ${itemInfo.value.documentNumber ? itemInfo.value.documentNumber : "XXXX"}` - } else if (itemInfo.value.type === "deliveryNotes") { - itemInfo.value.documentNumberTitle = "Lieferscheinnummer" - itemInfo.value.title = `Lieferschein-Nr. ${itemInfo.value.documentNumber ? itemInfo.value.documentNumber : "XXXX"}` + } else if (quoteLikeDocumentTypes.includes(itemInfo.value.type)) { + const titlePrefix = itemInfo.value.type === "costEstimates" ? "Kostenschätzung" : "Angebot" + itemInfo.value.documentNumberTitle = itemInfo.value.type === "costEstimates" ? "Kostenschätzungsnummer" : "Angebotsnummer" + itemInfo.value.title = `${titlePrefix}-Nr. ${itemInfo.value.documentNumber ? itemInfo.value.documentNumber : "XXXX"}` + } else if (deliveryNoteLikeDocumentTypes.includes(itemInfo.value.type)) { + const titlePrefix = itemInfo.value.type === "packingSlips" ? "Packschein" : "Lieferschein" + itemInfo.value.documentNumberTitle = itemInfo.value.type === "packingSlips" ? "Packscheinnummer" : "Lieferscheinnummer" + itemInfo.value.title = `${titlePrefix}-Nr. ${itemInfo.value.documentNumber ? itemInfo.value.documentNumber : "XXXX"}` } else if (itemInfo.value.type === "confirmationOrders") { itemInfo.value.documentNumberTitle = "Auftragsbestätigungsnr." itemInfo.value.title = `Auftragsbestätigung-Nr. ${itemInfo.value.documentNumber ? itemInfo.value.documentNumber : "XXXX"}` } if (withTexts) { - itemInfo.value.startText = getTextTemplateByType(itemInfo.value.type).find(i => i.default && i.pos === "startText").text - itemInfo.value.endText = getTextTemplateByType(itemInfo.value.type).find(i => i.default && i.pos === "endText").text + const startTemplate = getTextTemplateByType(itemInfo.value.type).find(i => i.default && i.pos === "startText") + const endTemplate = getTextTemplateByType(itemInfo.value.type).find(i => i.default && i.pos === "endText") + itemInfo.value.startText = startTemplate?.text || null + itemInfo.value.endText = endTemplate?.text || null //itemInfo.value.startText = texttemplates.value.find(i => i.documentType === itemInfo.value.type && i.default && i.pos === "startText").text //itemInfo.value.endText = texttemplates.value.find(i => i.documentType === itemInfo.value.type && i.default && i.pos === "endText").text } @@ -771,16 +788,16 @@ const findDocumentErrors = computed(() => { } else { itemInfo.value.rows.forEach((row,index) => { - if (itemInfo.value.type !== "quotes" && row.optional) { + if (!quoteLikeDocumentTypes.includes(itemInfo.value.type) && row.optional) { errors.push({ - message: `Position ${row.pos} ist als Optional markiert. Dies wird nur in Angeboten unterstützt.`, + message: `Position ${row.pos} ist als Optional markiert. Dies wird nur in Angeboten und Kostenschätzungen unterstützt.`, type: "breaking" }) } - if (itemInfo.value.type !== "quotes" && row.alternative) { + if (!quoteLikeDocumentTypes.includes(itemInfo.value.type) && row.alternative) { errors.push({ - message: `Position ${row.pos} ist als Alternativ markiert. Dies wird nur in Angeboten unterstützt.`, + message: `Position ${row.pos} ist als Alternativ markiert. Dies wird nur in Angeboten und Kostenschätzungen unterstützt.`, type: "breaking" }) } @@ -805,11 +822,11 @@ const findDocumentErrors = computed(() => { if (["normal", "service", "free"].includes(row.mode)) { - if (!row.taxPercent && typeof row.taxPercent !== "number" && itemInfo.value.type !== "deliveryNotes") errors.push({ + if (!row.taxPercent && typeof row.taxPercent !== "number" && !deliveryNoteLikeDocumentTypes.includes(itemInfo.value.type)) errors.push({ message: `In Position ${row.pos} ist kein Steuersatz hinterlegt`, type: "breaking" }) - if (!row.price && typeof row.price !== "number" && itemInfo.value.type !== "deliveryNotes") errors.push({ + if (!row.price && typeof row.price !== "number" && !deliveryNoteLikeDocumentTypes.includes(itemInfo.value.type)) errors.push({ message: `In Position ${row.pos} ist kein Preis hinterlegt`, type: "breaking" }) @@ -1421,12 +1438,7 @@ const saveDocument = async (state, resetup = false) => { if (state !== "Entwurf") { console.log("???") - let type = "" - if (itemInfo.value.type === "advanceInvoices" || itemInfo.value.type === "cancellationInvoices") { - type = "invoices" - } else { - type = itemInfo.value.type - } + const type = itemInfo.value.type try { itemInfo.value.documentNumber = await useFunctions().useNextNumber(type) //data.usedNumber @@ -1466,7 +1478,7 @@ const saveDocument = async (state, resetup = false) => { let createData = { type: itemInfo.value.type, - taxType: ['invoices', 'cancellationInvoices', 'advanceInvoices', 'quotes', 'confirmationOrders'].includes(itemInfo.value.type) ? normalizeTaxTypeValue(itemInfo.value.taxType) : null, + taxType: ['invoices', 'cancellationInvoices', 'advanceInvoices', 'confirmationOrders', ...quoteLikeDocumentTypes].includes(itemInfo.value.type) ? normalizeTaxTypeValue(itemInfo.value.taxType) : null, state: itemInfo.value.state || "Entwurf", customer: itemInfo.value.customer, contact: itemInfo.value.contact, @@ -1529,11 +1541,7 @@ const closeDocument = async () => { fileData.project = itemInfo.value.project fileData.createddocument = itemInfo.value.id - let mappedType = itemInfo.value.type - - if (mappedType === "advanceInvoices" || mappedType === "cancellationInvoices") { - mappedType = "invoices" - } + let mappedType = documentStorageFallbackTypes[itemInfo.value.type] || itemInfo.value.type const folders = await useEntities("folders").select() console.log(folders) @@ -1571,11 +1579,7 @@ const closeDocument = async () => { const getTextTemplateByType = (type, pos) => { - let finalType = type - - if (type === "serialInvoices") { - finalType = "invoices" - } + const finalType = textTemplateFallbackTypes[type] || type if (pos) { return texttemplates.value.filter(i => i.documentType === finalType && i.pos === pos) @@ -1806,7 +1810,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = { Name Menge Einheit - Preis - + Preis + - Gesamt + Gesamt