diff --git a/.gitea/ISSUE_TEMPLATE/bug_report.md b/.gitea/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..033d01b --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: 🐛 Bug Report +about: Erstelle einen Bericht, um uns zu helfen, das Projekt zu verbessern. +title: '[BUG] ' +labels: bug +assignees: '' + +--- + +**Beschreibung** +Eine klare und prĂ€gnante Beschreibung des Fehlers. + +**Reproduktion** +Schritte, um den Fehler zu reproduzieren: + +Entweder: +1. Gehe zu '...' +2. Klicke auf '...' +3. Scrolle runter zu '...' +4. Siehe Fehler + +Oder Link zur Seite + +**Erwartetes Verhalten** +Eine klare Beschreibung dessen, was du erwartet hast. + +**Screenshots** +Falls zutreffend, fĂŒge hier Screenshots oder Gifs hinzu, um das Problem zu verdeutlichen. + +**Achtung: Achte bitte auf Datenschutz deiner Daten sowie der Daten deiner Kunden. Sollten ein Screenshot nur mit Daten möglich sein, schwĂ€rze diese bitte vor dem Upload.** + +**Umgebung:** +- Betriebssystem: [z.B. Windows, macOS, Linux] +- Browser / Version (falls relevant): [z.B. Chrome 120] +- Projekt-Version: [z.B. v1.0.2] + +**ZusĂ€tzlicher Kontext** +FĂŒge hier alle anderen Informationen zum Problem hinzu. \ No newline at end of file diff --git a/README.md b/README.md index 3b12464..870f8f0 100644 --- a/README.md +++ b/README.md @@ -1 +1,109 @@ -TEST \ No newline at end of file + + + +# Docker Compose Setup + +## ENV Vars + +- DOMAIN +- PDF_LICENSE +- DB_PASS +- DB_USER +- CONTACT_EMAIL + +## Docker Compose File +~~~ +services: + frontend: + image: git.federspiel.tech/flfeders/fedeo/frontend:main + restart: always + environment: + - NUXT_PUBLIC_API_BASE=https://${DOMAIN}/backend + - NUXT_PUBLIC_PDF_LICENSE=${PDF_LICENSE} + networks: + - traefik + labels: + - "traefik.enable=true" + - "traefik.docker.network=traefik" + - "traefik.port=3000" + # Middlewares + - "traefik.http.middlewares.fedeo-frontend-redirect-web-secure.redirectscheme.scheme=https" + # Web Entrypoint + - "traefik.http.routers.fedeo-frontend.middlewares=fedeo-frontend-redirect-web-secure" + - "traefik.http.routers.fedeo-frontend.rule=Host(`${DOMAIN}`) && PathPrefix(`/`)" + - "traefik.http.routers.fedeo-frontend.entrypoints=web" + # Web Secure Entrypoint + - "traefik.http.routers.fedeo-frontend-secure.rule=Host(`${DOMAIN}`) && PathPrefix(`/`)" + - "traefik.http.routers.fedeo-frontend-secure.entrypoints=web-secured" # + - "traefik.http.routers.fedeo-frontend-secure.tls.certresolver=mytlschallenge" + backend: + image: git.federspiel.tech/flfeders/fedeo/backend:main + restart: always + environment: + - INFISICAL_CLIENT_ID= + - INFISICAL_CLIENT_SECRET= + - NODE_ENV=production + networks: + - traefik + labels: + - "traefik.enable=true" + - "traefik.docker.network=traefik" + - "traefik.port=3100" + # Middlewares + - "traefik.http.middlewares.fedeo-backend-redirect-web-secure.redirectscheme.scheme=https" + - "traefik.http.middlewares.fedeo-backend-strip.stripprefix.prefixes=/backend" + # Web Entrypoint + - "traefik.http.routers.fedeo-backend.middlewares=fedeo-backend-redirect-web-secure" + - "traefik.http.routers.fedeo-backend.rule=Host(`${DOMAIN}`) && PathPrefix(`/backend`)" + - "traefik.http.routers.fedeo-backend.entrypoints=web" + # Web Secure Entrypoint + - "traefik.http.routers.fedeo-backend-secure.rule=Host(`${DOMAIN}`) && PathPrefix(`/backend`)" + - "traefik.http.routers.fedeo-backend-secure.entrypoints=web-secured" # + - "traefik.http.routers.fedeo-backend-secure.tls.certresolver=mytlschallenge" + - "traefik.http.routers.fedeo-backend-secure.middlewares=fedeo-backend-strip" + # db: + # image: postgres + # restart: always + # shm_size: 128mb + # environment: + # POSTGRES_PASSWORD: + # POSTGRES_USER: + # POSTGRES_DB: + # volumes: + # - ./pg-data:/var/lib/postgresql/data + # ports: + # - "5432:5432" + traefik: + image: traefik:v2.11 + restart: unless-stopped + container_name: traefik + command: + - "--api.insecure=false" + - "--api.dashboard=false" + - "--api.debug=false" + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=false" + - "--providers.docker.network=traefik" + - "--entrypoints.web.address=:80" + - "--entrypoints.web-secured.address=:443" + - "--accesslog=true" + - "--accesslog.filepath=/logs/access.log" + - "--accesslog.bufferingsize=5000" + - "--accesslog.fields.defaultMode=keep" + - "--accesslog.fields.headers.defaultMode=keep" + - "--certificatesresolvers.mytlschallenge.acme.tlschallenge=true" # + - "--certificatesresolvers.mytlschallenge.acme.email=${CONTACT_EMAIL}" + - "--certificatesresolvers.mytlschallenge.acme.storage=/letsencrypt/acme.json" + ports: + - 80:80 + - 443:443 + volumes: + - "./traefik/letsencrypt:/letsencrypt" # <== Volume for certs (TLS) + - "/var/run/docker.sock:/var/run/docker.sock:ro" + - "./traefik/logs:/logs" + networks: + - traefik +networks: + traefik: + external: false +~~~ \ No newline at end of file diff --git a/backend/db/schema/projects.ts b/backend/db/schema/projects.ts index badf583..fdb0453 100644 --- a/backend/db/schema/projects.ts +++ b/backend/db/schema/projects.ts @@ -71,7 +71,7 @@ export const projects = pgTable("projects", { updatedAt: timestamp("updated_at", { withTimezone: true }), updatedBy: uuid("updated_by").references(() => authUsers.id), - active_phase: text("active_phase"), + active_phase: text("active_phase").default("Erstkontakt"), }) export type Project = typeof projects.$inferSelect diff --git a/backend/src/routes/exports.ts b/backend/src/routes/exports.ts index 5aff4ba..e00b4db 100644 --- a/backend/src/routes/exports.ts +++ b/backend/src/routes/exports.ts @@ -11,58 +11,64 @@ import {secrets} from "../utils/secrets"; import {createSEPAExport} from "../utils/export/sepa"; const createDatevExport = async (server:FastifyInstance,req:any,startDate,endDate,beraternr,mandantennr) => { - console.log(startDate,endDate,beraternr,mandantennr) + try { + 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") - console.log(buffer) + // 1) ZIP erzeugen + const buffer = await buildExportZip(server,req.user.tenant_id, startDate, endDate, beraternr, mandantennr) + console.log("ZIP created") + console.log(buffer) - // 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) + // 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: secrets.S3_BUCKET, - Key: fileKey, - Body: buffer, - ContentType: "application/zip", - }) - ) + // 3) In S3 hochladen + await s3.send( + new PutObjectCommand({ + Bucket: secrets.S3_BUCKET, + Key: fileKey, + Body: buffer, + ContentType: "application/zip", + }) + ) - // 4) Presigned URL erzeugen (24h gĂŒltig) - const url = await getSignedUrl( - s3, - new GetObjectCommand({ - Bucket: secrets.S3_BUCKET, - Key: fileKey, - }), - { expiresIn: 60 * 60 * 24 } - ) + // 4) Presigned URL erzeugen (24h gĂŒltig) + const url = await getSignedUrl( + s3, + new GetObjectCommand({ + Bucket: secrets.S3_BUCKET, + Key: fileKey, + }), + { expiresIn: 60 * 60 * 24 } + ) - console.log(url) + 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) + } catch (error) { + console.log(error) + } - // 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) } diff --git a/backend/src/routes/resources/main.ts b/backend/src/routes/resources/main.ts index 1c65d3d..2fb0cfc 100644 --- a/backend/src/routes/resources/main.ts +++ b/backend/src/routes/resources/main.ts @@ -461,10 +461,9 @@ export default async function resourceRoutes(server: FastifyInstance) { } Object.keys(createData).forEach((key) => { - if(key.toLowerCase().includes("date")) createData[key] = normalizeDate(createData[key]) + if(key.toLowerCase().includes("date") && key !== "deliveryDateType") createData[key] = normalizeDate(createData[key]) }) - const [created] = await server.db .insert(table) .values(createData) @@ -513,8 +512,17 @@ export default async function resourceRoutes(server: FastifyInstance) { let data = {...body, updated_at: new Date().toISOString(), updated_by: userId} + //@ts-ignore + delete data.updatedBy + //@ts-ignore + delete data.updatedAt + + console.log(data) + Object.keys(data).forEach((key) => { - if(key.includes("_at") || key.includes("At")) { + console.log(key) + + if(key.includes("_at") || key.includes("At") || key.toLowerCase().includes("date")) { data[key] = normalizeDate(data[key]) } }) diff --git a/backend/src/routes/staff/time.ts b/backend/src/routes/staff/time.ts index e2a3d93..75afdb4 100644 --- a/backend/src/routes/staff/time.ts +++ b/backend/src/routes/staff/time.ts @@ -22,7 +22,7 @@ export default async function staffTimeRoutes(server: FastifyInstance) { server.post("/staff/time/event", async (req, reply) => { try { - const userId = req.user.user_id + const actorId = req.user.user_id; const tenantId = req.user.tenant_id const body = req.body as any @@ -35,17 +35,15 @@ export default async function staffTimeRoutes(server: FastifyInstance) { const dataToInsert = { tenant_id: tenantId, - user_id: userId, + user_id: body.user_id, actortype: "user", - actoruser_id: userId, + actoruser_id: actorId, eventtime: normalizeDate(body.eventtime), eventtype: body.eventtype, source: "WEB", payload: body.payload // Payload (z.B. Description) mit speichern } - console.log(dataToInsert) - const [created] = await server.db .insert(stafftimeevents) //@ts-ignore @@ -390,7 +388,9 @@ export default async function staffTimeRoutes(server: FastifyInstance) { const evaluatedUserId = targetUserId || actingUserId; const startDate = new Date(from); - const endDate = new Date(to); + let endDateQuery = new Date(to); + endDateQuery.setDate(endDateQuery.getDate() + 1); + const endDate = endDateQuery; if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { return reply.code(400).send({ error: "UngĂŒltiges Datumsformat." }); diff --git a/backend/src/utils/export/datev.ts b/backend/src/utils/export/datev.ts index 8c9efa6..0b36511 100644 --- a/backend/src/utils/export/datev.ts +++ b/backend/src/utils/export/datev.ts @@ -1,327 +1,389 @@ 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"; -import {secrets} from "../secrets"; -dayjs.extend(isBetween) +import isBetween from "dayjs/plugin/isBetween.js"; +import { BlobWriter, Data64URIReader, TextReader, ZipWriter } from "@zip.js/zip.js"; +import { FastifyInstance } from "fastify"; +import { GetObjectCommand } from "@aws-sdk/client-s3"; +import { s3 } from "../s3"; +import { secrets } from "../secrets"; -const getCreatedDocumentTotal = (item) => { - let totalNet = 0 - let total19 = 0 - let total7 = 0 +// Drizzle Core Imports +import { eq, and, inArray, gte, lte, asc, aliasedTable } from "drizzle-orm"; - 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) +// Tabellen Imports (keine Relations nötig!) +import { + statementallocations, + createddocuments, + incominginvoices, + accounts, + files, + customers, + vendors, + bankaccounts, + bankstatements, + ownaccounts +} from "../../../db/schema"; - if(row.taxPercent === 19) { - // @ts-ignore - total19 = total19 + Number(rowPrice * 0.19) - } else if(row.taxPercent === 7) { - // @ts-ignore - total7 = total7 + Number(rowPrice * 0.07) - } +dayjs.extend(isBetween); + +// --------------------------------------------------------- +// HELPER FUNCTIONS (UnverĂ€ndert) +// --------------------------------------------------------- + +const getCreatedDocumentTotal = (item: any) => { + let totalNet = 0; + let total19:number = 0; + let total7:number = 0; + const rows = Array.isArray(item.rows) ? item.rows : []; + rows.forEach((row: any) => { + 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 += Number(rowPrice) * Number(0.19); + else if (row.taxPercent === 7) total7 += Number(rowPrice) * Number(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, - } -} + totalNet, total19, total7, + totalGross: Number(totalNet.toFixed(2)) + Number(total19.toFixed(2)) + Number(total7.toFixed(2)) + }; +}; -const escapeString = (str) => { +const escapeString = (str: string | null | undefined) => { + return (str || "").replaceAll("\n", "").replaceAll(";", "").replaceAll(/\r/g, "").replaceAll(/"/g, "").replaceAll(/ĂŒ/g, "ue").replaceAll(/Ă€/g, "ae").replaceAll(/ö/g, "oe"); +}; - str = (str ||"") - .replaceAll("\n","") - .replaceAll(";","") - .replaceAll(/\r/g,"") - .replaceAll(/"/g,"") - .replaceAll(/ĂŒ/g,"ue") - .replaceAll(/Ă€/g,"ae") - .replaceAll(/ö/g,"oe") - return str -} +const displayCurrency = (input: number, onlyAbs = false) => { + return (onlyAbs ? Math.abs(input) : input).toFixed(2).replace(".", ","); +}; -const displayCurrency = (input, onlyAbs = false) => { +// --------------------------------------------------------- +// MAIN EXPORT FUNCTION +// --------------------------------------------------------- - 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 { +export async function buildExportZip( + server: FastifyInstance, + tenantId: number, + startDate: string, + endDate: string, + beraternr: string, + mandantennr: string +): Promise { try { - const zipFileWriter = new BlobWriter() - const zipWriter = new ZipWriter(zipFileWriter) + const zipFileWriter = new BlobWriter(); + const zipWriter = new ZipWriter(zipFileWriter); + // Header Infos + const dateNowStr = dayjs().format("YYYYMMDDHHmmssSSS"); + const startDateFmt = dayjs(startDate).format("YYYYMMDD"); + const endDateFmt = dayjs(endDate).format("YYYYMMDD"); + let header = `"EXTF";700;21;"Buchungsstapel";13;${dateNowStr};;"FE";"Florian Federspiel";;${beraternr};${mandantennr};20250101;4;${startDateFmt};${endDateFmt};"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`; - //Basic Information + // --------------------------------------------------------- + // 1. DATEN LADEN (CORE API SELECT & JOIN) + // --------------------------------------------------------- - 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";;;"";""` + // --- A) Created Documents --- + // Wir brauchen das Dokument und den Kunden dazu + const cdRaw = await server.db.select({ + doc: createddocuments, + customer: customers + }) + .from(createddocuments) + .leftJoin(customers, eq(createddocuments.customer, customers.id)) + .where(and( + eq(createddocuments.tenant, tenantId), + inArray(createddocuments.type, ["invoices", "advanceInvoices", "cancellationInvoices"]), + eq(createddocuments.state, "Gebucht"), + eq(createddocuments.archived, false), + gte(createddocuments.documentDate, startDate), + lte(createddocuments.documentDate, endDate) + )); - 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` + // Mapping: Flat Result -> Nested Object (damit der Rest des Codes gleich bleiben kann) + const createddocumentsList = cdRaw.map(r => ({ + ...r.doc, + customer: r.customer + })); - //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").eq("archived",false) - let {data:incominginvoicesRaw,error: incominginvoicesError} = await server.supabase.from("incominginvoices").select('*, vendor(*)').eq("tenant", tenant).eq("state","Gebucht").eq("archived",false) - const {data:accounts} = await server.supabase.from("accounts").select() - const {data:tenantData} = await server.supabase.from("tenants").select().eq("id",tenant).single() + // --- B) Incoming Invoices --- + // Wir brauchen die Rechnung und den Lieferanten + const iiRaw = await server.db.select({ + inv: incominginvoices, + vendor: vendors + }) + .from(incominginvoices) + .leftJoin(vendors, eq(incominginvoices.vendor, vendors.id)) + .where(and( + eq(incominginvoices.tenant, tenantId), + eq(incominginvoices.state, "Gebucht"), + eq(incominginvoices.archived, false), + gte(incominginvoices.date, startDate), + lte(incominginvoices.date, endDate) + )); - 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 incominginvoicesList = iiRaw.map(r => ({ + ...r.inv, + vendor: r.vendor + })); + // --- C) Statement Allocations --- + // Das ist der komplexeste Teil. Wir mĂŒssen Tabellen aliasen, da wir z.B. Customers doppelt joinen könnten + // (Einmal via CreatedDocument, einmal direkt an der Allocation). - 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 CdCustomer = aliasedTable(customers, "cd_customer"); + const IiVendor = aliasedTable(vendors, "ii_vendor"); - const downloadFile = async (bucketName, filePath, downloadFilePath,fileId) => { + const allocRaw = await server.db.select({ + allocation: statementallocations, + bs: bankstatements, + ba: bankaccounts, + cd: createddocuments, + cd_cust: CdCustomer, + ii: incominginvoices, + ii_vend: IiVendor, + acc: accounts, + direct_vend: vendors, // Direkte Zuordnung an Kreditor + direct_cust: customers, // Direkte Zuordnung an Debitor + own: ownaccounts + }) + .from(statementallocations) + // JOIN 1: Bankstatement (Pflicht, fĂŒr Datum Filter) + .innerJoin(bankstatements, eq(statementallocations.bankstatement, bankstatements.id)) + // JOIN 2: Bankaccount (fĂŒr DATEV Nummer) + .leftJoin(bankaccounts, eq(bankstatements.account, bankaccounts.id)) - console.log(filePath) + // JOIN 3: Ausgangsrechnung & deren Kunde + .leftJoin(createddocuments, eq(statementallocations.createddocument, createddocuments.id)) + .leftJoin(CdCustomer, eq(createddocuments.customer, CdCustomer.id)) - const command = new GetObjectCommand({ - Bucket: secrets.S3_BUCKET, - Key: filePath, - }) + // JOIN 4: Eingangsrechnung & deren Lieferant + .leftJoin(incominginvoices, eq(statementallocations.incominginvoice, incominginvoices.id)) + .leftJoin(IiVendor, eq(incominginvoices.vendor, IiVendor.id)) - const { Body, ContentType } = await s3.send(command) + // JOIN 5: Direkte Zuordnungen + .leftJoin(accounts, eq(statementallocations.account, accounts.id)) + .leftJoin(vendors, eq(statementallocations.vendor, vendors.id)) + .leftJoin(customers, eq(statementallocations.customer, customers.id)) + .leftJoin(ownaccounts, eq(statementallocations.ownaccount, ownaccounts.id)) - const chunks: any[] = [] - // @ts-ignore - for await (const chunk of Body) { - chunks.push(chunk) + .where(and( + eq(statementallocations.tenant, tenantId), + eq(statementallocations.archived, false), + // Datum Filter direkt auf dem Bankstatement + gte(bankstatements.date, startDate), + lte(bankstatements.date, endDate) + )); + + // Mapping: Wir bauen das komplexe Objekt nach, das die CSV Logik erwartet + const statementallocationsList = allocRaw.map(r => ({ + ...r.allocation, + bankstatement: { + ...r.bs, + account: r.ba // Nesting fĂŒr bs.account.datevNumber + }, + createddocument: r.cd ? { + ...r.cd, + customer: r.cd_cust + } : null, + incominginvoice: r.ii ? { + ...r.ii, + vendor: r.ii_vend + } : null, + account: r.acc, + vendor: r.direct_vend, + customer: r.direct_cust, + ownaccount: r.own + })); + + // --- D) Stammdaten Accounts --- + const accountsList = await server.db.select().from(accounts); + + // --------------------------------------------------------- + // 2. FILES LADEN + // --------------------------------------------------------- + + // IDs sammeln fĂŒr IN (...) Abfragen + const cdIds = createddocumentsList.map(i => i.id); + const iiIds = incominginvoicesList.map(i => i.id); + + let filesCreateddocuments: any[] = []; + if (cdIds.length > 0) { + filesCreateddocuments = await server.db.select().from(files).where(and( + eq(files.tenant, tenantId), + inArray(files.createddocument, cdIds), + eq(files.archived, false) + )); + } + + let filesIncomingInvoices: any[] = []; + if (iiIds.length > 0) { + filesIncomingInvoices = await server.db.select().from(files).where(and( + eq(files.tenant, tenantId), + inArray(files.incominginvoice, iiIds), + eq(files.archived, false) + )); + } + + // --------------------------------------------------------- + // 3. DOWNLOAD & ZIP + // --------------------------------------------------------- + + const downloadFile = async (filePath: string, fileId: string) => { + try { + const command = new GetObjectCommand({ + Bucket: secrets.S3_BUCKET, + Key: filePath, + }); + const { Body } = await s3.send(command); + if (!Body) return; + 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); + const ext = filePath.includes('.') ? filePath.split(".").pop() : "pdf"; + await zipWriter.add(`${fileId}.${ext}`, dataURLReader); + } catch (e) { + console.error(`Error downloading file ${fileId}`, e); } - 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); - } + for (const file of filesCreateddocuments) if(file.path) await downloadFile(file.path, file.id); + for (const file of filesIncomingInvoices) if(file.path) await downloadFile(file.path, file.id); - let bookingLines = [] + // --------------------------------------------------------- + // 4. CSV GENERIERUNG (Logic ist gleich geblieben) + // --------------------------------------------------------- - createddocuments.forEach(createddocument => { + let bookingLines: string[] = []; - let file = filesCreateddocuments.find(i => i.createddocument === createddocument.id); + // AR + createddocumentsList.forEach(cd => { + let file = filesCreateddocuments.find(i => i.createddocument === cd.id); + let total = 0; + let typeString = ""; - 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" + if(cd.type === "invoices") { + total = getCreatedDocumentTotal(cd).totalGross; + typeString = "AR"; + } else if(cd.type === "advanceInvoices") { + total = getCreatedDocumentTotal(cd).totalGross; + typeString = "ARAbschlag"; + } else if(cd.type === "cancellationInvoices") { + total = getCreatedDocumentTotal(cd).totalGross; + typeString = "ARStorno"; } - let shSelector = "S" - if(Math.sign(total) === 1) { - shSelector = "S" - } else if (Math.sign(total) === -1) { - shSelector = "H" + let shSelector = Math.sign(total) === -1 ? "H" : "S"; + const cust = cd.customer; // durch Mapping verfĂŒgbar + + bookingLines.push(`${displayCurrency(total,true)};"${shSelector}";;;;;${cust?.customerNumber || ""};8400;"";${dayjs(cd.documentDate).format("DDMM")};"${cd.documentNumber}";;;"${`${typeString} ${cd.documentNumber} - ${cust?.name || ""}`.substring(0,59)}";;;;;;${file ? `"BEDI ""${file.id}"""` : ""};"GeschĂ€ftspartner";"${cust?.name || ""}";"Kundennummer";"${cust?.customerNumber || ""}";"Belegnummer";"${cd.documentNumber}";"Leistungsdatum";"${dayjs(cd.deliveryDate).format("DD.MM.YYYY")}";"Belegdatum";"${dayjs(cd.documentDate).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`); + }); + + // ER + incominginvoicesList.forEach(ii => { + const accs = ii.accounts as any[] || []; + accs.forEach(account => { + let file = filesIncomingInvoices.find(i => i.incominginvoice === ii.id); + let accountData = accountsList.find(i => i.id === account.account); + if (!accountData) return; + + 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 amountGross = account.amountGross ? account.amountGross : (account.amountNet || 0) + (account.amountTax || 0); + let shSelector = Math.sign(amountGross) === -1 ? "H" : "S"; + let text = `ER ${ii.reference}: ${escapeString(ii.description)}`.substring(0,59); + const vend = ii.vendor; // durch Mapping verfĂŒgbar + + bookingLines.push(`${Math.abs(amountGross).toFixed(2).replace(".",",")};"${shSelector}";;;;;${accountData.number};${vend?.vendorNumber || ""};"${buschluessel}";${dayjs(ii.date).format("DDMM")};"${ii.reference}";;;"${text}";;;;;;${file ? `"BEDI ""${file.id}"""` : ""};"GeschĂ€ftspartner";"${vend?.name || ""}";"Kundennummer";"${vend?.vendorNumber || ""}";"Belegnummer";"${ii.reference}";"Leistungsdatum";"${dayjs(ii.date).format("DD.MM.YYYY")}";"Belegdatum";"${dayjs(ii.date).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`); + }); + }); + + // Bank + statementallocationsList.forEach(alloc => { + const bs = alloc.bankstatement; // durch Mapping verfĂŒgbar + if(!bs) return; + + let shSelector = Math.sign(alloc.amount) === -1 ? "H" : "S"; + // @ts-ignore + let datevKonto = bs.account?.datevNumber || ""; + let dateVal = dayjs(bs.date).format("DDMM"); + let dateFull = dayjs(bs.date).format("DD.MM.YYYY"); + let bsText = escapeString(bs.text); + + if(alloc.createddocument && alloc.createddocument.customer) { + const cd = alloc.createddocument; + const cust = cd.customer; + bookingLines.push(`${displayCurrency(alloc.amount,true)};"H";;;;;${cust?.customerNumber};${datevKonto};"3";${dayjs(cd.documentDate).format("DDMM")};"${cd.documentNumber}";;;"${`ZE${alloc.description}${bsText}`.substring(0,59)}";;;;;;;"GeschĂ€ftspartner";"${cust?.name}";"Kundennummer";"${cust?.customerNumber}";"Belegnummer";"${cd.documentNumber}";"Leistungsdatum";"${dayjs(cd.deliveryDate).format("DD.MM.YYYY")}";"Belegdatum";"${dayjs(cd.documentDate).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`); + } else if(alloc.incominginvoice && alloc.incominginvoice.vendor) { + const ii = alloc.incominginvoice; + const vend = ii.vendor; + bookingLines.push(`${displayCurrency(alloc.amount,true)};"${shSelector}";;;;;${datevKonto};${vend?.vendorNumber};"";${dayjs(ii.date).format("DDMM")};"${ii.reference}";;;"${`ZA${alloc.description} ${bsText} `.substring(0,59)}";;;;;;;"GeschĂ€ftspartner";"${vend?.name}";"Kundennummer";"${vend?.vendorNumber}";"Belegnummer";"${ii.reference}";"Leistungsdatum";"${dayjs(ii.date).format("DD.MM.YYYY")}";"Belegdatum";"${dayjs(ii.date).format("DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`); + } else if(alloc.account) { + const acc = alloc.account; + let vorzeichen = Math.sign(alloc.amount) > 0 ? "ZE" : "ZA"; + bookingLines.push(`${displayCurrency(alloc.amount,true)};"${shSelector}";;;;;${datevKonto};${acc.number};"";${dateVal};"";;;"${`${vorzeichen} ${acc.number} - ${escapeString(acc.label)}${escapeString(alloc.description)}${bsText}`.substring(0,59)}";;;;;;;"GeschĂ€ftspartner";"${bs.credName || ''}";"Kundennummer";"";"Belegnummer";"";"Leistungsdatum";"";"Belegdatum";"${dateFull}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`); + } else if(alloc.vendor) { + const vend = alloc.vendor; + let vorzeichen = Math.sign(alloc.amount) > 0 ? "ZE" : "ZA"; + bookingLines.push(`${displayCurrency(alloc.amount,true)};"${shSelector}";;;;;${datevKonto};${vend.vendorNumber};"";${dateVal};"";;;"${`${vorzeichen} ${vend.vendorNumber} - ${escapeString(vend.name)}${escapeString(alloc.description)}${bsText}`.substring(0,59)}";;;;;;;"GeschĂ€ftspartner";"${vend.name}";"Kundennummer";"";"Belegnummer";"";"Leistungsdatum";"";"Belegdatum";"${dateFull}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`); + } else if(alloc.customer) { + const cust = alloc.customer; + let vorzeichen = Math.sign(alloc.amount) > 0 ? "ZE" : "ZA"; + bookingLines.push(`${displayCurrency(alloc.amount,true)};"${shSelector}";;;;;${datevKonto};${cust.customerNumber};"";${dateVal};"";;;"${`${vorzeichen} ${cust.customerNumber} - ${escapeString(cust.name)}${escapeString(alloc.description)}${bsText}`.substring(0,59)}";;;;;;;"GeschĂ€ftspartner";"${cust.name}";"Kundennummer";"";"Belegnummer";"";"Leistungsdatum";"";"Belegdatum";"${dateFull}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`); + } else if(alloc.ownaccount) { + const own = alloc.ownaccount; + let vorzeichen = Math.sign(alloc.amount) > 0 ? "ZE" : "ZA"; + bookingLines.push(`${displayCurrency(alloc.amount,true)};"${shSelector}";;;;;${datevKonto};${own.number};"";${dateVal};"";;;"${`${vorzeichen} ${own.number} - ${escapeString(own.name)}${escapeString(alloc.description)}${bsText}`.substring(0,59)}";;;;;;;"GeschĂ€ftspartner";"${own.name}";"Kundennummer";"";"Belegnummer";"";"Leistungsdatum";"";"Belegdatum";"${dateFull}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`); } + }); - 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;;;;"";;;;;;;`) + // --------------------------------------------------------- + // 5. STAMMDATEN CSV + // --------------------------------------------------------- + const csvString = `${header}\n${colHeaders}\n` + bookingLines.join("\n") + "\n"; + await zipWriter.add( + `EXTF_Buchungsstapel_von_${startDateFmt}_bis_${endDateFmt}.csv`, + new TextReader(csvString) + ); - }) + const headerStammdaten = `"EXTF";700;16;"Debitoren/Kreditoren";5;${dateNowStr};;"FE";"Florian Federspiel";;${beraternr};${mandantennr};20250101;4;${startDateFmt};${endDateFmt};"Debitoren & Kreditoren";"FF";1;0;1;"EUR";;"";;;"03";;;"";""`; + const 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`; - incominginvoices.forEach(incominginvoice => { - console.log(incominginvoice.id); - incominginvoice.accounts.forEach(account => { + const customersList = await server.db.select().from(customers).where(and(eq(customers.tenant, tenantId), eq(customers.active, true))).orderBy(asc(customers.customerNumber)); + const vendorsList = await server.db.select().from(vendors).where(and(eq(vendors.tenant, tenantId), eq(vendors.archived, false))).orderBy(asc(vendors.vendorNumber)); - let file = filesIncomingInvoices.find(i => i.incominginvoice === incominginvoice.id); + let bookinglinesStammdaten: string[] = []; + customersList.forEach(c => { + const info = c.infoData as any || {}; + bookinglinesStammdaten.push(`${c.customerNumber};"${c.isCompany ? (c.name || "").substring(0,48): ''}";;"${!c.isCompany ? (c.lastname ? c.lastname : c.name) : ''}";"${!c.isCompany ? (c.firstname ? c.firstname : '') : ''}";;${c.isCompany ? 2 : 1};;;;;;;;"STR";"${info.street || ''}";;"${info.zip || ''}";"${info.city || ''}";;;"${info.special || ''}";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;`); + }); - let accountData = accounts.find(i => i.id === account.account) - let buschluessel: string = "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!'); - } - });*/ + vendorsList.forEach(v => { + const info = v.infoData as any || {}; + bookinglinesStammdaten.push(`${v.vendorNumber};"${(v.name || "").substring(0,48)}";;;;;2;;;;;;;;"STR";"${info.street || ''}";;"${info.zip || ''}";"${info.city || ''}";;;"${info.special || ''}";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;`); + }); + await zipWriter.add( + `EXTF_Stammdaten_von_${startDateFmt}_bis_${endDateFmt}.csv`, + new TextReader(`${headerStammdaten}\n${colHeadersStammdaten}\n` + bookinglinesStammdaten.join("\n") + "\n") + ); + // --------------------------------------------------------- + // 6. XML METADATA + // --------------------------------------------------------- let obj = { archive: { '@version':"5.0", @@ -333,56 +395,34 @@ export async function buildExportZip(server: FastifyInstance, tenant: number, st date: dayjs().format("YYYY-MM-DDTHH:mm:ss") }, content: { - document: [] + document: [] as any[] } } - } + }; - filesCreateddocuments.forEach(file => { + const addXmlDoc = (file: any) => { + if(!file.path) return; + const ext = file.path.includes('.') ? file.path.split(".").pop() : "pdf"; obj.archive.content.document.push({ "@guid": file.id, extension: { "@xsi:type":"File", - "@name":`${file.id}.pdf` + "@name":`${file.id}.${ext}` } - }) - }) + }); + }; - filesIncomingInvoices.forEach(file => { - obj.archive.content.document.push({ - "@guid": file.id, - extension: { - "@xsi:type":"File", - "@name":`${file.id}.pdf` - } - }) - }) + filesCreateddocuments.forEach(addXmlDoc); + filesIncomingInvoices.forEach(addXmlDoc); - let doc = xmlbuilder.create(obj, {encoding: 'UTF-8', standalone: true}) + const doc = xmlbuilder.create(obj, {encoding: 'UTF-8', standalone: true}); + await zipWriter.add(`document.xml`, new TextReader(doc.end({pretty: true}))); - //console.log(doc.end({pretty: true})); + const arrayBuffer = await (await zipWriter.close()).arrayBuffer(); + return Buffer.from(arrayBuffer); - 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) + console.error("DATEV Export Error:", error); + throw error; } - - } \ No newline at end of file diff --git a/backend/src/utils/resource.config.ts b/backend/src/utils/resource.config.ts index 99c9713..0384d7c 100644 --- a/backend/src/utils/resource.config.ts +++ b/backend/src/utils/resource.config.ts @@ -57,6 +57,7 @@ export const resourceConfig = { table: contracts, searchColumns: ["name", "notes", "contractNumber", "paymentType", "sepaRef", "bankingName"], numberRangeHolder: "contractNumber", + mtoLoad: ["customer"], }, plants: { table: plants, diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 322cca4..7672b3c 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,15 +1,31 @@ -FROM node:20-alpine +# --- Stage 1: Build --- +FROM node:20-alpine AS builder -RUN mkdir -p /usr/src/nuxt-app WORKDIR /usr/src/nuxt-app -COPY . . -RUN npm i +# Nur Files kopieren, die fĂŒr die Installation nötig sind (besseres Caching) +COPY package*.json ./ +RUN npm install + +# Restlichen Code kopieren und bauen +COPY . . RUN npm run build +# --- Stage 2: Runtime --- +FROM node:20-alpine AS runner + +WORKDIR /usr/src/nuxt-app + +# Von der Build-Stage NUR den fertigen .output Ordner kopieren +COPY --from=builder /usr/src/nuxt-app/.output ./.output + +# Optional: Falls du statische Dateien aus public brauchst, +# sind diese normalerweise bereits in .output/public enthalten. + ENV NUXT_HOST=0.0.0.0 ENV NUXT_PORT=3000 +ENV NODE_ENV=production EXPOSE 3000 -ENTRYPOINT ["node", ".output/server/index.mjs"] +ENTRYPOINT ["node", ".output/server/index.mjs"] \ No newline at end of file diff --git a/frontend/components/DocumentDisplayModal.vue b/frontend/components/DocumentDisplayModal.vue index fd82c60..12cc971 100644 --- a/frontend/components/DocumentDisplayModal.vue +++ b/frontend/components/DocumentDisplayModal.vue @@ -55,7 +55,7 @@ const setup = async () => { }) filetypes.value = await useEntities("filetags").select() - documentboxes.value = await useEntities("documentboxes").select() + //documentboxes.value = await useEntities("documentboxes").select() } setup() diff --git a/frontend/components/LabelPrinterButton.vue b/frontend/components/LabelPrinterButton.vue index 2a29fee..9b01207 100644 --- a/frontend/components/LabelPrinterButton.vue +++ b/frontend/components/LabelPrinterButton.vue @@ -37,7 +37,7 @@ const handleClick = async () => { { icon: "i-heroicons-user-group", children: [ ... true ? [{ - label: "Anwesenheiten", + label: "Zeiten", to: "/staff/time", icon: "i-heroicons-clock", }] : [], diff --git a/frontend/components/PageLeaveGuard.vue b/frontend/components/PageLeaveGuard.vue index af9a563..928b946 100644 --- a/frontend/components/PageLeaveGuard.vue +++ b/frontend/components/PageLeaveGuard.vue @@ -2,22 +2,19 @@ import { ref, onMounted, onBeforeUnmount } from 'vue' import { onBeforeRouteLeave } from 'vue-router' -// Wir erwarten eine Prop, die sagt, ob geschĂŒtzt werden soll const props = defineProps<{ - when: boolean // z.B. true, wenn Formular dirty ist + when: boolean }>() const showModal = ref(false) const pendingNext = ref void)>(null) -// --- 1. Interne Navigation (Nuxt) --- +// --- 1. Interne Navigation --- onBeforeRouteLeave((to, from, next) => { if (!props.when) { next() return } - - // Navigation pausieren & Modal zeigen pendingNext.value = next showModal.value = true }) @@ -29,10 +26,10 @@ const confirmLeave = () => { const cancelLeave = () => { showModal.value = false - // Navigation wird implizit abgebrochen + // Navigation abbrechen (pendingNext verfĂ€llt) } -// --- 2. Externe Navigation (Browser Tab schließen) --- +// --- 2. Browser Tab schließen --- const handleBeforeUnload = (e: BeforeUnloadEvent) => { if (props.when) { e.preventDefault() @@ -50,12 +47,12 @@ onBeforeUnmount(() => window.removeEventListener('beforeunload', handleBeforeUnl
- Seite wirklich verlassen? + Seite verlassen?
- Du hast ungespeicherte Änderungen. Diese gehen verloren, wenn du die Seite verlĂ€sst. + Du hast ungespeicherte Änderungen. Wenn du die Seite verlĂ€sst, gehen diese verloren.
@@ -64,7 +61,7 @@ onBeforeUnmount(() => window.removeEventListener('beforeunload', handleBeforeUnl Nein, bleiben
@@ -74,25 +71,65 @@ onBeforeUnmount(() => window.removeEventListener('beforeunload', handleBeforeUnl \ No newline at end of file diff --git a/frontend/components/PublicDynamicForm.vue b/frontend/components/PublicDynamicForm.vue index a3cdd07..9a77972 100644 --- a/frontend/components/PublicDynamicForm.vue +++ b/frontend/components/PublicDynamicForm.vue @@ -159,7 +159,16 @@ const submit = async () => { @input="e => form.startDate = new Date(e.target.value)" /> - + + + + ) { start: startIso, // Die eingegebene Startzeit end: endIso, // Die eingegebene Endzeit (oder null) type: state.type, - description: state.description + description: state.description, + user_id: props.defaultUserId }) toast.add({ title: 'Zeit manuell erfasst', color: 'green' }) diff --git a/frontend/components/displayOpenBalances.vue b/frontend/components/displayOpenBalances.vue index c19dfdf..829c607 100644 --- a/frontend/components/displayOpenBalances.vue +++ b/frontend/components/displayOpenBalances.vue @@ -42,9 +42,7 @@ const setupPage = async () => { }) draftInvoicesCount.value = draftDocuments.length - countPreparedOpenIncomingInvoices.value = (await useEntities("incominginvoices").select("id, state")).filter(i => i.state === "Vorbereitet").length - - + countPreparedOpenIncomingInvoices.value = (await useEntities("incominginvoices").select("id, state")).filter(i => i.state === "Vorbereitet" && !i.archived).length } setupPage() @@ -78,10 +76,10 @@ setupPage() 0 Stk / 0,00€ - ToDo Eingangsrechnungsrechnungen: + Vorbereitete Eingangsrechnungen: {{countPreparedOpenIncomingInvoices}} Stk 0 Stk diff --git a/frontend/composables/useStaffTime.ts b/frontend/composables/useStaffTime.ts index 791c0a0..a986a00 100644 --- a/frontend/composables/useStaffTime.ts +++ b/frontend/composables/useStaffTime.ts @@ -91,15 +91,18 @@ export const useStaffTime = () => { } // 🆕 NEU: Manuellen Eintrag erstellen (Vergangenheit oder Zeitraum) - const createEntry = async (data: { start: string, end: string | null, type: string, description: string }) => { + const createEntry = async (data: { start: string, end: string | null, type: string, description: string, user_id: string }) => { // 1. Start Event senden // Wir nutzen den dynamischen Typ (work_start, vacation_start etc.) + console.log(data) + await $api('/api/staff/time/event', { method: 'POST', body: { eventtype: `${data.type}_start`, eventtime: data.start, - payload: { description: data.description } + payload: { description: data.description }, + user_id: data.user_id, } }) @@ -109,7 +112,9 @@ export const useStaffTime = () => { method: 'POST', body: { eventtype: `${data.type}_end`, - eventtime: data.end + eventtime: data.end, + payload: { description: data.description }, + user_id: data.user_id, } }) } diff --git a/frontend/layouts/default.vue b/frontend/layouts/default.vue index 918536c..6139269 100644 --- a/frontend/layouts/default.vue +++ b/frontend/layouts/default.vue @@ -236,11 +236,17 @@ const footerLinks = [ - - - +
+
Schnellauswahl
- - - +
+
+ Monat: + Letzter + Aktuell +
- - - +
+ Quartal: - - - + + Letztes + + + + Q{{ q }} + +
+
+
+
+ + + + + + + + + + + + + +
@@ -162,5 +213,4 @@ const createExport = async () => { + \ No newline at end of file diff --git a/frontend/pages/incomingInvoices/[mode]/[id].vue b/frontend/pages/incomingInvoices/[mode]/[id].vue index 3de06e0..f088711 100644 --- a/frontend/pages/incomingInvoices/[mode]/[id].vue +++ b/frontend/pages/incomingInvoices/[mode]/[id].vue @@ -122,7 +122,7 @@ const totalCalculated = computed(() => { } }) - totalGross = Number(totalNet + totalAmount19Tax) + totalGross = Number(totalNet + totalAmount19Tax + totalAmount7Tax) return { totalNet, @@ -147,7 +147,7 @@ const updateIncomingInvoice = async (setBooked = false) => { } else { item.state = "Entwurf" } - const data = await useEntities('incominginvoices').update(itemInfo.value.id,item) + const data = await useEntities('incominginvoices').update(itemInfo.value.id,item,true) } const findIncomingInvoiceErrors = computed(() => { diff --git a/frontend/pages/settings/texttemplates.vue b/frontend/pages/settings/texttemplates.vue index 7850478..af9a7bd 100644 --- a/frontend/pages/settings/texttemplates.vue +++ b/frontend/pages/settings/texttemplates.vue @@ -1,220 +1,352 @@