u
This commit is contained in:
@@ -358,6 +358,7 @@ export async function executeCompanyExport({
|
|||||||
'"attributes"',
|
'"attributes"',
|
||||||
'"brandPictureFiles"',
|
'"brandPictureFiles"',
|
||||||
'"pictureFiles"',
|
'"pictureFiles"',
|
||||||
|
'"supplieer"',
|
||||||
'"desc"',
|
'"desc"',
|
||||||
];
|
];
|
||||||
const articleCsvRows = [articleCsvHeaders.join(';')];
|
const articleCsvRows = [articleCsvHeaders.join(';')];
|
||||||
@@ -415,6 +416,21 @@ export async function executeCompanyExport({
|
|||||||
// Categories
|
// Categories
|
||||||
const catStr = Array.isArray(a.categoryIds) ? a.categoryIds.join(';') : '';
|
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 = [
|
const row = [
|
||||||
formatCsvField(a.id),
|
formatCsvField(a.id),
|
||||||
formatCsvStringFormula(a.sku),
|
formatCsvStringFormula(a.sku),
|
||||||
@@ -436,6 +452,7 @@ export async function executeCompanyExport({
|
|||||||
formatCsvField(pfandAttr),
|
formatCsvField(pfandAttr),
|
||||||
formatCsvField(brandPicRel),
|
formatCsvField(brandPicRel),
|
||||||
formatCsvField(picPaths.join(';')),
|
formatCsvField(picPaths.join(';')),
|
||||||
|
formatCsvField(supplierStr),
|
||||||
formatCsvField(a.desc || ''),
|
formatCsvField(a.desc || ''),
|
||||||
];
|
];
|
||||||
articleCsvRows.push(row.join(';'));
|
articleCsvRows.push(row.join(';'));
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import path from 'path';
|
|||||||
import { getStore } from '../db/store.js';
|
import { getStore } from '../db/store.js';
|
||||||
import { createLogger } from '../util/logger.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 { computeSkeleHash } from './companyImport/imageHasher.js';
|
||||||
import { normalizeAttributeName, extractVariantValue } from './companyImport/variantHelpers.js';
|
import { normalizeAttributeName, extractVariantValue } from './companyImport/variantHelpers.js';
|
||||||
import { createProgressTracker } from './companyImport/progressTracker.js';
|
import { createProgressTracker } from './companyImport/progressTracker.js';
|
||||||
@@ -25,6 +25,7 @@ export {
|
|||||||
cleanValue,
|
cleanValue,
|
||||||
parseGermanFloat,
|
parseGermanFloat,
|
||||||
parseCsvRecords,
|
parseCsvRecords,
|
||||||
|
parseSuppliers,
|
||||||
computeSkeleHash,
|
computeSkeleHash,
|
||||||
normalizeAttributeName,
|
normalizeAttributeName,
|
||||||
extractVariantValue,
|
extractVariantValue,
|
||||||
@@ -208,6 +209,7 @@ export async function executeCompanyImport({
|
|||||||
pfandArticlesCreated: articlesResult.pfandArticlesCreated,
|
pfandArticlesCreated: articlesResult.pfandArticlesCreated,
|
||||||
pfandArticlesUpdated: articlesResult.pfandArticlesUpdated,
|
pfandArticlesUpdated: articlesResult.pfandArticlesUpdated,
|
||||||
pfandOptionsLinked: articlesResult.pfandOptionsLinked,
|
pfandOptionsLinked: articlesResult.pfandOptionsLinked,
|
||||||
|
suppliersLinked: articlesResult.suppliersLinked || 0,
|
||||||
articlePictures: articlesResult.totalImagesImported,
|
articlePictures: articlesResult.totalImagesImported,
|
||||||
ordersTotal: totalReceiptsCount,
|
ordersTotal: totalReceiptsCount,
|
||||||
ordersImported: ordersResult.ordersImported,
|
ordersImported: ordersResult.ordersImported,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// MARK: - Imports
|
// MARK: - Imports
|
||||||
// #region Imports
|
// #region Imports
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { cleanValue, parseGermanFloat } from './csvParser.js';
|
import { cleanValue, parseGermanFloat, parseSuppliers } from './csvParser.js';
|
||||||
import { setupVariantAttributes } from './variantImporter.js';
|
import { setupVariantAttributes } from './variantImporter.js';
|
||||||
import { setupPfandArticles, linkArticlePfand, extractPfandValue } from './pfandImporter.js';
|
import { setupPfandArticles, linkArticlePfand, extractPfandValue } from './pfandImporter.js';
|
||||||
import { computeSkeleHash } from './imageHasher.js';
|
import { computeSkeleHash } from './imageHasher.js';
|
||||||
@@ -70,6 +70,34 @@ export async function importArticles({
|
|||||||
// 3. Setup variant attributes resolver
|
// 3. Setup variant attributes resolver
|
||||||
const variantSetup = await setupVariantAttributes({ store, googleAuthId });
|
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
|
// 4. Pre-scan articles to identify parents and map parent Pfand / parent rows
|
||||||
const parentJtlIds = new Set();
|
const parentJtlIds = new Set();
|
||||||
const parentPfandMap = new Map();
|
const parentPfandMap = new Map();
|
||||||
@@ -124,6 +152,7 @@ export async function importArticles({
|
|||||||
let articlesUpdated = 0;
|
let articlesUpdated = 0;
|
||||||
let variantsLinked = 0;
|
let variantsLinked = 0;
|
||||||
let pfandOptionsLinked = 0;
|
let pfandOptionsLinked = 0;
|
||||||
|
let suppliersLinked = 0;
|
||||||
let articlePicturesImported = 0;
|
let articlePicturesImported = 0;
|
||||||
|
|
||||||
// 6. Unified single loop over all articles
|
// 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 ---
|
// --- Pictures handling for this article ---
|
||||||
const imagesForThisArt = articleImagesBySku.get(sku);
|
const imagesForThisArt = articleImagesBySku.get(sku);
|
||||||
if (imagesForThisArt && imagesForThisArt.length > 0) {
|
if (imagesForThisArt && imagesForThisArt.length > 0) {
|
||||||
@@ -369,7 +474,7 @@ export async function importArticles({
|
|||||||
|
|
||||||
progressTracker.flushSkeleHashes();
|
progressTracker.flushSkeleHashes();
|
||||||
progressTracker.emitProgress(true);
|
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 {
|
return {
|
||||||
jtlArtIdToSku,
|
jtlArtIdToSku,
|
||||||
@@ -381,6 +486,7 @@ export async function importArticles({
|
|||||||
pfandArticlesCreated: pfandSetup.pfandArticlesCreated,
|
pfandArticlesCreated: pfandSetup.pfandArticlesCreated,
|
||||||
pfandArticlesUpdated: pfandSetup.pfandArticlesUpdated,
|
pfandArticlesUpdated: pfandSetup.pfandArticlesUpdated,
|
||||||
pfandOptionsLinked,
|
pfandOptionsLinked,
|
||||||
|
suppliersLinked,
|
||||||
totalImagesImported: articlePicturesImported,
|
totalImagesImported: articlePicturesImported,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,4 +125,31 @@ export function parseCsvRecords(text) {
|
|||||||
return records;
|
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
|
// #endregion
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user