feat(core): initial release of SKR03/SKR04 German accounting standards implementation
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

- 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
This commit is contained in:
2025-08-09 12:00:40 +00:00
commit 8a9056e767
31 changed files with 16560 additions and 0 deletions

10
ts/index.ts Normal file
View File

@@ -0,0 +1,10 @@
export * from './skr.types.js';
export * from './skr.classes.account.js';
export * from './skr.classes.transaction.js';
export * from './skr.classes.journalentry.js';
export * from './skr.classes.chartofaccounts.js';
export * from './skr.classes.ledger.js';
export * from './skr.classes.reports.js';
export * from './skr.api.js';
export * from './skr03.data.js';
export * from './skr04.data.js';

7
ts/plugins.ts Normal file
View File

@@ -0,0 +1,7 @@
// @push.rocks scope
import * as smartdata from '@push.rocks/smartdata';
import * as smartunique from '@push.rocks/smartunique';
import * as smarttime from '@push.rocks/smarttime';
import * as smartlog from '@push.rocks/smartlog';
export { smartdata, smartunique, smarttime, smartlog };

533
ts/skr.api.ts Normal file
View File

@@ -0,0 +1,533 @@
import * as plugins from './plugins.js';
import { ChartOfAccounts } from './skr.classes.chartofaccounts.js';
import { Ledger } from './skr.classes.ledger.js';
import { Reports } from './skr.classes.reports.js';
import { Account } from './skr.classes.account.js';
import { Transaction } from './skr.classes.transaction.js';
import { JournalEntry } from './skr.classes.journalentry.js';
import type {
IDatabaseConfig,
TSKRType,
IAccountData,
IAccountFilter,
ITransactionData,
ITransactionFilter,
IJournalEntry,
IReportParams,
ITrialBalanceReport,
IIncomeStatement,
IBalanceSheet,
} from './skr.types.js';
/**
* Main API class for SKR accounting operations
*/
export class SkrApi {
private chartOfAccounts: ChartOfAccounts;
private ledger: Ledger | null = null;
private reports: Reports | null = null;
private logger: plugins.smartlog.Smartlog;
private initialized: boolean = false;
private currentSKRType: TSKRType | null = null;
constructor(private config: IDatabaseConfig) {
this.chartOfAccounts = new ChartOfAccounts(config);
this.logger = new plugins.smartlog.Smartlog({
logContext: {
company: 'fin.cx',
companyunit: 'skr',
containerName: 'SkrApi',
environment: 'local',
runtime: 'node',
zone: 'local',
},
});
}
/**
* Initialize the API with specified SKR type
*/
public async initialize(skrType: TSKRType): Promise<void> {
this.logger.log('info', `Initializing SKR API with ${skrType}`);
// Initialize chart of accounts
if (skrType === 'SKR03') {
await this.chartOfAccounts.initializeSKR03();
} else if (skrType === 'SKR04') {
await this.chartOfAccounts.initializeSKR04();
} else {
throw new Error(`Invalid SKR type: ${skrType}`);
}
this.currentSKRType = skrType;
this.ledger = new Ledger(skrType);
this.reports = new Reports(skrType);
this.initialized = true;
this.logger.log('info', 'SKR API initialized successfully');
}
/**
* Ensure API is initialized
*/
private ensureInitialized(): void {
if (!this.initialized || !this.currentSKRType) {
throw new Error('API not initialized. Call initialize() first.');
}
}
// ========== Account Management ==========
/**
* Create a new account
*/
public async createAccount(
accountData: Partial<IAccountData>,
): Promise<Account> {
this.ensureInitialized();
return await this.chartOfAccounts.createCustomAccount(accountData);
}
/**
* Get account by number
*/
public async getAccount(accountNumber: string): Promise<Account | null> {
this.ensureInitialized();
return await this.chartOfAccounts.getAccountByNumber(accountNumber);
}
/**
* Update an account
*/
public async updateAccount(
accountNumber: string,
updates: Partial<IAccountData>,
): Promise<Account> {
this.ensureInitialized();
return await this.chartOfAccounts.updateAccount(accountNumber, updates);
}
/**
* Delete an account
*/
public async deleteAccount(accountNumber: string): Promise<void> {
this.ensureInitialized();
await this.chartOfAccounts.deleteAccount(accountNumber);
}
/**
* List accounts with optional filter
*/
public async listAccounts(filter?: IAccountFilter): Promise<Account[]> {
this.ensureInitialized();
return await this.chartOfAccounts.getAllAccounts(filter);
}
/**
* Search accounts by term
*/
public async searchAccounts(searchTerm: string): Promise<Account[]> {
this.ensureInitialized();
return await this.chartOfAccounts.searchAccounts(searchTerm);
}
/**
* Get accounts by class
*/
public async getAccountsByClass(accountClass: number): Promise<Account[]> {
this.ensureInitialized();
return await this.chartOfAccounts.getAccountsByClass(accountClass);
}
/**
* Get accounts by type
*/
public async getAccountsByType(
accountType: IAccountData['accountType'],
): Promise<Account[]> {
this.ensureInitialized();
return await this.chartOfAccounts.getAccountsByType(accountType);
}
// ========== Transaction Management ==========
/**
* Post a simple transaction
*/
public async postTransaction(
transactionData: ITransactionData,
): Promise<Transaction> {
this.ensureInitialized();
return await this.chartOfAccounts.postTransaction(transactionData);
}
/**
* Post a journal entry
*/
public async postJournalEntry(
journalData: IJournalEntry,
): Promise<JournalEntry> {
this.ensureInitialized();
return await this.chartOfAccounts.postJournalEntry(journalData);
}
/**
* Get transaction by ID
*/
public async getTransaction(
transactionId: string,
): Promise<Transaction | null> {
this.ensureInitialized();
return await Transaction.getTransactionById(transactionId);
}
/**
* List transactions with optional filter
*/
public async listTransactions(
filter?: ITransactionFilter,
): Promise<Transaction[]> {
this.ensureInitialized();
return await this.chartOfAccounts.getTransactions(filter);
}
/**
* Get transactions for specific account
*/
public async getAccountTransactions(
accountNumber: string,
): Promise<Transaction[]> {
this.ensureInitialized();
return await this.chartOfAccounts.getAccountTransactions(accountNumber);
}
/**
* Reverse a transaction
*/
public async reverseTransaction(transactionId: string): Promise<Transaction> {
this.ensureInitialized();
return await this.chartOfAccounts.reverseTransaction(transactionId);
}
/**
* Reverse a journal entry
*/
public async reverseJournalEntry(journalId: string): Promise<JournalEntry> {
this.ensureInitialized();
if (!this.ledger) throw new Error('Ledger not initialized');
return await this.ledger.reverseJournalEntry(journalId);
}
// ========== Reporting ==========
/**
* Generate trial balance
*/
public async generateTrialBalance(
params?: IReportParams,
): Promise<ITrialBalanceReport> {
this.ensureInitialized();
if (!this.reports) throw new Error('Reports not initialized');
return await this.reports.getTrialBalance(params);
}
/**
* Generate income statement
*/
public async generateIncomeStatement(
params?: IReportParams,
): Promise<IIncomeStatement> {
this.ensureInitialized();
if (!this.reports) throw new Error('Reports not initialized');
return await this.reports.getIncomeStatement(params);
}
/**
* Generate balance sheet
*/
public async generateBalanceSheet(
params?: IReportParams,
): Promise<IBalanceSheet> {
this.ensureInitialized();
if (!this.reports) throw new Error('Reports not initialized');
return await this.reports.getBalanceSheet(params);
}
/**
* Generate general ledger
*/
public async generateGeneralLedger(params?: IReportParams): Promise<any> {
this.ensureInitialized();
if (!this.reports) throw new Error('Reports not initialized');
return await this.reports.getGeneralLedger(params);
}
/**
* Generate cash flow statement
*/
public async generateCashFlowStatement(params?: IReportParams): Promise<any> {
this.ensureInitialized();
if (!this.reports) throw new Error('Reports not initialized');
return await this.reports.getCashFlowStatement(params);
}
/**
* Export report to CSV
*/
public async exportReportToCSV(
reportType: 'trial_balance' | 'income_statement' | 'balance_sheet',
params?: IReportParams,
): Promise<string> {
this.ensureInitialized();
if (!this.reports) throw new Error('Reports not initialized');
return await this.reports.exportToCSV(reportType, params);
}
/**
* Export to DATEV format
*/
public async exportToDATEV(params?: IReportParams): Promise<string> {
this.ensureInitialized();
if (!this.reports) throw new Error('Reports not initialized');
return await this.reports.exportToDATEV(params);
}
// ========== Period Management ==========
/**
* Close accounting period
*/
public async closePeriod(
period: string,
closingAccountNumber?: string,
): Promise<JournalEntry[]> {
this.ensureInitialized();
if (!this.ledger) throw new Error('Ledger not initialized');
return await this.ledger.closeAccountingPeriod(
period,
closingAccountNumber,
);
}
/**
* Get account balance
*/
public async getAccountBalance(
accountNumber: string,
asOfDate?: Date,
): Promise<any> {
this.ensureInitialized();
if (!this.ledger) throw new Error('Ledger not initialized');
return await this.ledger.getAccountBalance(accountNumber, asOfDate);
}
/**
* Recalculate all account balances
*/
public async recalculateBalances(): Promise<void> {
this.ensureInitialized();
if (!this.ledger) throw new Error('Ledger not initialized');
await this.ledger.recalculateAllBalances();
}
// ========== Import/Export ==========
/**
* Import accounts from CSV
*/
public async importAccountsFromCSV(csvContent: string): Promise<number> {
this.ensureInitialized();
return await this.chartOfAccounts.importAccountsFromCSV(csvContent);
}
/**
* Export accounts to CSV
*/
public async exportAccountsToCSV(): Promise<string> {
this.ensureInitialized();
return await this.chartOfAccounts.exportAccountsToCSV();
}
// ========== Utility Methods ==========
/**
* Get current SKR type
*/
public getSKRType(): TSKRType | null {
return this.currentSKRType;
}
/**
* Get account class description
*/
public getAccountClassDescription(accountClass: number): string {
this.ensureInitialized();
return this.chartOfAccounts.getAccountClassDescription(accountClass);
}
/**
* Validate double-entry rules
*/
public validateDoubleEntry(
debitAmount: number,
creditAmount: number,
): boolean {
if (!this.ledger) throw new Error('Ledger not initialized');
return this.ledger.validateDoubleEntry(debitAmount, creditAmount);
}
/**
* Get unbalanced transactions (for audit)
*/
public async getUnbalancedTransactions(): Promise<Transaction[]> {
this.ensureInitialized();
if (!this.ledger) throw new Error('Ledger not initialized');
return await this.ledger.getUnbalancedTransactions();
}
/**
* Close the API and database connection
*/
public async close(): Promise<void> {
await this.chartOfAccounts.close();
this.initialized = false;
this.currentSKRType = null;
this.ledger = null;
this.reports = null;
this.logger.log('info', 'SKR API closed');
}
// ========== Batch Operations ==========
/**
* Post multiple transactions
*/
public async postBatchTransactions(
transactions: ITransactionData[],
): Promise<Transaction[]> {
this.ensureInitialized();
const results: Transaction[] = [];
const errors: Array<{ index: number; error: string }> = [];
for (let i = 0; i < transactions.length; i++) {
try {
const transaction = await this.postTransaction(transactions[i]);
results.push(transaction);
} catch (error) {
errors.push({ index: i, error: error.message });
}
}
if (errors.length > 0) {
this.logger.log(
'warn',
`Batch transaction posting completed with ${errors.length} errors`,
);
throw new Error(
`Batch posting failed for ${errors.length} transactions: ${JSON.stringify(errors)}`,
);
}
return results;
}
/**
* Create multiple accounts
*/
public async createBatchAccounts(
accounts: IAccountData[],
): Promise<Account[]> {
this.ensureInitialized();
const results: Account[] = [];
const errors: Array<{ index: number; error: string }> = [];
for (let i = 0; i < accounts.length; i++) {
try {
const account = await this.createAccount(accounts[i]);
results.push(account);
} catch (error) {
errors.push({ index: i, error: error.message });
}
}
if (errors.length > 0) {
this.logger.log(
'warn',
`Batch account creation completed with ${errors.length} errors`,
);
throw new Error(
`Batch creation failed for ${errors.length} accounts: ${JSON.stringify(errors)}`,
);
}
return results;
}
// ========== Pagination Support ==========
/**
* Get paginated accounts
*/
public async getAccountsPaginated(
page: number = 1,
pageSize: number = 50,
filter?: IAccountFilter,
): Promise<{
data: Account[];
total: number;
page: number;
pageSize: number;
totalPages: number;
}> {
this.ensureInitialized();
const allAccounts = await this.listAccounts(filter);
const total = allAccounts.length;
const totalPages = Math.ceil(total / pageSize);
const start = (page - 1) * pageSize;
const end = start + pageSize;
const data = allAccounts.slice(start, end);
return {
data,
total,
page,
pageSize,
totalPages,
};
}
/**
* Get paginated transactions
*/
public async getTransactionsPaginated(
page: number = 1,
pageSize: number = 50,
filter?: ITransactionFilter,
): Promise<{
data: Transaction[];
total: number;
page: number;
pageSize: number;
totalPages: number;
}> {
this.ensureInitialized();
const allTransactions = await this.listTransactions(filter);
const total = allTransactions.length;
const totalPages = Math.ceil(total / pageSize);
const start = (page - 1) * pageSize;
const end = start + pageSize;
const data = allTransactions.slice(start, end);
return {
data,
total,
page,
pageSize,
totalPages,
};
}
}

