Files
dashback/src/services/companyImport/orderImporter.js
2026-10-16 01:11:17 +02:00

321 lines
12 KiB
JavaScript

// MARK: - Imports
// #region Imports
import { cleanValue, parseGermanFloat } from './csvParser.js';
import { createLogger } from '../../util/logger.js';
// #endregion
const log = createLogger('import');
// MARK: - Order / Manifest Importer
// #region Order / Manifest Importer
/**
* Imports order manifests and line items, resolving customer entities, payments, and shippings.
*
* @param {object} params
* @param {object} params.store - Store singleton
* @param {number} params.googleAuthId
* @param {Array<Record<string, string>>} params.manRecords
* @param {Array<Record<string, string>>} params.itemRecords
* @param {Map<string, string>} [params.jtlArtIdToSku]
* @param {Set<string>} [params.parentSkus]
* @param {object} params.progressTracker
* @returns {Promise<{ ordersImported: number, ordersSkipped: number, paymentsImported: number, shippingsImported: number }>}
*/
export async function importOrders({
store,
googleAuthId,
manRecords,
itemRecords,
jtlArtIdToSku = new Map(),
parentSkus = new Set(),
progressTracker,
}) {
let ordersImported = 0;
let ordersSkipped = 0;
let paymentsImported = 0;
let shippingsImported = 0;
if (manRecords.length === 0 || itemRecords.length === 0) {
return { ordersImported, ordersSkipped, paymentsImported, shippingsImported };
}
const itemsByAuftrag = new Map();
for (const it of itemRecords) {
const kAuftrag = String(it.manifestid ?? it.kAuftrag ?? '').trim();
if (!kAuftrag) continue;
if (!itemsByAuftrag.has(kAuftrag)) {
itemsByAuftrag.set(kAuftrag, []);
}
itemsByAuftrag.get(kAuftrag).push(it);
}
const existingOrdersResult = await store.order.list(googleAuthId);
const existingOrderList = Array.isArray(existingOrdersResult)
? existingOrdersResult
: (existingOrdersResult?.orders || []);
const existingOrderNumbers = new Set(
existingOrderList.map((o) => (o.number || '').trim()).filter(Boolean)
);
let defaultCustomerGroupId = null;
try {
const groups = await store.customergroup.list(googleAuthId);
defaultCustomerGroupId = groups[0]?.id || null;
} catch {}
let defaultWarehouseId = null;
try {
const warehouses = await store.warehouse.list(googleAuthId);
if (warehouses && warehouses.length > 0) {
defaultWarehouseId = warehouses[0].id;
} else {
const wh = await store.warehouse.create(googleAuthId, { name: 'Standardlager' });
defaultWarehouseId = wh.id;
}
} catch (err) {
log.warn('Could not ensure warehouse:', err.message);
}
const entityMap = new Map();
try {
const entities = await store.entity.list(googleAuthId);
for (const ent of entities) {
if (ent.name) {
entityMap.set(ent.name.trim().toLowerCase(), ent.id);
}
}
} catch {}
const resolveEntityId = async (name, firstName) => {
const cleanName = (name || '').trim();
if (!cleanName || !defaultCustomerGroupId) return null;
const key = cleanName.toLowerCase();
if (entityMap.has(key)) return entityMap.get(key);
try {
const ent = await store.entity.create(googleAuthId, {
customergroupId: defaultCustomerGroupId,
name: cleanName,
firstName: (firstName || '').trim() || undefined,
});
entityMap.set(key, ent.id);
return ent.id;
} catch (err) {
log.warn(`Could not create entity "${cleanName}":`, err.message);
return null;
}
};
const accountMap = new Map();
try {
const accounts = await store.account.list(googleAuthId);
for (const acc of accounts) {
if (acc.name) {
accountMap.set(acc.name.trim().toLowerCase(), acc.id);
}
}
} catch (err) {
log.warn('Could not list accounts:', err.message);
}
const resolveAccountId = async (accountName) => {
const cleanAccName = (accountName || 'Bar').trim();
const key = cleanAccName.toLowerCase();
if (accountMap.has(key)) return accountMap.get(key);
try {
const created = await store.account.create(googleAuthId, {
type: cleanAccName,
name: cleanAccName,
});
accountMap.set(key, created.id);
return created.id;
} catch (err) {
log.warn(`Could not create account "${cleanAccName}":`, err.message);
return accountMap.values().next().value || null;
}
};
for (const m of manRecords) {
const orderNumber = cleanValue(m.nr ?? m.cAuftragsNr);
if (!orderNumber || existingOrderNumbers.has(orderNumber)) {
ordersSkipped++;
progressTracker.counts.receiptsdone++;
progressTracker.emitProgress();
continue;
}
const manifestKey = String(m.id ?? m.kAuftrag ?? '').trim();
const rawItems = itemsByAuftrag.get(manifestKey) || [];
if (rawItems.length === 0) {
ordersSkipped++;
progressTracker.counts.receiptsdone++;
progressTracker.emitProgress();
continue;
}
rawItems.sort((a, b) => (Number(a.sort ?? a.nSort) || 0) - (Number(b.sort ?? b.nSort) || 0));
const lines = rawItems.map((it) => {
const articleId = cleanValue(it.articleid ?? it.kArtikel);
let sku = cleanValue(it.sku ?? it.cArtNr) || (articleId && jtlArtIdToSku?.get?.(articleId)) || null;
if (sku && parentSkus.has(sku)) {
sku = null;
}
const rawName = cleanValue(it.name ?? it.cName ?? it.cNameStandard);
const name = rawName || (sku ? `Artikel ${sku}` : 'Artikel');
const amount = parseGermanFloat(it.amount ?? it.fAnzahl, 1);
const net = parseGermanFloat(it.net ?? it.fVKNetto, 0);
const taxrate = parseGermanFloat(it.taxrate ?? it.fMwSt, 19);
const rebate = parseGermanFloat(it.rebate ?? it.fRabatt, 0);
const comment = cleanValue(it.comment ?? it.cHinweis) || null;
return {
sku,
name,
amount,
net,
taxrate,
rebate,
comment,
};
});
const custName = cleanValue(m.name ?? m.cName) || 'Laufkunde';
const custFirstName = cleanValue(m.firstName ?? m.cVorname);
const entityId = await resolveEntityId(custName, custFirstName);
const dateStr = cleanValue(m.created_at ?? m.dErstellt);
const parsedDate = dateStr ? new Date(dateStr) : new Date();
const validDate = !Number.isNaN(parsedDate.getTime()) ? parsedDate.toISOString() : new Date().toISOString();
let createdOrder = null;
try {
createdOrder = await store.order.create(googleAuthId, {
number: orderNumber,
externalNumber: cleanValue(m.extnr ?? m.cExterneAuftragsnummer) || null,
date: validDate,
createdAt: validDate,
entityId: entityId || undefined,
lines,
});
existingOrderNumbers.add(orderNumber);
ordersImported++;
} catch (err) {
const parentMatch = err.message?.match(/A parent article cannot be booked on documents \(([^)]+)\)/);
if (parentMatch) {
const offendingSku = parentMatch[1];
parentSkus.add(offendingSku);
for (const l of lines) {
if (l.sku === offendingSku) l.sku = null;
}
try {
createdOrder = await store.order.create(googleAuthId, {
number: orderNumber,
externalNumber: cleanValue(m.extnr ?? m.cExterneAuftragsnummer) || null,
date: validDate,
createdAt: validDate,
entityId: entityId || undefined,
lines,
});
existingOrderNumbers.add(orderNumber);
ordersImported++;
} catch (retryErr) {
log.warn(`Failed to create order ${orderNumber} after stripping parent SKU ${offendingSku}:`, retryErr.message);
}
} else {
log.warn(`Failed to create order ${orderNumber}:`, err.message);
}
}
if (createdOrder) {
// Handle Payment
const isPayed = String(m.payed ?? '').trim() === '1';
const paymentType = cleanValue(m.paymentType);
if (isPayed && paymentType) {
const orderTotal = Math.round(
lines.reduce(
(sum, l) => sum + (l.amount * l.net * (1 - (l.rebate || 0) / 100) * (1 + (l.taxrate || 0) / 100)),
0
) * 100
) / 100;
if (orderTotal > 0) {
try {
const accountId = await resolveAccountId(paymentType);
if (accountId) {
await store.payment.create(googleAuthId, {
accountId,
manifestId: createdOrder.id,
type: paymentType,
amount: orderTotal,
});
paymentsImported++;
}
} catch (err) {
log.warn(`Failed to create payment for order ${orderNumber}:`, err.message);
}
}
}
// Handle Delivery / Shipping
const shippingType = cleanValue(m.shippingType);
const isShipped = String(m.shipped ?? '').trim() === '1';
if (isShipped || shippingType) {
const shippableLines = lines
.filter((l) => l.sku)
.map((l) => ({ sku: l.sku, amount: l.amount }));
if (shippableLines.length > 0) {
try {
const isSelbstabholer = (shippingType || '').trim().toLowerCase() === 'selbstabholer';
const deliveryState = isShipped ? 'delivered' : 'new';
const shipment = await store.shipment.create(googleAuthId, {
orderId: createdOrder.id,
date: validDate,
state: deliveryState,
lines: shippableLines,
});
if (!isSelbstabholer) {
const trackingCode = cleanValue(m.trackingCode ?? m.tracking_code ?? m.cTracking);
const carrier = cleanValue(m.carrier ?? m.cCarrier);
const shippingState = isShipped ? 'shipped' : 'draft';
await store.shipping.create(googleAuthId, {
manifestId: shipment.id,
type: shippingType || 'standard',
state: shippingState,
trackingCode: trackingCode || undefined,
carrier: carrier || undefined,
warehouseId: defaultWarehouseId || undefined,
});
shippingsImported++;
}
} catch (err) {
log.warn(`Failed to create shipment/shipping for order ${orderNumber}:`, err.message);
}
}
}
}
progressTracker.counts.receiptsdone++;
progressTracker.emitProgress();
}
progressTracker.emitProgress(true);
log.info(`Orders complete: ${ordersImported} created, ${ordersSkipped} skipped, ${paymentsImported} payments, ${shippingsImported} shippings`);
return {
ordersImported,
ordersSkipped,
paymentsImported,
shippingsImported,
};
}
// #endregion