u
This commit is contained in:
@@ -52,6 +52,8 @@ const STORE_NAMESPACES = [
|
|||||||
'payment',
|
'payment',
|
||||||
'shipping',
|
'shipping',
|
||||||
'package',
|
'package',
|
||||||
|
'articlelist',
|
||||||
|
'optionslist',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { importCategories } from './companyImport/categoryImporter.js';
|
|||||||
import { importBrands } from './companyImport/brandImporter.js';
|
import { importBrands } from './companyImport/brandImporter.js';
|
||||||
import { importArticles } from './companyImport/articleImporter.js';
|
import { importArticles } from './companyImport/articleImporter.js';
|
||||||
import { importVariants } from './companyImport/variantImporter.js';
|
import { importVariants } from './companyImport/variantImporter.js';
|
||||||
|
import { importPfand, extractPfandValue, formatPfandSku, formatPfandName } from './companyImport/pfandImporter.js';
|
||||||
import { importArticleImages } from './companyImport/articleImageImporter.js';
|
import { importArticleImages } from './companyImport/articleImageImporter.js';
|
||||||
import { importOrders } from './companyImport/orderImporter.js';
|
import { importOrders } from './companyImport/orderImporter.js';
|
||||||
// #endregion
|
// #endregion
|
||||||
@@ -27,6 +28,10 @@ export {
|
|||||||
computeSkeleHash,
|
computeSkeleHash,
|
||||||
normalizeAttributeName,
|
normalizeAttributeName,
|
||||||
extractVariantValue,
|
extractVariantValue,
|
||||||
|
extractPfandValue,
|
||||||
|
formatPfandSku,
|
||||||
|
formatPfandName,
|
||||||
|
importPfand,
|
||||||
};
|
};
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
@@ -151,8 +156,19 @@ export async function executeCompanyImport({
|
|||||||
});
|
});
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
// MARK: - Phase 5: Article Pictures
|
// MARK: - Phase 5: Pfand / Deposit Options
|
||||||
// #region Phase 5: Article Pictures
|
// #region Phase 5: Pfand
|
||||||
|
const pfandResult = await importPfand({
|
||||||
|
store,
|
||||||
|
googleAuthId,
|
||||||
|
includedArticles,
|
||||||
|
hauptDashId: categoriesResult.hauptDashId,
|
||||||
|
parentSkus: variantsResult.parentSkus,
|
||||||
|
});
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// MARK: - Phase 6: Article Pictures
|
||||||
|
// #region Phase 6: Article Pictures
|
||||||
const imagesResult = await importArticleImages({
|
const imagesResult = await importArticleImages({
|
||||||
store,
|
store,
|
||||||
googleAuthId,
|
googleAuthId,
|
||||||
@@ -161,8 +177,8 @@ export async function executeCompanyImport({
|
|||||||
});
|
});
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
// MARK: - Phase 6: Orders / Receipts
|
// MARK: - Phase 7: Orders / Receipts
|
||||||
// #region Phase 6: Orders / Receipts
|
// #region Phase 7: Orders / Receipts
|
||||||
const ordersResult = await importOrders({
|
const ordersResult = await importOrders({
|
||||||
store,
|
store,
|
||||||
googleAuthId,
|
googleAuthId,
|
||||||
@@ -190,6 +206,9 @@ export async function executeCompanyImport({
|
|||||||
articlesImported: articlesResult.articlesImported,
|
articlesImported: articlesResult.articlesImported,
|
||||||
articlesUpdated: articlesResult.articlesUpdated,
|
articlesUpdated: articlesResult.articlesUpdated,
|
||||||
variantsLinked: variantsResult.variantsLinked,
|
variantsLinked: variantsResult.variantsLinked,
|
||||||
|
pfandArticlesCreated: pfandResult.pfandArticlesCreated,
|
||||||
|
pfandArticlesUpdated: pfandResult.pfandArticlesUpdated,
|
||||||
|
pfandOptionsLinked: pfandResult.pfandOptionsLinked,
|
||||||
articlePictures: imagesResult.totalImagesImported,
|
articlePictures: imagesResult.totalImagesImported,
|
||||||
ordersTotal: totalReceiptsCount,
|
ordersTotal: totalReceiptsCount,
|
||||||
ordersImported: ordersResult.ordersImported,
|
ordersImported: ordersResult.ordersImported,
|
||||||
|
|||||||
241
src/services/companyImport/pfandImporter.js
Normal file
241
src/services/companyImport/pfandImporter.js
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
// MARK: - Imports
|
||||||
|
// #region Imports
|
||||||
|
import { cleanValue } from './csvParser.js';
|
||||||
|
import { createLogger } from '../../util/logger.js';
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
const log = createLogger('import');
|
||||||
|
|
||||||
|
const PFAND_REGEX = /pfand\s*:\s*([0-9]+(?:[.,][0-9]+)?)/i;
|
||||||
|
|
||||||
|
// MARK: - Helpers
|
||||||
|
// #region Helpers
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the Pfand deposit value from an attributes string.
|
||||||
|
* E.g. "pfand:0.08" -> 0.08, "pfand:0,25" -> 0.25.
|
||||||
|
*
|
||||||
|
* @param {string} attrStr
|
||||||
|
* @returns {number|null}
|
||||||
|
*/
|
||||||
|
export function extractPfandValue(attrStr) {
|
||||||
|
if (!attrStr) return null;
|
||||||
|
const match = String(attrStr).match(PFAND_REGEX);
|
||||||
|
if (!match) return null;
|
||||||
|
const num = parseFloat(match[1].replace(',', '.'));
|
||||||
|
return Number.isFinite(num) && num > 0 ? num : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats standard Pfand SKU: e.g. PFAND-0.08, PFAND-0.15, PFAND-0.25.
|
||||||
|
*
|
||||||
|
* @param {number} val
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function formatPfandSku(val) {
|
||||||
|
return `PFAND-${Number(val).toFixed(2)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats standard Pfand article name: e.g. "Pfand 0,08 €", "Pfand 0,15 €", "Pfand 0,25 €".
|
||||||
|
*
|
||||||
|
* @param {number} val
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function formatPfandName(val) {
|
||||||
|
return `Pfand ${Number(val).toFixed(2).replace('.', ',')} €`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// MARK: - Pfand Importer
|
||||||
|
// #region Pfand Importer
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Imports Pfand deposit articles and links them as mandatory options to beverages/articles.
|
||||||
|
*
|
||||||
|
* @param {object} params
|
||||||
|
* @param {object} params.store - Store singleton
|
||||||
|
* @param {number} params.googleAuthId
|
||||||
|
* @param {Array<Record<string, string>>} params.includedArticles
|
||||||
|
* @param {number} params.hauptDashId
|
||||||
|
* @param {Set<string>} [params.parentSkus] - Set of SKUs that are parent articles (which cannot have options)
|
||||||
|
* @returns {Promise<{ pfandArticlesCreated: number, pfandArticlesUpdated: number, pfandOptionsLinked: number }>}
|
||||||
|
*/
|
||||||
|
export async function importPfand({
|
||||||
|
store,
|
||||||
|
googleAuthId,
|
||||||
|
includedArticles,
|
||||||
|
hauptDashId,
|
||||||
|
parentSkus = new Set(),
|
||||||
|
}) {
|
||||||
|
// 1. Identify parent articles and map their Pfand values so child variants can inherit
|
||||||
|
const parentPfandMap = new Map();
|
||||||
|
for (const a of includedArticles) {
|
||||||
|
const sku = cleanValue(a.cArtNr);
|
||||||
|
const isParent = a.nIstVater === '1' || (sku && parentSkus.has(sku));
|
||||||
|
if (isParent) {
|
||||||
|
const val = extractPfandValue(a.attributes);
|
||||||
|
if (val != null) {
|
||||||
|
parentPfandMap.set(String(a.kArtikel), val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Discover target articles (purchasable standalone or variant items) and their Pfand values
|
||||||
|
const articleTargets = [];
|
||||||
|
for (const a of includedArticles) {
|
||||||
|
const sku = cleanValue(a.cArtNr);
|
||||||
|
if (!sku) continue;
|
||||||
|
|
||||||
|
const isParent = a.nIstVater === '1' || parentSkus.has(sku);
|
||||||
|
if (isParent) {
|
||||||
|
// Parent articles cannot have options in Dash
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pfandVal = extractPfandValue(a.attributes);
|
||||||
|
if (pfandVal == null && a.kVaterArtikel && a.kVaterArtikel !== '0') {
|
||||||
|
if (parentPfandMap.has(String(a.kVaterArtikel))) {
|
||||||
|
pfandVal = parentPfandMap.get(String(a.kVaterArtikel));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pfandVal != null) {
|
||||||
|
articleTargets.push({ sku, pfandVal });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (articleTargets.length === 0) {
|
||||||
|
log.info('No pfand attributes detected among articles.');
|
||||||
|
return {
|
||||||
|
pfandArticlesCreated: 0,
|
||||||
|
pfandArticlesUpdated: 0,
|
||||||
|
pfandOptionsLinked: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Find unique Pfand values
|
||||||
|
const uniqueValues = Array.from(new Set(articleTargets.map((t) => t.pfandVal))).sort((a, b) => a - b);
|
||||||
|
log.info(`Discovered ${articleTargets.length} articles with Pfand across ${uniqueValues.length} unique values: ${uniqueValues.join(', ')}`);
|
||||||
|
|
||||||
|
// 4. Resolve or create "Pfand" subcategory under "Haupt"
|
||||||
|
let pfandCatId = null;
|
||||||
|
try {
|
||||||
|
const treeResult = await store.category.getTreeForCompany(googleAuthId);
|
||||||
|
const existingTree = treeResult.tree || [];
|
||||||
|
const existingPfandCat = existingTree.find(
|
||||||
|
(c) => c.parentId === hauptDashId && String(c.name || '').trim().toLowerCase() === 'pfand'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingPfandCat) {
|
||||||
|
pfandCatId = existingPfandCat.id;
|
||||||
|
} else {
|
||||||
|
const created = await store.category.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: hauptDashId,
|
||||||
|
name: 'Pfand',
|
||||||
|
});
|
||||||
|
pfandCatId = created.id;
|
||||||
|
log.info(`Created "Pfand" category under Haupt (id: ${pfandCatId})`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
log.warn('Could not resolve or create Pfand category under Haupt, falling back to Haupt:', err.message);
|
||||||
|
pfandCatId = hauptDashId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Create or update Pfand articles
|
||||||
|
let pfandArticlesCreated = 0;
|
||||||
|
let pfandArticlesUpdated = 0;
|
||||||
|
|
||||||
|
for (const val of uniqueValues) {
|
||||||
|
const pfandSku = formatPfandSku(val);
|
||||||
|
const pfandName = formatPfandName(val);
|
||||||
|
|
||||||
|
let existingArticle = null;
|
||||||
|
try {
|
||||||
|
const res = await store.article.getBySku(googleAuthId, pfandSku);
|
||||||
|
existingArticle = res.article;
|
||||||
|
} catch {
|
||||||
|
existingArticle = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (existingArticle) {
|
||||||
|
await store.article.updateBySku(googleAuthId, pfandSku, {
|
||||||
|
name: pfandName,
|
||||||
|
track_stock: 0,
|
||||||
|
});
|
||||||
|
await store.article.setPricesBySku(googleAuthId, pfandSku, [
|
||||||
|
{ gross: val, tax: 19 },
|
||||||
|
]);
|
||||||
|
pfandArticlesUpdated++;
|
||||||
|
log.info(`Updated existing Pfand article: ${pfandSku} ("${pfandName}")`);
|
||||||
|
} else {
|
||||||
|
await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: pfandCatId,
|
||||||
|
name: pfandName,
|
||||||
|
sku: pfandSku,
|
||||||
|
track_stock: 0,
|
||||||
|
});
|
||||||
|
await store.article.setPricesBySku(googleAuthId, pfandSku, [
|
||||||
|
{ gross: val, tax: 19 },
|
||||||
|
]);
|
||||||
|
pfandArticlesCreated++;
|
||||||
|
log.info(`Created new Pfand article: ${pfandSku} ("${pfandName}")`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
log.warn(`Failed to create or update Pfand article ${pfandSku}:`, err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Link each target article to its Pfand article as a mandatory option
|
||||||
|
let pfandOptionsLinked = 0;
|
||||||
|
|
||||||
|
for (const { sku, pfandVal } of articleTargets) {
|
||||||
|
const pfandSku = formatPfandSku(pfandVal);
|
||||||
|
|
||||||
|
let existingOptions = [];
|
||||||
|
try {
|
||||||
|
existingOptions = (await store.article.getOptionslist(googleAuthId, sku)) || [];
|
||||||
|
} catch {
|
||||||
|
existingOptions = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out any existing PFAND options to prevent duplicates, keep any other options
|
||||||
|
const otherOptions = existingOptions.filter((o) => !o.sku?.startsWith('PFAND-'));
|
||||||
|
const newOptions = [
|
||||||
|
...otherOptions.map((o, idx) => ({
|
||||||
|
sku: o.sku,
|
||||||
|
articleId: o.articleId,
|
||||||
|
count: o.count || 1,
|
||||||
|
sort: idx,
|
||||||
|
mandated: Boolean(o.mandated),
|
||||||
|
})),
|
||||||
|
{
|
||||||
|
sku: pfandSku,
|
||||||
|
count: 1,
|
||||||
|
sort: otherOptions.length,
|
||||||
|
mandated: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
await store.article.setOptionslist(googleAuthId, sku, newOptions);
|
||||||
|
pfandOptionsLinked++;
|
||||||
|
} catch (err) {
|
||||||
|
log.warn(`Failed to link Pfand option ${pfandSku} to article ${sku}:`, err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info(`Pfand import complete: ${pfandArticlesCreated} created, ${pfandArticlesUpdated} updated, ${pfandOptionsLinked} linked as mandatory options.`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
pfandArticlesCreated,
|
||||||
|
pfandArticlesUpdated,
|
||||||
|
pfandOptionsLinked,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// #endregion
|
||||||
@@ -123,6 +123,7 @@ export async function importVariants({
|
|||||||
return {
|
return {
|
||||||
variantsLinked,
|
variantsLinked,
|
||||||
parentsCount: parentChildrenMap.size,
|
parentsCount: parentChildrenMap.size,
|
||||||
|
parentSkus: new Set(parentChildrenMap.keys()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user