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

@@ -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(';'));

View File

@@ -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,

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