This commit is contained in:
seb
2026-10-08 18:16:31 +02:00
parent bc323f2891
commit f4ae04af15

View File

@@ -91,6 +91,28 @@ export function parseCsvRecords(text) {
return records; return records;
} }
/**
* Normalizes common variant attribute names and typos from JTL exports.
*
* @param {string} name
* @returns {string}
*/
export function normalizeAttributeName(name) {
const clean = (name || '').trim();
if (!clean) return 'Variante';
const lower = clean.toLowerCase();
if (lower === 'menge' || lower === 'mange' || lower === 'meneg') return 'Menge';
if (lower === 'farbe') return 'Farbe';
if (lower === 'größe' || lower === 'grosse') return 'Größe';
if (lower === 'sorte') return 'Sorte';
if (lower === 'geschmack') return 'Geschmack';
if (lower === 'typ') return 'Typ';
if (lower === 'stück' || lower === 'stueck') return 'Stück';
if (lower === 'ausführung' || lower === 'ausfuehrung') return 'Ausführung';
if (lower === 'motiv') return 'Motiv';
return clean.charAt(0).toUpperCase() + clean.slice(1);
}
/** /**
* Extracts or derives a variant attribute value for a child article * Extracts or derives a variant attribute value for a child article
* based on its name relative to its parent name, or SKU suffixes. * based on its name relative to its parent name, or SKU suffixes.
@@ -547,27 +569,40 @@ export async function executeCompanyImport({
console.log(`[Import] Linking ${variantChildren.length} child variants to parents...`); console.log(`[Import] Linking ${variantChildren.length} child variants to parents...`);
onProgress('variants', 0, variantChildren.length, 'Linking variants'); onProgress('variants', 0, variantChildren.length, 'Linking variants');
// Ensure a default attribute (e.g. "Variante") exists for variant parents // Fetch existing attribute names for this company
let defaultVariantAttrId = null; const attributeMap = new Map(); // lowercase name -> attributeId
try { try {
const attrList = await store.attributename.list(googleAuthId); const attrList = await store.attributename.list(googleAuthId);
const existingAttr = attrList.find( for (const attr of attrList) {
(a) => (a.name || '').trim().toLowerCase() === 'variante' if (attr.name) {
); attributeMap.set(attr.name.trim().toLowerCase(), attr.id);
if (existingAttr) { }
defaultVariantAttrId = existingAttr.id;
} else {
const createdAttr = await store.attributename.create(googleAuthId, {
name: 'Variante',
exclusive: true,
});
defaultVariantAttrId = createdAttr.id;
} }
} catch (err) { } catch (err) {
log.warn('Could not ensure default "Variante" attribute:', err.message); log.warn('Could not list attributes:', err.message);
} }
// Group children by parent SKU // Helper to resolve or create an attribute name
const resolveAttributeId = async (rawName) => {
const normalized = normalizeAttributeName(rawName);
const key = normalized.toLowerCase();
if (attributeMap.has(key)) {
return attributeMap.get(key);
}
try {
const created = await store.attributename.create(googleAuthId, {
name: normalized,
exclusive: true,
});
attributeMap.set(key, created.id);
return created.id;
} catch (err) {
log.warn(`Could not create attribute "${normalized}":`, err.message);
return null;
}
};
// Group children by parent SKU and preserve parent row reference
const parentChildrenMap = new Map(); const parentChildrenMap = new Map();
for (const child of variantChildren) { for (const child of variantChildren) {
const parentJtlId = String(child.kVaterArtikel || '').trim(); const parentJtlId = String(child.kVaterArtikel || '').trim();
@@ -576,15 +611,16 @@ export async function executeCompanyImport({
if (!parentSku || !childSku || parentSku === childSku) continue; if (!parentSku || !childSku || parentSku === childSku) continue;
if (!parentChildrenMap.has(parentSku)) { if (!parentChildrenMap.has(parentSku)) {
parentChildrenMap.set(parentSku, []); const parentRow = includedArticles.find((a) => String(a.kArtikel) === parentJtlId) || null;
parentChildrenMap.set(parentSku, { parentRow, children: [] });
} }
parentChildrenMap.get(parentSku).push(child); parentChildrenMap.get(parentSku).children.push(child);
} }
let variantsLinked = 0; let variantsLinked = 0;
let processedCount = 0; let processedCount = 0;
for (const [parentSku, children] of parentChildrenMap.entries()) { for (const [parentSku, { parentRow, children }] of parentChildrenMap.entries()) {
let parentDetail = null; let parentDetail = null;
try { try {
const res = await store.article.getBySku(googleAuthId, parentSku); const res = await store.article.getBySku(googleAuthId, parentSku);
@@ -595,16 +631,35 @@ export async function executeCompanyImport({
continue; continue;
} }
// Determine variant attribute name from parent row or children variations
let rawAttrName = null;
try {
const pVars = JSON.parse(parentRow?.variations || '[]');
rawAttrName = pVars[0]?.cEigenschaftName?.trim() || null;
} catch {}
if (!rawAttrName) {
for (const child of children) {
try {
const cVars = JSON.parse(child.variations || '[]');
rawAttrName = cVars[0]?.cEigenschaftName?.trim() || null;
if (rawAttrName) break;
} catch {}
}
}
const targetAttrId = await resolveAttributeId(rawAttrName || 'Variante');
// Ensure parent has is_parent = 1 and parentattributename_id set // Ensure parent has is_parent = 1 and parentattributename_id set
let parentAttrId = parentDetail?.parentAttributeNameId; let parentAttrId = parentDetail?.parentAttributeNameId;
if (!parentDetail?.isParent || !parentAttrId) { if (!parentDetail?.isParent || !parentAttrId) {
if (defaultVariantAttrId) { if (targetAttrId) {
try { try {
await store.article.toggleParent(googleAuthId, parentSku, { await store.article.toggleParent(googleAuthId, parentSku, {
isParent: true, isParent: true,
attributeNameId: defaultVariantAttrId, attributeNameId: targetAttrId,
}); });
parentAttrId = defaultVariantAttrId; parentAttrId = targetAttrId;
} catch (err) { } catch (err) {
log.warn(`[Import] Could not toggle parent for ${parentSku}:`, err.message); log.warn(`[Import] Could not toggle parent for ${parentSku}:`, err.message);
} }
@@ -623,7 +678,18 @@ export async function executeCompanyImport({
childName = (Array.isArray(desc) ? desc[0]?.cName : desc?.cName) || ''; childName = (Array.isArray(desc) ? desc[0]?.cName : desc?.cName) || '';
} catch {} } catch {}
let val = extractVariantValue(parentName, childName, childSku, parentSku); // Determine attribute value from child variations, falling back to name derivation
let val = null;
try {
const cVars = JSON.parse(child.variations || '[]');
val = cVars[0]?.selectedValues?.[0]?.cEigenschaftWertName?.trim() ||
cVars[0]?.variationValues?.[0]?.cEigenschaftWertName?.trim() || null;
} catch {}
if (!val) {
val = extractVariantValue(parentName, childName, childSku, parentSku);
}
if (usedValues.has(val.toLowerCase())) { if (usedValues.has(val.toLowerCase())) {
val = `${val} (${childSku})`; val = `${val} (${childSku})`;
} }
@@ -684,6 +750,144 @@ export async function executeCompanyImport({
console.log(`[Import] Completed article pictures: ${totalImagesImported} images processed`); console.log(`[Import] Completed article pictures: ${totalImagesImported} images processed`);
// ─── Phase 5: Orders (Manifests) Import ──────────────────────────────────
const manCsvPath = path.join(importDir, 'manifests.csv');
const itemsCsvPath = path.join(importDir, 'manifestitems.csv');
let ordersImported = 0;
let ordersSkipped = 0;
let totalOrders = 0;
if (fs.existsSync(manCsvPath) && fs.existsSync(itemsCsvPath)) {
const manRecords = parseCsvRecords(fs.readFileSync(manCsvPath, 'utf8'));
const itemRecords = parseCsvRecords(fs.readFileSync(itemsCsvPath, 'utf8'));
totalOrders = manRecords.length;
console.log(`[Import] Manifests found: ${totalOrders} orders, ${itemRecords.length} items`);
onProgress('orders', 0, totalOrders, 'Starting orders import');
// Group line items by kAuftrag
const itemsByAuftrag = new Map();
for (const it of itemRecords) {
const kAuftrag = String(it.kAuftrag || '').trim();
if (!kAuftrag) continue;
if (!itemsByAuftrag.has(kAuftrag)) {
itemsByAuftrag.set(kAuftrag, []);
}
itemsByAuftrag.get(kAuftrag).push(it);
}
// Pre-fetch existing orders to make import idempotent
const existingOrders = await store.order.list(googleAuthId);
const existingOrderNumbers = new Set(
existingOrders.map((o) => (o.number || '').trim()).filter(Boolean)
);
// Pre-fetch customer groups and existing entities to resolve customer
let defaultCustomerGroupId = null;
try {
const groups = await store.customergroup.list(googleAuthId);
defaultCustomerGroupId = groups[0]?.id || null;
} catch {}
const entityMap = new Map(); // lowercase name -> entityId
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(`[Import] Could not create entity "${cleanName}":`, err.message);
return null;
}
};
for (let i = 0; i < totalOrders; i++) {
const m = manRecords[i];
const orderNumber = String(m.cAuftragsNr || '').trim();
if (!orderNumber) continue;
if (existingOrderNumbers.has(orderNumber)) {
ordersSkipped++;
if (i % 50 === 0 || i === totalOrders - 1) {
onProgress('orders', i + 1, totalOrders, `${orderNumber} (skipped)`);
}
continue;
}
const rawItems = itemsByAuftrag.get(String(m.kAuftrag || '').trim()) || [];
if (rawItems.length === 0) {
ordersSkipped++;
continue;
}
const lines = rawItems.map((it) => {
const sku = (it.cArtNr || '').trim() || null;
const name = (it.cName || it.cNameStandard || '').trim() || (sku ? `Artikel ${sku}` : 'Artikel');
const amount = parseFloat(String(it.fAnzahl || '1').replace(',', '.')) || 1;
const net = parseFloat(String(it.fVKNetto || '0').replace(',', '.')) || 0;
const taxrate = parseFloat(String(it.fMwSt || '19').replace(',', '.')) || 0;
const rebate = parseFloat(String(it.fRabatt || '0').replace(',', '.')) || 0;
const comment = (it.cHinweis || '').trim() || null;
return {
sku,
name,
amount,
net,
taxrate,
rebate,
comment,
};
});
const custName = (m.cName || '').trim() || 'Laufkunde';
const custFirstName = (m.cVorname || '').trim();
const entityId = await resolveEntityId(custName, custFirstName);
const dateStr = (m.dErstellt || '').trim();
const parsedDate = dateStr ? new Date(dateStr) : new Date();
const validDate = !Number.isNaN(parsedDate.getTime()) ? parsedDate.toISOString() : new Date().toISOString();
try {
await store.order.create(googleAuthId, {
number: orderNumber,
externalNumber: (m.cExterneAuftragsnummer || '').trim() || null,
date: validDate,
createdAt: validDate,
entityId: entityId || undefined,
lines,
});
existingOrderNumbers.add(orderNumber);
ordersImported++;
} catch (err) {
console.warn(`[Import] Failed to create order ${orderNumber}:`, err.message);
}
if (i % 50 === 0 || i === totalOrders - 1) {
onProgress('orders', i + 1, totalOrders, orderNumber);
}
}
console.log(`[Import] Orders import complete: ${ordersImported} created, ${ordersSkipped} skipped`);
}
return { return {
ok: true, ok: true,
message: 'Import erfolgreich abgeschlossen.', message: 'Import erfolgreich abgeschlossen.',
@@ -699,6 +903,9 @@ export async function executeCompanyImport({
articlesUpdated, articlesUpdated,
variantsLinked, variantsLinked,
articlePictures: totalImagesImported, articlePictures: totalImagesImported,
ordersTotal: totalOrders,
ordersImported,
ordersSkipped,
}, },
}; };
} }