- Complete implementation of German standard charts of accounts - SKR03 (Process Structure Principle) for trading/service companies - SKR04 (Financial Classification Principle) for manufacturing companies - Double-entry bookkeeping with MongoDB persistence - Comprehensive reporting suite with DATEV export - Full TypeScript support and type safety
722 lines
20 KiB
TypeScript
722 lines
20 KiB
TypeScript
import * as plugins from './plugins.js';
|
|
import { Account } from './skr.classes.account.js';
|
|
import { Transaction } from './skr.classes.transaction.js';
|
|
import { Ledger } from './skr.classes.ledger.js';
|
|
import type {
|
|
TSKRType,
|
|
ITrialBalanceReport,
|
|
ITrialBalanceEntry,
|
|
IIncomeStatement,
|
|
IIncomeStatementEntry,
|
|
IBalanceSheet,
|
|
IBalanceSheetEntry,
|
|
IReportParams,
|
|
} from './skr.types.js';
|
|
|
|
export class Reports {
|
|
private logger: plugins.smartlog.Smartlog;
|
|
private ledger: Ledger;
|
|
|
|
constructor(private skrType: TSKRType) {
|
|
this.logger = new plugins.smartlog.Smartlog({
|
|
logContext: {
|
|
company: 'fin.cx',
|
|
companyunit: 'skr',
|
|
containerName: 'Reports',
|
|
environment: 'local',
|
|
runtime: 'node',
|
|
zone: 'local',
|
|
},
|
|
});
|
|
this.ledger = new Ledger(skrType);
|
|
}
|
|
|
|
/**
|
|
* Generate Trial Balance
|
|
*/
|
|
public async getTrialBalance(
|
|
params?: IReportParams,
|
|
): Promise<ITrialBalanceReport> {
|
|
this.logger.log('info', 'Generating trial balance');
|
|
|
|
const accounts = await Account.getInstances({
|
|
skrType: this.skrType,
|
|
isActive: true,
|
|
});
|
|
|
|
const entries: ITrialBalanceEntry[] = [];
|
|
let totalDebits = 0;
|
|
let totalCredits = 0;
|
|
|
|
for (const account of accounts) {
|
|
// Get balance for the period if specified
|
|
const balance = params?.dateTo
|
|
? await this.ledger.getAccountBalance(
|
|
account.accountNumber,
|
|
params.dateTo,
|
|
)
|
|
: await this.ledger.getAccountBalance(account.accountNumber);
|
|
|
|
if (balance.debitTotal !== 0 || balance.creditTotal !== 0) {
|
|
const entry: ITrialBalanceEntry = {
|
|
accountNumber: account.accountNumber,
|
|
accountName: account.accountName,
|
|
debitBalance: balance.debitTotal,
|
|
creditBalance: balance.creditTotal,
|
|
netBalance: balance.balance,
|
|
};
|
|
|
|
entries.push(entry);
|
|
totalDebits += balance.debitTotal;
|
|
totalCredits += balance.creditTotal;
|
|
}
|
|
}
|
|
|
|
// Sort entries by account number
|
|
entries.sort((a, b) => a.accountNumber.localeCompare(b.accountNumber));
|
|
|
|
const report: ITrialBalanceReport = {
|
|
date: params?.dateTo || new Date(),
|
|
skrType: this.skrType,
|
|
entries,
|
|
totalDebits,
|
|
totalCredits,
|
|
isBalanced: Math.abs(totalDebits - totalCredits) < 0.01,
|
|
};
|
|
|
|
this.logger.log(
|
|
'info',
|
|
`Trial balance generated with ${entries.length} accounts`,
|
|
);
|
|
return report;
|
|
}
|
|
|
|
/**
|
|
* Generate Income Statement (P&L)
|
|
*/
|
|
public async getIncomeStatement(
|
|
params?: IReportParams,
|
|
): Promise<IIncomeStatement> {
|
|
this.logger.log('info', 'Generating income statement');
|
|
|
|
// Get revenue accounts
|
|
const revenueAccounts = await Account.getAccountsByType(
|
|
'revenue',
|
|
this.skrType,
|
|
);
|
|
const expenseAccounts = await Account.getAccountsByType(
|
|
'expense',
|
|
this.skrType,
|
|
);
|
|
|
|
const revenueEntries: IIncomeStatementEntry[] = [];
|
|
const expenseEntries: IIncomeStatementEntry[] = [];
|
|
let totalRevenue = 0;
|
|
let totalExpenses = 0;
|
|
|
|
// Process revenue accounts
|
|
for (const account of revenueAccounts) {
|
|
const balance = await this.getAccountBalanceForPeriod(account, params);
|
|
|
|
if (balance !== 0) {
|
|
const entry: IIncomeStatementEntry = {
|
|
accountNumber: account.accountNumber,
|
|
accountName: account.accountName,
|
|
amount: Math.abs(balance),
|
|
};
|
|
|
|
revenueEntries.push(entry);
|
|
totalRevenue += Math.abs(balance);
|
|
}
|
|
}
|
|
|
|
// Process expense accounts
|
|
for (const account of expenseAccounts) {
|
|
const balance = await this.getAccountBalanceForPeriod(account, params);
|
|
|
|
if (balance !== 0) {
|
|
const entry: IIncomeStatementEntry = {
|
|
accountNumber: account.accountNumber,
|
|
accountName: account.accountName,
|
|
amount: Math.abs(balance),
|
|
};
|
|
|
|
expenseEntries.push(entry);
|
|
totalExpenses += Math.abs(balance);
|
|
}
|
|
}
|
|
|
|
// Calculate percentages
|
|
revenueEntries.forEach((entry) => {
|
|
entry.percentage =
|
|
totalRevenue > 0 ? (entry.amount / totalRevenue) * 100 : 0;
|
|
});
|
|
|
|
expenseEntries.forEach((entry) => {
|
|
entry.percentage =
|
|
totalRevenue > 0 ? (entry.amount / totalRevenue) * 100 : 0;
|
|
});
|
|
|
|
// Sort entries by account number
|
|
revenueEntries.sort((a, b) =>
|
|
a.accountNumber.localeCompare(b.accountNumber),
|
|
);
|
|
expenseEntries.sort((a, b) =>
|
|
a.accountNumber.localeCompare(b.accountNumber),
|
|
);
|
|
|
|
const report: IIncomeStatement = {
|
|
date: params?.dateTo || new Date(),
|
|
skrType: this.skrType,
|
|
revenue: revenueEntries,
|
|
expenses: expenseEntries,
|
|
totalRevenue,
|
|
totalExpenses,
|
|
netIncome: totalRevenue - totalExpenses,
|
|
};
|
|
|
|
this.logger.log(
|
|
'info',
|
|
`Income statement generated: Revenue ${totalRevenue}, Expenses ${totalExpenses}`,
|
|
);
|
|
return report;
|
|
}
|
|
|
|
/**
|
|
* Generate Balance Sheet
|
|
*/
|
|
public async getBalanceSheet(params?: IReportParams): Promise<IBalanceSheet> {
|
|
this.logger.log('info', 'Generating balance sheet');
|
|
|
|
// Get accounts by type
|
|
const assetAccounts = await Account.getAccountsByType(
|
|
'asset',
|
|
this.skrType,
|
|
);
|
|
const liabilityAccounts = await Account.getAccountsByType(
|
|
'liability',
|
|
this.skrType,
|
|
);
|
|
const equityAccounts = await Account.getAccountsByType(
|
|
'equity',
|
|
this.skrType,
|
|
);
|
|
|
|
// Process assets
|
|
const currentAssets: IBalanceSheetEntry[] = [];
|
|
const fixedAssets: IBalanceSheetEntry[] = [];
|
|
let totalAssets = 0;
|
|
|
|
for (const account of assetAccounts) {
|
|
const balance = await this.getAccountBalanceForPeriod(account, params);
|
|
|
|
if (balance !== 0) {
|
|
const entry: IBalanceSheetEntry = {
|
|
accountNumber: account.accountNumber,
|
|
accountName: account.accountName,
|
|
amount: Math.abs(balance),
|
|
};
|
|
|
|
// Classify as current or fixed based on account class
|
|
if (account.accountClass === 1) {
|
|
currentAssets.push(entry);
|
|
} else {
|
|
fixedAssets.push(entry);
|
|
}
|
|
|
|
totalAssets += Math.abs(balance);
|
|
}
|
|
}
|
|
|
|
// Process liabilities
|
|
const currentLiabilities: IBalanceSheetEntry[] = [];
|
|
const longTermLiabilities: IBalanceSheetEntry[] = [];
|
|
let totalLiabilities = 0;
|
|
|
|
for (const account of liabilityAccounts) {
|
|
const balance = await this.getAccountBalanceForPeriod(account, params);
|
|
|
|
if (balance !== 0) {
|
|
const entry: IBalanceSheetEntry = {
|
|
accountNumber: account.accountNumber,
|
|
accountName: account.accountName,
|
|
amount: Math.abs(balance),
|
|
};
|
|
|
|
// Classify as current or long-term based on account number
|
|
if (
|
|
account.accountNumber.startsWith('16') ||
|
|
account.accountNumber.startsWith('17')
|
|
) {
|
|
currentLiabilities.push(entry);
|
|
} else {
|
|
longTermLiabilities.push(entry);
|
|
}
|
|
|
|
totalLiabilities += Math.abs(balance);
|
|
}
|
|
}
|
|
|
|
// Process equity
|
|
const equityEntries: IBalanceSheetEntry[] = [];
|
|
let totalEquity = 0;
|
|
|
|
for (const account of equityAccounts) {
|
|
const balance = await this.getAccountBalanceForPeriod(account, params);
|
|
|
|
if (balance !== 0) {
|
|
const entry: IBalanceSheetEntry = {
|
|
accountNumber: account.accountNumber,
|
|
accountName: account.accountName,
|
|
amount: Math.abs(balance),
|
|
};
|
|
|
|
equityEntries.push(entry);
|
|
totalEquity += Math.abs(balance);
|
|
}
|
|
}
|
|
|
|
// Add current year profit/loss
|
|
const incomeStatement = await this.getIncomeStatement(params);
|
|
if (incomeStatement.netIncome !== 0) {
|
|
equityEntries.push({
|
|
accountNumber: '9999',
|
|
accountName: 'Current Year Profit/Loss',
|
|
amount: Math.abs(incomeStatement.netIncome),
|
|
});
|
|
totalEquity += Math.abs(incomeStatement.netIncome);
|
|
}
|
|
|
|
// Sort entries
|
|
currentAssets.sort((a, b) =>
|
|
a.accountNumber.localeCompare(b.accountNumber),
|
|
);
|
|
fixedAssets.sort((a, b) => a.accountNumber.localeCompare(b.accountNumber));
|
|
currentLiabilities.sort((a, b) =>
|
|
a.accountNumber.localeCompare(b.accountNumber),
|
|
);
|
|
longTermLiabilities.sort((a, b) =>
|
|
a.accountNumber.localeCompare(b.accountNumber),
|
|
);
|
|
equityEntries.sort((a, b) =>
|
|
a.accountNumber.localeCompare(b.accountNumber),
|
|
);
|
|
|
|
const report: IBalanceSheet = {
|
|
date: params?.dateTo || new Date(),
|
|
skrType: this.skrType,
|
|
assets: {
|
|
current: currentAssets,
|
|
fixed: fixedAssets,
|
|
totalAssets,
|
|
},
|
|
liabilities: {
|
|
current: currentLiabilities,
|
|
longTerm: longTermLiabilities,
|
|
totalLiabilities,
|
|
},
|
|
equity: {
|
|
entries: equityEntries,
|
|
totalEquity,
|
|
},
|
|
isBalanced:
|
|
Math.abs(totalAssets - (totalLiabilities + totalEquity)) < 0.01,
|
|
};
|
|
|
|
this.logger.log(
|
|
'info',
|
|
`Balance sheet generated: Assets ${totalAssets}, Liabilities ${totalLiabilities}, Equity ${totalEquity}`,
|
|
);
|
|
return report;
|
|
}
|
|
|
|
/**
|
|
* Get account balance for a specific period
|
|
*/
|
|
private async getAccountBalanceForPeriod(
|
|
account: Account,
|
|
params?: IReportParams,
|
|
): Promise<number> {
|
|
let transactions = await Transaction.getTransactionsByAccount(
|
|
account.accountNumber,
|
|
this.skrType,
|
|
);
|
|
|
|
// Apply date filter if provided
|
|
if (params?.dateFrom || params?.dateTo) {
|
|
transactions = transactions.filter((transaction) => {
|
|
if (params.dateFrom && transaction.date < params.dateFrom) return false;
|
|
if (params.dateTo && transaction.date > params.dateTo) return false;
|
|
return true;
|
|
});
|
|
}
|
|
|
|
let debitTotal = 0;
|
|
let creditTotal = 0;
|
|
|
|
for (const transaction of transactions) {
|
|
if (transaction.debitAccount === account.accountNumber) {
|
|
debitTotal += transaction.amount;
|
|
}
|
|
if (transaction.creditAccount === account.accountNumber) {
|
|
creditTotal += transaction.amount;
|
|
}
|
|
}
|
|
|
|
// Calculate net balance based on account type
|
|
switch (account.accountType) {
|
|
case 'asset':
|
|
case 'expense':
|
|
return debitTotal - creditTotal;
|
|
case 'liability':
|
|
case 'equity':
|
|
case 'revenue':
|
|
return creditTotal - debitTotal;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate General Ledger report
|
|
*/
|
|
public async getGeneralLedger(params?: IReportParams): Promise<any> {
|
|
this.logger.log('info', 'Generating general ledger');
|
|
|
|
const accounts = await Account.getInstances({
|
|
skrType: this.skrType,
|
|
isActive: true,
|
|
});
|
|
|
|
const ledgerEntries = [];
|
|
|
|
for (const account of accounts) {
|
|
const transactions = await this.getAccountTransactions(
|
|
account.accountNumber,
|
|
params,
|
|
);
|
|
|
|
if (transactions.length > 0) {
|
|
let runningBalance = 0;
|
|
const accountEntries = [];
|
|
|
|
for (const transaction of transactions) {
|
|
const isDebit = transaction.debitAccount === account.accountNumber;
|
|
const amount = transaction.amount;
|
|
|
|
// Update running balance based on account type
|
|
if (
|
|
account.accountType === 'asset' ||
|
|
account.accountType === 'expense'
|
|
) {
|
|
runningBalance += isDebit ? amount : -amount;
|
|
} else {
|
|
runningBalance += isDebit ? -amount : amount;
|
|
}
|
|
|
|
accountEntries.push({
|
|
date: transaction.date,
|
|
reference: transaction.reference,
|
|
description: transaction.description,
|
|
debit: isDebit ? amount : 0,
|
|
credit: !isDebit ? amount : 0,
|
|
balance: runningBalance,
|
|
});
|
|
}
|
|
|
|
ledgerEntries.push({
|
|
accountNumber: account.accountNumber,
|
|
accountName: account.accountName,
|
|
accountType: account.accountType,
|
|
entries: accountEntries,
|
|
finalBalance: runningBalance,
|
|
});
|
|
}
|
|
}
|
|
|
|
return {
|
|
date: params?.dateTo || new Date(),
|
|
skrType: this.skrType,
|
|
accounts: ledgerEntries,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get account transactions for reporting
|
|
*/
|
|
private async getAccountTransactions(
|
|
accountNumber: string,
|
|
params?: IReportParams,
|
|
): Promise<Transaction[]> {
|
|
let transactions = await Transaction.getTransactionsByAccount(
|
|
accountNumber,
|
|
this.skrType,
|
|
);
|
|
|
|
// Apply date filter
|
|
if (params?.dateFrom || params?.dateTo) {
|
|
transactions = transactions.filter((transaction) => {
|
|
if (params.dateFrom && transaction.date < params.dateFrom) return false;
|
|
if (params.dateTo && transaction.date > params.dateTo) return false;
|
|
return true;
|
|
});
|
|
}
|
|
|
|
// Sort by date
|
|
transactions.sort((a, b) => a.date.getTime() - b.date.getTime());
|
|
|
|
return transactions;
|
|
}
|
|
|
|
/**
|
|
* Generate Cash Flow Statement
|
|
*/
|
|
public async getCashFlowStatement(params?: IReportParams): Promise<any> {
|
|
this.logger.log('info', 'Generating cash flow statement');
|
|
|
|
// Get cash and bank accounts
|
|
const cashAccounts = ['1000', '1100', '1200', '1210']; // Standard cash/bank accounts
|
|
|
|
let operatingCashFlow = 0;
|
|
let investingCashFlow = 0;
|
|
let financingCashFlow = 0;
|
|
|
|
for (const accountNumber of cashAccounts) {
|
|
const account = await Account.getAccountByNumber(
|
|
accountNumber,
|
|
this.skrType,
|
|
);
|
|
if (!account) continue;
|
|
|
|
const transactions = await this.getAccountTransactions(
|
|
accountNumber,
|
|
params,
|
|
);
|
|
|
|
for (const transaction of transactions) {
|
|
const otherAccount =
|
|
transaction.debitAccount === accountNumber
|
|
? transaction.creditAccount
|
|
: transaction.debitAccount;
|
|
|
|
const otherAccountObj = await Account.getAccountByNumber(
|
|
otherAccount,
|
|
this.skrType,
|
|
);
|
|
if (!otherAccountObj) continue;
|
|
|
|
const amount =
|
|
transaction.debitAccount === accountNumber
|
|
? transaction.amount
|
|
: -transaction.amount;
|
|
|
|
// Classify cash flow
|
|
if (
|
|
otherAccountObj.accountType === 'revenue' ||
|
|
otherAccountObj.accountType === 'expense'
|
|
) {
|
|
operatingCashFlow += amount;
|
|
} else if (otherAccountObj.accountClass === 0) {
|
|
// Fixed assets
|
|
investingCashFlow += amount;
|
|
} else if (
|
|
otherAccountObj.accountType === 'liability' ||
|
|
otherAccountObj.accountType === 'equity'
|
|
) {
|
|
financingCashFlow += amount;
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
date: params?.dateTo || new Date(),
|
|
skrType: this.skrType,
|
|
operatingActivities: operatingCashFlow,
|
|
investingActivities: investingCashFlow,
|
|
financingActivities: financingCashFlow,
|
|
netCashFlow: operatingCashFlow + investingCashFlow + financingCashFlow,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Export report to CSV format
|
|
*/
|
|
public async exportToCSV(
|
|
reportType: 'trial_balance' | 'income_statement' | 'balance_sheet',
|
|
params?: IReportParams,
|
|
): Promise<string> {
|
|
let csvContent = '';
|
|
|
|
switch (reportType) {
|
|
case 'trial_balance':
|
|
const trialBalance = await this.getTrialBalance(params);
|
|
csvContent = this.trialBalanceToCSV(trialBalance);
|
|
break;
|
|
|
|
case 'income_statement':
|
|
const incomeStatement = await this.getIncomeStatement(params);
|
|
csvContent = this.incomeStatementToCSV(incomeStatement);
|
|
break;
|
|
|
|
case 'balance_sheet':
|
|
const balanceSheet = await this.getBalanceSheet(params);
|
|
csvContent = this.balanceSheetToCSV(balanceSheet);
|
|
break;
|
|
}
|
|
|
|
return csvContent;
|
|
}
|
|
|
|
/**
|
|
* Convert trial balance to CSV
|
|
*/
|
|
private trialBalanceToCSV(report: ITrialBalanceReport): string {
|
|
const lines: string[] = [];
|
|
lines.push('"Account Number";"Account Name";"Debit";"Credit";"Balance"');
|
|
|
|
for (const entry of report.entries) {
|
|
lines.push(
|
|
`"${entry.accountNumber}";"${entry.accountName}";${entry.debitBalance};${entry.creditBalance};${entry.netBalance}`,
|
|
);
|
|
}
|
|
|
|
lines.push(
|
|
`"TOTAL";"";"${report.totalDebits}";"${report.totalCredits}";"""`,
|
|
);
|
|
|
|
return lines.join('\n');
|
|
}
|
|
|
|
/**
|
|
* Convert income statement to CSV
|
|
*/
|
|
private incomeStatementToCSV(report: IIncomeStatement): string {
|
|
const lines: string[] = [];
|
|
lines.push('"Type";"Account Number";"Account Name";"Amount";"Percentage"');
|
|
|
|
lines.push('"REVENUE";"";"";"";""');
|
|
for (const entry of report.revenue) {
|
|
lines.push(
|
|
`"Revenue";"${entry.accountNumber}";"${entry.accountName}";${entry.amount};${entry.percentage?.toFixed(2)}%`,
|
|
);
|
|
}
|
|
|
|
lines.push(`"Total Revenue";"";"";"${report.totalRevenue}";"""`);
|
|
lines.push('"";"";"";"";""');
|
|
|
|
lines.push('"EXPENSES";"";"";"";""');
|
|
for (const entry of report.expenses) {
|
|
lines.push(
|
|
`"Expense";"${entry.accountNumber}";"${entry.accountName}";${entry.amount};${entry.percentage?.toFixed(2)}%`,
|
|
);
|
|
}
|
|
|
|
lines.push(`"Total Expenses";"";"";"${report.totalExpenses}";"""`);
|
|
lines.push('"";"";"";"";""');
|
|
lines.push(`"NET INCOME";"";"";"${report.netIncome}";"""`);
|
|
|
|
return lines.join('\n');
|
|
}
|
|
|
|
/**
|
|
* Convert balance sheet to CSV
|
|
*/
|
|
private balanceSheetToCSV(report: IBalanceSheet): string {
|
|
const lines: string[] = [];
|
|
lines.push('"Category";"Account Number";"Account Name";"Amount"');
|
|
|
|
lines.push('"ASSETS";"";"";"";');
|
|
lines.push('"Current Assets";"";"";"";');
|
|
for (const entry of report.assets.current) {
|
|
lines.push(
|
|
`"";"${entry.accountNumber}";"${entry.accountName}";${entry.amount}`,
|
|
);
|
|
}
|
|
|
|
lines.push('"Fixed Assets";"";"";"";');
|
|
for (const entry of report.assets.fixed) {
|
|
lines.push(
|
|
`"";"${entry.accountNumber}";"${entry.accountName}";${entry.amount}`,
|
|
);
|
|
}
|
|
|
|
lines.push(`"Total Assets";"";"";"${report.assets.totalAssets}"`);
|
|
lines.push('"";"";"";"";');
|
|
|
|
lines.push('"LIABILITIES";"";"";"";');
|
|
lines.push('"Current Liabilities";"";"";"";');
|
|
for (const entry of report.liabilities.current) {
|
|
lines.push(
|
|
`"";"${entry.accountNumber}";"${entry.accountName}";${entry.amount}`,
|
|
);
|
|
}
|
|
|
|
lines.push('"Long-term Liabilities";"";"";"";');
|
|
for (const entry of report.liabilities.longTerm) {
|
|
lines.push(
|
|
`"";"${entry.accountNumber}";"${entry.accountName}";${entry.amount}`,
|
|
);
|
|
}
|
|
|
|
lines.push(
|
|
`"Total Liabilities";"";"";"${report.liabilities.totalLiabilities}"`,
|
|
);
|
|
lines.push('"";"";"";"";');
|
|
|
|
lines.push('"EQUITY";"";"";"";');
|
|
for (const entry of report.equity.entries) {
|
|
lines.push(
|
|
`"";"${entry.accountNumber}";"${entry.accountName}";${entry.amount}`,
|
|
);
|
|
}
|
|
|
|
lines.push(`"Total Equity";"";"";"${report.equity.totalEquity}"`);
|
|
lines.push('"";"";"";"";');
|
|
lines.push(
|
|
`"Total Liabilities + Equity";"";"";"${report.liabilities.totalLiabilities + report.equity.totalEquity}"`,
|
|
);
|
|
|
|
return lines.join('\n');
|
|
}
|
|
|
|
/**
|
|
* Export to DATEV format
|
|
*/
|
|
public async exportToDATEV(params?: IReportParams): Promise<string> {
|
|
// DATEV format is specific to German accounting software
|
|
// This is a simplified implementation
|
|
const transactions = await Transaction.getInstances({
|
|
skrType: this.skrType,
|
|
status: 'posted',
|
|
});
|
|
|
|
const lines: string[] = [];
|
|
|
|
// DATEV header
|
|
lines.push('EXTF;510;21;"Buchungsstapel";1;;;;;;;;;;;;;;');
|
|
|
|
for (const transaction of transactions) {
|
|
const date = transaction.date
|
|
.toISOString()
|
|
.split('T')[0]
|
|
.replace(/-/g, '');
|
|
const line = [
|
|
transaction.amount.toFixed(2).replace('.', ','),
|
|
'S',
|
|
'EUR',
|
|
'',
|
|
'',
|
|
transaction.debitAccount,
|
|
transaction.creditAccount,
|
|
'',
|
|
date,
|
|
'',
|
|
transaction.description.substring(0, 60),
|
|
'',
|
|
].join(';');
|
|
|
|
lines.push(line);
|
|
}
|
|
|
|
return lines.join('\n');
|
|
}
|
|
}
|