651 lines
22 KiB
JavaScript
651 lines
22 KiB
JavaScript
// MARK: - Imports
|
|
// #region Imports
|
|
import JSZip from 'jszip';
|
|
import { getStore, getProvider } from '../db/store.js';
|
|
import { createLogger } from '../util/logger.js';
|
|
// #endregion
|
|
|
|
const log = createLogger('export');
|
|
|
|
const UTF8_BOM = '\uFEFF';
|
|
|
|
// MARK: - CSV Helpers
|
|
// #region CSV Helpers
|
|
|
|
/**
|
|
* Formats a value as a double-quoted CSV field.
|
|
*
|
|
* @param {any} val
|
|
* @returns {string}
|
|
*/
|
|
function formatCsvField(val) {
|
|
if (val == null) return '""';
|
|
const str = String(val);
|
|
return `"${str.replace(/"/g, '""')}"`;
|
|
}
|
|
|
|
/**
|
|
* Formats a value as an Excel string formula (e.g. ="4270001588593") so that
|
|
* spreadsheet tools treat it strictly as text rather than converting to a number / scientific notation.
|
|
* Empty or null values return '""'.
|
|
*
|
|
* @param {any} val
|
|
* @returns {string}
|
|
*/
|
|
function formatCsvStringFormula(val) {
|
|
if (val == null) return '""';
|
|
const str = String(val).trim();
|
|
if (str === '') return '""';
|
|
return `="${str.replace(/"/g, '""')}"`;
|
|
}
|
|
|
|
/**
|
|
* Formats a number in German decimal format (e.g. 8.36 -> "8,36").
|
|
*
|
|
* @param {number|string|null} val
|
|
* @param {string} [fallback='0']
|
|
* @returns {string}
|
|
*/
|
|
function formatGermanFloat(val, fallback = '0') {
|
|
if (val == null || val === '') return fallback;
|
|
const num = Number(val);
|
|
if (Number.isNaN(num)) return fallback;
|
|
return String(num).replace('.', ',');
|
|
}
|
|
|
|
/**
|
|
* Extracts Pfand deposit amount from article options if present.
|
|
*
|
|
* @param {Array<object>} options
|
|
* @returns {string} e.g. "pfand:0.25" or ""
|
|
*/
|
|
function extractPfandAttribute(options) {
|
|
if (!Array.isArray(options) || options.length === 0) return '';
|
|
for (const opt of options) {
|
|
const sku = String(opt.sku || '').toUpperCase();
|
|
const match = sku.match(/PFAND[-_]?([0-9]+(?:[.,][0-9]+)?)/i);
|
|
if (match) {
|
|
const val = parseFloat(match[1].replace(',', '.'));
|
|
if (Number.isFinite(val) && val > 0) {
|
|
return `pfand:${val.toFixed(2)}`;
|
|
}
|
|
}
|
|
if (opt.name) {
|
|
const nameMatch = String(opt.name).match(/pfand\s*([0-9]+(?:[.,][0-9]+)?)/i);
|
|
if (nameMatch) {
|
|
const val = parseFloat(nameMatch[1].replace(',', '.'));
|
|
if (Number.isFinite(val) && val > 0) {
|
|
return `pfand:${val.toFixed(2)}`;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return '';
|
|
}
|
|
|
|
// #endregion
|
|
|
|
// MARK: - Concurrency Pool
|
|
// #region Concurrency Pool
|
|
|
|
/**
|
|
* Runs tasks with a controlled concurrency limit.
|
|
*
|
|
* @template T, R
|
|
* @param {Array<T>} items
|
|
* @param {number} concurrency
|
|
* @param {(item: T, index: number) => Promise<R>} fn
|
|
* @returns {Promise<Array<R>>}
|
|
*/
|
|
async function asyncPool(items, concurrency, fn) {
|
|
const results = new Array(items.length);
|
|
let index = 0;
|
|
|
|
const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
|
|
while (index < items.length) {
|
|
const current = index++;
|
|
results[current] = await fn(items[current], current);
|
|
}
|
|
});
|
|
|
|
await Promise.all(workers);
|
|
return results;
|
|
}
|
|
|
|
// #endregion
|
|
|
|
// MARK: - Company Export Orchestrator
|
|
// #region Company Export Orchestrator
|
|
|
|
/**
|
|
* Executes full catalog export from store into a round-trip compatible ZIP buffer.
|
|
*
|
|
* @param {object} opts
|
|
* @param {number} opts.googleAuthId
|
|
* @param {number} opts.companyId
|
|
* @param {function} [opts.onStatus] - (statusObj) => void
|
|
* @param {function} [opts.onProgress] - (counts) => void
|
|
* @returns {Promise<{ zipBuffer: Buffer, stats: object, filename: string }>}
|
|
*/
|
|
export async function executeCompanyExport({
|
|
googleAuthId,
|
|
companyId,
|
|
onStatus = () => {},
|
|
onProgress = () => {},
|
|
}) {
|
|
const store = await getStore();
|
|
const zip = new JSZip();
|
|
|
|
// 1. Categories
|
|
onStatus({ message: 'Lade Kategorien...' });
|
|
const treeResult = await store.category.getTreeForCompany(googleAuthId);
|
|
const categoryTree = treeResult.tree || [];
|
|
|
|
onProgress({
|
|
stage: 'categories',
|
|
categoriestotal: categoryTree.length,
|
|
categoriesdone: categoryTree.length,
|
|
percent: 10,
|
|
});
|
|
|
|
// 2. Brands
|
|
onStatus({ message: 'Lade Hersteller / Marken...' });
|
|
const brands = [];
|
|
let brandOffset = 0;
|
|
const brandLimit = 100;
|
|
while (true) {
|
|
const brandListRes = await store.brand.list(googleAuthId, { offset: brandOffset, limit: brandLimit });
|
|
const batch = brandListRes.brands || [];
|
|
brands.push(...batch);
|
|
brandOffset += batch.length;
|
|
if (batch.length < brandLimit || brandOffset >= (brandListRes.total ?? 0)) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
const brandById = new Map();
|
|
const brandByName = new Map();
|
|
for (const b of brands) {
|
|
brandById.set(b.id, b);
|
|
if (b.name) brandByName.set(b.name.trim().toLowerCase(), b);
|
|
}
|
|
|
|
onProgress({
|
|
stage: 'brands',
|
|
brandstotal: brands.length,
|
|
brandsdone: brands.length,
|
|
percent: 15,
|
|
});
|
|
|
|
// 3. Articles
|
|
onStatus({ message: 'Lade Artikelliste...' });
|
|
const exportArticles = await store.article.listExport(googleAuthId, null);
|
|
const totalArticles = exportArticles.length;
|
|
|
|
onProgress({
|
|
stage: 'articles',
|
|
articlestotal: totalArticles,
|
|
articlesdone: 0,
|
|
percent: 20,
|
|
});
|
|
|
|
onStatus({ message: `Lade Detaildaten für ${totalArticles} Artikel...` });
|
|
let articlesProcessed = 0;
|
|
const fullArticles = await asyncPool(exportArticles, 5, async (item) => {
|
|
try {
|
|
const detail = await store.article.getBySku(googleAuthId, item.sku);
|
|
articlesProcessed++;
|
|
if (articlesProcessed % 50 === 0 || articlesProcessed === totalArticles) {
|
|
onProgress({
|
|
stage: 'articles',
|
|
articlestotal: totalArticles,
|
|
articlesdone: articlesProcessed,
|
|
percent: 20 + Math.floor((articlesProcessed / totalArticles) * 40),
|
|
});
|
|
}
|
|
return detail.article;
|
|
} catch (err) {
|
|
log.warn(`Could not fetch details for article ${item.sku}:`, err.message);
|
|
articlesProcessed++;
|
|
return {
|
|
id: item.id,
|
|
sku: item.sku,
|
|
name: item.name,
|
|
net: item.net,
|
|
taxRate: item.taxRate,
|
|
track_stock: item.track_stock,
|
|
eans: item.eans || [],
|
|
categoryIds: item.categoryIds || [],
|
|
photos: (item.fullHashes || []).map((h) => ({ fullHash: h })),
|
|
brand: item.brandName ? { name: item.brandName } : null,
|
|
};
|
|
}
|
|
});
|
|
|
|
// Build parent-child lookup for variant linking
|
|
const articleBySku = new Map();
|
|
const articleById = new Map();
|
|
for (const a of fullArticles) {
|
|
if (a.sku) articleBySku.set(a.sku, a);
|
|
if (a.id) articleById.set(a.id, a);
|
|
}
|
|
|
|
// Populate attributeValue for children from parent's children array if needed
|
|
for (const a of fullArticles) {
|
|
if (a.isParent && Array.isArray(a.children)) {
|
|
for (const childRef of a.children) {
|
|
const childObj = articleBySku.get(childRef.sku) || articleById.get(childRef.id);
|
|
if (childObj && !childObj.attributeValue && childRef.attributeValue) {
|
|
childObj.attributeValue = childRef.attributeValue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 4. Collect Images
|
|
onStatus({ message: 'Sammle Bildreferenzen...' });
|
|
const uniqueHashes = new Set();
|
|
|
|
// Category pictures
|
|
for (const c of categoryTree) {
|
|
if (c.fullHash) uniqueHashes.add(c.fullHash);
|
|
}
|
|
|
|
// Brand logos
|
|
for (const b of brands) {
|
|
if (b.pics?.[0]?.fullHash) {
|
|
uniqueHashes.add(b.pics[0].fullHash);
|
|
}
|
|
}
|
|
|
|
// Article pictures
|
|
for (const a of fullArticles) {
|
|
for (const p of a.photos || []) {
|
|
if (p.fullHash) uniqueHashes.add(p.fullHash);
|
|
}
|
|
if (a.brand?.fullHash) {
|
|
uniqueHashes.add(a.brand.fullHash);
|
|
}
|
|
}
|
|
|
|
const hashList = Array.from(uniqueHashes);
|
|
const totalImages = hashList.length;
|
|
onProgress({
|
|
stage: 'images',
|
|
imagestotal: totalImages,
|
|
imagesdone: 0,
|
|
percent: 60,
|
|
});
|
|
|
|
onStatus({ message: `Lade ${totalImages} Bilddateien...` });
|
|
const hashToBuffer = new Map();
|
|
let imagesProcessed = 0;
|
|
|
|
await asyncPool(hashList, 5, async (fullHash) => {
|
|
try {
|
|
const pic = await store.pic.getImageByHash(googleAuthId, fullHash);
|
|
if (pic?.buffer) {
|
|
hashToBuffer.set(fullHash, pic.buffer);
|
|
}
|
|
} catch (err) {
|
|
log.warn(`Could not load image hash ${fullHash}:`, err.message);
|
|
}
|
|
imagesProcessed++;
|
|
if (imagesProcessed % 25 === 0 || imagesProcessed === totalImages) {
|
|
onProgress({
|
|
stage: 'images',
|
|
imagestotal: totalImages,
|
|
imagesdone: imagesProcessed,
|
|
percent: 60 + Math.floor((imagesProcessed / Math.max(totalImages, 1)) * 25),
|
|
});
|
|
}
|
|
});
|
|
|
|
// 5. Build categories.csv & place category images in ZIP
|
|
onStatus({ message: 'Erstelle categories.csv...' });
|
|
const categoryCsvRows = [
|
|
['"id"', '"parentid"', '"sort"', '"name"', '"pictureFiles"'].join(';'),
|
|
];
|
|
|
|
for (const c of categoryTree) {
|
|
let picRel = '';
|
|
if (c.fullHash && hashToBuffer.has(c.fullHash)) {
|
|
picRel = `pictures/categories/${c.id}/${c.fullHash}.avif`;
|
|
zip.file(picRel, hashToBuffer.get(c.fullHash));
|
|
}
|
|
|
|
const catName = c.name || (c.parentId == null ? 'Haupt' : `Kategorie ${c.id}`);
|
|
const row = [
|
|
formatCsvField(c.id),
|
|
formatCsvField(c.parentId == null ? '0' : String(c.parentId)),
|
|
formatCsvField(c.sort ?? 0),
|
|
formatCsvField(catName),
|
|
formatCsvField(picRel),
|
|
];
|
|
categoryCsvRows.push(row.join(';'));
|
|
}
|
|
zip.file('categories.csv', UTF8_BOM + categoryCsvRows.join('\r\n'));
|
|
|
|
// 6. Place brand images in ZIP
|
|
for (const b of brands) {
|
|
const hash = b.pics?.[0]?.fullHash;
|
|
if (hash && hashToBuffer.has(hash)) {
|
|
const rel = `pictures/brands/${b.id}/${hash}.avif`;
|
|
zip.file(rel, hashToBuffer.get(hash));
|
|
}
|
|
}
|
|
|
|
// 7. Build articles.csv & place article images in ZIP
|
|
onStatus({ message: 'Erstelle articles.csv...' });
|
|
const articleCsvHeaders = [
|
|
'"id"',
|
|
'"sku"',
|
|
'"name"',
|
|
'"net"',
|
|
'"taxrate"',
|
|
'"active"',
|
|
'"stock"',
|
|
'"ean"',
|
|
'"weight"',
|
|
'"track_strock"',
|
|
'"created_at"',
|
|
'"sort"',
|
|
'"variation"',
|
|
'"parentid"',
|
|
'"is_parent"',
|
|
'"brand"',
|
|
'"categories"',
|
|
'"attributes"',
|
|
'"brandPictureFiles"',
|
|
'"pictureFiles"',
|
|
'"supplieer"',
|
|
'"desc"',
|
|
];
|
|
const articleCsvRows = [articleCsvHeaders.join(';')];
|
|
|
|
for (const a of fullArticles) {
|
|
// Collect article pictures
|
|
const picPaths = [];
|
|
for (const p of a.photos || []) {
|
|
if (p.fullHash && hashToBuffer.has(p.fullHash)) {
|
|
const rel = `pictures/articles/${a.id}/${p.fullHash}.avif`;
|
|
zip.file(rel, hashToBuffer.get(p.fullHash));
|
|
picPaths.push(rel);
|
|
}
|
|
}
|
|
|
|
// Brand image path
|
|
let brandPicRel = '';
|
|
const brandObj = a.brand?.id
|
|
? brandById.get(a.brand.id)
|
|
: a.brand?.name
|
|
? brandByName.get(a.brand.name.trim().toLowerCase())
|
|
: null;
|
|
|
|
const brandHash = brandObj?.pics?.[0]?.fullHash || a.brand?.fullHash;
|
|
if (brandHash && hashToBuffer.has(brandHash) && (brandObj?.id || a.brand?.id)) {
|
|
const bId = brandObj?.id || a.brand.id;
|
|
brandPicRel = `pictures/brands/${bId}/${brandHash}.avif`;
|
|
}
|
|
|
|
// Price & Tax
|
|
const netPrice = a.prices?.[0]?.net ?? a.net ?? 0;
|
|
const taxRate = a.prices?.[0]?.taxRate ?? a.taxRate ?? 19;
|
|
|
|
// Parent / Variant fields
|
|
const isParent = Boolean(a.isParent);
|
|
let parentId = '0';
|
|
let variation = '';
|
|
|
|
if (isParent) {
|
|
variation = a.parentAttributeName || 'Variante';
|
|
} else if (a.parentArticleId != null || a.parentSku != null) {
|
|
const parentObj = a.parentArticleId
|
|
? articleById.get(a.parentArticleId)
|
|
: articleBySku.get(a.parentSku);
|
|
parentId = String(parentObj?.id ?? a.parentArticleId ?? 0);
|
|
variation = a.attributeValue || 'Standard';
|
|
}
|
|
|
|
// Pfand attribute
|
|
const pfandAttr = extractPfandAttribute(a.options);
|
|
|
|
// Barcode / EAN
|
|
const ean = Array.isArray(a.eans) && a.eans.length > 0 ? a.eans[0] : '';
|
|
|
|
// 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),
|
|
formatCsvField(a.name ?? ''),
|
|
formatCsvField(formatGermanFloat(netPrice)),
|
|
formatCsvField(formatGermanFloat(taxRate, '19')),
|
|
formatCsvField('1'), // active
|
|
formatCsvField(a.stock ?? 0),
|
|
formatCsvStringFormula(ean),
|
|
formatCsvField(formatGermanFloat(a.weight ?? 0)),
|
|
formatCsvField(a.track_stock ? '1' : '0'),
|
|
formatCsvField(a.createdAt || new Date().toISOString()),
|
|
formatCsvField(a.sort ?? 0),
|
|
formatCsvField(variation),
|
|
formatCsvField(parentId),
|
|
formatCsvField(isParent ? '1' : '0'),
|
|
formatCsvField(a.brand?.name || ''),
|
|
formatCsvField(catStr),
|
|
formatCsvField(pfandAttr),
|
|
formatCsvField(brandPicRel),
|
|
formatCsvField(picPaths.join(';')),
|
|
formatCsvField(supplierStr),
|
|
formatCsvField(a.desc || ''),
|
|
];
|
|
articleCsvRows.push(row.join(';'));
|
|
}
|
|
zip.file('articles.csv', UTF8_BOM + articleCsvRows.join('\r\n'));
|
|
|
|
// 8. Orders / Manifests (if present)
|
|
let exportedReceiptsCount = 0;
|
|
try {
|
|
const orderListRes = await store.order.list(googleAuthId, { offset: 0, limit: 1 });
|
|
const totalOrders = orderListRes?.total ?? (Array.isArray(orderListRes) ? orderListRes.length : 0);
|
|
|
|
if (totalOrders > 0) {
|
|
onStatus({ message: `Exportiere ${totalOrders} Aufträge / Belege...` });
|
|
|
|
// Prefetch payment types and shipping types by manifest ID
|
|
const paymentMap = new Map();
|
|
const shippingMap = new Map();
|
|
try {
|
|
const provider = await getProvider();
|
|
const isSqlite = provider?.dialect === 'sqlite3';
|
|
const typeCol = isSqlite ? '"type"' : '[type]';
|
|
const [paymentsRes, shippingsRes] = await Promise.all([
|
|
provider.query(`
|
|
SELECT p.manifest_id AS manifestId, p.${typeCol} AS type
|
|
FROM payment p
|
|
INNER JOIN manifest m ON m.id = p.manifest_id
|
|
WHERE m.company_id = @companyId
|
|
ORDER BY p.id ASC
|
|
`, { companyId }),
|
|
provider.query(`
|
|
SELECT s.manifest_id AS manifestId, s.${typeCol} AS type
|
|
FROM shipping s
|
|
INNER JOIN manifest m ON m.id = s.manifest_id
|
|
WHERE m.company_id = @companyId
|
|
ORDER BY s.id ASC
|
|
`, { companyId }),
|
|
]);
|
|
|
|
for (const row of paymentsRes.rows || []) {
|
|
if (!paymentMap.has(row.manifestId)) {
|
|
paymentMap.set(row.manifestId, row.type);
|
|
}
|
|
}
|
|
for (const row of shippingsRes.rows || []) {
|
|
if (!shippingMap.has(row.manifestId)) {
|
|
shippingMap.set(row.manifestId, row.type);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
log.warn('Could not prefetch payment/shipping types:', err.message);
|
|
}
|
|
|
|
const entityMap = new Map();
|
|
try {
|
|
const entities = await store.entity.list(googleAuthId);
|
|
for (const ent of entities) {
|
|
entityMap.set(ent.id, ent);
|
|
}
|
|
} catch (err) {
|
|
log.warn('Could not prefetch entities for order export:', err.message);
|
|
}
|
|
|
|
const manifestCsvHeaders = [
|
|
'"id"',
|
|
'"nr"',
|
|
'"extnr"',
|
|
'"created_at"',
|
|
'"customerNr"',
|
|
'"customerName"',
|
|
'"paymentType"',
|
|
'"shippingType"',
|
|
'"payed"',
|
|
'"shipped"',
|
|
];
|
|
const manifestItemCsvHeaders = [
|
|
'"id"',
|
|
'"manifestid"',
|
|
'"articleid"',
|
|
'"name"',
|
|
'"amount"',
|
|
'"amountShipped"',
|
|
'"net"',
|
|
'"taxrate"',
|
|
'"sort"',
|
|
'"shippable"',
|
|
];
|
|
|
|
const manifestCsvRows = [manifestCsvHeaders.join(';')];
|
|
const manifestItemCsvRows = [manifestItemCsvHeaders.join(';')];
|
|
let orderOffset = 0;
|
|
const orderLimit = 200;
|
|
let itemSeq = 0;
|
|
|
|
while (true) {
|
|
const batchRes = await store.order.list(googleAuthId, { offset: orderOffset, limit: orderLimit });
|
|
const batch = Array.isArray(batchRes) ? batchRes : (batchRes?.orders || []);
|
|
if (batch.length === 0) break;
|
|
|
|
for (const ord of batch) {
|
|
const payType = paymentMap.get(ord.id) || ord.payment?.type || ord.payments?.[0]?.type || 'Bar';
|
|
const shipType = shippingMap.get(ord.id) || ord.shipping?.type || ord.shippings?.[0]?.type || 'Selbstabholer';
|
|
|
|
const entity = ord.entity || (ord.entityId ? entityMap.get(ord.entityId) : null);
|
|
const customerNr = ord.customerNr || entity?.number || ord.customer?.number || '';
|
|
const customerName = ord.customerName
|
|
|| entity?.name
|
|
|| [entity?.firstName, entity?.lastName].filter(Boolean).join(' ')
|
|
|| [ord.customerFirstName, ord.customerLastName].filter(Boolean).join(' ')
|
|
|| '';
|
|
|
|
manifestCsvRows.push([
|
|
formatCsvField(ord.id),
|
|
formatCsvField(ord.number),
|
|
formatCsvField(ord.externalNumber || ''),
|
|
formatCsvField(ord.createdAt || ord.date || new Date().toISOString()),
|
|
formatCsvField(customerNr),
|
|
formatCsvField(customerName),
|
|
formatCsvField(payType),
|
|
formatCsvField(shipType),
|
|
formatCsvField(ord.isPaid || ord.paid ? '1' : '0'),
|
|
formatCsvField(ord.isShipped || ord.shipped ? '1' : '0'),
|
|
].join(';'));
|
|
|
|
for (const line of ord.lines || []) {
|
|
const artId = (line.sku && articleBySku.get(line.sku)?.id) || line.articleId || '';
|
|
manifestItemCsvRows.push([
|
|
formatCsvField(line.id || ++itemSeq),
|
|
formatCsvField(ord.id),
|
|
formatCsvField(artId),
|
|
formatCsvField(line.name || ''),
|
|
formatCsvField(line.amount ?? 1),
|
|
formatCsvField(ord.isShipped || ord.shipped ? (line.amount ?? 1) : 0),
|
|
formatCsvField(formatGermanFloat(line.net ?? 0)),
|
|
formatCsvField(formatGermanFloat(line.taxrate ?? 19)),
|
|
formatCsvField(line.sort ?? 0),
|
|
formatCsvField(line.shippable !== false ? '1' : '0'),
|
|
].join(';'));
|
|
}
|
|
}
|
|
|
|
orderOffset += batch.length;
|
|
if (batch.length < orderLimit || orderOffset >= totalOrders) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (manifestCsvRows.length > 1) {
|
|
zip.file('manifests.csv', UTF8_BOM + manifestCsvRows.join('\r\n'));
|
|
zip.file('manifestitems.csv', UTF8_BOM + manifestItemCsvRows.join('\r\n'));
|
|
exportedReceiptsCount = manifestCsvRows.length - 1;
|
|
}
|
|
}
|
|
} catch (orderErr) {
|
|
log.warn('Could not export orders / manifests:', orderErr.message);
|
|
}
|
|
|
|
// 9. Generate ZIP buffer
|
|
onStatus({ message: 'Erstelle ZIP-Archiv...' });
|
|
onProgress({
|
|
stage: 'packaging',
|
|
percent: 92,
|
|
});
|
|
|
|
const zipBuffer = await zip.generateAsync({
|
|
type: 'nodebuffer',
|
|
compression: 'DEFLATE',
|
|
compressionOptions: { level: 6 },
|
|
});
|
|
|
|
onProgress({
|
|
stage: 'packaging',
|
|
percent: 100,
|
|
});
|
|
|
|
const filename = `company-${companyId}-export.zip`;
|
|
const stats = {
|
|
categories: categoryTree.length,
|
|
brands: brands.length,
|
|
articles: fullArticles.length,
|
|
images: hashToBuffer.size,
|
|
orders: exportedReceiptsCount,
|
|
zipSize: zipBuffer.length,
|
|
};
|
|
|
|
log.info(`Company export complete: ${filename} (${zipBuffer.length} bytes, ${stats.articles} articles, ${stats.images} images)`);
|
|
|
|
return {
|
|
zipBuffer,
|
|
stats,
|
|
filename,
|
|
};
|
|
}
|
|
|
|
// #endregion
|