u
This commit is contained in:
@@ -91,6 +91,28 @@ export function parseCsvRecords(text) {
|
||||
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
|
||||
* 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...`);
|
||||
onProgress('variants', 0, variantChildren.length, 'Linking variants');
|
||||
|
||||
// Ensure a default attribute (e.g. "Variante") exists for variant parents
|
||||
let defaultVariantAttrId = null;
|
||||
// Fetch existing attribute names for this company
|
||||
const attributeMap = new Map(); // lowercase name -> attributeId
|
||||
try {
|
||||
const attrList = await store.attributename.list(googleAuthId);
|
||||
const existingAttr = attrList.find(
|
||||
(a) => (a.name || '').trim().toLowerCase() === 'variante'
|
||||
);
|
||||
if (existingAttr) {
|
||||
defaultVariantAttrId = existingAttr.id;
|
||||
} else {
|
||||
const createdAttr = await store.attributename.create(googleAuthId, {
|
||||
name: 'Variante',
|
||||
exclusive: true,
|
||||
});
|
||||
defaultVariantAttrId = createdAttr.id;
|
||||
for (const attr of attrList) {
|
||||
if (attr.name) {
|
||||
attributeMap.set(attr.name.trim().toLowerCase(), attr.id);
|
||||
}
|
||||
}
|
||||
} 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();
|
||||
for (const child of variantChildren) {
|
||||
const parentJtlId = String(child.kVaterArtikel || '').trim();
|
||||
@@ -576,15 +611,16 @@ export async function executeCompanyImport({
|
||||
if (!parentSku || !childSku || parentSku === childSku) continue;
|
||||
|
||||
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 processedCount = 0;
|
||||
|
||||
for (const [parentSku, children] of parentChildrenMap.entries()) {
|
||||
for (const [parentSku, { parentRow, children }] of parentChildrenMap.entries()) {
|
||||
let parentDetail = null;
|
||||
try {
|
||||
const res = await store.article.getBySku(googleAuthId, parentSku);
|
||||
@@ -595,16 +631,35 @@ export async function executeCompanyImport({
|
||||
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
|
||||
let parentAttrId = parentDetail?.parentAttributeNameId;
|
||||
if (!parentDetail?.isParent || !parentAttrId) {
|
||||
if (defaultVariantAttrId) {
|
||||
if (targetAttrId) {
|
||||
try {
|
||||
await store.article.toggleParent(googleAuthId, parentSku, {
|
||||
isParent: true,
|
||||
attributeNameId: defaultVariantAttrId,
|
||||
attributeNameId: targetAttrId,
|
||||
});
|
||||
parentAttrId = defaultVariantAttrId;
|
||||
parentAttrId = targetAttrId;
|
||||
} catch (err) {
|
||||
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) || '';
|
||||
} 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())) {
|
||||
val = `${val} (${childSku})`;
|
||||
}
|
||||
@@ -684,6 +750,144 @@ export async function executeCompanyImport({
|
||||
|
||||
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 {
|
||||
ok: true,
|
||||
message: 'Import erfolgreich abgeschlossen.',
|
||||
@@ -699,6 +903,9 @@ export async function executeCompanyImport({
|
||||
articlesUpdated,
|
||||
variantsLinked,
|
||||
articlePictures: totalImagesImported,
|
||||
ordersTotal: totalOrders,
|
||||
ordersImported,
|
||||
ordersSkipped,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user