u
This commit is contained in:
@@ -358,6 +358,7 @@ export async function executeCompanyExport({
|
||||
'"attributes"',
|
||||
'"brandPictureFiles"',
|
||||
'"pictureFiles"',
|
||||
'"supplieer"',
|
||||
'"desc"',
|
||||
];
|
||||
const articleCsvRows = [articleCsvHeaders.join(';')];
|
||||
@@ -415,6 +416,21 @@ export async function executeCompanyExport({
|
||||
// Categories
|
||||
const catStr = Array.isArray(a.categoryIds) ? a.categoryIds.join(';') : '';
|
||||
|
||||
// Suppliers: <supplier_name>:<supplier_sku>:<vpe>:<tax_rate>:<net_purchase_price> separated by |
|
||||
let supplierStr = '';
|
||||
if (Array.isArray(a.suppliers) && a.suppliers.length > 0) {
|
||||
supplierStr = a.suppliers
|
||||
.map((s) => {
|
||||
const sName = String(s.supplierName || '').replace(/[:|]/g, ' ').trim();
|
||||
const sSku = String(s.sku || '').replace(/[:|]/g, ' ').trim();
|
||||
const vpe = (s.pu != null && Number(s.pu) > 1) ? Number(s.pu) : 0;
|
||||
const sTaxRate = s.price?.taxRate != null ? Number(s.price.taxRate) : 0;
|
||||
const netPrice = Number(s.price?.net || 0).toFixed(2);
|
||||
return `${sName}:${sSku}:${vpe}:${sTaxRate}:${netPrice}`;
|
||||
})
|
||||
.join('|');
|
||||
}
|
||||
|
||||
const row = [
|
||||
formatCsvField(a.id),
|
||||
formatCsvStringFormula(a.sku),
|
||||
@@ -436,6 +452,7 @@ export async function executeCompanyExport({
|
||||
formatCsvField(pfandAttr),
|
||||
formatCsvField(brandPicRel),
|
||||
formatCsvField(picPaths.join(';')),
|
||||
formatCsvField(supplierStr),
|
||||
formatCsvField(a.desc || ''),
|
||||
];
|
||||
articleCsvRows.push(row.join(';'));
|
||||
|
||||
@@ -5,7 +5,7 @@ import path from 'path';
|
||||
import { getStore } from '../db/store.js';
|
||||
import { createLogger } from '../util/logger.js';
|
||||
|
||||
import { cleanValue, parseGermanFloat, parseCsvRecords } from './companyImport/csvParser.js';
|
||||
import { cleanValue, parseGermanFloat, parseCsvRecords, parseSuppliers } from './companyImport/csvParser.js';
|
||||
import { computeSkeleHash } from './companyImport/imageHasher.js';
|
||||
import { normalizeAttributeName, extractVariantValue } from './companyImport/variantHelpers.js';
|
||||
import { createProgressTracker } from './companyImport/progressTracker.js';
|
||||
@@ -25,6 +25,7 @@ export {
|
||||
cleanValue,
|
||||
parseGermanFloat,
|
||||
parseCsvRecords,
|
||||
parseSuppliers,
|
||||
computeSkeleHash,
|
||||
normalizeAttributeName,
|
||||
extractVariantValue,
|
||||
@@ -208,6 +209,7 @@ export async function executeCompanyImport({
|
||||
pfandArticlesCreated: articlesResult.pfandArticlesCreated,
|
||||
pfandArticlesUpdated: articlesResult.pfandArticlesUpdated,
|
||||
pfandOptionsLinked: articlesResult.pfandOptionsLinked,
|
||||
suppliersLinked: articlesResult.suppliersLinked || 0,
|
||||
articlePictures: articlesResult.totalImagesImported,
|
||||
ordersTotal: totalReceiptsCount,
|
||||
ordersImported: ordersResult.ordersImported,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user