diff --git a/src/services/companyImport.js b/src/services/companyImport.js index acd8e97..c824e10 100644 --- a/src/services/companyImport.js +++ b/src/services/companyImport.js @@ -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, }, }; }