238
ts/skr.classes.account.ts Normal file
View File

@@ -0,0 +1,238 @@
import * as plugins from './plugins.js';
import { getDb, getDbSync } from './skr.database.js';
import type { TAccountType, TSKRType, IAccountData } from './skr.types.js';
const { SmartDataDbDoc, svDb, unI, index, searchable } = plugins.smartdata;
@plugins.smartdata.Collection(() => getDbSync())
export class Account extends SmartDataDbDoc<Account, Account> {
@unI()
public id: string;
@svDb()
@index()
public accountNumber: string;
@svDb()
@searchable()
public accountName: string;
@svDb()
@index()
public accountClass: number;
@svDb()
public accountGroup: number;
@svDb()
public accountSubgroup: number;
@svDb()
public accountType: TAccountType;
@svDb()
@index()
public skrType: TSKRType;
@svDb()
@searchable()
public description: string;
@svDb()
public vatRate: number;
@svDb()
public balance: number;
@svDb()
public debitTotal: number;
@svDb()
public creditTotal: number;
@svDb()
public isActive: boolean;
@svDb()
public isSystemAccount: boolean;
@svDb()
public createdAt: Date;
@svDb()
public updatedAt: Date;
constructor(data?: Partial<IAccountData>) {
super();
if (data) {
this.id = plugins.smartunique.shortId();
this.accountNumber = data.accountNumber || '';
this.accountName = data.accountName || '';
this.accountClass = data.accountClass || 0;
this.accountType = data.accountType || 'asset';
this.skrType = data.skrType || 'SKR03';
this.description = data.description || '';
this.vatRate = data.vatRate || 0;
this.isActive = data.isActive !== undefined ? data.isActive : true;
// Parse account structure from number
if (this.accountNumber && this.accountNumber.length === 4) {
this.accountClass = parseInt(this.accountNumber[0]);
this.accountGroup = parseInt(this.accountNumber[1]);
this.accountSubgroup = parseInt(this.accountNumber[2]);
} else {
this.accountGroup = 0;
this.accountSubgroup = 0;
}
this.balance = 0;
this.debitTotal = 0;
this.creditTotal = 0;
this.isSystemAccount = true;
this.createdAt = new Date();
this.updatedAt = new Date();
}
}
public static async createAccount(data: IAccountData): Promise<Account> {
const account = new Account(data);
await account.save();
return account;
}
public static async getAccountByNumber(
accountNumber: string,
skrType: TSKRType,
): Promise<Account | null> {
const account = await Account.getInstance({
accountNumber,
skrType,
});
return account;
}
public static async getAccountsByClass(
accountClass: number,
skrType: TSKRType,
): Promise<Account[]> {
const accounts = await Account.getInstances({
accountClass,
skrType,
isActive: true,
});
return accounts;
}
public static async getAccountsByType(
accountType: TAccountType,
skrType: TSKRType,
): Promise<Account[]> {
const accounts = await Account.getInstances({
accountType,
skrType,
isActive: true,
});
return accounts;
}
public static async searchAccounts(
searchTerm: string,
skrType?: TSKRType,
): Promise<Account[]> {
const query: any = {};
if (skrType) {
query.skrType = skrType;
}
const accounts = await Account.getInstances(query);
// Filter by search term
const lowerSearchTerm = searchTerm.toLowerCase();
return accounts.filter(
(account) =>
account.accountNumber.includes(searchTerm) ||
account.accountName.toLowerCase().includes(lowerSearchTerm) ||
account.description.toLowerCase().includes(lowerSearchTerm),
);
}
public async updateBalance(
debitAmount: number = 0,
creditAmount: number = 0,
): Promise<void> {
this.debitTotal += debitAmount;
this.creditTotal += creditAmount;
// Calculate balance based on account type
switch (this.accountType) {
case 'asset':
case 'expense':
// Normal debit accounts
this.balance = this.debitTotal - this.creditTotal;
break;
case 'liability':
case 'equity':
case 'revenue':
// Normal credit accounts
this.balance = this.creditTotal - this.debitTotal;
break;
}
this.updatedAt = new Date();
await this.save();
}
public async deactivate(): Promise<void> {
this.isActive = false;
this.updatedAt = new Date();
await this.save();
}
public async activate(): Promise<void> {
this.isActive = true;
this.updatedAt = new Date();
await this.save();
}
public getNormalBalance(): 'debit' | 'credit' {
switch (this.accountType) {
case 'asset':
case 'expense':
return 'debit';
case 'liability':
case 'equity':
case 'revenue':
return 'credit';
}
}
public async beforeSave(): Promise<void> {
// Validate account number format
if (!this.accountNumber || this.accountNumber.length !== 4) {
throw new Error(
`Invalid account number format: ${this.accountNumber}. Must be 4 digits.`,
);
}
// Validate account number is numeric
if (!/^\d{4}$/.test(this.accountNumber)) {
throw new Error(
`Account number must contain only digits: ${this.accountNumber}`,
);
}
// Validate account class matches first digit
const firstDigit = parseInt(this.accountNumber[0]);
if (this.accountClass !== firstDigit) {
throw new Error(
`Account class ${this.accountClass} does not match account number ${this.accountNumber}`,
);
}
// Validate SKR type
if (this.skrType !== 'SKR03' && this.skrType !== 'SKR04') {
throw new Error(`Invalid SKR type: ${this.skrType}`);
}
}
}

View File

@@ -0,0 +1,508 @@
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');
}
}

View File

@@ -0,0 +1,318 @@
import * as plugins from './plugins.js';
import { getDbSync } from './skr.database.js';
import { Account } from './skr.classes.account.js';
import { Transaction } from './skr.classes.transaction.js';
import type {
TSKRType,
IJournalEntry,
IJournalEntryLine,
} from './skr.types.js';
const { SmartDataDbDoc, svDb, unI, index, searchable } = plugins.smartdata;
@plugins.smartdata.Collection(() => getDbSync())
export class JournalEntry extends SmartDataDbDoc<JournalEntry, JournalEntry> {
@unI()
public id: string;
@svDb()
@index()
public journalNumber: string;
@svDb()
@index()
public date: Date;
@svDb()
@searchable()
public description: string;
@svDb()
@index()
public reference: string;
@svDb()
public lines: IJournalEntryLine[];
@svDb()
@index()
public skrType: TSKRType;
@svDb()
public totalDebits: number;
@svDb()
public totalCredits: number;
@svDb()
public isBalanced: boolean;
@svDb()
@index()
public status: 'draft' | 'posted' | 'reversed';
@svDb()
public transactionIds: string[];
@svDb()
@index()
public period: string;
@svDb()
public fiscalYear: number;
@svDb()
public createdAt: Date;
@svDb()
public postedAt: Date;
@svDb()
public createdBy: string;
constructor(data?: Partial<IJournalEntry>) {
super();
if (data) {
this.id = plugins.smartunique.shortId();
this.journalNumber = this.generateJournalNumber();
this.date = data.date || new Date();
this.description = data.description || '';
this.reference = data.reference || '';
this.lines = data.lines || [];
this.skrType = data.skrType || 'SKR03';
this.totalDebits = 0;
this.totalCredits = 0;
this.isBalanced = false;
this.status = 'draft';
this.transactionIds = [];
// Set period and fiscal year
const entryDate = new Date(this.date);
this.period = `${entryDate.getFullYear()}-${String(entryDate.getMonth() + 1).padStart(2, '0')}`;
this.fiscalYear = entryDate.getFullYear();
this.createdAt = new Date();
this.postedAt = null;
this.createdBy = 'system';
// Calculate totals
this.calculateTotals();
}
}
private generateJournalNumber(): string {
const timestamp = Date.now();
const random = Math.floor(Math.random() * 1000);
return `JE-${timestamp}-${random}`;
}
private calculateTotals(): void {
this.totalDebits = 0;
this.totalCredits = 0;
for (const line of this.lines) {
if (line.debit) {
this.totalDebits += line.debit;
}
if (line.credit) {
this.totalCredits += line.credit;
}
}
// Check if balanced (allowing for small rounding differences)
const difference = Math.abs(this.totalDebits - this.totalCredits);
this.isBalanced = difference < 0.01;
}
public static async createJournalEntry(
data: IJournalEntry,
): Promise<JournalEntry> {
const journalEntry = new JournalEntry(data);
await journalEntry.validate();
await journalEntry.save();
return journalEntry;
}
public addLine(line: IJournalEntryLine): void {
// Validate line
if (!line.accountNumber) {
throw new Error('Account number is required for journal entry line');
}
if (!line.debit && !line.credit) {
throw new Error('Either debit or credit amount is required');
}
if (line.debit && line.credit) {
throw new Error('A line cannot have both debit and credit amounts');
}
if (line.debit && line.debit < 0) {
throw new Error('Debit amount must be positive');
}
if (line.credit && line.credit < 0) {
throw new Error('Credit amount must be positive');
}
this.lines.push(line);
this.calculateTotals();
}
public removeLine(index: number): void {
if (index >= 0 && index < this.lines.length) {
this.lines.splice(index, 1);
this.calculateTotals();
}
}
public async validate(): Promise<void> {
// Check if entry is balanced
if (!this.isBalanced) {
throw new Error(
`Journal entry is not balanced. Debits: ${this.totalDebits}, Credits: ${this.totalCredits}`,
);
}
// Check minimum lines
if (this.lines.length < 2) {
throw new Error('Journal entry must have at least 2 lines');
}
// Validate all accounts exist and are active
for (const line of this.lines) {
const account = await Account.getAccountByNumber(
line.accountNumber,
this.skrType,
);
if (!account) {
throw new Error(
`Account ${line.accountNumber} not found for ${this.skrType}`,
);
}
if (!account.isActive) {
throw new Error(`Account ${line.accountNumber} is not active`);
}
}
}
public async post(): Promise<void> {
if (this.status === 'posted') {
throw new Error('Journal entry is already posted');
}
// Validate before posting
await this.validate();
// Create individual transactions for each debit-credit pair
const transactions: Transaction[] = [];
// Simple posting logic: match debits with credits
// For complex entries, this could be enhanced with specific pairing logic
const debitLines = this.lines.filter((l) => l.debit);
const creditLines = this.lines.filter((l) => l.credit);
if (debitLines.length === 1 && creditLines.length === 1) {
// Simple entry: one debit, one credit
const transaction = await Transaction.createTransaction({
date: this.date,
debitAccount: debitLines[0].accountNumber,
creditAccount: creditLines[0].accountNumber,
amount: debitLines[0].debit,
description: this.description,
reference: this.reference,
skrType: this.skrType,
costCenter: debitLines[0].costCenter,
});
transactions.push(transaction);
} else {
// Complex entry: multiple debits and/or credits
// Create transactions to balance the entry
for (const debitLine of debitLines) {
for (const creditLine of creditLines) {
const amount = Math.min(debitLine.debit || 0, creditLine.credit || 0);
if (amount > 0) {
const transaction = await Transaction.createTransaction({
date: this.date,
debitAccount: debitLine.accountNumber,
creditAccount: creditLine.accountNumber,
amount: amount,
description: `${this.description} - ${debitLine.description || creditLine.description || ''}`,
reference: this.reference,
skrType: this.skrType,
costCenter: debitLine.costCenter || creditLine.costCenter,
});
transactions.push(transaction);
// Reduce amounts for tracking
if (debitLine.debit) debitLine.debit -= amount;
if (creditLine.credit) creditLine.credit -= amount;
}
}
}
}
// Store transaction IDs
this.transactionIds = transactions.map((t) => t.id);
// Update status
this.status = 'posted';
this.postedAt = new Date();
await this.save();
}
public async reverse(): Promise<JournalEntry> {
if (this.status !== 'posted') {
throw new Error('Can only reverse posted journal entries');
}
// Create reversal entry with swapped debits and credits
const reversalLines: IJournalEntryLine[] = this.lines.map((line) => ({
accountNumber: line.accountNumber,
debit: line.credit, // Swap
credit: line.debit, // Swap
description: `Reversal: ${line.description || ''}`,
costCenter: line.costCenter,
}));
const reversalEntry = new JournalEntry({
date: new Date(),
description: `Reversal of ${this.journalNumber}: ${this.description}`,
reference: `REV-${this.journalNumber}`,
lines: reversalLines,
skrType: this.skrType,
});
await reversalEntry.validate();
await reversalEntry.post();
// Update original entry status
this.status = 'reversed';
await this.save();
return reversalEntry;
}
public async beforeSave(): Promise<void> {
// Recalculate totals before saving
this.calculateTotals();
// Validate required fields
if (!this.date) {
throw new Error('Journal entry date is required');
}
if (!this.description) {
throw new Error('Journal entry description is required');
}
if (this.lines.length === 0) {
throw new Error('Journal entry must have at least one line');
}
}
}

