Files
skr/ts/skr.classes.account.ts
Juergen Kunz 8a9056e767
Some checks failed
Default (tags) / security (push) Successful in 44s
Default (tags) / test (push) Failing after 4m4s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
feat(core): initial release of SKR03/SKR04 German accounting standards implementation
- Complete implementation of German standard charts of accounts
- SKR03 (Process Structure Principle) for trading/service companies
- SKR04 (Financial Classification Principle) for manufacturing companies
- Double-entry bookkeeping with MongoDB persistence
- Comprehensive reporting suite with DATEV export
- Full TypeScript support and type safety
2025-08-09 12:00:40 +00:00

239 lines
5.7 KiB
TypeScript

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}`);
}
}
}