Ausgangs- und Eingangsbelege erhalten Skontosatz und Frist, mit Vorgaben bei Kunden und Lieferanten. Das PDF ergänzt automatisch einen Skontohinweis (oder nutzt neue Textvorlagen-Variablen); bei Schlussrechnungen gilt der Skonto für die Restforderung. Skontobedingungen werden aus E-Rechnungen (#SKONTO# und ZUGFeRD) sowie von der KI-Vorbereitung übernommen. Die Bankzuordnung erkennt Zahlungen abzüglich Skonto innerhalb der Frist und bietet sie in der Vorschlagsübersicht direkt an. BWA-Erlöse und -Aufwand berücksichtigen gebuchten Skonto im Zeitraum der Zahlung. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
105 lines
3.8 KiB
TypeScript
105 lines
3.8 KiB
TypeScript
import { and, eq } from "drizzle-orm"
|
|
import type { FastifyInstance } from "fastify"
|
|
import { vendors } from "../../../db/schema"
|
|
import type { ParsedElectronicInvoice } from "./types"
|
|
|
|
const normalizeIdentifier = (value: unknown) => String(value || "").replace(/[^A-Za-z0-9]/g, "").toUpperCase()
|
|
const normalizeName = (value: unknown) => String(value || "").normalize("NFKD").replace(/[^A-Za-z0-9]/g, "").toUpperCase()
|
|
|
|
const paymentTypeByCode: Record<string, string> = {
|
|
"10": "Bar",
|
|
"30": "Überweisung",
|
|
"48": "Kreditkarte",
|
|
"49": "Lastschrift",
|
|
"57": "Sonstiges",
|
|
"58": "Überweisung",
|
|
"59": "Lastschrift",
|
|
}
|
|
|
|
export const findElectronicInvoiceVendor = async (
|
|
server: FastifyInstance,
|
|
tenantId: number,
|
|
invoice: ParsedElectronicInvoice,
|
|
) => {
|
|
const candidates = await server.db.select().from(vendors).where(and(
|
|
eq(vendors.tenant, tenantId),
|
|
eq(vendors.archived, false),
|
|
))
|
|
|
|
const vatId = normalizeIdentifier(invoice.seller.vatId)
|
|
if (vatId) {
|
|
const vatMatches = candidates.filter((vendor: any) => normalizeIdentifier(vendor.infoData?.ustid) === vatId)
|
|
if (vatMatches.length === 1) return vatMatches[0]
|
|
}
|
|
|
|
const sellerName = normalizeName(invoice.seller.name)
|
|
if (sellerName) {
|
|
const nameMatches = candidates.filter((vendor) => normalizeName(vendor.name) === sellerName)
|
|
if (nameMatches.length === 1) return nameMatches[0]
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
const addDaysToDate = (date: string, days: number) => {
|
|
const parsed = new Date(`${String(date).slice(0, 10)}T00:00:00Z`)
|
|
if (Number.isNaN(parsed.getTime())) return null
|
|
parsed.setUTCDate(parsed.getUTCDate() + days)
|
|
return parsed.toISOString().slice(0, 10)
|
|
}
|
|
|
|
export const mapElectronicInvoiceToIncomingInvoice = (
|
|
invoice: ParsedElectronicInvoice,
|
|
tenantId: number,
|
|
vendorId: number | null,
|
|
container: "pdf" | "xml",
|
|
attachmentName: string | null,
|
|
) => {
|
|
const warnings = [...invoice.validation.warnings]
|
|
if (!vendorId) warnings.push("Rechnungssteller konnte keinem Lieferanten eindeutig zugeordnet werden.")
|
|
|
|
const state = invoice.validation.errors.length > 0 || !vendorId ? "Prüfung erforderlich" : "Vorbereitet"
|
|
const descriptionParts = [
|
|
invoice.buyerReference ? `Käuferreferenz: ${invoice.buyerReference}` : null,
|
|
...invoice.items.map((item) => item.description),
|
|
].filter(Boolean)
|
|
|
|
return {
|
|
tenant: tenantId,
|
|
state,
|
|
vendor: vendorId,
|
|
reference: invoice.invoiceNumber,
|
|
date: invoice.invoiceDate,
|
|
dueDate: invoice.dueDate,
|
|
cashDiscountPercent: invoice.cashDiscount?.percent ?? null,
|
|
cashDiscountDueDate: invoice.cashDiscount && invoice.invoiceDate
|
|
? addDaysToDate(invoice.invoiceDate, invoice.cashDiscount.days)
|
|
: null,
|
|
paymentType: paymentTypeByCode[invoice.paymentMeansCode || ""] || "Sonstiges",
|
|
description: descriptionParts.join("\n"),
|
|
accounts: invoice.items.map((item) => ({
|
|
account: null,
|
|
description: item.description,
|
|
amountNet: item.netAmount,
|
|
amountTax: item.taxAmount,
|
|
taxType: item.taxCategory === "AE" ? "13B" : item.taxRate === 0 ? "null" : String(item.taxRate),
|
|
amountGross: item.grossAmount,
|
|
costCentre: null,
|
|
quantity: item.quantity,
|
|
unitCode: item.unitCode,
|
|
})),
|
|
preparationSource: "e-invoice",
|
|
eInvoiceSyntax: invoice.syntax,
|
|
eInvoiceProfile: invoice.profileId,
|
|
eInvoiceValidation: {
|
|
errors: invoice.validation.errors,
|
|
warnings,
|
|
totals: invoice.totals,
|
|
seller: invoice.seller,
|
|
invoiceType: invoice.invoiceType,
|
|
container,
|
|
attachmentName,
|
|
},
|
|
}
|
|
}
|