726 lines
27 KiB
TypeScript
726 lines
27 KiB
TypeScript
import dayjs from "dayjs";
|
|
import quarterOfYear from "dayjs/plugin/quarterOfYear";
|
|
import Handlebars from "handlebars";
|
|
import axios from "axios";
|
|
import { eq, inArray, and } from "drizzle-orm"; // Drizzle Operatoren
|
|
|
|
// DEINE IMPORTS
|
|
import * as schema from "../../db/schema"; // Importiere dein Drizzle Schema
|
|
import { saveFile } from "../utils/files";
|
|
import {FastifyInstance} from "fastify";
|
|
import {useNextNumberRangeNumber} from "../utils/functions";
|
|
import {createInvoicePDF} from "../utils/pdf"; // Achtung: Muss Node.js Buffer unterstützen!
|
|
|
|
dayjs.extend(quarterOfYear);
|
|
|
|
|
|
export const executeManualGeneration = async (server:FastifyInstance,executionDate,templateIds,tenantId,executedBy) => {
|
|
try {
|
|
console.log(executedBy)
|
|
|
|
const executionDayjs = dayjs(executionDate);
|
|
|
|
console.log(`Starte manuelle Generierung für Tenant ${tenantId} am ${executionDate}`);
|
|
|
|
// 1. Tenant laden (Drizzle)
|
|
// Wir nehmen an, dass 'tenants' im Schema definiert ist
|
|
const [tenant] = await server.db
|
|
.select()
|
|
.from(schema.tenants)
|
|
.where(eq(schema.tenants.id, tenantId))
|
|
.limit(1);
|
|
|
|
if (!tenant) throw new Error(`Tenant mit ID ${tenantId} nicht gefunden.`);
|
|
|
|
// 2. Templates laden
|
|
const templates = await server.db
|
|
.select()
|
|
.from(schema.createddocuments)
|
|
.where(
|
|
and(
|
|
eq(schema.createddocuments.tenant, tenantId),
|
|
eq(schema.createddocuments.type, "serialInvoices"),
|
|
inArray(schema.createddocuments.id, templateIds)
|
|
)
|
|
);
|
|
|
|
if (templates.length === 0) {
|
|
console.warn("Keine passenden Vorlagen gefunden.");
|
|
return [];
|
|
}
|
|
|
|
// 3. Folder & FileType IDs holen (Hilfsfunktionen unten)
|
|
const folderId = await getFolderId(server,tenantId);
|
|
const fileTypeId = await getFileTypeId(server,tenantId);
|
|
|
|
const results = [];
|
|
|
|
const [executionRecord] = await server.db
|
|
.insert(schema.serialExecutions)
|
|
.values({
|
|
tenant: tenantId,
|
|
executionDate: executionDayjs.toDate(),
|
|
status: "draft",
|
|
createdBy: executedBy,
|
|
summary: `${templateIds.length} Vorlagen verarbeitet`
|
|
})
|
|
.returning();
|
|
|
|
console.log(executionRecord);
|
|
|
|
// 4. Loop durch die Templates
|
|
for (const template of templates) {
|
|
try {
|
|
const resultId = await processSingleTemplate(
|
|
server,
|
|
template,
|
|
tenant,
|
|
executionDayjs,
|
|
folderId,
|
|
fileTypeId,
|
|
executedBy,
|
|
executionRecord.id
|
|
);
|
|
results.push({ id: template.id, status: "success", newDocumentId: resultId });
|
|
} catch (e: any) {
|
|
console.error(`Fehler bei Template ${template.id}:`, e);
|
|
results.push({ id: template.id, status: "error", error: e.message });
|
|
}
|
|
}
|
|
|
|
return results;
|
|
} catch (error) {
|
|
console.log(error);
|
|
}
|
|
}
|
|
|
|
export const finishManualGeneration = async (server: FastifyInstance, executionId: number) => {
|
|
try {
|
|
console.log(`Beende Ausführung ${executionId}...`);
|
|
|
|
// 1. Execution und Tenant laden
|
|
|
|
const [executionRecord] = await server.db
|
|
.select()
|
|
.from(schema.serialExecutions)// @ts-ignore
|
|
.where(eq(schema.serialExecutions.id, executionId))
|
|
.limit(1);
|
|
|
|
if (!executionRecord) throw new Error("Execution nicht gefunden");
|
|
|
|
console.log(executionRecord);
|
|
|
|
const tenantId = executionRecord.tenant;
|
|
|
|
console.log(tenantId)
|
|
|
|
// Tenant laden (für Settings etc.)
|
|
const [tenant] = await server.db
|
|
.select()
|
|
.from(schema.tenants)
|
|
.where(eq(schema.tenants.id, tenantId))
|
|
.limit(1);
|
|
|
|
// 2. Status auf "processing" setzen (optional, damit UI feedback hat)
|
|
|
|
/*await server.db
|
|
.update(schema.serialExecutions)
|
|
.set({ status: "processing" })// @ts-ignore
|
|
.where(eq(schema.serialExecutions.id, executionId));*/
|
|
|
|
// 3. Alle erstellten Dokumente dieser Execution laden
|
|
const documents = await server.db
|
|
.select()
|
|
.from(schema.createddocuments)
|
|
.where(eq(schema.createddocuments.serialexecution, executionId));
|
|
|
|
console.log(`${documents.length} Dokumente werden finalisiert...`);
|
|
|
|
// 4. IDs für File-System laden (nur einmalig nötig)
|
|
const folderId = await getFolderId(server, tenantId);
|
|
const fileTypeId = await getFileTypeId(server, tenantId);
|
|
|
|
// Globale Daten laden, die für alle gleich sind (Optimierung)
|
|
const [units, products, services] = await Promise.all([
|
|
server.db.select().from(schema.units),
|
|
server.db.select().from(schema.products).where(eq(schema.products.tenant, tenantId)),
|
|
server.db.select().from(schema.services).where(eq(schema.services.tenant, tenantId)),
|
|
]);
|
|
|
|
let successCount = 0;
|
|
let errorCount = 0;
|
|
|
|
// 5. Loop durch Dokumente
|
|
for (const doc of documents) {
|
|
try {
|
|
|
|
const [letterhead] = await Promise.all([
|
|
/*fetchById(server, schema.contacts, doc.contact),
|
|
fetchById(server, schema.customers, doc.customer),
|
|
fetchById(server, schema.authProfiles, doc.contactPerson), // oder createdBy, je nach Logik
|
|
fetchById(server, schema.projects, doc.project),
|
|
fetchById(server, schema.contracts, doc.contract),*/
|
|
doc.letterhead ? fetchById(server, schema.letterheads, doc.letterhead) : null
|
|
]);
|
|
|
|
const pdfData = await getCloseData(
|
|
server,
|
|
doc,
|
|
tenant,
|
|
units,
|
|
products,
|
|
services,
|
|
);
|
|
|
|
console.log(pdfData);
|
|
|
|
// D. PDF Generieren
|
|
const pdfBase64 = await createInvoicePDF(server,"base64",pdfData, letterhead?.path);
|
|
|
|
console.log(pdfBase64);
|
|
|
|
// E. Datei speichern
|
|
// @ts-ignore
|
|
const fileBuffer = Buffer.from(pdfBase64.base64, "base64");
|
|
const filename = `${pdfData.documentNumber}.pdf`;
|
|
|
|
await saveFile(
|
|
server,
|
|
tenantId,
|
|
null, // User ID (hier ggf. executionRecord.createdBy nutzen wenn verfügbar)
|
|
fileBuffer,
|
|
folderId,
|
|
fileTypeId,
|
|
{
|
|
createddocument: doc.id,
|
|
filename: filename,
|
|
filesize: fileBuffer.length // Falls saveFile das braucht
|
|
}
|
|
);
|
|
|
|
// F. Dokument in DB final updaten
|
|
await server.db
|
|
.update(schema.createddocuments)
|
|
.set({
|
|
state: "Gebucht",
|
|
documentNumber: pdfData.documentNumber,
|
|
title: pdfData.title,
|
|
pdf_path: filename // Optional, falls du den Pfad direkt am Doc speicherst
|
|
})
|
|
.where(eq(schema.createddocuments.id, doc.id));
|
|
|
|
successCount++;
|
|
|
|
} catch (innerErr) {
|
|
console.error(`Fehler beim Finalisieren von Doc ID ${doc.id}:`, innerErr);
|
|
errorCount++;
|
|
// Optional: Status des einzelnen Dokuments auf Error setzen
|
|
}
|
|
}
|
|
|
|
// 6. Execution abschließen
|
|
const finalStatus = errorCount > 0 ? "error" : "completed"; // Oder 'completed' auch wenn Teilerfolge
|
|
|
|
|
|
await server.db
|
|
.update(schema.serialExecutions)
|
|
.set({
|
|
status: finalStatus,
|
|
summary: `Abgeschlossen: ${successCount} erfolgreich, ${errorCount} Fehler.`
|
|
})// @ts-ignore
|
|
.where(eq(schema.serialExecutions.id, executionId));
|
|
|
|
return { success: true, processed: successCount, errors: errorCount };
|
|
|
|
} catch (error) {
|
|
console.error("Critical Error in finishManualGeneration:", error);
|
|
// Execution auf Error setzen
|
|
// @ts-ignore
|
|
await server.db
|
|
.update(schema.serialExecutions)
|
|
.set({ status: "error", summary: "Kritischer Fehler beim Finalisieren." })
|
|
//@ts-ignore
|
|
.where(eq(schema.serialExecutions.id, executionId));
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Verarbeitet eine einzelne Vorlage
|
|
*/
|
|
async function processSingleTemplate(server: FastifyInstance, template: any, tenant: any,executionDate: dayjs.Dayjs,folderId: string,fileTypeId: string,executedBy: string,executionId: string) {
|
|
// A. Zugehörige Daten parallel laden
|
|
const [contact, customer, profile, project, contract, units, products, services, letterhead] = await Promise.all([
|
|
fetchById(server, schema.contacts, template.contact),
|
|
fetchById(server, schema.customers, template.customer),
|
|
fetchById(server, schema.authProfiles, template.contactPerson),
|
|
fetchById(server, schema.projects, template.project),
|
|
fetchById(server, schema.contracts, template.contract),
|
|
server.db.select().from(schema.units),
|
|
server.db.select().from(schema.products).where(eq(schema.products.tenant, tenant.id)),
|
|
server.db.select().from(schema.services).where(eq(schema.services.tenant, tenant.id)),
|
|
template.letterhead ? fetchById(server, schema.letterheads, template.letterhead) : null
|
|
]);
|
|
|
|
// B. Datumsberechnung (Logik aus dem Original)
|
|
const { firstDate, lastDate } = calculateDateRange(template.serialConfig, executionDate);
|
|
|
|
// C. Rechnungsnummer & Save Data
|
|
const savePayload = await getSaveData(
|
|
template,
|
|
tenant,
|
|
firstDate,
|
|
lastDate,
|
|
executionDate.toISOString(),
|
|
executedBy
|
|
);
|
|
|
|
const payloadWithRelation = {
|
|
...savePayload,
|
|
serialexecution: executionId
|
|
};
|
|
|
|
// D. Dokument in DB anlegen (Drizzle Insert)
|
|
const [createdDoc] = await server.db
|
|
.insert(schema.createddocuments)
|
|
.values(payloadWithRelation)
|
|
.returning(); // Wichtig für Postgres: returning() gibt das erstellte Objekt zurück
|
|
|
|
return createdDoc.id;
|
|
}
|
|
|
|
// --- Drizzle Helper ---
|
|
|
|
async function fetchById(server: FastifyInstance, table: any, id: number | null) {
|
|
if (!id) return null;
|
|
const [result] = await server.db.select().from(table).where(eq(table.id, id)).limit(1);
|
|
return result || null;
|
|
}
|
|
|
|
async function getFolderId(server:FastifyInstance, tenantId: number) {
|
|
const [folder] = await server.db
|
|
.select({ id: schema.folders.id })
|
|
.from(schema.folders)
|
|
.where(
|
|
and(
|
|
eq(schema.folders.tenant, tenantId),
|
|
eq(schema.folders.function, "invoices"), // oder 'invoices', check deine DB
|
|
eq(schema.folders.year, dayjs().format("YYYY"))
|
|
)
|
|
)
|
|
.limit(1);
|
|
return folder?.id;
|
|
}
|
|
|
|
async function getFileTypeId(server: FastifyInstance,tenantId: number) {
|
|
const [tag] = await server.db
|
|
.select({ id: schema.filetags.id })
|
|
.from(schema.filetags)
|
|
.where(
|
|
and(
|
|
eq(schema.filetags.tenant, tenantId),
|
|
eq(schema.filetags.createdDocumentType, "invoices")
|
|
)
|
|
)
|
|
.limit(1);
|
|
return tag?.id;
|
|
}
|
|
|
|
|
|
|
|
// --- Logik Helper (Unverändert zur Business Logik) ---
|
|
|
|
function calculateDateRange(config: any, executionDate: dayjs.Dayjs) {
|
|
// Basis nehmen
|
|
let baseDate = executionDate;
|
|
|
|
let firstDate = baseDate;
|
|
let lastDate = baseDate;
|
|
|
|
if (config.intervall === "monatlich" && config.dateDirection === "Rückwirkend") {
|
|
// 1. Monat abziehen
|
|
// 2. Start/Ende des Monats berechnen
|
|
// 3. WICHTIG: Zeit hart auf 12:00:00 setzen, damit Zeitzonen das Datum nicht kippen
|
|
firstDate = baseDate.subtract(1, "month").startOf("month").hour(12).minute(0).second(0).millisecond(0);
|
|
lastDate = baseDate.subtract(1, "month").endOf("month").hour(12).minute(0).second(0).millisecond(0);
|
|
|
|
} else if (config.intervall === "vierteljährlich" && config.dateDirection === "Rückwirkend") {
|
|
|
|
firstDate = baseDate.subtract(1, "quarter").startOf("quarter").hour(12).minute(0).second(0).millisecond(0);
|
|
lastDate = baseDate.subtract(1, "quarter").endOf("quarter").hour(12).minute(0).second(0).millisecond(0);
|
|
}
|
|
|
|
// Das Ergebnis ist nun z.B.:
|
|
// firstDate: '2025-12-01T12:00:00.000Z' (Eindeutig der 1. Dezember)
|
|
// lastDate: '2025-12-31T12:00:00.000Z' (Eindeutig der 31. Dezember)
|
|
|
|
return {
|
|
firstDate: firstDate.toISOString(),
|
|
lastDate: lastDate.toISOString()
|
|
};
|
|
}
|
|
|
|
async function getSaveData(item: any, tenant: any, firstDate: string, lastDate: string, executionDate: string, executedBy: string) {
|
|
const cleanRows = item.rows.map((row: any) => ({
|
|
...row,
|
|
descriptionText: row.description || null,
|
|
}));
|
|
|
|
//const documentNumber = await this.useNextInvoicesNumber(item.tenant);
|
|
|
|
return {
|
|
tenant: item.tenant,
|
|
type: "invoices",
|
|
state: "Entwurf",
|
|
customer: item.customer,
|
|
contact: item.contact,
|
|
contract: item.contract,
|
|
address: item.address,
|
|
project: item.project,
|
|
documentDate: executionDate,
|
|
deliveryDate: firstDate,
|
|
deliveryDateEnd: lastDate,
|
|
paymentDays: item.paymentDays,
|
|
payment_type: item.payment_type,
|
|
deliveryDateType: item.deliveryDateType,
|
|
info: {}, // Achtung: Postgres erwartet hier valides JSON Objekt
|
|
createdBy: item.createdBy,
|
|
created_by: item.created_by,
|
|
title: `Rechnung-Nr. XXX`,
|
|
description: item.description,
|
|
startText: item.startText,
|
|
endText: item.endText,
|
|
rows: cleanRows, // JSON Array
|
|
contactPerson: item.contactPerson,
|
|
linkedDocument: item.linkedDocument,
|
|
letterhead: item.letterhead,
|
|
taxType: item.taxType,
|
|
};
|
|
}
|
|
|
|
async function getCloseData(server:FastifyInstance,item: any, tenant: any, units, products,services) {
|
|
const documentNumber = await useNextNumberRangeNumber(server,tenant.id,"invoices");
|
|
|
|
console.log(item);
|
|
|
|
const [contact, customer, project, contract] = await Promise.all([
|
|
fetchById(server, schema.contacts, item.contact),
|
|
fetchById(server, schema.customers, item.customer),
|
|
fetchById(server, schema.projects, item.project),
|
|
fetchById(server, schema.contracts, item.contract),
|
|
item.letterhead ? fetchById(server, schema.letterheads, item.letterhead) : null
|
|
|
|
]);
|
|
|
|
const profile = (await server.db.select().from(schema.authProfiles).where(and(eq(schema.authProfiles.user_id, item.created_by),eq(schema.authProfiles.tenant_id,tenant.id))).limit(1))[0];
|
|
|
|
console.log(profile)
|
|
|
|
const pdfData = getDocumentDataBackend(
|
|
{
|
|
...item,
|
|
state: "Gebucht",
|
|
documentNumber: documentNumber.usedNumber,
|
|
title: `Rechnung-Nr. ${documentNumber.usedNumber}`,
|
|
}, // Das Dokument (mit neuer Nummer)
|
|
tenant, // Tenant Object
|
|
customer, // Customer Object
|
|
contact, // Contact Object (kann null sein)
|
|
profile, // User Profile (Contact Person)
|
|
project, // Project Object
|
|
contract, // Contract Object
|
|
units, // Units Array
|
|
products, // Products Array
|
|
services // Services Array
|
|
);
|
|
|
|
|
|
return pdfData;
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Formatiert Zahlen zu deutscher Währung
|
|
function renderCurrency(value: any, currency = "€") {
|
|
if (value === undefined || value === null) return "0,00 " + currency;
|
|
return Number(value).toFixed(2).replace(".", ",") + " " + currency;
|
|
}
|
|
|
|
// Berechnet den Zeilenpreis (Menge * Preis * Rabatt)
|
|
function getRowAmount(row: any) {
|
|
const price = Number(row.price || 0);
|
|
const quantity = Number(row.quantity || 0);
|
|
const discount = Number(row.discountPercent || 0);
|
|
return quantity * price * (1 - discount / 100);
|
|
}
|
|
|
|
// Berechnet alle Summen (Netto, Brutto, Steuern, Titel-Summen)
|
|
// Dies ersetzt 'documentTotal.value' aus dem Frontend
|
|
function calculateDocumentTotals(rows: any[], taxType: string) {
|
|
console.log(rows);
|
|
|
|
let totalNet = 0;
|
|
let totalNet19 = 0;
|
|
let totalNet7 = 0;
|
|
let totalNet0 = 0;
|
|
let titleSums: Record<string, number> = {};
|
|
|
|
// Aktueller Titel für Gruppierung
|
|
let currentTitle = "";
|
|
|
|
rows.forEach(row => {
|
|
if (row.mode === 'title') {
|
|
currentTitle = row.text || row.description || "Titel";
|
|
if (!titleSums[currentTitle]) titleSums[currentTitle] = 0;
|
|
return;
|
|
}
|
|
|
|
if (['normal', 'service', 'free'].includes(row.mode)) {
|
|
const amount = getRowAmount(row);
|
|
totalNet += amount;
|
|
|
|
// Summen pro Titel addieren
|
|
//if (!titleSums[currentTitle]) titleSums[currentTitle] = 0;
|
|
if(currentTitle.length > 0) titleSums[currentTitle] += amount;
|
|
|
|
// Steuer-Logik
|
|
const tax = taxType === "19 UStG" || taxType === "13b UStG" ? 0 : Number(row.taxPercent);
|
|
|
|
if (tax === 19) totalNet19 += amount;
|
|
else if (tax === 7) totalNet7 += amount;
|
|
else totalNet0 += amount;
|
|
}
|
|
});
|
|
|
|
const isTaxFree = ["13b UStG", "19 UStG"].includes(taxType);
|
|
|
|
const tax19 = isTaxFree ? 0 : totalNet19 * 0.19;
|
|
const tax7 = isTaxFree ? 0 : totalNet7 * 0.07;
|
|
const totalGross = totalNet + tax19 + tax7;
|
|
|
|
return {
|
|
totalNet,
|
|
totalNet19,
|
|
totalNet7,
|
|
totalNet0,
|
|
total19: tax19,
|
|
total7: tax7,
|
|
total0: 0,
|
|
totalGross,
|
|
titleSums // Gibt ein Objekt zurück: { "Titel A": 150.00, "Titel B": 200.00 }
|
|
};
|
|
}
|
|
|
|
export function getDocumentDataBackend(
|
|
itemInfo: any, // Das Dokument objekt (createddocument)
|
|
tenant: any, // Tenant Infos (auth.activeTenantData)
|
|
customerData: any, // Geladener Kunde
|
|
contactData: any, // Geladener Kontakt (optional)
|
|
contactPerson: any, // Geladenes User-Profil (ersetzt den API Call)
|
|
projectData: any, // Projekt
|
|
contractData: any, // Vertrag
|
|
units: any[], // Array aller Einheiten
|
|
products: any[], // Array aller Produkte
|
|
services: any[] // Array aller Services
|
|
) {
|
|
const businessInfo = tenant.businessInfo || {}; // Fallback falls leer
|
|
|
|
// --- 1. Agriculture Logic ---
|
|
// Prüfen ob 'extraModules' existiert, sonst leeres Array annehmen
|
|
const modules = tenant.extraModules || [];
|
|
if (modules.includes("agriculture")) {
|
|
itemInfo.rows.forEach((row: any) => {
|
|
if (row.agriculture && row.agriculture.dieselUsage) {
|
|
row.agriculture.description = `${row.agriculture.dieselUsage} L Diesel zu ${renderCurrency(row.agriculture.dieselPrice)}/L verbraucht ${row.description ? "\n" + row.description : ""}`;
|
|
}
|
|
});
|
|
}
|
|
|
|
// --- 2. Tax Override Logic ---
|
|
let rows = JSON.parse(JSON.stringify(itemInfo.rows)); // Deep Clone um Original nicht zu mutieren
|
|
if (itemInfo.taxType === "13b UStG" || itemInfo.taxType === "19 UStG") {
|
|
rows = rows.map((row: any) => ({ ...row, taxPercent: 0 }));
|
|
}
|
|
|
|
// --- 4. Berechnungen (Ersetzt Vue computed props) ---
|
|
const totals = calculateDocumentTotals(rows, itemInfo.taxType);
|
|
|
|
console.log(totals);
|
|
|
|
// --- 3. Rows Mapping & Processing ---
|
|
rows = rows.map((row: any) => {
|
|
const unit = units.find(i => i.id === row.unit) || { short: "" };
|
|
|
|
// Description Text Logic
|
|
if (!['pagebreak', 'title'].includes(row.mode)) {
|
|
if (row.agriculture && row.agriculture.description) {
|
|
row.descriptionText = row.agriculture.description;
|
|
} else if (row.description) {
|
|
row.descriptionText = row.description;
|
|
} else {
|
|
delete row.descriptionText;
|
|
}
|
|
}
|
|
|
|
// Product/Service Name Resolution
|
|
if (!['pagebreak', 'title', 'text'].includes(row.mode)) {
|
|
if (row.mode === 'normal') {
|
|
const prod = products.find(i => i.id === row.product);
|
|
if (prod) row.text = prod.name;
|
|
}
|
|
if (row.mode === 'service') {
|
|
const serv = services.find(i => i.id === row.service);
|
|
if (serv) row.text = serv.name;
|
|
}
|
|
|
|
const rowAmount = getRowAmount(row);
|
|
|
|
return {
|
|
...row,
|
|
rowAmount: renderCurrency(rowAmount),
|
|
quantity: String(row.quantity).replace(".", ","),
|
|
unit: unit.short,
|
|
pos: String(row.pos),
|
|
price: renderCurrency(row.price),
|
|
discountText: row.discountPercent > 0 ? `(Rabatt: ${row.discountPercent} %)` : ""
|
|
};
|
|
} else {
|
|
return row;
|
|
}
|
|
});
|
|
|
|
|
|
|
|
// --- 5. Handlebars Context ---
|
|
const generateContext = () => {
|
|
return {
|
|
// lohnkosten: null, // Backend hat diesen Wert oft nicht direkt, ggf. aus itemInfo holen
|
|
anrede: (contactData && contactData.salutation) || (customerData && customerData.salutation),
|
|
titel: (contactData && contactData.title) || (customerData && customerData.title),
|
|
vorname: (contactData && contactData.firstName) || (customerData && customerData.firstname), // Achte auf casing (firstName vs firstname) in deiner DB
|
|
nachname: (contactData && contactData.lastName) || (customerData && customerData.lastname),
|
|
kundenname: customerData && customerData.name,
|
|
zahlungsziel_in_tagen: itemInfo.paymentDays,
|
|
zahlungsart: itemInfo.payment_type === "transfer" ? "Überweisung" : "Lastschrift",
|
|
diesel_gesamtverbrauch: (itemInfo.agriculture && itemInfo.agriculture.dieselUsageTotal) || null
|
|
};
|
|
};
|
|
|
|
const templateStartText = Handlebars.compile(itemInfo.startText || "");
|
|
const templateEndText = Handlebars.compile(itemInfo.endText || "");
|
|
|
|
// --- 6. Title Sums Formatting ---
|
|
let returnTitleSums: Record<string, string> = {};
|
|
Object.keys(totals.titleSums).forEach(key => {
|
|
returnTitleSums[key] = renderCurrency(totals.titleSums[key]);
|
|
});
|
|
|
|
// Transfer logic (Falls nötig, hier vereinfacht)
|
|
let returnTitleSumsTransfer = { ...returnTitleSums };
|
|
|
|
// --- 7. Construct Final Object ---
|
|
|
|
// Adresse aufbereiten
|
|
const recipientArray = [
|
|
customerData.name,
|
|
...(customerData.nameAddition ? [customerData.nameAddition] : []),
|
|
...(contactData ? [`${contactData.firstName} ${contactData.lastName}`] : []),
|
|
itemInfo.address?.street || customerData.street || "",
|
|
...(itemInfo.address?.special ? [itemInfo.address.special] : []),
|
|
`${itemInfo.address?.zip || customerData.zip} ${itemInfo.address?.city || customerData.city}`,
|
|
].filter(Boolean); // Leere Einträge entfernen
|
|
|
|
console.log(contactPerson)
|
|
|
|
// Info Block aufbereiten
|
|
const infoBlock = [
|
|
{
|
|
label: itemInfo.documentNumberTitle || "Rechnungsnummer",
|
|
content: itemInfo.documentNumber || "ENTWURF",
|
|
}, {
|
|
label: "Kundennummer",
|
|
content: customerData.customerNumber,
|
|
}, {
|
|
label: "Belegdatum",
|
|
content: itemInfo.documentDate ? dayjs(itemInfo.documentDate).format("DD.MM.YYYY") : dayjs().format("DD.MM.YYYY"),
|
|
},
|
|
// Lieferdatum Logik
|
|
...(itemInfo.deliveryDateType !== "Kein Lieferdatum anzeigen" ? [{
|
|
label: itemInfo.deliveryDateType || "Lieferdatum",
|
|
content: !['Lieferzeitraum', 'Leistungszeitraum'].includes(itemInfo.deliveryDateType)
|
|
? (itemInfo.deliveryDate ? dayjs(itemInfo.deliveryDate).format("DD.MM.YYYY") : "")
|
|
: `${itemInfo.deliveryDate ? dayjs(itemInfo.deliveryDate).format("DD.MM.YYYY") : ""} - ${itemInfo.deliveryDateEnd ? dayjs(itemInfo.deliveryDateEnd).format("DD.MM.YYYY") : ""}`,
|
|
}] : []),
|
|
{
|
|
label: "Ansprechpartner",
|
|
content: contactPerson ? (contactPerson.name || contactPerson.full_name || contactPerson.email) : "-",
|
|
},
|
|
// Kontakt Infos
|
|
...((itemInfo.contactTel || contactPerson?.fixed_tel || contactPerson?.mobile_tel) ? [{
|
|
label: "Telefon",
|
|
content: itemInfo.contactTel || contactPerson?.fixed_tel || contactPerson?.mobile_tel,
|
|
}] : []),
|
|
...(contactPerson?.email ? [{
|
|
label: "E-Mail",
|
|
content: contactPerson.email,
|
|
}] : []),
|
|
// Objekt / Projekt / Vertrag
|
|
...(itemInfo.plant ? [{ label: "Objekt", content: "Objekt Name" }] : []), // Hier müsstest du Plant Data übergeben wenn nötig
|
|
...(projectData ? [{ label: "Projekt", content: projectData.name }] : []),
|
|
...(contractData ? [{ label: "Vertrag", content: contractData.contractNumber }] : [])
|
|
];
|
|
|
|
// Total Array für PDF Footer
|
|
const totalArray = [
|
|
{
|
|
label: "Nettobetrag",
|
|
content: renderCurrency(totals.totalNet),
|
|
},
|
|
...(totals.totalNet19 > 0 && !["13b UStG"].includes(itemInfo.taxType) ? [{
|
|
label: `zzgl. 19% USt auf ${renderCurrency(totals.totalNet19)}`,
|
|
content: renderCurrency(totals.total19),
|
|
}] : []),
|
|
...(totals.totalNet7 > 0 && !["13b UStG"].includes(itemInfo.taxType) ? [{
|
|
label: `zzgl. 7% USt auf ${renderCurrency(totals.totalNet7)}`,
|
|
content: renderCurrency(totals.total7),
|
|
}] : []),
|
|
{
|
|
label: "Gesamtbetrag",
|
|
content: renderCurrency(totals.totalGross),
|
|
},
|
|
];
|
|
|
|
return {
|
|
...itemInfo,
|
|
type: itemInfo.type,
|
|
taxType: itemInfo.taxType,
|
|
adressLine: `${businessInfo.name || ''}, ${businessInfo.street || ''}, ${businessInfo.zip || ''} ${businessInfo.city || ''}`,
|
|
recipient: recipientArray,
|
|
info: infoBlock,
|
|
title: itemInfo.title,
|
|
description: itemInfo.description,
|
|
// Handlebars Compilation ausführen
|
|
endText: templateEndText(generateContext()),
|
|
startText: templateStartText(generateContext()),
|
|
rows: rows,
|
|
totalArray: totalArray,
|
|
total: {
|
|
totalNet: renderCurrency(totals.totalNet),
|
|
total19: renderCurrency(totals.total19),
|
|
total0: renderCurrency(totals.total0), // 0% USt Zeilen
|
|
totalGross: renderCurrency(totals.totalGross),
|
|
// Diese Werte existieren im einfachen Backend-Kontext oft nicht (Zahlungen checken), daher 0 oder Logik bauen
|
|
totalGrossAlreadyPaid: renderCurrency(0),
|
|
totalSumToPay: renderCurrency(totals.totalGross),
|
|
titleSums: returnTitleSums,
|
|
titleSumsTransfer: returnTitleSumsTransfer
|
|
},
|
|
agriculture: itemInfo.agriculture,
|
|
// Falls du AdvanceInvoices brauchst, musst du die Objekte hier übergeben oder leer lassen
|
|
usedAdvanceInvoices: []
|
|
};
|
|
}
|