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
This commit is contained in:
238
ts/skr.classes.account.ts
Normal file
238
ts/skr.classes.account.ts
Normal 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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user