u
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
// MARK: - Imports
|
||||
// #region Imports
|
||||
import JSZip from 'jszip';
|
||||
import { getStore } from '../db/store.js';
|
||||
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
|
||||
|
||||
@@ -22,6 +24,21 @@ function formatCsvField(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").
|
||||
*
|
||||
@@ -297,16 +314,17 @@ export async function executeCompanyExport({
|
||||
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(c.name ?? ''),
|
||||
formatCsvField(catName),
|
||||
formatCsvField(picRel),
|
||||
];
|
||||
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
|
||||
for (const b of brands) {
|
||||
@@ -399,13 +417,13 @@ export async function executeCompanyExport({
|
||||
|
||||
const row = [
|
||||
formatCsvField(a.id),
|
||||
formatCsvField(a.sku),
|
||||
formatCsvStringFormula(a.sku),
|
||||
formatCsvField(a.name ?? ''),
|
||||
formatCsvField(formatGermanFloat(netPrice)),
|
||||
formatCsvField(formatGermanFloat(taxRate, '19')),
|
||||
formatCsvField('1'), // active
|
||||
formatCsvField(a.stock ?? 0),
|
||||
formatCsvField(ean),
|
||||
formatCsvStringFormula(ean),
|
||||
formatCsvField(formatGermanFloat(a.weight ?? 0)),
|
||||
formatCsvField(a.track_stock ? '1' : '0'),
|
||||
formatCsvField(a.createdAt || new Date().toISOString()),
|
||||
@@ -422,9 +440,136 @@ export async function executeCompanyExport({
|
||||
];
|
||||
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...' });
|
||||
onProgress({
|
||||
stage: 'packaging',
|
||||
@@ -448,6 +593,7 @@ export async function executeCompanyExport({
|
||||
brands: brands.length,
|
||||
articles: fullArticles.length,
|
||||
images: hashToBuffer.size,
|
||||
orders: exportedReceiptsCount,
|
||||
zipSize: zipBuffer.length,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user