diff --git a/backend/src/utils/export/datev.ts b/backend/src/utils/export/datev.ts index 57d4021..3a13cb4 100644 --- a/backend/src/utils/export/datev.ts +++ b/backend/src/utils/export/datev.ts @@ -1,6 +1,8 @@ import xmlbuilder from "xmlbuilder"; import dayjs from "dayjs"; import isBetween from "dayjs/plugin/isBetween.js"; +import utc from "dayjs/plugin/utc.js"; +import timezone from "dayjs/plugin/timezone.js"; import { BlobWriter, Data64URIReader, TextReader, ZipWriter } from "@zip.js/zip.js"; import { FastifyInstance } from "fastify"; import { GetObjectCommand } from "@aws-sdk/client-s3"; @@ -25,6 +27,8 @@ import { } from "../../../db/schema"; dayjs.extend(isBetween); +dayjs.extend(utc); +dayjs.extend(timezone); // --------------------------------------------------------- // HELPER FUNCTIONS (Unverändert) @@ -68,6 +72,14 @@ const displayCurrency = (input: number, onlyAbs = false) => { return (onlyAbs ? Math.abs(input) : input).toFixed(2).replace(".", ","); }; +const DATEV_TIMEZONE = "Europe/Berlin"; + +const formatDatevDate = (date: dayjs.ConfigType, format: string) => { + if (!date) return ""; + const parsed = dayjs(date); + return parsed.isValid() ? parsed.tz(DATEV_TIMEZONE).format(format) : ""; +}; + const getCreatedDocumentRevenueLines = (document: any) => { const totals = getCreatedDocumentTotal(document); @@ -105,8 +117,10 @@ export async function buildExportZip( // Header Infos const dateNowStr = dayjs().format("YYYYMMDDHHmmssSSS"); - const startDateFmt = dayjs(startDate).format("YYYYMMDD"); - const endDateFmt = dayjs(endDate).format("YYYYMMDD"); + const startDateValue = formatDatevDate(startDate, "YYYY-MM-DD"); + const endDateValue = formatDatevDate(endDate, "YYYY-MM-DD"); + const startDateFmt = formatDatevDate(startDate, "YYYYMMDD"); + const endDateFmt = formatDatevDate(endDate, "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`; @@ -128,8 +142,8 @@ export async function buildExportZip( inArray(createddocuments.type, ["invoices", "advanceInvoices", "cancellationInvoices"]), eq(createddocuments.state, "Gebucht"), eq(createddocuments.archived, false), - gte(createddocuments.documentDate, startDate), - lte(createddocuments.documentDate, endDate) + gte(createddocuments.documentDate, startDateValue), + lte(createddocuments.documentDate, endDateValue) )); // Mapping: Flat Result -> Nested Object (damit der Rest des Codes gleich bleiben kann) @@ -150,8 +164,8 @@ export async function buildExportZip( eq(incominginvoices.tenant, tenantId), eq(incominginvoices.state, "Gebucht"), eq(incominginvoices.archived, false), - gte(incominginvoices.date, startDate), - lte(incominginvoices.date, endDate) + gte(incominginvoices.date, startDateValue), + lte(incominginvoices.date, endDateValue) )); const incominginvoicesList = iiRaw.map(r => ({ @@ -216,13 +230,13 @@ export async function buildExportZip( eq(statementallocations.archived, false), or( and( - gte(bankstatements.date, startDate), - lte(bankstatements.date, endDate) + gte(bankstatements.date, startDateValue), + lte(bankstatements.date, endDateValue) ), and( isNull(statementallocations.bankstatement), - gte(statementallocations.manualBookingDate, startDate), - lte(statementallocations.manualBookingDate, endDate) + gte(statementallocations.manualBookingDate, startDateValue), + lte(statementallocations.manualBookingDate, endDateValue) ) ) )); @@ -333,7 +347,7 @@ export async function buildExportZip( revenueLines.forEach((revenueLine) => { let shSelector = Math.sign(revenueLine.amount) === -1 ? "H" : "S"; - bookingLines.push(`${displayCurrency(revenueLine.amount,true)};"${shSelector}";;;;;${cust?.customerNumber || ""};${revenueLine.account};"";${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;;;;"";;;;;;;`); + bookingLines.push(`${displayCurrency(revenueLine.amount,true)};"${shSelector}";;;;;${cust?.customerNumber || ""};${revenueLine.account};"";${formatDatevDate(cd.documentDate, "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";"${formatDatevDate(cd.deliveryDate, "DD.MM.YYYY")}";"Belegdatum";"${formatDatevDate(cd.documentDate, "DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`); }); }); @@ -358,7 +372,7 @@ export async function buildExportZip( 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;;;;"";;;;;;;`); + bookingLines.push(`${Math.abs(amountGross).toFixed(2).replace(".",",")};"${shSelector}";;;;;${accountData.number};${vend?.vendorNumber || ""};"${buschluessel}";${formatDatevDate(ii.date, "DDMM")};"${ii.reference}";;;"${text}";;;;;;${file ? `"BEDI ""${file.id}"""` : ""};"Geschäftspartner";"${vend?.name || ""}";"Kundennummer";"${vend?.vendorNumber || ""}";"Belegnummer";"${ii.reference}";"Leistungsdatum";"${formatDatevDate(ii.date, "DD.MM.YYYY")}";"Belegdatum";"${formatDatevDate(ii.date, "DD.MM.YYYY")}";;;;;;;;;;"";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;;`); }); }); @@ -392,8 +406,8 @@ export async function buildExportZip( if(!bs && alloc.manualBookingDate) { const debit = getManualBookingSide(alloc, "debit"); const credit = getManualBookingSide(alloc, "credit"); - const dateManual = dayjs(alloc.manualBookingDate).format("DDMM"); - const dateManualFull = dayjs(alloc.manualBookingDate).format("DD.MM.YYYY"); + const dateManual = formatDatevDate(alloc.manualBookingDate, "DDMM"); + const dateManualFull = formatDatevDate(alloc.manualBookingDate, "DD.MM.YYYY"); const belegnummer = debit.reference || credit.reference || ""; bookingLines.push(`${displayCurrency(alloc.amount,true)};"S";;;;;${debit.number};${credit.number};"${alloc.datevTaxKey || ""}";${dateManual};"${belegnummer}";;;"${`MB ${debit.number} an ${credit.number} ${escapeString(alloc.description)}`.substring(0,59)}";;;;;;;"Geschäftspartner";"${escapeString(debit.name || credit.name)}";"Kundennummer";"${debit.number}";"Belegnummer";"${belegnummer}";"Leistungsdatum";"${dateManualFull}";"Belegdatum";"${dateManualFull}";;;;;;;;;;"";;;;;;;;Manuelle-Buchung;${alloc.id};;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;`); return; @@ -404,18 +418,18 @@ export async function buildExportZip( 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 dateVal = formatDatevDate(bs.date, "DDMM"); + let dateFull = formatDatevDate(bs.date, "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};"";${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";"${dateFull}";;;;;;;;;;"";;;;;;;;Bank-Id;${alloc.bankstatement.id};;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;`); + bookingLines.push(`${displayCurrency(alloc.amount,true)};"H";;;;;${cust?.customerNumber};${datevKonto};"";${formatDatevDate(cd.documentDate, "DDMM")};"${cd.documentNumber}";;;"${`ZE${alloc.description}${bsText}`.substring(0,59)}";;;;;;;"Geschäftspartner";"${cust?.name}";"Kundennummer";"${cust?.customerNumber}";"Belegnummer";"${cd.documentNumber}";"Leistungsdatum";"${formatDatevDate(cd.deliveryDate, "DD.MM.YYYY")}";"Belegdatum";"${dateFull}";;;;;;;;;;"";;;;;;;;Bank-Id;${alloc.bankstatement.id};;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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";"${dateFull}";;;;;;;;;;"";;;;;;;;Bank-Id;${alloc.bankstatement.id};;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;`); + bookingLines.push(`${displayCurrency(alloc.amount,true)};"${shSelector}";;;;;${datevKonto};${vend?.vendorNumber};"";${formatDatevDate(ii.date, "DDMM")};"${ii.reference}";;;"${`ZA${alloc.description} ${bsText} `.substring(0,59)}";;;;;;;"Geschäftspartner";"${vend?.name}";"Kundennummer";"${vend?.vendorNumber}";"Belegnummer";"${ii.reference}";"Leistungsdatum";"${formatDatevDate(ii.date, "DD.MM.YYYY")}";"Belegdatum";"${dateFull}";;;;;;;;;;"";;;;;;;;Bank-Id;${alloc.bankstatement.id};;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0;;;;"";;;;;;`); } else if(alloc.account) { const acc = alloc.account; let vorzeichen = Math.sign(alloc.amount) > 0 ? "ZE" : "ZA";