This commit is contained in:
seb
2026-10-18 03:25:03 +02:00
parent 04943fb7a6
commit a01b42b24b
4 changed files with 155 additions and 3 deletions

View File

@@ -1,7 +1,7 @@
// MARK: - Imports
// #region Imports
import fs from 'fs';
import { cleanValue, parseGermanFloat } from './csvParser.js';
import { cleanValue, parseGermanFloat, parseSuppliers } from './csvParser.js';
import { setupVariantAttributes } from './variantImporter.js';
import { setupPfandArticles, linkArticlePfand, extractPfandValue } from './pfandImporter.js';
import { computeSkeleHash } from './imageHasher.js';
@@ -70,6 +70,34 @@ export async function importArticles({
// 3. Setup variant attributes resolver
const variantSetup = await setupVariantAttributes({ store, googleAuthId });
// 3b. Setup suppliers lookup and helper
const supplierByName = new Map();
try {
const existingSuppliers = await store.supplier.list(googleAuthId);
for (const s of existingSuppliers || []) {
if (s.name) supplierByName.set(s.name.trim().toLowerCase(), s);
}
} catch (sErr) {
log.warn('Could not list suppliers upfront:', sErr.message);
}
async function getOrCreateSupplier(name) {
const cleaned = (name || '').trim();
if (!cleaned) return null;
const key = cleaned.toLowerCase();
if (supplierByName.has(key)) {
return supplierByName.get(key);
}
try {
const created = await store.supplier.create(googleAuthId, { name: cleaned });
supplierByName.set(key, created);
return created;
} catch (err) {
log.warn(`Could not create supplier "${cleaned}":`, err.message);
return null;
}
}
// 4. Pre-scan articles to identify parents and map parent Pfand / parent rows
const parentJtlIds = new Set();
const parentPfandMap = new Map();
@@ -124,6 +152,7 @@ export async function importArticles({
let articlesUpdated = 0;
let variantsLinked = 0;
let pfandOptionsLinked = 0;
let suppliersLinked = 0;
let articlePicturesImported = 0;
// 6. Unified single loop over all articles
@@ -344,6 +373,82 @@ export async function importArticles({
}
}
// --- Suppliers handling (only for non-parents) ---
if (!isParent) {
const rawSuppliers = cleanValue(a.supplieer ?? a.supplier);
const parsedSuppliers = parseSuppliers(rawSuppliers);
if (parsedSuppliers.length > 0) {
const existingByEntityId = new Map();
for (const asup of (existingArticle?.suppliers || [])) {
if (asup.entityId != null) {
existingByEntityId.set(asup.entityId, asup);
}
}
const assignedIds = [];
for (let i = 0; i < parsedSuppliers.length; i++) {
const sup = parsedSuppliers[i];
try {
const supplierEntity = await getOrCreateSupplier(sup.supplierName);
if (!supplierEntity?.id) continue;
const pu = sup.vpe > 0 ? sup.vpe : 1;
const netPrice = sup.netPurchasePrice;
const taxRate = sup.taxRate;
const sSku = sup.supplierSku || null;
const existingAssoc = existingByEntityId.get(supplierEntity.id);
if (existingAssoc) {
await store.articlesupplier.update(googleAuthId, sku, existingAssoc.id, {
sku: sSku,
pu,
netPrice,
taxRate,
});
assignedIds.push(existingAssoc.id);
} else {
const created = await store.articlesupplier.create(googleAuthId, sku, {
entityId: supplierEntity.id,
sku: sSku,
pu,
netPrice,
taxRate,
});
assignedIds.push(created.id);
}
} catch (supErr) {
log.warn(`Failed to process supplier "${sup.supplierName}" for ${sku}:`, supErr.message);
}
}
// Preserve primary / secondary ordering
if (assignedIds.length > 0) {
try {
const allExisting = new Map();
for (const asup of (existingArticle?.suppliers || [])) {
allExisting.set(asup.id, asup);
}
for (const id of assignedIds) {
allExisting.set(id, true);
}
const orderIds = [...assignedIds];
for (const id of allExisting.keys()) {
if (!orderIds.includes(id)) {
orderIds.push(id);
}
}
if (orderIds.length > 1) {
await store.articlesupplier.order(googleAuthId, sku, { ids: orderIds });
}
suppliersLinked += assignedIds.length;
} catch (orderErr) {
log.warn(`Failed to order suppliers for ${sku}:`, orderErr.message);
}
}
}
}
// --- Pictures handling for this article ---
const imagesForThisArt = articleImagesBySku.get(sku);
if (imagesForThisArt && imagesForThisArt.length > 0) {
@@ -369,7 +474,7 @@ export async function importArticles({
progressTracker.flushSkeleHashes();
progressTracker.emitProgress(true);
log.info(`Articles complete: New: ${articlesImported}, Updated: ${articlesUpdated}, Variants: ${variantsLinked}, Pfand: ${pfandOptionsLinked}, Pictures: ${articlePicturesImported}`);
log.info(`Articles complete: New: ${articlesImported}, Updated: ${articlesUpdated}, Variants: ${variantsLinked}, Pfand: ${pfandOptionsLinked}, Suppliers: ${suppliersLinked}, Pictures: ${articlePicturesImported}`);
return {
jtlArtIdToSku,
@@ -381,6 +486,7 @@ export async function importArticles({
pfandArticlesCreated: pfandSetup.pfandArticlesCreated,
pfandArticlesUpdated: pfandSetup.pfandArticlesUpdated,
pfandOptionsLinked,
suppliersLinked,
totalImagesImported: articlePicturesImported,
};
}

View File

@@ -125,4 +125,31 @@ export function parseCsvRecords(text) {
return records;
}
/**
* Parses the pipe/colon delimited supplieer column into structured supplier objects.
* Format: <supplier_name>:<supplier_sku>:<vpe>:<tax_rate>:<net_purchase_price>
* Delimited by '|' between suppliers and ':' between fields.
* The first entry is always the primary / standard supplier.
*
* @param {string} cellValue
* @returns {Array<{ isPrimary: boolean, supplierName: string, supplierSku: string, vpe: number, taxRate: number, netPurchasePrice: number }>}
*/
export function parseSuppliers(cellValue) {
if (!cellValue || typeof cellValue !== 'string' || cellValue.trim() === '') return [];
return cellValue.split('|').map((entry, index) => {
const [supplierName, supplierSku, vpe, taxRate, netPrice] = entry.split(':');
return {
isPrimary: index === 0, // First entry is always the standard supplier
supplierName: (supplierName || '').trim(),
supplierSku: (supplierSku || '').trim(),
vpe: Number(vpe) || 0,
taxRate: Number(taxRate) || 0,
netPurchasePrice: Number(netPrice) || 0,
};
}).filter((s) => s.supplierName);
}
// #endregion