Add payment QR code to invoice PDFs
This commit is contained in:
@@ -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,11 @@ 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
|
||||||
|
|
||||||
// --- 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 +730,12 @@ 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: paymentQrBankAccount && itemInfo.payment_type === "transfer" ? {
|
||||||
|
iban: paymentQrBankAccount.iban,
|
||||||
|
recipientName: paymentQrBankAccount.ownerName || businessInfo.name || tenant.name,
|
||||||
|
amount: totals.totalGross,
|
||||||
|
remittance: itemInfo.documentNumber || itemInfo.title,
|
||||||
|
} : null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,8 @@ 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
|
||||||
|
|
||||||
let pages = []
|
let pages = []
|
||||||
let pageCounter = 1
|
let pageCounter = 1
|
||||||
@@ -923,8 +981,31 @@ export const createInvoicePDF = async (server:FastifyInstance, returnMode, invoi
|
|||||||
color: rgb(0, 0, 0),
|
color: rgb(0, 0, 0),
|
||||||
lineHeight: 10,
|
lineHeight: 10,
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
maxWidth: 500
|
maxWidth: paymentQrCode ? 360 : 500
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (paymentQrCode) {
|
||||||
|
const qrSize = 32 * 2.83
|
||||||
|
const qrPosition = getCoordinatesForPDFLib(166, rowHeight + endTextDiff + (invoiceData.totalArray.length - 3) * 8 - 1, page1)
|
||||||
|
|
||||||
|
pages[pageCounter - 1].drawText("Überweisung per QR-Code", {
|
||||||
|
y: qrPosition.y + qrSize + 7,
|
||||||
|
x: getCoordinatesForPDFLib(166, 0, page1).x,
|
||||||
|
size: 8,
|
||||||
|
color: rgb(0, 0, 0),
|
||||||
|
lineHeight: 8,
|
||||||
|
opacity: 1,
|
||||||
|
maxWidth: 120,
|
||||||
|
font: fontBold,
|
||||||
|
})
|
||||||
|
|
||||||
|
pages[pageCounter - 1].drawImage(paymentQrCode, {
|
||||||
|
x: qrPosition.x,
|
||||||
|
y: qrPosition.y,
|
||||||
|
width: qrSize,
|
||||||
|
height: qrSize,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return await pdfDoc.saveAsBase64()
|
return await pdfDoc.saveAsBase64()
|
||||||
|
|||||||
@@ -95,6 +95,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([])
|
||||||
@@ -236,6 +237,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`, {
|
||||||
@@ -1365,6 +1372,7 @@ 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)
|
||||||
|
|
||||||
if (auth.activeTenantData.extraModules.includes("agriculture")) {
|
if (auth.activeTenantData.extraModules.includes("agriculture")) {
|
||||||
itemInfo.value.rows.forEach(row => {
|
itemInfo.value.rows.forEach(row => {
|
||||||
@@ -1580,7 +1588,13 @@ 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: paymentQrBankAccount && itemInfo.value.payment_type === "transfer" ? {
|
||||||
|
iban: paymentQrBankAccount.iban,
|
||||||
|
recipientName: paymentQrBankAccount.ownerName || businessInfo.name || auth.activeTenantData.name,
|
||||||
|
amount: documentTotal.value.totalSumToPay,
|
||||||
|
remittance: itemInfo.value.documentNumber || itemInfo.value.title,
|
||||||
|
} : null
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(returnData)
|
console.log(returnData)
|
||||||
|
|||||||
Reference in New Issue
Block a user