Files
skr/ts/skr.classes.chartofaccounts.ts
Juergen Kunz 8a9056e767
Some checks failed
Default (tags) / security (push) Successful in 44s
Default (tags) / test (push) Failing after 4m4s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
feat(core): initial release of SKR03/SKR04 German accounting standards implementation
- 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
2025-08-09 12:00:40 +00:00

509 lines
14 KiB
TypeScript

import * as plugins from './plugins.js';
import { getDb, closeDb } from './skr.database.js';
import { Account } from './skr.classes.account.js';
import { Transaction } from './skr.classes.transaction.js';
import { JournalEntry } from './skr.classes.journalentry.js';
import { SKR03_ACCOUNTS, SKR03_ACCOUNT_CLASSES } from './skr03.data.js';
import { SKR04_ACCOUNTS, SKR04_ACCOUNT_CLASSES } from './skr04.data.js';
import type {
IDatabaseConfig,
TSKRType,
IAccountData,
IAccountFilter,
ITransactionFilter,
ITransactionData,
IJournalEntry,
} from './skr.types.js';
export class ChartOfAccounts {
private logger: plugins.smartlog.Smartlog;
private initialized: boolean = false;
private skrType: TSKRType | null = null;
constructor(private config?: IDatabaseConfig) {
this.logger = new plugins.smartlog.Smartlog({
logContext: {
company: 'fin.cx',
companyunit: 'skr',
containerName: 'ChartOfAccounts',
environment: 'local',
runtime: 'node',
zone: 'local',
},
});
this.logger.enableConsole();
}
/**
* Initialize the database connection
*/
public async init(): Promise<void> {
if (this.initialized) {
this.logger.log('info', 'ChartOfAccounts already initialized');
return;
}
if (!this.config) {
throw new Error('Database configuration required for initialization');
}
await getDb(this.config);
this.initialized = true;
this.logger.log('info', 'ChartOfAccounts initialized successfully');
}
/**
* Initialize SKR03 chart of accounts
*/
public async initializeSKR03(): Promise<void> {
await this.init();
this.logger.log('info', 'Initializing SKR03 chart of accounts');
// Check if SKR03 accounts already exist
const existingAccounts = await Account.getInstances({ skrType: 'SKR03' });
if (existingAccounts.length > 0) {
this.logger.log(
'info',
`SKR03 already initialized with ${existingAccounts.length} accounts`,
);
this.skrType = 'SKR03';
return;
}
// Create all SKR03 accounts
const accounts: Account[] = [];
for (const accountData of SKR03_ACCOUNTS) {
const account = await Account.createAccount(accountData);
accounts.push(account);
}
this.skrType = 'SKR03';
this.logger.log(
'info',
`Successfully initialized SKR03 with ${accounts.length} accounts`,
);
}
/**
* Initialize SKR04 chart of accounts
*/
public async initializeSKR04(): Promise<void> {
await this.init();
this.logger.log('info', 'Initializing SKR04 chart of accounts');
// Check if SKR04 accounts already exist
const existingAccounts = await Account.getInstances({ skrType: 'SKR04' });
if (existingAccounts.length > 0) {
this.logger.log(
'info',
`SKR04 already initialized with ${existingAccounts.length} accounts`,
);
this.skrType = 'SKR04';
return;
}
// Create all SKR04 accounts
const accounts: Account[] = [];
for (const accountData of SKR04_ACCOUNTS) {
const account = await Account.createAccount(accountData);
accounts.push(account);
}
this.skrType = 'SKR04';
this.logger.log(
'info',
`Successfully initialized SKR04 with ${accounts.length} accounts`,
);
}
/**
* Get the current SKR type
*/
public getSKRType(): TSKRType | null {
return this.skrType;
}
/**
* Set the active SKR type
*/
public setSKRType(skrType: TSKRType): void {
this.skrType = skrType;
}
/**
* Get account by number
*/
public async getAccountByNumber(
accountNumber: string,
): Promise<Account | null> {
if (!this.skrType) {
throw new Error('SKR type not set. Initialize SKR03 or SKR04 first.');
}
return await Account.getAccountByNumber(accountNumber, this.skrType);
}
/**
* Get accounts by class
*/
public async getAccountsByClass(accountClass: number): Promise<Account[]> {
if (!this.skrType) {
throw new Error('SKR type not set. Initialize SKR03 or SKR04 first.');
}
return await Account.getAccountsByClass(accountClass, this.skrType);
}
/**
* Get accounts by type
*/
public async getAccountsByType(
accountType: IAccountData['accountType'],
): Promise<Account[]> {
if (!this.skrType) {
throw new Error('SKR type not set. Initialize SKR03 or SKR04 first.');
}
return await Account.getAccountsByType(accountType, this.skrType);
}
/**
* Create a custom account
*/
public async createCustomAccount(
accountData: Partial<IAccountData>,
): Promise<Account> {
if (!this.skrType) {
throw new Error('SKR type not set. Initialize SKR03 or SKR04 first.');
}
// Ensure the account uses the current SKR type
const fullAccountData: IAccountData = {
accountNumber: accountData.accountNumber || '',
accountName: accountData.accountName || '',
accountClass: accountData.accountClass || 0,
accountType: accountData.accountType || 'asset',
skrType: this.skrType,
description: accountData.description,
vatRate: accountData.vatRate,
isActive:
accountData.isActive !== undefined ? accountData.isActive : true,
};
// Validate account number doesn't already exist
const existing = await this.getAccountByNumber(
fullAccountData.accountNumber,
);
if (existing) {
throw new Error(
`Account ${fullAccountData.accountNumber} already exists`,
);
}
return await Account.createAccount(fullAccountData);
}
/**
* Update an existing account
*/
public async updateAccount(
accountNumber: string,
updates: Partial<IAccountData>,
): Promise<Account> {
const account = await this.getAccountByNumber(accountNumber);
if (!account) {
throw new Error(`Account ${accountNumber} not found`);
}
// Apply updates
if (updates.accountName !== undefined)
account.accountName = updates.accountName;
if (updates.description !== undefined)
account.description = updates.description;
if (updates.vatRate !== undefined) account.vatRate = updates.vatRate;
if (updates.isActive !== undefined) account.isActive = updates.isActive;
account.updatedAt = new Date();
await account.save();
return account;
}
/**
* Delete a custom account (only non-system accounts)
*/
public async deleteAccount(accountNumber: string): Promise<void> {
const account = await this.getAccountByNumber(accountNumber);
if (!account) {
throw new Error(`Account ${accountNumber} not found`);
}
if (account.isSystemAccount) {
throw new Error(`Cannot delete system account ${accountNumber}`);
}
// Check if account has transactions
const transactions = await Transaction.getTransactionsByAccount(
accountNumber,
account.skrType,
);
if (transactions.length > 0) {
throw new Error(
`Cannot delete account ${accountNumber} with existing transactions`,
);
}
await account.delete();
}
/**
* Search accounts
*/
public async searchAccounts(searchTerm: string): Promise<Account[]> {
return await Account.searchAccounts(searchTerm, this.skrType);
}
/**
* Get all accounts
*/
public async getAllAccounts(filter?: IAccountFilter): Promise<Account[]> {
const query: any = {};
if (this.skrType) {
query.skrType = this.skrType;
}
if (filter) {
if (filter.accountClass !== undefined)
query.accountClass = filter.accountClass;
if (filter.accountType !== undefined)
query.accountType = filter.accountType;
if (filter.isActive !== undefined) query.isActive = filter.isActive;
}
const accounts = await Account.getInstances(query);
// Apply text search if provided
if (filter?.searchTerm) {
const lowerSearchTerm = filter.searchTerm.toLowerCase();
return accounts.filter(
(account) =>
account.accountNumber.includes(filter.searchTerm) ||
account.accountName.toLowerCase().includes(lowerSearchTerm) ||
account.description.toLowerCase().includes(lowerSearchTerm),
);
}
return accounts;
}
/**
* Post a simple transaction
*/
public async postTransaction(
transactionData: ITransactionData,
): Promise<Transaction> {
if (!this.skrType) {
throw new Error('SKR type not set. Initialize SKR03 or SKR04 first.');
}
// Ensure the transaction uses the current SKR type
const fullTransactionData: ITransactionData = {
...transactionData,
skrType: this.skrType,
};
return await Transaction.createTransaction(fullTransactionData);
}
/**
* Post a journal entry
*/
public async postJournalEntry(
journalData: IJournalEntry,
): Promise<JournalEntry> {
if (!this.skrType) {
throw new Error('SKR type not set. Initialize SKR03 or SKR04 first.');
}
// Ensure the journal entry uses the current SKR type
const fullJournalData: IJournalEntry = {
...journalData,
skrType: this.skrType,
};
const journalEntry = await JournalEntry.createJournalEntry(fullJournalData);
await journalEntry.post();
return journalEntry;
}
/**
* Get transactions for an account
*/
public async getAccountTransactions(
accountNumber: string,
): Promise<Transaction[]> {
if (!this.skrType) {
throw new Error('SKR type not set. Initialize SKR03 or SKR04 first.');
}
return await Transaction.getTransactionsByAccount(
accountNumber,
this.skrType,
);
}
/**
* Get transactions by filter
*/
public async getTransactions(
filter?: ITransactionFilter,
): Promise<Transaction[]> {
if (!this.skrType) {
throw new Error('SKR type not set. Initialize SKR03 or SKR04 first.');
}
const query: any = {
skrType: this.skrType,
status: 'posted',
};
if (filter) {
if (filter.dateFrom || filter.dateTo) {
query.date = {};
if (filter.dateFrom) query.date.$gte = filter.dateFrom;
if (filter.dateTo) query.date.$lte = filter.dateTo;
}
if (filter.accountNumber) {
query.$or = [
{ debitAccount: filter.accountNumber },
{ creditAccount: filter.accountNumber },
];
}
if (filter.minAmount || filter.maxAmount) {
query.amount = {};
if (filter.minAmount) query.amount.$gte = filter.minAmount;
if (filter.maxAmount) query.amount.$lte = filter.maxAmount;
}
}
const transactions = await Transaction.getInstances(query);
// Apply text search if provided
if (filter?.searchTerm) {
const lowerSearchTerm = filter.searchTerm.toLowerCase();
return transactions.filter(
(transaction) =>
transaction.description.toLowerCase().includes(lowerSearchTerm) ||
transaction.reference.toLowerCase().includes(lowerSearchTerm),
);
}
return transactions;
}
/**
* Reverse a transaction
*/
public async reverseTransaction(transactionId: string): Promise<Transaction> {
const transaction = await Transaction.getTransactionById(transactionId);
if (!transaction) {
throw new Error(`Transaction ${transactionId} not found`);
}
return await transaction.reverseTransaction();
}
/**
* Get account class description
*/
public getAccountClassDescription(accountClass: number): string {
if (!this.skrType) {
throw new Error('SKR type not set. Initialize SKR03 or SKR04 first.');
}
const classes =
this.skrType === 'SKR03' ? SKR03_ACCOUNT_CLASSES : SKR04_ACCOUNT_CLASSES;
return (
classes[accountClass as keyof typeof classes] || `Class ${accountClass}`
);
}
/**
* Import accounts from CSV
*/
public async importAccountsFromCSV(csvContent: string): Promise<number> {
if (!this.skrType) {
throw new Error('SKR type not set. Initialize SKR03 or SKR04 first.');
}
const lines = csvContent.split('\n').filter((line) => line.trim());
let importedCount = 0;
for (const line of lines) {
// Parse CSV line (expecting format: "account";"name";"description";"type";"active")
const parts = line
.split(';')
.map((part) => part.replace(/"/g, '').trim());
if (parts.length >= 5) {
const accountData: IAccountData = {
accountNumber: parts[0],
accountName: parts[1],
accountClass: parseInt(parts[0][0]),
accountType: parts[3] as IAccountData['accountType'],
skrType: this.skrType,
description: parts[2],
isActive:
parts[4].toLowerCase() === 'standard' ||
parts[4].toLowerCase() === 'active',
};
try {
await this.createCustomAccount(accountData);
importedCount++;
} catch (error) {
this.logger.log(
'warn',
`Failed to import account ${parts[0]}: ${error.message}`,
);
}
}
}
return importedCount;
}
/**
* Export accounts to CSV
*/
public async exportAccountsToCSV(): Promise<string> {
const accounts = await this.getAllAccounts();
const csvLines: string[] = [];
csvLines.push('"Account";"Name";"Description";"Type";"Active"');
for (const account of accounts) {
csvLines.push(
`"${account.accountNumber}";"${account.accountName}";"${account.description}";"${account.accountType}";"${account.isActive ? 'Active' : 'Inactive'}"`,
);
}
return csvLines.join('\n');
}
/**
* Close the database connection
*/
public async close(): Promise<void> {
await closeDb();
this.initialized = false;
this.logger.log('info', 'ChartOfAccounts closed');
}
}