DATEV-Export Datumsformat korrigieren #181

KI-AGENT: Normalisiert Export-Zeiträume und Belegdatumsfelder vor der DATEV-Ausgabe auf Europe/Berlin, damit per UI gewählte Daten nicht durch UTC-Verschiebung auf den Vortag fallen.
This commit is contained in:
2026-05-15 16:54:34 +02:00
parent 0e71899c57
commit cb939f2197

View File

@@ -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";