@fin.cx/skr 1.3.0 → 2.0.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/.smartconfig.json +33 -6
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/core/core.accountpolicy.d.ts +67 -0
- package/dist_ts/core/core.accountpolicy.js +91 -0
- package/dist_ts/core/core.bukeys.d.ts +38 -0
- package/dist_ts/core/core.bukeys.js +117 -0
- package/dist_ts/core/core.datev.captions.d.ts +7 -0
- package/dist_ts/core/core.datev.captions.js +134 -0
- package/dist_ts/core/core.datev.d.ts +47 -0
- package/dist_ts/core/core.datev.js +129 -0
- package/dist_ts/core/core.hash.d.ts +27 -0
- package/dist_ts/core/core.hash.js +72 -0
- package/dist_ts/core/core.plugins.d.ts +3 -0
- package/dist_ts/core/core.plugins.js +9 -0
- package/dist_ts/core/core.recipes.d.ts +118 -0
- package/dist_ts/core/core.recipes.js +504 -0
- package/dist_ts/core/core.taxscenario.d.ts +27 -0
- package/dist_ts/core/core.taxscenario.js +85 -0
- package/dist_ts/core/core.types.d.ts +48 -0
- package/dist_ts/core/core.types.js +2 -0
- package/dist_ts/core/core.validate.d.ts +17 -0
- package/dist_ts/core/core.validate.js +89 -0
- package/dist_ts/core/index.d.ts +15 -0
- package/dist_ts/core/index.js +16 -0
- package/dist_ts/index.d.ts +15 -0
- package/dist_ts/index.js +10 -1
- package/dist_ts/plugins.d.ts +2 -1
- package/dist_ts/plugins.js +4 -2
- package/dist_ts/skr.api.d.ts +91 -22
- package/dist_ts/skr.api.js +357 -42
- package/dist_ts/skr.balances.d.ts +28 -0
- package/dist_ts/skr.balances.js +96 -0
- package/dist_ts/skr.classes.journalentry.d.ts +19 -3
- package/dist_ts/skr.classes.journalentry.js +67 -4
- package/dist_ts/skr.classes.reports.js +49 -102
- package/dist_ts/skr.database.d.ts +2 -0
- package/dist_ts/skr.database.js +1 -1
- package/dist_ts/skr.errors.d.ts +18 -0
- package/dist_ts/skr.errors.js +34 -0
- package/dist_ts/skr.export.js +4 -7
- package/dist_ts/skr.invoice.adapter.d.ts +15 -1
- package/dist_ts/skr.invoice.adapter.js +253 -41
- package/dist_ts/skr.invoice.booking.js +14 -28
- package/dist_ts/skr.migrate.d.ts +20 -0
- package/dist_ts/skr.migrate.js +223 -0
- package/dist_ts/skr.period.d.ts +47 -0
- package/dist_ts/skr.period.js +95 -0
- package/dist_ts/skr.posting.d.ts +74 -0
- package/dist_ts/skr.posting.js +209 -0
- package/dist_ts/skr.security.d.ts +6 -9
- package/dist_ts/skr.security.js +16 -89
- package/dist_ts/skr.sequence.d.ts +32 -0
- package/dist_ts/skr.sequence.js +51 -0
- package/dist_ts/skr.txview.d.ts +46 -0
- package/dist_ts/skr.txview.js +235 -0
- package/dist_ts/skr.types.d.ts +35 -1
- package/dist_ts/skr.verify.d.ts +13 -0
- package/dist_ts/skr.verify.js +84 -0
- package/package.json +21 -24
- package/readme.hints.md +37 -0
- package/readme.md +94 -1
- package/readme.plan.md +180 -243
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/core/core.accountpolicy.ts +154 -0
- package/ts/core/core.bukeys.ts +146 -0
- package/ts/core/core.datev.captions.ts +133 -0
- package/ts/core/core.datev.ts +174 -0
- package/ts/core/core.hash.ts +76 -0
- package/ts/core/core.plugins.ts +9 -0
- package/ts/core/core.recipes.ts +659 -0
- package/ts/core/core.taxscenario.ts +156 -0
- package/ts/core/core.types.ts +74 -0
- package/ts/core/core.validate.ts +119 -0
- package/ts/core/index.ts +15 -0
- package/ts/index.ts +31 -0
- package/ts/plugins.ts +4 -0
- package/ts/skr.api.ts +432 -46
- package/ts/skr.balances.ts +127 -0
- package/ts/skr.classes.journalentry.ts +54 -5
- package/ts/skr.classes.reports.ts +54 -126
- package/ts/skr.database.ts +3 -0
- package/ts/skr.errors.ts +44 -0
- package/ts/skr.export.ts +3 -6
- package/ts/skr.invoice.adapter.ts +291 -45
- package/ts/skr.invoice.booking.ts +22 -34
- package/ts/skr.migrate.ts +268 -0
- package/ts/skr.period.ts +139 -0
- package/ts/skr.posting.ts +273 -0
- package/ts/skr.security.ts +18 -93
- package/ts/skr.sequence.ts +89 -0
- package/ts/skr.txview.ts +268 -0
- package/ts/skr.types.ts +37 -1
- package/ts/skr.verify.ts +99 -0
- /package/{license.md → license} +0 -0
|
@@ -69,13 +69,13 @@ export class InvoiceAdapter {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
// Get detected format
|
|
72
|
-
const format = this.mapEInvoiceFormat(einvoice
|
|
72
|
+
const format = this.mapEInvoiceFormat(this.getDetectedFormat(einvoice));
|
|
73
73
|
|
|
74
74
|
// Validate the invoice (takes ~2.2ms)
|
|
75
75
|
const validationResult = await this.validateInvoice(einvoice);
|
|
76
76
|
|
|
77
77
|
// Extract invoice data
|
|
78
|
-
const invoiceData =
|
|
78
|
+
const invoiceData = this.mapEInvoiceToBusinessTerms(einvoice);
|
|
79
79
|
|
|
80
80
|
// Map to internal invoice model
|
|
81
81
|
const invoice = await this.mapToInternalModel(
|
|
@@ -106,33 +106,9 @@ export class InvoiceAdapter {
|
|
|
106
106
|
* Validate an invoice using multi-level validation
|
|
107
107
|
*/
|
|
108
108
|
private async validateInvoice(einvoice: any): Promise<IValidationResult> {
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
// Parse validation results into our structure
|
|
113
|
-
const syntaxResult = {
|
|
114
|
-
isValid: validationResult.syntax?.valid !== false,
|
|
115
|
-
errors: validationResult.syntax?.errors || [],
|
|
116
|
-
warnings: validationResult.syntax?.warnings || []
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
const semanticResult = {
|
|
120
|
-
isValid: validationResult.semantic?.valid !== false,
|
|
121
|
-
errors: validationResult.semantic?.errors || [],
|
|
122
|
-
warnings: validationResult.semantic?.warnings || []
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
const businessResult = {
|
|
126
|
-
isValid: validationResult.business?.valid !== false,
|
|
127
|
-
errors: validationResult.business?.errors || [],
|
|
128
|
-
warnings: validationResult.business?.warnings || []
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
const countryResult = {
|
|
132
|
-
isValid: validationResult.country?.valid !== false,
|
|
133
|
-
errors: validationResult.country?.errors || [],
|
|
134
|
-
warnings: validationResult.country?.warnings || []
|
|
135
|
-
};
|
|
109
|
+
const syntaxResult = await this.validateInvoiceLevel(einvoice, 'syntax');
|
|
110
|
+
const semanticResult = await this.validateInvoiceLevel(einvoice, 'semantic');
|
|
111
|
+
const businessResult = await this.validateInvoiceLevel(einvoice, 'business');
|
|
136
112
|
|
|
137
113
|
return {
|
|
138
114
|
isValid: syntaxResult.isValid && semanticResult.isValid && businessResult.isValid,
|
|
@@ -152,15 +128,219 @@ export class InvoiceAdapter {
|
|
|
152
128
|
warnings: businessResult.warnings || []
|
|
153
129
|
},
|
|
154
130
|
countrySpecific: {
|
|
155
|
-
valid:
|
|
156
|
-
errors:
|
|
157
|
-
warnings:
|
|
131
|
+
valid: true,
|
|
132
|
+
errors: [],
|
|
133
|
+
warnings: []
|
|
158
134
|
},
|
|
159
135
|
validatedAt: new Date(),
|
|
160
|
-
validatorVersion: '
|
|
136
|
+
validatorVersion: '6.0.0'
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private async validateInvoiceLevel(
|
|
141
|
+
einvoice: any,
|
|
142
|
+
level: 'syntax' | 'semantic' | 'business'
|
|
143
|
+
): Promise<{ isValid: boolean; errors: string[]; warnings: string[] }> {
|
|
144
|
+
const validationResult = await einvoice.validate(level);
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
isValid: validationResult.valid !== false,
|
|
148
|
+
errors: this.formatValidationMessages(validationResult.errors),
|
|
149
|
+
warnings: this.formatValidationMessages(validationResult.warnings),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private formatValidationMessages(messages: any[] = []): string[] {
|
|
154
|
+
return messages.map((message) => {
|
|
155
|
+
if (typeof message === 'string') {
|
|
156
|
+
return message;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return [message.code, message.message, message.location]
|
|
160
|
+
.filter(Boolean)
|
|
161
|
+
.join(': ');
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private getDetectedFormat(einvoice: any): string {
|
|
166
|
+
if (typeof einvoice.getFormat === 'function') {
|
|
167
|
+
return einvoice.getFormat();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return einvoice.format || 'xrechnung';
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private mapEInvoiceToBusinessTerms(einvoice: any): any {
|
|
174
|
+
const issueDate = new Date(einvoice.date || Date.now());
|
|
175
|
+
const invoiceLines = this.mapEInvoiceLinesToBusinessTerms(einvoice.items || [], Boolean(einvoice.reverseCharge));
|
|
176
|
+
const vatBreakdown = this.buildVatBreakdown(invoiceLines);
|
|
177
|
+
const lineNetAmount = invoiceLines.reduce((sum, line) => sum + line.BT131_LineNetAmount, 0);
|
|
178
|
+
const totalVATAmount = vatBreakdown.reduce((sum, vat) => sum + vat.BT117_TaxAmount, 0);
|
|
179
|
+
const taxInclusiveAmount = lineNetAmount + totalVATAmount;
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
BT1_InvoiceNumber: einvoice.accountingDocId || einvoice.id || einvoice.invoiceId || '',
|
|
183
|
+
BT2_IssueDate: issueDate.toISOString(),
|
|
184
|
+
BT3_InvoiceTypeCode: einvoice.accountingDocType === 'creditnote' ? '381' : '380',
|
|
185
|
+
BT5_CurrencyCode: einvoice.currency || 'EUR',
|
|
186
|
+
BT7_TaxPointDate: einvoice.deliveryDate ? new Date(einvoice.deliveryDate).toISOString() : undefined,
|
|
187
|
+
BT9_PaymentDueDate: this.getDueDate(issueDate, einvoice.dueInDays)?.toISOString(),
|
|
188
|
+
BT10_BuyerReference: einvoice.buyerReference,
|
|
189
|
+
BT22_InvoiceNote: Array.isArray(einvoice.notes) ? einvoice.notes.join('\n') : undefined,
|
|
190
|
+
BG4_Seller: this.mapEInvoiceContactToBusinessTerms(einvoice.from, 'seller', einvoice.paymentOptions),
|
|
191
|
+
BG7_Buyer: this.mapEInvoiceContactToBusinessTerms(einvoice.to, 'buyer'),
|
|
192
|
+
BG25_InvoiceLines: invoiceLines,
|
|
193
|
+
BG20_DocumentAllowances: [],
|
|
194
|
+
BG21_DocumentCharges: [],
|
|
195
|
+
BT106_SumOfLineNetAmounts: lineNetAmount,
|
|
196
|
+
BT107_AllowanceTotalAmount: 0,
|
|
197
|
+
BT108_ChargeTotalAmount: 0,
|
|
198
|
+
BT109_TaxExclusiveAmount: lineNetAmount,
|
|
199
|
+
BT110_TotalVATAmount: totalVATAmount,
|
|
200
|
+
BT112_TaxInclusiveAmount: taxInclusiveAmount,
|
|
201
|
+
BT113_PrepaidAmount: 0,
|
|
202
|
+
BT115_PayableAmount: taxInclusiveAmount,
|
|
203
|
+
BG23_VATBreakdown: vatBreakdown,
|
|
204
|
+
BG16_PaymentInstructions: this.mapPaymentOptionsToBusinessTerms(einvoice.paymentOptions),
|
|
161
205
|
};
|
|
162
206
|
}
|
|
163
207
|
|
|
208
|
+
private mapEInvoiceLinesToBusinessTerms(items: any[], reverseCharge: boolean): any[] {
|
|
209
|
+
return items.map((item, index) => {
|
|
210
|
+
const quantity = Number(item.unitQuantity || 0);
|
|
211
|
+
const unitPrice = Number(item.unitNetPrice || 0);
|
|
212
|
+
const vatRate = Number(item.vatPercentage || 0);
|
|
213
|
+
const netAmount = quantity * unitPrice;
|
|
214
|
+
const vatAmount = reverseCharge ? 0 : netAmount * vatRate / 100;
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
BT154_ItemDescription: item.name || '',
|
|
218
|
+
BT129_Quantity: quantity,
|
|
219
|
+
BT146_NetPrice: unitPrice,
|
|
220
|
+
BT131_LineNetAmount: netAmount,
|
|
221
|
+
BT151_ItemVATCategory: this.getVatCategoryCode(vatRate, reverseCharge),
|
|
222
|
+
BT152_ItemVATRate: vatRate,
|
|
223
|
+
BT155_ItemSellerID: item.articleNumber,
|
|
224
|
+
lineVATAmount: vatAmount,
|
|
225
|
+
};
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private buildVatBreakdown(invoiceLines: any[]): any[] {
|
|
230
|
+
const vatBreakdownMap = new Map<string, { categoryCode: string; rate: number; taxableAmount: number; taxAmount: number }>();
|
|
231
|
+
|
|
232
|
+
for (const line of invoiceLines) {
|
|
233
|
+
const categoryCode = line.BT151_ItemVATCategory || 'S';
|
|
234
|
+
const rate = Number(line.BT152_ItemVATRate || 0);
|
|
235
|
+
const key = `${categoryCode}:${rate}`;
|
|
236
|
+
const existingBreakdown = vatBreakdownMap.get(key) || {
|
|
237
|
+
categoryCode,
|
|
238
|
+
rate,
|
|
239
|
+
taxableAmount: 0,
|
|
240
|
+
taxAmount: 0,
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
existingBreakdown.taxableAmount += Number(line.BT131_LineNetAmount || 0);
|
|
244
|
+
existingBreakdown.taxAmount += Number(line.lineVATAmount || 0);
|
|
245
|
+
vatBreakdownMap.set(key, existingBreakdown);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return Array.from(vatBreakdownMap.values()).map((vatBreakdown) => ({
|
|
249
|
+
BT118_VATCategory: vatBreakdown.categoryCode,
|
|
250
|
+
BT119_VATRate: vatBreakdown.rate,
|
|
251
|
+
BT116_TaxableAmount: vatBreakdown.taxableAmount,
|
|
252
|
+
BT117_TaxAmount: vatBreakdown.taxAmount,
|
|
253
|
+
}));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private mapEInvoiceContactToBusinessTerms(
|
|
257
|
+
contact: any,
|
|
258
|
+
role: 'seller' | 'buyer',
|
|
259
|
+
paymentOptions?: any,
|
|
260
|
+
): any {
|
|
261
|
+
const address = contact?.address || {};
|
|
262
|
+
const registrationDetails = contact?.registrationDetails || {};
|
|
263
|
+
const countryCode = this.normalizeCountryCode(address.countryCode || address.country);
|
|
264
|
+
|
|
265
|
+
if (role === 'seller') {
|
|
266
|
+
return {
|
|
267
|
+
BT27_SellerName: contact?.name || '',
|
|
268
|
+
BT29_SellerID: registrationDetails.registrationId,
|
|
269
|
+
BT31_SellerVATID: registrationDetails.vatId,
|
|
270
|
+
BT32_SellerTaxID: registrationDetails.registrationId,
|
|
271
|
+
BT33_SellerPhone: contact?.phone,
|
|
272
|
+
BT34_SellerEmail: contact?.email,
|
|
273
|
+
BT35_SellerStreet: this.formatStreet(address),
|
|
274
|
+
BT37_SellerCity: address.city,
|
|
275
|
+
BT38_SellerPostalCode: address.postalCode,
|
|
276
|
+
BT40_SellerCountryCode: countryCode,
|
|
277
|
+
BT84_PaymentAccountID: paymentOptions?.sepaConnection?.iban,
|
|
278
|
+
BT85_PaymentAccountName: contact?.name,
|
|
279
|
+
BT86_PaymentServiceProviderID: paymentOptions?.sepaConnection?.bic,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
BT44_BuyerName: contact?.name || '',
|
|
285
|
+
BT46_BuyerID: registrationDetails.registrationId,
|
|
286
|
+
BT47_BuyerTaxID: registrationDetails.registrationId,
|
|
287
|
+
BT48_BuyerVATID: registrationDetails.vatId,
|
|
288
|
+
BT49_BuyerEmail: contact?.email,
|
|
289
|
+
BT50_BuyerStreet: this.formatStreet(address),
|
|
290
|
+
BT52_BuyerCity: address.city,
|
|
291
|
+
BT53_BuyerPostalCode: address.postalCode,
|
|
292
|
+
BT55_BuyerCountryCode: countryCode,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
private mapPaymentOptionsToBusinessTerms(paymentOptions: any): any | undefined {
|
|
297
|
+
if (!paymentOptions?.sepaConnection?.iban) {
|
|
298
|
+
return undefined;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
BT81_PaymentMeansCode: '30',
|
|
303
|
+
BT84_PaymentAccountID: paymentOptions.sepaConnection.iban,
|
|
304
|
+
BT85_PaymentAccountName: paymentOptions.description,
|
|
305
|
+
BT86_PaymentServiceProviderID: paymentOptions.sepaConnection.bic,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private getDueDate(issueDate: Date, dueInDays?: number): Date | undefined {
|
|
310
|
+
if (typeof dueInDays !== 'number') {
|
|
311
|
+
return undefined;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return new Date(issueDate.getTime() + dueInDays * 24 * 60 * 60 * 1000);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private getVatCategoryCode(vatRate: number, reverseCharge: boolean): string {
|
|
318
|
+
if (reverseCharge) {
|
|
319
|
+
return 'AE';
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return vatRate === 0 ? 'Z' : 'S';
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private normalizeCountryCode(country?: string): string {
|
|
326
|
+
if (!country) {
|
|
327
|
+
return 'DE';
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const countryMap: Record<string, string> = {
|
|
331
|
+
Germany: 'DE',
|
|
332
|
+
Deutschland: 'DE',
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
return countryMap[country] || country;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private formatStreet(address: any): string {
|
|
339
|
+
return [address.streetName, address.houseNumber]
|
|
340
|
+
.filter(Boolean)
|
|
341
|
+
.join(' ');
|
|
342
|
+
}
|
|
343
|
+
|
|
164
344
|
/**
|
|
165
345
|
* Map EN16931 Business Terms to internal invoice model
|
|
166
346
|
*/
|
|
@@ -559,8 +739,8 @@ export class InvoiceAdapter {
|
|
|
559
739
|
const einvoice: any = new EInvoice();
|
|
560
740
|
|
|
561
741
|
// Set invoice data
|
|
562
|
-
const
|
|
563
|
-
Object.assign(einvoice,
|
|
742
|
+
const einvoiceData = this.mapToEInvoiceData(invoiceData);
|
|
743
|
+
Object.assign(einvoice, einvoiceData);
|
|
564
744
|
|
|
565
745
|
// Generate XML in requested format
|
|
566
746
|
const xml = await einvoice.exportXml(format as any);
|
|
@@ -585,17 +765,83 @@ export class InvoiceAdapter {
|
|
|
585
765
|
/**
|
|
586
766
|
* Map internal invoice to EN16931 Business Terms
|
|
587
767
|
*/
|
|
588
|
-
private
|
|
768
|
+
private mapToEInvoiceData(invoice: Partial<IInvoice>): any {
|
|
769
|
+
const issueDate = invoice.issueDate || new Date();
|
|
770
|
+
|
|
589
771
|
return {
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
772
|
+
id: invoice.invoiceNumber || '',
|
|
773
|
+
accountingDocId: invoice.invoiceNumber || '',
|
|
774
|
+
accountingDocType: invoice.invoiceTypeCode === '381' ? 'creditnote' : 'invoice',
|
|
775
|
+
date: issueDate.getTime(),
|
|
776
|
+
currency: invoice.currencyCode || 'EUR',
|
|
777
|
+
buyerReference: invoice.buyerReference,
|
|
778
|
+
deliveryDate: invoice.taxPointDate?.getTime(),
|
|
779
|
+
dueInDays: this.calculateDueInDays(issueDate, invoice.paymentDueDate),
|
|
780
|
+
notes: invoice.invoiceNote ? [invoice.invoiceNote] : [],
|
|
781
|
+
from: this.mapInvoicePartyToEInvoiceContact(invoice.supplier),
|
|
782
|
+
to: this.mapInvoicePartyToEInvoiceContact(invoice.customer),
|
|
783
|
+
items: (invoice.lines || []).map((line) => ({
|
|
784
|
+
position: line.lineNumber,
|
|
785
|
+
name: line.description,
|
|
786
|
+
articleNumber: line.productCode,
|
|
787
|
+
unitType: 'EA',
|
|
788
|
+
unitQuantity: line.quantity,
|
|
789
|
+
unitNetPrice: line.unitPrice,
|
|
790
|
+
vatPercentage: line.vatCategory.rate,
|
|
791
|
+
})),
|
|
792
|
+
paymentOptions: invoice.paymentMeans?.account ? {
|
|
793
|
+
description: invoice.paymentMeans.account.accountHolder,
|
|
794
|
+
sepaConnection: {
|
|
795
|
+
iban: invoice.paymentMeans.account.iban,
|
|
796
|
+
bic: invoice.paymentMeans.account.bic || '',
|
|
797
|
+
},
|
|
798
|
+
payPal: {
|
|
799
|
+
email: '',
|
|
800
|
+
},
|
|
801
|
+
} : undefined,
|
|
599
802
|
};
|
|
600
803
|
}
|
|
804
|
+
|
|
805
|
+
private mapInvoicePartyToEInvoiceContact(party?: IInvoiceParty): any {
|
|
806
|
+
const countryCode = this.normalizeCountryCode(party?.address?.countryCode);
|
|
807
|
+
|
|
808
|
+
return {
|
|
809
|
+
type: 'company',
|
|
810
|
+
name: party?.name || '',
|
|
811
|
+
description: '',
|
|
812
|
+
address: {
|
|
813
|
+
streetName: party?.address?.street || '',
|
|
814
|
+
houseNumber: '',
|
|
815
|
+
city: party?.address?.city || '',
|
|
816
|
+
postalCode: party?.address?.postalCode || '',
|
|
817
|
+
country: countryCode,
|
|
818
|
+
countryCode,
|
|
819
|
+
},
|
|
820
|
+
registrationDetails: {
|
|
821
|
+
vatId: party?.vatId || '',
|
|
822
|
+
registrationId: party?.id || party?.taxId || '',
|
|
823
|
+
registrationName: party?.name || '',
|
|
824
|
+
},
|
|
825
|
+
status: 'active',
|
|
826
|
+
foundedDate: {
|
|
827
|
+
year: new Date().getFullYear(),
|
|
828
|
+
month: new Date().getMonth() + 1,
|
|
829
|
+
day: new Date().getDate(),
|
|
830
|
+
},
|
|
831
|
+
email: party?.email,
|
|
832
|
+
phone: party?.phone,
|
|
833
|
+
sepaConnection: party?.bankAccount ? {
|
|
834
|
+
iban: party.bankAccount.iban,
|
|
835
|
+
bic: party.bankAccount.bic || '',
|
|
836
|
+
} : undefined,
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
private calculateDueInDays(issueDate: Date, paymentDueDate?: Date): number {
|
|
841
|
+
if (!paymentDueDate) {
|
|
842
|
+
return 30;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
return Math.max(0, Math.round((paymentDueDate.getTime() - issueDate.getTime()) / (24 * 60 * 60 * 1000)));
|
|
846
|
+
}
|
|
601
847
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
2
|
import { JournalEntry } from './skr.classes.journalentry.js';
|
|
3
|
+
import { JournalPoster } from './skr.posting.js';
|
|
4
|
+
import { journalInputToDraft } from './skr.txview.js';
|
|
3
5
|
import { SKRInvoiceMapper } from './skr.invoice.mapper.js';
|
|
4
6
|
import { suggestPostingKey } from './skr.postingkeys.js';
|
|
5
7
|
import type { TSKRType, IJournalEntry, IJournalEntryLine } from './skr.types.js';
|
|
@@ -86,13 +88,18 @@ export class InvoiceBookingEngine {
|
|
|
86
88
|
}
|
|
87
89
|
}
|
|
88
90
|
|
|
89
|
-
// Build journal
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
// Build journal input and post it atomically via the v2 path
|
|
92
|
+
const journalData = await this.buildJournalEntry(invoice, rules, options);
|
|
93
|
+
const journalEntry = await new JournalPoster().postDraft(
|
|
94
|
+
journalInputToDraft(journalData),
|
|
95
|
+
);
|
|
96
|
+
if (invoice.status !== 'posted') {
|
|
97
|
+
invoice.status = 'posted';
|
|
98
|
+
}
|
|
99
|
+
|
|
93
100
|
const bookingInfo: IBookingInfo = {
|
|
94
101
|
journalEntryId: journalEntry.id,
|
|
95
|
-
transactionIds:
|
|
102
|
+
transactionIds: [],
|
|
96
103
|
bookedAt: new Date(),
|
|
97
104
|
bookedBy: 'system',
|
|
98
105
|
bookingRules: {
|
|
@@ -105,23 +112,7 @@ export class InvoiceBookingEngine {
|
|
|
105
112
|
confidence,
|
|
106
113
|
autoBooked: options?.autoBook || false
|
|
107
114
|
};
|
|
108
|
-
|
|
109
|
-
// Post the journal entry
|
|
110
|
-
// TODO: When MongoDB transactions are available, wrap this in a transaction
|
|
111
|
-
// Example: await db.withTransaction(async (session) => { ... })
|
|
112
|
-
try {
|
|
113
|
-
await journalEntry.validate();
|
|
114
|
-
await journalEntry.post();
|
|
115
|
-
|
|
116
|
-
// Mark invoice as posted if we have a reference to it
|
|
117
|
-
if (invoice.status !== 'posted') {
|
|
118
|
-
invoice.status = 'posted';
|
|
119
|
-
}
|
|
120
|
-
} catch (postError) {
|
|
121
|
-
this.logger.log('error', `Failed to post journal entry: ${postError}`);
|
|
122
|
-
throw postError; // Re-throw to trigger rollback when transactions are available
|
|
123
|
-
}
|
|
124
|
-
|
|
115
|
+
|
|
125
116
|
return {
|
|
126
117
|
success: true,
|
|
127
118
|
journalEntry,
|
|
@@ -147,7 +138,7 @@ export class InvoiceBookingEngine {
|
|
|
147
138
|
invoice: IInvoice,
|
|
148
139
|
rules: IBookingRules,
|
|
149
140
|
options?: IBookingOptions
|
|
150
|
-
): Promise<
|
|
141
|
+
): Promise<IJournalEntry> {
|
|
151
142
|
const lines: IJournalEntryLine[] = [];
|
|
152
143
|
const isInbound = invoice.direction === 'inbound';
|
|
153
144
|
const isCredit = invoice.invoiceTypeCode === '381'; // Credit note
|
|
@@ -163,17 +154,14 @@ export class InvoiceBookingEngine {
|
|
|
163
154
|
lines.push(...this.buildAREntry(invoice, rules, reverseDirection));
|
|
164
155
|
}
|
|
165
156
|
|
|
166
|
-
//
|
|
167
|
-
|
|
157
|
+
// Journal input data — posted through the v2 atomic path by the caller
|
|
158
|
+
return {
|
|
168
159
|
date: options?.bookingDate || invoice.issueDate,
|
|
169
160
|
description: this.buildDescription(invoice),
|
|
170
161
|
reference: options?.bookingReference || invoice.invoiceNumber,
|
|
171
162
|
lines,
|
|
172
163
|
skrType: this.skrType
|
|
173
164
|
};
|
|
174
|
-
|
|
175
|
-
const journalEntry = new JournalEntry(journalData);
|
|
176
|
-
return journalEntry;
|
|
177
165
|
}
|
|
178
166
|
|
|
179
167
|
/**
|
|
@@ -555,7 +543,7 @@ export class InvoiceBookingEngine {
|
|
|
555
543
|
}
|
|
556
544
|
}
|
|
557
545
|
|
|
558
|
-
// Create journal entry for payment
|
|
546
|
+
// Create journal entry for payment (v2 atomic path)
|
|
559
547
|
const journalData: IJournalEntry = {
|
|
560
548
|
date: payment.paymentDate,
|
|
561
549
|
description: `Payment for invoice ${invoice.invoiceNumber}`,
|
|
@@ -563,11 +551,11 @@ export class InvoiceBookingEngine {
|
|
|
563
551
|
lines,
|
|
564
552
|
skrType: this.skrType
|
|
565
553
|
};
|
|
566
|
-
|
|
567
|
-
const journalEntry = new
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
554
|
+
|
|
555
|
+
const journalEntry = await new JournalPoster().postDraft(
|
|
556
|
+
journalInputToDraft(journalData),
|
|
557
|
+
);
|
|
558
|
+
|
|
571
559
|
return {
|
|
572
560
|
success: true,
|
|
573
561
|
journalEntry,
|