471 lines
19 KiB
JavaScript
471 lines
19 KiB
JavaScript
|
|
/**
|
|||
|
|
* ═══════════════════════════════════════════════════════════════════════════
|
|||
|
|
* AAMOS Ledger — SIE4 Export
|
|||
|
|
* Exporterar bokföringsdata i SIE4-format (svensk standard, typ 4)
|
|||
|
|
*
|
|||
|
|
* Endpoints:
|
|||
|
|
* GET /api/ledger/export/sie4 → SIE4-fil (text/plain download)
|
|||
|
|
* GET /api/ledger/export/sie4/preview → JSON-förhandsgranskning
|
|||
|
|
*
|
|||
|
|
* SIE4 standard: https://sie.se/
|
|||
|
|
* Teckenset: UTF-8 (moderna program accepterar detta; CP437/PC8 är äldre norm)
|
|||
|
|
* Radslut: CRLF per SIE-standard
|
|||
|
|
* ═══════════════════════════════════════════════════════════════════════════
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
// ── Hjälpfunktioner ──────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Mappar source_type till SIE4-serie:
|
|||
|
|
* A = manuella verifikationer
|
|||
|
|
* B = bankimport
|
|||
|
|
* S = SIE-import
|
|||
|
|
*/
|
|||
|
|
function sourceTypeToSeries(sourceType) {
|
|||
|
|
switch (sourceType) {
|
|||
|
|
case 'import_bank':
|
|||
|
|
case 'bank':
|
|||
|
|
return 'B';
|
|||
|
|
case 'import_sie':
|
|||
|
|
return 'S';
|
|||
|
|
default:
|
|||
|
|
return 'A';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Formaterar Date eller ISO-sträng till YYYYMMDD (SIE-datumformat).
|
|||
|
|
*/
|
|||
|
|
function fmtDate(d) {
|
|||
|
|
if (!d) return '';
|
|||
|
|
const dt = typeof d === 'string' ? new Date(d) : d;
|
|||
|
|
const y = dt.getUTCFullYear();
|
|||
|
|
const m = String(dt.getUTCMonth() + 1).padStart(2, '0');
|
|||
|
|
const day = String(dt.getUTCDate()).padStart(2, '0');
|
|||
|
|
return `${y}${m}${day}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Formaterar numeriskt belopp till SIE4-format (punkt, 2 decimaler).
|
|||
|
|
* Debet = positivt, Kredit = negativt.
|
|||
|
|
*/
|
|||
|
|
function fmtAmount(n) {
|
|||
|
|
return (parseFloat(n) || 0).toFixed(2);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Escapar citattecken i SIE4-strängar: " → \"
|
|||
|
|
*/
|
|||
|
|
function escapeStr(s) {
|
|||
|
|
if (s == null) return '';
|
|||
|
|
return String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── Databas-hämtning ─────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Hämtar all data som behövs för en SIE4-export.
|
|||
|
|
*
|
|||
|
|
* @param {object} pool - pg.Pool
|
|||
|
|
* @param {string} tenant_id
|
|||
|
|
* @param {number} fiscal_year
|
|||
|
|
* @param {string|null} period - "YYYY-MM" eller null (= hela räkenskapsåret)
|
|||
|
|
* @param {boolean} include_unposted
|
|||
|
|
* @returns {Promise<object>} { entries, accountMap, accountNumbers, ibMap, ubMap, dateFrom, dateTo }
|
|||
|
|
*/
|
|||
|
|
async function fetchExportData(pool, tenant_id, fiscal_year, period, include_unposted) {
|
|||
|
|
// ── Datumintervall ──────────────────────────────────────────────────────────
|
|||
|
|
let dateFromUtc, dateToUtc;
|
|||
|
|
if (period) {
|
|||
|
|
const [py, pm] = period.split('-').map(Number);
|
|||
|
|
dateFromUtc = new Date(Date.UTC(py, pm - 1, 1));
|
|||
|
|
dateToUtc = new Date(Date.UTC(py, pm, 0)); // sista dagen i månaden
|
|||
|
|
} else {
|
|||
|
|
dateFromUtc = new Date(Date.UTC(fiscal_year, 0, 1)); // 1 jan
|
|||
|
|
dateToUtc = new Date(Date.UTC(fiscal_year, 11, 31)); // 31 dec
|
|||
|
|
}
|
|||
|
|
const dateFromStr = dateFromUtc.toISOString().split('T')[0];
|
|||
|
|
const dateToStr = dateToUtc.toISOString().split('T')[0];
|
|||
|
|
|
|||
|
|
// ── Verifikationer med rader ────────────────────────────────────────────────
|
|||
|
|
const statusFilter = include_unposted ? ['draft', 'posted'] : ['posted'];
|
|||
|
|
const statusPlaceholders = statusFilter.map((_, i) => `$${i + 3}`).join(',');
|
|||
|
|
const dateFromIdx = statusFilter.length + 3;
|
|||
|
|
const dateToIdx = statusFilter.length + 4;
|
|||
|
|
|
|||
|
|
const entriesSQL = `
|
|||
|
|
SELECT
|
|||
|
|
e.id,
|
|||
|
|
e.entry_number,
|
|||
|
|
e.entry_date,
|
|||
|
|
e.description,
|
|||
|
|
e.reference,
|
|||
|
|
e.source_type,
|
|||
|
|
e.status,
|
|||
|
|
e.posted_at,
|
|||
|
|
e.created_at,
|
|||
|
|
json_agg(
|
|||
|
|
json_build_object(
|
|||
|
|
'account_number', l.account_number,
|
|||
|
|
'account_name', l.account_name,
|
|||
|
|
'debit', l.debit,
|
|||
|
|
'credit', l.credit,
|
|||
|
|
'description', l.description
|
|||
|
|
) ORDER BY l.line_number
|
|||
|
|
) AS lines
|
|||
|
|
FROM ledger_journal_entries e
|
|||
|
|
JOIN ledger_journal_lines l ON l.entry_id = e.id
|
|||
|
|
WHERE e.tenant_id = $1
|
|||
|
|
AND e.fiscal_year = $2
|
|||
|
|
AND e.status IN (${statusPlaceholders})
|
|||
|
|
AND e.entry_date >= $${dateFromIdx}
|
|||
|
|
AND e.entry_date <= $${dateToIdx}
|
|||
|
|
GROUP BY e.id
|
|||
|
|
ORDER BY e.entry_date ASC,
|
|||
|
|
e.entry_number ASC NULLS LAST,
|
|||
|
|
e.created_at ASC
|
|||
|
|
`;
|
|||
|
|
const { rows: entries } = await pool.query(entriesSQL, [
|
|||
|
|
tenant_id, fiscal_year, ...statusFilter, dateFromStr, dateToStr,
|
|||
|
|
]);
|
|||
|
|
|
|||
|
|
// ── Samla berörda kontonummer ───────────────────────────────────────────────
|
|||
|
|
const accountNumberSet = new Set();
|
|||
|
|
for (const e of entries) {
|
|||
|
|
for (const l of (e.lines || [])) {
|
|||
|
|
if (l.account_number) accountNumberSet.add(l.account_number);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
const accountNumbers = Array.from(accountNumberSet).sort();
|
|||
|
|
|
|||
|
|
// ── Kontoinformation ────────────────────────────────────────────────────────
|
|||
|
|
const accountMap = {};
|
|||
|
|
if (accountNumbers.length > 0) {
|
|||
|
|
const placeholders = accountNumbers.map((_, i) => `$${i + 2}`).join(',');
|
|||
|
|
const { rows: acctRows } = await pool.query(
|
|||
|
|
`SELECT account_number, name, account_type, normal_balance
|
|||
|
|
FROM ledger_accounts
|
|||
|
|
WHERE tenant_id = $1 AND account_number IN (${placeholders})`,
|
|||
|
|
[tenant_id, ...accountNumbers]
|
|||
|
|
);
|
|||
|
|
for (const a of acctRows) {
|
|||
|
|
accountMap[a.account_number] = a;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── IB — ingångssaldo (alla poster INNAN perioden startar) ──────────────────
|
|||
|
|
// IB = kumulativt nettosaldo t.o.m. dagen FÖRE periodens start
|
|||
|
|
const ibDate = new Date(dateFromUtc.getTime() - 86_400_000); // -1 dag
|
|||
|
|
const ibDateStr = ibDate.toISOString().split('T')[0];
|
|||
|
|
|
|||
|
|
const ibMap = {};
|
|||
|
|
if (accountNumbers.length > 0) {
|
|||
|
|
const placeholders = accountNumbers.map((_, i) => `$${i + 3}`).join(',');
|
|||
|
|
const { rows: ibRows } = await pool.query(
|
|||
|
|
`SELECT
|
|||
|
|
l.account_number,
|
|||
|
|
COALESCE(SUM(l.debit), 0) - COALESCE(SUM(l.credit), 0) AS balance
|
|||
|
|
FROM ledger_journal_lines l
|
|||
|
|
JOIN ledger_journal_entries e ON e.id = l.entry_id
|
|||
|
|
WHERE e.tenant_id = $1
|
|||
|
|
AND e.status = 'posted'
|
|||
|
|
AND e.entry_date <= $2
|
|||
|
|
AND l.account_number IN (${placeholders})
|
|||
|
|
GROUP BY l.account_number`,
|
|||
|
|
[tenant_id, ibDateStr, ...accountNumbers]
|
|||
|
|
);
|
|||
|
|
for (const r of ibRows) {
|
|||
|
|
ibMap[r.account_number] = parseFloat(r.balance) || 0;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── UB — utgångssaldo (alla poster T.O.M. periodens slut) ──────────────────
|
|||
|
|
const ubMap = {};
|
|||
|
|
if (accountNumbers.length > 0) {
|
|||
|
|
const placeholders = accountNumbers.map((_, i) => `$${i + 3}`).join(',');
|
|||
|
|
const { rows: ubRows } = await pool.query(
|
|||
|
|
`SELECT
|
|||
|
|
l.account_number,
|
|||
|
|
COALESCE(SUM(l.debit), 0) - COALESCE(SUM(l.credit), 0) AS balance
|
|||
|
|
FROM ledger_journal_lines l
|
|||
|
|
JOIN ledger_journal_entries e ON e.id = l.entry_id
|
|||
|
|
WHERE e.tenant_id = $1
|
|||
|
|
AND e.status = 'posted'
|
|||
|
|
AND e.entry_date <= $2
|
|||
|
|
AND l.account_number IN (${placeholders})
|
|||
|
|
GROUP BY l.account_number`,
|
|||
|
|
[tenant_id, dateToStr, ...accountNumbers]
|
|||
|
|
);
|
|||
|
|
for (const r of ubRows) {
|
|||
|
|
ubMap[r.account_number] = parseFloat(r.balance) || 0;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return { entries, accountMap, accountNumbers, ibMap, ubMap, dateFrom: dateFromStr, dateTo: dateToStr };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── SIE4-textgenerator ───────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Bygger SIE4-textfilen från exportdata och alternativ.
|
|||
|
|
*
|
|||
|
|
* @param {object} data - Returnerat från fetchExportData
|
|||
|
|
* @param {object} opts - { fiscal_year, org_nr, company_name }
|
|||
|
|
* @returns {string} - SIE4-text med CRLF-radslut
|
|||
|
|
*/
|
|||
|
|
function buildSIE4(data, opts) {
|
|||
|
|
const { entries, accountMap, accountNumbers, ibMap, ubMap } = data;
|
|||
|
|
const { fiscal_year, org_nr, company_name } = opts;
|
|||
|
|
|
|||
|
|
// Dagens datum för #GEN
|
|||
|
|
const now = new Date();
|
|||
|
|
const genDate = [
|
|||
|
|
now.getUTCFullYear(),
|
|||
|
|
String(now.getUTCMonth() + 1).padStart(2, '0'),
|
|||
|
|
String(now.getUTCDate()).padStart(2, '0'),
|
|||
|
|
].join('');
|
|||
|
|
|
|||
|
|
const fyStart = `${fiscal_year}0101`;
|
|||
|
|
const fyEnd = `${fiscal_year}1231`;
|
|||
|
|
|
|||
|
|
const out = [];
|
|||
|
|
|
|||
|
|
// ── Huvudblock ─────────────────────────────────────────────────────────────
|
|||
|
|
out.push('#FLAGGA 0');
|
|||
|
|
out.push('#PROGRAM "AAMOS Ledger" 1.0');
|
|||
|
|
out.push('#FORMAT PC8');
|
|||
|
|
out.push(`#GEN ${genDate} system`);
|
|||
|
|
out.push('#SIETYP 4');
|
|||
|
|
if (org_nr) out.push(`#ORGNR ${org_nr}`);
|
|||
|
|
if (company_name) out.push(`#FNAMN "${escapeStr(company_name)}"`);
|
|||
|
|
out.push(`#RAR 0 ${fyStart} ${fyEnd}`);
|
|||
|
|
out.push('');
|
|||
|
|
|
|||
|
|
// ── Kontoplan (#KONTO) ──────────────────────────────────────────────────────
|
|||
|
|
for (const acctNr of accountNumbers) {
|
|||
|
|
const acct = accountMap[acctNr];
|
|||
|
|
const name = acct ? acct.name : acctNr;
|
|||
|
|
out.push(`#KONTO ${acctNr} "${escapeStr(name)}"`);
|
|||
|
|
}
|
|||
|
|
out.push('');
|
|||
|
|
|
|||
|
|
// ── Saldon (#IB / #UB / #RES) ──────────────────────────────────────────────
|
|||
|
|
// Balansräkningskonton → #IB + #UB
|
|||
|
|
// Resultaträkningskonton → #RES (periodens netto: UB − IB)
|
|||
|
|
for (const acctNr of accountNumbers) {
|
|||
|
|
const acct = accountMap[acctNr];
|
|||
|
|
const ib = ibMap[acctNr] || 0;
|
|||
|
|
const ub = ubMap[acctNr] || 0;
|
|||
|
|
|
|||
|
|
if (acct && (acct.account_type === 'revenue' || acct.account_type === 'expense')) {
|
|||
|
|
out.push(`#RES 0 ${acctNr} ${fmtAmount(ub - ib)}`);
|
|||
|
|
} else {
|
|||
|
|
out.push(`#IB 0 ${acctNr} ${fmtAmount(ib)}`);
|
|||
|
|
out.push(`#UB 0 ${acctNr} ${fmtAmount(ub)}`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
out.push('');
|
|||
|
|
|
|||
|
|
// ── Verifikationer (#VER med #TRANS) ───────────────────────────────────────
|
|||
|
|
let fallbackIdx = 1;
|
|||
|
|
for (const entry of entries) {
|
|||
|
|
const series = sourceTypeToSeries(entry.source_type);
|
|||
|
|
const verNr = entry.entry_number != null ? String(entry.entry_number) : String(fallbackIdx);
|
|||
|
|
fallbackIdx++;
|
|||
|
|
|
|||
|
|
const verDate = fmtDate(entry.entry_date);
|
|||
|
|
const regDate = entry.posted_at ? fmtDate(entry.posted_at) : verDate;
|
|||
|
|
const desc = escapeStr(entry.description);
|
|||
|
|
|
|||
|
|
out.push(`#VER ${series} ${verNr} ${verDate} "${desc}" ${regDate}`);
|
|||
|
|
out.push('{');
|
|||
|
|
|
|||
|
|
for (const l of (entry.lines || [])) {
|
|||
|
|
// Debet = positivt, Kredit = negativt i SIE4
|
|||
|
|
const amount = (parseFloat(l.debit) || 0) - (parseFloat(l.credit) || 0);
|
|||
|
|
out.push(`#TRANS ${l.account_number} {} ${fmtAmount(amount)}`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
out.push('}');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// SIE4-standard: CRLF radslut
|
|||
|
|
return out.join('\r\n') + '\r\n';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── Route-registrering ───────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Registrerar SIE4 export-endpoints på Express-appen.
|
|||
|
|
*
|
|||
|
|
* @param {import('express').Application} app
|
|||
|
|
* @param {import('pg').Pool} pool
|
|||
|
|
* @param {Function} buildCtx - Context-byggare från index.mjs (används ej — read-only export)
|
|||
|
|
* @param {object} hermes - Event-fabric (används ej — read-only export)
|
|||
|
|
*/
|
|||
|
|
export function register(app, pool, buildCtx, hermes) {
|
|||
|
|
|
|||
|
|
// ── GET /api/ledger/export/sie4 ────────────────────────────────────────────
|
|||
|
|
// Returnerar SIE4-fil som nedladdningsbar bifogad fil (text/plain).
|
|||
|
|
//
|
|||
|
|
// Query params:
|
|||
|
|
// fiscal_year (krävs) t.ex. 2026
|
|||
|
|
// period (valfritt) t.ex. 2026-01; utelämnad = hela räkenskapsåret
|
|||
|
|
// org_nr (valfritt) organisationsnummer
|
|||
|
|
// company_name (valfritt) företagsnamn
|
|||
|
|
// include_unposted (valfritt, default "false") inkludera draft-poster
|
|||
|
|
app.get('/api/ledger/export/sie4', async (req, res) => {
|
|||
|
|
const tenant_id = req.headers['x-tenant-id'] || 'wavult-group';
|
|||
|
|
const {
|
|||
|
|
fiscal_year,
|
|||
|
|
period = null,
|
|||
|
|
org_nr = null,
|
|||
|
|
company_name = null,
|
|||
|
|
include_unposted = 'false',
|
|||
|
|
} = req.query;
|
|||
|
|
|
|||
|
|
if (!fiscal_year) {
|
|||
|
|
return res.status(400).json({ ok: false, error: 'fiscal_year krävs' });
|
|||
|
|
}
|
|||
|
|
const fy = parseInt(fiscal_year, 10);
|
|||
|
|
if (isNaN(fy) || fy < 1900 || fy > 2100) {
|
|||
|
|
return res.status(400).json({ ok: false, error: 'Ogiltigt fiscal_year' });
|
|||
|
|
}
|
|||
|
|
if (period && !/^\d{4}-\d{2}$/.test(period)) {
|
|||
|
|
return res.status(400).json({ ok: false, error: 'period måste vara i format YYYY-MM' });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const incUnposted = include_unposted === 'true';
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
console.log(`[sie4-export] Export: tenant=${tenant_id} fy=${fy} period=${period ?? 'hela året'} unposted=${incUnposted}`);
|
|||
|
|
|
|||
|
|
const data = await fetchExportData(pool, tenant_id, fy, period, incUnposted);
|
|||
|
|
const sieText = buildSIE4(data, { fiscal_year: fy, org_nr, company_name });
|
|||
|
|
const filename = period ? `SIE4_${period}.se` : `SIE4_${fy}.se`;
|
|||
|
|
|
|||
|
|
console.log(
|
|||
|
|
`[sie4-export] OK: ${data.entries.length} verifikationer, ` +
|
|||
|
|
`${data.accountNumbers.length} konton → ${filename}`
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
res.set('Content-Type', 'text/plain; charset=utf-8');
|
|||
|
|
res.set('Content-Disposition', `attachment; filename="${filename}"`);
|
|||
|
|
res.send(sieText);
|
|||
|
|
|
|||
|
|
} catch (e) {
|
|||
|
|
console.error('[sie4-export] Export misslyckades:', e.message);
|
|||
|
|
res.status(500).json({ ok: false, error: e.message });
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// ── GET /api/ledger/export/sie4/preview ───────────────────────────────────
|
|||
|
|
// Returnerar JSON med metadata + max 20 verifikationer för förhandsgranskning.
|
|||
|
|
// Samma query-params som /sie4.
|
|||
|
|
app.get('/api/ledger/export/sie4/preview', async (req, res) => {
|
|||
|
|
const tenant_id = req.headers['x-tenant-id'] || 'wavult-group';
|
|||
|
|
const {
|
|||
|
|
fiscal_year,
|
|||
|
|
period = null,
|
|||
|
|
org_nr = null,
|
|||
|
|
company_name = null,
|
|||
|
|
include_unposted = 'false',
|
|||
|
|
} = req.query;
|
|||
|
|
|
|||
|
|
if (!fiscal_year) {
|
|||
|
|
return res.status(400).json({ ok: false, error: 'fiscal_year krävs' });
|
|||
|
|
}
|
|||
|
|
const fy = parseInt(fiscal_year, 10);
|
|||
|
|
if (isNaN(fy) || fy < 1900 || fy > 2100) {
|
|||
|
|
return res.status(400).json({ ok: false, error: 'Ogiltigt fiscal_year' });
|
|||
|
|
}
|
|||
|
|
if (period && !/^\d{4}-\d{2}$/.test(period)) {
|
|||
|
|
return res.status(400).json({ ok: false, error: 'period måste vara i format YYYY-MM' });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const incUnposted = include_unposted === 'true';
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
console.log(`[sie4-export] Preview: tenant=${tenant_id} fy=${fy} period=${period ?? 'hela året'}`);
|
|||
|
|
|
|||
|
|
const data = await fetchExportData(pool, tenant_id, fy, period, incUnposted);
|
|||
|
|
|
|||
|
|
// Förhandsgranskning (max 20 verifikationer)
|
|||
|
|
let fallbackIdx = 1;
|
|||
|
|
const previewEntries = data.entries.slice(0, 20).map(e => {
|
|||
|
|
const verNr = e.entry_number != null ? String(e.entry_number) : String(fallbackIdx);
|
|||
|
|
fallbackIdx++;
|
|||
|
|
return {
|
|||
|
|
series: sourceTypeToSeries(e.source_type),
|
|||
|
|
ver_nr: verNr,
|
|||
|
|
date: fmtDate(e.entry_date),
|
|||
|
|
reg_date: e.posted_at ? fmtDate(e.posted_at) : fmtDate(e.entry_date),
|
|||
|
|
description: e.description,
|
|||
|
|
source_type: e.source_type,
|
|||
|
|
status: e.status,
|
|||
|
|
lines: (e.lines || []).map(l => ({
|
|||
|
|
account_number: l.account_number,
|
|||
|
|
account_name: l.account_name || null,
|
|||
|
|
amount: (parseFloat(l.debit) || 0) - (parseFloat(l.credit) || 0),
|
|||
|
|
})),
|
|||
|
|
};
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Kontosaldo-sammanfattning
|
|||
|
|
const accountSummary = data.accountNumbers.map(acctNr => {
|
|||
|
|
const acct = data.accountMap[acctNr];
|
|||
|
|
const ib = data.ibMap[acctNr] || 0;
|
|||
|
|
const ub = data.ubMap[acctNr] || 0;
|
|||
|
|
const isResultat = acct && (acct.account_type === 'revenue' || acct.account_type === 'expense');
|
|||
|
|
return {
|
|||
|
|
account_number: acctNr,
|
|||
|
|
name: acct ? acct.name : null,
|
|||
|
|
account_type: acct ? acct.account_type : 'unknown',
|
|||
|
|
sie4_type: isResultat ? 'RES' : 'IB/UB',
|
|||
|
|
ib: isResultat ? null : ib,
|
|||
|
|
ub: isResultat ? null : ub,
|
|||
|
|
res: isResultat ? (ub - ib) : null,
|
|||
|
|
};
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Totaler för balans-check
|
|||
|
|
const totalDebit = data.entries.reduce(
|
|||
|
|
(s, e) => s + (e.lines || []).reduce((ls, l) => ls + (parseFloat(l.debit) || 0), 0), 0
|
|||
|
|
);
|
|||
|
|
const totalCredit = data.entries.reduce(
|
|||
|
|
(s, e) => s + (e.lines || []).reduce((ls, l) => ls + (parseFloat(l.credit) || 0), 0), 0
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
res.json({
|
|||
|
|
ok: true,
|
|||
|
|
meta: {
|
|||
|
|
tenant_id,
|
|||
|
|
fiscal_year: fy,
|
|||
|
|
period: period ?? `${fy} (hela räkenskapsåret)`,
|
|||
|
|
date_from: data.dateFrom,
|
|||
|
|
date_to: data.dateTo,
|
|||
|
|
org_nr: org_nr ?? null,
|
|||
|
|
company_name: company_name ?? null,
|
|||
|
|
include_unposted: incUnposted,
|
|||
|
|
entry_count: data.entries.length,
|
|||
|
|
account_count: data.accountNumbers.length,
|
|||
|
|
total_debit: totalDebit,
|
|||
|
|
total_credit: totalCredit,
|
|||
|
|
balanced: Math.abs(totalDebit - totalCredit) < 0.01,
|
|||
|
|
},
|
|||
|
|
accounts: accountSummary,
|
|||
|
|
preview_entries: previewEntries,
|
|||
|
|
note: data.entries.length > 20
|
|||
|
|
? `Visar 20 av ${data.entries.length} verifikationer`
|
|||
|
|
: `Visar alla ${data.entries.length} verifikationer`,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
} catch (e) {
|
|||
|
|
console.error('[sie4-export] Preview misslyckades:', e.message);
|
|||
|
|
res.status(500).json({ ok: false, error: e.message });
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
console.log('[aamos-ledger] SIE4 Export registrerad: /api/ledger/export/sie4 · /api/ledger/export/sie4/preview');
|
|||
|
|
}
|