@fin.cx/skr 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/index.d.ts +6 -0
- package/dist_ts/index.js +7 -1
- package/dist_ts/plugins.d.ts +8 -1
- package/dist_ts/plugins.js +10 -2
- package/dist_ts/skr.api.d.ts +70 -0
- package/dist_ts/skr.api.js +354 -3
- package/dist_ts/skr.classes.journalentry.d.ts +1 -0
- package/dist_ts/skr.classes.journalentry.js +55 -15
- package/dist_ts/skr.classes.ledger.d.ts +4 -0
- package/dist_ts/skr.classes.ledger.js +72 -1
- package/dist_ts/skr.classes.reports.js +56 -22
- package/dist_ts/skr.export.accounts.d.ts +53 -0
- package/dist_ts/skr.export.accounts.js +111 -0
- package/dist_ts/skr.export.balances.d.ts +59 -0
- package/dist_ts/skr.export.balances.js +205 -0
- package/dist_ts/skr.export.d.ts +110 -0
- package/dist_ts/skr.export.js +315 -0
- package/dist_ts/skr.export.ledger.d.ts +95 -0
- package/dist_ts/skr.export.ledger.js +164 -0
- package/dist_ts/skr.export.pdf.d.ts +82 -0
- package/dist_ts/skr.export.pdf.js +548 -0
- package/dist_ts/skr.invoice.adapter.d.ts +98 -0
- package/dist_ts/skr.invoice.adapter.js +476 -0
- package/dist_ts/skr.invoice.booking.d.ts +102 -0
- package/dist_ts/skr.invoice.booking.js +556 -0
- package/dist_ts/skr.invoice.entity.d.ts +287 -0
- package/dist_ts/skr.invoice.entity.js +2 -0
- package/dist_ts/skr.invoice.mapper.d.ts +69 -0
- package/dist_ts/skr.invoice.mapper.js +401 -0
- package/dist_ts/skr.invoice.storage.d.ts +140 -0
- package/dist_ts/skr.invoice.storage.js +529 -0
- package/dist_ts/skr.security.d.ts +65 -0
- package/dist_ts/skr.security.js +319 -0
- package/dist_ts/skr.types.d.ts +1 -0
- package/package.json +18 -12
- package/readme.md +461 -132
- package/ts/index.ts +6 -0
- package/ts/plugins.ts +22 -1
- package/ts/skr.api.ts +489 -2
- package/ts/skr.classes.journalentry.ts +63 -14
- package/ts/skr.classes.ledger.ts +85 -0
- package/ts/skr.classes.reports.ts +64 -21
- package/ts/skr.export.accounts.ts +154 -0
- package/ts/skr.export.balances.ts +270 -0
- package/ts/skr.export.ledger.ts +249 -0
- package/ts/skr.export.pdf.ts +601 -0
- package/ts/skr.export.ts +443 -0
- package/ts/skr.invoice.adapter.ts +581 -0
- package/ts/skr.invoice.booking.ts +738 -0
- package/ts/skr.invoice.entity.ts +351 -0
- package/ts/skr.invoice.mapper.ts +486 -0
- package/ts/skr.invoice.storage.ts +710 -0
- package/ts/skr.security.ts +405 -0
- package/ts/skr.types.ts +1 -0
package/ts/index.ts
CHANGED
|
@@ -8,3 +8,9 @@ export * from './skr.classes.reports.js';
|
|
|
8
8
|
export * from './skr.api.js';
|
|
9
9
|
export * from './skr03.data.js';
|
|
10
10
|
export * from './skr04.data.js';
|
|
11
|
+
export * from './skr.export.js';
|
|
12
|
+
export * from './skr.export.ledger.js';
|
|
13
|
+
export * from './skr.export.accounts.js';
|
|
14
|
+
export * from './skr.export.balances.js';
|
|
15
|
+
export * from './skr.export.pdf.js';
|
|
16
|
+
export * from './skr.security.js';
|
package/ts/plugins.ts
CHANGED
|
@@ -3,5 +3,26 @@ import * as smartdata from '@push.rocks/smartdata';
|
|
|
3
3
|
import * as smartunique from '@push.rocks/smartunique';
|
|
4
4
|
import * as smarttime from '@push.rocks/smarttime';
|
|
5
5
|
import * as smartlog from '@push.rocks/smartlog';
|
|
6
|
+
import * as smartfile from '@push.rocks/smartfile';
|
|
7
|
+
import * as smarthash from '@push.rocks/smarthash';
|
|
8
|
+
import * as smartpath from '@push.rocks/smartpath';
|
|
9
|
+
import * as smartpdf from '@push.rocks/smartpdf';
|
|
6
10
|
|
|
7
|
-
|
|
11
|
+
// third party
|
|
12
|
+
import * as nodeForge from 'node-forge';
|
|
13
|
+
import { MerkleTree } from 'merkletreejs';
|
|
14
|
+
import * as einvoice from '@fin.cx/einvoice';
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
smartdata,
|
|
18
|
+
smartunique,
|
|
19
|
+
smarttime,
|
|
20
|
+
smartlog,
|
|
21
|
+
smartfile,
|
|
22
|
+
smarthash,
|
|
23
|
+
smartpath,
|
|
24
|
+
smartpdf,
|
|
25
|
+
nodeForge,
|
|
26
|
+
MerkleTree,
|
|
27
|
+
einvoice
|
|
28
|
+
};
|
package/ts/skr.api.ts
CHANGED
|
@@ -1,10 +1,28 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
|
+
import * as path from 'path';
|
|
2
3
|
import { ChartOfAccounts } from './skr.classes.chartofaccounts.js';
|
|
3
4
|
import { Ledger } from './skr.classes.ledger.js';
|
|
4
5
|
import { Reports } from './skr.classes.reports.js';
|
|
5
6
|
import { Account } from './skr.classes.account.js';
|
|
6
7
|
import { Transaction } from './skr.classes.transaction.js';
|
|
7
8
|
import { JournalEntry } from './skr.classes.journalentry.js';
|
|
9
|
+
import { SkrExport, type IExportOptions } from './skr.export.js';
|
|
10
|
+
import { LedgerExporter } from './skr.export.ledger.js';
|
|
11
|
+
import { AccountsExporter } from './skr.export.accounts.js';
|
|
12
|
+
import { BalancesExporter } from './skr.export.balances.js';
|
|
13
|
+
import { PdfReportGenerator, type IPdfReportOptions } from './skr.export.pdf.js';
|
|
14
|
+
import { SecurityManager, type ISigningOptions } from './skr.security.js';
|
|
15
|
+
import { InvoiceAdapter } from './skr.invoice.adapter.js';
|
|
16
|
+
import { InvoiceStorage } from './skr.invoice.storage.js';
|
|
17
|
+
import { InvoiceBookingEngine, type IBookingOptions, type IBookingResult } from './skr.invoice.booking.js';
|
|
18
|
+
import type {
|
|
19
|
+
IInvoice,
|
|
20
|
+
IInvoiceFilter,
|
|
21
|
+
IInvoiceImportOptions,
|
|
22
|
+
IInvoiceExportOptions,
|
|
23
|
+
IBookingRules,
|
|
24
|
+
TInvoiceDirection,
|
|
25
|
+
} from './skr.invoice.entity.js';
|
|
8
26
|
import type {
|
|
9
27
|
IDatabaseConfig,
|
|
10
28
|
TSKRType,
|
|
@@ -17,6 +35,7 @@ import type {
|
|
|
17
35
|
ITrialBalanceReport,
|
|
18
36
|
IIncomeStatement,
|
|
19
37
|
IBalanceSheet,
|
|
38
|
+
IAccountBalance,
|
|
20
39
|
} from './skr.types.js';
|
|
21
40
|
|
|
22
41
|
/**
|
|
@@ -29,6 +48,9 @@ export class SkrApi {
|
|
|
29
48
|
private logger: plugins.smartlog.Smartlog;
|
|
30
49
|
private initialized: boolean = false;
|
|
31
50
|
private currentSKRType: TSKRType | null = null;
|
|
51
|
+
private invoiceAdapter: InvoiceAdapter | null = null;
|
|
52
|
+
private invoiceStorage: InvoiceStorage | null = null;
|
|
53
|
+
private invoiceBookingEngine: InvoiceBookingEngine | null = null;
|
|
32
54
|
|
|
33
55
|
constructor(private config: IDatabaseConfig) {
|
|
34
56
|
this.chartOfAccounts = new ChartOfAccounts(config);
|
|
@@ -62,6 +84,13 @@ export class SkrApi {
|
|
|
62
84
|
this.currentSKRType = skrType;
|
|
63
85
|
this.ledger = new Ledger(skrType);
|
|
64
86
|
this.reports = new Reports(skrType);
|
|
87
|
+
|
|
88
|
+
// Initialize invoice components
|
|
89
|
+
this.invoiceAdapter = new InvoiceAdapter();
|
|
90
|
+
const invoicePath = this.config.invoiceExportPath || path.resolve(process.cwd(), 'exports', 'invoices');
|
|
91
|
+
this.invoiceStorage = new InvoiceStorage(invoicePath);
|
|
92
|
+
this.invoiceBookingEngine = new InvoiceBookingEngine(skrType);
|
|
93
|
+
|
|
65
94
|
this.initialized = true;
|
|
66
95
|
|
|
67
96
|
this.logger.log('info', 'SKR API initialized successfully');
|
|
@@ -158,7 +187,8 @@ export class SkrApi {
|
|
|
158
187
|
transactionData: ITransactionData,
|
|
159
188
|
): Promise<Transaction> {
|
|
160
189
|
this.ensureInitialized();
|
|
161
|
-
|
|
190
|
+
if (!this.ledger) throw new Error('Ledger not initialized');
|
|
191
|
+
return await this.ledger.postTransaction(transactionData);
|
|
162
192
|
}
|
|
163
193
|
|
|
164
194
|
/**
|
|
@@ -168,7 +198,8 @@ export class SkrApi {
|
|
|
168
198
|
journalData: IJournalEntry,
|
|
169
199
|
): Promise<JournalEntry> {
|
|
170
200
|
this.ensureInitialized();
|
|
171
|
-
|
|
201
|
+
if (!this.ledger) throw new Error('Ledger not initialized');
|
|
202
|
+
return await this.ledger.postJournalEntry(journalData);
|
|
172
203
|
}
|
|
173
204
|
|
|
174
205
|
/**
|
|
@@ -348,6 +379,262 @@ export class SkrApi {
|
|
|
348
379
|
return await this.chartOfAccounts.exportAccountsToCSV();
|
|
349
380
|
}
|
|
350
381
|
|
|
382
|
+
/**
|
|
383
|
+
* Export Jahresabschluss in GoBD-compliant BagIt format
|
|
384
|
+
* Creates a revision-safe export for 10-year archival
|
|
385
|
+
*/
|
|
386
|
+
public async exportJahresabschluss(options: IExportOptions): Promise<string> {
|
|
387
|
+
this.ensureInitialized();
|
|
388
|
+
if (!this.ledger || !this.reports || !this.currentSKRType) {
|
|
389
|
+
throw new Error('API not fully initialized');
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
this.logger.log('info', `Starting Jahresabschluss export for fiscal year ${options.fiscalYear}`);
|
|
393
|
+
|
|
394
|
+
// Create export instance
|
|
395
|
+
const exporter = new SkrExport(options);
|
|
396
|
+
|
|
397
|
+
// Create BagIt structure
|
|
398
|
+
await exporter.createBagItStructure();
|
|
399
|
+
await exporter.createExportMetadata(this.currentSKRType);
|
|
400
|
+
await exporter.createSchemas();
|
|
401
|
+
|
|
402
|
+
// Export accounting data
|
|
403
|
+
await this.exportLedgerData(exporter, options);
|
|
404
|
+
await this.exportAccountData(exporter, options);
|
|
405
|
+
await this.exportBalanceData(exporter, options);
|
|
406
|
+
|
|
407
|
+
// Generate PDF reports if requested
|
|
408
|
+
if (options.generatePdfReports) {
|
|
409
|
+
await this.generatePdfReports(exporter, options);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Sign export if requested
|
|
413
|
+
if (options.signExport) {
|
|
414
|
+
await this.signExport(exporter, options);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Create manifests and validate
|
|
418
|
+
await exporter.writeManifests();
|
|
419
|
+
const merkleRoot = await exporter.createMerkleTree();
|
|
420
|
+
|
|
421
|
+
const isValid = await exporter.validateBagIt();
|
|
422
|
+
if (!isValid) {
|
|
423
|
+
throw new Error('BagIt validation failed');
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
this.logger.log('ok', `Jahresabschluss export completed. Merkle root: ${merkleRoot}`);
|
|
427
|
+
|
|
428
|
+
return options.exportPath;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Export ledger data in NDJSON format
|
|
433
|
+
*/
|
|
434
|
+
private async exportLedgerData(exporter: SkrExport, options: IExportOptions): Promise<void> {
|
|
435
|
+
if (!this.ledger) throw new Error('Ledger not initialized');
|
|
436
|
+
|
|
437
|
+
const ledgerExporter = new LedgerExporter(options.exportPath);
|
|
438
|
+
await ledgerExporter.initialize();
|
|
439
|
+
|
|
440
|
+
// Get all transactions for the period
|
|
441
|
+
const transactions = await this.chartOfAccounts.getTransactions({
|
|
442
|
+
dateFrom: options.dateFrom,
|
|
443
|
+
dateTo: options.dateTo
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// Export each transaction
|
|
447
|
+
for (const transaction of transactions) {
|
|
448
|
+
const transactionData = transaction;
|
|
449
|
+
await ledgerExporter.exportTransaction(transactionData as any);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Get all journal entries for the period
|
|
453
|
+
// Use MongoDB query syntax for date range
|
|
454
|
+
const journalEntries = await JournalEntry.getInstances({
|
|
455
|
+
date: {
|
|
456
|
+
$gte: options.dateFrom,
|
|
457
|
+
$lte: options.dateTo
|
|
458
|
+
} as any, // SmartData supports MongoDB query operators
|
|
459
|
+
skrType: this.currentSKRType
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
// Export each journal entry
|
|
463
|
+
for (const entry of journalEntries) {
|
|
464
|
+
const entryData = entry;
|
|
465
|
+
await ledgerExporter.exportJournalEntry(entryData as any);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const entryCount = await ledgerExporter.close();
|
|
469
|
+
this.logger.log('info', `Exported ${entryCount} ledger entries`);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Export account data in CSV format
|
|
474
|
+
*/
|
|
475
|
+
private async exportAccountData(exporter: SkrExport, options: IExportOptions): Promise<void> {
|
|
476
|
+
const accountsExporter = new AccountsExporter(options.exportPath);
|
|
477
|
+
|
|
478
|
+
// Get all accounts
|
|
479
|
+
const accounts = await this.chartOfAccounts.getAllAccounts();
|
|
480
|
+
|
|
481
|
+
// Add each account to export
|
|
482
|
+
for (const account of accounts) {
|
|
483
|
+
const accountData = account;
|
|
484
|
+
accountsExporter.addAccount(accountData as any);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Export to CSV and JSON
|
|
488
|
+
await accountsExporter.exportToCSV();
|
|
489
|
+
await accountsExporter.exportToJSON();
|
|
490
|
+
|
|
491
|
+
this.logger.log('info', `Exported ${accountsExporter.getAccountCount()} accounts`);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Export balance data in CSV format
|
|
496
|
+
*/
|
|
497
|
+
private async exportBalanceData(exporter: SkrExport, options: IExportOptions): Promise<void> {
|
|
498
|
+
if (!this.ledger) throw new Error('Ledger not initialized');
|
|
499
|
+
|
|
500
|
+
const balancesExporter = new BalancesExporter(
|
|
501
|
+
options.exportPath,
|
|
502
|
+
options.fiscalYear
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
// Get all accounts with balances
|
|
506
|
+
const accounts = await this.chartOfAccounts.getAllAccounts();
|
|
507
|
+
|
|
508
|
+
for (const account of accounts) {
|
|
509
|
+
const balance = await this.ledger.getAccountBalance(
|
|
510
|
+
account.accountNumber,
|
|
511
|
+
options.dateTo
|
|
512
|
+
);
|
|
513
|
+
|
|
514
|
+
if (balance) {
|
|
515
|
+
balancesExporter.addBalance(
|
|
516
|
+
account.accountNumber,
|
|
517
|
+
account.accountName,
|
|
518
|
+
balance as IAccountBalance,
|
|
519
|
+
`${options.fiscalYear}`
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Export balance reports
|
|
525
|
+
await balancesExporter.exportToCSV();
|
|
526
|
+
await balancesExporter.exportTrialBalance();
|
|
527
|
+
await balancesExporter.exportClassSummary();
|
|
528
|
+
|
|
529
|
+
this.logger.log('info', `Exported ${balancesExporter.getBalanceCount()} account balances`);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Generate PDF reports for the export
|
|
534
|
+
*/
|
|
535
|
+
private async generatePdfReports(exporter: SkrExport, options: IExportOptions): Promise<void> {
|
|
536
|
+
if (!this.reports) throw new Error('Reports not initialized');
|
|
537
|
+
|
|
538
|
+
const pdfOptions: IPdfReportOptions = {
|
|
539
|
+
companyName: options.companyInfo?.name || 'Unternehmen',
|
|
540
|
+
companyAddress: options.companyInfo?.address,
|
|
541
|
+
taxId: options.companyInfo?.taxId,
|
|
542
|
+
registrationNumber: options.companyInfo?.registrationNumber,
|
|
543
|
+
fiscalYear: options.fiscalYear,
|
|
544
|
+
dateFrom: options.dateFrom,
|
|
545
|
+
dateTo: options.dateTo,
|
|
546
|
+
preparedDate: new Date()
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
const pdfGenerator = new PdfReportGenerator(options.exportPath, pdfOptions);
|
|
550
|
+
await pdfGenerator.initialize();
|
|
551
|
+
|
|
552
|
+
try {
|
|
553
|
+
// Generate reports
|
|
554
|
+
const trialBalance = await this.reports.getTrialBalance({
|
|
555
|
+
dateFrom: options.dateFrom,
|
|
556
|
+
dateTo: options.dateTo,
|
|
557
|
+
skrType: this.currentSKRType
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
const incomeStatement = await this.reports.getIncomeStatement({
|
|
561
|
+
dateFrom: options.dateFrom,
|
|
562
|
+
dateTo: options.dateTo,
|
|
563
|
+
skrType: this.currentSKRType
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
const balanceSheet = await this.reports.getBalanceSheet({
|
|
567
|
+
dateFrom: options.dateFrom,
|
|
568
|
+
dateTo: options.dateTo,
|
|
569
|
+
skrType: this.currentSKRType
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
// Generate PDFs
|
|
573
|
+
const jahresabschlussPdf = await pdfGenerator.generateJahresabschlussPdf(
|
|
574
|
+
trialBalance,
|
|
575
|
+
incomeStatement,
|
|
576
|
+
balanceSheet
|
|
577
|
+
);
|
|
578
|
+
|
|
579
|
+
// Save PDFs
|
|
580
|
+
await pdfGenerator.savePdfReport('jahresabschluss.pdf', jahresabschlussPdf);
|
|
581
|
+
|
|
582
|
+
// Store in BagIt structure
|
|
583
|
+
await exporter.storeDocument(jahresabschlussPdf, 'jahresabschluss.pdf');
|
|
584
|
+
|
|
585
|
+
this.logger.log('info', 'PDF reports generated successfully');
|
|
586
|
+
} finally {
|
|
587
|
+
await pdfGenerator.close();
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Sign the export with CAdES signature
|
|
593
|
+
*/
|
|
594
|
+
private async signExport(exporter: SkrExport, options: IExportOptions): Promise<void> {
|
|
595
|
+
const signingOptions: ISigningOptions = {
|
|
596
|
+
certificatePem: options.signExport ? undefined : undefined, // Use provided cert or generate
|
|
597
|
+
privateKeyPem: options.signExport ? undefined : undefined,
|
|
598
|
+
includeTimestamp: options.timestampExport !== false
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
const security = new SecurityManager(signingOptions);
|
|
602
|
+
|
|
603
|
+
// Generate self-signed certificate if none provided
|
|
604
|
+
let cert: string, key: string;
|
|
605
|
+
if (!signingOptions.certificatePem) {
|
|
606
|
+
const generated = await security.generateSelfSignedCertificate(
|
|
607
|
+
options.companyInfo?.name || 'SKR Export System'
|
|
608
|
+
);
|
|
609
|
+
cert = generated.certificate;
|
|
610
|
+
key = generated.privateKey;
|
|
611
|
+
} else {
|
|
612
|
+
cert = signingOptions.certificatePem;
|
|
613
|
+
key = signingOptions.privateKeyPem!;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Sign the manifest
|
|
617
|
+
const manifestPath = path.resolve(
|
|
618
|
+
options.exportPath,
|
|
619
|
+
`jahresabschluss_${options.fiscalYear}`,
|
|
620
|
+
'manifest-sha256.txt'
|
|
621
|
+
);
|
|
622
|
+
|
|
623
|
+
await security.createDetachedSignature(
|
|
624
|
+
manifestPath,
|
|
625
|
+
path.resolve(
|
|
626
|
+
options.exportPath,
|
|
627
|
+
`jahresabschluss_${options.fiscalYear}`,
|
|
628
|
+
'data',
|
|
629
|
+
'metadata',
|
|
630
|
+
'signatures',
|
|
631
|
+
'manifest.cades'
|
|
632
|
+
)
|
|
633
|
+
);
|
|
634
|
+
|
|
635
|
+
this.logger.log('info', 'Export signed with CAdES signature');
|
|
636
|
+
}
|
|
637
|
+
|
|
351
638
|
// ========== Utility Methods ==========
|
|
352
639
|
|
|
353
640
|
/**
|
|
@@ -530,4 +817,204 @@ export class SkrApi {
|
|
|
530
817
|
totalPages,
|
|
531
818
|
};
|
|
532
819
|
}
|
|
820
|
+
|
|
821
|
+
// ========== Invoice Management ==========
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* Import an invoice from file or buffer
|
|
825
|
+
* Parses, validates, and optionally books the invoice
|
|
826
|
+
*/
|
|
827
|
+
public async importInvoice(
|
|
828
|
+
file: Buffer | string,
|
|
829
|
+
direction: TInvoiceDirection,
|
|
830
|
+
options?: IInvoiceImportOptions
|
|
831
|
+
): Promise<IInvoice> {
|
|
832
|
+
this.ensureInitialized();
|
|
833
|
+
if (!this.invoiceAdapter || !this.invoiceStorage || !this.invoiceBookingEngine) {
|
|
834
|
+
throw new Error('Invoice components not initialized');
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
this.logger.log('info', `Importing ${direction} invoice`);
|
|
838
|
+
|
|
839
|
+
// Parse and validate invoice
|
|
840
|
+
const invoice = await this.invoiceAdapter.parseInvoice(file, direction);
|
|
841
|
+
|
|
842
|
+
// Store invoice
|
|
843
|
+
await this.invoiceStorage.initialize();
|
|
844
|
+
const contentHash = await this.invoiceStorage.storeInvoice(invoice);
|
|
845
|
+
invoice.contentHash = contentHash;
|
|
846
|
+
|
|
847
|
+
// Auto-book if requested
|
|
848
|
+
if (options?.autoBook) {
|
|
849
|
+
const bookingResult = await this.bookInvoice(
|
|
850
|
+
invoice,
|
|
851
|
+
options.bookingRules,
|
|
852
|
+
{
|
|
853
|
+
autoBook: true,
|
|
854
|
+
confidenceThreshold: options.confidenceThreshold || 80,
|
|
855
|
+
skipValidation: options.validateOnly
|
|
856
|
+
}
|
|
857
|
+
);
|
|
858
|
+
|
|
859
|
+
if (bookingResult.success && bookingResult.bookingInfo) {
|
|
860
|
+
invoice.bookingInfo = bookingResult.bookingInfo;
|
|
861
|
+
invoice.status = 'posted';
|
|
862
|
+
|
|
863
|
+
// Update stored metadata with booking information
|
|
864
|
+
await this.invoiceStorage.updateMetadata(invoice.contentHash, {
|
|
865
|
+
journalEntryId: bookingResult.bookingInfo.journalEntryId,
|
|
866
|
+
transactionIds: bookingResult.bookingInfo.transactionIds
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
this.logger.log('info', `Invoice imported successfully: ${invoice.invoiceNumber}`);
|
|
872
|
+
return invoice;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* Book an invoice to the ledger
|
|
877
|
+
*/
|
|
878
|
+
public async bookInvoice(
|
|
879
|
+
invoice: IInvoice,
|
|
880
|
+
bookingRules?: Partial<IBookingRules>,
|
|
881
|
+
options?: IBookingOptions
|
|
882
|
+
): Promise<IBookingResult> {
|
|
883
|
+
this.ensureInitialized();
|
|
884
|
+
if (!this.invoiceBookingEngine) {
|
|
885
|
+
throw new Error('Invoice booking engine not initialized');
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
this.logger.log('info', `Booking invoice ${invoice.invoiceNumber}`);
|
|
889
|
+
|
|
890
|
+
const result = await this.invoiceBookingEngine.bookInvoice(
|
|
891
|
+
invoice,
|
|
892
|
+
bookingRules,
|
|
893
|
+
options
|
|
894
|
+
);
|
|
895
|
+
|
|
896
|
+
if (result.success) {
|
|
897
|
+
this.logger.log('info', `Invoice booked successfully with confidence ${result.confidence}%`);
|
|
898
|
+
|
|
899
|
+
// Update stored metadata if invoice has a content hash
|
|
900
|
+
if (invoice.contentHash && result.bookingInfo && this.invoiceStorage) {
|
|
901
|
+
await this.invoiceStorage.updateMetadata(invoice.contentHash, {
|
|
902
|
+
journalEntryId: result.bookingInfo.journalEntryId,
|
|
903
|
+
transactionIds: result.bookingInfo.transactionIds
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
} else {
|
|
907
|
+
this.logger.log('error', `Invoice booking failed: ${result.errors?.join(', ')}`);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
return result;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* Export an invoice in a different format
|
|
915
|
+
*/
|
|
916
|
+
public async exportInvoice(
|
|
917
|
+
invoice: IInvoice,
|
|
918
|
+
options: IInvoiceExportOptions
|
|
919
|
+
): Promise<{ xml: string; pdf?: Buffer }> {
|
|
920
|
+
this.ensureInitialized();
|
|
921
|
+
if (!this.invoiceAdapter) {
|
|
922
|
+
throw new Error('Invoice adapter not initialized');
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
this.logger.log('info', `Exporting invoice ${invoice.invoiceNumber} to ${options.format}`);
|
|
926
|
+
|
|
927
|
+
// Convert format if needed
|
|
928
|
+
const xml = await this.invoiceAdapter.convertFormat(invoice, options.format);
|
|
929
|
+
|
|
930
|
+
// Generate PDF if requested
|
|
931
|
+
let pdf: Buffer | undefined;
|
|
932
|
+
if (options.embedInPdf) {
|
|
933
|
+
const result = await this.invoiceAdapter.generateInvoice(invoice, options.format);
|
|
934
|
+
pdf = result.pdf;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
return { xml, pdf };
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* Search invoices by filter
|
|
942
|
+
*/
|
|
943
|
+
public async searchInvoices(filter: IInvoiceFilter): Promise<IInvoice[]> {
|
|
944
|
+
this.ensureInitialized();
|
|
945
|
+
if (!this.invoiceStorage) {
|
|
946
|
+
throw new Error('Invoice storage not initialized');
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
await this.invoiceStorage.initialize();
|
|
950
|
+
const metadata = await this.invoiceStorage.searchInvoices(filter);
|
|
951
|
+
|
|
952
|
+
const invoices: IInvoice[] = [];
|
|
953
|
+
for (const meta of metadata) {
|
|
954
|
+
const invoice = await this.invoiceStorage.retrieveInvoice(meta.contentHash);
|
|
955
|
+
if (invoice) {
|
|
956
|
+
invoices.push(invoice);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
return invoices;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/**
|
|
964
|
+
* Get invoice by content hash
|
|
965
|
+
*/
|
|
966
|
+
public async getInvoice(contentHash: string): Promise<IInvoice | null> {
|
|
967
|
+
this.ensureInitialized();
|
|
968
|
+
if (!this.invoiceStorage) {
|
|
969
|
+
throw new Error('Invoice storage not initialized');
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
await this.invoiceStorage.initialize();
|
|
973
|
+
return await this.invoiceStorage.retrieveInvoice(contentHash);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/**
|
|
977
|
+
* Get invoice storage statistics
|
|
978
|
+
*/
|
|
979
|
+
public async getInvoiceStatistics(): Promise<any> {
|
|
980
|
+
this.ensureInitialized();
|
|
981
|
+
if (!this.invoiceStorage) {
|
|
982
|
+
throw new Error('Invoice storage not initialized');
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
await this.invoiceStorage.initialize();
|
|
986
|
+
return await this.invoiceStorage.getStatistics();
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
/**
|
|
990
|
+
* Create EN16931 compliance report for invoices
|
|
991
|
+
*/
|
|
992
|
+
public async createInvoiceComplianceReport(): Promise<void> {
|
|
993
|
+
this.ensureInitialized();
|
|
994
|
+
if (!this.invoiceStorage) {
|
|
995
|
+
throw new Error('Invoice storage not initialized');
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
await this.invoiceStorage.initialize();
|
|
999
|
+
await this.invoiceStorage.createComplianceReport();
|
|
1000
|
+
|
|
1001
|
+
this.logger.log('info', 'Invoice compliance report created');
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* Generate an invoice from internal data
|
|
1006
|
+
*/
|
|
1007
|
+
public async generateInvoice(
|
|
1008
|
+
invoiceData: Partial<IInvoice>,
|
|
1009
|
+
format: IInvoiceExportOptions['format']
|
|
1010
|
+
): Promise<{ xml: string; pdf?: Buffer }> {
|
|
1011
|
+
this.ensureInitialized();
|
|
1012
|
+
if (!this.invoiceAdapter) {
|
|
1013
|
+
throw new Error('Invoice adapter not initialized');
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
this.logger.log('info', `Generating invoice in ${format} format`);
|
|
1017
|
+
|
|
1018
|
+
return await this.invoiceAdapter.generateInvoice(invoiceData, format);
|
|
1019
|
+
}
|
|
533
1020
|
}
|
|
@@ -96,6 +96,8 @@ export class JournalEntry extends SmartDataDbDoc<JournalEntry, JournalEntry> {
|
|
|
96
96
|
this.postedAt = null;
|
|
97
97
|
this.createdBy = 'system';
|
|
98
98
|
|
|
99
|
+
// Normalize any negative amounts to the correct side
|
|
100
|
+
this.sanitizeLines();
|
|
99
101
|
// Calculate totals
|
|
100
102
|
this.calculateTotals();
|
|
101
103
|
}
|
|
@@ -107,6 +109,36 @@ export class JournalEntry extends SmartDataDbDoc<JournalEntry, JournalEntry> {
|
|
|
107
109
|
return `JE-${timestamp}-${random}`;
|
|
108
110
|
}
|
|
109
111
|
|
|
112
|
+
private sanitizeLines(): void {
|
|
113
|
+
for (const line of this.lines) {
|
|
114
|
+
// Check if both debit and credit are set (not allowed)
|
|
115
|
+
if (line.debit !== undefined && line.debit !== 0 &&
|
|
116
|
+
line.credit !== undefined && line.credit !== 0) {
|
|
117
|
+
throw new Error('A line cannot have both debit and credit amounts');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Handle negative debit - convert to positive credit
|
|
121
|
+
if (line.debit !== undefined && line.debit < 0) {
|
|
122
|
+
line.credit = Math.abs(line.debit);
|
|
123
|
+
delete (line as any).debit;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Handle negative credit - convert to positive debit
|
|
127
|
+
if (line.credit !== undefined && line.credit < 0) {
|
|
128
|
+
line.debit = Math.abs(line.credit);
|
|
129
|
+
delete (line as any).credit;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Check that at least one side has a positive value
|
|
133
|
+
const hasDebit = line.debit !== undefined && line.debit > 0;
|
|
134
|
+
const hasCredit = line.credit !== undefined && line.credit > 0;
|
|
135
|
+
|
|
136
|
+
if (!hasDebit && !hasCredit) {
|
|
137
|
+
throw new Error('Either debit or credit must be a positive number');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
110
142
|
private calculateTotals(): void {
|
|
111
143
|
this.totalDebits = 0;
|
|
112
144
|
this.totalCredits = 0;
|
|
@@ -204,6 +236,8 @@ export class JournalEntry extends SmartDataDbDoc<JournalEntry, JournalEntry> {
|
|
|
204
236
|
throw new Error('Journal entry is already posted');
|
|
205
237
|
}
|
|
206
238
|
|
|
239
|
+
// Normalize any negative amounts to the correct side
|
|
240
|
+
this.sanitizeLines();
|
|
207
241
|
// Validate before posting
|
|
208
242
|
await this.validate();
|
|
209
243
|
|
|
@@ -230,28 +264,41 @@ export class JournalEntry extends SmartDataDbDoc<JournalEntry, JournalEntry> {
|
|
|
230
264
|
transactions.push(transaction);
|
|
231
265
|
} else {
|
|
232
266
|
// Complex entry: multiple debits and/or credits
|
|
233
|
-
//
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
267
|
+
// Build working queues with remaining amounts (don't mutate original lines)
|
|
268
|
+
const debitQueue = debitLines.map(l => ({
|
|
269
|
+
line: l,
|
|
270
|
+
remaining: l.debit || 0
|
|
271
|
+
}));
|
|
272
|
+
|
|
273
|
+
const creditQueue = creditLines.map(l => ({
|
|
274
|
+
line: l,
|
|
275
|
+
remaining: l.credit || 0
|
|
276
|
+
}));
|
|
237
277
|
|
|
238
|
-
|
|
278
|
+
// Create transactions to balance the entry
|
|
279
|
+
for (const d of debitQueue) {
|
|
280
|
+
for (const c of creditQueue) {
|
|
281
|
+
const amount = Math.min(d.remaining, c.remaining);
|
|
282
|
+
|
|
283
|
+
if (amount > 0.0000001) { // small epsilon to avoid float artifacts
|
|
239
284
|
const transaction = await Transaction.createTransaction({
|
|
240
285
|
date: this.date,
|
|
241
|
-
debitAccount:
|
|
242
|
-
creditAccount:
|
|
243
|
-
amount: amount,
|
|
244
|
-
description: `${this.description} - ${
|
|
286
|
+
debitAccount: d.line.accountNumber,
|
|
287
|
+
creditAccount: c.line.accountNumber,
|
|
288
|
+
amount: Math.round(amount * 100) / 100, // round to 2 decimals
|
|
289
|
+
description: `${this.description} - ${d.line.description || c.line.description || ''}`,
|
|
245
290
|
reference: this.reference,
|
|
246
291
|
skrType: this.skrType,
|
|
247
|
-
costCenter:
|
|
292
|
+
costCenter: d.line.costCenter || c.line.costCenter,
|
|
248
293
|
});
|
|
249
294
|
transactions.push(transaction);
|
|
250
|
-
|
|
251
|
-
// Reduce amounts
|
|
252
|
-
|
|
253
|
-
|
|
295
|
+
|
|
296
|
+
// Reduce remaining amounts in working copies (not original lines)
|
|
297
|
+
d.remaining -= amount;
|
|
298
|
+
c.remaining -= amount;
|
|
254
299
|
}
|
|
300
|
+
|
|
301
|
+
if (d.remaining <= 0.0000001) break;
|
|
255
302
|
}
|
|
256
303
|
}
|
|
257
304
|
}
|
|
@@ -299,6 +346,8 @@ export class JournalEntry extends SmartDataDbDoc<JournalEntry, JournalEntry> {
|
|
|
299
346
|
}
|
|
300
347
|
|
|
301
348
|
public async beforeSave(): Promise<void> {
|
|
349
|
+
// Normalize any negative amounts to the correct side
|
|
350
|
+
this.sanitizeLines();
|
|
302
351
|
// Recalculate totals before saving
|
|
303
352
|
this.calculateTotals();
|
|
304
353
|
|