- 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
239 lines
5.7 KiB
TypeScript
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}`);
|
|
}
|
|
}
|
|
}
|