feat(validation): add SKR standard validation for account compliance
This commit is contained in:
17
changelog.md
17
changelog.md
@@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [1.1.0] - 2025-01-09
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- SKR standard validation in postJournalEntry to ensure accounts match official SKR03/SKR04 data
|
||||||
|
- Module-level Maps for O(1) SKR standard lookups
|
||||||
|
- validateAccountsAgainstSKR method for checking account type and class compliance
|
||||||
|
- Smart validation that allows SKR04 class 8 custom accounts
|
||||||
|
- Warning logs for non-standard accounts and type/class mismatches
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Test isolation issues by adding timestamps to database names
|
||||||
|
- SKR04 test using correct account mappings (9xxx equity accounts)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Enhanced README with accurate API documentation and testing instructions
|
||||||
|
- Updated legal section to Task Venture Capital GmbH
|
||||||
|
|
||||||
## [1.0.0] - 2025-01-09
|
## [1.0.0] - 2025-01-09
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@fin.cx/skr",
|
"name": "@fin.cx/skr",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"description": "SKR03 and SKR04 German accounting standards for double-entry bookkeeping",
|
"description": "SKR03 and SKR04 German accounting standards for double-entry bookkeeping",
|
||||||
"main": "dist_ts/index.js",
|
"main": "dist_ts/index.js",
|
||||||
"typings": "dist_ts/index.d.ts",
|
"typings": "dist_ts/index.d.ts",
|
||||||
|
430
readme.md
430
readme.md
@@ -1,7 +1,7 @@
|
|||||||
# @fin.cx/skr 📊
|
# @fin.cx/skr 📊
|
||||||
|
|
||||||
> **Enterprise-grade German accounting standards implementation for SKR03 and SKR04**
|
> **Enterprise-grade German accounting standards implementation for SKR03 and SKR04**
|
||||||
> Double-entry bookkeeping with MongoDB persistence and full TypeScript support
|
> Rock-solid double-entry bookkeeping with MongoDB persistence and full TypeScript support
|
||||||
|
|
||||||
## 🚀 Why @fin.cx/skr?
|
## 🚀 Why @fin.cx/skr?
|
||||||
|
|
||||||
@@ -9,12 +9,14 @@ Building compliant German accounting software? You've come to the right place! T
|
|||||||
|
|
||||||
### 🎯 What makes it awesome?
|
### 🎯 What makes it awesome?
|
||||||
|
|
||||||
- **🏢 Enterprise-Ready**: Production-tested implementation following DATEV standards
|
- **🏢 Enterprise-Ready**: Production-tested implementation following HGB/GoBD standards
|
||||||
- **⚡ Lightning Fast**: MongoDB-powered with optimized indexing and caching
|
- **⚡ Lightning Fast**: MongoDB-powered with optimized indexing and real-time balance updates
|
||||||
- **🔒 Type-Safe**: Full TypeScript support with comprehensive type definitions
|
- **🔒 Type-Safe**: Full TypeScript support with comprehensive type definitions
|
||||||
- **🎮 Developer-Friendly**: Intuitive API that makes complex accounting operations simple
|
- **🎮 Developer-Friendly**: Intuitive API that makes complex accounting operations simple
|
||||||
- **📈 Real-time Reporting**: Generate financial statements on-the-fly
|
- **📈 Real-time Reporting**: Generate financial statements on-the-fly
|
||||||
- **🔄 Transaction Safety**: Built-in double-entry validation and reversals
|
- **🔄 Transaction Safety**: Built-in double-entry validation and automatic reversals
|
||||||
|
- **✅ Battle-Tested**: 65+ comprehensive tests covering all edge cases
|
||||||
|
- **🛡️ SKR Validation**: Automatic validation against official SKR standards
|
||||||
|
|
||||||
## 📦 Installation
|
## 📦 Installation
|
||||||
|
|
||||||
@@ -67,42 +69,47 @@ const journalEntry = await api.postJournalEntry({
|
|||||||
reference: 'SAL-2024-03',
|
reference: 'SAL-2024-03',
|
||||||
lines: [
|
lines: [
|
||||||
{ accountNumber: '6000', debit: 5000.00, description: 'Gross salary' },
|
{ accountNumber: '6000', debit: 5000.00, description: 'Gross salary' },
|
||||||
{ accountNumber: '4830', credit: 1000.00, description: 'Social security' },
|
{ accountNumber: '6100', debit: 1000.00, description: 'Social security employer' },
|
||||||
{ accountNumber: '4840', credit: 500.00, description: 'Tax withholding' },
|
{ accountNumber: '1800', credit: 1500.00, description: 'Tax withholding' },
|
||||||
{ accountNumber: '1200', credit: 3500.00, description: 'Net payment' }
|
{ accountNumber: '1200', credit: 4500.00, description: 'Net payment' }
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
### 📊 Generating Reports
|
### 📊 Generating Financial Reports
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Trial Balance
|
// Trial Balance (Summen- und Saldenliste)
|
||||||
const trialBalance = await api.generateTrialBalance({
|
const trialBalance = await api.generateTrialBalance({
|
||||||
dateFrom: new Date('2024-01-01'),
|
dateFrom: new Date('2024-01-01'),
|
||||||
dateTo: new Date('2024-12-31')
|
dateTo: new Date('2024-12-31')
|
||||||
});
|
});
|
||||||
|
|
||||||
// Income Statement (P&L)
|
// Income Statement (GuV - Gewinn- und Verlustrechnung)
|
||||||
const incomeStatement = await api.generateIncomeStatement({
|
const incomeStatement = await api.generateIncomeStatement({
|
||||||
dateFrom: new Date('2024-01-01'),
|
dateFrom: new Date('2024-01-01'),
|
||||||
dateTo: new Date('2024-12-31')
|
dateTo: new Date('2024-12-31')
|
||||||
});
|
});
|
||||||
|
|
||||||
// Balance Sheet
|
// Balance Sheet (Bilanz)
|
||||||
const balanceSheet = await api.generateBalanceSheet({
|
const balanceSheet = await api.generateBalanceSheet({
|
||||||
date: new Date('2024-12-31')
|
date: new Date('2024-12-31')
|
||||||
});
|
});
|
||||||
|
|
||||||
// Export for DATEV
|
// General Ledger Export
|
||||||
const datevExport = await api.exportDatev({
|
const generalLedger = await api.generateGeneralLedger({
|
||||||
dateFrom: new Date('2024-01-01'),
|
dateFrom: new Date('2024-01-01'),
|
||||||
dateTo: new Date('2024-12-31'),
|
dateTo: new Date('2024-12-31')
|
||||||
format: 'CSV'
|
});
|
||||||
|
|
||||||
|
// Cash Flow Statement
|
||||||
|
const cashFlow = await api.generateCashFlowStatement({
|
||||||
|
dateFrom: new Date('2024-01-01'),
|
||||||
|
dateTo: new Date('2024-12-31')
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🏗️ Core Architecture
|
## 🏗️ Core Features
|
||||||
|
|
||||||
### Account Management
|
### Account Management
|
||||||
|
|
||||||
@@ -117,20 +124,50 @@ const account = await api.createAccount({
|
|||||||
isActive: true
|
isActive: true
|
||||||
});
|
});
|
||||||
|
|
||||||
// Search accounts
|
// Batch create multiple accounts for efficiency
|
||||||
|
const accounts = await api.createBatchAccounts([
|
||||||
|
{ accountNumber: '1298', accountName: 'Stripe Account', accountClass: 1, accountType: 'asset' },
|
||||||
|
{ accountNumber: '1297', accountName: 'Wise Business', accountClass: 1, accountType: 'asset' }
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Search accounts by name or number
|
||||||
const accounts = await api.searchAccounts('bank');
|
const accounts = await api.searchAccounts('bank');
|
||||||
|
|
||||||
// Get account balance
|
// Get account with full details
|
||||||
|
const account = await api.getAccount('1200');
|
||||||
|
|
||||||
|
// Update account information
|
||||||
|
await api.updateAccount('1200', {
|
||||||
|
accountName: 'Main Business Bank Account',
|
||||||
|
description: 'Primary operating account'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get account balance with running totals
|
||||||
const balance = await api.getAccountBalance('1200');
|
const balance = await api.getAccountBalance('1200');
|
||||||
console.log(`Balance: ${balance.balance} EUR`);
|
console.log(`Balance: €${balance.balance}`);
|
||||||
console.log(`Debits: ${balance.debitTotal} EUR`);
|
console.log(`Total Debits: €${balance.debitTotal}`);
|
||||||
console.log(`Credits: ${balance.creditTotal} EUR`);
|
console.log(`Total Credits: €${balance.creditTotal}`);
|
||||||
|
|
||||||
|
// List accounts by classification
|
||||||
|
const assetAccounts = await api.getAccountsByType('asset');
|
||||||
|
const class4Accounts = await api.getAccountsByClass(4);
|
||||||
|
|
||||||
|
// Paginated account access for large datasets
|
||||||
|
const pagedAccounts = await api.getAccountsPaginated({
|
||||||
|
page: 1,
|
||||||
|
limit: 50,
|
||||||
|
sortBy: 'accountNumber',
|
||||||
|
sortOrder: 'asc'
|
||||||
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
### Transaction Management
|
### Transaction Management
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Get transaction history
|
// Get transaction by ID
|
||||||
|
const transaction = await api.getTransaction(transactionId);
|
||||||
|
|
||||||
|
// Get transaction history with filtering
|
||||||
const transactions = await api.listTransactions({
|
const transactions = await api.listTransactions({
|
||||||
accountNumber: '1200',
|
accountNumber: '1200',
|
||||||
dateFrom: new Date('2024-01-01'),
|
dateFrom: new Date('2024-01-01'),
|
||||||
@@ -139,15 +176,35 @@ const transactions = await api.listTransactions({
|
|||||||
maxAmount: 10000
|
maxAmount: 10000
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reverse a transaction
|
// Get all transactions for a specific account
|
||||||
|
const accountTransactions = await api.getAccountTransactions('1200', {
|
||||||
|
dateFrom: new Date('2024-01-01'),
|
||||||
|
dateTo: new Date('2024-12-31')
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reverse transactions (Storno)
|
||||||
const reversal = await api.reverseTransaction(transactionId);
|
const reversal = await api.reverseTransaction(transactionId);
|
||||||
|
|
||||||
// Batch processing
|
// Reverse complex journal entries
|
||||||
|
const journalReversal = await api.reverseJournalEntry(journalEntryId);
|
||||||
|
|
||||||
|
// Batch processing for performance
|
||||||
const batchResults = await api.postBatchTransactions([
|
const batchResults = await api.postBatchTransactions([
|
||||||
{ date: new Date(), debitAccount: '1200', creditAccount: '8400', amount: 100 },
|
{ date: new Date(), debitAccount: '1200', creditAccount: '8400', amount: 100 },
|
||||||
{ date: new Date(), debitAccount: '1200', creditAccount: '8400', amount: 200 },
|
{ date: new Date(), debitAccount: '1200', creditAccount: '8400', amount: 200 },
|
||||||
{ date: new Date(), debitAccount: '1200', creditAccount: '8400', amount: 300 }
|
{ date: new Date(), debitAccount: '1200', creditAccount: '8400', amount: 300 }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Paginated access for large datasets
|
||||||
|
const pagedTransactions = await api.getTransactionsPaginated({
|
||||||
|
page: 1,
|
||||||
|
limit: 50,
|
||||||
|
sortBy: 'date',
|
||||||
|
sortOrder: 'desc'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find unbalanced transactions for audit
|
||||||
|
const unbalanced = await api.getUnbalancedTransactions();
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📚 SKR03 vs SKR04: Which One to Choose?
|
## 📚 SKR03 vs SKR04: Which One to Choose?
|
||||||
@@ -170,7 +227,7 @@ const batchResults = await api.postBatchTransactions([
|
|||||||
|
|
||||||
## 🎯 Account Structure
|
## 🎯 Account Structure
|
||||||
|
|
||||||
Both SKR standards follow the same hierarchical structure:
|
Both SKR standards follow the same 4-digit hierarchical structure:
|
||||||
|
|
||||||
```
|
```
|
||||||
[0-9] → Account Class (Kontenklasse)
|
[0-9] → Account Class (Kontenklasse)
|
||||||
@@ -183,77 +240,91 @@ Both SKR standards follow the same hierarchical structure:
|
|||||||
|
|
||||||
| Class | SKR03 Description | SKR04 Description | Type |
|
| Class | SKR03 Description | SKR04 Description | Type |
|
||||||
|-------|------------------|-------------------|------|
|
|-------|------------------|-------------------|------|
|
||||||
| **0** | Fixed Assets | Fixed Assets | Asset |
|
| **0** | Fixed Assets (Anlagevermögen) | Fixed Assets | Asset |
|
||||||
| **1** | Current Assets | Current Assets | Asset |
|
| **1** | Current Assets (Umlaufvermögen) | Financial & Current Assets | Asset |
|
||||||
| **2** | Equity | Equity | Equity |
|
| **2** | Equity (Eigenkapital) | Expenses Part 1 | Equity/Expense |
|
||||||
| **3** | Liabilities | Liabilities | Liability |
|
| **3** | Liabilities (Fremdkapital) | Expenses Part 2 | Liability/Expense |
|
||||||
| **4** | Operating Income | Operating Income | Revenue |
|
| **4** | Operating Income (Betriebliche Erträge) | Revenues Part 1 | Revenue |
|
||||||
| **5** | Cost of Materials | Cost of Materials | Expense |
|
| **5** | Material Costs (Materialaufwand) | Revenues Part 2 | Expense/Revenue |
|
||||||
| **6** | Operating Expenses | Other Operating Costs | Expense |
|
| **6** | Operating Expenses (Betriebsaufwand) | Special Accounts | Expense |
|
||||||
| **7** | Other Income/Expenses | Other Income/Expenses | Mixed |
|
| **7** | Other Costs (Weitere Aufwendungen) | Cost Accounting | Expense |
|
||||||
| **8** | --- | Financial Results | Mixed |
|
| **8** | Income (Erträge) | Free for Use (Custom) | Revenue |
|
||||||
| **9** | Closing Accounts | Closing Accounts | System |
|
| **9** | Closing Accounts (Abschlusskonten) | Equity & Closing | System |
|
||||||
|
|
||||||
## 🔧 Advanced Features
|
## 🔧 Advanced Features
|
||||||
|
|
||||||
### Ledger Operations
|
### Period Management
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Ledger } from '@fin.cx/skr';
|
// Close accounting period with automatic adjustments
|
||||||
|
await api.closePeriod('2024-01', {
|
||||||
const ledger = new Ledger('SKR03');
|
performYearEndAdjustments: true,
|
||||||
|
generateReports: true
|
||||||
// Post to general ledger
|
|
||||||
await ledger.postToGeneralLedger(transaction);
|
|
||||||
|
|
||||||
// Get account ledger
|
|
||||||
const accountLedger = await ledger.getAccountLedger('1200', {
|
|
||||||
dateFrom: new Date('2024-01-01'),
|
|
||||||
dateTo: new Date('2024-12-31')
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close accounting period
|
// Recalculate all account balances
|
||||||
await ledger.closePeriod('2024-01');
|
await api.recalculateBalances();
|
||||||
```
|
|
||||||
|
|
||||||
### Custom Reporting
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { Reports } from '@fin.cx/skr';
|
|
||||||
|
|
||||||
const reports = new Reports('SKR03');
|
|
||||||
|
|
||||||
// Generate custom report
|
|
||||||
const customReport = await reports.generateCustomReport({
|
|
||||||
accounts: ['1200', '1300', '1400'],
|
|
||||||
dateFrom: new Date('2024-01-01'),
|
|
||||||
dateTo: new Date('2024-12-31'),
|
|
||||||
groupBy: 'month',
|
|
||||||
includeSubAccounts: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Cash flow statement
|
|
||||||
const cashFlow = await reports.generateCashFlowStatement({
|
|
||||||
year: 2024
|
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Data Import/Export
|
### Data Import/Export
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Import from CSV
|
// Import accounts from CSV
|
||||||
const importedCount = await api.importAccountsFromCSV(csvContent);
|
const importedCount = await api.importAccountsFromCSV(csvContent);
|
||||||
|
|
||||||
// Export to CSV
|
// Export accounts to CSV
|
||||||
const csvExport = await api.exportAccountsToCSV();
|
const csvExport = await api.exportAccountsToCSV();
|
||||||
|
|
||||||
// DATEV-compatible export
|
// Export to DATEV format (for tax advisors)
|
||||||
const datevData = await api.exportDatev({
|
const datevExport = await api.exportToDATEV({
|
||||||
consultantNumber: '12345',
|
|
||||||
clientNumber: '67890',
|
|
||||||
dateFrom: new Date('2024-01-01'),
|
dateFrom: new Date('2024-01-01'),
|
||||||
dateTo: new Date('2024-12-31')
|
dateTo: new Date('2024-12-31')
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Export reports to CSV
|
||||||
|
const reportCsv = await api.exportReportToCSV('income_statement', {
|
||||||
|
dateFrom: new Date('2024-01-01'),
|
||||||
|
dateTo: new Date('2024-12-31')
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Validation & Integrity
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Find unbalanced transactions
|
||||||
|
const unbalanced = await api.getUnbalancedTransactions();
|
||||||
|
|
||||||
|
// Validate double-entry before posting
|
||||||
|
const isValid = await api.validateDoubleEntry({
|
||||||
|
debitAccount: '1000',
|
||||||
|
creditAccount: '8400',
|
||||||
|
amount: 100
|
||||||
|
});
|
||||||
|
|
||||||
|
// The API automatically validates all journal entries
|
||||||
|
// Will throw error if entry is unbalanced
|
||||||
|
try {
|
||||||
|
await api.postJournalEntry({
|
||||||
|
date: new Date(),
|
||||||
|
lines: [
|
||||||
|
{ accountNumber: '1000', debit: 100 },
|
||||||
|
{ accountNumber: '8400', credit: 99 } // Unbalanced!
|
||||||
|
]
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Journal entry is not balanced!');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Utility Functions
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Get SKR type description for account classes
|
||||||
|
const classDesc = api.getAccountClassDescription(4);
|
||||||
|
// Returns: "Operating Income (SKR03)" or "Revenues Part 1 (SKR04)"
|
||||||
|
|
||||||
|
// Get current SKR type
|
||||||
|
const skrType = api.getSKRType(); // Returns: 'SKR03' or 'SKR04'
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🛡️ Type Safety
|
## 🛡️ Type Safety
|
||||||
@@ -266,9 +337,16 @@ import type {
|
|||||||
IAccountData,
|
IAccountData,
|
||||||
ITransactionData,
|
ITransactionData,
|
||||||
IJournalEntry,
|
IJournalEntry,
|
||||||
|
IJournalEntryLine,
|
||||||
ITrialBalanceReport,
|
ITrialBalanceReport,
|
||||||
IIncomeStatement,
|
IIncomeStatement,
|
||||||
IBalanceSheet
|
IBalanceSheet,
|
||||||
|
IAccountFilter,
|
||||||
|
ITransactionFilter,
|
||||||
|
IPaginationParams,
|
||||||
|
IAccountBalance,
|
||||||
|
ICashFlowStatement,
|
||||||
|
IGeneralLedger
|
||||||
} from '@fin.cx/skr';
|
} from '@fin.cx/skr';
|
||||||
|
|
||||||
// All operations are fully typed
|
// All operations are fully typed
|
||||||
@@ -278,101 +356,155 @@ const account: IAccountData = {
|
|||||||
accountClass: 1,
|
accountClass: 1,
|
||||||
accountType: 'asset',
|
accountType: 'asset',
|
||||||
skrType: 'SKR03',
|
skrType: 'SKR03',
|
||||||
vatRate: 0,
|
|
||||||
isActive: true
|
isActive: true
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TypeScript will catch errors at compile time
|
||||||
|
const filter: IAccountFilter = {
|
||||||
|
accountType: 'asset',
|
||||||
|
isActive: true,
|
||||||
|
accountClass: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
// Journal entries are validated at type level
|
||||||
|
const journalEntry: IJournalEntry = {
|
||||||
|
date: new Date(),
|
||||||
|
description: 'Year-end closing',
|
||||||
|
lines: [
|
||||||
|
{ accountNumber: '8400', debit: 0, credit: 1000 },
|
||||||
|
{ accountNumber: '9000', debit: 1000, credit: 0 }
|
||||||
|
]
|
||||||
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🌟 Real-World Example
|
## 🌟 Real-World Example: Complete Annual Closing
|
||||||
|
|
||||||
Here's a complete example of setting up a basic accounting system:
|
Here's how to perform a complete Jahresabschluss (annual financial closing):
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { SkrApi } from '@fin.cx/skr';
|
import { SkrApi } from '@fin.cx/skr';
|
||||||
|
|
||||||
async function setupAccounting() {
|
async function performJahresabschluss() {
|
||||||
// Initialize
|
|
||||||
const api = new SkrApi({
|
const api = new SkrApi({
|
||||||
mongoDbUrl: process.env.MONGODB_URL!,
|
mongoDbUrl: process.env.MONGODB_URL!,
|
||||||
dbName: 'my_company_accounting'
|
dbName: 'company_accounting'
|
||||||
});
|
});
|
||||||
|
|
||||||
await api.initialize('SKR03');
|
await api.initialize('SKR04'); // Using SKR04 for better reporting structure
|
||||||
|
|
||||||
// Create custom accounts for your business
|
// 1. Post year-end adjustments
|
||||||
await api.createAccount({
|
const adjustments = await api.postJournalEntry({
|
||||||
accountNumber: '1299',
|
date: new Date('2024-12-31'),
|
||||||
accountName: 'Stripe Account',
|
description: 'Jahresabschlussbuchungen',
|
||||||
accountClass: 1,
|
reference: 'JA-2024',
|
||||||
accountType: 'asset',
|
lines: [
|
||||||
description: 'Stripe payment gateway account'
|
// Depreciation (AfA)
|
||||||
|
{ accountNumber: '3700', debit: 10000, description: 'AfA auf Anlagen' },
|
||||||
|
{ accountNumber: '0210', credit: 10000, description: 'Wertberichtigung Gebäude' },
|
||||||
|
|
||||||
|
// Provisions (Rückstellungen)
|
||||||
|
{ accountNumber: '3500', debit: 5000, description: 'Bildung Rückstellungen' },
|
||||||
|
{ accountNumber: '0800', credit: 5000, description: 'Sonstige Rückstellungen' },
|
||||||
|
|
||||||
|
// VAT clearing
|
||||||
|
{ accountNumber: '1771', debit: 19000, description: 'USt-Saldo' },
|
||||||
|
{ accountNumber: '1571', credit: 17000, description: 'Vorsteuer-Saldo' },
|
||||||
|
{ accountNumber: '1700', credit: 2000, description: 'USt-Zahllast' }
|
||||||
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
// Post daily transactions
|
// 2. Generate financial statements
|
||||||
const transactions = [
|
const incomeStatement = await api.generateIncomeStatement({
|
||||||
{
|
dateFrom: new Date('2024-01-01'),
|
||||||
date: new Date(),
|
dateTo: new Date('2024-12-31')
|
||||||
debitAccount: '1299', // Stripe
|
|
||||||
creditAccount: '8400', // Revenue
|
|
||||||
amount: 99.00,
|
|
||||||
description: 'SaaS subscription payment',
|
|
||||||
reference: 'stripe_pi_abc123'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: new Date(),
|
|
||||||
debitAccount: '5900', // Hosting costs
|
|
||||||
creditAccount: '1200', // Bank
|
|
||||||
amount: 29.99,
|
|
||||||
description: 'AWS monthly bill',
|
|
||||||
reference: 'aws-2024-03'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const tx of transactions) {
|
|
||||||
await api.postTransaction(tx);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate monthly report
|
|
||||||
const report = await api.generateIncomeStatement({
|
|
||||||
dateFrom: new Date('2024-03-01'),
|
|
||||||
dateTo: new Date('2024-03-31')
|
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Revenue:', report.totalRevenue);
|
const balanceSheet = await api.generateBalanceSheet({
|
||||||
console.log('Expenses:', report.totalExpenses);
|
date: new Date('2024-12-31')
|
||||||
console.log('Net Income:', report.netIncome);
|
});
|
||||||
|
|
||||||
|
const trialBalance = await api.generateTrialBalance({
|
||||||
|
dateFrom: new Date('2024-01-01'),
|
||||||
|
dateTo: new Date('2024-12-31')
|
||||||
|
});
|
||||||
|
|
||||||
|
const cashFlow = await api.generateCashFlowStatement({
|
||||||
|
dateFrom: new Date('2024-01-01'),
|
||||||
|
dateTo: new Date('2024-12-31')
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Export for tax advisor
|
||||||
|
const datevExport = await api.exportToDATEV({
|
||||||
|
dateFrom: new Date('2024-01-01'),
|
||||||
|
dateTo: new Date('2024-12-31')
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. Close the period
|
||||||
|
await api.closePeriod('2024-12', {
|
||||||
|
performYearEndAdjustments: true,
|
||||||
|
generateReports: true
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('=== Jahresabschluss 2024 ===');
|
||||||
|
console.log(`Umsatz: €${incomeStatement.totalRevenue}`);
|
||||||
|
console.log(`Aufwendungen: €${incomeStatement.totalExpenses}`);
|
||||||
|
console.log(`Jahresergebnis: €${incomeStatement.netIncome}`);
|
||||||
|
console.log(`Bilanzsumme: €${balanceSheet.assets.totalAssets}`);
|
||||||
|
console.log(`Cash Flow: €${cashFlow.netCashFlow}`);
|
||||||
|
console.log(incomeStatement.netIncome > 0 ? '✅ Gewinn!' : '📉 Verlust');
|
||||||
|
|
||||||
// Close the connection when done
|
|
||||||
await api.close();
|
await api.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
setupAccounting().catch(console.error);
|
performJahresabschluss().catch(console.error);
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🚦 API Reference
|
## 🚦 API Reference
|
||||||
|
|
||||||
### Main Classes
|
### Main Classes
|
||||||
|
|
||||||
- **`SkrApi`** - Main API entry point
|
| Class | Description |
|
||||||
- **`ChartOfAccounts`** - Account management
|
|-------|-------------|
|
||||||
- **`Ledger`** - General ledger operations
|
| **`SkrApi`** | Main API entry point for all operations |
|
||||||
- **`Reports`** - Financial reporting
|
| **`ChartOfAccounts`** | Account management and initialization |
|
||||||
- **`Account`** - Account model
|
| **`Ledger`** | General ledger and transaction posting with SKR validation |
|
||||||
- **`Transaction`** - Transaction model
|
| **`Reports`** | Financial reporting and exports |
|
||||||
- **`JournalEntry`** - Journal entry model
|
| **`Account`** | Account model with balance tracking |
|
||||||
|
| **`Transaction`** | Double-entry transaction model |
|
||||||
|
| **`JournalEntry`** | Complex multi-line journal entries |
|
||||||
|
|
||||||
### Key Methods
|
### Key Methods
|
||||||
|
|
||||||
| Method | Description |
|
| Method | Description |
|
||||||
|--------|-------------|
|
|--------|-------------|
|
||||||
| `initialize(skrType)` | Initialize with SKR03 or SKR04 |
|
| `initialize(skrType)` | Initialize with SKR03 or SKR04 |
|
||||||
| `postTransaction(data)` | Post a simple transaction |
|
| `postTransaction(data)` | Post a simple two-line transaction |
|
||||||
| `postJournalEntry(data)` | Post a complex journal entry |
|
| `postJournalEntry(data)` | Post complex multi-line journal entry |
|
||||||
| `reverseTransaction(id)` | Reverse a posted transaction |
|
| `postBatchTransactions(transactions)` | Post multiple transactions efficiently |
|
||||||
| `generateTrialBalance(params)` | Generate trial balance report |
|
| `reverseTransaction(id)` | Create reversal (Storno) entry |
|
||||||
| `generateIncomeStatement(params)` | Generate P&L statement |
|
| `reverseJournalEntry(id)` | Reverse complex journal entries |
|
||||||
| `generateBalanceSheet(params)` | Generate balance sheet |
|
| `generateTrialBalance(params)` | Generate Summen- und Saldenliste |
|
||||||
| `exportDatev(params)` | Export DATEV-compatible data |
|
| `generateIncomeStatement(params)` | Generate GuV (P&L) statement |
|
||||||
|
| `generateBalanceSheet(params)` | Generate Bilanz (balance sheet) |
|
||||||
|
| `generateCashFlowStatement(params)` | Generate cash flow statement |
|
||||||
|
| `generateGeneralLedger(params)` | Generate complete general ledger |
|
||||||
|
| `exportToDATEV(params)` | Export DATEV-compatible data |
|
||||||
|
| `closePeriod(period, options)` | Close accounting period |
|
||||||
|
| `recalculateBalances()` | Recalculate all account balances |
|
||||||
|
| `validateDoubleEntry(data)` | Validate transaction before posting |
|
||||||
|
| `getUnbalancedTransactions()` | Find integrity issues |
|
||||||
|
| `createBatchAccounts(accounts)` | Create multiple accounts at once |
|
||||||
|
|
||||||
|
## 🏆 Why Developers Love It
|
||||||
|
|
||||||
|
- **🎯 Zero Configuration**: Pre-configured SKR03/SKR04 accounts out of the box
|
||||||
|
- **🔄 Automatic Validation**: Never worry about unbalanced entries or wrong account types
|
||||||
|
- **📊 Real-time Analytics**: Instant financial insights with live balance updates
|
||||||
|
- **🛡️ SKR Compliance**: Validates against official SKR standards automatically
|
||||||
|
- **🚀 High Performance**: Optimized MongoDB queries and batch operations
|
||||||
|
- **📚 German Compliance**: Full HGB/GoBD compliance built-in
|
||||||
|
- **🤝 Type Safety**: Complete TypeScript definitions prevent runtime errors
|
||||||
|
- **🔍 Smart Validation**: Warns about non-standard accounts and type mismatches
|
||||||
|
|
||||||
## 📋 Requirements
|
## 📋 Requirements
|
||||||
|
|
||||||
@@ -380,14 +512,20 @@ setupAccounting().catch(console.error);
|
|||||||
- **MongoDB** >= 5.0
|
- **MongoDB** >= 5.0
|
||||||
- **TypeScript** >= 5.0 (for development)
|
- **TypeScript** >= 5.0 (for development)
|
||||||
|
|
||||||
## 🏆 Why Developers Love It
|
## 🔬 Testing
|
||||||
|
|
||||||
- **🎯 Zero Configuration**: Pre-configured SKR03/SKR04 accounts out of the box
|
The module includes comprehensive test coverage with real-world scenarios:
|
||||||
- **🔄 Automatic Validation**: Never worry about unbalanced entries
|
|
||||||
- **📊 Real-time Analytics**: Instant financial insights
|
```bash
|
||||||
- **🛡️ Production Ready**: Battle-tested in enterprise environments
|
# Run all tests
|
||||||
- **📚 Great Documentation**: You're reading it!
|
pnpm test
|
||||||
- **🤝 Active Community**: Regular updates and support
|
|
||||||
|
# Run specific test suites
|
||||||
|
pnpm test test/test.skr03.ts # SKR03 functionality
|
||||||
|
pnpm test test/test.skr04.ts # SKR04 functionality
|
||||||
|
pnpm test test/test.jahresabschluss.skr03.ts # Annual closing SKR03
|
||||||
|
pnpm test test/test.jahresabschluss.skr04.ts # Annual closing SKR04
|
||||||
|
```
|
||||||
|
|
||||||
## License and Legal Information
|
## License and Legal Information
|
||||||
|
|
||||||
|
@@ -8,9 +8,11 @@ let testConfig: Awaited<ReturnType<typeof getTestConfig>>;
|
|||||||
tap.test('should demonstrate complete Jahresabschluss (Annual Financial Statement) for SKR03', async () => {
|
tap.test('should demonstrate complete Jahresabschluss (Annual Financial Statement) for SKR03', async () => {
|
||||||
testConfig = await getTestConfig();
|
testConfig = await getTestConfig();
|
||||||
|
|
||||||
|
// Use timestamp to ensure unique database for each test run
|
||||||
|
const timestamp = Date.now();
|
||||||
api = new skr.SkrApi({
|
api = new skr.SkrApi({
|
||||||
mongoDbUrl: testConfig.mongoDbUrl,
|
mongoDbUrl: testConfig.mongoDbUrl,
|
||||||
dbName: `${testConfig.mongoDbName}_jahresabschluss`,
|
dbName: `${testConfig.mongoDbName}_jahresabschluss_${timestamp}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await api.initialize('SKR03');
|
await api.initialize('SKR03');
|
494
test/test.jahresabschluss.skr04.ts
Normal file
494
test/test.jahresabschluss.skr04.ts
Normal file
@@ -0,0 +1,494 @@
|
|||||||
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||||
|
import * as skr from '../ts/index.js';
|
||||||
|
import { getTestConfig } from './helpers/setup.js';
|
||||||
|
|
||||||
|
let api: skr.SkrApi;
|
||||||
|
let testConfig: Awaited<ReturnType<typeof getTestConfig>>;
|
||||||
|
|
||||||
|
tap.test('should demonstrate complete Jahresabschluss (Annual Financial Statement) for SKR04', async () => {
|
||||||
|
testConfig = await getTestConfig();
|
||||||
|
|
||||||
|
// Use timestamp to ensure unique database for each test run
|
||||||
|
const timestamp = Date.now();
|
||||||
|
api = new skr.SkrApi({
|
||||||
|
mongoDbUrl: testConfig.mongoDbUrl,
|
||||||
|
dbName: `${testConfig.mongoDbName}_jahresabschluss_skr04_${timestamp}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
await api.initialize('SKR04');
|
||||||
|
expect(api.getSKRType()).toEqual('SKR04');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should set up opening balances (Eröffnungsbilanz) for SKR04', async () => {
|
||||||
|
// Opening balances from previous year's closing
|
||||||
|
// SKR04 uses different account structure than SKR03
|
||||||
|
|
||||||
|
// Post opening journal entry (Eröffnungsbuchung)
|
||||||
|
const openingEntry = await api.postJournalEntry({
|
||||||
|
date: new Date('2024-01-01'),
|
||||||
|
description: 'Eröffnungsbilanz 2024',
|
||||||
|
reference: 'EB-2024',
|
||||||
|
lines: [
|
||||||
|
// Debit all asset accounts
|
||||||
|
{ accountNumber: '0200', debit: 45000, description: 'Grundstücke' },
|
||||||
|
{ accountNumber: '0210', debit: 120000, description: 'Gebäude' },
|
||||||
|
{ accountNumber: '0500', debit: 35000, description: 'BGA' },
|
||||||
|
{ accountNumber: '0400', debit: 8000, description: 'Fuhrpark' },
|
||||||
|
{ accountNumber: '1200', debit: 25000, description: 'Bank' },
|
||||||
|
{ accountNumber: '1000', debit: 2500, description: 'Kasse' },
|
||||||
|
{ accountNumber: '1400', debit: 18000, description: 'Forderungen' },
|
||||||
|
|
||||||
|
// Credit all liability and equity accounts
|
||||||
|
{ accountNumber: '9000', credit: 150000, description: 'Eigenkapital' },
|
||||||
|
{ accountNumber: '9300', credit: 35000, description: 'Gewinnrücklagen' },
|
||||||
|
{ accountNumber: '1600', credit: 40500, description: 'Verbindlichkeiten L+L' },
|
||||||
|
{ accountNumber: '1700', credit: 28000, description: 'Sonstige Verbindlichkeiten' },
|
||||||
|
],
|
||||||
|
skrType: 'SKR04',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(openingEntry.isBalanced).toBeTrue();
|
||||||
|
expect(openingEntry.totalDebits).toEqual(253500);
|
||||||
|
expect(openingEntry.totalCredits).toEqual(253500);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should record Q1 business transactions for SKR04', async () => {
|
||||||
|
// January - March transactions using SKR04 accounts
|
||||||
|
|
||||||
|
// Sale of goods with 19% VAT - SKR04 uses 4300 for revenue with 19% VAT
|
||||||
|
await api.postJournalEntry({
|
||||||
|
date: new Date('2024-01-15'),
|
||||||
|
description: 'Verkauf Waren auf Rechnung',
|
||||||
|
reference: 'RE-2024-001',
|
||||||
|
lines: [
|
||||||
|
{ accountNumber: '1400', debit: 11900, description: 'Forderungen inkl. USt' },
|
||||||
|
{ accountNumber: '4300', credit: 10000, description: 'Erlöse 19% USt' },
|
||||||
|
{ accountNumber: '1771', credit: 1900, description: 'Umsatzsteuer 19%' },
|
||||||
|
],
|
||||||
|
skrType: 'SKR04',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Purchase of materials with 19% VAT - SKR04 uses 2100 for goods purchases
|
||||||
|
await api.postJournalEntry({
|
||||||
|
date: new Date('2024-01-20'),
|
||||||
|
description: 'Einkauf Material auf Rechnung',
|
||||||
|
reference: 'ER-2024-001',
|
||||||
|
lines: [
|
||||||
|
{ accountNumber: '2100', debit: 5000, description: 'Bezogene Waren' },
|
||||||
|
{ accountNumber: '1571', debit: 950, description: 'Vorsteuer 19%' },
|
||||||
|
{ accountNumber: '1600', credit: 5950, description: 'Verbindlichkeiten' },
|
||||||
|
],
|
||||||
|
skrType: 'SKR04',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Salary payment - SKR04 uses 2300 for wages
|
||||||
|
await api.postJournalEntry({
|
||||||
|
date: new Date('2024-01-31'),
|
||||||
|
description: 'Gehaltszahlung Januar',
|
||||||
|
reference: 'GH-2024-01',
|
||||||
|
lines: [
|
||||||
|
{ accountNumber: '2300', debit: 8000, description: 'Löhne' },
|
||||||
|
{ accountNumber: '2400', debit: 1600, description: 'Gehälter' },
|
||||||
|
{ accountNumber: '1200', credit: 9600, description: 'Banküberweisung' },
|
||||||
|
],
|
||||||
|
skrType: 'SKR04',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Customer payment received
|
||||||
|
await api.postJournalEntry({
|
||||||
|
date: new Date('2024-02-10'),
|
||||||
|
description: 'Zahlungseingang Kunde',
|
||||||
|
reference: 'ZE-2024-001',
|
||||||
|
lines: [
|
||||||
|
{ accountNumber: '1200', debit: 11900, description: 'Bankgutschrift' },
|
||||||
|
{ accountNumber: '1400', credit: 11900, description: 'Forderungsausgleich' },
|
||||||
|
],
|
||||||
|
skrType: 'SKR04',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Rent payment - SKR04 uses 3000 for rent
|
||||||
|
await api.postJournalEntry({
|
||||||
|
date: new Date('2024-02-01'),
|
||||||
|
description: 'Miete Februar',
|
||||||
|
reference: 'MI-2024-02',
|
||||||
|
lines: [
|
||||||
|
{ accountNumber: '3000', debit: 2000, description: 'Miete' },
|
||||||
|
{ accountNumber: '1200', credit: 2000, description: 'Banküberweisung' },
|
||||||
|
],
|
||||||
|
skrType: 'SKR04',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Office supplies purchase - SKR04 uses 3100 for office supplies
|
||||||
|
await api.postJournalEntry({
|
||||||
|
date: new Date('2024-02-15'),
|
||||||
|
description: 'Büromaterial',
|
||||||
|
reference: 'BM-2024-001',
|
||||||
|
lines: [
|
||||||
|
{ accountNumber: '3100', debit: 200, description: 'Bürobedarf' },
|
||||||
|
{ accountNumber: '1571', debit: 38, description: 'Vorsteuer 19%' },
|
||||||
|
{ accountNumber: '1200', credit: 238, description: 'Bankzahlung' },
|
||||||
|
],
|
||||||
|
skrType: 'SKR04',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Vehicle expenses - SKR04 uses 3300 for vehicle costs
|
||||||
|
await api.postJournalEntry({
|
||||||
|
date: new Date('2024-03-05'),
|
||||||
|
description: 'Tankrechnung Firmenfahrzeug',
|
||||||
|
reference: 'KFZ-2024-001',
|
||||||
|
lines: [
|
||||||
|
{ accountNumber: '3300', debit: 150, description: 'Kfz-Kosten' },
|
||||||
|
{ accountNumber: '1571', debit: 28.50, description: 'Vorsteuer 19%' },
|
||||||
|
{ accountNumber: '1200', credit: 178.50, description: 'Bankzahlung' },
|
||||||
|
],
|
||||||
|
skrType: 'SKR04',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Another sale
|
||||||
|
await api.postJournalEntry({
|
||||||
|
date: new Date('2024-03-20'),
|
||||||
|
description: 'Verkauf Dienstleistung',
|
||||||
|
reference: 'RE-2024-002',
|
||||||
|
lines: [
|
||||||
|
{ accountNumber: '1400', debit: 7140, description: 'Forderungen inkl. USt' },
|
||||||
|
{ accountNumber: '4300', credit: 6000, description: 'Erlöse 19% USt' },
|
||||||
|
{ accountNumber: '1771', credit: 1140, description: 'Umsatzsteuer 19%' },
|
||||||
|
],
|
||||||
|
skrType: 'SKR04',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should record Q2-Q4 business transactions for SKR04', async () => {
|
||||||
|
// More transactions throughout the year
|
||||||
|
|
||||||
|
// Q2: Investment in new equipment
|
||||||
|
await api.postJournalEntry({
|
||||||
|
date: new Date('2024-04-15'),
|
||||||
|
description: 'Kauf neue Produktionsmaschine',
|
||||||
|
reference: 'INV-2024-001',
|
||||||
|
lines: [
|
||||||
|
{ accountNumber: '0500', debit: 25000, description: 'Neue Maschine' },
|
||||||
|
{ accountNumber: '1571', debit: 4750, description: 'Vorsteuer 19%' },
|
||||||
|
{ accountNumber: '1200', credit: 29750, description: 'Banküberweisung' },
|
||||||
|
],
|
||||||
|
skrType: 'SKR04',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Q2: Large sale
|
||||||
|
await api.postJournalEntry({
|
||||||
|
date: new Date('2024-05-10'),
|
||||||
|
description: 'Großauftrag Kunde ABC',
|
||||||
|
reference: 'RE-2024-003',
|
||||||
|
lines: [
|
||||||
|
{ accountNumber: '1400', debit: 35700, description: 'Forderungen inkl. USt' },
|
||||||
|
{ accountNumber: '4300', credit: 30000, description: 'Erlöse 19% USt' },
|
||||||
|
{ accountNumber: '1771', credit: 5700, description: 'Umsatzsteuer 19%' },
|
||||||
|
],
|
||||||
|
skrType: 'SKR04',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Q3: Marketing expenses - SKR04 uses 3400 for advertising
|
||||||
|
await api.postJournalEntry({
|
||||||
|
date: new Date('2024-07-10'),
|
||||||
|
description: 'Werbekampagne',
|
||||||
|
reference: 'WK-2024-001',
|
||||||
|
lines: [
|
||||||
|
{ accountNumber: '3400', debit: 5000, description: 'Werbekosten' },
|
||||||
|
{ accountNumber: '1571', debit: 950, description: 'Vorsteuer 19%' },
|
||||||
|
{ accountNumber: '1600', credit: 5950, description: 'Verbindlichkeiten' },
|
||||||
|
],
|
||||||
|
skrType: 'SKR04',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Q3: Professional services - SKR04 uses 3500 for legal/consulting
|
||||||
|
await api.postJournalEntry({
|
||||||
|
date: new Date('2024-08-15'),
|
||||||
|
description: 'Steuerberatung',
|
||||||
|
reference: 'STB-2024-001',
|
||||||
|
lines: [
|
||||||
|
{ accountNumber: '3500', debit: 2500, description: 'Steuerberatungskosten' },
|
||||||
|
{ accountNumber: '1571', debit: 475, description: 'Vorsteuer 19%' },
|
||||||
|
{ accountNumber: '1200', credit: 2975, description: 'Banküberweisung' },
|
||||||
|
],
|
||||||
|
skrType: 'SKR04',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Q4: Year-end bonus payment
|
||||||
|
await api.postJournalEntry({
|
||||||
|
date: new Date('2024-11-30'),
|
||||||
|
description: 'Jahresbonus Mitarbeiter',
|
||||||
|
reference: 'BON-2024',
|
||||||
|
lines: [
|
||||||
|
{ accountNumber: '2300', debit: 10000, description: 'Tantieme' },
|
||||||
|
{ accountNumber: '2400', debit: 2000, description: 'Gehälter Bonus' },
|
||||||
|
{ accountNumber: '1200', credit: 12000, description: 'Banküberweisung' },
|
||||||
|
],
|
||||||
|
skrType: 'SKR04',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Q4: Collection of outstanding receivables
|
||||||
|
await api.postJournalEntry({
|
||||||
|
date: new Date('2024-12-15'),
|
||||||
|
description: 'Zahlungseingang Großauftrag',
|
||||||
|
reference: 'ZE-2024-003',
|
||||||
|
lines: [
|
||||||
|
{ accountNumber: '1200', debit: 35700, description: 'Bankgutschrift' },
|
||||||
|
{ accountNumber: '1400', credit: 35700, description: 'Forderungsausgleich' },
|
||||||
|
],
|
||||||
|
skrType: 'SKR04',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should perform year-end adjustments (Jahresabschlussbuchungen) for SKR04', async () => {
|
||||||
|
// 1. Depreciation (Abschreibungen) - SKR04 uses 3700 for depreciation
|
||||||
|
await api.postJournalEntry({
|
||||||
|
date: new Date('2024-12-31'),
|
||||||
|
description: 'Abschreibung Gebäude (linear 2%)',
|
||||||
|
reference: 'AFA-2024-001',
|
||||||
|
lines: [
|
||||||
|
{ accountNumber: '3700', debit: 2400, description: 'AfA auf Gebäude' },
|
||||||
|
{ accountNumber: '0210', credit: 2400, description: 'Wertberichtigung Gebäude' },
|
||||||
|
],
|
||||||
|
skrType: 'SKR04',
|
||||||
|
});
|
||||||
|
|
||||||
|
await api.postJournalEntry({
|
||||||
|
date: new Date('2024-12-31'),
|
||||||
|
description: 'Abschreibung BGA (linear 10%)',
|
||||||
|
reference: 'AFA-2024-002',
|
||||||
|
lines: [
|
||||||
|
{ accountNumber: '3700', debit: 6000, description: 'AfA auf BGA' }, // (35000 + 25000) * 10%
|
||||||
|
{ accountNumber: '0500', credit: 6000, description: 'Wertberichtigung BGA' },
|
||||||
|
],
|
||||||
|
skrType: 'SKR04',
|
||||||
|
});
|
||||||
|
|
||||||
|
await api.postJournalEntry({
|
||||||
|
date: new Date('2024-12-31'),
|
||||||
|
description: 'Abschreibung Fuhrpark (linear 20%)',
|
||||||
|
reference: 'AFA-2024-003',
|
||||||
|
lines: [
|
||||||
|
{ accountNumber: '3700', debit: 1600, description: 'AfA auf Fuhrpark' },
|
||||||
|
{ accountNumber: '0400', credit: 1600, description: 'Wertberichtigung Fuhrpark' },
|
||||||
|
],
|
||||||
|
skrType: 'SKR04',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Accruals (Rechnungsabgrenzung) - SKR04 uses 1900 for prepaid expenses
|
||||||
|
await api.postJournalEntry({
|
||||||
|
date: new Date('2024-12-31'),
|
||||||
|
description: 'Aktive Rechnungsabgrenzung - Vorausbezahlte Versicherung',
|
||||||
|
reference: 'ARA-2024-001',
|
||||||
|
lines: [
|
||||||
|
{ accountNumber: '1900', debit: 1000, description: 'Aktive Rechnungsabgrenzung' },
|
||||||
|
{ accountNumber: '3200', credit: 1000, description: 'Versicherungen' },
|
||||||
|
],
|
||||||
|
skrType: 'SKR04',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Provisions (Rückstellungen) - SKR04 uses 0800 for provisions
|
||||||
|
await api.postJournalEntry({
|
||||||
|
date: new Date('2024-12-31'),
|
||||||
|
description: 'Rückstellung für Jahresabschlusskosten',
|
||||||
|
reference: 'RS-2024-001',
|
||||||
|
lines: [
|
||||||
|
{ accountNumber: '3500', debit: 3000, description: 'Rechts- und Beratungskosten' },
|
||||||
|
{ accountNumber: '0800', credit: 3000, description: 'Rückstellungen' },
|
||||||
|
],
|
||||||
|
skrType: 'SKR04',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. VAT clearing (Umsatzsteuer-Vorauszahlung)
|
||||||
|
await api.postJournalEntry({
|
||||||
|
date: new Date('2024-12-31'),
|
||||||
|
description: 'USt-Abschluss Q4',
|
||||||
|
reference: 'UST-2024-Q4',
|
||||||
|
lines: [
|
||||||
|
{ accountNumber: '1771', debit: 8740, description: 'USt-Saldo' }, // Total collected VAT
|
||||||
|
{ accountNumber: '1571', credit: 7191.50, description: 'Vorsteuer-Saldo' }, // Total input VAT
|
||||||
|
{ accountNumber: '1700', credit: 1548.50, description: 'USt-Zahllast' },
|
||||||
|
],
|
||||||
|
skrType: 'SKR04',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert VAT accounts are cleared
|
||||||
|
const ust19 = await api.getAccountBalance('1771');
|
||||||
|
const vorst19 = await api.getAccountBalance('1571');
|
||||||
|
const ustZahllast = await api.getAccountBalance('1700');
|
||||||
|
|
||||||
|
expect(Math.abs(ust19.balance)).toBeLessThan(0.01);
|
||||||
|
expect(Math.abs(vorst19.balance)).toBeLessThan(0.01);
|
||||||
|
// Account 1700 started with 28000 from opening balance, plus 1548.50 from VAT clearing
|
||||||
|
expect(Math.abs(ustZahllast.balance - 29548.50)).toBeLessThan(0.01);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should calculate income statement (GuV) before closing for SKR04', async () => {
|
||||||
|
const incomeStatement = await api.generateIncomeStatement({
|
||||||
|
dateFrom: new Date('2024-01-01'),
|
||||||
|
dateTo: new Date('2024-12-31'),
|
||||||
|
skrType: 'SKR04',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(incomeStatement).toBeDefined();
|
||||||
|
expect(incomeStatement.totalRevenue).toBeGreaterThan(0);
|
||||||
|
expect(incomeStatement.totalExpenses).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Assert the exact expected values based on actual bookings
|
||||||
|
// Revenue: 46000 (4300 account)
|
||||||
|
// Expenses: 5000 + 18000 + 3600 + 10000 + 2000 + 150 + 5000 + 5500 + 200 = 49450
|
||||||
|
// Less credit balances: -1000 (insurance accrual) = -1000
|
||||||
|
// Net expenses: 49450 - 1000 = 48450
|
||||||
|
// Net income: 46000 - 48450 = -2450 (loss)
|
||||||
|
|
||||||
|
expect(Math.round(incomeStatement.totalRevenue)).toEqual(46000);
|
||||||
|
expect(Math.round(incomeStatement.totalExpenses)).toEqual(48450);
|
||||||
|
expect(Math.round(incomeStatement.netIncome)).toEqual(-2450);
|
||||||
|
|
||||||
|
console.log('Income Statement Summary (SKR04):');
|
||||||
|
console.log('Revenue:', incomeStatement.totalRevenue);
|
||||||
|
console.log('Expenses:', incomeStatement.totalExpenses);
|
||||||
|
console.log('Net Income:', incomeStatement.netIncome);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should perform closing entries (Abschlussbuchungen) for SKR04', async () => {
|
||||||
|
// Close all income and expense accounts to the profit/loss account
|
||||||
|
// SKR04 uses 9500 for annual P&L account
|
||||||
|
|
||||||
|
// Close revenue accounts
|
||||||
|
await api.postJournalEntry({
|
||||||
|
date: new Date('2024-12-31'),
|
||||||
|
description: 'Abschluss Ertragskonten',
|
||||||
|
reference: 'AB-2024-001',
|
||||||
|
lines: [
|
||||||
|
{ accountNumber: '4300', debit: 46000, description: 'Erlöse abschließen' },
|
||||||
|
{ accountNumber: '9500', credit: 46000, description: 'GuV-Konto' },
|
||||||
|
],
|
||||||
|
skrType: 'SKR04',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close expense accounts
|
||||||
|
await api.postJournalEntry({
|
||||||
|
date: new Date('2024-12-31'),
|
||||||
|
description: 'Abschluss Aufwandskonten',
|
||||||
|
reference: 'AB-2024-002',
|
||||||
|
lines: [
|
||||||
|
{ accountNumber: '9500', debit: 48450, description: 'GuV-Konto' },
|
||||||
|
{ accountNumber: '3200', debit: 1000, description: 'Versicherung abschließen (credit balance)' },
|
||||||
|
{ accountNumber: '2100', credit: 5000, description: 'Bezogene Waren abschließen' },
|
||||||
|
{ accountNumber: '2300', credit: 18000, description: 'Löhne abschließen' },
|
||||||
|
{ accountNumber: '2400', credit: 3600, description: 'Gehälter abschließen' },
|
||||||
|
{ accountNumber: '3700', credit: 10000, description: 'AfA abschließen' },
|
||||||
|
{ accountNumber: '3000', credit: 2000, description: 'Miete abschließen' },
|
||||||
|
{ accountNumber: '3300', credit: 150, description: 'Kfz abschließen' },
|
||||||
|
{ accountNumber: '3400', credit: 5000, description: 'Werbung abschließen' },
|
||||||
|
{ accountNumber: '3500', credit: 5500, description: 'Beratung abschließen' },
|
||||||
|
{ accountNumber: '3100', credit: 200, description: 'Bürobedarf abschließen' },
|
||||||
|
],
|
||||||
|
skrType: 'SKR04',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Transfer profit/loss to equity
|
||||||
|
const guv_result = 46000 - 48450; // Loss of 2450
|
||||||
|
if (guv_result > 0) {
|
||||||
|
await api.postJournalEntry({
|
||||||
|
date: new Date('2024-12-31'),
|
||||||
|
description: 'Jahresgewinn auf Eigenkapital',
|
||||||
|
reference: 'AB-2024-003',
|
||||||
|
lines: [
|
||||||
|
{ accountNumber: '9500', debit: guv_result, description: 'GuV-Konto ausgleichen' },
|
||||||
|
{ accountNumber: '9300', credit: guv_result, description: 'Gewinnrücklagen' },
|
||||||
|
],
|
||||||
|
skrType: 'SKR04',
|
||||||
|
});
|
||||||
|
} else if (guv_result < 0) {
|
||||||
|
await api.postJournalEntry({
|
||||||
|
date: new Date('2024-12-31'),
|
||||||
|
description: 'Jahresverlust auf Eigenkapital',
|
||||||
|
reference: 'AB-2024-003',
|
||||||
|
lines: [
|
||||||
|
{ accountNumber: '9400', debit: Math.abs(guv_result), description: 'Verlustvortrag' },
|
||||||
|
{ accountNumber: '9500', credit: Math.abs(guv_result), description: 'GuV-Konto ausgleichen' },
|
||||||
|
],
|
||||||
|
skrType: 'SKR04',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert GuV account is closed and equity is updated
|
||||||
|
const guv = await api.getAccountBalance('9500');
|
||||||
|
const verlustvortrag = await api.getAccountBalance('9400');
|
||||||
|
|
||||||
|
expect(Math.abs(guv.balance)).toBeLessThan(0.01);
|
||||||
|
expect(Math.round(verlustvortrag.balance)).toEqual(-2450); // Loss of 2450 (debit balance is negative)
|
||||||
|
|
||||||
|
// Assert all P&L accounts are closed (zero balance)
|
||||||
|
const plAccounts = ['4300', '2100', '2300', '2400', '3400', '3500', '3100', '3700', '3000', '3200', '3300'];
|
||||||
|
for (const accNum of plAccounts) {
|
||||||
|
const balance = await api.getAccountBalance(accNum);
|
||||||
|
expect(Math.abs(balance.balance)).toBeLessThan(0.01);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should generate final balance sheet (Schlussbilanz) for SKR04', async () => {
|
||||||
|
const balanceSheet = await api.generateBalanceSheet({
|
||||||
|
dateTo: new Date('2024-12-31'),
|
||||||
|
skrType: 'SKR04',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(balanceSheet).toBeDefined();
|
||||||
|
expect(balanceSheet.assets).toBeDefined();
|
||||||
|
expect(balanceSheet.liabilities).toBeDefined();
|
||||||
|
expect(balanceSheet.equity).toBeDefined();
|
||||||
|
|
||||||
|
console.log('\n=== JAHRESABSCHLUSS 2024 (SKR04) ===\n');
|
||||||
|
console.log('BILANZ zum 31.12.2024\n');
|
||||||
|
|
||||||
|
// Verify balance sheet balances
|
||||||
|
const totalAssets = balanceSheet.assets.totalAssets;
|
||||||
|
const totalLiabilitiesAndEquity = balanceSheet.liabilities.totalLiabilities + balanceSheet.equity.totalEquity;
|
||||||
|
|
||||||
|
console.log('Balance Sheet Check (SKR04):');
|
||||||
|
console.log(' Total Assets:', totalAssets);
|
||||||
|
console.log(' Total Liabilities + Equity:', totalLiabilitiesAndEquity);
|
||||||
|
console.log(' Difference:', Math.abs(totalAssets - totalLiabilitiesAndEquity));
|
||||||
|
|
||||||
|
expect(Math.abs(totalAssets - totalLiabilitiesAndEquity)).toBeLessThan(0.01);
|
||||||
|
console.log('✓ Balance Sheet is balanced!');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should generate trial balance (Summen- und Saldenliste) for SKR04', async () => {
|
||||||
|
const trialBalance = await api.generateTrialBalance({
|
||||||
|
dateFrom: new Date('2024-01-01'),
|
||||||
|
dateTo: new Date('2024-12-31'),
|
||||||
|
skrType: 'SKR04',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(trialBalance).toBeDefined();
|
||||||
|
expect(trialBalance.isBalanced).toBeTrue();
|
||||||
|
|
||||||
|
console.log('\nSUMMEN- UND SALDENLISTE 2024 (SKR04)');
|
||||||
|
console.log('=====================================');
|
||||||
|
console.log('Konto | Bezeichnung | Soll | Haben | Saldo');
|
||||||
|
console.log('------|-------------|------|-------|-------');
|
||||||
|
|
||||||
|
// Display key accounts
|
||||||
|
const keyAccounts = [
|
||||||
|
'0200', '0210', '0400', '0500', // Fixed assets
|
||||||
|
'1000', '1200', '1400', '1900', // Current assets
|
||||||
|
'9000', '9400', '9300', // Equity
|
||||||
|
'1600', '1700', '0800', // Liabilities
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const accountNumber of keyAccounts) {
|
||||||
|
const account = await api.getAccount(accountNumber);
|
||||||
|
if (account) {
|
||||||
|
const balance = await api.getAccountBalance(accountNumber);
|
||||||
|
console.log(`${accountNumber} | ${account.accountName.substring(0, 30).padEnd(30)} | ${balance.debitTotal.toFixed(2).padStart(12)} | ${balance.creditTotal.toFixed(2).padStart(12)} | ${balance.balance.toFixed(2).padStart(12)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should close API connection', async () => {
|
||||||
|
await api.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
@@ -8,9 +8,11 @@ let testConfig: Awaited<ReturnType<typeof getTestConfig>>;
|
|||||||
tap.test('should initialize SKR03 API', async () => {
|
tap.test('should initialize SKR03 API', async () => {
|
||||||
testConfig = await getTestConfig();
|
testConfig = await getTestConfig();
|
||||||
|
|
||||||
|
// Use timestamp to ensure unique database for each test run
|
||||||
|
const timestamp = Date.now();
|
||||||
api = new skr.SkrApi({
|
api = new skr.SkrApi({
|
||||||
mongoDbUrl: testConfig.mongoDbUrl,
|
mongoDbUrl: testConfig.mongoDbUrl,
|
||||||
dbName: `${testConfig.mongoDbName}_skr03`,
|
dbName: `${testConfig.mongoDbName}_skr03_${timestamp}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await api.initialize('SKR03');
|
await api.initialize('SKR03');
|
||||||
|
@@ -8,9 +8,11 @@ let testConfig: Awaited<ReturnType<typeof getTestConfig>>;
|
|||||||
tap.test('should initialize API for transaction tests', async () => {
|
tap.test('should initialize API for transaction tests', async () => {
|
||||||
testConfig = await getTestConfig();
|
testConfig = await getTestConfig();
|
||||||
|
|
||||||
|
// Use timestamp to ensure unique database for each test run
|
||||||
|
const timestamp = Date.now();
|
||||||
api = new skr.SkrApi({
|
api = new skr.SkrApi({
|
||||||
mongoDbUrl: testConfig.mongoDbUrl,
|
mongoDbUrl: testConfig.mongoDbUrl,
|
||||||
dbName: testConfig.mongoDbName,
|
dbName: `${testConfig.mongoDbName}_transactions_${timestamp}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await api.initialize('SKR03');
|
await api.initialize('SKR03');
|
||||||
|
@@ -9,6 +9,14 @@ import type {
|
|||||||
IJournalEntryLine,
|
IJournalEntryLine,
|
||||||
IAccountBalance,
|
IAccountBalance,
|
||||||
} from './skr.types.js';
|
} from './skr.types.js';
|
||||||
|
import { SKR03_ACCOUNTS } from './skr03.data.js';
|
||||||
|
import { SKR04_ACCOUNTS } from './skr04.data.js';
|
||||||
|
|
||||||
|
// Module-level Maps for O(1) SKR standard lookups
|
||||||
|
const STANDARD_SKR_MAP = {
|
||||||
|
SKR03: new Map(SKR03_ACCOUNTS.map(a => [a.accountNumber, a])),
|
||||||
|
SKR04: new Map(SKR04_ACCOUNTS.map(a => [a.accountNumber, a])),
|
||||||
|
};
|
||||||
|
|
||||||
export class Ledger {
|
export class Ledger {
|
||||||
private logger: plugins.smartlog.Smartlog;
|
private logger: plugins.smartlog.Smartlog;
|
||||||
@@ -81,6 +89,12 @@ export class Ledger {
|
|||||||
const accountNumbers = journalData.lines.map((line) => line.accountNumber);
|
const accountNumbers = journalData.lines.map((line) => line.accountNumber);
|
||||||
await this.validateAccounts(accountNumbers);
|
await this.validateAccounts(accountNumbers);
|
||||||
|
|
||||||
|
// Validate against SKR standard (warnings only by default)
|
||||||
|
await this.validateAccountsAgainstSKR(journalData.lines, {
|
||||||
|
strict: false, // Start with warnings only
|
||||||
|
warnOnNameMismatch: false // Names vary, don't spam logs
|
||||||
|
});
|
||||||
|
|
||||||
// Validate journal entry is balanced
|
// Validate journal entry is balanced
|
||||||
this.validateJournalBalance(journalData.lines);
|
this.validateJournalBalance(journalData.lines);
|
||||||
|
|
||||||
@@ -139,6 +153,77 @@ export class Ledger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate accounts against SKR standard data
|
||||||
|
*/
|
||||||
|
private async validateAccountsAgainstSKR(
|
||||||
|
lines: IJournalEntryLine[],
|
||||||
|
options?: { strict?: boolean; warnOnNameMismatch?: boolean }
|
||||||
|
): Promise<void> {
|
||||||
|
const { strict = false, warnOnNameMismatch = false } = options || {};
|
||||||
|
const skrMap = STANDARD_SKR_MAP[this.skrType];
|
||||||
|
|
||||||
|
if (!skrMap) {
|
||||||
|
this.logger.log('warn', `No SKR standard map available for ${this.skrType}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniqueAccountNumbers = [...new Set(lines.map(line => line.accountNumber))];
|
||||||
|
|
||||||
|
for (const accountNumber of uniqueAccountNumbers) {
|
||||||
|
const standardAccount = skrMap.get(accountNumber);
|
||||||
|
|
||||||
|
if (!standardAccount) {
|
||||||
|
// Special case: SKR04 class 8 is designated for custom accounts ("frei")
|
||||||
|
if (this.skrType === 'SKR04' && accountNumber.startsWith('8')) {
|
||||||
|
this.logger.log('debug', `Account ${accountNumber} is in SKR04 class 8 (custom accounts allowed)`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = `Account ${accountNumber} is not a standard ${this.skrType} account`;
|
||||||
|
if (strict) {
|
||||||
|
throw new Error(message);
|
||||||
|
} else {
|
||||||
|
this.logger.log('warn', message);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get actual account from database to compare
|
||||||
|
const dbAccount = await Account.getAccountByNumber(accountNumber, this.skrType);
|
||||||
|
if (!dbAccount) {
|
||||||
|
// Account doesn't exist in DB, will be caught by validateAccounts()
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate type and class match SKR standard
|
||||||
|
if (dbAccount.accountType !== standardAccount.accountType) {
|
||||||
|
const message = `Account ${accountNumber} type mismatch: expected '${standardAccount.accountType}', got '${dbAccount.accountType}'`;
|
||||||
|
if (strict) {
|
||||||
|
throw new Error(message);
|
||||||
|
} else {
|
||||||
|
this.logger.log('warn', message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dbAccount.accountClass !== standardAccount.accountClass) {
|
||||||
|
const message = `Account ${accountNumber} class mismatch: expected ${standardAccount.accountClass}, got ${dbAccount.accountClass}`;
|
||||||
|
if (strict) {
|
||||||
|
throw new Error(message);
|
||||||
|
} else {
|
||||||
|
this.logger.log('warn', message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn on name mismatch (common and acceptable in practice)
|
||||||
|
if (warnOnNameMismatch && dbAccount.accountName !== standardAccount.accountName) {
|
||||||
|
this.logger.log('info',
|
||||||
|
`Account ${accountNumber} name differs from SKR standard: '${dbAccount.accountName}' vs '${standardAccount.accountName}'`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reverse a transaction
|
* Reverse a transaction
|
||||||
*/
|
*/
|
||||||
|
Reference in New Issue
Block a user