u
This commit is contained in:
@@ -1,12 +1,14 @@
|
|||||||
// MARK: - Imports
|
// MARK: - Imports
|
||||||
// #region Imports
|
// #region Imports
|
||||||
import JSZip from 'jszip';
|
import JSZip from 'jszip';
|
||||||
import { getStore } from '../db/store.js';
|
import { getStore, getProvider } from '../db/store.js';
|
||||||
import { createLogger } from '../util/logger.js';
|
import { createLogger } from '../util/logger.js';
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
const log = createLogger('export');
|
const log = createLogger('export');
|
||||||
|
|
||||||
|
const UTF8_BOM = '\uFEFF';
|
||||||
|
|
||||||
// MARK: - CSV Helpers
|
// MARK: - CSV Helpers
|
||||||
// #region CSV Helpers
|
// #region CSV Helpers
|
||||||
|
|
||||||
@@ -22,6 +24,21 @@ function formatCsvField(val) {
|
|||||||
return `"${str.replace(/"/g, '""')}"`;
|
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").
|
* Formats a number in German decimal format (e.g. 8.36 -> "8,36").
|
||||||
*
|
*
|
||||||
@@ -297,16 +314,17 @@ export async function executeCompanyExport({
|
|||||||
zip.file(picRel, hashToBuffer.get(c.fullHash));
|
zip.file(picRel, hashToBuffer.get(c.fullHash));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const catName = c.name || (c.parentId == null ? 'Haupt' : `Kategorie ${c.id}`);
|
||||||
const row = [
|
const row = [
|
||||||
formatCsvField(c.id),
|
formatCsvField(c.id),
|
||||||
formatCsvField(c.parentId == null ? '0' : String(c.parentId)),
|
formatCsvField(c.parentId == null ? '0' : String(c.parentId)),
|
||||||
formatCsvField(c.sort ?? 0),
|
formatCsvField(c.sort ?? 0),
|
||||||
formatCsvField(c.name ?? ''),
|
formatCsvField(catName),
|
||||||
formatCsvField(picRel),
|
formatCsvField(picRel),
|
||||||
];
|
];
|
||||||
categoryCsvRows.push(row.join(';'));
|
categoryCsvRows.push(row.join(';'));
|
||||||
}
|
}
|
||||||
zip.file('categories.csv', categoryCsvRows.join('\r\n'));
|
zip.file('categories.csv', UTF8_BOM + categoryCsvRows.join('\r\n'));
|
||||||
|
|
||||||
// 6. Place brand images in ZIP
|
// 6. Place brand images in ZIP
|
||||||
for (const b of brands) {
|
for (const b of brands) {
|
||||||
@@ -399,13 +417,13 @@ export async function executeCompanyExport({
|
|||||||
|
|
||||||
const row = [
|
const row = [
|
||||||
formatCsvField(a.id),
|
formatCsvField(a.id),
|
||||||
formatCsvField(a.sku),
|
formatCsvStringFormula(a.sku),
|
||||||
formatCsvField(a.name ?? ''),
|
formatCsvField(a.name ?? ''),
|
||||||
formatCsvField(formatGermanFloat(netPrice)),
|
formatCsvField(formatGermanFloat(netPrice)),
|
||||||
formatCsvField(formatGermanFloat(taxRate, '19')),
|
formatCsvField(formatGermanFloat(taxRate, '19')),
|
||||||
formatCsvField('1'), // active
|
formatCsvField('1'), // active
|
||||||
formatCsvField(a.stock ?? 0),
|
formatCsvField(a.stock ?? 0),
|
||||||
formatCsvField(ean),
|
formatCsvStringFormula(ean),
|
||||||
formatCsvField(formatGermanFloat(a.weight ?? 0)),
|
formatCsvField(formatGermanFloat(a.weight ?? 0)),
|
||||||
formatCsvField(a.track_stock ? '1' : '0'),
|
formatCsvField(a.track_stock ? '1' : '0'),
|
||||||
formatCsvField(a.createdAt || new Date().toISOString()),
|
formatCsvField(a.createdAt || new Date().toISOString()),
|
||||||
@@ -422,9 +440,136 @@ export async function executeCompanyExport({
|
|||||||
];
|
];
|
||||||
articleCsvRows.push(row.join(';'));
|
articleCsvRows.push(row.join(';'));
|
||||||
}
|
}
|
||||||
zip.file('articles.csv', articleCsvRows.join('\r\n'));
|
zip.file('articles.csv', UTF8_BOM + articleCsvRows.join('\r\n'));
|
||||||
|
|
||||||
// 8. Generate ZIP buffer
|
// 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 manifestCsvHeaders = [
|
||||||
|
'"id"',
|
||||||
|
'"nr"',
|
||||||
|
'"extnr"',
|
||||||
|
'"created_at"',
|
||||||
|
'"paymentType"',
|
||||||
|
'"shippingType"',
|
||||||
|
'"payed"',
|
||||||
|
'"shipped"',
|
||||||
|
];
|
||||||
|
const manifestItemCsvHeaders = [
|
||||||
|
'"id"',
|
||||||
|
'"manifestid"',
|
||||||
|
'"articleid"',
|
||||||
|
'"name"',
|
||||||
|
'"amount"',
|
||||||
|
'"amountShipped"',
|
||||||
|
'"net"',
|
||||||
|
'"taxrate"',
|
||||||
|
'"sort"',
|
||||||
|
];
|
||||||
|
|
||||||
|
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';
|
||||||
|
|
||||||
|
manifestCsvRows.push([
|
||||||
|
formatCsvField(ord.id),
|
||||||
|
formatCsvField(ord.number),
|
||||||
|
formatCsvField(ord.externalNumber || ''),
|
||||||
|
formatCsvField(ord.createdAt || ord.date || new Date().toISOString()),
|
||||||
|
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),
|
||||||
|
].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...' });
|
onStatus({ message: 'Erstelle ZIP-Archiv...' });
|
||||||
onProgress({
|
onProgress({
|
||||||
stage: 'packaging',
|
stage: 'packaging',
|
||||||
@@ -448,6 +593,7 @@ export async function executeCompanyExport({
|
|||||||
brands: brands.length,
|
brands: brands.length,
|
||||||
articles: fullArticles.length,
|
articles: fullArticles.length,
|
||||||
images: hashToBuffer.size,
|
images: hashToBuffer.size,
|
||||||
|
orders: exportedReceiptsCount,
|
||||||
zipSize: zipBuffer.length,
|
zipSize: zipBuffer.length,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user