528
ts/skr.classes.ledger.ts Normal file
View File

@@ -0,0 +1,528 @@
import * as plugins from './plugins.js';
import { Account } from './skr.classes.account.js';
import { Transaction } from './skr.classes.transaction.js';
import { JournalEntry } from './skr.classes.journalentry.js';
import type {
TSKRType,
ITransactionData,
IJournalEntry,
IJournalEntryLine,
IAccountBalance,
} from './skr.types.js';
export class Ledger {
private logger: plugins.smartlog.Smartlog;
constructor(private skrType: TSKRType) {
this.logger = new plugins.smartlog.Smartlog({
logContext: {
company: 'fin.cx',
companyunit: 'skr',
containerName: 'Ledger',
environment: 'local',
runtime: 'node',
zone: 'local',
},
});
}
/**
* Post a transaction with validation
*/
public async postTransaction(
transactionData: ITransactionData,
): Promise<Transaction> {
this.logger.log(
'info',
`Posting transaction: ${transactionData.description}`,
);
// Ensure SKR type matches
const fullTransactionData: ITransactionData = {
...transactionData,
skrType: this.skrType,
};
// Validate accounts exist
await this.validateAccounts([
transactionData.debitAccount,
transactionData.creditAccount,
]);
// Create and post transaction
const transaction =
await Transaction.createTransaction(fullTransactionData);
this.logger.log(
'info',
`Transaction ${transaction.transactionNumber} posted successfully`,
);
return transaction;
}
/**
* Post a journal entry with validation
*/
public async postJournalEntry(
journalData: IJournalEntry,
): Promise<JournalEntry> {
this.logger.log(
'info',
`Posting journal entry: ${journalData.description}`,
);
// Ensure SKR type matches
const fullJournalData: IJournalEntry = {
...journalData,
skrType: this.skrType,
};
// Validate all accounts exist
const accountNumbers = journalData.lines.map((line) => line.accountNumber);
await this.validateAccounts(accountNumbers);
// Validate journal entry is balanced
this.validateJournalBalance(journalData.lines);
// Create and post journal entry
const journalEntry = await JournalEntry.createJournalEntry(fullJournalData);
await journalEntry.post();
this.logger.log(
'info',
`Journal entry ${journalEntry.journalNumber} posted successfully`,
);
return journalEntry;
}
/**
* Validate that accounts exist and are active
*/
private async validateAccounts(accountNumbers: string[]): Promise<void> {
const uniqueAccountNumbers = [...new Set(accountNumbers)];
for (const accountNumber of uniqueAccountNumbers) {
const account = await Account.getAccountByNumber(
accountNumber,
this.skrType,
);
if (!account) {
throw new Error(
`Account ${accountNumber} not found for ${this.skrType}`,
);
}
if (!account.isActive) {
throw new Error(`Account ${accountNumber} is not active`);
}
}
}
/**
* Validate journal entry balance
*/
private validateJournalBalance(lines: IJournalEntryLine[]): void {
let totalDebits = 0;
let totalCredits = 0;
for (const line of lines) {
if (line.debit) totalDebits += line.debit;
if (line.credit) totalCredits += line.credit;
}
const difference = Math.abs(totalDebits - totalCredits);
if (difference >= 0.01) {
throw new Error(
`Journal entry is not balanced. Debits: ${totalDebits}, Credits: ${totalCredits}`,
);
}
}
/**
* Reverse a transaction
*/
public async reverseTransaction(transactionId: string): Promise<Transaction> {
this.logger.log('info', `Reversing transaction: ${transactionId}`);
const transaction = await Transaction.getTransactionById(transactionId);
if (!transaction) {
throw new Error(`Transaction ${transactionId} not found`);
}
if (transaction.skrType !== this.skrType) {
throw new Error(
`Transaction ${transactionId} belongs to different SKR type`,
);
}
const reversalTransaction = await transaction.reverseTransaction();
this.logger.log(
'info',
`Transaction reversed: ${reversalTransaction.transactionNumber}`,
);
return reversalTransaction;
}
/**
* Reverse a journal entry
*/
public async reverseJournalEntry(journalId: string): Promise<JournalEntry> {
this.logger.log('info', `Reversing journal entry: ${journalId}`);
const journalEntry = await JournalEntry.getInstance({ id: journalId });
if (!journalEntry) {
throw new Error(`Journal entry ${journalId} not found`);
}
if (journalEntry.skrType !== this.skrType) {
throw new Error(
`Journal entry ${journalId} belongs to different SKR type`,
);
}
const reversalEntry = await journalEntry.reverse();
this.logger.log(
'info',
`Journal entry reversed: ${reversalEntry.journalNumber}`,
);
return reversalEntry;
}
/**
* Get account history (all transactions for an account)
*/
public async getAccountHistory(
accountNumber: string,
dateFrom?: Date,
dateTo?: Date,
): Promise<Transaction[]> {
const account = await Account.getAccountByNumber(
accountNumber,
this.skrType,
);
if (!account) {
throw new Error(`Account ${accountNumber} not found`);
}
let transactions = await Transaction.getTransactionsByAccount(
accountNumber,
this.skrType,
);
// Apply date filter if provided
if (dateFrom || dateTo) {
transactions = transactions.filter((transaction) => {
if (dateFrom && transaction.date < dateFrom) return false;
if (dateTo && transaction.date > dateTo) return false;
return true;
});
}
// Sort by date
transactions.sort((a, b) => a.date.getTime() - b.date.getTime());
return transactions;
}
/**
* Get account balance at a specific date
*/
public async getAccountBalance(
accountNumber: string,
asOfDate?: Date,
): Promise<IAccountBalance> {
const account = await Account.getAccountByNumber(
accountNumber,
this.skrType,
);
if (!account) {
throw new Error(`Account ${accountNumber} not found`);
}
let transactions = await Transaction.getTransactionsByAccount(
accountNumber,
this.skrType,
);
// Filter transactions up to the specified date
if (asOfDate) {
transactions = transactions.filter((t) => t.date <= asOfDate);
}
// Calculate balance
let debitTotal = 0;
let creditTotal = 0;
for (const transaction of transactions) {
if (transaction.debitAccount === accountNumber) {
debitTotal += transaction.amount;
}
if (transaction.creditAccount === accountNumber) {
creditTotal += transaction.amount;
}
}
// Calculate net balance based on account type
let balance: number;
switch (account.accountType) {
case 'asset':
case 'expense':
// Normal debit accounts
balance = debitTotal - creditTotal;
break;
case 'liability':
case 'equity':
case 'revenue':
// Normal credit accounts
balance = creditTotal - debitTotal;
break;
}
return {
accountNumber,
debitTotal,
creditTotal,
balance,
lastUpdated: new Date(),
};
}
/**
* Close accounting period (create closing entries)
*/
public async closeAccountingPeriod(
period: string, // Format: YYYY-MM
closingAccountNumber: string = '9400', // Default P&L account
): Promise<JournalEntry[]> {
this.logger.log('info', `Closing accounting period: ${period}`);
const closingEntries: JournalEntry[] = [];
// Get all revenue and expense accounts
const revenueAccounts = await Account.getAccountsByType(
'revenue',
this.skrType,
);
const expenseAccounts = await Account.getAccountsByType(
'expense',
this.skrType,
);
// Calculate totals for each account in the period
const periodTransactions = await Transaction.getTransactionsByPeriod(
period,
this.skrType,
);
// Create closing entry for revenue accounts
const revenueLines: IJournalEntryLine[] = [];
let totalRevenue = 0;
for (const account of revenueAccounts) {
const balance = await this.getAccountBalanceForPeriod(
account.accountNumber,
periodTransactions,
);
if (balance !== 0) {
// Revenue accounts have credit balance, so debit to close
revenueLines.push({
accountNumber: account.accountNumber,
debit: Math.abs(balance),
description: `Closing ${account.accountName}`,
});
totalRevenue += Math.abs(balance);
}
}
if (totalRevenue > 0) {
// Credit the closing account
revenueLines.push({
accountNumber: closingAccountNumber,
credit: totalRevenue,
description: 'Revenue closing to P&L',
});
const revenueClosingEntry = await this.postJournalEntry({
date: new Date(),
description: `Closing revenue accounts for period ${period}`,
reference: `CLOSE-REV-${period}`,
lines: revenueLines,
skrType: this.skrType,
});
closingEntries.push(revenueClosingEntry);
}
// Create closing entry for expense accounts
const expenseLines: IJournalEntryLine[] = [];
let totalExpense = 0;
for (const account of expenseAccounts) {
const balance = await this.getAccountBalanceForPeriod(
account.accountNumber,
periodTransactions,
);
if (balance !== 0) {
// Expense accounts have debit balance, so credit to close
expenseLines.push({
accountNumber: account.accountNumber,
credit: Math.abs(balance),
description: `Closing ${account.accountName}`,
});
totalExpense += Math.abs(balance);
}
}
if (totalExpense > 0) {
// Debit the closing account
expenseLines.push({
accountNumber: closingAccountNumber,
debit: totalExpense,
description: 'Expense closing to P&L',
});
const expenseClosingEntry = await this.postJournalEntry({
date: new Date(),
description: `Closing expense accounts for period ${period}`,
reference: `CLOSE-EXP-${period}`,
lines: expenseLines,
skrType: this.skrType,
});
closingEntries.push(expenseClosingEntry);
}
this.logger.log(
'info',
`Period ${period} closed with ${closingEntries.length} entries`,
);
return closingEntries;
}
/**
* Calculate account balance for a specific set of transactions
*/
private async getAccountBalanceForPeriod(
accountNumber: string,
transactions: Transaction[],
): Promise<number> {
const account = await Account.getAccountByNumber(
accountNumber,
this.skrType,
);
if (!account) return 0;
let debitTotal = 0;
let creditTotal = 0;
for (const transaction of transactions) {
if (transaction.debitAccount === accountNumber) {
debitTotal += transaction.amount;
}
if (transaction.creditAccount === 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;
}
}
/**
* Validate double-entry rules
*/
public validateDoubleEntry(
debitAmount: number,
creditAmount: number,
): boolean {
return Math.abs(debitAmount - creditAmount) < 0.01;
}
/**
* Get unbalanced transactions (for audit)
*/
public async getUnbalancedTransactions(): Promise<Transaction[]> {
// In a proper double-entry system, all posted transactions should be balanced
// This method is mainly for audit purposes
const allTransactions = await Transaction.getInstances({
skrType: this.skrType,
status: 'posted',
});
// Group transactions by journal entry if they have one
const unbalanced: Transaction[] = [];
// Since our system ensures balance at posting time,
// this should typically return an empty array
// But we include it for completeness and audit purposes
return unbalanced;
}
/**
* Recalculate all account balances
*/
public async recalculateAllBalances(): Promise<void> {
this.logger.log('info', 'Recalculating all account balances');
// Get all accounts
const accounts = await Account.getInstances({ skrType: this.skrType });
for (const account of accounts) {
// Reset balances
account.debitTotal = 0;
account.creditTotal = 0;
account.balance = 0;
// Get all transactions for this account
const transactions = await Transaction.getTransactionsByAccount(
account.accountNumber,
this.skrType,
);
// Recalculate totals
for (const transaction of transactions) {
if (transaction.debitAccount === account.accountNumber) {
account.debitTotal += transaction.amount;
}
if (transaction.creditAccount === account.accountNumber) {
account.creditTotal += transaction.amount;
}
}
// Calculate balance based on account type
switch (account.accountType) {
case 'asset':
case 'expense':
account.balance = account.debitTotal - account.creditTotal;
break;
case 'liability':
case 'equity':
case 'revenue':
account.balance = account.creditTotal - account.debitTotal;
break;
}
account.updatedAt = new Date();
await account.save();
}
this.logger.log(
'info',
`Recalculated balances for ${accounts.length} accounts`,
);
}
}

721
ts/skr.classes.reports.ts Normal file
View File

@@ -0,0 +1,721 @@
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');
}
}

