From c98394b5bfdb4775136dae1002aad92ee19dbc9f Mon Sep 17 00:00:00 2001 From: florianfederspiel Date: Fri, 12 Sep 2025 18:29:13 +0200 Subject: [PATCH] Changes --- src/index.ts | 16 +- src/routes/{ => auth}/auth-authenticated.ts | 0 src/routes/{ => auth}/auth.ts | 4 +- src/routes/{ => auth}/me.ts | 2 +- src/routes/auth/user.ts | 67 ++ src/routes/banking.ts | 72 ++ src/routes/emailAsUser.ts | 30 + src/routes/exports.ts | 105 +++ src/routes/files.ts | 3 +- src/routes/functions.ts | 42 + src/routes/tenant.ts | 96 +++ src/utils/emailengine.ts | 45 ++ src/utils/export/datev.ts | 382 +++++++++ src/utils/functions.ts | 23 + src/utils/history.ts | 3 +- src/utils/pdf.ts | 854 ++++++++++++++++++++ src/utils/stringRendering.ts | 51 ++ 17 files changed, 1786 insertions(+), 9 deletions(-) rename src/routes/{ => auth}/auth-authenticated.ts (100%) rename src/routes/{ => auth}/auth.ts (98%) rename src/routes/{ => auth}/me.ts (94%) create mode 100644 src/routes/auth/user.ts create mode 100644 src/routes/banking.ts create mode 100644 src/routes/emailAsUser.ts create mode 100644 src/routes/exports.ts create mode 100644 src/routes/functions.ts create mode 100644 src/utils/emailengine.ts create mode 100644 src/utils/export/datev.ts create mode 100644 src/utils/functions.ts create mode 100644 src/utils/pdf.ts create mode 100644 src/utils/stringRendering.ts diff --git a/src/index.ts b/src/index.ts index ab73502..9b7697f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,11 +2,11 @@ import Fastify from "fastify"; import swaggerPlugin from "./plugins/swagger" import supabasePlugin from "./plugins/supabase"; import healthRoutes from "./routes/health"; -import meRoutes from "./routes/me"; +import meRoutes from "./routes/auth/me"; import tenantRoutes from "./routes/tenant"; import tenantPlugin from "./plugins/tenant"; -import authRoutes from "./routes/auth"; -import authRoutesAuthenticated from "./routes/auth-authenticated"; +import authRoutes from "./routes/auth/auth"; +import authRoutesAuthenticated from "./routes/auth/auth-authenticated"; import authPlugin from "./plugins/auth"; import adminRoutes from "./routes/admin"; import corsPlugin from "./plugins/cors"; @@ -15,6 +15,11 @@ import resourceRoutesSpecial from "./routes/resourcesSpecial"; import fastifyCookie from "@fastify/cookie"; import historyRoutes from "./routes/history"; import fileRoutes from "./routes/files"; +import userRoutes from "./routes/auth/user" +import functionRoutes from "./routes/functions"; +import bankingRoutes from "./routes/banking"; +import exportRoutes from "./routes/exports" +import emailAsUserRoutes from "./routes/emailAsUser"; import {sendMail} from "./utils/mailer"; @@ -52,6 +57,11 @@ async function main() { await subApp.register(resourceRoutesSpecial); await subApp.register(historyRoutes); await subApp.register(fileRoutes); + await subApp.register(userRoutes); + await subApp.register(functionRoutes); + await subApp.register(bankingRoutes); + await subApp.register(exportRoutes); + await subApp.register(emailAsUserRoutes); },{prefix: "/api"}) diff --git a/src/routes/auth-authenticated.ts b/src/routes/auth/auth-authenticated.ts similarity index 100% rename from src/routes/auth-authenticated.ts rename to src/routes/auth/auth-authenticated.ts diff --git a/src/routes/auth.ts b/src/routes/auth/auth.ts similarity index 98% rename from src/routes/auth.ts rename to src/routes/auth/auth.ts index 3e2b601..5e70e86 100644 --- a/src/routes/auth.ts +++ b/src/routes/auth/auth.ts @@ -1,8 +1,8 @@ import { FastifyInstance } from "fastify"; import bcrypt from "bcrypt"; import jwt from "jsonwebtoken"; -import { generateRandomPassword, hashPassword } from "../utils/password" -import { sendMail } from "../utils/mailer" +import { generateRandomPassword, hashPassword } from "../../utils/password" +import { sendMail } from "../../utils/mailer" export default async function authRoutes(server: FastifyInstance) { // Registrierung diff --git a/src/routes/me.ts b/src/routes/auth/me.ts similarity index 94% rename from src/routes/me.ts rename to src/routes/auth/me.ts index 8b12994..510ea89 100644 --- a/src/routes/me.ts +++ b/src/routes/auth/me.ts @@ -25,7 +25,7 @@ export default async function meRoutes(server: FastifyInstance) { // 2. Tenants laden (alle Tenants des Users) const { data: tenantLinks, error: tenantLinksError } = await server.supabase .from("auth_users") - .select(`*, tenants!auth_tenant_users ( id, name, locked )`) + .select(`*, tenants!auth_tenant_users ( id, name,short, locked, extraModules, businessInfo, numberRanges, dokuboxkey )`) .eq("id", authUser.user_id) .single(); diff --git a/src/routes/auth/user.ts b/src/routes/auth/user.ts new file mode 100644 index 0000000..687ea15 --- /dev/null +++ b/src/routes/auth/user.ts @@ -0,0 +1,67 @@ +import { FastifyInstance } from "fastify"; + +export default async function userRoutes(server: FastifyInstance) { + //TODO: PERMISSIONS Rückmeldung beschränken + + server.get("/user/:id", async (req, reply) => { + const authUser = req.user // kommt aus JWT (user_id + tenant_id) + + const {id} = req.params + + if (!authUser) { + return reply.code(401).send({ error: "Unauthorized" }) + } + + + // 1. User laden + const { data: user, error: userError } = await server.supabase + .from("auth_users") + .select("id, email, created_at, must_change_password") + .eq("id", id) + .single() + + if (userError || !user) { + return reply.code(401).send({ error: "User not found" }) + } + + // 2. Tenants laden (alle Tenants des Users) + /*const { data: tenantLinks, error: tenantLinksError } = await server.supabase + .from("auth_users") + .select(`*, tenants!auth_tenant_users ( id, name, locked )`) + .eq("id", authUser.user_id) + .single(); + + if (tenantLinksError) { + + console.log(tenantLinksError) + + return reply.code(401).send({ error: "Tenant Error" }) + } + + const tenants = tenantLinks?.tenants*/ + + // 3. Aktiven Tenant bestimmen + const activeTenant = authUser.tenant_id /*|| tenants[0].id*/ + + // 4. Profil für den aktiven Tenant laden + let profile = null + if (activeTenant) { + const { data: profileData } = await server.supabase + .from("auth_profiles") + .select("*") + .eq("user_id", id) + .eq("tenant_id", activeTenant) + .single() + + profile = profileData + } + + // 5. Permissions laden (über Funktion) + + // 6. Response zurückgeben + return { + user, + profile, + } + }) +} \ No newline at end of file diff --git a/src/routes/banking.ts b/src/routes/banking.ts new file mode 100644 index 0000000..a85fed4 --- /dev/null +++ b/src/routes/banking.ts @@ -0,0 +1,72 @@ +import { FastifyInstance } from "fastify"; +import jwt from "jsonwebtoken"; +import {insertHistoryItem} from "../utils/history"; + +export default async function bankingRoutes(server: FastifyInstance) { + + //Create Banking Statement + server.post("/banking/statements", async (req, reply) => { + if (!req.user) { + return reply.code(401).send({ error: "Unauthorized" }); + } + + const body = req.body as { data: string }; + console.log(body); + + const {data,error} = await server.supabase.from("statementallocations").insert({ + ...body.data, + tenant: req.user.tenant_id, + }).select() + + await insertHistoryItem(server,{ + entity: "bankstatements", + entityId: data.id, + action: "created", + created_by: req.user.user_id, + tenant_id: req.user.tenant_id, + oldVal: null, + newVal: data, + text: `Buchung erstellt`, + }); + + if(data && !error){ + return reply.send(data) + } + }); + + //Delete Banking Statement + server.delete("/banking/statements/:id", async (req, reply) => { + if (!req.user) { + return reply.code(401).send({ error: "Unauthorized" }); + } + + const { id } = req.params as { id?: string } + + const {data} = await server.supabase.from("statementallocations").select().eq("id",id).single() + + const {error} = await server.supabase.from("statementallocations").delete().eq("id",id) + + if(!error){ + + await insertHistoryItem(server,{ + entity: "bankstatements", + entityId: id, + action: "deleted", + created_by: req.user.user_id, + tenant_id: req.user.tenant_id, + oldVal: data, + newVal: null, + text: `Buchung gelöscht`, + }); + + return reply.send({success:true}) + } else { + return reply.code(500).send({error:"Fehler beim löschen"}) + } + + + }) + + + +} \ No newline at end of file diff --git a/src/routes/emailAsUser.ts b/src/routes/emailAsUser.ts new file mode 100644 index 0000000..9755e45 --- /dev/null +++ b/src/routes/emailAsUser.ts @@ -0,0 +1,30 @@ +import { FastifyInstance } from "fastify"; +import {createInvoicePDF} from "../utils/pdf"; +import {useNextNumberRangeNumber} from "../utils/functions"; +import {sendMailAsUser} from "../utils/emailengine"; +import {subtle} from "node:crypto"; + +export default async function emailAsUserRoutes(server: FastifyInstance) { + server.post("/emailasuser/send", async (req, reply) => { + const body = req.body as { + to: string + cc?: string + bcc?: string + subject?: string + text?: string + html?: string + attachments?: any, + account: string + } + + try { + reply.send(await sendMailAsUser(body.to,body.subject,body.html,body.text,body.account,body.cc,body.bcc,body.attachments)) + + } catch (err) { + console.log(err) + reply.code(500).send({ error: "Failed to send E-Mail as User" }) + } + }) + + +} \ No newline at end of file diff --git a/src/routes/exports.ts b/src/routes/exports.ts new file mode 100644 index 0000000..eb10186 --- /dev/null +++ b/src/routes/exports.ts @@ -0,0 +1,105 @@ +import { FastifyInstance } from "fastify"; +import jwt from "jsonwebtoken"; +import {insertHistoryItem} from "../utils/history"; +import {buildExportZip} from "../utils/export/datev"; +import {s3} from "../utils/s3"; +import {GetObjectCommand, PutObjectCommand} from "@aws-sdk/client-s3" +import {getSignedUrl} from "@aws-sdk/s3-request-presigner"; +import dayjs from "dayjs"; +import {randomUUID} from "node:crypto"; + +const createExport = async (server:FastifyInstance,req:any,startDate,endDate,beraternr,mandantennr) => { + console.log(startDate,endDate,beraternr,mandantennr) + + // 1) ZIP erzeugen + const buffer = await buildExportZip(server,req.user.tenant_id, startDate, endDate, beraternr, mandantennr) + console.log("ZIP created") + + // 2) Dateiname & Key festlegen + const fileKey = `${req.user.tenant_id}/exports/Export_${dayjs(startDate).format("YYYY-MM-DD")}_${dayjs(endDate).format("YYYY-MM-DD")}_${randomUUID()}.zip` + console.log(fileKey) + + // 3) In S3 hochladen + await s3.send( + new PutObjectCommand({ + Bucket: process.env.S3_BUCKET || "FEDEO", + Key: fileKey, + Body: buffer, + ContentType: "application/zip", + }) + ) + + // 4) Presigned URL erzeugen (24h gültig) + const url = await getSignedUrl( + s3, + new GetObjectCommand({ + Bucket: process.env.S3_BUCKET || "FEDEO", + Key: fileKey, + }), + { expiresIn: 60 * 60 * 24 } + ) + + console.log(url) + + // 5) In Supabase-DB speichern + const { data, error } = await server.supabase + .from("exports") + .insert([ + { + tenant_id: req.user.tenant_id, + start_date: startDate, + end_date: endDate, + valid_until: dayjs().add(24,"hours").toISOString(), + file_path: fileKey, + url: url, + created_at: new Date().toISOString(), + }, + ]) + .select() + .single() + + console.log(data) + console.log(error) +} + + +export default async function exportRoutes(server: FastifyInstance) { + //Export DATEV + server.post("/exports/datev", async (req, reply) => { + const { start_date, end_date, beraternr, mandantennr } = req.body as { + start_date: string + end_date: string + beraternr: string + mandantennr: string + } + + + + reply.send({success:true}) + + setImmediate(async () => { + try { + await createExport(server,req,start_date,end_date,beraternr,mandantennr) + console.log("Job done ✅") + } catch (err) { + console.error("Job failed ❌", err) + } + }) + + }) + + //List Exports Available for Download + + server.get("/exports", async (req,reply) => { + const {data,error} = await server.supabase.from("exports").select().eq("tenant_id",req.user.tenant_id) + + console.log(data,error) + reply.send(data) + + }) + + + + + +} \ No newline at end of file diff --git a/src/routes/files.ts b/src/routes/files.ts index 4da37fc..8e2f2da 100644 --- a/src/routes/files.ts +++ b/src/routes/files.ts @@ -43,7 +43,6 @@ export default async function fileRoutes(server: FastifyInstance) { if (!data.file) return reply.code(400).send({ error: "No file uploaded" }) - console.log("ENDE") const {data:createdFileData,error:createdFileError} = await server.supabase .from("files") @@ -207,7 +206,7 @@ export default async function fileRoutes(server: FastifyInstance) { } try { - const {data:supabaseFileEntries,error} = await server.supabase.from("files").select("*").in("id", ids) + const {data:supabaseFileEntries,error} = await server.supabase.from("files").select("*, createddocument(*, customer(*))").in("id", ids) diff --git a/src/routes/functions.ts b/src/routes/functions.ts new file mode 100644 index 0000000..b3618c0 --- /dev/null +++ b/src/routes/functions.ts @@ -0,0 +1,42 @@ +import { FastifyInstance } from "fastify"; +import {createInvoicePDF} from "../utils/pdf"; +import {useNextNumberRangeNumber} from "../utils/functions"; + +export default async function functionRoutes(server: FastifyInstance) { + server.post("/functions/createinvoicepdf", async (req, reply) => { + const body = req.body as { + invoiceData: any + backgroundPath?: string + } + + try { + const pdf = await createInvoicePDF( + server, + "base64", + body.invoiceData, + body.backgroundPath + ) + + reply.send(pdf) // Fastify wandelt automatisch in JSON + } catch (err) { + console.log(err) + reply.code(500).send({ error: "Failed to create PDF" }) + } + }) + + server.get( + "/functions/usenextnumber/:numberrange", + async (req, reply) => { + const { numberrange } = req.params as { numberrange: string }; + const tenant = (req as any).user.tenant_id + + try { + const result = await useNextNumberRangeNumber(server,tenant, numberrange) + reply.send(result) // JSON automatisch + } catch (err) { + req.log.error(err) + reply.code(500).send({ error: "Failed to generate next number" }) + } + } + ) +} \ No newline at end of file diff --git a/src/routes/tenant.ts b/src/routes/tenant.ts index 24c0c66..6ad68d7 100644 --- a/src/routes/tenant.ts +++ b/src/routes/tenant.ts @@ -60,5 +60,101 @@ export default async function routes(server: FastifyInstance) { return { token }; }); + server.get("/tenant/users", async (req, reply) => { + const { tenant_id } = req.params as { tenant_id: string }; + const authUser = req.user // kommt aus JWT (user_id + tenant_id) + + if (!authUser) { + return reply.code(401).send({ error: "Unauthorized" }) + } + + const { data, error } = await server.supabase + .from("auth_tenant_users") + .select(` + user_id, + auth_users!tenantusers_user_id_fkey ( id, email, created_at, auth_profiles(*))`) + .eq("tenant_id", authUser.tenant_id); + + if (error) { + console.log(error); + return reply.code(400).send({ error: error.message }); + } + + let correctedData = data.map(i => { + return { + id: i.user_id, + email: i.auth_users.email, + profile: i.auth_users.auth_profiles.find(x => x.tenant_id === authUser.tenant_id), + full_name: i.auth_users.auth_profiles.find(x => x.tenant_id === authUser.tenant_id)?.full_name, + } + }) + + return { tenant_id, users: correctedData }; + }); + + server.put("/tenant/numberrange/:numberrange", async (req, reply) => { + if (!req.user) { + return reply.code(401).send({ error: "Unauthorized" }); + } + const { numberrange } = req.params as { numberrange?: string } + + const body = req.body as { numberRange: object }; + console.log(body); + + if(!body.numberRange) { + return reply.code(400).send({ error: "numberRange required" }); + } + + const {data:currentTenantData,error:numberRangesError} = await server.supabase.from("tenants").select().eq("id", req.user.tenant_id).single() + + console.log(currentTenantData) + console.log(numberRangesError) + + + let numberRanges = { + // @ts-ignore + ...currentTenantData.numberRanges + } + + // @ts-ignore + numberRanges[numberrange] = body.numberRange + + + console.log(numberRanges) + + const {data,error} = await server.supabase + .from("tenants") + .update({numberRanges: numberRanges}) + .eq('id',req.user.tenant_id) + .select() + + if(data && !error) { + return reply.send(data) + } + }); + + server.put("/tenant/other/:id", async (req, reply) => { + if (!req.user) { + return reply.code(401).send({ error: "Unauthorized" }); + } + const { id } = req.params as { id?: string } + + const body = req.body as { data: object }; + console.log(body); + + if(!body.data) { + return reply.code(400).send({ error: "data required" }); + } + + const {data:dataReturn,error} = await server.supabase + .from("tenants") + .update(body.data) + .eq('id',req.user.tenant_id) + .select() + + if(dataReturn && !error) { + return reply.send(dataReturn) + } + }); } \ No newline at end of file diff --git a/src/utils/emailengine.ts b/src/utils/emailengine.ts new file mode 100644 index 0000000..705a399 --- /dev/null +++ b/src/utils/emailengine.ts @@ -0,0 +1,45 @@ +import axios from "axios" + +const AxiosEE = axios.create({ + baseURL: process.env.EMAILENGINE_URL ||"https://ee.fedeo.io/v1", + headers: { + Authorization: `Bearer ${process.env.EMAILENGINE_TOKEN || "dcd8209bc5371c728f9ec951600afcfc74e8c391a7e984b2a6df9c4665dc7ad6"}`, + Accept: "application/json", + }, +}) + + + +export async function sendMailAsUser( + to: string, + subject: string, + html: string, + text: string, + account: string, + cc: string, + bcc: string, + attachments: any, +): Promise<{ success: boolean; info?: any; error?: any }> { + try { + const sendData = { + to: to.split(";").map(i => { return {address: i}}), + cc: cc ? cc.split(";").map((i:any) => { return {address: i}}) : null, + bcc: bcc ? bcc.split(";").map((i:any) => { return {address: i}}) : null, + subject, + text, + html, + attachments + } + + if(sendData.cc === null) delete sendData.cc + if(sendData.bcc === null) delete sendData.bcc + + const {data} = await AxiosEE.post(`/account/${account}/submit`, sendData) + + return { success: true, info: data } + + } catch (err) { + console.error("❌ Fehler beim Mailversand:", err) + return { success: false, error: err } + } +} \ No newline at end of file diff --git a/src/utils/export/datev.ts b/src/utils/export/datev.ts new file mode 100644 index 0000000..c8d5298 --- /dev/null +++ b/src/utils/export/datev.ts @@ -0,0 +1,382 @@ +import xmlbuilder from "xmlbuilder"; +import dayjs from "dayjs"; +import isBetween from "dayjs/plugin/isBetween.js" +import {BlobWriter, Data64URIReader, TextReader, TextWriter, ZipWriter} from "@zip.js/zip.js"; +import {FastifyInstance} from "fastify"; +import {GetObjectCommand} from "@aws-sdk/client-s3"; +import {s3} from "../s3"; +dayjs.extend(isBetween) + +const getCreatedDocumentTotal = (item) => { + let totalNet = 0 + let total19 = 0 + let total7 = 0 + + item.rows.forEach(row => { + if(!['pagebreak','title','text'].includes(row.mode)){ + let rowPrice = Number(Number(row.quantity) * Number(row.price) * (1 - Number(row.discountPercent) /100) ).toFixed(3) + totalNet = totalNet + Number(rowPrice) + + if(row.taxPercent === 19) { + total19 = total19 + Number(rowPrice * 0.19) + } else if(row.taxPercent === 7) { + total7 = total7 + Number(rowPrice * 0.07) + } + } + }) + + let totalGross = Number(totalNet.toFixed(2)) + Number(total19.toFixed(2)) + Number(total7.toFixed(2)) + + + + return { + totalNet: totalNet, + total19: total19, + total7: total7, + totalGross: totalGross, + } +} + +const escapeString = (str) => { + + str = (str ||"") + .replaceAll("\n","") + .replaceAll(";","") + .replaceAll(/\r/g,"") + .replaceAll(/"/g,"") + .replaceAll(/ü/g,"ue") + .replaceAll(/ä/g,"ae") + .replaceAll(/ö/g,"oe") + return str +} + +const displayCurrency = (input, onlyAbs = false) => { + + if(onlyAbs) { + return Math.abs(input).toFixed(2).replace(".",",") + } else { + return input.toFixed(2).replace(".",",") + } +} + +export async function buildExportZip(server: FastifyInstance, tenant: number, startDate: string, endDate: string, beraternr: string, mandantennr: string): Promise { + + try { + const zipFileWriter = new BlobWriter() + const zipWriter = new ZipWriter(zipFileWriter) + + + + //Basic Information + + let header = `"EXTF";700;21;"Buchungsstapel";13;${dayjs().format("YYYYMMDDHHmmssSSS")};;"FE";"Florian Federspiel";;${beraternr};${mandantennr};20250101;4;${dayjs(startDate).format("YYYYMMDD")};${dayjs(endDate).format("YYYYMMDD")};"Buchungsstapel";"FF";1;0;1;"EUR";;"";;;"03";;;"";""` + + let colHeaders = `Umsatz;Soll-/Haben-Kennzeichen;WKZ Umsatz;Kurs;Basisumsatz;WKZ Basisumsatz;Konto;Gegenkonto;BU-Schluessel;Belegdatum;Belegfeld 1;Belegfeld 2;Skonto;Buchungstext;Postensperre;Diverse Adressnummer;Geschaeftspartnerbank;Sachverhalt;Zinssperre;Beleglink;Beleginfo - Art 1;Beleginfo - Inhalt 1;Beleginfo - Art 2;Beleginfo - Inhalt 2;Beleginfo - Art 3;Beleginfo - Inhalt 3;Beleginfo - Art 4;Beleginfo - Inhalt 4;Beleginfo - Art 5;Beleginfo - Inhalt 5;Beleginfo - Art 6;Beleginfo - Inhalt 6;Beleginfo - Art 7;Beleginfo - Inhalt 7;Beleginfo - Art 8;Beleginfo - Inhalt 8;KOST1 - Kostenstelle;KOST2 - Kostenstelle;Kost Menge;EU-Land u. USt-IdNr. (Bestimmung);EU-Steuersatz (Bestimmung);Abw. Versteuerungsart;Sachverhalt L+L;Funktionsergaenzung L+L;BU 49 Hauptfunktionstyp;BU 49 Hauptfunktionsnummer;BU 49 Funktionsergaenzung;Zusatzinformation - Art 1;Zusatzinformation - Inhalt 1;Zusatzinformation - Art 2;Zusatzinformation - Inhalt 2;Zusatzinformation - Art 3;Zusatzinformation - Inhalt 3;Zusatzinformation - Art 4;Zusatzinformation - Inhalt 4;Zusatzinformation - Art 5;Zusatzinformation - Inhalt 5;Zusatzinformation - Art 6;Zusatzinformation - Inhalt 6;Zusatzinformation - Art 7;Zusatzinformation - Inhalt 7;Zusatzinformation - Art 8;Zusatzinformation - Inhalt 8;Zusatzinformation - Art 9;Zusatzinformation - Inhalt 9;Zusatzinformation - Art 10;Zusatzinformation - Inhalt 10;Zusatzinformation - Art 11;Zusatzinformation - Inhalt 11;Zusatzinformation - Art 12;Zusatzinformation - Inhalt 12;Zusatzinformation - Art 13;Zusatzinformation - Inhalt 13;Zusatzinformation - Art 14;Zusatzinformation - Inhalt 14;Zusatzinformation - Art 15;Zusatzinformation - Inhalt 15;Zusatzinformation - Art 16;Zusatzinformation - Inhalt 16;Zusatzinformation - Art 17;Zusatzinformation - Inhalt 17;Zusatzinformation - Art 18;Zusatzinformation - Inhalt 18;Zusatzinformation - Art 19;Zusatzinformation - Inhalt 19;Zusatzinformation - Art 20;Zusatzinformation - Inhalt 20;Stueck;Gewicht;Zahlweise;Zahlweise;Veranlagungsjahr;Zugeordnete Faelligkeit;Skontotyp;Auftragsnummer;Buchungstyp;USt-Schluessel (Anzahlungen);EU-Mitgliedstaat (Anzahlungen);Sachverhalt L+L (Anzahlungen);EU-Steuersatz (Anzahlungen);Erloeskonto (Anzahlungen);Herkunft-Kz;Leerfeld;KOST-Datum;SEPA-Mandatsreferenz;Skontosperre;Gesellschaftername;Beteiligtennummer;Identifikationsnummer;Zeichnernummer;Postensperre bis;Bezeichnung SoBil-Sachverhalt;Kennzeichen SoBil-Buchung;Festschreibung;Leistungsdatum;Datum Zuord. Steuerperiode;Faelligkeit;Generalumkehr;Steuersatz;Land;Abrechnungsreferenz;BVV-Position;EU-Mitgliedstaat u. UStID(Ursprung);EU-Steuersatz(Ursprung);Abw. Skontokonto` + + //Get Bookings + const {data:statementallocationsRaw,error: statementallocationsError} = await server.supabase.from("statementallocations").select('*, account(*), bs_id(*, account(*)), cd_id(*,customer(*)), ii_id(*, vendor(*)), vendor(*), customer(*), ownaccount(*)').eq("tenant", tenant); + let {data:createddocumentsRaw,error: createddocumentsError} = await server.supabase.from("createddocuments").select('*,customer(*)').eq("tenant", tenant).in("type",["invoices","advanceInvoices","cancellationInvoices"]).eq("state","Gebucht") + let {data:incominginvoicesRaw,error: incominginvoicesError} = await server.supabase.from("incominginvoices").select('*, vendor(*)').eq("tenant", tenant) + const {data:accounts} = await server.supabase.from("accounts").select() + const {data:tenantData} = await server.supabase.from("tenants").select().eq("id",tenant).single() + + let createddocuments = createddocumentsRaw.filter(i => dayjs(i.documentDate).isBetween(startDate,endDate,"day","[]")) + let incominginvoices = incominginvoicesRaw.filter(i => dayjs(i.date).isBetween(startDate,endDate,"day","[]")) + let statementallocations = statementallocationsRaw.filter(i => dayjs(i.bs_id.date).isBetween(startDate,endDate,"day","[]")) + + + const {data:filesCreateddocuments, error: filesErrorCD} = await server.supabase.from("files").select().eq("tenant",tenant).or(`createddocument.in.(${createddocuments.map(i => i.id).join(",")})`) + const {data:filesIncomingInvoices, error: filesErrorII} = await server.supabase.from("files").select().eq("tenant",tenant).or(`incominginvoice.in.(${incominginvoices.map(i => i.id).join(",")})`) + + const downloadFile = async (bucketName, filePath, downloadFilePath,fileId) => { + const command = new GetObjectCommand({ + Bucket: process.env.S3_BUCKET || "FEDEO", + Key: filePath, + }) + + const { Body, ContentType } = await s3.send(command) + + const chunks: any[] = [] + // @ts-ignore + for await (const chunk of Body) { + chunks.push(chunk) + } + const buffer = Buffer.concat(chunks) + + const dataURL = `data:application/pdf;base64,${buffer.toString('base64')}` + + const dataURLReader = new Data64URIReader(dataURL) + await zipWriter.add(`${fileId}.${downloadFilePath.split(".").pop()}`, dataURLReader) + + //await fs.writeFile(`./output/${fileId}.${downloadFilePath.split(".").pop()}`, buffer, () => {}); + console.log(`File added to Zip`); + }; + + for (const file of filesCreateddocuments) { + await downloadFile("filesdev",file.path,`./output/files/${file.path.split("/")[file.path.split("/").length - 1]}`,file.id); + } + for (const file of filesIncomingInvoices) { + await downloadFile("filesdev",file.path,`./output/files/${file.path.split("/")[file.path.split("/").length - 1]}`,file.id); + } + + let bookingLines = [] + + createddocuments.forEach(createddocument => { + + let file = filesCreateddocuments.find(i => i.createddocument === createddocument.id); + + let total = 0 + let typeString = "" + + if(createddocument.type === "invoices") { + total = getCreatedDocumentTotal(createddocument).totalGross + + console.log() + if(createddocument.usedAdvanceInvoices.length > 0){ + createddocument.usedAdvanceInvoices.forEach(usedAdvanceInvoice => { + total -= getCreatedDocumentTotal(createddocumentsRaw.find(i => i.id === usedAdvanceInvoice)).totalGross + }) + } + + console.log(total) + + typeString = "AR" + } else if(createddocument.type === "advanceInvoices") { + total = getCreatedDocumentTotal(createddocument).totalGross + typeString = "ARAbschlag" + } else if(createddocument.type === "cancellationInvoices") { + total = getCreatedDocumentTotal(createddocument).totalGross + typeString = "ARStorno" + } + + let shSelector = "S" + if(Math.sign(total) === 1) { + shSelector = "S" + } else if (Math.sign(total) === -1) { + shSelector = "H" + } + + bookingLines.push(`${displayCurrency(total,true)};"${shSelector}";;;;;${createddocument.customer.customerNumber};8400;"";${dayjs(createddocument.documentDate).format("DDMM")};"${createddocument.documentNumber}";;;"${`${typeString} ${createddocument.documentNumber} - ${createddocument.customer.name}`.substring(0,59)}";;;;;;${file ? `"BEDI ""${file.id}"""` : ""};"Geschäftspartner";"${createddocument.customer.name}";"Kundennummer";"${createddocument.customer.customerNumber}";"Belegnummer";"${createddocument.documentNumber}";"Leistungsdatum";"${dayjs(createddocument.deliveryDate).format("DD.MM.YYYY")}";"Belegdatum";"${dayjs(createddocument.documentDate).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`) + + }) + + incominginvoices.forEach(incominginvoice => { + console.log(incominginvoice.id); + incominginvoice.accounts.forEach(account => { + + let file = filesIncomingInvoices.find(i => i.incominginvoice === incominginvoice.id); + + + let accountData = accounts.find(i => i.id === account.account) + let buschluessel = 9 + + if(account.taxType === '19'){ + buschluessel = 9 + } else if(account.taxType === 'null') { + buschluessel = "" + } else if(account.taxType === '7') { + buschluessel = "8" + } else if(account.taxType === '19I') { + buschluessel = "19" + } else if(account.taxType === '7I') { + buschluessel = "18" + } else { + buschluessel = "-" + } + + let shSelector = "S" + let amountGross = account.amountGross ? account.amountGross : account.amountNet + account.amountTax + + + if(Math.sign(amountGross) === 1) { + shSelector = "S" + } else if(Math.sign(amountGross) === -1) { + shSelector = "H" + } + + let text = `ER ${incominginvoice.reference}: ${escapeString(incominginvoice.description)}`.substring(0,59) + console.log(incominginvoice) + bookingLines.push(`${Math.abs(amountGross).toFixed(2).replace(".",",")};"${shSelector}";;;;;${accountData.number};${incominginvoice.vendor.vendorNumber};"${buschluessel}";${dayjs(incominginvoice.date).format("DDMM")};"${incominginvoice.reference}";;;"${text}";;;;;;${file ? `"BEDI ""${file.id}"""` : ""};"Geschäftspartner";"${incominginvoice.vendor.name}";"Kundennummer";"${incominginvoice.vendor.vendorNumber}";"Belegnummer";"${incominginvoice.reference}";"Leistungsdatum";"${dayjs(incominginvoice.date).format("DD.MM.YYYY")}";"Belegdatum";"${dayjs(incominginvoice.date).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`) + }) + + + }) + + statementallocations.forEach(statementallocation => { + + let shSelector = "S" + + if(Math.sign(statementallocation.amount) === 1) { + shSelector = "S" + } else if(Math.sign(statementallocation.amount) === -1) { + shSelector = "H" + } + + if(statementallocation.cd_id) { + bookingLines.push(`${displayCurrency(statementallocation.amount,true)};"H";;;;;${statementallocation.cd_id.customer.customerNumber};${statementallocation.bs_id.account.datevNumber};"3";${dayjs(statementallocation.cd_id.documentDate).format("DDMM")};"${statementallocation.cd_id.documentNumber}";;;"${`ZE${statementallocation.description}${escapeString(statementallocation.bs_id.text)}`.substring(0,59)}";;;;;;;"Geschäftspartner";"${statementallocation.cd_id.customer.name}";"Kundennummer";"${statementallocation.cd_id.customer.customerNumber}";"Belegnummer";"${statementallocation.cd_id.documentNumber}";"Leistungsdatum";"${dayjs(statementallocation.cd_id.deliveryDate).format("DD.MM.YYYY")}";"Belegdatum";"${dayjs(statementallocation.cd_id.documentDate).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`) + } else if(statementallocation.ii_id) { + bookingLines.push(`${displayCurrency(statementallocation.amount,true)};"${shSelector}";;;;;${statementallocation.bs_id.account.datevNumber};${statementallocation.ii_id.vendor.vendorNumber};"";${dayjs(statementallocation.ii_id.date).format("DDMM")};"${statementallocation.ii_id.reference}";;;"${`ZA${statementallocation.description} ${escapeString(statementallocation.bs_id.text)} `.substring(0,59)}";;;;;;;"Geschäftspartner";"${statementallocation.ii_id.vendor.name}";"Kundennummer";"${statementallocation.ii_id.vendor.vendorNumber}";"Belegnummer";"${statementallocation.ii_id.reference}";"Leistungsdatum";"${dayjs(statementallocation.ii_id.date).format("DD.MM.YYYY")}";"Belegdatum";"${dayjs(statementallocation.ii_id.date).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`) + } else if(statementallocation.account) { + bookingLines.push(`${displayCurrency(statementallocation.amount,true)};"${shSelector}";;;;;${statementallocation.bs_id.account.datevNumber};${statementallocation.account.number};"";${dayjs(statementallocation.bs_id.date).format("DDMM")};"";;;"${`${Math.sign(statementallocation.amount) > 0 ? "ZE" : "ZA"} ${statementallocation.account.number} - ${escapeString(statementallocation.account.label)}${escapeString(statementallocation.description)}${statementallocation.bs_id.text}`.substring(0,59)}";;;;;;;"Geschäftspartner";"${statementallocation.bs_id.credName}";"Kundennummer";"";"Belegnummer";"";"Leistungsdatum";"";"Belegdatum";"${dayjs(statementallocation.bs_id.date).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`) + } else if(statementallocation.vendor) { + bookingLines.push(`${displayCurrency(statementallocation.amount,true)};"${shSelector}";;;;;${statementallocation.bs_id.account.datevNumber};${statementallocation.vendor.vendorNumber};"";${dayjs(statementallocation.bs_id.date).format("DDMM")};"";;;"${`${Math.sign(statementallocation.amount) > 0 ? "ZE" : "ZA"} ${statementallocation.vendor.vendorNumber} - ${escapeString(statementallocation.vendor.name)}${escapeString(statementallocation.description)}${statementallocation.bs_id.text}`.substring(0,59)}";;;;;;;"Geschäftspartner";"${statementallocation.vendor.name}";"Kundennummer";"";"Belegnummer";"";"Leistungsdatum";"";"Belegdatum";"${dayjs(statementallocation.bs_id.date).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`) + } else if(statementallocation.customer) { + bookingLines.push(`${displayCurrency(statementallocation.amount,true)};"${shSelector}";;;;;${statementallocation.bs_id.account.datevNumber};${statementallocation.customer.customerNumber};"";${dayjs(statementallocation.bs_id.date).format("DDMM")};"";;;"${`${Math.sign(statementallocation.amount) > 0 ? "ZE" : "ZA"} ${statementallocation.customer.customerNumber} - ${escapeString(statementallocation.customer.name)}${escapeString(statementallocation.description)}${statementallocation.bs_id.text}`.substring(0,59)}";;;;;;;"Geschäftspartner";"${statementallocation.customer.name}";"Kundennummer";"";"Belegnummer";"";"Leistungsdatum";"";"Belegdatum";"${dayjs(statementallocation.bs_id.date).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`) + } else if(statementallocation.ownaccount) { + bookingLines.push(`${displayCurrency(statementallocation.amount,true)};"${shSelector}";;;;;${statementallocation.bs_id.account.datevNumber};${statementallocation.ownaccount.number};"";${dayjs(statementallocation.bs_id.date).format("DDMM")};"";;;"${`${Math.sign(statementallocation.amount) > 0 ? "ZE" : "ZA"} ${statementallocation.ownaccount.number} - ${escapeString(statementallocation.ownaccount.name)}${escapeString(statementallocation.description)}${statementallocation.bs_id.text}`.substring(0,59)}";;;;;;;"Geschäftspartner";"${statementallocation.ownaccount.name}";"Kundennummer";"";"Belegnummer";"";"Leistungsdatum";"";"Belegdatum";"${dayjs(statementallocation.bs_id.date).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`) + } + + + }) + + + let csvString = `${header}\n${colHeaders}\n`; + bookingLines.forEach(line => { + csvString += `${line}\n`; + }) + + const buchungsstapelReader = new TextReader(csvString) + await zipWriter.add(`EXTF_Buchungsstapel_von_${dayjs(startDate).format("DDMMYYYY")}_bis_${dayjs(endDate).format("DDMMYYYY")}.csv`, buchungsstapelReader) + + /*fs.writeFile(`output/EXTF_Buchungsstapel_von_${dayjs(startDate).format("DDMMYYYY")}_bis_${dayjs(endDate).format("DDMMYYYY")}.csv`, csvString, 'utf8', function (err) { + if (err) { + console.log('Some error occured - file either not saved or corrupted file saved.'); + console.log(err); + } else{ + console.log('It\'s saved!'); + } + });*/ + + // Kreditoren/Debitoren + let headerStammdaten = `"EXTF";700;16;"Debitoren/Kreditoren";5;${dayjs().format("YYYYMMDDHHmmssSSS")};;"FE";"Florian Federspiel";;${beraternr};${mandantennr};20250101;4;${dayjs(startDate).format("YYYYMMDD")};${dayjs(endDate).format("YYYYMMDD")};"Debitoren & Kreditoren";"FF";1;0;1;"EUR";;"";;;"03";;;"";""` + + let colHeadersStammdaten = `Konto;Name (Adressattyp Unternehmen);Unternehmensgegenstand;Name (Adressattyp natuerl. Person);Vorname (Adressattyp natuerl. Person);Name (Adressattyp keine Angabe);Adressatentyp;Kurzbezeichnung;EU-Land;EU-UStID;Anrede;Titel/Akad. Grad;Adelstitel;Namensvorsatz;Adressart;Strasse;Postfach;Postleitzahl;Ort;Land;Versandzusatz;Adresszusatz;Abweichende Anrede;Abw. Zustellbezeichnung 1;Abw. Zustellbezeichnung 2;Kennz. Korrespondenzadresse;Adresse Gueltig von;Adresse Gueltig bis;Telefon;Bemerkung (Telefon);Telefon GL;Bemerkung (Telefon GL);E-Mail;Bemerkung (E-Mail);Internet;Bemerkung (Internet);Fax;Bemerkung (Fax);Sonstige;Bemerkung (Sonstige);Bankleitzahl 1;Bankbezeichnung 1;Bankkonto-Nummer 1;Laenderkennzeichen 1;IBAN-Nr. 1;Leerfeld;SWIFT-Code 1;Abw. Kontoinhaber 1;Kennz. Haupt-Bankverb. 1;Bankverb. 1 Gueltig von;Bankverb. 1 Gueltig bis;Bankleitzahl 2;Bankbezeichnung 2;Bankkonto-Nummer 2;Laenderkennzeichen 2;IBAN-Nr. 2;Leerfeld;SWIFT-Code 2;Abw. Kontoinhaber 2;Kennz. Haupt-Bankverb. 2;Bankverb. 2 gueltig von;Bankverb. 2 gueltig bis;Bankleitzahl 3;Bankbezeichnung 3;Bankkonto-Nummer 3;Laenderkennzeichen 3;IBAN-Nr. 3;Leerfeld;SWIFT-Code 3;Abw. Kontoinhaber 3;Kennz. Haupt-Bankverb. 3;Bankverb. 3 gueltig von;Bankverb. 3 gueltig bis;Bankleitzahl 4;Bankbezeichnung 4;Bankkonto-Nummer 4;Laenderkennzeichen 4;IBAN-Nr. 4;Leerfeld;SWIFT-Code 4;Abw. Kontoinhaber 4;Kennz. Haupt-Bankverb. 4;Bankverb. 4 gueltig von;Bankverb. 4 gueltig bis;Bankleitzahl 5;Bankbezeichnung 5;Bankkonto-Nummer 5;Laenderkennzeichen 5;IBAN-Nr. 5;Leerfeld;SWIFT-Code 5;Abw. Kontoinhaber 5;Kennz. Haupt-Bankverb. 5;Bankverb. 5 gueltig von;Bankverb. 5 gueltig bis;Leerfeld;Briefanrede;Grussformel;Kunden-/Lief.-Nr.;Steuernummer;Sprache;Ansprechpartner;Vertreter;Sachbearbeiter;Diverse-Konto;Ausgabeziel;Waehrungssteuerung;Kreditlimit (Debitor);Zahlungsbedingung;Faelligkeit in Tagen (Debitor);Skonto in Prozent (Debitor);Kreditoren-Ziel 1 Tg.;Kreditoren-Skonto 1 %;Kreditoren-Ziel 2 Tg.;Kreditoren-Skonto 2 %;Kreditoren-Ziel 3 Brutto Tg.;Kreditoren-Ziel 4 Tg.;Kreditoren-Skonto 4 %;Kreditoren-Ziel 5 Tg.;Kreditoren-Skonto 5 %;Mahnung;Kontoauszug;Mahntext 1;Mahntext 2;Mahntext 3;Kontoauszugstext;Mahnlimit Betrag;Mahnlimit %;Zinsberechnung;Mahnzinssatz 1;Mahnzinssatz 2;Mahnzinssatz 3;Lastschrift;Verfahren;Mandantenbank;Zahlungstraeger;Indiv. Feld 1;Indiv. Feld 2;Indiv. Feld 3;Indiv. Feld 4;Indiv. Feld 5;Indiv. Feld 6;Indiv. Feld 7;Indiv. Feld 8;Indiv. Feld 9;Indiv. Feld 10;Indiv. Feld 11;Indiv. Feld 12;Indiv. Feld 13;Indiv. Feld 14;Indiv. Feld 15;Abweichende Anrede (Rechnungsadresse);Adressart (Rechnungsadresse);Strasse (Rechnungsadresse);Postfach (Rechnungsadresse);Postleitzahl (Rechnungsadresse);Ort (Rechnungsadresse);Land (Rechnungsadresse);Versandzusatz (Rechnungsadresse);Adresszusatz (Rechnungsadresse);Abw. Zustellbezeichnung 1 (Rechnungsadresse);Abw. Zustellbezeichnung 2 (Rechnungsadresse);Adresse Gueltig von (Rechnungsadresse);Adresse Gueltig bis (Rechnungsadresse);Bankleitzahl 6;Bankbezeichnung 6;Bankkonto-Nummer 6;Laenderkennzeichen 6;IBAN-Nr. 6;Leerfeld;SWIFT-Code 6;Abw. Kontoinhaber 6;Kennz. Haupt-Bankverb. 6;Bankverb. 6 gueltig von;Bankverb. 6 gueltig bis;Bankleitzahl 7;Bankbezeichnung 7;Bankkonto-Nummer 7;Laenderkennzeichen 7;IBAN-Nr. 7;Leerfeld;SWIFT-Code 7;Abw. Kontoinhaber 7;Kennz. Haupt-Bankverb. 7;Bankverb. 7 gueltig von;Bankverb. 7 gueltig bis;Bankleitzahl 8;Bankbezeichnung 8;Bankkonto-Nummer 8;Laenderkennzeichen 8;IBAN-Nr. 8;Leerfeld;SWIFT-Code 8;Abw. Kontoinhaber 8;Kennz. Haupt-Bankverb. 8;Bankverb. 8 gueltig von;Bankverb. 8 gueltig bis;Bankleitzahl 9;Bankbezeichnung 9;Bankkonto-Nummer 9;Laenderkennzeichen 9;IBAN-Nr. 9;Leerfeld;SWIFT-Code 9;Abw. Kontoinhaber 9;Kennz. Haupt-Bankverb. 9;Bankverb. 9 gueltig von;Bankverb. 9 gueltig bis;Bankleitzahl 10;Bankbezeichnung 10;Bankkonto-Nummer 10;Laenderkennzeichen 10;IBAN-Nr. 10;Leerfeld;SWIFT-Code 10;Abw. Kontoinhaber 10;Kennz. Haupt-Bankverb. 10;Bankverb 10 Gueltig von;Bankverb 10 Gueltig bis;Nummer Fremdsystem;Insolvent;SEPA-Mandatsreferenz 1;SEPA-Mandatsreferenz 2;SEPA-Mandatsreferenz 3;SEPA-Mandatsreferenz 4;SEPA-Mandatsreferenz 5;SEPA-Mandatsreferenz 6;SEPA-Mandatsreferenz 7;SEPA-Mandatsreferenz 8;SEPA-Mandatsreferenz 9;SEPA-Mandatsreferenz 10;Verknuepftes OPOS-Konto;Mahnsperre bis;Lastschriftsperre bis;Zahlungssperre bis;Gebuehrenberechnung;Mahngebuehr 1;Mahngebuehr 2;Mahngebuehr 3;Pauschalberechnung;Verzugspauschale 1;Verzugspauschale 2;Verzugspauschale 3;Alternativer Suchname;Status;Anschrift manuell geaendert (Korrespondenzadresse);Anschrift individuell (Korrespondenzadresse);Anschrift manuell geaendert (Rechnungsadresse);Anschrift individuell (Rechnungsadresse);Fristberechnung bei Debitor;Mahnfrist 1;Mahnfrist 2;Mahnfrist 3;Letzte Frist` + const {data:customers} = await server.supabase.from("customers").select().eq("tenant",tenant).order("customerNumber") + const {data:vendors} = await server.supabase.from("vendors").select().eq("tenant",tenant).order("vendorNumber") + + let bookinglinesStammdaten = [] + + customers.forEach(customer => { + bookinglinesStammdaten.push(`${customer.customerNumber};"${customer.isCompany ? customer.name.substring(0,48): ''}";;"${!customer.isCompany ? (customer.lastname ? customer.lastname : customer.name) : ''}";"${!customer.isCompany ? (customer.firstname ? customer.firstname : '') : ''}";;${customer.isCompany ? 2 : 1};;;;;;;;"STR";"${customer.infoData.street ? customer.infoData.street : ''}";;"${customer.infoData.zip ? customer.infoData.zip : ''}";"${customer.infoData.city ? customer.infoData.city : ''}";;;"${customer.infoData.special ? customer.infoData.special : ''}";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;`) + + }) + + vendors.forEach(vendor => { + bookinglinesStammdaten.push(`${vendor.vendorNumber};"${vendor.name.substring(0,48)}";;;;;2;;;;;;;;"STR";"${vendor.infoData.street ? vendor.infoData.street : ''}";;"${vendor.infoData.zip ? vendor.infoData.zip : ''}";"${vendor.infoData.city ? vendor.infoData.city : ''}";;;"${vendor.infoData.special ? vendor.infoData.special : ''}";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;`) + + }) + + let csvStringStammdaten = `${headerStammdaten}\n${colHeadersStammdaten}\n`; + bookinglinesStammdaten.forEach(line => { + csvStringStammdaten += `${line}\n`; + }) + + const stammdatenReader = new TextReader(csvStringStammdaten) + await zipWriter.add(`EXTF_Stammdaten_von_${dayjs(startDate).format("DDMMYYYY")}_bis_${dayjs(endDate).format("DDMMYYYY")}.csv`, stammdatenReader) + + + + /*fs.writeFile(`output/EXTF_Stammdaten_von_${dayjs(startDate).format("DDMMYYYY")}_bis_${dayjs(endDate).format("DDMMYYYY")}.csv`, csvStringStammdaten, 'utf8', function (err) { + if (err) { + console.log('Some error occured - file either not saved or corrupted file saved.'); + console.log(err); + } else{ + console.log('It\'s saved!'); + } + });*/ + + //Sachkonten + let headerSachkonten = `"EXTF";700;20;"Kontenbeschriftungen";3;${dayjs().format("YYYYMMDDHHmmssSSS")};;"FE";"Florian Federspiel";;${beraternr};${mandantennr};20250101;4;${dayjs(startDate).format("YYYYMMDD")};${dayjs(endDate).format("YYYYMMDD")};"Sachkonten";"FF";1;0;1;"EUR";;"";;;"03";;;"";""` + + let colHeadersSachkonten = `Konto;Kontenbeschriftung;Sprach-ID;Kontenbeschriftung lang` + const {data:bankaccounts} = await server.supabase.from("bankaccounts").select().eq("tenant",tenant).order("datevNumber") + + let bookinglinesSachkonten = [] + + bankaccounts.forEach(bankaccount => { + bookinglinesSachkonten.push(`${bankaccount.datevNumber};"${bankaccount.name}";"de-DE";`) + + }) + + let csvStringSachkonten = `${headerSachkonten}\n${colHeadersSachkonten}\n`; + bookinglinesSachkonten.forEach(line => { + csvStringSachkonten += `${line}\n`; + }) + + const sachkontenReader = new TextReader(csvStringSachkonten) + await zipWriter.add(`EXTF_Sachkonten_von_${dayjs(startDate).format("DDMMYYYY")}_bis_${dayjs(endDate).format("DDMMYYYY")}.csv`, sachkontenReader) + + /*fs.writeFile(`output/EXTF_Sachkonten_von_${dayjs(startDate).format("DDMMYYYY")}_bis_${dayjs(endDate).format("DDMMYYYY")}.csv`, csvStringSachkonten, 'utf8', function (err) { + if (err) { + console.log('Some error occured - file either not saved or corrupted file saved.'); + console.log(err); + } else{ + console.log('It\'s saved!'); + } + });*/ + + + let obj = { + archive: { + '@version':"5.0", + "@generatingSystem":"fedeo.de", + "@xsi:schemaLocation":"http://xml.datev.de/bedi/tps/document/v05.0 Document_v050.xsd", + "@xmlns":"http://xml.datev.de/bedi/tps/document/v05.0", + "@xmlns:xsi":"http://www.w3.org/2001/XMLSchema-instance", + header: { + date: dayjs().format("YYYY-MM-DDTHH:mm:ss") + }, + content: { + document: [] + } + } + } + + filesCreateddocuments.forEach(file => { + obj.archive.content.document.push({ + "@guid": file.id, + extension: { + "@xsi:type":"File", + "@name":`${file.id}.pdf` + } + }) + }) + + filesIncomingInvoices.forEach(file => { + obj.archive.content.document.push({ + "@guid": file.id, + extension: { + "@xsi:type":"File", + "@name":`${file.id}.pdf` + } + }) + }) + + let doc = xmlbuilder.create(obj, {encoding: 'UTF-8', standalone: true}) + + //console.log(doc.end({pretty: true})); + + const documentsReader = new TextReader(doc.end({pretty: true})) + await zipWriter.add(`document.xml`, documentsReader) + + + + + /*function toBuffer(arrayBuffer) { + const buffer = Buffer.alloc(arrayBuffer.byteLength); + const view = new Uint8Array(arrayBuffer); + for (let i = 0; i < buffer.length; ++i) { + buffer[i] = view[i]; + } + return buffer; + }*/ + + + const arrayBuffer = await (await zipWriter.close()).arrayBuffer() + return Buffer.from(arrayBuffer) + } catch(error) { + console.log(error) + } + + +} \ No newline at end of file diff --git a/src/utils/functions.ts b/src/utils/functions.ts new file mode 100644 index 0000000..e5fdda8 --- /dev/null +++ b/src/utils/functions.ts @@ -0,0 +1,23 @@ +import {FastifyInstance} from "fastify"; + +export const useNextNumberRangeNumber = async (server:FastifyInstance, tenantId:number,numberRange)=> { + const {data:tenant} = await server.supabase.from("tenants").select().eq("id",tenantId).single() + + const numberRanges = tenant.numberRanges + + const usedNumber = (numberRanges[numberRange].prefix ? numberRanges[numberRange].prefix : "") + numberRanges[numberRange].nextNumber + (numberRanges[numberRange].suffix ? numberRanges[numberRange].suffix : "") + + let newNumberRange = numberRanges + + newNumberRange[numberRange].nextNumber += 1 + + const {error} = await server.supabase.from("tenants").update({numberRanges: newNumberRange}).eq("id",tenantId) + + if(error) { + console.log(error) + } else { + return { + usedNumber + } + } +} \ No newline at end of file diff --git a/src/utils/history.ts b/src/utils/history.ts index 5cc39b0..477c666 100644 --- a/src/utils/history.ts +++ b/src/utils/history.ts @@ -43,7 +43,8 @@ export async function insertHistoryItem( spaces: "space", trackingtrips: "trackingtrip", createddocuments: "createddocument", - inventoryitemgroups: "inventoryitemgroup" + inventoryitemgroups: "inventoryitemgroup", + bankstatements: "bankstatement" } const fkColumn = columnMap[params.entity] diff --git a/src/utils/pdf.ts b/src/utils/pdf.ts new file mode 100644 index 0000000..6e1681a --- /dev/null +++ b/src/utils/pdf.ts @@ -0,0 +1,854 @@ +import {PDFDocument, StandardFonts, rgb} from "pdf-lib" +import dayjs from "dayjs" +import {renderAsCurrency, splitStringBySpace} from "./stringRendering"; +import {FastifyInstance} from "fastify"; + +const getCoordinatesForPDFLib = (x:number ,y:number, page:any) => { + /* + * @param x the wanted X Parameter in Millimeters from Top Left + * @param y the wanted Y Parameter in Millimeters from Top Left + * @param page the page Object + * + * @returns x,y object + * */ + + + let retX = x * 2.83 + let retY = page.getHeight()-(y*2.83) + + return { + x: retX, + y: retY + } +} + + + + + +export const createInvoicePDF = async (server:FastifyInstance, returnMode, invoiceData, backgroundPath:string) => { + + console.log(returnMode, invoiceData, backgroundPath) + + const genPDF = async (invoiceData, backgroundSourceBuffer) => { + const pdfDoc = await PDFDocument.create() + + const font = await pdfDoc.embedFont(StandardFonts.Helvetica) + const fontBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold) + + let pages = [] + let pageCounter = 1 + + + //const backgroundPdfSourceBuffer = await fetch("/Briefpapier.pdf").then((res) => res.arrayBuffer()) + const backgroudPdf = await PDFDocument.load(backgroundSourceBuffer) + + const firstPageBackground = await pdfDoc.embedPage(backgroudPdf.getPages()[0]) + const secondPageBackground = await pdfDoc.embedPage(backgroudPdf.getPages()[backgroudPdf.getPages().length > 1 ? 1 : 0]) + + // + const page1 = pdfDoc.addPage() + + // + + page1.drawPage(firstPageBackground, { + x: 0, + y: 0, + }) + // + pages.push(page1) + // + + + //Falzmarke 1 + pages[pageCounter - 1].drawLine({ + start: getCoordinatesForPDFLib(0, 105, page1), + end: getCoordinatesForPDFLib(5, 105, page1), + thickness: 0.2, + color: rgb(0, 0, 0), + opacity: 1 + }) + + //Lochmarke + pages[pageCounter - 1].drawLine({ + start: getCoordinatesForPDFLib(0, 148.5, page1), + end: getCoordinatesForPDFLib(5, 148.5, page1), + thickness: 0.2, + color: rgb(0, 0, 0), + opacity: 1 + }) + + //Falzmarke 2 + pages[pageCounter - 1].drawLine({ + start: getCoordinatesForPDFLib(0, 210, page1), + end: getCoordinatesForPDFLib(5, 210, page1), + thickness: 0.2, + color: rgb(0, 0, 0), + opacity: 1 + }) + + + /*page1.drawLine({ + start: getCoordinatesForPDFLib(20,45,page1), + end: getCoordinatesForPDFLib(105,45,page1), + thickness: 0.5, + color: rgb(0,0,0), + opacity: 1 + })*/ + + if (!invoiceData.addressLine) console.log("Missing Addressline") + + pages[pageCounter - 1].drawText(invoiceData.adressLine, { + ...getCoordinatesForPDFLib(21, 48, page1), + size: 6, + color: rgb(0, 0, 0), + lineHeight: 6, + opacity: 1, + maxWidth: 240 + }) + + + /*page1.drawLine({ + start: getCoordinatesForPDFLib(20,50,page1), + end: getCoordinatesForPDFLib(105,50,page1), + thickness: 0.5, + color: rgb(0,0,0), + opacity: 1 + })*/ + + let partLinesAdded = 0 + invoiceData.recipient.forEach((info, index) => { + + + let maxSplitLength = 35 + let splittedContent = splitStringBySpace(info, maxSplitLength) + + + splittedContent.forEach((part, partIndex) => { + if (partIndex === 0) { + pages[pageCounter - 1].drawText(part, { + ...getCoordinatesForPDFLib(21, 55 + index * 5 + partLinesAdded * 5, page1), + size: 10, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + maxWidth: 240 + }) + } else { + partLinesAdded++ + + pages[pageCounter - 1].drawText(part, { + ...getCoordinatesForPDFLib(21, 55 + index * 5 + partLinesAdded * 5, page1), + size: 10, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + maxWidth: 240 + }) + } + + /*if(partIndex > 0) partLinesAdded++ + + pages[pageCounter - 1].drawText(part, { + y: getCoordinatesForPDFLib(21,55+index*5+partLinesAdded*5, page1).y, + x: getCoordinatesForPDFLib(21,55+index*5+partLinesAdded*5,page1).x + 230 - font.widthOfTextAtSize(part,10), + size:10, + color:rgb(0,0,0), + lineHeight:10, + opacity: 1, + maxWidth: 240 + })*/ + + }) + }) + + //Rechts + + partLinesAdded = 0 + + invoiceData.info.forEach((info, index) => { + + let maxSplitLength = 34 + let splittedContent = splitStringBySpace(info.content, maxSplitLength) + + + splittedContent.forEach((part, partIndex) => { + if (partIndex === 0) { + pages[pageCounter - 1].drawText(info.label, { + ...getCoordinatesForPDFLib(116, 55 + index * 5 + partLinesAdded * 5, page1), + size: 10, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + maxWidth: 240 + }) + } + + if (partIndex > 0) partLinesAdded++ + + pages[pageCounter - 1].drawText(part, { + y: getCoordinatesForPDFLib(116, 55 + index * 5 + partLinesAdded * 5, page1).y, + x: getCoordinatesForPDFLib(116, 55 + index * 5 + partLinesAdded * 5, page1).x + 230 - font.widthOfTextAtSize(part, 10), + size: 10, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + maxWidth: 240 + }) + + }) + }) + + + /*page1.drawLine({ + start: getCoordinatesForPDFLib(125,90,page1), + end: getCoordinatesForPDFLib(200,90,page1), + thickness: 0.5, + color: rgb(0,0,0), + opacity: 1 + })*/ + + //Title + + /*page1.drawLine({ + start: getCoordinatesForPDFLib(20,95,page1), + end: getCoordinatesForPDFLib(200,95,page1), + thickness: 0.5, + color: rgb(0,0,0), + opacity: 1 + })*/ + + if (!invoiceData.title) console.log("Missing Title") + + pages[pageCounter - 1].drawText(invoiceData.title, { + ...getCoordinatesForPDFLib(20, 100, page1), + size: 13, + color: rgb(0, 0, 0), + lineHeight: 15, + opacity: 1, + maxWidth: 500 + }) + + /*page1.drawLine({ + start: getCoordinatesForPDFLib(20,105,page1), + end: getCoordinatesForPDFLib(200,105,page1), + thickness: 0.5, + color: rgb(0,0,0), + opacity: 1 + })*/ + if (!invoiceData.description) console.log("Missing Description") + + if (invoiceData.description) { + pages[pageCounter - 1].drawText(invoiceData.description, { + ...getCoordinatesForPDFLib(20, 112, page1), + size: 13, + color: rgb(0, 0, 0), + lineHeight: 15, + opacity: 1, + maxWidth: 500 + }) + } + + if (!invoiceData.startText) console.log("Missing StartText") + + + pages[pageCounter - 1].drawText(invoiceData.startText, { + ...getCoordinatesForPDFLib(20, 119, page1), + size: 10, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + maxWidth: 500 + }) + + /*page1.drawLine({ + start: getCoordinatesForPDFLib(20,115,page1), + end: getCoordinatesForPDFLib(200,115,page1), + thickness: 0.5, + color: rgb(0,0,0), + opacity: 1 + })*/ + + pages[pageCounter - 1].drawLine({ + start: getCoordinatesForPDFLib(20, 140, page1), + end: getCoordinatesForPDFLib(199, 140, page1), + thickness: 0.1, + color: rgb(0, 0, 0), + opacity: 1, + }) + + /*pages[pageCounter - 1].drawRectangle({ + ...getCoordinatesForPDFLib(20,140, page1), + width: 180 * 2.83, + height: 8 * 2.83, + color: rgb(0,0,0), + opacity: 0, + borderWidth: 0.1 + })*/ + + //Header + + pages[pageCounter - 1].drawText("Pos", { + ...getCoordinatesForPDFLib(21, 137, page1), + size: 12, + color: rgb(0, 0, 0), + lineHeight: 12, + opacity: 1, + maxWidth: 240, + font: fontBold + }) + + pages[pageCounter - 1].drawText("Menge", { + ...getCoordinatesForPDFLib(35, 137, page1), + size: 12, + color: rgb(0, 0, 0), + lineHeight: 12, + opacity: 1, + maxWidth: 240, + font: fontBold + }) + + pages[pageCounter - 1].drawText("Bezeichnung", { + ...getCoordinatesForPDFLib(52, 137, page1), + size: 12, + color: rgb(0, 0, 0), + lineHeight: 12, + opacity: 1, + maxWidth: 240, + font: fontBold + }) + + if (invoiceData.type !== "deliveryNotes") { + pages[pageCounter - 1].drawText("Steuer", { + ...getCoordinatesForPDFLib(135, 137, page1), + size: 12, + color: rgb(0, 0, 0), + lineHeight: 12, + opacity: 1, + maxWidth: 240, + font: fontBold + }) + + pages[pageCounter - 1].drawText("Einheitspreis", { + ...getCoordinatesForPDFLib(150, 137, page1), + size: 12, + color: rgb(0, 0, 0), + lineHeight: 12, + opacity: 1, + maxWidth: 240, + font: fontBold + }) + + pages[pageCounter - 1].drawText("Gesamt", { + y: getCoordinatesForPDFLib(25, 137, page1).y, + x: getCoordinatesForPDFLib(25, 137, page1).x + 490 - fontBold.widthOfTextAtSize("Gesamt", 12), + size: 12, + color: rgb(0, 0, 0), + lineHeight: 12, + opacity: 1, + maxWidth: 240, + font: fontBold + }) + } + + + let rowHeight = 145.5 + + let pageIndex = 0 + + + invoiceData.rows.forEach((row, index) => { + + if (!["pagebreak", "title", "text"].includes(row.mode)) { + + + if (!row.pos) console.log("Missing Row Pos") + + pages[pageCounter - 1].drawText(String(row.pos), { + ...getCoordinatesForPDFLib(21, rowHeight, page1), + size: 10, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + maxWidth: 240 + }) + + if (!row.quantity) console.log("Missing Row Quantity") + if (!row.unit) console.log("Missing Row Unit") + + pages[pageCounter - 1].drawText((row.optional || row.alternative) ? `(${row.quantity} ${row.unit})` : `${row.quantity} ${row.unit}`, { + ...getCoordinatesForPDFLib(35, rowHeight, page1), + size: 10, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + maxWidth: 240 + }) + + let rowTextLines = 0 + + if (invoiceData.type !== "deliveryNotes") { + pages[pageCounter - 1].drawText(splitStringBySpace(row.text, 35).join("\n"), { + ...getCoordinatesForPDFLib(52, rowHeight, page1), + size: 10, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + font: fontBold + }) + rowTextLines = splitStringBySpace(row.text, 35).length + + } else { + pages[pageCounter - 1].drawText(splitStringBySpace(row.text, 80).join("\n"), { + ...getCoordinatesForPDFLib(52, rowHeight, page1), + size: 10, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + font: fontBold + }) + + rowTextLines = splitStringBySpace(row.text, 80).length + } + + let rowDescriptionLines = 0 + + if (row.descriptionText) { + if (invoiceData.type !== "deliveryNotes") { + rowDescriptionLines = splitStringBySpace(row.descriptionText, 60).length + pages[pageCounter - 1].drawText(splitStringBySpace(row.descriptionText, 60).join("\n"), { + ...getCoordinatesForPDFLib(52, rowHeight + (rowTextLines * 5), page1), + size: 10, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + }) + + } else { + rowDescriptionLines = splitStringBySpace(row.descriptionText, 80).length + pages[pageCounter - 1].drawText(splitStringBySpace(row.descriptionText, 80).join("\n"), { + ...getCoordinatesForPDFLib(52, rowHeight + (rowTextLines * 5), page1), + size: 10, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + }) + } + } + + + if (invoiceData.type !== "deliveryNotes") { + pages[pageCounter - 1].drawText(`${row.taxPercent} %`, { + ...getCoordinatesForPDFLib(135, rowHeight, page1), + size: 10, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + maxWidth: 240 + }) + pages[pageCounter - 1].drawText(row.price, { + ...getCoordinatesForPDFLib(150, rowHeight, page1), + size: 10, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + maxWidth: 240 + }) + + + pages[pageCounter - 1].drawText((row.optional || row.alternative) ? `(${row.rowAmount})` : row.rowAmount, { + y: getCoordinatesForPDFLib(25, rowHeight, page1).y, + x: getCoordinatesForPDFLib(25, rowHeight, page1).x + 490 - font.widthOfTextAtSize((row.optional || row.alternative) ? `(${row.rowAmount})` : row.rowAmount, 10), + size: 10, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + maxWidth: 240, + }) + + if (row.discountPercent > 0) { + + let text = row.discountText + + if (row.optional) text = `Optional - ${text}` + if (row.alternative) text = `Alternativ - ${text}` + + + pages[pageCounter - 1].drawText(text, { + y: getCoordinatesForPDFLib(25, rowHeight + 5, page1).y, + x: getCoordinatesForPDFLib(25, rowHeight + 5, page1).x + 490 - font.widthOfTextAtSize(text, 8), + size: 8, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + maxWidth: 240, + }) + } else if (row.optional) { + pages[pageCounter - 1].drawText("Optional", { + y: getCoordinatesForPDFLib(25, rowHeight + 5, page1).y, + x: getCoordinatesForPDFLib(25, rowHeight + 5, page1).x + 490 - font.widthOfTextAtSize("Optional", 8), + size: 8, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + maxWidth: 240, + }) + } else if (row.alternative) { + pages[pageCounter - 1].drawText("Alternativ", { + y: getCoordinatesForPDFLib(25, rowHeight + 5, page1).y, + x: getCoordinatesForPDFLib(25, rowHeight + 5, page1).x + 490 - font.widthOfTextAtSize("Alternativ", 8), + size: 8, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + maxWidth: 240, + }) + } + } + + if (row.descriptionText) { + rowHeight += rowDescriptionLines * 4.5 + rowTextLines * 5.5 + } else if (row.discountPercent) { + rowHeight += (rowTextLines + 1) * 5.5 + } else if (row.optional || row.alternative) { + rowHeight += (rowTextLines + 1) * 5.5 + } else { + rowHeight += rowTextLines * 5.5 + } + + + pageIndex += 1 + + + } else if (row.mode === 'pagebreak') { + + console.log(invoiceData.rows[index + 1]) + + if (invoiceData.rows[index + 1].mode === 'title') { + let transferSumText = `Übertrag: ${invoiceData.total.titleSumsTransfer[Object.keys(invoiceData.total.titleSums)[invoiceData.rows[index + 1].pos - 2]]}` + pages[pageCounter - 1].drawText(transferSumText, { + y: getCoordinatesForPDFLib(21, rowHeight - 2, page1).y, + x: getCoordinatesForPDFLib(21, rowHeight - 2, page1).x + 500 - fontBold.widthOfTextAtSize(transferSumText, 10), + size: 10, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + maxWidth: 240, + font: fontBold + }) + } + + + const page = pdfDoc.addPage() + + page.drawPage(secondPageBackground, { + x: 0, + y: 0, + }) + + //Falzmarke 1 + page.drawLine({ + start: getCoordinatesForPDFLib(0, 105, page1), + end: getCoordinatesForPDFLib(7, 105, page1), + thickness: 0.25, + color: rgb(0, 0, 0), + opacity: 1 + }) + + //Lochmarke + page.drawLine({ + start: getCoordinatesForPDFLib(0, 148.5, page1), + end: getCoordinatesForPDFLib(7, 148.5, page1), + thickness: 0.25, + color: rgb(0, 0, 0), + opacity: 1 + }) + + //Falzmarke 2 + page.drawLine({ + start: getCoordinatesForPDFLib(0, 210, page1), + end: getCoordinatesForPDFLib(7, 210, page1), + thickness: 0.25, + color: rgb(0, 0, 0), + opacity: 1 + }) + + page.drawText("Pos", { + ...getCoordinatesForPDFLib(21, 22, page1), + size: 12, + color: rgb(0, 0, 0), + lineHeight: 12, + opacity: 1, + maxWidth: 240, + font: fontBold + }) + + page.drawText("Menge", { + ...getCoordinatesForPDFLib(35, 22, page1), + size: 12, + color: rgb(0, 0, 0), + lineHeight: 12, + opacity: 1, + maxWidth: 240, + font: fontBold + }) + + page.drawText("Bezeichnung", { + ...getCoordinatesForPDFLib(52, 22, page1), + size: 12, + color: rgb(0, 0, 0), + lineHeight: 12, + opacity: 1, + maxWidth: 240, + font: fontBold + }) + + if (invoiceData.type !== "deliveryNotes") { + page.drawText("Steuer", { + ...getCoordinatesForPDFLib(135, 22, page1), + size: 12, + color: rgb(0, 0, 0), + lineHeight: 12, + opacity: 1, + maxWidth: 240, + font: fontBold + }) + + page.drawText("Einheitspreis", { + ...getCoordinatesForPDFLib(150, 22, page1), + size: 12, + color: rgb(0, 0, 0), + lineHeight: 12, + opacity: 1, + maxWidth: 240, + font: fontBold + }) + + page.drawText("Gesamt", { + y: getCoordinatesForPDFLib(25, 22, page1).y, + x: getCoordinatesForPDFLib(25, 22, page1).x + 490 - fontBold.widthOfTextAtSize("Gesamt", 12), + size: 12, + color: rgb(0, 0, 0), + lineHeight: 12, + opacity: 1, + maxWidth: 240, + font: fontBold + }) + } + + pageCounter += 1; + pageIndex = 0; + rowHeight = 30; + + pages.push(page) + + } else if (row.mode === 'title') { + if (index === 0 || pageIndex === 0) { + rowHeight += 3 + } else { + let transferSumText = `Übertrag: ${invoiceData.total.titleSumsTransfer[Object.keys(invoiceData.total.titleSums)[row.pos - 2]]}` + pages[pageCounter - 1].drawText(transferSumText, { + y: getCoordinatesForPDFLib(21, rowHeight - 2, page1).y, + x: getCoordinatesForPDFLib(21, rowHeight - 2, page1).x + 500 - fontBold.widthOfTextAtSize(transferSumText, 10), + size: 10, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + maxWidth: 240, + font: fontBold + }) + + pages[pageCounter - 1].drawLine({ + start: getCoordinatesForPDFLib(20, rowHeight, page1), + end: getCoordinatesForPDFLib(199, rowHeight, page1), + thickness: 0.2, + color: rgb(0, 0, 0), + opacity: 1, + }) + rowHeight += 5 + } + + pages[pageCounter - 1].drawText(String(row.pos), { + ...getCoordinatesForPDFLib(21, rowHeight, page1), + size: 10, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + maxWidth: 240, + font: fontBold + }) + + pages[pageCounter - 1].drawText(splitStringBySpace(row.text, 60).join("\n"), { + ...getCoordinatesForPDFLib(35, rowHeight, page1), + size: 12, + color: rgb(0, 0, 0), + lineHeight: 12, + opacity: 1, + maxWidth: 500, + font: fontBold + }) + + rowHeight += splitStringBySpace(row.text, 60).length * 4.5 + } else if (row.mode === 'text') { + if (index === 0 || pageIndex === 0) { + rowHeight += 3 + } + + if (row.descriptionText) { + pages[pageCounter - 1].drawText(splitStringBySpace(row.descriptionText, 70).join("\n"), { + ...getCoordinatesForPDFLib(35, rowHeight, page1), + size: 10, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + }) + + rowHeight += (splitStringBySpace(row.descriptionText, 70) || []).length * 4 + rowHeight += 4 + } + } + console.log(rowHeight) + }) + + + let endTextDiff = 35 + + if (invoiceData.type !== "deliveryNotes") { + pages[pageCounter - 1].drawLine({ + start: getCoordinatesForPDFLib(20, rowHeight, page1), + end: getCoordinatesForPDFLib(198, rowHeight, page1), + thickness: 0.2, + color: rgb(0, 0, 0), + opacity: 1, + }) + rowHeight += 6 + + + if (Object.keys(invoiceData.total.titleSums).length > 0) { + Object.keys(invoiceData.total.titleSums).forEach((key, index) => { + pages[pageCounter - 1].drawText(splitStringBySpace(key, 60).join("\n"), { + ...getCoordinatesForPDFLib(21, rowHeight, page1), + size: 11, + color: rgb(0, 0, 0), + lineHeight: 11, + opacity: 1, + font: fontBold + }) + + pages[pageCounter - 1].drawText(invoiceData.total.titleSums[key], { + y: getCoordinatesForPDFLib(21, rowHeight, page1).y, + x: getCoordinatesForPDFLib(21, rowHeight, page1).x + 500 - fontBold.widthOfTextAtSize(invoiceData.total.titleSums[key], 11), + size: 11, + color: rgb(0, 0, 0), + lineHeight: 11, + opacity: 1, + maxWidth: 240, + font: fontBold + }) + + rowHeight += splitStringBySpace(key, 60).length * 5 + }) + + /*let titleSumsArray = Object.keys(invoiceData.total.titleSums) + titleSumsArray.forEach(sum => { + let length = splitStringBySpace(sum,60).length + rowHeight += length *6 + })*/ + + //rowHeight += Object.keys(invoiceData.total.titleSums) + + pages[pageCounter - 1].drawLine({ + start: getCoordinatesForPDFLib(20, rowHeight, page1), + end: getCoordinatesForPDFLib(198, rowHeight, page1), + thickness: 0.2, + color: rgb(0, 0, 0), + opacity: 1, + }) + + rowHeight += 5 + } + + invoiceData.totalArray.forEach((item, index) => { + pages[pageCounter - 1].drawText(item.label, { + ...getCoordinatesForPDFLib(21, rowHeight + 8 * index, page1), + size: 11, + color: rgb(0, 0, 0), + lineHeight: 11, + opacity: 1, + maxWidth: 240, + font: fontBold + }) + + pages[pageCounter - 1].drawText(item.content, { + y: getCoordinatesForPDFLib(21, rowHeight + 8 * index, page1).y, + x: getCoordinatesForPDFLib(21, rowHeight + 8 * index, page1).x + 500 - fontBold.widthOfTextAtSize(item.content, 11), + size: 11, + color: rgb(0, 0, 0), + lineHeight: 11, + opacity: 1, + maxWidth: 240, + font: fontBold + }) + }) + + if (invoiceData.taxType !== "13b UStG" && invoiceData.taxType !== "19 UStG" && invoiceData.taxType !== "12.3 UStG") { + + } else { + if (invoiceData.taxType === "13b UStG") { + pages[pageCounter - 1].drawText("Die Umsatzsteuer für diese Leistung schuldet nach §13b UStG der Leistungsempfänger", { + ...getCoordinatesForPDFLib(21, rowHeight + invoiceData.totalArray.length * 8, page1), + size: 10, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + maxWidth: 500 + }) + } else if (invoiceData.taxType === "19 UStG") { + pages[pageCounter - 1].drawText("Als Kleinunternehmer im Sinne von § 19 Abs. 1 UStG wird keine Umsatzsteuer berechnet.", { + ...getCoordinatesForPDFLib(20, rowHeight + invoiceData.totalArray.length * 8, page1), + size: 10, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + maxWidth: 500 + }) + } else if (invoiceData.taxType === "12.3 UStG") { + pages[pageCounter - 1].drawText("Umsatzsteuer befreite Lieferung/Leistung für PV-Anlagen gemäß § 12 Absatz 3 UStG.", { + ...getCoordinatesForPDFLib(20, rowHeight + invoiceData.totalArray.length * 8, page1), + size: 10, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + maxWidth: 500 + }) + } + + + } + + + pages[pageCounter - 1].drawText(invoiceData.endText, { + ...getCoordinatesForPDFLib(21, rowHeight + endTextDiff + (invoiceData.totalArray.length - 3) * 8, page1), + size: 10, + color: rgb(0, 0, 0), + lineHeight: 10, + opacity: 1, + maxWidth: 500 + }) + + return await pdfDoc.saveAsBase64() + } + + } + + const {data:backgroundPDFData,error:backgroundPDFError} = await server.supabase.storage.from("files").download(backgroundPath) + + const pdfBytes = await genPDF(invoiceData, await backgroundPDFData.arrayBuffer()) + + if(returnMode === "base64"){ + return { + mimeType: 'application/pdf', + base64: pdfBytes + } + } +} \ No newline at end of file diff --git a/src/utils/stringRendering.ts b/src/utils/stringRendering.ts new file mode 100644 index 0000000..72dbad1 --- /dev/null +++ b/src/utils/stringRendering.ts @@ -0,0 +1,51 @@ + +export const renderAsCurrency = (value: string | number,currencyString = "€") => { + return `${Number(value).toFixed(2).replace(".",",")} ${currencyString}` +} + +export const splitStringBySpace = (input:string,maxSplitLength:number,removeLinebreaks = false) => { + + if(removeLinebreaks) { + input = input.replaceAll("\n","") + } + + let splitStrings: string[] = [] + + input.split("\n").forEach(string => { + splitStrings.push(string) + }) + + let returnSplitStrings: string[] = [] + + splitStrings.forEach(string => { + let regex = / /gi, result, indices = []; + while ( (result = regex.exec(string)) ) { + indices.push(result.index); + } + + let lastIndex = 0 + + if(string.length > maxSplitLength) { + let tempStrings = [] + + for (let i = maxSplitLength; i < string.length; i = i + maxSplitLength) { + let nearestIndex = indices.length > 0 ? indices.reduce(function(prev, curr) { + return (Math.abs(curr - i) < Math.abs(prev - i) ? curr : prev); + }) : i + + tempStrings.push(string.substring(lastIndex,nearestIndex)) + + lastIndex = indices.length > 0 ? nearestIndex + 1 : nearestIndex + } + + tempStrings.push(string.substring(lastIndex,input.length)) + + returnSplitStrings.push(...tempStrings) + + } else { + returnSplitStrings.push(string) + } + }) + + return returnSplitStrings +} \ No newline at end of file