Files
FEDEO/backend/src/utils/export/datev.ts

497 lines
34 KiB
TypeScript

import xmlbuilder from "xmlbuilder";
import dayjs from "dayjs";
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";
// Drizzle Core Imports
import { eq, and, or, isNull, inArray, gte, lte, asc, aliasedTable } from "drizzle-orm";
// Tabellen Imports (keine Relations nötig!)
import {
statementallocations,
createddocuments,
incominginvoices,
accounts,
files,
customers,
vendors,
bankaccounts,
bankstatements,
ownaccounts
} from "../../../db/schema";
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);
}
});
return {
totalNet, total19, total7,
totalGross: Number(totalNet.toFixed(2)) + Number(total19.toFixed(2)) + Number(total7.toFixed(2))
};
};
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");
};
const displayCurrency = (input: number, onlyAbs = false) => {
return (onlyAbs ? Math.abs(input) : input).toFixed(2).replace(".", ",");
};
const getCreatedDocumentDatevTaxKey = (document: { taxType?: string | null }) => {
return document.taxType === "13b UStG" ? "46" : "";
};
const getCreatedDocumentPaymentDatevTaxKey = (document: { taxType?: string | null }) => {
return document.taxType === "13b UStG" ? "46" : "3";
};
// ---------------------------------------------------------
// MAIN EXPORT FUNCTION
// ---------------------------------------------------------
export async function buildExportZip(
server: FastifyInstance,
tenantId: number,
startDate: string,
endDate: string,
beraternr: string,
mandantennr: string
): Promise<Buffer> {
try {
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`;
// ---------------------------------------------------------
// 1. DATEN LADEN (CORE API SELECT & JOIN)
// ---------------------------------------------------------
// --- 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)
));
// Mapping: Flat Result -> Nested Object (damit der Rest des Codes gleich bleiben kann)
const createddocumentsList = cdRaw.map(r => ({
...r.doc,
customer: r.customer
}));
// --- 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)
));
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 CdCustomer = aliasedTable(customers, "cd_customer");
const IiVendor = aliasedTable(vendors, "ii_vendor");
const ContraAccount = aliasedTable(accounts, "contra_account");
const ContraVendor = aliasedTable(vendors, "contra_vendor");
const ContraCustomer = aliasedTable(customers, "contra_customer");
const ContraOwnaccount = aliasedTable(ownaccounts, "contra_ownaccount");
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,
contra_acc: ContraAccount,
contra_vend: ContraVendor,
contra_cust: ContraCustomer,
contra_own: ContraOwnaccount
})
.from(statementallocations)
// JOIN 1: Bankstatement (optional, manuelle Buchungen haben keinen Bankumsatz)
.leftJoin(bankstatements, eq(statementallocations.bankstatement, bankstatements.id))
// JOIN 2: Bankaccount (für DATEV Nummer)
.leftJoin(bankaccounts, eq(bankstatements.account, bankaccounts.id))
// JOIN 3: Ausgangsrechnung & deren Kunde
.leftJoin(createddocuments, eq(statementallocations.createddocument, createddocuments.id))
.leftJoin(CdCustomer, eq(createddocuments.customer, CdCustomer.id))
// JOIN 4: Eingangsrechnung & deren Lieferant
.leftJoin(incominginvoices, eq(statementallocations.incominginvoice, incominginvoices.id))
.leftJoin(IiVendor, eq(incominginvoices.vendor, IiVendor.id))
// 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))
.leftJoin(ContraAccount, eq(statementallocations.contraAccount, ContraAccount.id))
.leftJoin(ContraVendor, eq(statementallocations.contraVendor, ContraVendor.id))
.leftJoin(ContraCustomer, eq(statementallocations.contraCustomer, ContraCustomer.id))
.leftJoin(ContraOwnaccount, eq(statementallocations.contraOwnaccount, ContraOwnaccount.id))
.where(and(
eq(statementallocations.tenant, tenantId),
eq(statementallocations.archived, false),
or(
and(
gte(bankstatements.date, startDate),
lte(bankstatements.date, endDate)
),
and(
isNull(statementallocations.bankstatement),
gte(statementallocations.manualBookingDate, startDate),
lte(statementallocations.manualBookingDate, 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,
contraAccount: r.contra_acc,
contraVendor: r.contra_vend,
contraCustomer: r.contra_cust,
contraOwnaccount: r.contra_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);
}
};
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);
// ---------------------------------------------------------
// 4. CSV GENERIERUNG (Logic ist gleich geblieben)
// ---------------------------------------------------------
let bookingLines: string[] = [];
// AR
createddocumentsList.forEach(cd => {
let file = filesCreateddocuments.find(i => i.createddocument === cd.id);
let total = 0;
let typeString = "";
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 = Math.sign(total) === -1 ? "H" : "S";
const cust = cd.customer; // durch Mapping verfügbar
const datevTaxKey = getCreatedDocumentDatevTaxKey(cd);
bookingLines.push(`${displayCurrency(total,true)};"${shSelector}";;;;;${cust?.customerNumber || ""};8400;"${datevTaxKey}";${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
const getManualBookingSide = (alloc: any, side: "debit" | "credit") => {
const prefix = side === "credit" ? "contra" : "";
const account = side === "credit" ? alloc.contraAccount : alloc.account;
const vendor = side === "credit" ? alloc.contraVendor : alloc.vendor;
const customer = side === "credit" ? alloc.contraCustomer : alloc.customer;
const ownaccount = side === "credit" ? alloc.contraOwnaccount : alloc.ownaccount;
const incominginvoice = alloc.manualInvoiceSide === side ? alloc.incominginvoice : null;
if (account) return { number: account.number, name: account.label, type: "Sachkonto" };
if (vendor) return { number: vendor.vendorNumber, name: vendor.name, type: "Kreditor" };
if (customer) return { number: customer.customerNumber, name: customer.name, type: "Debitor" };
if (ownaccount) return { number: ownaccount.number, name: ownaccount.name, type: "Eigenes Konto" };
if (incominginvoice) {
return {
number: incominginvoice.vendor?.vendorNumber || "",
name: `${incominginvoice.reference || "Eingangsbeleg"} ${incominginvoice.vendor?.name || ""}`.trim(),
type: "Eingangsbeleg",
reference: incominginvoice.reference || "",
};
}
return { number: "", name: "", type: prefix };
};
statementallocationsList.forEach(alloc => {
const bs = alloc.bankstatement; // durch Mapping verfügbar
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 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;
}
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;
const datevTaxKey = getCreatedDocumentPaymentDatevTaxKey(cd);
bookingLines.push(`${displayCurrency(alloc.amount,true)};"H";;;;;${cust?.customerNumber};${datevKonto};"${datevTaxKey}";${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;;;;"";;;;;;`);
} 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;;;;"";;;;;;`);
} 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}";;;;;;;;;;"";;;;;;;;Bank-Id;${alloc.bankstatement.id};;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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}";;;;;;;;;;"";;;;;;;;Bank-Id;${alloc.bankstatement.id};;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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}";;;;;;;;;;"";;;;;;;;Bank-Id;${alloc.bankstatement.id};;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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}";;;;;;;;;;"";;;;;;;;Bank-Id;${alloc.bankstatement.id};;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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`;
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 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 || ''}";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;`);
});
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",
"@generatingSystem":"fedeo.de",
"@xsi:schemaLocation":"http://xml.datev.de/bedi/tps/document/v05.0 Document_v050.xsd",
"@xmlns":"http://xml.datev.de/bedi/tps/document/v05.0",
"@xmlns:xsi":"http://www.w3.org/2001/XMLSchema-instance",
header: {
date: dayjs().format("YYYY-MM-DDTHH:mm:ss")
},
content: {
document: [] as any[]
}
}
};
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}.${ext}`
}
});
};
filesCreateddocuments.forEach(addXmlDoc);
filesIncomingInvoices.forEach(addXmlDoc);
const doc = xmlbuilder.create(obj, {encoding: 'UTF-8', standalone: true});
await zipWriter.add(`document.xml`, new TextReader(doc.end({pretty: true})));
const arrayBuffer = await (await zipWriter.close()).arrayBuffer();
return Buffer.from(arrayBuffer);
} catch(error) {
console.error("DATEV Export Error:", error);
throw error;
}
}