fix(dkv): fix Kennzeichen matching and NaN/invalid km values in Excel export
Three issues fixed:
1. Kennzeichen normalization: DKV PDF extracts plates without hyphens
("GP JL 740E" vs CSV-imported "GP-JL 740E"). Added _normalizeKennzeichen()
which strips hyphens, spaces, and dots before lookup — resolves vehicle
master match failure that caused Marke/Modell/Fahrer to appear empty.
2. Empty-string NaN: parser used ?? '0' which doesn't catch empty strings,
causing parseDE('') = NaN. Changed to || '0' for km, menge, and totals.
3. Invalid km values: EV charging rows from DKV have misaligned columns —
km position contains a decimal price (e.g. 18.64 EUR or kWh). Added
sanity check: non-integer km values are written as null (empty cell)
instead of a misleading decimal. ExportRow.kilometerstand is now number|null.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -207,13 +207,14 @@ export class DkvParserService {
|
|||||||
return dates.map((date, i) => ({
|
return dates.map((date, i) => ({
|
||||||
lieferdatum: date,
|
lieferdatum: date,
|
||||||
ort: orts[i] ?? '',
|
ort: orts[i] ?? '',
|
||||||
// German number format: "19.234,56" → 19234.56 (strip dots, replace comma)
|
// German number format: "19.234,56" → 19234.56 (strip dots, replace comma).
|
||||||
kilometerstand: parseDE(kms[i] ?? '0'),
|
// Use || '0' (not ??) so empty strings also fall back to '0' (avoids NaN).
|
||||||
|
kilometerstand: parseDE(kms[i] || '0'),
|
||||||
produkt: products[i] ?? '',
|
produkt: products[i] ?? '',
|
||||||
menge: parseDE(quantities[i] ?? '0'),
|
menge: parseDE(quantities[i] || '0'),
|
||||||
einheit: units[i] ?? '',
|
einheit: units[i] ?? '',
|
||||||
netto: parseDE(totals_netto[i] ?? '0'),
|
netto: parseDE(totals_netto[i] || '0'),
|
||||||
brutto: parseDE(totals_brutto[i] ?? '0'),
|
brutto: parseDE(totals_brutto[i] || '0'),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -579,15 +579,16 @@ export class DkvService {
|
|||||||
tenantId: string,
|
tenantId: string,
|
||||||
vehicles: DkvVehicleBlock[],
|
vehicles: DkvVehicleBlock[],
|
||||||
vehicleFormatString: string,
|
vehicleFormatString: string,
|
||||||
): Promise<{ lieferdatum: string; fahrzeug: string; fahrer: string; ort: string; kilometerstand: number }[]> {
|
): Promise<{ lieferdatum: string; fahrzeug: string; fahrer: string; ort: string; kilometerstand: number | null }[]> {
|
||||||
// Batch load vehicle master to avoid N+1 queries
|
// Batch load vehicle master to avoid N+1 queries
|
||||||
const masters = await this.prisma.dkvVehicleMaster.findMany({ where: { tenantId } });
|
const masters = await this.prisma.dkvVehicleMaster.findMany({ where: { tenantId } });
|
||||||
const masterMap = new Map(masters.map((m) => [m.kennzeichen, m]));
|
// Normalize keys: DKV PDF may omit hyphens or use spaces ("GP JL 740E" vs "GP-JL 740E")
|
||||||
|
const masterMap = new Map(masters.map((m) => [_normalizeKennzeichen(m.kennzeichen), m]));
|
||||||
|
|
||||||
const rows: { lieferdatum: string; fahrzeug: string; fahrer: string; ort: string; kilometerstand: number }[] = [];
|
const rows: { lieferdatum: string; fahrzeug: string; fahrer: string; ort: string; kilometerstand: number | null }[] = [];
|
||||||
|
|
||||||
for (const vehicle of vehicles) {
|
for (const vehicle of vehicles) {
|
||||||
const master = masterMap.get(vehicle.kennzeichen);
|
const master = masterMap.get(_normalizeKennzeichen(vehicle.kennzeichen));
|
||||||
|
|
||||||
// For unknown plates: use empty string values for unknown fields
|
// For unknown plates: use empty string values for unknown fields
|
||||||
const vehicleForFormat = {
|
const vehicleForFormat = {
|
||||||
@@ -601,12 +602,20 @@ export class DkvService {
|
|||||||
const fahrer = master?.fahrer ?? '';
|
const fahrer = master?.fahrer ?? '';
|
||||||
|
|
||||||
for (const tx of vehicle.transactions) {
|
for (const tx of vehicle.transactions) {
|
||||||
|
// Only keep km values that are plausible odometer readings (whole numbers ≥ 0).
|
||||||
|
// Decimal values indicate a misaligned column (e.g. kWh or EUR from EV charging rows).
|
||||||
|
const rawKm = tx.kilometerstand;
|
||||||
|
const km: number | null =
|
||||||
|
Number.isNaN(rawKm) || !Number.isFinite(rawKm) || !Number.isInteger(rawKm)
|
||||||
|
? null
|
||||||
|
: rawKm;
|
||||||
|
|
||||||
rows.push({
|
rows.push({
|
||||||
lieferdatum: tx.lieferdatum,
|
lieferdatum: tx.lieferdatum,
|
||||||
fahrzeug,
|
fahrzeug,
|
||||||
fahrer,
|
fahrer,
|
||||||
ort: tx.ort,
|
ort: tx.ort,
|
||||||
kilometerstand: tx.kilometerstand,
|
kilometerstand: km,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -684,3 +693,12 @@ function _parseVehicleCsv(
|
|||||||
function _delay(ms: number): Promise<void> {
|
function _delay(ms: number): Promise<void> {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize a Kennzeichen for fuzzy vehicle lookup.
|
||||||
|
* DKV PDF extraction may omit hyphens or use spaces as separators.
|
||||||
|
* Example: "GP JL 740E" == "GP-JL 740E" == "GPJL740E" after normalization.
|
||||||
|
*/
|
||||||
|
function _normalizeKennzeichen(k: string): string {
|
||||||
|
return k.toUpperCase().replace(/[\s\-\.]/g, '');
|
||||||
|
}
|
||||||
|
|||||||
@@ -98,6 +98,6 @@ export interface ExportRow {
|
|||||||
fahrer: string;
|
fahrer: string;
|
||||||
/** Service station city from PDF */
|
/** Service station city from PDF */
|
||||||
ort: string;
|
ort: string;
|
||||||
/** Numeric odometer reading */
|
/** Numeric odometer reading; null when not available (e.g. EV charging rows) */
|
||||||
kilometerstand: number;
|
kilometerstand: number | null;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user