View File

@@ -0,0 +1,300 @@
import * as plugins from './plugins.js';
import { getDbSync } from './skr.database.js';
import { Account } from './skr.classes.account.js';
import type {
TSKRType,
TTransactionStatus,
ITransactionData,
} from './skr.types.js';
const { SmartDataDbDoc, svDb, unI, index, searchable } = plugins.smartdata;
@plugins.smartdata.Collection(() => getDbSync())
export class Transaction extends SmartDataDbDoc<Transaction, Transaction> {
@unI()
public id: string;
@svDb()
@index()
public transactionNumber: string;
@svDb()
@index()
public date: Date;
@svDb()
@index()
public debitAccount: string;
@svDb()
@index()
public creditAccount: string;
@svDb()
public amount: number;
@svDb()
@searchable()
public description: string;
@svDb()
@index()
public reference: string;
@svDb()
@index()
public skrType: TSKRType;
@svDb()
public vatAmount: number;
@svDb()
public costCenter: string;
@svDb()
@index()
public status: TTransactionStatus;
@svDb()
public reversalOf: string;
@svDb()
public reversedBy: string;
@svDb()
@index()
public period: string; // Format: YYYY-MM
@svDb()
public fiscalYear: number;
@svDb()
public createdAt: Date;
@svDb()
public postedAt: Date;
@svDb()
public createdBy: string;
constructor(data?: Partial<ITransactionData>) {
super();
if (data) {
this.id = plugins.smartunique.shortId();
this.transactionNumber = this.generateTransactionNumber();
this.date = data.date || new Date();
this.debitAccount = data.debitAccount || '';
this.creditAccount = data.creditAccount || '';
this.amount = data.amount || 0;
this.description = data.description || '';
this.reference = data.reference || '';
this.skrType = data.skrType || 'SKR03';
this.vatAmount = data.vatAmount || 0;
this.costCenter = data.costCenter || '';
this.status = 'pending';
this.reversalOf = '';
this.reversedBy = '';
// Set period and fiscal year
const transDate = new Date(this.date);
this.period = `${transDate.getFullYear()}-${String(transDate.getMonth() + 1).padStart(2, '0')}`;
this.fiscalYear = transDate.getFullYear();
this.createdAt = new Date();
this.postedAt = null;
this.createdBy = 'system';
}
}
private generateTransactionNumber(): string {
const timestamp = Date.now();
const random = Math.floor(Math.random() * 1000);
return `TXN-${timestamp}-${random}`;
}
public static async createTransaction(
data: ITransactionData,
): Promise<Transaction> {
const transaction = new Transaction(data);
await transaction.validateAndPost();
return transaction;
}
public static async getTransactionById(
id: string,
): Promise<Transaction | null> {
const transaction = await Transaction.getInstance({ id });
return transaction;
}
public static async getTransactionsByAccount(
accountNumber: string,
skrType: TSKRType,
): Promise<Transaction[]> {
const transactionsDebit = await Transaction.getInstances({
debitAccount: accountNumber,
skrType,
status: 'posted',
});
const transactionsCredit = await Transaction.getInstances({
creditAccount: accountNumber,
skrType,
status: 'posted',
});
const transactions = [...transactionsDebit, ...transactionsCredit];
return transactions;
}
public static async getTransactionsByPeriod(
period: string,
skrType: TSKRType,
): Promise<Transaction[]> {
const transactions = await Transaction.getInstances({
period,
skrType,
status: 'posted',
});
return transactions;
}
public static async getTransactionsByDateRange(
dateFrom: Date,
dateTo: Date,
skrType: TSKRType,
): Promise<Transaction[]> {
const allTransactions = await Transaction.getInstances({
skrType,
status: 'posted',
});
const transactions = allTransactions.filter(
(t) => t.date >= dateFrom && t.date <= dateTo,
);
return transactions;
}
public async validateAndPost(): Promise<void> {
// Validate transaction
await this.validateTransaction();
// Update account balances
await this.updateAccountBalances();
// Mark as posted
this.status = 'posted';
this.postedAt = new Date();
await this.save();
}
private async validateTransaction(): Promise<void> {
// Check if accounts exist
const debitAccount = await Account.getAccountByNumber(
this.debitAccount,
this.skrType,
);
const creditAccount = await Account.getAccountByNumber(
this.creditAccount,
this.skrType,
);
if (!debitAccount) {
throw new Error(
`Debit account ${this.debitAccount} not found for ${this.skrType}`,
);
}
if (!creditAccount) {
throw new Error(
`Credit account ${this.creditAccount} not found for ${this.skrType}`,
);
}
// Check if accounts are active
if (!debitAccount.isActive) {
throw new Error(`Debit account ${this.debitAccount} is not active`);
}
if (!creditAccount.isActive) {
throw new Error(`Credit account ${this.creditAccount} is not active`);
}
// Validate amount
if (this.amount <= 0) {
throw new Error('Transaction amount must be greater than zero');
}
// Check for same account
if (this.debitAccount === this.creditAccount) {
throw new Error('Debit and credit accounts cannot be the same');
}
}
private async updateAccountBalances(): Promise<void> {
const debitAccount = await Account.getAccountByNumber(
this.debitAccount,
this.skrType,
);
const creditAccount = await Account.getAccountByNumber(
this.creditAccount,
this.skrType,
);
if (debitAccount) {
await debitAccount.updateBalance(this.amount, 0);
}
if (creditAccount) {
await creditAccount.updateBalance(0, this.amount);
}
}
public async reverseTransaction(): Promise<Transaction> {
if (this.status !== 'posted') {
throw new Error('Can only reverse posted transactions');
}
if (this.reversedBy) {
throw new Error('Transaction has already been reversed');
}
// Create reversal transaction
const reversalData: ITransactionData = {
date: new Date(),
debitAccount: this.creditAccount, // Swap accounts
creditAccount: this.debitAccount, // Swap accounts
amount: this.amount,
description: `Reversal of ${this.transactionNumber}: ${this.description}`,
reference: `REV-${this.transactionNumber}`,
skrType: this.skrType,
vatAmount: this.vatAmount,
costCenter: this.costCenter,
};
const reversalTransaction = new Transaction(reversalData);
reversalTransaction.reversalOf = this.id;
await reversalTransaction.validateAndPost();
// Update original transaction
this.reversedBy = reversalTransaction.id;
this.status = 'reversed';
await this.save();
return reversalTransaction;
}
public async beforeSave(): Promise<void> {
// Additional validation before saving
if (!this.debitAccount || !this.creditAccount) {
throw new Error('Both debit and credit accounts are required');
}
if (!this.date) {
throw new Error('Transaction date is required');
}
if (!this.description) {
throw new Error('Transaction description is required');
}
}
}

39
ts/skr.database.ts Normal file
View File

@@ -0,0 +1,39 @@
import * as plugins from './plugins.js';
import type { IDatabaseConfig } from './skr.types.js';
let dbInstance: plugins.smartdata.SmartdataDb | null = null;
export const getDb = async (
config?: IDatabaseConfig,
): Promise<plugins.smartdata.SmartdataDb> => {
if (!dbInstance) {
if (!config) {
throw new Error(
'Database configuration required for first initialization',
);
}
dbInstance = new plugins.smartdata.SmartdataDb({
mongoDbUrl: config.mongoDbUrl,
mongoDbName: config.dbName || 'skr_accounting',
});
await dbInstance.init();
}
return dbInstance;
};
export const getDbSync = (): plugins.smartdata.SmartdataDb => {
if (!dbInstance) {
throw new Error('Database not initialized. Call getDb() first.');
}
return dbInstance;
};
export const closeDb = async (): Promise<void> => {
if (dbInstance) {
await dbInstance.close();
dbInstance = null;
}
};

154
ts/skr.types.ts Normal file
View File

