Compare commits
9 Commits
3230fc6cf0
...
d3ad53bcf0
| Author | SHA1 | Date | |
|---|---|---|---|
| d3ad53bcf0 | |||
| 4ab4c24b1a | |||
| e7107f151a | |||
| c222b793b0 | |||
| 5dcdc29a18 | |||
| fbea66b754 | |||
| 621243ba7b | |||
| dd2a9926dc | |||
| 769185cd82 |
@@ -60,6 +60,15 @@ export const tenants = pgTable(
|
|||||||
city: "",
|
city: "",
|
||||||
name: "",
|
name: "",
|
||||||
street: "",
|
street: "",
|
||||||
|
email: "",
|
||||||
|
phone: "",
|
||||||
|
website: "",
|
||||||
|
taxNumber: "",
|
||||||
|
vatId: "",
|
||||||
|
bankName: "",
|
||||||
|
bankAccountOwner: "",
|
||||||
|
iban: "",
|
||||||
|
bic: "",
|
||||||
}),
|
}),
|
||||||
|
|
||||||
features: jsonb("features").default({
|
features: jsonb("features").default({
|
||||||
|
|||||||
@@ -405,12 +405,15 @@ async function getCloseData(server:FastifyInstance,item: any, tenant: any, units
|
|||||||
|
|
||||||
console.log(item);
|
console.log(item);
|
||||||
|
|
||||||
const [contact, customer, project, contract] = await Promise.all([
|
const [contact, customer, project, contract, bankAccounts] = await Promise.all([
|
||||||
fetchById(server, schema.contacts, item.contact),
|
fetchById(server, schema.contacts, item.contact),
|
||||||
fetchById(server, schema.customers, item.customer),
|
fetchById(server, schema.customers, item.customer),
|
||||||
fetchById(server, schema.projects, item.project),
|
fetchById(server, schema.projects, item.project),
|
||||||
fetchById(server, schema.contracts, item.contract),
|
fetchById(server, schema.contracts, item.contract),
|
||||||
item.letterhead ? fetchById(server, schema.letterheads, item.letterhead) : null
|
server.db
|
||||||
|
.select()
|
||||||
|
.from(schema.bankaccounts)
|
||||||
|
.where(and(eq(schema.bankaccounts.tenant, tenant.id), eq(schema.bankaccounts.archived, false), eq(schema.bankaccounts.expired, false)))
|
||||||
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -433,7 +436,8 @@ async function getCloseData(server:FastifyInstance,item: any, tenant: any, units
|
|||||||
contract, // Contract Object
|
contract, // Contract Object
|
||||||
units, // Units Array
|
units, // Units Array
|
||||||
products, // Products Array
|
products, // Products Array
|
||||||
services // Services Array
|
services, // Services Array
|
||||||
|
bankAccounts // Tenant Bank Accounts
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
@@ -528,9 +532,14 @@ export function getDocumentDataBackend(
|
|||||||
contractData: any, // Vertrag
|
contractData: any, // Vertrag
|
||||||
units: any[], // Array aller Einheiten
|
units: any[], // Array aller Einheiten
|
||||||
products: any[], // Array aller Produkte
|
products: any[], // Array aller Produkte
|
||||||
services: any[] // Array aller Services
|
services: any[], // Array aller Services
|
||||||
|
bankAccounts: any[] = [] // Array aller Bankkonten
|
||||||
) {
|
) {
|
||||||
const businessInfo = tenant.businessInfo || {}; // Fallback falls leer
|
const businessInfo = tenant.businessInfo || {}; // Fallback falls leer
|
||||||
|
const paymentQrBankAccount = bankAccounts.find((account: any) => !account.archived && !account.expired && account.iban) || null
|
||||||
|
const paymentQrIban = businessInfo.iban || paymentQrBankAccount?.iban
|
||||||
|
const paymentQrBic = businessInfo.bic || paymentQrBankAccount?.bic
|
||||||
|
const paymentQrRecipient = businessInfo.bankAccountOwner || paymentQrBankAccount?.ownerName || businessInfo.name || tenant.name
|
||||||
|
|
||||||
// --- 1. Agriculture Logic ---
|
// --- 1. Agriculture Logic ---
|
||||||
// Prüfen ob 'extraModules' existiert, sonst leeres Array annehmen
|
// Prüfen ob 'extraModules' existiert, sonst leeres Array annehmen
|
||||||
@@ -724,6 +733,13 @@ export function getDocumentDataBackend(
|
|||||||
},
|
},
|
||||||
agriculture: itemInfo.agriculture,
|
agriculture: itemInfo.agriculture,
|
||||||
// Falls du AdvanceInvoices brauchst, musst du die Objekte hier übergeben oder leer lassen
|
// Falls du AdvanceInvoices brauchst, musst du die Objekte hier übergeben oder leer lassen
|
||||||
usedAdvanceInvoices: []
|
usedAdvanceInvoices: [],
|
||||||
|
paymentQr: paymentQrIban && itemInfo.payment_type === "transfer" ? {
|
||||||
|
iban: paymentQrIban,
|
||||||
|
bic: paymentQrBic,
|
||||||
|
recipientName: paymentQrRecipient,
|
||||||
|
amount: totals.totalGross,
|
||||||
|
remittance: itemInfo.documentNumber || itemInfo.title,
|
||||||
|
} : null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { s3 } from "../utils/s3";
|
|||||||
import { secrets } from "../utils/secrets";
|
import { secrets } from "../utils/secrets";
|
||||||
import { storeExtractedTextForFile } from "../utils/documentText";
|
import { storeExtractedTextForFile } from "../utils/documentText";
|
||||||
import { generateLiquidityForecast } from "../utils/liquidityForecast";
|
import { generateLiquidityForecast } from "../utils/liquidityForecast";
|
||||||
|
import { getUnsupportedPdfCharacterMessage } from "../utils/pdfCharacterError";
|
||||||
dayjs.extend(customParseFormat)
|
dayjs.extend(customParseFormat)
|
||||||
dayjs.extend(isoWeek)
|
dayjs.extend(isoWeek)
|
||||||
dayjs.extend(isBetween)
|
dayjs.extend(isBetween)
|
||||||
@@ -134,7 +135,15 @@ export default async function functionRoutes(server: FastifyInstance) {
|
|||||||
return pdf // Fastify wandelt automatisch in JSON
|
return pdf // Fastify wandelt automatisch in JSON
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
reply.code(500).send({ error: "Failed to create PDF" })
|
const unsupportedCharacterMessage = getUnsupportedPdfCharacterMessage(err, body.data)
|
||||||
|
if (unsupportedCharacterMessage) {
|
||||||
|
return reply.code(422).send({
|
||||||
|
error: unsupportedCharacterMessage,
|
||||||
|
code: "UNSUPPORTED_PDF_CHARACTER",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return reply.code(500).send({ error: "Failed to create PDF" })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -239,6 +239,13 @@ function isDateValue(value: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizeCreatedDocumentPayload(payload: Record<string, any>) {
|
function normalizeCreatedDocumentPayload(payload: Record<string, any>) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(payload, "customSurchargePercentage")) {
|
||||||
|
const customSurchargePercentage = Number(payload.customSurchargePercentage)
|
||||||
|
payload.customSurchargePercentage = Number.isFinite(customSurchargePercentage)
|
||||||
|
? customSurchargePercentage
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
|
||||||
const numberRelationFields = [
|
const numberRelationFields = [
|
||||||
"customer",
|
"customer",
|
||||||
"contact",
|
"contact",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {FastifyInstance} from "fastify";
|
|||||||
import { GetObjectCommand } from "@aws-sdk/client-s3";
|
import { GetObjectCommand } from "@aws-sdk/client-s3";
|
||||||
import { s3 } from "./s3";
|
import { s3 } from "./s3";
|
||||||
import { secrets } from "./secrets";
|
import { secrets } from "./secrets";
|
||||||
|
import bwipjs from "bwip-js"
|
||||||
|
|
||||||
const getCoordinatesForPDFLib = (x:number ,y:number, page:any) => {
|
const getCoordinatesForPDFLib = (x:number ,y:number, page:any) => {
|
||||||
/*
|
/*
|
||||||
@@ -29,6 +30,61 @@ const getRightAlignedX = (rightEdgeMm: number, text: string, size: number, font:
|
|||||||
return getCoordinatesForPDFLib(rightEdgeMm, 0, page).x - font.widthOfTextAtSize(text, size)
|
return getCoordinatesForPDFLib(rightEdgeMm, 0, page).x - font.widthOfTextAtSize(text, size)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalizePaymentQrText = (value: any, maxLength: number) => {
|
||||||
|
return String(value || "")
|
||||||
|
.replace(/\r?\n/g, " ")
|
||||||
|
.replace(/[^\x20-\x7EÄÖÜäöüß]/g, "")
|
||||||
|
.trim()
|
||||||
|
.slice(0, maxLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeIban = (value: any) => String(value || "").replace(/\s+/g, "").toUpperCase()
|
||||||
|
|
||||||
|
const getPaymentQrAmount = (value: any) => {
|
||||||
|
if (typeof value === "number" && Number.isFinite(value)) return value
|
||||||
|
|
||||||
|
const normalized = String(value || "")
|
||||||
|
.replace(/[^\d,.-]/g, "")
|
||||||
|
.replace(/\./g, "")
|
||||||
|
.replace(",", ".")
|
||||||
|
|
||||||
|
const amount = Number(normalized)
|
||||||
|
|
||||||
|
return Number.isFinite(amount) ? amount : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPaymentQrCodeBuffer = async (invoiceData: any) => {
|
||||||
|
if (invoiceData?.payment_type && invoiceData.payment_type !== "transfer") return null
|
||||||
|
|
||||||
|
const paymentQr = invoiceData?.paymentQr || {}
|
||||||
|
const iban = normalizeIban(paymentQr.iban)
|
||||||
|
const recipientName = normalizePaymentQrText(paymentQr.recipientName || paymentQr.name, 70)
|
||||||
|
const amount = getPaymentQrAmount(paymentQr.amount ?? invoiceData?.total?.totalSumToPay)
|
||||||
|
|
||||||
|
if (!iban || !recipientName || amount <= 0) return null
|
||||||
|
|
||||||
|
const epcPayload = [
|
||||||
|
"BCD",
|
||||||
|
"002",
|
||||||
|
"1",
|
||||||
|
"SCT",
|
||||||
|
normalizePaymentQrText(paymentQr.bic, 11),
|
||||||
|
recipientName,
|
||||||
|
iban,
|
||||||
|
`EUR${amount.toFixed(2)}`,
|
||||||
|
"",
|
||||||
|
normalizePaymentQrText(paymentQr.remittance || invoiceData?.documentNumber || invoiceData?.title, 140),
|
||||||
|
"",
|
||||||
|
].join("\n")
|
||||||
|
|
||||||
|
return bwipjs.toBuffer({
|
||||||
|
bcid: "qrcode",
|
||||||
|
text: epcPayload,
|
||||||
|
scale: 4,
|
||||||
|
includetext: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const getBackgroundSourceBuffer = async (server:FastifyInstance, path:string) => {
|
const getBackgroundSourceBuffer = async (server:FastifyInstance, path:string) => {
|
||||||
|
|
||||||
@@ -70,6 +126,16 @@ export const createInvoicePDF = async (server:FastifyInstance, returnMode, invoi
|
|||||||
|
|
||||||
const font = await pdfDoc.embedFont(StandardFonts.Helvetica)
|
const font = await pdfDoc.embedFont(StandardFonts.Helvetica)
|
||||||
const fontBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold)
|
const fontBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold)
|
||||||
|
const paymentQrCodeBuffer = await getPaymentQrCodeBuffer(invoiceData)
|
||||||
|
const paymentQrCode = paymentQrCodeBuffer ? await pdfDoc.embedPng(paymentQrCodeBuffer) : null
|
||||||
|
const mm = 2.83
|
||||||
|
const paymentQrLayout = {
|
||||||
|
sizeMm: 32,
|
||||||
|
rightMarginMm: 20,
|
||||||
|
bottomMarginMm: 40,
|
||||||
|
labelGapMm: 5,
|
||||||
|
safeGapMm: 5,
|
||||||
|
}
|
||||||
|
|
||||||
let pages = []
|
let pages = []
|
||||||
let pageCounter = 1
|
let pageCounter = 1
|
||||||
@@ -81,6 +147,21 @@ export const createInvoicePDF = async (server:FastifyInstance, returnMode, invoi
|
|||||||
const firstPageBackground = await pdfDoc.embedPage(backgroudPdf.getPages()[0])
|
const firstPageBackground = await pdfDoc.embedPage(backgroudPdf.getPages()[0])
|
||||||
const secondPageBackground = await pdfDoc.embedPage(backgroudPdf.getPages()[backgroudPdf.getPages().length > 1 ? 1 : 0])
|
const secondPageBackground = await pdfDoc.embedPage(backgroudPdf.getPages()[backgroudPdf.getPages().length > 1 ? 1 : 0])
|
||||||
|
|
||||||
|
const getPaymentQrX = (page: any) => {
|
||||||
|
return page.getWidth()
|
||||||
|
- paymentQrLayout.rightMarginMm * mm
|
||||||
|
- paymentQrLayout.sizeMm * mm
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTextWidthBeforePaymentQr = (page: any, startX: number) => {
|
||||||
|
if (!paymentQrCode) return 500
|
||||||
|
|
||||||
|
return Math.max(
|
||||||
|
240,
|
||||||
|
getPaymentQrX(page) - startX - paymentQrLayout.safeGapMm * mm
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
const page1 = pdfDoc.addPage()
|
const page1 = pdfDoc.addPage()
|
||||||
|
|
||||||
@@ -916,14 +997,42 @@ export const createInvoicePDF = async (server:FastifyInstance, returnMode, invoi
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const endTextPosition = getCoordinatesForPDFLib(21, rowHeight + endTextDiff + (invoiceData.totalArray.length - 3) * 8, page1)
|
||||||
|
|
||||||
pages[pageCounter - 1].drawText(invoiceData.endText, {
|
pages[pageCounter - 1].drawText(invoiceData.endText, {
|
||||||
...getCoordinatesForPDFLib(21, rowHeight + endTextDiff + (invoiceData.totalArray.length - 3) * 8, page1),
|
...endTextPosition,
|
||||||
size: 10,
|
size: 10,
|
||||||
color: rgb(0, 0, 0),
|
color: rgb(0, 0, 0),
|
||||||
lineHeight: 10,
|
lineHeight: 10,
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
maxWidth: 500
|
maxWidth: getTextWidthBeforePaymentQr(pages[pageCounter - 1], endTextPosition.x)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (paymentQrCode) {
|
||||||
|
const lastPage = pages[pages.length - 1]
|
||||||
|
const qrSize = paymentQrLayout.sizeMm * mm
|
||||||
|
const bottomMargin = paymentQrLayout.bottomMarginMm * mm
|
||||||
|
const labelGap = paymentQrLayout.labelGapMm * mm
|
||||||
|
const qrX = getPaymentQrX(lastPage)
|
||||||
|
const qrY = bottomMargin
|
||||||
|
|
||||||
|
lastPage.drawText("Überweisung per QR-Code", {
|
||||||
|
x: qrX,
|
||||||
|
y: qrY + qrSize + labelGap,
|
||||||
|
size: 8,
|
||||||
|
color: rgb(0, 0, 0),
|
||||||
|
lineHeight: 8,
|
||||||
|
opacity: 1,
|
||||||
|
maxWidth: qrSize,
|
||||||
|
font: fontBold,
|
||||||
|
})
|
||||||
|
|
||||||
|
lastPage.drawImage(paymentQrCode, {
|
||||||
|
x: qrX,
|
||||||
|
y: qrY,
|
||||||
|
width: qrSize,
|
||||||
|
height: qrSize,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
79
backend/src/utils/pdfCharacterError.ts
Normal file
79
backend/src/utils/pdfCharacterError.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
const PDF_CHARACTER_ERROR_PATTERN = /WinAnsi cannot encode "[\s\S]+?" \(0x([0-9a-f]+)\)/i
|
||||||
|
|
||||||
|
const DOCUMENT_FIELD_LABELS: Record<string, string> = {
|
||||||
|
title: "Dokumenttitel",
|
||||||
|
description: "Dokumentbeschreibung",
|
||||||
|
startText: "Einleitungstext",
|
||||||
|
endText: "Schlusstext",
|
||||||
|
text: "Bezeichnung",
|
||||||
|
descriptionText: "Beschreibung",
|
||||||
|
unit: "Einheit",
|
||||||
|
}
|
||||||
|
|
||||||
|
type CharacterLocation = {
|
||||||
|
label: string
|
||||||
|
characterPosition: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelForPath = (path: Array<string | number>, root: any): string => {
|
||||||
|
if (path[0] === "rows" && typeof path[1] === "number") {
|
||||||
|
const row = root?.rows?.[path[1]]
|
||||||
|
const position = row?.pos ? `Position ${row.pos}` : `Dokumentzeile ${path[1] + 1}`
|
||||||
|
const field = DOCUMENT_FIELD_LABELS[String(path[path.length - 1])] || String(path[path.length - 1])
|
||||||
|
return `${position}, ${field}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const field = String(path[path.length - 1])
|
||||||
|
return DOCUMENT_FIELD_LABELS[field] || field
|
||||||
|
}
|
||||||
|
|
||||||
|
const findCharacterLocations = (
|
||||||
|
value: unknown,
|
||||||
|
unsupportedCharacter: string,
|
||||||
|
root: any,
|
||||||
|
path: Array<string | number> = [],
|
||||||
|
): CharacterLocation[] => {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
const characters = Array.from(value)
|
||||||
|
return characters.flatMap((character, index) =>
|
||||||
|
character === unsupportedCharacter
|
||||||
|
? [{ label: labelForPath(path, root), characterPosition: index + 1 }]
|
||||||
|
: []
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.flatMap((entry, index) =>
|
||||||
|
findCharacterLocations(entry, unsupportedCharacter, root, [...path, index])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value && typeof value === "object") {
|
||||||
|
return Object.entries(value).flatMap(([key, entry]) =>
|
||||||
|
findCharacterLocations(entry, unsupportedCharacter, root, [...path, key])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getUnsupportedPdfCharacterMessage = (error: unknown, documentData: any): string | null => {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||||
|
const match = errorMessage.match(PDF_CHARACTER_ERROR_PATTERN)
|
||||||
|
if (!match) return null
|
||||||
|
|
||||||
|
const numericCodePoint = Number.parseInt(match[1], 16)
|
||||||
|
if (!Number.isSafeInteger(numericCodePoint) || numericCodePoint > 0x10FFFF) return null
|
||||||
|
|
||||||
|
const unsupportedCharacter = String.fromCodePoint(numericCodePoint)
|
||||||
|
const [location] = findCharacterLocations(documentData, unsupportedCharacter, documentData)
|
||||||
|
const codePoint = numericCodePoint
|
||||||
|
.toString(16)
|
||||||
|
.toUpperCase()
|
||||||
|
.padStart(4, "0")
|
||||||
|
const locationText = location
|
||||||
|
? ` Fundstelle: ${location.label}, Zeichen ${location.characterPosition}.`
|
||||||
|
: ""
|
||||||
|
|
||||||
|
return `Das Zeichen „${unsupportedCharacter}“ (Unicode U+${codePoint}) wird in PDF-Dokumenten nicht unterstützt.${locationText} Bitte ersetze oder entferne das Zeichen.`
|
||||||
|
}
|
||||||
27
backend/tests/pdfCharacterError.test.ts
Normal file
27
backend/tests/pdfCharacterError.test.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import assert from "node:assert/strict"
|
||||||
|
import test from "node:test"
|
||||||
|
import { getUnsupportedPdfCharacterMessage } from "../src/utils/pdfCharacterError"
|
||||||
|
|
||||||
|
test("reports unsupported characters with the document position and character offset", () => {
|
||||||
|
// pdf-lib displays supplementary Unicode characters as a private-use glyph,
|
||||||
|
// while the hexadecimal value still contains the original code point.
|
||||||
|
const error = new Error('WinAnsi cannot encode "" (0x1f600)')
|
||||||
|
const documentData = {
|
||||||
|
rows: [
|
||||||
|
{ pos: "1", text: "Montage" },
|
||||||
|
{ pos: "2.3", descriptionText: "Gerät 😀 montieren" },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
getUnsupportedPdfCharacterMessage(error, documentData),
|
||||||
|
"Das Zeichen „😀“ (Unicode U+1F600) wird in PDF-Dokumenten nicht unterstützt. Fundstelle: Position 2.3, Beschreibung, Zeichen 7. Bitte ersetze oder entferne das Zeichen."
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns null for unrelated PDF errors", () => {
|
||||||
|
assert.equal(
|
||||||
|
getUnsupportedPdfCharacterMessage(new Error("Background PDF is invalid"), {}),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
})
|
||||||
@@ -96,6 +96,7 @@ const customers = ref([])
|
|||||||
const contacts = ref([])
|
const contacts = ref([])
|
||||||
const contracts = ref([])
|
const contracts = ref([])
|
||||||
const outgoingsepamandates = ref([])
|
const outgoingsepamandates = ref([])
|
||||||
|
const bankaccounts = ref([])
|
||||||
const texttemplates = ref([])
|
const texttemplates = ref([])
|
||||||
const units = ref([])
|
const units = ref([])
|
||||||
const tenantUsers = ref([])
|
const tenantUsers = ref([])
|
||||||
@@ -237,6 +238,12 @@ const setupData = async () => {
|
|||||||
contacts.value = await useEntities("contacts").select("*")
|
contacts.value = await useEntities("contacts").select("*")
|
||||||
contracts.value = await useEntities("contracts").select("*")
|
contracts.value = await useEntities("contracts").select("*")
|
||||||
outgoingsepamandates.value = await useEntities("outgoingsepamandates").select("*")
|
outgoingsepamandates.value = await useEntities("outgoingsepamandates").select("*")
|
||||||
|
try {
|
||||||
|
bankaccounts.value = await useEntities("bankaccounts").select("*")
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Bankkonten konnten nicht für den Zahlungs-QR-Code geladen werden", error)
|
||||||
|
bankaccounts.value = []
|
||||||
|
}
|
||||||
texttemplates.value = await useEntities("texttemplates").select("*")
|
texttemplates.value = await useEntities("texttemplates").select("*")
|
||||||
units.value = await useEntities("units").selectSpecial("*")
|
units.value = await useEntities("units").selectSpecial("*")
|
||||||
tenantUsers.value = (await useNuxtApp().$api(`/api/tenant/users`, {
|
tenantUsers.value = (await useNuxtApp().$api(`/api/tenant/users`, {
|
||||||
@@ -1404,7 +1411,11 @@ const getDocumentData = async () => {
|
|||||||
|
|
||||||
let customerData = customers.value.find(i => i.id === itemInfo.value.customer)
|
let customerData = customers.value.find(i => i.id === itemInfo.value.customer)
|
||||||
let contactData = contacts.value.find(i => i.id === itemInfo.value.contact)
|
let contactData = contacts.value.find(i => i.id === itemInfo.value.contact)
|
||||||
let businessInfo = auth.activeTenantData.businessInfo
|
let businessInfo = auth.activeTenantData.businessInfo || {}
|
||||||
|
const paymentQrBankAccount = bankaccounts.value.find(account => !account.archived && !account.expired && account.iban)
|
||||||
|
const paymentQrIban = businessInfo.iban || paymentQrBankAccount?.iban
|
||||||
|
const paymentQrBic = businessInfo.bic || paymentQrBankAccount?.bic
|
||||||
|
const paymentQrRecipient = businessInfo.bankAccountOwner || paymentQrBankAccount?.ownerName || businessInfo.name || auth.activeTenantData.name
|
||||||
|
|
||||||
if (auth.activeTenantData.extraModules.includes("agriculture")) {
|
if (auth.activeTenantData.extraModules.includes("agriculture")) {
|
||||||
itemInfo.value.rows.forEach(row => {
|
itemInfo.value.rows.forEach(row => {
|
||||||
@@ -1620,7 +1631,14 @@ const getDocumentData = async () => {
|
|||||||
agriculture: itemInfo.value.agriculture,
|
agriculture: itemInfo.value.agriculture,
|
||||||
usedAdvanceInvoices: itemInfo.value.usedAdvanceInvoices.map(i => {
|
usedAdvanceInvoices: itemInfo.value.usedAdvanceInvoices.map(i => {
|
||||||
return createddocuments.value.find(x => x.id === i)
|
return createddocuments.value.find(x => x.id === i)
|
||||||
})
|
}),
|
||||||
|
paymentQr: paymentQrIban && itemInfo.value.payment_type === "transfer" ? {
|
||||||
|
iban: paymentQrIban,
|
||||||
|
bic: paymentQrBic,
|
||||||
|
recipientName: paymentQrRecipient,
|
||||||
|
amount: documentTotal.value.totalSumToPay,
|
||||||
|
remittance: itemInfo.value.documentNumber || itemInfo.value.title,
|
||||||
|
} : null
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(returnData)
|
console.log(returnData)
|
||||||
@@ -1741,6 +1759,7 @@ const saveSerialInvoice = async () => {
|
|||||||
const saveDocument = async (state, resetup = false) => {
|
const saveDocument = async (state, resetup = false) => {
|
||||||
|
|
||||||
itemInfo.value.state = state
|
itemInfo.value.state = state
|
||||||
|
normalizeCustomSurchargePercentage()
|
||||||
|
|
||||||
if (state !== "Entwurf") {
|
if (state !== "Entwurf") {
|
||||||
console.log("???")
|
console.log("???")
|
||||||
@@ -1910,7 +1929,14 @@ const checkCompatibilityWithInputPrice = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalizeCustomSurchargePercentage = () => {
|
||||||
|
const percentage = Number(itemInfo.value.customSurchargePercentage)
|
||||||
|
itemInfo.value.customSurchargePercentage = Number.isFinite(percentage) ? percentage : 0
|
||||||
|
}
|
||||||
|
|
||||||
const updateCustomSurcharge = () => {
|
const updateCustomSurcharge = () => {
|
||||||
|
normalizeCustomSurchargePercentage()
|
||||||
|
|
||||||
itemInfo.value.rows.forEach(row => {
|
itemInfo.value.rows.forEach(row => {
|
||||||
if (!["pagebreak", "title", "text"].includes(row.mode) && isImportedAdvanceInvoiceRow(row)) {
|
if (!["pagebreak", "title", "text"].includes(row.mode) && isImportedAdvanceInvoiceRow(row)) {
|
||||||
row.price = Number(row.inputPrice || 0)
|
row.price = Number(row.inputPrice || 0)
|
||||||
|
|||||||
@@ -214,17 +214,34 @@ const activeTelephonyTargetOptions = computed(() => {
|
|||||||
if (telephonyExtensionForm.targetType === "branch") return telephonyExtensionOptions.value.branches || []
|
if (telephonyExtensionForm.targetType === "branch") return telephonyExtensionOptions.value.branches || []
|
||||||
return telephonyExtensionOptions.value.users || []
|
return telephonyExtensionOptions.value.users || []
|
||||||
})
|
})
|
||||||
|
const defaultBusinessInfo = {
|
||||||
|
name: "",
|
||||||
|
street: "",
|
||||||
|
zip: "",
|
||||||
|
city: "",
|
||||||
|
email: "",
|
||||||
|
phone: "",
|
||||||
|
website: "",
|
||||||
|
taxNumber: "",
|
||||||
|
vatId: "",
|
||||||
|
bankName: "",
|
||||||
|
bankAccountOwner: "",
|
||||||
|
iban: "",
|
||||||
|
bic: "",
|
||||||
|
}
|
||||||
|
const normalizeBusinessInfo = (value) => ({ ...defaultBusinessInfo, ...(value || {}) })
|
||||||
|
|
||||||
const setupPage = async () => {
|
const setupPage = async () => {
|
||||||
itemInfo.value = auth.activeTenantData
|
itemInfo.value = auth.activeTenantData
|
||||||
console.log(itemInfo.value)
|
console.log(itemInfo.value)
|
||||||
|
businessInfo.value = normalizeBusinessInfo(auth.activeTenantData?.businessInfo)
|
||||||
planningBoardConfig.startTime = auth.activeTenantData?.calendarConfig?.planningBoard?.startTime || "06:00"
|
planningBoardConfig.startTime = auth.activeTenantData?.calendarConfig?.planningBoard?.startTime || "06:00"
|
||||||
planningBoardConfig.endTime = auth.activeTenantData?.calendarConfig?.planningBoard?.endTime || "21:00"
|
planningBoardConfig.endTime = auth.activeTenantData?.calendarConfig?.planningBoard?.endTime || "21:00"
|
||||||
planningBoardConfig.slotMinutes = auth.activeTenantData?.calendarConfig?.planningBoard?.slotMinutes || 180
|
planningBoardConfig.slotMinutes = auth.activeTenantData?.calendarConfig?.planningBoard?.slotMinutes || 180
|
||||||
}
|
}
|
||||||
|
|
||||||
const features = ref({ ...defaultFeatures, ...(auth.activeTenantData?.features || {}) })
|
const features = ref({ ...defaultFeatures, ...(auth.activeTenantData?.features || {}) })
|
||||||
const businessInfo = ref(auth.activeTenantData.businessInfo)
|
const businessInfo = ref(normalizeBusinessInfo(auth.activeTenantData?.businessInfo))
|
||||||
const accountChart = ref(auth.activeTenantData.accountChart || "skr03")
|
const accountChart = ref(auth.activeTenantData.accountChart || "skr03")
|
||||||
const taxEvaluationPeriod = ref(normalizeTaxEvaluationPeriod(auth.activeTenantData.taxEvaluationPeriod))
|
const taxEvaluationPeriod = ref(normalizeTaxEvaluationPeriod(auth.activeTenantData.taxEvaluationPeriod))
|
||||||
const accountChartOptions = [
|
const accountChartOptions = [
|
||||||
@@ -245,6 +262,7 @@ const updateTenant = async (newData) => {
|
|||||||
itemInfo.value = res
|
itemInfo.value = res
|
||||||
auth.activeTenantData = res
|
auth.activeTenantData = res
|
||||||
features.value = { ...defaultFeatures, ...(res?.features || {}) }
|
features.value = { ...defaultFeatures, ...(res?.features || {}) }
|
||||||
|
businessInfo.value = normalizeBusinessInfo(res?.businessInfo)
|
||||||
taxEvaluationPeriod.value = normalizeTaxEvaluationPeriod(res?.taxEvaluationPeriod)
|
taxEvaluationPeriod.value = normalizeTaxEvaluationPeriod(res?.taxEvaluationPeriod)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -628,6 +646,51 @@ onMounted(() => {
|
|||||||
<UInput v-model="businessInfo.city" class="flex-auto"/>
|
<UInput v-model="businessInfo.city" class="flex-auto"/>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
</UFormField>
|
</UFormField>
|
||||||
|
<UFormField
|
||||||
|
label="E-Mail:"
|
||||||
|
>
|
||||||
|
<UInput v-model="businessInfo.email"/>
|
||||||
|
</UFormField>
|
||||||
|
<UFormField
|
||||||
|
label="Telefon:"
|
||||||
|
>
|
||||||
|
<UInput v-model="businessInfo.phone"/>
|
||||||
|
</UFormField>
|
||||||
|
<UFormField
|
||||||
|
label="Website:"
|
||||||
|
>
|
||||||
|
<UInput v-model="businessInfo.website"/>
|
||||||
|
</UFormField>
|
||||||
|
<UFormField
|
||||||
|
label="Steuernummer:"
|
||||||
|
>
|
||||||
|
<UInput v-model="businessInfo.taxNumber"/>
|
||||||
|
</UFormField>
|
||||||
|
<UFormField
|
||||||
|
label="USt-IdNr.:"
|
||||||
|
>
|
||||||
|
<UInput v-model="businessInfo.vatId"/>
|
||||||
|
</UFormField>
|
||||||
|
<UFormField
|
||||||
|
label="Bank:"
|
||||||
|
>
|
||||||
|
<UInput v-model="businessInfo.bankName"/>
|
||||||
|
</UFormField>
|
||||||
|
<UFormField
|
||||||
|
label="Kontoinhaber:"
|
||||||
|
>
|
||||||
|
<UInput v-model="businessInfo.bankAccountOwner"/>
|
||||||
|
</UFormField>
|
||||||
|
<UFormField
|
||||||
|
label="IBAN:"
|
||||||
|
>
|
||||||
|
<UInput v-model="businessInfo.iban"/>
|
||||||
|
</UFormField>
|
||||||
|
<UFormField
|
||||||
|
label="BIC:"
|
||||||
|
>
|
||||||
|
<UInput v-model="businessInfo.bic"/>
|
||||||
|
</UFormField>
|
||||||
<UButton
|
<UButton
|
||||||
class="mt-3"
|
class="mt-3"
|
||||||
@click="updateTenant({businessInfo: businessInfo})"
|
@click="updateTenant({businessInfo: businessInfo})"
|
||||||
|
|||||||
@@ -45,6 +45,11 @@ export default defineNuxtPlugin(() => {
|
|||||||
let title = "Fehler"
|
let title = "Fehler"
|
||||||
let description = "Ein unerwarteter Fehler ist aufgetreten."
|
let description = "Ein unerwarteter Fehler ist aufgetreten."
|
||||||
|
|
||||||
|
if (status === 422 && response._data?.code === "UNSUPPORTED_PDF_CHARACTER") {
|
||||||
|
title = "Nicht unterstütztes Zeichen"
|
||||||
|
description = response._data.error
|
||||||
|
}
|
||||||
|
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 400:
|
case 400:
|
||||||
title = "Anfrage fehlerhaft"
|
title = "Anfrage fehlerhaft"
|
||||||
|
|||||||
Reference in New Issue
Block a user