@fin.cx/compatibility 5.2.0 → 5.3.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/00_commitinfo_data.js +1 -1
- package/dist_ts/buchungsliste/index.d.ts +1 -1
- package/dist_ts/buchungsliste/index.js +21 -19
- package/dist_ts/error.d.ts +20 -1
- package/dist_ts/error.js +1 -1
- package/dist_ts/extf/index.d.ts +30 -20
- package/dist_ts/extf/index.js +92 -75
- package/package.json +2 -2
- package/readme.md +13 -10
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/buchungsliste/index.ts +34 -19
- package/ts/error.ts +28 -1
- package/ts/extf/index.ts +100 -77
package/ts/error.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* centres its record does not state (stored before 4.1) and no rows sent were given; the message names it;
|
|
15
15
|
* - `invalid_buchungsliste`: a Buchungsliste that cannot be read or booked as it is: no header line, a required
|
|
16
16
|
* column missing, a field that is not of its format, a field this version does not map, or a row that gives no
|
|
17
|
-
* valid entry; `details` names the line, the row and the field where it can.
|
|
17
|
+
* valid entry; `details` names why (`reason`), the line, the row and the field where it can.
|
|
18
18
|
*/
|
|
19
19
|
export type TCompatibilityErrorCode =
|
|
20
20
|
| 'invalid_bookings'
|
|
@@ -25,8 +25,35 @@ export type TCompatibilityErrorCode =
|
|
|
25
25
|
| 'handed_over_cost_centres_unknown'
|
|
26
26
|
| 'invalid_buchungsliste';
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Why a Buchungsliste was refused (`invalid_buchungsliste`), so a consumer can tell its user without reading the
|
|
30
|
+
* message:
|
|
31
|
+
* - `unreadable`: the text is no list of rows of the first line's fields: a byte order mark of UTF-8, no first line,
|
|
32
|
+
* no delimiter, a quote never closed or text after a closing quote, a required column missing or two columns of
|
|
33
|
+
* one field, a row of another number of fields;
|
|
34
|
+
* - `field_malformed`: a field not of its format (a day, an amount or 0, an account, a Buchungsperiode), or no
|
|
35
|
+
* Belegdatum;
|
|
36
|
+
* - `field_unmapped`: a row that states Zusatzangaben, an EU Land or an EU %, which this version does not map;
|
|
37
|
+
* - `currency_unsupported`: a Währung other than EUR;
|
|
38
|
+
* - `bu_key_unknown`: a Steuerschlüssel `@fin.cx/skr` does not know;
|
|
39
|
+
* - `tax_account_unclear`: a row with a tax where the account it is computed on cannot be told (skr's
|
|
40
|
+
* `extf_tax_account_unclear`);
|
|
41
|
+
* - `row_invalid`: a row whose entry skr refuses otherwise; where its check (`validateJournalDraft`) refused it, what
|
|
42
|
+
* the check found in `details.errors`.
|
|
43
|
+
*/
|
|
44
|
+
export type TBuchungslisteRefusalReason =
|
|
45
|
+
| 'unreadable'
|
|
46
|
+
| 'field_malformed'
|
|
47
|
+
| 'field_unmapped'
|
|
48
|
+
| 'currency_unsupported'
|
|
49
|
+
| 'bu_key_unknown'
|
|
50
|
+
| 'tax_account_unclear'
|
|
51
|
+
| 'row_invalid';
|
|
52
|
+
|
|
28
53
|
/** Where a refusal of a file read stands, as far as it is known; empty for a file written. */
|
|
29
54
|
export interface ICompatibilityErrorDetails {
|
|
55
|
+
/** why a Buchungsliste was refused; stated on every `invalid_buchungsliste` */
|
|
56
|
+
reason?: TBuchungslisteRefusalReason;
|
|
30
57
|
/** the data row, from 1 */
|
|
31
58
|
row?: number;
|
|
32
59
|
/** the line of the file, from 1 */
|
package/ts/extf/index.ts
CHANGED
|
@@ -36,8 +36,9 @@ export interface IExportBooking {
|
|
|
36
36
|
kost1?: string;
|
|
37
37
|
kost2?: string;
|
|
38
38
|
/**
|
|
39
|
-
* The row is a general reversal (field 118 "Generalumkehr", written "1"): it
|
|
40
|
-
* fields
|
|
39
|
+
* The row is a general reversal (field 118 "Generalumkehr", written "1"): DATEV books it on the other side, so it
|
|
40
|
+
* takes back a booking of the same fields, and one that mirrors none (a Storno posted on a later day) stands as the
|
|
41
|
+
* counter-booking on its own day. Absent on an ordinary row.
|
|
41
42
|
*/
|
|
42
43
|
generalumkehr?: true;
|
|
43
44
|
}
|
|
@@ -470,6 +471,65 @@ const bookingNameOf = (bookingArg: IExportBooking, indexArg: number): string =>
|
|
|
470
471
|
`booking ${indexArg + 1} (${bookingArg.date}, ${bookingArg.umsatzCents} ${bookingArg.sollHaben} ${bookingArg.konto}/${bookingArg.gegenkonto}`
|
|
471
472
|
+ `${bookingArg.belegfeld1 ? `, Belegfeld 1 ${bookingArg.belegfeld1}` : ''})`;
|
|
472
473
|
|
|
474
|
+
/** The booking that takes a booking back on DATEV's side: what it says, its general reversal (field 118) turned over. */
|
|
475
|
+
const mirrorOf = (bookingArg: IExportBooking): IExportBooking => {
|
|
476
|
+
const { generalumkehr, ...fields } = bookingArg;
|
|
477
|
+
return generalumkehr ? fields : { ...fields, generalumkehr: true };
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* The indexes of the bookings that stand on DATEV's side after all of them, handed over in their order and compared as
|
|
482
|
+
* a file states them (`comparableOf`), in their order. A booking that mirrors one standing before it (`mirrorOf`: it
|
|
483
|
+
* says what it says, `differingBookingCount`'s fields, the general reversal turned over) takes it back, and neither
|
|
484
|
+
* stands: a general reversal takes back the booking it mirrors, and an ordinary row the general reversal it mirrors. A
|
|
485
|
+
* booking that mirrors none stands, a general reversal too: DATEV books it as the counter-booking on its own day. Of the
|
|
486
|
+
* bookings a booking mirrors, one on the cost centres it states is taken first, then one stored before 4.1, which records
|
|
487
|
+
* none; of several, the last. Linear in their number.
|
|
488
|
+
*/
|
|
489
|
+
const standingIndexesOf = (comparableArg: readonly IExportBooking[]): number[] => {
|
|
490
|
+
const standing = comparableArg.map(() => true);
|
|
491
|
+
// the bookings that stand, by what they say: recorded ones on every key field and on the rest, unrecorded ones on the rest
|
|
492
|
+
const recordedByFullKey = new Map<string, number[]>();
|
|
493
|
+
const recordedByBaseKey = new Map<string, number[]>();
|
|
494
|
+
const unrecordedByBaseKey = new Map<string, number[]>();
|
|
495
|
+
const stand = (queuesArg: Map<string, number[]>, keyArg: string, indexArg: number): void => {
|
|
496
|
+
const queue = queuesArg.get(keyArg);
|
|
497
|
+
if (queue) {
|
|
498
|
+
queue.push(indexArg);
|
|
499
|
+
} else {
|
|
500
|
+
queuesArg.set(keyArg, [indexArg]);
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
// the last booking of a queue that still stands; a booking taken back through another queue is passed over for good
|
|
504
|
+
const lastStanding = (queuesArg: Map<string, number[]>, keyArg: string): number | undefined => {
|
|
505
|
+
const queue = queuesArg.get(keyArg);
|
|
506
|
+
while (queue && queue.length > 0 && !standing[queue[queue.length - 1]]) {
|
|
507
|
+
queue.pop();
|
|
508
|
+
}
|
|
509
|
+
return queue?.[queue.length - 1];
|
|
510
|
+
};
|
|
511
|
+
comparableArg.forEach((bookingArg, indexArg) => {
|
|
512
|
+
const mirrored = mirrorOf(bookingArg);
|
|
513
|
+
const baseKey = baseKeyOf(mirrored);
|
|
514
|
+
const unrecorded = lastStanding(unrecordedByBaseKey, baseKey);
|
|
515
|
+
const taken = recordsCostCentres(mirrored)
|
|
516
|
+
? lastStanding(recordedByFullKey, fullKeyOf(mirrored)) ?? unrecorded
|
|
517
|
+
: Math.max(lastStanding(recordedByBaseKey, baseKey) ?? -1, unrecorded ?? -1);
|
|
518
|
+
if (taken !== undefined && taken >= 0) {
|
|
519
|
+
standing[taken] = false;
|
|
520
|
+
standing[indexArg] = false;
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
if (recordsCostCentres(bookingArg)) {
|
|
524
|
+
stand(recordedByFullKey, fullKeyOf(bookingArg), indexArg);
|
|
525
|
+
stand(recordedByBaseKey, baseKeyOf(bookingArg), indexArg);
|
|
526
|
+
} else {
|
|
527
|
+
stand(unrecordedByBaseKey, baseKeyOf(bookingArg), indexArg);
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
return comparableArg.flatMap((_bookingArg, indexArg) => (standing[indexArg] ? [indexArg] : []));
|
|
531
|
+
};
|
|
532
|
+
|
|
473
533
|
/**
|
|
474
534
|
* Refuses a state handed over that holds a booking whose cost centres its record does not state (stored before 4.1).
|
|
475
535
|
* The Stapel sent stated them (fields 37 and 38), so DATEV holds them: whether the booking must be taken back turns on
|
|
@@ -491,7 +551,10 @@ const refuseCostCentresUnknown = (handedOverArg: readonly IExportBooking[]): voi
|
|
|
491
551
|
* handed over that the current ones no longer hold (`reversed`, each once for each time it is missing), the ones they
|
|
492
552
|
* still hold (`kept`, as they were handed over) and the current rows the bookings handed over do not hold (`added`),
|
|
493
553
|
* each in its order. A booking that stands in both is kept: a changed text or document link is no changed booking,
|
|
494
|
-
* a changed cost centre is.
|
|
554
|
+
* a changed cost centre is. Each side is compared as what stands of it (`bookingsHeldOf`): a general reversal and the
|
|
555
|
+
* booking before it that it mirrors take each other back, among the bookings handed over and among the current rows (a
|
|
556
|
+
* Storno on the day of the booking it takes back), and a general reversal that mirrors none (a Storno posted on a later
|
|
557
|
+
* day) is a booking as any other. The current rows are compared as a file states them (`bookingsAsWritten`), as the
|
|
495
558
|
* bookings handed over are; `added` are the rows themselves, which the writer writes. A booking handed over whose
|
|
496
559
|
* cost centres its record does not state (stored before 4.1) is refused (`handed_over_cost_centres_unknown`): pass
|
|
497
560
|
* `bookingsOfRows` of the rows sent instead.
|
|
@@ -502,18 +565,23 @@ export const correctionOf = (
|
|
|
502
565
|
): { reversed: IExportBooking[]; kept: IExportBooking[]; added: IDatevBuchungsstapelRow[] } => {
|
|
503
566
|
refuseCostCentresUnknown(handedOverArg);
|
|
504
567
|
// the current rows as a file states them, as the state handed over was: a reference the writer changes is the same
|
|
505
|
-
const
|
|
568
|
+
const handedOver = comparableOf(handedOverArg);
|
|
569
|
+
const current = bookingsAsWritten(currentArg);
|
|
570
|
+
// what stands of each
|
|
571
|
+
const held = standingIndexesOf(handedOver);
|
|
572
|
+
const standing = standingIndexesOf(current);
|
|
573
|
+
const { leftPaired, rightPaired } = pairBookings(held.map((indexArg) => handedOver[indexArg]), standing.map((indexArg) => current[indexArg]));
|
|
506
574
|
return {
|
|
507
|
-
reversed:
|
|
508
|
-
kept:
|
|
509
|
-
added:
|
|
575
|
+
reversed: held.filter((_indexArg, positionArg) => !leftPaired[positionArg]).map((indexArg) => handedOverArg[indexArg]),
|
|
576
|
+
kept: held.filter((_indexArg, positionArg) => leftPaired[positionArg]).map((indexArg) => handedOverArg[indexArg]),
|
|
577
|
+
added: standing.filter((_indexArg, positionArg) => !rightPaired[positionArg]).map((indexArg) => currentArg[indexArg]),
|
|
510
578
|
};
|
|
511
579
|
};
|
|
512
580
|
|
|
513
581
|
/**
|
|
514
582
|
* The state of a period handed over, given once: the bookings stored of it (`handedOver`) or the rows of the Stapel
|
|
515
|
-
* sent (`handedOverRows`).
|
|
516
|
-
*
|
|
583
|
+
* sent (`handedOverRows`). What stands of it is compared (`bookingsHeldOf`): a general reversal in it takes back the
|
|
584
|
+
* booking before it that it mirrors, and one that mirrors none (a Storno posted on a later day) stands.
|
|
517
585
|
*/
|
|
518
586
|
export interface ICorrectionBuchungsstapelOptions {
|
|
519
587
|
/**
|
|
@@ -544,7 +612,10 @@ export interface ICorrectionBuchungsstapelOptions {
|
|
|
544
612
|
}
|
|
545
613
|
|
|
546
614
|
export interface ICorrectionBuchungsstapelFile extends IBuchungsstapelFile {
|
|
547
|
-
/**
|
|
615
|
+
/**
|
|
616
|
+
* The bookings handed over it takes back, each written as the row that mirrors it: an ordinary booking with field
|
|
617
|
+
* 118 "Generalumkehr" set, a general reversal that stands as the ordinary row.
|
|
618
|
+
*/
|
|
548
619
|
reversed: IExportBooking[];
|
|
549
620
|
/** the rows it books anew */
|
|
550
621
|
added: IDatevBuchungsstapelRow[];
|
|
@@ -559,11 +630,12 @@ export interface ICorrectionBuchungsstapelFile extends IBuchungsstapelFile {
|
|
|
559
630
|
/**
|
|
560
631
|
* Writes the correction Buchungsstapel of a period handed over whose bookings changed since: only what changed. Each
|
|
561
632
|
* booking handed over that the drafts no longer hold is written again with field 118 "Generalumkehr" set, so DATEV
|
|
562
|
-
* takes it back on its side and day
|
|
563
|
-
*
|
|
564
|
-
*
|
|
565
|
-
*
|
|
566
|
-
*
|
|
633
|
+
* takes it back on its side and day (a general reversal handed over that stands, as the ordinary row), and each row
|
|
634
|
+
* the drafts hold that was not handed over is written as it is; rows in both are not written. Both are compared as
|
|
635
|
+
* what stands of them (`correctionOf`). Every row keeps its Belegdatum, so it counts in the period it belongs to. A
|
|
636
|
+
* booking handed over is never changed or replaced (§ 146 Abs. 4 AO): it is taken back and booked anew. Throws
|
|
637
|
+
* `nothing_to_correct` when nothing changed; `invalid_bookings` for a state given twice or not at all;
|
|
638
|
+
* `handed_over_cost_centres_unknown` for a booking whose cost centres are not known.
|
|
567
639
|
*/
|
|
568
640
|
export const writeCorrectionBuchungsstapel = (optionsArg: ICorrectionBuchungsstapelOptions): ICorrectionBuchungsstapelFile => {
|
|
569
641
|
// rows read back from the file sent are as written already; rows given to the writer become what it wrote
|
|
@@ -571,16 +643,12 @@ export const writeCorrectionBuchungsstapel = (optionsArg: ICorrectionBuchungssta
|
|
|
571
643
|
if (handedOver === undefined || (optionsArg.handedOver !== undefined && optionsArg.handedOverRows !== undefined)) {
|
|
572
644
|
throw new CompatibilityError('invalid_bookings', 'pass the state handed over once: the bookings stored (handedOver) or the rows sent (handedOverRows)');
|
|
573
645
|
}
|
|
574
|
-
const reversal = handedOver.findIndex((bookingArg) => bookingArg.generalumkehr === true);
|
|
575
|
-
if (reversal >= 0) {
|
|
576
|
-
throw new CompatibilityError('invalid_bookings', `the state handed over holds a general reversal (booking ${reversal + 1}); pass the period's bookings as they stood, not a correction file`);
|
|
577
|
-
}
|
|
578
646
|
const current = datevRowsOf(optionsArg.drafts, optionsArg.policy);
|
|
579
647
|
const { reversed, kept, added } = correctionOf(handedOver, current);
|
|
580
648
|
if (reversed.length === 0 && added.length === 0) {
|
|
581
649
|
throw new CompatibilityError('nothing_to_correct', 'the bookings of the period are the ones handed over; there is nothing to correct');
|
|
582
650
|
}
|
|
583
|
-
const rows = [...reversed.map((bookingArg) =>
|
|
651
|
+
const rows = [...reversed.map((bookingArg) => rowOfBooking(mirrorOf(bookingArg))), ...added];
|
|
584
652
|
const file = fileOfRows(rows, optionsArg.header, optionsArg.filename);
|
|
585
653
|
// the rows booked anew as the correction file states them, after the reversals
|
|
586
654
|
return { ...file, reversed, added, bookingsAfter: [...kept, ...file.bookings.slice(reversed.length)] };
|
|
@@ -590,64 +658,19 @@ export const writeCorrectionBuchungsstapel = (optionsArg: ICorrectionBuchungssta
|
|
|
590
658
|
* The bookings of a period as DATEV holds them after its Stapel, handed over in the order given: the period's
|
|
591
659
|
* Buchungsstapel, the additional Stapel after it and its correction Stapel (`writeCorrectionBuchungsstapel`), each as
|
|
592
660
|
* the bookings it stated (`IBuchungsstapelFile.bookings` as stored, or `bookingsOfRows` of its file read back), in
|
|
593
|
-
* their order.
|
|
594
|
-
*
|
|
595
|
-
*
|
|
596
|
-
*
|
|
597
|
-
*
|
|
598
|
-
*
|
|
599
|
-
*
|
|
600
|
-
*
|
|
601
|
-
*
|
|
661
|
+
* their order. A booking that mirrors one standing before it, in an earlier Stapel or earlier in its own, takes it
|
|
662
|
+
* back, and neither stands: the booking it mirrors says what it says (`differingBookingCount`'s fields), compared as a
|
|
663
|
+
* file states them (`comparableOf`), so wording and document link do not count, and is a general reversal (field 118
|
|
664
|
+
* "Generalumkehr") where it is not. A general reversal takes back the booking it mirrors, and an ordinary row the general
|
|
665
|
+
* reversal it mirrors, as a correction takes back a general reversal that stands. A booking that mirrors none stands,
|
|
666
|
+
* a general reversal too: DATEV books it as the counter-booking on its own day, as it books a Storno posted on a later
|
|
667
|
+
* day than the booking it takes back, or in a later period. A booking on the cost centres the reversal states is taken
|
|
668
|
+
* first, then one stored before 4.1, which records none; of several, the last, as a correction reverses the last of
|
|
669
|
+
* several bookings it no longer holds. The bookings that stand, as they were given and in their order: what the next
|
|
670
|
+
* correction is compared against (`handedOver`), and after one correction its `bookingsAfter`. A booking a
|
|
671
|
+
* Buchungsstapel cannot hold is refused (`invalid_bookings`).
|
|
602
672
|
*/
|
|
603
673
|
export const bookingsHeldOf = (stapelArg: readonly (readonly IExportBooking[])[]): IExportBooking[] => {
|
|
604
674
|
const given = stapelArg.flat();
|
|
605
|
-
|
|
606
|
-
// where each booking stands: its Stapel and its place in it, for a refusal
|
|
607
|
-
const places = stapelArg.flatMap((stapelBookingsArg, stapelIndexArg) => stapelBookingsArg.map((_bookingArg, indexArg) => ({ stapel: stapelIndexArg, index: indexArg })));
|
|
608
|
-
const takenBack = given.map(() => false);
|
|
609
|
-
// the bookings that stand, by what they say: recorded ones on every key field and on the rest, unrecorded ones on the rest
|
|
610
|
-
const recordedByFullKey = new Map<string, number[]>();
|
|
611
|
-
const recordedByBaseKey = new Map<string, number[]>();
|
|
612
|
-
const unrecordedByBaseKey = new Map<string, number[]>();
|
|
613
|
-
const stand = (queuesArg: Map<string, number[]>, keyArg: string, indexArg: number): void => {
|
|
614
|
-
const queue = queuesArg.get(keyArg);
|
|
615
|
-
if (queue) {
|
|
616
|
-
queue.push(indexArg);
|
|
617
|
-
} else {
|
|
618
|
-
queuesArg.set(keyArg, [indexArg]);
|
|
619
|
-
}
|
|
620
|
-
};
|
|
621
|
-
// the last booking of a queue that still stands; a booking taken back through another queue is passed over for good
|
|
622
|
-
const lastStanding = (queuesArg: Map<string, number[]>, keyArg: string): number | undefined => {
|
|
623
|
-
const queue = queuesArg.get(keyArg);
|
|
624
|
-
while (queue && queue.length > 0 && takenBack[queue[queue.length - 1]]) {
|
|
625
|
-
queue.pop();
|
|
626
|
-
}
|
|
627
|
-
return queue?.[queue.length - 1];
|
|
628
|
-
};
|
|
629
|
-
comparable.forEach((bookingArg, indexArg) => {
|
|
630
|
-
if (!bookingArg.generalumkehr) {
|
|
631
|
-
if (recordsCostCentres(bookingArg)) {
|
|
632
|
-
stand(recordedByFullKey, fullKeyOf(bookingArg), indexArg);
|
|
633
|
-
stand(recordedByBaseKey, baseKeyOf(bookingArg), indexArg);
|
|
634
|
-
} else {
|
|
635
|
-
stand(unrecordedByBaseKey, baseKeyOf(bookingArg), indexArg);
|
|
636
|
-
}
|
|
637
|
-
return;
|
|
638
|
-
}
|
|
639
|
-
// the booking the reversal mirrors says what it says
|
|
640
|
-
const { generalumkehr: _generalumkehr, ...mirrored } = bookingArg;
|
|
641
|
-
const baseKey = baseKeyOf(mirrored);
|
|
642
|
-
const unrecorded = lastStanding(unrecordedByBaseKey, baseKey);
|
|
643
|
-
const taken = recordsCostCentres(mirrored)
|
|
644
|
-
? lastStanding(recordedByFullKey, fullKeyOf(mirrored)) ?? unrecorded
|
|
645
|
-
: Math.max(lastStanding(recordedByBaseKey, baseKey) ?? -1, unrecorded ?? -1);
|
|
646
|
-
if (taken === undefined || taken < 0) {
|
|
647
|
-
const { stapel, index } = places[indexArg];
|
|
648
|
-
throw new CompatibilityError('invalid_bookings', `Stapel ${stapel + 1}, ${bookingNameOf(given[indexArg], index)} is a general reversal of no booking handed over before it`);
|
|
649
|
-
}
|
|
650
|
-
takenBack[taken] = true;
|
|
651
|
-
});
|
|
652
|
-
return given.filter((_bookingArg, indexArg) => !comparable[indexArg].generalumkehr && !takenBack[indexArg]);
|
|
675
|
+
return standingIndexesOf(comparableOf(given)).map((indexArg) => given[indexArg]);
|
|
653
676
|
};
|