@@ -0,0 +1,154 @@
export type TAccountType =
| 'asset'
| 'liability'
| 'equity'
| 'revenue'
| 'expense';
export type TSKRType = 'SKR03' | 'SKR04';
export type TTransactionStatus = 'pending' | 'posted' | 'reversed';
export type TReportType =
| 'trial_balance'
| 'income_statement'
| 'balance_sheet'
| 'general_ledger'
| 'cash_flow';
export interface IAccountData {
accountNumber: string;
accountName: string;
accountClass: number;
accountType: TAccountType;
skrType: TSKRType;
description?: string;
vatRate?: number;
isActive?: boolean;
}
export interface ITransactionData {
date: Date;
debitAccount: string;
creditAccount: string;
amount: number;
description: string;
reference?: string;
skrType: TSKRType;
vatAmount?: number;
costCenter?: string;
}
export interface IJournalEntry {
date: Date;
description: string;
reference?: string;
lines: IJournalEntryLine[];
skrType: TSKRType;
}
export interface IJournalEntryLine {
accountNumber: string;
debit?: number;
credit?: number;
description?: string;
costCenter?: string;
}
export interface ITrialBalanceEntry {
accountNumber: string;
accountName: string;
debitBalance: number;
creditBalance: number;
netBalance: number;
}
export interface ITrialBalanceReport {
date: Date;
skrType: TSKRType;
entries: ITrialBalanceEntry[];
totalDebits: number;
totalCredits: number;
isBalanced: boolean;
}
export interface IIncomeStatementEntry {
accountNumber: string;
accountName: string;
amount: number;
percentage?: number;
}
export interface IIncomeStatement {
date: Date;
skrType: TSKRType;
revenue: IIncomeStatementEntry[];
expenses: IIncomeStatementEntry[];
totalRevenue: number;
totalExpenses: number;
netIncome: number;
}
export interface IBalanceSheetEntry {
accountNumber: string;
accountName: string;
amount: number;
}
export interface IBalanceSheet {
date: Date;
skrType: TSKRType;
assets: {
current: IBalanceSheetEntry[];
fixed: IBalanceSheetEntry[];
totalAssets: number;
};
liabilities: {
current: IBalanceSheetEntry[];
longTerm: IBalanceSheetEntry[];
totalLiabilities: number;
};
equity: {
entries: IBalanceSheetEntry[];
totalEquity: number;
};
isBalanced: boolean;
}
export interface IAccountFilter {
skrType?: TSKRType;
accountClass?: number;
accountType?: TAccountType;
isActive?: boolean;
searchTerm?: string;
}
export interface ITransactionFilter {
skrType?: TSKRType;
dateFrom?: Date;
dateTo?: Date;
accountNumber?: string;
minAmount?: number;
maxAmount?: number;
searchTerm?: string;
}
export interface IDatabaseConfig {
mongoDbUrl: string;
dbName?: string;
}
export interface IReportParams {
dateFrom?: Date;
dateTo?: Date;
skrType: TSKRType;
format?: 'json' | 'csv' | 'datev';
}
export interface IAccountBalance {
accountNumber: string;
debitTotal: number;
creditTotal: number;
balance: number;
lastUpdated: Date;
}

901
ts/skr03.data.ts Normal file
View File

@@ -0,0 +1,901 @@
import type { IAccountData } from './skr.types.js';
/**
* SKR03 - Process Structure Principle (Prozessgliederungsprinzip)
* Organized by business process flow
*/
export const SKR03_ACCOUNTS: IAccountData[] = [
// Class 0: Capital Accounts (Anlagekonten)
{
accountNumber: '0001',
accountName: 'Aufwendungen für Ingangsetzung',
accountClass: 0,
accountType: 'asset',
skrType: 'SKR03',
description: 'Start-up expenses',
},
{
accountNumber: '0010',
accountName: 'Konzessionen',
accountClass: 0,
accountType: 'asset',
skrType: 'SKR03',
description: 'Concessions',
},
{
accountNumber: '0020',
accountName: 'Patente',
accountClass: 0,
accountType: 'asset',
skrType: 'SKR03',
description: 'Patents',
},
{
accountNumber: '0030',
accountName: 'Lizenzen',
accountClass: 0,
accountType: 'asset',
skrType: 'SKR03',
description: 'Licenses',
},
{
accountNumber: '0050',
accountName: 'Firmenwert',
accountClass: 0,
accountType: 'asset',
skrType: 'SKR03',
description: 'Goodwill',
},
{
accountNumber: '0100',
accountName: 'EDV-Software',
accountClass: 0,
accountType: 'asset',
skrType: 'SKR03',
description: 'IT Software',
},
{
accountNumber: '0200',
accountName: 'Grundstücke',
accountClass: 0,
accountType: 'asset',
skrType: 'SKR03',
description: 'Land and property',
},
{
accountNumber: '0210',
accountName: 'Gebäude',
accountClass: 0,
accountType: 'asset',
skrType: 'SKR03',
description: 'Buildings',
},
{
accountNumber: '0300',
accountName: 'Maschinen',
accountClass: 0,
accountType: 'asset',
skrType: 'SKR03',
description: 'Machinery',
},
{
accountNumber: '0400',
accountName: 'Fuhrpark',
accountClass: 0,
accountType: 'asset',
skrType: 'SKR03',
description: 'Vehicles',
},
{
accountNumber: '0500',
accountName: 'Betriebs- und Geschäftsausstattung',
accountClass: 0,
accountType: 'asset',
skrType: 'SKR03',
description: 'Office equipment',
},
{
accountNumber: '0600',
accountName: 'Geleistete Anzahlungen',
accountClass: 0,
accountType: 'asset',
skrType: 'SKR03',
description: 'Prepayments on fixed assets',
},
{
accountNumber: '0800',
accountName: 'Finanzanlagen',
accountClass: 0,
accountType: 'asset',
skrType: 'SKR03',
description: 'Financial assets',
},
// Class 1: Current Assets (Umlaufvermögen)
{
accountNumber: '1000',
accountName: 'Kasse',
accountClass: 1,
accountType: 'asset',
skrType: 'SKR03',
description: 'Cash on hand',
},
{
accountNumber: '1100',
accountName: 'Postbank',
accountClass: 1,
accountType: 'asset',
skrType: 'SKR03',
description: 'Postal bank account',
},
{
accountNumber: '1200',
accountName: 'Bank',
accountClass: 1,
accountType: 'asset',
skrType: 'SKR03',
description: 'Bank account',
},
{
accountNumber: '1210',
accountName: 'Sparkasse',
accountClass: 1,
accountType: 'asset',
skrType: 'SKR03',
description: 'Savings bank',
},
{
accountNumber: '1300',
accountName: 'Wertpapiere',
accountClass: 1,
accountType: 'asset',
skrType: 'SKR03',
description: 'Securities',
},
{
accountNumber: '1400',
accountName: 'Forderungen aus Lieferungen und Leistungen',
accountClass: 1,
accountType: 'asset',
skrType: 'SKR03',
description: 'Trade receivables',
},
{
accountNumber: '1500',
accountName: 'Sonstige Vermögensgegenstände',
accountClass: 1,
accountType: 'asset',
skrType: 'SKR03',
description: 'Other assets',
},
{
accountNumber: '1520',
accountName: 'Abziehbare Vorsteuer',
accountClass: 1,
accountType: 'asset',
skrType: 'SKR03',
description: 'Input VAT',
},
{
accountNumber: '1570',
accountName: 'Vorsteuer 7%',
accountClass: 1,
accountType: 'asset',
skrType: 'SKR03',
description: 'Input VAT 7%',
},
{
accountNumber: '1571',
accountName: 'Vorsteuer 19%',
accountClass: 1,
accountType: 'asset',
skrType: 'SKR03',
description: 'Input VAT 19%',
},
{
accountNumber: '1600',
accountName: 'Verbindlichkeiten aus Lieferungen und Leistungen',
accountClass: 1,
accountType: 'liability',
skrType: 'SKR03',
description: 'Trade payables',
},
{
accountNumber: '1700',
accountName: 'Sonstige Verbindlichkeiten',
accountClass: 1,
accountType: 'liability',
skrType: 'SKR03',
description: 'Other liabilities',
},
{
accountNumber: '1770',
accountName: 'Umsatzsteuer 7%',
accountClass: 1,
accountType: 'liability',
skrType: 'SKR03',
description: 'VAT payable 7%',
},
{
accountNumber: '1771',
accountName: 'Umsatzsteuer 19%',
accountClass: 1,
accountType: 'liability',
skrType: 'SKR03',
description: 'VAT payable 19%',
},
{
accountNumber: '1800',
accountName: 'Privatentnahmen',
accountClass: 1,
accountType: 'equity',
skrType: 'SKR03',
description: 'Private withdrawals',
},
{
accountNumber: '1810',
accountName: 'Privateinlagen',
accountClass: 1,
accountType: 'equity',
skrType: 'SKR03',
description: 'Private deposits',
},
{
accountNumber: '1900',
accountName: 'Verrechnungskonto',
accountClass: 1,
accountType: 'asset',
skrType: 'SKR03',
description: 'Clearing account',
},
// Class 2: Equity (Eigenkapital)
{
accountNumber: '2000',
accountName: 'Eigenkapital',
accountClass: 2,
accountType: 'equity',
skrType: 'SKR03',
description: 'Equity capital',
},
{
accountNumber: '2100',
accountName: 'Gezeichnetes Kapital',
accountClass: 2,
accountType: 'equity',
skrType: 'SKR03',
description: 'Subscribed capital',
},
{
accountNumber: '2200',
accountName: 'Kapitalrücklage',
accountClass: 2,
accountType: 'equity',
skrType: 'SKR03',
description: 'Capital reserves',
},
{
accountNumber: '2300',
accountName: 'Gewinnrücklagen',
accountClass: 2,
accountType: 'equity',
skrType: 'SKR03',
description: 'Revenue reserves',
},
{
accountNumber: '2400',
accountName: 'Gewinnvortrag',
accountClass: 2,
accountType: 'equity',
skrType: 'SKR03',
description: 'Profit carried forward',
},
{
accountNumber: '2500',
accountName: 'Verlustvortrag',
accountClass: 2,
accountType: 'equity',
skrType: 'SKR03',
description: 'Loss carried forward',
},
{
accountNumber: '2600',
accountName: 'Jahresüberschuss',
accountClass: 2,
accountType: 'equity',
skrType: 'SKR03',
description: 'Annual surplus',
},
{
accountNumber: '2700',
accountName: 'Jahresfehlbetrag',
accountClass: 2,
accountType: 'equity',
skrType: 'SKR03',
description: 'Annual deficit',
},
{
accountNumber: '2900',
accountName: 'Sonderposten mit Rücklageanteil',
accountClass: 2,
accountType: 'equity',
skrType: 'SKR03',
description: 'Special items with reserve portion',
},
// Class 3: Provisions and Liabilities (Rückstellungen und Verbindlichkeiten)
{
accountNumber: '3000',
accountName: 'Rückstellungen für Pensionen',
accountClass: 3,
accountType: 'liability',
skrType: 'SKR03',
description: 'Pension provisions',
},
{
accountNumber: '3100',
accountName: 'Steuerrückstellungen',
accountClass: 3,
accountType: 'liability',
skrType: 'SKR03',
description: 'Tax provisions',
},
{
accountNumber: '3200',
accountName: 'Sonstige Rückstellungen',
accountClass: 3,
accountType: 'liability',
skrType: 'SKR03',
description: 'Other provisions',
},
{
accountNumber: '3300',
accountName: 'Verbindlichkeiten gegenüber Kreditinstituten',
accountClass: 3,
accountType: 'liability',
skrType: 'SKR03',
description: 'Bank loans',
},
{
accountNumber: '3400',
accountName: 'Erhaltene Anzahlungen',
accountClass: 3,
accountType: 'liability',
skrType: 'SKR03',
description: 'Advance payments received',
},
{
accountNumber: '3500',
accountName: 'Verbindlichkeiten aus Steuern',
accountClass: 3,
accountType: 'liability',
skrType: 'SKR03',
description: 'Tax liabilities',
},
{
accountNumber: '3600',
accountName: 'Verbindlichkeiten im Rahmen der sozialen Sicherheit',
accountClass: 3,
accountType: 'liability',
skrType: 'SKR03',
description: 'Social security liabilities',
},
{
accountNumber: '3700',
accountName: 'Sonstige Verbindlichkeiten',
accountClass: 3,
accountType: 'liability',
skrType: 'SKR03',
description: 'Other liabilities',
},
{
accountNumber: '3900',
accountName: 'Passive Rechnungsabgrenzung',
accountClass: 3,
accountType: 'liability',
skrType: 'SKR03',
description: 'Deferred income',
},
// Class 4: Operating Income (Betriebliche Erträge)
{
accountNumber: '4000',
accountName: 'Umsatzerlöse',
accountClass: 4,
accountType: 'revenue',
skrType: 'SKR03',
description: 'Sales revenue',
vatRate: 19,
},
{
accountNumber: '4100',
accountName: 'steuerfreie Umsätze',
accountClass: 4,
accountType: 'revenue',
skrType: 'SKR03',
description: 'Tax-free sales',
},
{
accountNumber: '4200',
accountName: 'Erlöse 7% USt',
accountClass: 4,
accountType: 'revenue',
skrType: 'SKR03',
description: 'Revenue 7% VAT',
vatRate: 7,
},
{
accountNumber: '4300',
accountName: 'Erlöse 19% USt',
accountClass: 4,
accountType: 'revenue',
skrType: 'SKR03',
description: 'Revenue 19% VAT',
vatRate: 19,
},
{
accountNumber: '4400',
accountName: 'Erlöse innergemeinschaftliche Lieferungen',
accountClass: 4,
accountType: 'revenue',
skrType: 'SKR03',
description: 'EU sales',
},
{
accountNumber: '4500',
accountName: 'Erlöse Export',
accountClass: 4,
accountType: 'revenue',
skrType: 'SKR03',
description: 'Export sales',
},
{
accountNumber: '4600',
accountName: 'Bestandsveränderungen',
accountClass: 4,
accountType: 'revenue',
skrType: 'SKR03',
description: 'Inventory changes',
},
{
accountNumber: '4700',
accountName: 'Aktivierte Eigenleistungen',
accountClass: 4,
accountType: 'revenue',
skrType: 'SKR03',
description: 'Capitalized own work',
},
{
accountNumber: '4800',
accountName: 'Sonstige betriebliche Erträge',
accountClass: 4,
accountType: 'revenue',
skrType: 'SKR03',
description: 'Other operating income',
},
{
accountNumber: '4900',
accountName: 'Erträge aus Beteiligungen',
accountClass: 4,
accountType: 'revenue',
skrType: 'SKR03',
description: 'Investment income',
},
// Class 5: Material Costs (Materialkosten)
{
accountNumber: '5000',
accountName: 'Aufwendungen für Roh-, Hilfs- und Betriebsstoffe',
accountClass: 5,
accountType: 'expense',
skrType: 'SKR03',
description: 'Raw materials and supplies',
},
{
accountNumber: '5100',
accountName: 'Einkauf Waren',
accountClass: 5,
accountType: 'expense',
skrType: 'SKR03',
description: 'Purchase of goods',
},
{
accountNumber: '5200',
accountName: 'Wareneingang 7% Vorsteuer',
accountClass: 5,
accountType: 'expense',
skrType: 'SKR03',
description: 'Goods receipt 7% input tax',
vatRate: 7,
},
{
accountNumber: '5400',
accountName: 'Wareneingang 19% Vorsteuer',
accountClass: 5,
accountType: 'expense',
skrType: 'SKR03',
description: 'Goods receipt 19% input tax',
vatRate: 19,
},
{
accountNumber: '5500',
accountName: 'Aufwendungen für bezogene Leistungen',
accountClass: 5,
accountType: 'expense',
skrType: 'SKR03',
description: 'Purchased services',
},
{
accountNumber: '5600',
accountName: 'Aufwendungen für Energie',
accountClass: 5,
accountType: 'expense',
skrType: 'SKR03',
description: 'Energy costs',
},
{
accountNumber: '5700',
accountName: 'Reisekosten',
accountClass: 5,
accountType: 'expense',
skrType: 'SKR03',
description: 'Travel expenses',
},
{
accountNumber: '5800',
accountName: 'Bewirtungskosten',
accountClass: 5,
accountType: 'expense',
skrType: 'SKR03',
description: 'Entertainment expenses',
},
{
accountNumber: '5900',
accountName: 'Fremdleistungen',
accountClass: 5,
accountType: 'expense',
skrType: 'SKR03',
description: 'External services',
},
// Class 6: Personnel Costs (Personalkosten)
{
accountNumber: '6000',
accountName: 'Löhne und Gehälter',
accountClass: 6,
accountType: 'expense',
skrType: 'SKR03',
description: 'Wages and salaries',
},
{
accountNumber: '6100',
accountName: 'Soziale Abgaben',
accountClass: 6,
accountType: 'expense',
skrType: 'SKR03',
description: 'Social security contributions',
},
{
accountNumber: '6200',
accountName: 'Aufwendungen für Altersversorgung',
accountClass: 6,
accountType: 'expense',
skrType: 'SKR03',
description: 'Pension expenses',
},
{
accountNumber: '6300',
accountName: 'Sonstige soziale Aufwendungen',
accountClass: 6,
accountType: 'expense',
skrType: 'SKR03',
description: 'Other social expenses',
},
{
accountNumber: '6400',
accountName: 'Versicherungen',
accountClass: 6,
accountType: 'expense',
skrType: 'SKR03',
description: 'Insurance',
},
{
accountNumber: '6500',
accountName: 'Berufsgenossenschaft',
accountClass: 6,
accountType: 'expense',
skrType: 'SKR03',
description: 'Occupational insurance',
},
{
accountNumber: '6600',
accountName: 'Vermögenswirksame Leistungen',
accountClass: 6,
accountType: 'expense',
skrType: 'SKR03',
description: 'Employee savings schemes',
},
{
accountNumber: '6700',
accountName: 'Aufwendungen für Fortbildung',
accountClass: 6,
accountType: 'expense',
skrType: 'SKR03',
description: 'Training expenses',
},
{
accountNumber: '6800',
accountName: 'Aushilfslöhne',
accountClass: 6,
accountType: 'expense',
skrType: 'SKR03',
description: 'Temporary staff wages',
},
{
accountNumber: '6900',
accountName: 'Aufwendungen für freie Mitarbeiter',
accountClass: 6,
accountType: 'expense',
skrType: 'SKR03',
description: 'Freelancer expenses',
},
// Class 7: Other Operating Expenses (Sonstige betriebliche Aufwendungen)
{
accountNumber: '7000',
accountName: 'Abschreibungen',
accountClass: 7,
accountType: 'expense',
skrType: 'SKR03',
description: 'Depreciation',
},
{
accountNumber: '7100',
accountName: 'Raumkosten',
accountClass: 7,
accountType: 'expense',
skrType: 'SKR03',
description: 'Rent and lease',
},
{
accountNumber: '7200',
accountName: 'Instandhaltung',
accountClass: 7,
accountType: 'expense',
skrType: 'SKR03',
description: 'Maintenance',
},
{
accountNumber: '7300',
accountName: 'Versicherungen',
accountClass: 7,
accountType: 'expense',
skrType: 'SKR03',
description: 'Insurance',
},
{
accountNumber: '7400',
accountName: 'Fahrzeugkosten',
accountClass: 7,
accountType: 'expense',
skrType: 'SKR03',
description: 'Vehicle expenses',
},
{
accountNumber: '7500',
accountName: 'Werbe- und Reisekosten',
accountClass: 7,
accountType: 'expense',
skrType: 'SKR03',
description: 'Marketing and travel',
},
{
accountNumber: '7600',
accountName: 'Kosten der Warenabgabe',
accountClass: 7,
accountType: 'expense',
skrType: 'SKR03',
description: 'Distribution costs',
},
{
accountNumber: '7700',
accountName: 'Verschiedene betriebliche Kosten',
accountClass: 7,
accountType: 'expense',
skrType: 'SKR03',
description: 'Miscellaneous operating costs',
},
{
accountNumber: '7800',
accountName: 'Steuern vom Einkommen und Ertrag',
accountClass: 7,
accountType: 'expense',
skrType: 'SKR03',
description: 'Income taxes',
},
{
accountNumber: '7900',
accountName: 'Sonstige Steuern',
accountClass: 7,
accountType: 'expense',
skrType: 'SKR03',
description: 'Other taxes',
},
// Class 8: Financial Accounts (Finanzkonten)
{
accountNumber: '8000',
accountName: 'Erlöse aus Anlagenabgängen',
accountClass: 8,
accountType: 'revenue',
skrType: 'SKR03',
description: 'Gains from asset disposals',
},
{
accountNumber: '8100',
accountName: 'Sonstige Zinsen und ähnliche Erträge',
accountClass: 8,
accountType: 'revenue',
skrType: 'SKR03',
description: 'Interest income',
},
{
accountNumber: '8200',
accountName: 'Erträge aus Beteiligungen',
accountClass: 8,
accountType: 'revenue',
skrType: 'SKR03',
description: 'Investment income',
},
{
accountNumber: '8300',
accountName: 'Zinsen und ähnliche Aufwendungen',
accountClass: 8,
accountType: 'expense',
skrType: 'SKR03',
description: 'Interest expense',
},
{
accountNumber: '8400',
accountName: 'Sonstige Erträge',
accountClass: 8,
accountType: 'revenue',
skrType: 'SKR03',
description: 'Other income',
},
{
accountNumber: '8500',
accountName: 'Sonstige Aufwendungen',
accountClass: 8,
accountType: 'expense',
skrType: 'SKR03',
description: 'Other expenses',
},
{
accountNumber: '8600',
accountName: 'Außerordentliche Erträge',
accountClass: 8,
accountType: 'revenue',
skrType: 'SKR03',
description: 'Extraordinary income',
},
{
accountNumber: '8700',
accountName: 'Außerordentliche Aufwendungen',
accountClass: 8,
accountType: 'expense',
skrType: 'SKR03',
description: 'Extraordinary expenses',
},
{
accountNumber: '8800',
accountName: 'Erträge aus Verlustübernahme',
accountClass: 8,
accountType: 'revenue',
skrType: 'SKR03',
description: 'Income from loss absorption',
},
{
accountNumber: '8900',
accountName: 'Aufgrund von Gewinnabführung abgeführte Gewinne',
accountClass: 8,
accountType: 'expense',
skrType: 'SKR03',
description: 'Profits transferred',
},
// Class 9: Closing Accounts (Abschlusskonten)
{
accountNumber: '9000',
accountName: 'Saldenvorträge',
accountClass: 9,
accountType: 'equity',
skrType: 'SKR03',
description: 'Opening balances',
},
{
accountNumber: '9100',
accountName: 'Summenvortrag',
accountClass: 9,
accountType: 'equity',
skrType: 'SKR03',
description: 'Total carried forward',
},
{
accountNumber: '9200',
accountName: 'Eröffnungsbilanzkonto',
accountClass: 9,
accountType: 'equity',
skrType: 'SKR03',
description: 'Opening balance sheet account',
},
{
accountNumber: '9300',
accountName: 'Schlussbilanzkonto',
accountClass: 9,
accountType: 'equity',
skrType: 'SKR03',
description: 'Closing balance sheet account',
},
{
accountNumber: '9400',
accountName: 'Gewinn- und Verlustkonto',
accountClass: 9,
accountType: 'equity',
skrType: 'SKR03',
description: 'Profit and loss account',
},
{
accountNumber: '9500',
accountName: 'Kapitalkonto',
accountClass: 9,
accountType: 'equity',
skrType: 'SKR03',
description: 'Capital account',
},
{
accountNumber: '9600',
accountName: 'Privatkonto',
accountClass: 9,
accountType: 'equity',
skrType: 'SKR03',
description: 'Private account',
},
{
accountNumber: '9700',
accountName: 'Eigenverbrauch',
accountClass: 9,
accountType: 'equity',
skrType: 'SKR03',
description: 'Personal consumption',
},
{
accountNumber: '9800',
accountName: 'Statistische Konten',
accountClass: 9,
accountType: 'equity',
skrType: 'SKR03',
description: 'Statistical accounts',
},
{
accountNumber: '9900',
accountName: 'Verrechnungskonten',
accountClass: 9,
accountType: 'equity',
skrType: 'SKR03',
description: 'Clearing accounts',
},
];
export const SKR03_ACCOUNT_CLASSES = {
0: 'Anlagekonten (Fixed Assets)',
1: 'Umlaufvermögen (Current Assets)',
2: 'Eigenkapital (Equity)',
3: 'Rückstellungen und Verbindlichkeiten (Provisions and Liabilities)',
4: 'Betriebliche Erträge (Operating Income)',
5: 'Materialkosten (Material Costs)',
6: 'Personalkosten (Personnel Costs)',
7: 'Sonstige betriebliche Aufwendungen (Other Operating Expenses)',
8: 'Finanzkonten (Financial Accounts)',
9: 'Abschlusskonten (Closing Accounts)',
};

923
ts/skr04.data.ts Normal file
View File

@@ -0,0 +1,923 @@
import type { IAccountData } from './skr.types.js';
/**
* SKR04 - Financial Classification Principle (Abschlussgliederungsprinzip)
* Organized by financial statement structure
*/
export const SKR04_ACCOUNTS: IAccountData[] = [
// Class 0: Capital Accounts (Anlagekonten)
{
accountNumber: '0001',
accountName: 'Aufwendungen für Ingangsetzung',
accountClass: 0,
accountType: 'asset',
skrType: 'SKR04',
description: 'Start-up expenses',
},
{
accountNumber: '0010',
accountName: 'Konzessionen',
accountClass: 0,
accountType: 'asset',
skrType: 'SKR04',
description: 'Concessions',
},
{
accountNumber: '0020',
accountName: 'Patente',
accountClass: 0,
accountType: 'asset',
skrType: 'SKR04',
description: 'Patents',
},
{
accountNumber: '0030',
accountName: 'Lizenzen',
accountClass: 0,
accountType: 'asset',
skrType: 'SKR04',
description: 'Licenses',
},
{
accountNumber: '0050',
accountName: 'Firmenwert',
accountClass: 0,
accountType: 'asset',
skrType: 'SKR04',
description: 'Goodwill',
},
{
accountNumber: '0100',
accountName: 'EDV-Software',
accountClass: 0,
accountType: 'asset',
skrType: 'SKR04',
description: 'IT Software',
},
{
accountNumber: '0200',
accountName: 'Grundstücke',
accountClass: 0,
accountType: 'asset',
skrType: 'SKR04',
description: 'Land and property',
},
{
accountNumber: '0210',
accountName: 'Gebäude',
accountClass: 0,
accountType: 'asset',
skrType: 'SKR04',
description: 'Buildings',
},
{
accountNumber: '0300',
accountName: 'Maschinen',
accountClass: 0,
accountType: 'asset',
skrType: 'SKR04',
description: 'Machinery',
},
{
accountNumber: '0400',
accountName: 'Fuhrpark',
accountClass: 0,
accountType: 'asset',
skrType: 'SKR04',
description: 'Vehicles',
},
{
accountNumber: '0500',
accountName: 'Betriebs- und Geschäftsausstattung',
accountClass: 0,
accountType: 'asset',
skrType: 'SKR04',
description: 'Office equipment',
},
{
accountNumber: '0600',
accountName: 'Geleistete Anzahlungen',
accountClass: 0,
accountType: 'asset',
skrType: 'SKR04',
description: 'Prepayments on fixed assets',
},
{
accountNumber: '0800',
accountName: 'Finanzanlagen',
accountClass: 0,
accountType: 'asset',
skrType: 'SKR04',
description: 'Financial assets',
},
// Class 1: Financial and Current Assets (Finanz- und Umlaufvermögen)
{
accountNumber: '1000',
accountName: 'Kasse',
accountClass: 1,
accountType: 'asset',
skrType: 'SKR04',
description: 'Cash on hand',
},
{
accountNumber: '1100',
accountName: 'Postbank',
accountClass: 1,
accountType: 'asset',
skrType: 'SKR04',
description: 'Postal bank account',
},
{
accountNumber: '1200',
accountName: 'Bank',
accountClass: 1,
accountType: 'asset',
skrType: 'SKR04',
description: 'Bank account',
},
{
accountNumber: '1210',
accountName: 'Sparkasse',
accountClass: 1,
accountType: 'asset',
skrType: 'SKR04',
description: 'Savings bank',
},
{
accountNumber: '1300',
accountName: 'Wertpapiere',
accountClass: 1,
accountType: 'asset',
skrType: 'SKR04',
description: 'Securities',
},
{
accountNumber: '1400',
accountName: 'Forderungen aus Lieferungen und Leistungen',
accountClass: 1,
accountType: 'asset',
skrType: 'SKR04',
description: 'Trade receivables',
},
{
accountNumber: '1500',
accountName: 'Sonstige Vermögensgegenstände',
accountClass: 1,
accountType: 'asset',
skrType: 'SKR04',
description: 'Other assets',
},
{
accountNumber: '1520',
accountName: 'Abziehbare Vorsteuer',
accountClass: 1,
accountType: 'asset',
skrType: 'SKR04',
description: 'Input VAT',
},
{
accountNumber: '1570',
accountName: 'Vorsteuer 7%',
accountClass: 1,
accountType: 'asset',
skrType: 'SKR04',
description: 'Input VAT 7%',
},
{
accountNumber: '1571',
accountName: 'Vorsteuer 19%',
accountClass: 1,
accountType: 'asset',
skrType: 'SKR04',
description: 'Input VAT 19%',
},
{
accountNumber: '1600',
accountName: 'Verbindlichkeiten aus Lieferungen und Leistungen',
accountClass: 1,
accountType: 'liability',
skrType: 'SKR04',
description: 'Trade payables',
},
{
accountNumber: '1700',
accountName: 'Sonstige Verbindlichkeiten',
accountClass: 1,
accountType: 'liability',
skrType: 'SKR04',
description: 'Other liabilities',
},
{
accountNumber: '1770',
accountName: 'Umsatzsteuer 7%',
accountClass: 1,
accountType: 'liability',
skrType: 'SKR04',
description: 'VAT payable 7%',
},
{
accountNumber: '1771',
accountName: 'Umsatzsteuer 19%',
accountClass: 1,
accountType: 'liability',
skrType: 'SKR04',
description: 'VAT payable 19%',
},
{
accountNumber: '1800',
accountName: 'Privatentnahmen',
accountClass: 1,
accountType: 'equity',
skrType: 'SKR04',
description: 'Private withdrawals',
},
{
accountNumber: '1810',
accountName: 'Privateinlagen',
accountClass: 1,
accountType: 'equity',
skrType: 'SKR04',
description: 'Private deposits',
},
{
accountNumber: '1900',
accountName: 'Verrechnungskonto',
accountClass: 1,
accountType: 'asset',
skrType: 'SKR04',
description: 'Clearing account',
},
// Class 2: Expenses (Aufwendungen) - Part 1
{
accountNumber: '2000',
accountName: 'Roh-, Hilfs- und Betriebsstoffe',
accountClass: 2,
accountType: 'expense',
skrType: 'SKR04',
description: 'Raw materials and supplies',
},
{
accountNumber: '2100',
accountName: 'Bezogene Waren',
accountClass: 2,
accountType: 'expense',
skrType: 'SKR04',
description: 'Purchased goods',
},
{
accountNumber: '2200',
accountName: 'Bezogene Leistungen',
accountClass: 2,
accountType: 'expense',
skrType: 'SKR04',
description: 'Purchased services',
},
{
accountNumber: '2300',
accountName: 'Löhne',
accountClass: 2,
accountType: 'expense',
skrType: 'SKR04',
description: 'Wages',
},
{
accountNumber: '2400',
accountName: 'Gehälter',
accountClass: 2,
accountType: 'expense',
skrType: 'SKR04',
description: 'Salaries',
},
{
accountNumber: '2500',
accountName: 'Soziale Abgaben',
accountClass: 2,
accountType: 'expense',
skrType: 'SKR04',
description: 'Social security contributions',
},
{
accountNumber: '2600',
accountName: 'Aufwendungen für Altersversorgung',
accountClass: 2,
accountType: 'expense',
skrType: 'SKR04',
description: 'Pension expenses',
},
{
accountNumber: '2700',
accountName: 'Abschreibungen auf immaterielle Vermögensgegenstände',
accountClass: 2,
accountType: 'expense',
skrType: 'SKR04',
description: 'Depreciation on intangible assets',
},
{
accountNumber: '2800',
accountName: 'Abschreibungen auf Sachanlagen',
accountClass: 2,
accountType: 'expense',
skrType: 'SKR04',
description: 'Depreciation on fixed assets',
},
{
accountNumber: '2900',
accountName: 'Abschreibungen auf Finanzanlagen',
accountClass: 2,
accountType: 'expense',
skrType: 'SKR04',
description: 'Depreciation on financial assets',
},
// Class 3: Expenses (Aufwendungen) - Part 2
{
accountNumber: '3000',
accountName: 'Raumkosten',
accountClass: 3,
accountType: 'expense',
skrType: 'SKR04',
description: 'Rent and lease',
},
{
accountNumber: '3100',
accountName: 'Sonstige Raumkosten',
accountClass: 3,
accountType: 'expense',
skrType: 'SKR04',
description: 'Other occupancy costs',
},
{
accountNumber: '3200',
accountName: 'Instandhaltung',
accountClass: 3,
accountType: 'expense',
skrType: 'SKR04',
description: 'Maintenance',
},
{
accountNumber: '3300',
accountName: 'Fahrzeugkosten',
accountClass: 3,
accountType: 'expense',
skrType: 'SKR04',
description: 'Vehicle expenses',
},
{
accountNumber: '3400',
accountName: 'Werbe- und Reisekosten',
accountClass: 3,
accountType: 'expense',
skrType: 'SKR04',
description: 'Marketing and travel',
},
{
accountNumber: '3500',
accountName: 'Bewirtungskosten',
accountClass: 3,
accountType: 'expense',
skrType: 'SKR04',
description: 'Entertainment expenses',
},
{
accountNumber: '3600',
accountName: 'Versicherungen',
accountClass: 3,
accountType: 'expense',
skrType: 'SKR04',
description: 'Insurance',
},
{
accountNumber: '3700',
accountName: 'Beiträge und Gebühren',
accountClass: 3,
accountType: 'expense',
skrType: 'SKR04',
description: 'Fees and subscriptions',
},
{
accountNumber: '3800',
accountName: 'Büromaterial',
accountClass: 3,
accountType: 'expense',
skrType: 'SKR04',
description: 'Office supplies',
},
{
accountNumber: '3900',
accountName: 'Sonstige Aufwendungen',
accountClass: 3,
accountType: 'expense',
skrType: 'SKR04',
description: 'Other expenses',
},
// Class 4: Revenues (Erträge) - Part 1
{
accountNumber: '4000',
accountName: 'Umsatzerlöse',
accountClass: 4,
accountType: 'revenue',
skrType: 'SKR04',
description: 'Sales revenue',
vatRate: 19,
},
{
accountNumber: '4100',
accountName: 'steuerfreie Umsätze',
accountClass: 4,
accountType: 'revenue',
skrType: 'SKR04',
description: 'Tax-free sales',
},
{
accountNumber: '4200',
accountName: 'Erlöse 7% USt',
accountClass: 4,
accountType: 'revenue',
skrType: 'SKR04',
description: 'Revenue 7% VAT',
vatRate: 7,
},
{
accountNumber: '4300',
accountName: 'Erlöse 19% USt',
accountClass: 4,
accountType: 'revenue',
skrType: 'SKR04',
description: 'Revenue 19% VAT',
vatRate: 19,
},
{
accountNumber: '4400',
accountName: 'Erlöse innergemeinschaftliche Lieferungen',
accountClass: 4,
accountType: 'revenue',
skrType: 'SKR04',
description: 'EU sales',
},
{
accountNumber: '4500',
accountName: 'Erlöse Export',
accountClass: 4,
accountType: 'revenue',
skrType: 'SKR04',
description: 'Export sales',
},
{
accountNumber: '4600',
accountName: 'Bestandsveränderungen',
accountClass: 4,
accountType: 'revenue',
skrType: 'SKR04',
description: 'Inventory changes',
},
{
accountNumber: '4700',
accountName: 'Aktivierte Eigenleistungen',
accountClass: 4,
accountType: 'revenue',
skrType: 'SKR04',
description: 'Capitalized own work',
},
{
accountNumber: '4800',
accountName: 'Sonstige betriebliche Erträge',
accountClass: 4,
accountType: 'revenue',
skrType: 'SKR04',
description: 'Other operating income',
},
{
accountNumber: '4900',
accountName: 'Erträge aus Beteiligungen',
accountClass: 4,
accountType: 'revenue',
skrType: 'SKR04',
description: 'Investment income',
},
// Class 5: Revenues (Erträge) - Part 2
{
accountNumber: '5000',
accountName: 'Zinserträge',
accountClass: 5,
accountType: 'revenue',
skrType: 'SKR04',
description: 'Interest income',
},
{
accountNumber: '5100',
accountName: 'Erträge aus Wertpapieren',
accountClass: 5,
accountType: 'revenue',
skrType: 'SKR04',
description: 'Securities income',
},
{
accountNumber: '5200',
accountName: 'Erträge aus Anlagenabgängen',
accountClass: 5,
accountType: 'revenue',
skrType: 'SKR04',
description: 'Gains from asset disposals',
},
{
accountNumber: '5300',
accountName: 'Währungsgewinne',
accountClass: 5,
accountType: 'revenue',
skrType: 'SKR04',
description: 'Currency gains',
},
{
accountNumber: '5400',
accountName: 'Erträge aus der Auflösung von Rückstellungen',
accountClass: 5,
accountType: 'revenue',
skrType: 'SKR04',
description: 'Income from provision releases',
},
{
accountNumber: '5500',
accountName: 'Periodenfremde Erträge',
accountClass: 5,
accountType: 'revenue',
skrType: 'SKR04',
description: 'Prior period income',
},
{
accountNumber: '5600',
accountName: 'Außerordentliche Erträge',
accountClass: 5,
accountType: 'revenue',
skrType: 'SKR04',
description: 'Extraordinary income',
},
{
accountNumber: '5700',
accountName: 'Verwendung von Rücklagen',
accountClass: 5,
accountType: 'revenue',
skrType: 'SKR04',
description: 'Use of reserves',
},
{
accountNumber: '5800',
accountName: 'Gewinne aus Unternehmensverträgen',
accountClass: 5,
accountType: 'revenue',
skrType: 'SKR04',
description: 'Profits from company agreements',
},
{
accountNumber: '5900',
accountName: 'Sonstige Erträge',
accountClass: 5,
accountType: 'revenue',
skrType: 'SKR04',
description: 'Other income',
},
// Class 6: Special Accounts (Sonderkonten)
{
accountNumber: '6000',
accountName: 'Betriebssteuern',
accountClass: 6,
accountType: 'expense',
skrType: 'SKR04',
description: 'Operating taxes',
},
{
accountNumber: '6100',
accountName: 'Vermögensteuer',
accountClass: 6,
accountType: 'expense',
skrType: 'SKR04',
description: 'Wealth tax',
},
{
accountNumber: '6200',
accountName: 'Körperschaftsteuer',
accountClass: 6,
accountType: 'expense',
skrType: 'SKR04',
description: 'Corporate tax',
},
{
accountNumber: '6300',
accountName: 'Einkommensteuer',
accountClass: 6,
accountType: 'expense',
skrType: 'SKR04',
description: 'Income tax',
},
{
accountNumber: '6400',
accountName: 'Gewerbesteuer',
accountClass: 6,
accountType: 'expense',
skrType: 'SKR04',
description: 'Trade tax',
},
{
accountNumber: '6500',
accountName: 'Sonstige Steuern',
accountClass: 6,
accountType: 'expense',
skrType: 'SKR04',
description: 'Other taxes',
},
{
accountNumber: '6600',
accountName: 'Zinsaufwendungen',
accountClass: 6,
accountType: 'expense',
skrType: 'SKR04',
description: 'Interest expense',
},
{
accountNumber: '6700',
accountName: 'Währungsverluste',
accountClass: 6,
accountType: 'expense',
skrType: 'SKR04',
description: 'Currency losses',
},
{
accountNumber: '6800',
accountName: 'Außerordentliche Aufwendungen',
accountClass: 6,
accountType: 'expense',
skrType: 'SKR04',
description: 'Extraordinary expenses',
},
{
accountNumber: '6900',
accountName: 'Verluste aus Unternehmensverträgen',
accountClass: 6,
accountType: 'expense',
skrType: 'SKR04',
description: 'Losses from company agreements',
},
// Class 7: Cost Accounting (Kosten- und Leistungsrechnung)
{
accountNumber: '7000',
accountName: 'Kostenstellenrechnung',
accountClass: 7,
accountType: 'expense',
skrType: 'SKR04',
description: 'Cost center accounting',
},
{
accountNumber: '7100',
accountName: 'Kostenträgerrechnung',
accountClass: 7,
accountType: 'expense',
skrType: 'SKR04',
description: 'Cost object accounting',
},
{
accountNumber: '7200',
accountName: 'Kostenartenrechnung',
accountClass: 7,
accountType: 'expense',
skrType: 'SKR04',
description: 'Cost type accounting',
},
{
accountNumber: '7300',
accountName: 'Kalkulatorische Kosten',
accountClass: 7,
accountType: 'expense',
skrType: 'SKR04',
description: 'Imputed costs',
},
{
accountNumber: '7400',
accountName: 'Kalkulatorische Abschreibungen',
accountClass: 7,
accountType: 'expense',
skrType: 'SKR04',
description: 'Imputed depreciation',
},
{
accountNumber: '7500',
accountName: 'Kalkulatorische Zinsen',
accountClass: 7,
accountType: 'expense',
skrType: 'SKR04',
description: 'Imputed interest',
},
{
accountNumber: '7600',
accountName: 'Kalkulatorischer Unternehmerlohn',
accountClass: 7,
accountType: 'expense',
skrType: 'SKR04',
description: 'Imputed entrepreneur salary',
},
{
accountNumber: '7700',
accountName: 'Kalkulatorische Miete',
accountClass: 7,
accountType: 'expense',
skrType: 'SKR04',
description: 'Imputed rent',
},
{
accountNumber: '7800',
accountName: 'Verrechnete Kosten',
accountClass: 7,
accountType: 'expense',
skrType: 'SKR04',
description: 'Allocated costs',
},
{
accountNumber: '7900',
accountName: 'Verrechnete Leistungen',
accountClass: 7,
accountType: 'revenue',
skrType: 'SKR04',
description: 'Allocated services',
},
// Class 8: Free for Use (Zur freien Verfügung)
{
accountNumber: '8000',
accountName: 'frei',
accountClass: 8,
accountType: 'equity',
skrType: 'SKR04',
description: 'Available for custom use',
},
{
accountNumber: '8100',
accountName: 'frei',
accountClass: 8,
accountType: 'equity',
skrType: 'SKR04',
description: 'Available for custom use',
},
{
accountNumber: '8200',
accountName: 'frei',
accountClass: 8,
accountType: 'equity',
skrType: 'SKR04',
description: 'Available for custom use',
},
{
accountNumber: '8300',
accountName: 'frei',
accountClass: 8,
accountType: 'equity',
skrType: 'SKR04',
description: 'Available for custom use',
},
{
accountNumber: '8400',
accountName: 'frei',
accountClass: 8,
accountType: 'equity',
skrType: 'SKR04',
description: 'Available for custom use',
},
{
accountNumber: '8500',
accountName: 'frei',
accountClass: 8,
accountType: 'equity',
skrType: 'SKR04',
description: 'Available for custom use',
},
{
accountNumber: '8600',
accountName: 'frei',
accountClass: 8,
accountType: 'equity',
skrType: 'SKR04',
description: 'Available for custom use',
},
{
accountNumber: '8700',
accountName: 'frei',
accountClass: 8,
accountType: 'equity',
skrType: 'SKR04',
description: 'Available for custom use',
},
{
accountNumber: '8800',
accountName: 'frei',
accountClass: 8,
accountType: 'equity',
skrType: 'SKR04',
description: 'Available for custom use',
},
{
accountNumber: '8900',
accountName: 'frei',
accountClass: 8,
accountType: 'equity',
skrType: 'SKR04',
description: 'Available for custom use',
},
// Class 9: Equity and Closing Accounts (Eigenkapital und Abschlusskonten)
{
accountNumber: '9000',
accountName: 'Eigenkapital',
accountClass: 9,
accountType: 'equity',
skrType: 'SKR04',
description: 'Equity capital',
},
{
accountNumber: '9100',
accountName: 'Gezeichnetes Kapital',
accountClass: 9,
accountType: 'equity',
skrType: 'SKR04',
description: 'Subscribed capital',
},
{
accountNumber: '9200',
accountName: 'Kapitalrücklage',
accountClass: 9,
accountType: 'equity',
skrType: 'SKR04',
description: 'Capital reserves',
},
{
accountNumber: '9300',
accountName: 'Gewinnrücklagen',
accountClass: 9,
accountType: 'equity',
skrType: 'SKR04',
description: 'Revenue reserves',
},
{
accountNumber: '9400',
accountName: 'Gewinnvortrag/Verlustvortrag',
accountClass: 9,
accountType: 'equity',
skrType: 'SKR04',
description: 'Profit/loss carried forward',
},
{
accountNumber: '9500',
accountName: 'Jahresüberschuss/Jahresfehlbetrag',
accountClass: 9,
accountType: 'equity',
skrType: 'SKR04',
description: 'Annual profit/loss',
},
{
accountNumber: '9600',
accountName: 'Rückstellungen',
accountClass: 9,
accountType: 'liability',
skrType: 'SKR04',
description: 'Provisions',
},
{
accountNumber: '9700',
accountName: 'Verbindlichkeiten',
accountClass: 9,
accountType: 'liability',
skrType: 'SKR04',
description: 'Liabilities',
},
{
accountNumber: '9800',
accountName: 'Rechnungsabgrenzungsposten',
accountClass: 9,
accountType: 'liability',
skrType: 'SKR04',
description: 'Accruals and deferrals',
},
{
accountNumber: '9900',
accountName: 'Statistische Konten',
accountClass: 9,
accountType: 'equity',
skrType: 'SKR04',
description: 'Statistical accounts',
},
];
export const SKR04_ACCOUNT_CLASSES = {
0: 'Anlagekonten (Fixed Assets)',
1: 'Finanz- und Umlaufvermögen (Financial and Current Assets)',
2: 'Aufwendungen Teil 1 (Expenses Part 1)',
3: 'Aufwendungen Teil 2 (Expenses Part 2)',
4: 'Erträge Teil 1 (Revenues Part 1)',
5: 'Erträge Teil 2 (Revenues Part 2)',
6: 'Sonderkonten (Special Accounts)',
7: 'Kosten- und Leistungsrechnung (Cost Accounting)',
8: 'Zur freien Verfügung (Free for Use)',
9: 'Eigenkapital und Abschlusskonten (Equity and Closing Accounts)',
};