@ekwo-ai/transaction-journal 0.9.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/LICENSE +21 -0
- package/README.md +96 -0
- package/dist/csv.d.ts +25 -0
- package/dist/csv.d.ts.map +1 -0
- package/dist/csv.js +125 -0
- package/dist/csv.js.map +1 -0
- package/dist/decimal.d.ts +12 -0
- package/dist/decimal.d.ts.map +1 -0
- package/dist/decimal.js +19 -0
- package/dist/decimal.js.map +1 -0
- package/dist/errors.d.ts +25 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +12 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +80 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +325 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +96 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +16 -0
- package/dist/types.js.map +1 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Karuna Co OÜ, trading as Ekwo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# `@ekwo-ai/transaction-journal`
|
|
2
|
+
|
|
3
|
+
Reads a **transaction journal** saved as CSV — the report a ledger prints of
|
|
4
|
+
every transaction it posted, one after the other, each line of a transaction
|
|
5
|
+
on a row of its own — into plain objects, in TypeScript, with no
|
|
6
|
+
dependencies. Beside it, the list of accounts, exported the same way.
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { readTransactionJournal } from '@ekwo-ai/transaction-journal';
|
|
10
|
+
|
|
11
|
+
const books = readTransactionJournal([journal, accountList], { dateOrder: 'mdy' });
|
|
12
|
+
|
|
13
|
+
books.entries; // [{ journal: 'Invoice', number: '1001', date: '2025-01-15', lines: […] }, …]
|
|
14
|
+
books.accounts; // [{ code: '1200', name: 'Accounts Receivable (A/R)', type: 'Accounts receivable (A/R)' }, …]
|
|
15
|
+
books.violations; // a transaction that does not balance, a line that belongs to none
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## The files
|
|
19
|
+
|
|
20
|
+
**The journal** (required). Such a ledger exports its reports as a
|
|
21
|
+
spreadsheet, not as CSV: open it and save it as CSV. A report is laid out for a
|
|
22
|
+
person, and nothing on a line names the transaction it belongs to, so it is
|
|
23
|
+
read in order:
|
|
24
|
+
|
|
25
|
+
- the **header** is the first row that names a `Date`, an `Account` (or an
|
|
26
|
+
`Account #`), a `Debit` and a `Credit` — the title rows above it are passed
|
|
27
|
+
over, and so is an empty first column;
|
|
28
|
+
- a row with an account and a **date starts** a transaction — unless it
|
|
29
|
+
repeats the date, the `Transaction Type` and the `Num` of the transaction
|
|
30
|
+
just before it, which a layout that prints them on every line does;
|
|
31
|
+
- a row with an account and **no date continues** the transaction above;
|
|
32
|
+
- a row with **no account ends** it: the total of a transaction, a blank row,
|
|
33
|
+
the total of the report, the footer.
|
|
34
|
+
|
|
35
|
+
Optional columns: `Transaction Type` (kept as the journal of the entry), `Num`
|
|
36
|
+
(its number and reference), `Name` — or `Customer full name`, `Vendor`,
|
|
37
|
+
`Supplier`, `Employee` where the report shows those instead — and
|
|
38
|
+
`Memo/Description`.
|
|
39
|
+
|
|
40
|
+
- An **account** is identified by its number, `Account #`, where the report
|
|
41
|
+
has the column — tick it among the report's columns before exporting — and
|
|
42
|
+
otherwise by the name the list of accounts gives a number to, or by its
|
|
43
|
+
name alone.
|
|
44
|
+
- A **date written in digits** — `03/04/2026` — is read in the order the
|
|
45
|
+
caller names (`dateOrder: 'dmy' | 'mdy' | 'ymd'`) and refused without one:
|
|
46
|
+
the order follows the region of the company, and the file does not say it.
|
|
47
|
+
A date in ISO or with its month in letters (`Jan 15, 2025`) needs none.
|
|
48
|
+
- An **amount** is read as a spreadsheet displays it. In a comma-separated file
|
|
49
|
+
the decimal mark is a point and a comma only groups thousands, three digits at
|
|
50
|
+
a time (`1,234.50`). In a file separated by a semicolon or a tab the decimal
|
|
51
|
+
mark is the one mark the number carries, and thousands are grouped by a space
|
|
52
|
+
only. A negative is a minus or brackets. A currency symbol, or anything else,
|
|
53
|
+
is refused.
|
|
54
|
+
|
|
55
|
+
**The list of accounts** (optional): `Account` (or `Full name`), `Type`, and
|
|
56
|
+
`Account #` where it has one; the rest is ignored. The type is kept in the
|
|
57
|
+
list's words.
|
|
58
|
+
|
|
59
|
+
## What it returns
|
|
60
|
+
|
|
61
|
+
The shape declared in [`src/types.ts`](./src/types.ts), which is, field for
|
|
62
|
+
field, the shape every reader of an accounting export in this repository
|
|
63
|
+
declares for itself. A report says no currency: it prints the company's and
|
|
64
|
+
does not name it, and no amount in another currency is read. Amounts are decimal strings, never negative. What does not add up comes back in
|
|
65
|
+
`violations`; what is not a journal is thrown as a `BooksFileError` with a
|
|
66
|
+
`code`.
|
|
67
|
+
|
|
68
|
+
It knows no chart of accounts: in [Ekwo OS](https://github.com/Ekwo-ai/ekwo-os)
|
|
69
|
+
the correspondence is made by
|
|
70
|
+
`ekwo import transaction-journal <files…> --date-order mdy`.
|
|
71
|
+
|
|
72
|
+
## Sources
|
|
73
|
+
|
|
74
|
+
This reader was written against the **Journal** report of **QuickBooks
|
|
75
|
+
Online**, read on 24 September 2026 from its help centre:
|
|
76
|
+
|
|
77
|
+
- [Print a journal entry report](https://quickbooks.intuit.com/learn-support/en-us/help-article/journal-entries/print-journal-entry-report/L1IhAG8PT_US_en_US):
|
|
78
|
+
the Journal report, its filter by transaction type, and its columns — `Name`,
|
|
79
|
+
or `Customer full name`, `Employee`, `Vendor` in its place;
|
|
80
|
+
- [Customize reports](https://quickbooks.intuit.com/learn-support/en-us/help-article/customize-reports/customize-reports-quickbooks-online/L0gKmSawG_US_en_US)
|
|
81
|
+
and [Export reports to Excel](https://quickbooks.intuit.com/learn-support/en-us/help-article/report-management/export-reports-excel-quickbooks-online/L7iAoP97n_US_en_US):
|
|
82
|
+
the columns a report shows, its number format, and its export — to Excel or
|
|
83
|
+
PDF, not to CSV;
|
|
84
|
+
- [Export your QuickBooks Online data](https://quickbooks.intuit.com/learn-support/en-us/help-article/list-management/export-reports-lists-data-quickbooks-online/L1xleDrLp_US_en_US):
|
|
85
|
+
the Account List, the chart of accounts, exported to Excel the same way.
|
|
86
|
+
|
|
87
|
+
`ekwo import quickbooks-online <files…>` is the same as
|
|
88
|
+
`ekwo import transaction-journal`.
|
|
89
|
+
|
|
90
|
+
What those pages do not state — the exact text of the title and total rows,
|
|
91
|
+
whether a line repeats the date of its transaction — is why the reader finds
|
|
92
|
+
the header by its columns, and a transaction by the order of its rows under
|
|
93
|
+
either layout, rather than by position. The fixtures under `test/fixtures/`
|
|
94
|
+
are written by hand from those pages and hold no real data.
|
|
95
|
+
|
|
96
|
+
MIT.
|
package/dist/csv.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The comma-separated values a spreadsheet saves, as RFC 4180 describes them:
|
|
3
|
+
* fields between double quotes may hold the separator, a line break and a
|
|
4
|
+
* doubled quote. Nothing else about the file is assumed.
|
|
5
|
+
*
|
|
6
|
+
* The separator is **read from the header**, which this format requires: the
|
|
7
|
+
* first of `,`, `;` and a tab that the header row contains outside quotes. A
|
|
8
|
+
* spreadsheet saves with a comma or a semicolon depending on the language of
|
|
9
|
+
* the machine, and the header is the one row whose content is known, so this
|
|
10
|
+
* is reading the file rather than guessing at it.
|
|
11
|
+
*/
|
|
12
|
+
import type { Encoding } from './types.js';
|
|
13
|
+
export type Separator = ',' | ';' | '\t';
|
|
14
|
+
/** Bytes to text, in the encoding the caller said, refusing what is not that. */
|
|
15
|
+
export declare function decode(input: string | Uint8Array, encoding: Encoding, maxBytes: number): string;
|
|
16
|
+
/** The separator of a header line: the first of the three found outside quotes. */
|
|
17
|
+
export declare function separatorOf(headerLine: string): Separator;
|
|
18
|
+
/** Every row of the file, each an array of fields, with the row number it starts on. */
|
|
19
|
+
export declare function parseCsv(text: string, separator: Separator): {
|
|
20
|
+
row: number;
|
|
21
|
+
fields: string[];
|
|
22
|
+
}[];
|
|
23
|
+
/** A header cell as it is compared: trimmed, lower case, without the `*` some exports put on a required column. */
|
|
24
|
+
export declare function headerKey(cell: string): string;
|
|
25
|
+
//# sourceMappingURL=csv.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"csv.d.ts","sourceRoot":"","sources":["../src/csv.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAI3C,MAAM,MAAM,SAAS,GAAG,GAAG,GAAG,GAAG,GAAG,IAAI,CAAC;AAEzC,iFAAiF;AACjF,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CA0B/F;AAED,mFAAmF;AACnF,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,CAQzD;AAED,wFAAwF;AACxF,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,EAAE,CAqDhG;AAED,mHAAmH;AACnH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9C"}
|
package/dist/csv.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The comma-separated values a spreadsheet saves, as RFC 4180 describes them:
|
|
3
|
+
* fields between double quotes may hold the separator, a line break and a
|
|
4
|
+
* doubled quote. Nothing else about the file is assumed.
|
|
5
|
+
*
|
|
6
|
+
* The separator is **read from the header**, which this format requires: the
|
|
7
|
+
* first of `,`, `;` and a tab that the header row contains outside quotes. A
|
|
8
|
+
* spreadsheet saves with a comma or a semicolon depending on the language of
|
|
9
|
+
* the machine, and the header is the one row whose content is known, so this
|
|
10
|
+
* is reading the file rather than guessing at it.
|
|
11
|
+
*/
|
|
12
|
+
import { BooksFileError } from './errors.js';
|
|
13
|
+
const BYTE_ORDER_MARK = 0xfeff;
|
|
14
|
+
/** Bytes to text, in the encoding the caller said, refusing what is not that. */
|
|
15
|
+
export function decode(input, encoding, maxBytes) {
|
|
16
|
+
let text;
|
|
17
|
+
if (typeof input === 'string') {
|
|
18
|
+
if (input.length > maxBytes)
|
|
19
|
+
throw new BooksFileError('too_large', `the file is longer than ${maxBytes} bytes`);
|
|
20
|
+
text = input;
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
if (input.byteLength > maxBytes) {
|
|
24
|
+
throw new BooksFileError('too_large', `the file is ${input.byteLength} bytes, over the limit of ${maxBytes}`);
|
|
25
|
+
}
|
|
26
|
+
if (encoding === 'iso-8859-1') {
|
|
27
|
+
const parts = [];
|
|
28
|
+
for (let i = 0; i < input.length; i += 8192)
|
|
29
|
+
parts.push(String.fromCharCode(...input.subarray(i, i + 8192)));
|
|
30
|
+
text = parts.join('');
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
try {
|
|
34
|
+
text = new TextDecoder('utf-8', { fatal: true }).decode(input);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
throw new BooksFileError('unsupported_encoding', "the bytes are not UTF-8; if the file was saved in Latin-1, say so with { encoding: 'iso-8859-1' } — it is not guessed, because every sequence of bytes is valid Latin-1");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (text.charCodeAt(0) === BYTE_ORDER_MARK)
|
|
42
|
+
text = text.slice(1);
|
|
43
|
+
return text;
|
|
44
|
+
}
|
|
45
|
+
/** The separator of a header line: the first of the three found outside quotes. */
|
|
46
|
+
export function separatorOf(headerLine) {
|
|
47
|
+
let quoted = false;
|
|
48
|
+
for (const char of headerLine) {
|
|
49
|
+
if (char === '"')
|
|
50
|
+
quoted = !quoted;
|
|
51
|
+
else if (!quoted && (char === ',' || char === ';' || char === '\t'))
|
|
52
|
+
return char;
|
|
53
|
+
}
|
|
54
|
+
// A header of one column has no separator; any will do, and a comma is it.
|
|
55
|
+
return ',';
|
|
56
|
+
}
|
|
57
|
+
/** Every row of the file, each an array of fields, with the row number it starts on. */
|
|
58
|
+
export function parseCsv(text, separator) {
|
|
59
|
+
const rows = [];
|
|
60
|
+
let fields = [];
|
|
61
|
+
let field = '';
|
|
62
|
+
let quoted = false;
|
|
63
|
+
let line = 1;
|
|
64
|
+
let startedOn = 1;
|
|
65
|
+
let fieldStarted = false;
|
|
66
|
+
const endRow = () => {
|
|
67
|
+
fields.push(field);
|
|
68
|
+
// A blank line is no row; a row of empty fields is kept and read.
|
|
69
|
+
if (!(fields.length === 1 && fields[0] === ''))
|
|
70
|
+
rows.push({ row: startedOn, fields });
|
|
71
|
+
fields = [];
|
|
72
|
+
field = '';
|
|
73
|
+
fieldStarted = false;
|
|
74
|
+
};
|
|
75
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
76
|
+
const char = text[i];
|
|
77
|
+
if (quoted) {
|
|
78
|
+
if (char === '"') {
|
|
79
|
+
if (text[i + 1] === '"') {
|
|
80
|
+
field += '"';
|
|
81
|
+
i += 1;
|
|
82
|
+
}
|
|
83
|
+
else
|
|
84
|
+
quoted = false;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
if (char === '\n')
|
|
88
|
+
line += 1;
|
|
89
|
+
field += char;
|
|
90
|
+
}
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (char === '"' && !fieldStarted) {
|
|
94
|
+
quoted = true;
|
|
95
|
+
fieldStarted = true;
|
|
96
|
+
}
|
|
97
|
+
else if (char === separator) {
|
|
98
|
+
fields.push(field);
|
|
99
|
+
field = '';
|
|
100
|
+
fieldStarted = false;
|
|
101
|
+
}
|
|
102
|
+
else if (char === '\r' && text[i + 1] === '\n') {
|
|
103
|
+
// The \n that follows ends the row.
|
|
104
|
+
}
|
|
105
|
+
else if (char === '\n' || char === '\r') {
|
|
106
|
+
endRow();
|
|
107
|
+
line += 1;
|
|
108
|
+
startedOn = line;
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
field += char;
|
|
112
|
+
fieldStarted = true;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (quoted)
|
|
116
|
+
throw new BooksFileError('malformed_csv', 'a quoted field is never closed', startedOn);
|
|
117
|
+
if (field !== '' || fields.length > 0)
|
|
118
|
+
endRow();
|
|
119
|
+
return rows;
|
|
120
|
+
}
|
|
121
|
+
/** A header cell as it is compared: trimmed, lower case, without the `*` some exports put on a required column. */
|
|
122
|
+
export function headerKey(cell) {
|
|
123
|
+
return cell.trim().replace(/^\*/, '').trim().toLowerCase();
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=csv.js.map
|
package/dist/csv.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"csv.js","sourceRoot":"","sources":["../src/csv.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Amounts added without a float.
|
|
3
|
+
*
|
|
4
|
+
* An amount is held as a `bigint` of millionths: enough for any currency and
|
|
5
|
+
* any rate a spreadsheet prints, and exact. What leaves is a string with two
|
|
6
|
+
* decimals, and more when the file wrote more that are not zero — dropping a
|
|
7
|
+
* written zero is not rounding, dropping anything else would be, and is never
|
|
8
|
+
* done here.
|
|
9
|
+
*/
|
|
10
|
+
/** Millionths as a decimal string: signed, two decimals, more when they are written. */
|
|
11
|
+
export declare function formatDecimal(value: bigint): string;
|
|
12
|
+
//# sourceMappingURL=decimal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decimal.d.ts","sourceRoot":"","sources":["../src/decimal.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,wFAAwF;AACxF,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMnD"}
|
package/dist/decimal.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Amounts added without a float.
|
|
3
|
+
*
|
|
4
|
+
* An amount is held as a `bigint` of millionths: enough for any currency and
|
|
5
|
+
* any rate a spreadsheet prints, and exact. What leaves is a string with two
|
|
6
|
+
* decimals, and more when the file wrote more that are not zero — dropping a
|
|
7
|
+
* written zero is not rounding, dropping anything else would be, and is never
|
|
8
|
+
* done here.
|
|
9
|
+
*/
|
|
10
|
+
const SCALE = 6;
|
|
11
|
+
/** Millionths as a decimal string: signed, two decimals, more when they are written. */
|
|
12
|
+
export function formatDecimal(value) {
|
|
13
|
+
const negative = value < 0n;
|
|
14
|
+
const digits = (negative ? -value : value).toString().padStart(SCALE + 1, '0');
|
|
15
|
+
const whole = digits.slice(0, -SCALE);
|
|
16
|
+
const fraction = digits.slice(-SCALE).replace(/0+$/, '').padEnd(2, '0');
|
|
17
|
+
return `${negative ? '-' : ''}${whole}.${fraction}`;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=decimal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decimal.js","sourceRoot":"","sources":["../src/decimal.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,KAAK,GAAG,CAAC,CAAC;AAEhB,wFAAwF;AACxF,MAAM,UAAU,aAAa,CAAC,KAAa;IACzC,MAAM,QAAQ,GAAG,KAAK,GAAG,EAAE,CAAC;IAC5B,MAAM,MAAM,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,KAAK,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC;IAC/E,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC;IACtC,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IACxE,OAAO,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,QAAQ,EAAE,CAAC;AACtD,CAAC"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What makes a file unreadable, as opposed to books that read and do not add
|
|
3
|
+
* up. The second comes back in `violations`; the first is thrown, because a
|
|
4
|
+
* file with no header the reader recognises has no row to hang a violation on.
|
|
5
|
+
*/
|
|
6
|
+
export type BooksFileErrorCode =
|
|
7
|
+
/** Larger than `maxBytes`. Refused before a single character is read. */
|
|
8
|
+
'too_large'
|
|
9
|
+
/** Bytes that are not the encoding announced. */
|
|
10
|
+
| 'unsupported_encoding'
|
|
11
|
+
/** Nothing in the file, or a header and nothing under it. */
|
|
12
|
+
| 'empty_file'
|
|
13
|
+
/** A header that lacks a column the format needs, or names one twice. */
|
|
14
|
+
| 'missing_column'
|
|
15
|
+
/** A quote that is never closed. */
|
|
16
|
+
| 'malformed_csv'
|
|
17
|
+
/** A value the format does not allow where it is: a date, an amount. */
|
|
18
|
+
| 'invalid_value';
|
|
19
|
+
export declare class BooksFileError extends Error {
|
|
20
|
+
readonly code: BooksFileErrorCode;
|
|
21
|
+
/** The row the reader stopped on, from 1, when there is one. */
|
|
22
|
+
readonly row: number | null;
|
|
23
|
+
constructor(code: BooksFileErrorCode, message: string, row?: number | null);
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,MAAM,kBAAkB;AAC5B,yEAAyE;AACvE,WAAW;AACb,iDAAiD;GAC/C,sBAAsB;AACxB,6DAA6D;GAC3D,YAAY;AACd,yEAAyE;GACvE,gBAAgB;AAClB,oCAAoC;GAClC,eAAe;AACjB,wEAAwE;GACtE,eAAe,CAAC;AAEpB,qBAAa,cAAe,SAAQ,KAAK;IACvC,QAAQ,CAAC,IAAI,EAAE,kBAAkB,CAAC;IAClC,gEAAgE;IAChE,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;gBAEhB,IAAI,EAAE,kBAAkB,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,GAAE,MAAM,GAAG,IAAW;CAMjF"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export class BooksFileError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
/** The row the reader stopped on, from 1, when there is one. */
|
|
4
|
+
row;
|
|
5
|
+
constructor(code, message, row = null) {
|
|
6
|
+
super(row === null ? message : `row ${row}: ${message}`);
|
|
7
|
+
this.name = 'BooksFileError';
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.row = row;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAmBA,MAAM,OAAO,cAAe,SAAQ,KAAK;IAC9B,IAAI,CAAqB;IAClC,gEAAgE;IACvD,GAAG,CAAgB;IAE5B,YAAY,IAAwB,EAAE,OAAe,EAAE,MAAqB,IAAI;QAC9E,KAAK,CAAC,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,GAAG,KAAK,OAAO,EAAE,CAAC,CAAC;QACzD,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;QAC7B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;IACjB,CAAC;CACF"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A transaction journal saved as CSV: the report a ledger prints of every
|
|
3
|
+
* transaction it posted, one after the other, each line of a transaction on a
|
|
4
|
+
* row of its own. Such a ledger exports its reports to a spreadsheet and not
|
|
5
|
+
* to CSV; the spreadsheet is saved as CSV, and that file is what this reads —
|
|
6
|
+
* beside it, the list of accounts, exported and saved the same way.
|
|
7
|
+
*
|
|
8
|
+
* What sets this report apart from a journal report that numbers its journals
|
|
9
|
+
* is that **nothing on a line names the transaction it belongs to**: a
|
|
10
|
+
* transaction is the rows that follow each other under one date, one type and
|
|
11
|
+
* one number. So the report is read in order:
|
|
12
|
+
*
|
|
13
|
+
* - a row with an account and a date **starts** a transaction — unless it
|
|
14
|
+
* repeats the date, the type and the number of the transaction just before,
|
|
15
|
+
* which some layouts print on every line;
|
|
16
|
+
* - a row with an account and no date **continues** the transaction above it;
|
|
17
|
+
* - a row with no account — the total of a transaction, a blank row, the total
|
|
18
|
+
* of the report, a footer — **ends** it.
|
|
19
|
+
*
|
|
20
|
+
* The header is the first row that names a date, an account, a debit and a
|
|
21
|
+
* credit; the title rows above it are passed over. Columns are found by their
|
|
22
|
+
* names, never by their position.
|
|
23
|
+
*
|
|
24
|
+
* A date written in digits in an order nobody can read off the digits —
|
|
25
|
+
* `03/04/2026` — is read in the order the caller names, and refused without
|
|
26
|
+
* one: which order a report uses follows the region of the company, and the
|
|
27
|
+
* file does not say it.
|
|
28
|
+
*
|
|
29
|
+
* It depends on nothing, reads no database and knows no chart of accounts.
|
|
30
|
+
*/
|
|
31
|
+
import type { ImportedBooks, ReadOptions } from './types.js';
|
|
32
|
+
export * from './types.js';
|
|
33
|
+
export { BooksFileError, type BooksFileErrorCode } from './errors.js';
|
|
34
|
+
/** The order of day, month and year in a date written only in digits. */
|
|
35
|
+
export type DateOrder = 'dmy' | 'mdy' | 'ymd';
|
|
36
|
+
export interface TransactionJournalOptions extends ReadOptions {
|
|
37
|
+
/** How a date written only in digits is to be read. Required when the report writes one; never guessed. */
|
|
38
|
+
dateOrder?: DateOrder;
|
|
39
|
+
}
|
|
40
|
+
/** The columns of the report, each under the names it may carry. */
|
|
41
|
+
export declare const JOURNAL_COLUMNS: {
|
|
42
|
+
readonly date: readonly ["date", "transaction date"];
|
|
43
|
+
readonly type: readonly ["transaction type", "type"];
|
|
44
|
+
readonly number: readonly ["num", "no.", "no", "number", "ref no."];
|
|
45
|
+
readonly name: readonly ["name"];
|
|
46
|
+
readonly customer: readonly ["customer full name", "customer"];
|
|
47
|
+
readonly vendor: readonly ["vendor", "supplier"];
|
|
48
|
+
readonly employee: readonly ["employee"];
|
|
49
|
+
readonly memo: readonly ["memo/description", "memo", "description"];
|
|
50
|
+
readonly accountNumber: readonly ["account #", "account number", "account no."];
|
|
51
|
+
readonly account: readonly ["account", "account full name", "account name"];
|
|
52
|
+
readonly debit: readonly ["debit"];
|
|
53
|
+
readonly credit: readonly ["credit"];
|
|
54
|
+
};
|
|
55
|
+
/** The columns of the list of accounts. */
|
|
56
|
+
export declare const ACCOUNT_LIST_COLUMNS: {
|
|
57
|
+
readonly account: readonly ["account", "full name", "account full name", "account name", "name"];
|
|
58
|
+
readonly number: readonly ["account #", "account number", "number"];
|
|
59
|
+
readonly type: readonly ["type", "account type"];
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Reads the files — the journal, and optionally the list of accounts — in any
|
|
63
|
+
* order, each recognised by its header.
|
|
64
|
+
*/
|
|
65
|
+
export declare function readTransactionJournal(inputs: readonly (string | Uint8Array)[], options?: TransactionJournalOptions): ImportedBooks;
|
|
66
|
+
/**
|
|
67
|
+
* A date, or null for a cell that is not one. A cell that looks like a date
|
|
68
|
+
* and is not a day of the calendar is refused, never passed over.
|
|
69
|
+
*/
|
|
70
|
+
export declare function readDate(value: string, order: DateOrder | undefined, row: number): string | null;
|
|
71
|
+
/**
|
|
72
|
+
* An amount as a spreadsheet displays it once saved as CSV. In a file
|
|
73
|
+
* separated by commas the decimal mark is a point and a comma can only group
|
|
74
|
+
* thousands, three digits at a time (`1,234.50`). In a file separated by a
|
|
75
|
+
* semicolon or a tab the decimal mark is whichever of the two the number
|
|
76
|
+
* carries once, and thousands may only be grouped by a space. A negative is a
|
|
77
|
+
* minus or brackets. Anything else — a currency symbol included — is refused.
|
|
78
|
+
*/
|
|
79
|
+
export declare function amount(raw: string, separator: string, row: number): bigint;
|
|
80
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAKH,OAAO,KAAK,EAEV,aAAa,EAIb,WAAW,EAEZ,MAAM,YAAY,CAAC;AAEpB,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,cAAc,EAAE,KAAK,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAItE,yEAAyE;AACzE,MAAM,MAAM,SAAS,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC;AAE9C,MAAM,WAAW,yBAA0B,SAAQ,WAAW;IAC5D,2GAA2G;IAC3G,SAAS,CAAC,EAAE,SAAS,CAAC;CACvB;AAED,oEAAoE;AACpE,eAAO,MAAM,eAAe;;;;;;;;;;;;;CAalB,CAAC;AAEX,2CAA2C;AAC3C,eAAO,MAAM,oBAAoB;;;;CAIvB,CAAC;AAwCX;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,CAAC,MAAM,GAAG,UAAU,CAAC,EAAE,EAAE,OAAO,GAAE,yBAA8B,GAAG,aAAa,CAoKvI;AAID;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,SAAS,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAoChG;AAED;;;;;;;GAOG;AACH,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CA0B1E"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A transaction journal saved as CSV: the report a ledger prints of every
|
|
3
|
+
* transaction it posted, one after the other, each line of a transaction on a
|
|
4
|
+
* row of its own. Such a ledger exports its reports to a spreadsheet and not
|
|
5
|
+
* to CSV; the spreadsheet is saved as CSV, and that file is what this reads —
|
|
6
|
+
* beside it, the list of accounts, exported and saved the same way.
|
|
7
|
+
*
|
|
8
|
+
* What sets this report apart from a journal report that numbers its journals
|
|
9
|
+
* is that **nothing on a line names the transaction it belongs to**: a
|
|
10
|
+
* transaction is the rows that follow each other under one date, one type and
|
|
11
|
+
* one number. So the report is read in order:
|
|
12
|
+
*
|
|
13
|
+
* - a row with an account and a date **starts** a transaction — unless it
|
|
14
|
+
* repeats the date, the type and the number of the transaction just before,
|
|
15
|
+
* which some layouts print on every line;
|
|
16
|
+
* - a row with an account and no date **continues** the transaction above it;
|
|
17
|
+
* - a row with no account — the total of a transaction, a blank row, the total
|
|
18
|
+
* of the report, a footer — **ends** it.
|
|
19
|
+
*
|
|
20
|
+
* The header is the first row that names a date, an account, a debit and a
|
|
21
|
+
* credit; the title rows above it are passed over. Columns are found by their
|
|
22
|
+
* names, never by their position.
|
|
23
|
+
*
|
|
24
|
+
* A date written in digits in an order nobody can read off the digits —
|
|
25
|
+
* `03/04/2026` — is read in the order the caller names, and refused without
|
|
26
|
+
* one: which order a report uses follows the region of the company, and the
|
|
27
|
+
* file does not say it.
|
|
28
|
+
*
|
|
29
|
+
* It depends on nothing, reads no database and knows no chart of accounts.
|
|
30
|
+
*/
|
|
31
|
+
import { decode, headerKey, parseCsv, separatorOf } from './csv.js';
|
|
32
|
+
import { formatDecimal } from './decimal.js';
|
|
33
|
+
import { BooksFileError } from './errors.js';
|
|
34
|
+
export * from './types.js';
|
|
35
|
+
export { BooksFileError } from './errors.js';
|
|
36
|
+
const DEFAULTS = { encoding: 'utf-8', maxBytes: 256 * 1024 * 1024 };
|
|
37
|
+
/** The columns of the report, each under the names it may carry. */
|
|
38
|
+
export const JOURNAL_COLUMNS = {
|
|
39
|
+
date: ['date', 'transaction date'],
|
|
40
|
+
type: ['transaction type', 'type'],
|
|
41
|
+
number: ['num', 'no.', 'no', 'number', 'ref no.'],
|
|
42
|
+
name: ['name'],
|
|
43
|
+
customer: ['customer full name', 'customer'],
|
|
44
|
+
vendor: ['vendor', 'supplier'],
|
|
45
|
+
employee: ['employee'],
|
|
46
|
+
memo: ['memo/description', 'memo', 'description'],
|
|
47
|
+
accountNumber: ['account #', 'account number', 'account no.'],
|
|
48
|
+
account: ['account', 'account full name', 'account name'],
|
|
49
|
+
debit: ['debit'],
|
|
50
|
+
credit: ['credit'],
|
|
51
|
+
};
|
|
52
|
+
/** The columns of the list of accounts. */
|
|
53
|
+
export const ACCOUNT_LIST_COLUMNS = {
|
|
54
|
+
account: ['account', 'full name', 'account full name', 'account name', 'name'],
|
|
55
|
+
number: ['account #', 'account number', 'number'],
|
|
56
|
+
type: ['type', 'account type'],
|
|
57
|
+
};
|
|
58
|
+
function locate(keys, spec) {
|
|
59
|
+
const at = {};
|
|
60
|
+
for (const [column, names] of Object.entries(spec)) {
|
|
61
|
+
const found = keys.findIndex((key) => names.includes(key));
|
|
62
|
+
if (found !== -1)
|
|
63
|
+
at[column] = found;
|
|
64
|
+
}
|
|
65
|
+
return at;
|
|
66
|
+
}
|
|
67
|
+
const has = (keys, names) => keys.some((key) => names.includes(key));
|
|
68
|
+
function isJournalHeader(keys) {
|
|
69
|
+
return (has(keys, JOURNAL_COLUMNS.date) &&
|
|
70
|
+
(has(keys, JOURNAL_COLUMNS.account) || has(keys, JOURNAL_COLUMNS.accountNumber)) &&
|
|
71
|
+
has(keys, JOURNAL_COLUMNS.debit) &&
|
|
72
|
+
has(keys, JOURNAL_COLUMNS.credit));
|
|
73
|
+
}
|
|
74
|
+
function isAccountListHeader(keys) {
|
|
75
|
+
return has(keys, ACCOUNT_LIST_COLUMNS.account) && has(keys, ACCOUNT_LIST_COLUMNS.type) && !has(keys, JOURNAL_COLUMNS.debit);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Reads the files — the journal, and optionally the list of accounts — in any
|
|
79
|
+
* order, each recognised by its header.
|
|
80
|
+
*/
|
|
81
|
+
export function readTransactionJournal(inputs, options = {}) {
|
|
82
|
+
const sheets = inputs.map((input, index) => {
|
|
83
|
+
const text = decode(input, options.encoding ?? DEFAULTS.encoding, options.maxBytes ?? DEFAULTS.maxBytes);
|
|
84
|
+
// The title rows above the header may hold no separator at all, or one
|
|
85
|
+
// inside quotes only ("January 1-31, 2025"), so the separator is read from
|
|
86
|
+
// the first line that holds one outside quotes.
|
|
87
|
+
const firstWithSeparator = text.split(/\r?\n/).find((line) => /[,;\t]/.test(line.replace(/"[^"]*"/g, ''))) ?? '';
|
|
88
|
+
const separator = separatorOf(firstWithSeparator);
|
|
89
|
+
return { file: index + 1, separator, rows: parseCsv(text, separator) };
|
|
90
|
+
});
|
|
91
|
+
const violations = [];
|
|
92
|
+
const accounts = new Map();
|
|
93
|
+
const contacts = new Map();
|
|
94
|
+
/** The list of accounts, by the name a line displays: its number where the list gives one, and its type. */
|
|
95
|
+
const listed = new Map();
|
|
96
|
+
const journals = [];
|
|
97
|
+
for (const sheet of sheets) {
|
|
98
|
+
if (sheet.rows.length === 0)
|
|
99
|
+
throw new BooksFileError('empty_file', `file ${sheet.file} is empty`);
|
|
100
|
+
const journalAt = sheet.rows.findIndex((r) => isJournalHeader(r.fields.map(headerKey)));
|
|
101
|
+
if (journalAt !== -1) {
|
|
102
|
+
journals.push({ sheet, headerAt: journalAt });
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const listAt = sheet.rows.findIndex((r) => isAccountListHeader(r.fields.map(headerKey)));
|
|
106
|
+
if (listAt === -1) {
|
|
107
|
+
throw new BooksFileError('missing_column', `file ${sheet.file} is neither a journal (a header with Date, Account, Debit and Credit) nor a list of accounts (Account, Type)`);
|
|
108
|
+
}
|
|
109
|
+
const at = locate(sheet.rows[listAt].fields.map(headerKey), ACCOUNT_LIST_COLUMNS);
|
|
110
|
+
for (const { fields } of sheet.rows.slice(listAt + 1)) {
|
|
111
|
+
const cell = (column) => {
|
|
112
|
+
const index = at[column];
|
|
113
|
+
return index === undefined ? '' : (fields[index] ?? '').trim();
|
|
114
|
+
};
|
|
115
|
+
const name = cell('account');
|
|
116
|
+
const type = cell('type');
|
|
117
|
+
// A row with a name and no type is a heading or a footer of the list.
|
|
118
|
+
if (name === '' || type === '')
|
|
119
|
+
continue;
|
|
120
|
+
listed.set(name, { number: cell('number') === '' ? null : cell('number'), type });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (journals.length === 0) {
|
|
124
|
+
throw new BooksFileError('missing_column', 'no file is a journal: one needs a header row with Date, Account, Debit and Credit');
|
|
125
|
+
}
|
|
126
|
+
const entries = [];
|
|
127
|
+
for (const { sheet, headerAt } of journals) {
|
|
128
|
+
const header = sheet.rows[headerAt];
|
|
129
|
+
const at = locate(header.fields.map(headerKey), JOURNAL_COLUMNS);
|
|
130
|
+
let open = null;
|
|
131
|
+
for (const { row, fields } of sheet.rows.slice(headerAt + 1)) {
|
|
132
|
+
const cell = (column) => {
|
|
133
|
+
const index = at[column];
|
|
134
|
+
return index === undefined ? '' : (fields[index] ?? '').trim();
|
|
135
|
+
};
|
|
136
|
+
const accountName = cell('account');
|
|
137
|
+
const accountNumber = cell('accountNumber');
|
|
138
|
+
if (accountName === '' && accountNumber === '') {
|
|
139
|
+
// A total, a blank row, the total of the report, a footer: the
|
|
140
|
+
// transaction above is over.
|
|
141
|
+
open = null;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const date = readDate(cell('date'), options.dateOrder, row);
|
|
145
|
+
if (date === null && cell('date') !== '') {
|
|
146
|
+
violations.push({ rule: 'required', message: `"${cell('date')}" stands where the date of a line is, and the line was left out`, row });
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const type = cell('type');
|
|
150
|
+
const number = cell('number');
|
|
151
|
+
if (date !== null) {
|
|
152
|
+
const key = `${date}\u0000${type}\u0000${number}`;
|
|
153
|
+
if (open === null || open.key !== key) {
|
|
154
|
+
const memo = cell('memo');
|
|
155
|
+
const entry = {
|
|
156
|
+
journal: type === '' ? null : type,
|
|
157
|
+
journalName: type === '' ? null : type,
|
|
158
|
+
number: number === '' ? null : number,
|
|
159
|
+
date,
|
|
160
|
+
reference: number === '' ? null : number,
|
|
161
|
+
description: memo === '' ? null : memo,
|
|
162
|
+
lines: [],
|
|
163
|
+
row,
|
|
164
|
+
debits: 0n,
|
|
165
|
+
credits: 0n,
|
|
166
|
+
};
|
|
167
|
+
entries.push(entry);
|
|
168
|
+
open = { key, entry };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
else if (open === null) {
|
|
172
|
+
violations.push({ rule: 'no_transaction', message: 'a line with no date follows no dated line of the same transaction, and was left out', row });
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const entry = open.entry;
|
|
176
|
+
let debit = amount(cell('debit'), sheet.separator, row);
|
|
177
|
+
let credit = amount(cell('credit'), sheet.separator, row);
|
|
178
|
+
if (debit !== 0n && credit !== 0n)
|
|
179
|
+
violations.push({ rule: 'one_side', message: 'a line carries both a debit and a credit', row });
|
|
180
|
+
if (debit < 0n) {
|
|
181
|
+
credit -= debit;
|
|
182
|
+
debit = 0n;
|
|
183
|
+
}
|
|
184
|
+
if (credit < 0n) {
|
|
185
|
+
debit -= credit;
|
|
186
|
+
credit = 0n;
|
|
187
|
+
}
|
|
188
|
+
const onList = listed.get(accountName);
|
|
189
|
+
const code = accountNumber !== '' ? accountNumber : (onList?.number ?? accountName);
|
|
190
|
+
if (!accounts.has(code)) {
|
|
191
|
+
accounts.set(code, { code, name: accountName === '' ? null : accountName, type: onList?.type ?? null });
|
|
192
|
+
}
|
|
193
|
+
const party = [cell('name'), cell('customer'), cell('vendor'), cell('employee')].find((value) => value !== '') ?? '';
|
|
194
|
+
if (party !== '' && !contacts.has(party)) {
|
|
195
|
+
contacts.set(party, { code: party, name: party, vatNumber: null, registrationNumber: null, email: null, country: null });
|
|
196
|
+
}
|
|
197
|
+
const label = cell('memo');
|
|
198
|
+
const line = {
|
|
199
|
+
account: code,
|
|
200
|
+
contact: party === '' ? null : party,
|
|
201
|
+
label: label === '' ? null : label,
|
|
202
|
+
debit: formatDecimal(debit),
|
|
203
|
+
credit: formatDecimal(credit),
|
|
204
|
+
currency: null,
|
|
205
|
+
amountCurrency: null,
|
|
206
|
+
dueDate: null,
|
|
207
|
+
matching: null,
|
|
208
|
+
};
|
|
209
|
+
entry.lines.push(line);
|
|
210
|
+
entry.debits += debit;
|
|
211
|
+
entry.credits += credit;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const read = [];
|
|
215
|
+
for (const { debits, credits, ...entry } of entries) {
|
|
216
|
+
if (debits !== credits) {
|
|
217
|
+
const name = [entry.journal, entry.number].filter((part) => part !== null).join(' ') || 'a transaction';
|
|
218
|
+
violations.push({
|
|
219
|
+
rule: 'balance',
|
|
220
|
+
message: `${name} of ${entry.date} has debit ${formatDecimal(debits)} and credit ${formatDecimal(credits)}`,
|
|
221
|
+
row: entry.row,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
read.push(entry);
|
|
225
|
+
}
|
|
226
|
+
if (read.length === 0)
|
|
227
|
+
throw new BooksFileError('empty_file', 'the journal holds no dated line');
|
|
228
|
+
return {
|
|
229
|
+
format: 'transaction-journal',
|
|
230
|
+
// A report prints amounts in the currency of the company and does not say which.
|
|
231
|
+
currency: null,
|
|
232
|
+
accounts: [...accounts.values()],
|
|
233
|
+
contacts: [...contacts.values()],
|
|
234
|
+
entries: read,
|
|
235
|
+
opening: [],
|
|
236
|
+
violations,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
const MONTHS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
|
|
240
|
+
/**
|
|
241
|
+
* A date, or null for a cell that is not one. A cell that looks like a date
|
|
242
|
+
* and is not a day of the calendar is refused, never passed over.
|
|
243
|
+
*/
|
|
244
|
+
export function readDate(value, order, row) {
|
|
245
|
+
let y;
|
|
246
|
+
let m;
|
|
247
|
+
let d;
|
|
248
|
+
let match;
|
|
249
|
+
if (value === '')
|
|
250
|
+
return null;
|
|
251
|
+
if ((match = /^(\d{4})-(\d{2})-(\d{2})(?:[ T].*)?$/.exec(value)) !== null) {
|
|
252
|
+
[y, m, d] = [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
253
|
+
}
|
|
254
|
+
else if ((match = /^(\d{1,2})[ -]([A-Za-z]{3,9})[ -](\d{4})$/.exec(value)) !== null) {
|
|
255
|
+
const month = MONTHS.indexOf(match[2].slice(0, 3).toLowerCase());
|
|
256
|
+
if (month === -1)
|
|
257
|
+
return null;
|
|
258
|
+
[y, m, d] = [Number(match[3]), month + 1, Number(match[1])];
|
|
259
|
+
}
|
|
260
|
+
else if ((match = /^([A-Za-z]{3,9}) (\d{1,2}),? (\d{4})$/.exec(value)) !== null) {
|
|
261
|
+
const month = MONTHS.indexOf(match[1].slice(0, 3).toLowerCase());
|
|
262
|
+
if (month === -1)
|
|
263
|
+
return null;
|
|
264
|
+
[y, m, d] = [Number(match[3]), month + 1, Number(match[2])];
|
|
265
|
+
}
|
|
266
|
+
else if ((match = /^(\d{1,4})[/.-](\d{1,2})[/.-](\d{1,4})$/.exec(value)) !== null) {
|
|
267
|
+
if (order === undefined) {
|
|
268
|
+
throw new BooksFileError('invalid_value', `"${value}" is a date written in digits, and nothing in the file says in which order; name it (dateOrder: 'dmy', 'mdy' or 'ymd')`, row);
|
|
269
|
+
}
|
|
270
|
+
const [a, b, c] = [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
271
|
+
[y, m, d] = order === 'dmy' ? [c, b, a] : order === 'mdy' ? [c, a, b] : [a, b, c];
|
|
272
|
+
if (y < 100)
|
|
273
|
+
y += 2000;
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
const iso = `${String(y).padStart(4, '0')}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
|
|
279
|
+
const parsed = new Date(`${iso}T00:00:00Z`);
|
|
280
|
+
if (Number.isNaN(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== iso) {
|
|
281
|
+
throw new BooksFileError('invalid_value', `"${value}" is not a date of the calendar`, row);
|
|
282
|
+
}
|
|
283
|
+
return iso;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* An amount as a spreadsheet displays it once saved as CSV. In a file
|
|
287
|
+
* separated by commas the decimal mark is a point and a comma can only group
|
|
288
|
+
* thousands, three digits at a time (`1,234.50`). In a file separated by a
|
|
289
|
+
* semicolon or a tab the decimal mark is whichever of the two the number
|
|
290
|
+
* carries once, and thousands may only be grouped by a space. A negative is a
|
|
291
|
+
* minus or brackets. Anything else — a currency symbol included — is refused.
|
|
292
|
+
*/
|
|
293
|
+
export function amount(raw, separator, row) {
|
|
294
|
+
let value = raw.replace(/[ ]/g, ' ').trim();
|
|
295
|
+
if (value === '')
|
|
296
|
+
return 0n;
|
|
297
|
+
let negative = false;
|
|
298
|
+
if (value.startsWith('(') && value.endsWith(')')) {
|
|
299
|
+
negative = true;
|
|
300
|
+
value = value.slice(1, -1).trim();
|
|
301
|
+
}
|
|
302
|
+
if (value.startsWith('-')) {
|
|
303
|
+
negative = !negative;
|
|
304
|
+
value = value.slice(1).trim();
|
|
305
|
+
}
|
|
306
|
+
let normalised = null;
|
|
307
|
+
if (separator === ',') {
|
|
308
|
+
if (/^\d{1,3}(,\d{3})+(\.\d+)?$/.test(value) || /^\d+(\.\d+)?$/.test(value))
|
|
309
|
+
normalised = value.replace(/,/g, '');
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
const grouped = value.replace(/ (?=\d{3}(\D|$))/g, '');
|
|
313
|
+
if (/^\d+([.,]\d+)?$/.test(grouped))
|
|
314
|
+
normalised = grouped.replace(',', '.');
|
|
315
|
+
}
|
|
316
|
+
if (normalised === null) {
|
|
317
|
+
throw new BooksFileError('invalid_value', `"${raw}" is not an amount this reader accepts`, row);
|
|
318
|
+
}
|
|
319
|
+
const [whole, fraction = ''] = normalised.split('.');
|
|
320
|
+
if (fraction.length > 6)
|
|
321
|
+
throw new BooksFileError('invalid_value', `"${raw}" has more than six decimals`, row);
|
|
322
|
+
const units = BigInt(whole) * 1000000n + BigInt(fraction.padEnd(6, '0') || '0');
|
|
323
|
+
return negative ? -units : units;
|
|
324
|
+
}
|
|
325
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a set of books is once it has been read out of the software that kept
|
|
3
|
+
* them: accounts, parties, entries and an opening balance. The shape is
|
|
4
|
+
* declared here and imported from nowhere — it names no table and no ledger.
|
|
5
|
+
*
|
|
6
|
+
* It is, field for field, the shape every reader of an accounting export in
|
|
7
|
+
* this repository declares for itself (`@ekwo-ai/fec`, and the readers beside
|
|
8
|
+
* this one), so whatever takes one takes the others. It is not a shared
|
|
9
|
+
* abstraction: none imports another, and a reader that has nothing to say for
|
|
10
|
+
* a part of it returns that part empty.
|
|
11
|
+
*
|
|
12
|
+
* Every amount is a **decimal string**, never negative: a line carries a debit
|
|
13
|
+
* or a credit, and the side is the sign. No figure passes through a `number`.
|
|
14
|
+
*/
|
|
15
|
+
/** ISO 8601 calendar date, `YYYY-MM-DD`. */
|
|
16
|
+
export type IsoDate = string;
|
|
17
|
+
/** A fixed-point number, `1234.50`, never negative. */
|
|
18
|
+
export type Decimal = string;
|
|
19
|
+
/** One account of the chart the books were kept on, in the words of that software. */
|
|
20
|
+
export interface ImportedAccount {
|
|
21
|
+
code: string;
|
|
22
|
+
name: string | null;
|
|
23
|
+
/** What the source calls its kind, verbatim. Null when it says nothing. */
|
|
24
|
+
type: string | null;
|
|
25
|
+
}
|
|
26
|
+
/** A customer, a supplier or anybody else a line names. */
|
|
27
|
+
export interface ImportedContact {
|
|
28
|
+
/** How the source identifies the party on a line: its code, or its name where it has no code. */
|
|
29
|
+
code: string;
|
|
30
|
+
name: string;
|
|
31
|
+
vatNumber: string | null;
|
|
32
|
+
registrationNumber: string | null;
|
|
33
|
+
email: string | null;
|
|
34
|
+
/** ISO 3166-1 alpha-2, when the source writes one. Never inferred. */
|
|
35
|
+
country: string | null;
|
|
36
|
+
}
|
|
37
|
+
export interface ImportedLine {
|
|
38
|
+
/** The account code in the source's chart. */
|
|
39
|
+
account: string;
|
|
40
|
+
/** The `code` of an {@link ImportedContact}, or null. */
|
|
41
|
+
contact: string | null;
|
|
42
|
+
label: string | null;
|
|
43
|
+
debit: Decimal;
|
|
44
|
+
credit: Decimal;
|
|
45
|
+
/** A foreign currency the line was in, when the source says so. */
|
|
46
|
+
currency: string | null;
|
|
47
|
+
/** The amount in that currency, never negative. */
|
|
48
|
+
amountCurrency: Decimal | null;
|
|
49
|
+
dueDate: IsoDate | null;
|
|
50
|
+
/** The reconciliation mark the source put on the line, as written. */
|
|
51
|
+
matching: string | null;
|
|
52
|
+
}
|
|
53
|
+
export interface ImportedEntry {
|
|
54
|
+
/** The journal code in the source, or null when it has none. */
|
|
55
|
+
journal: string | null;
|
|
56
|
+
journalName: string | null;
|
|
57
|
+
/** The entry's number in the source. */
|
|
58
|
+
number: string | null;
|
|
59
|
+
date: IsoDate;
|
|
60
|
+
reference: string | null;
|
|
61
|
+
description: string | null;
|
|
62
|
+
lines: ImportedLine[];
|
|
63
|
+
/** The row of the file the entry starts on, from 1, for a message. */
|
|
64
|
+
row: number | null;
|
|
65
|
+
}
|
|
66
|
+
export interface Violation {
|
|
67
|
+
rule: string;
|
|
68
|
+
message: string;
|
|
69
|
+
/** The row of the file, from 1, or null for a rule about the whole file. */
|
|
70
|
+
row: number | null;
|
|
71
|
+
}
|
|
72
|
+
export interface ImportedBooks {
|
|
73
|
+
/** Which reader: `transaction-journal` here. */
|
|
74
|
+
format: string;
|
|
75
|
+
/** The currency the file says its amounts are in. Null when it says none: never guessed. */
|
|
76
|
+
currency: string | null;
|
|
77
|
+
accounts: ImportedAccount[];
|
|
78
|
+
contacts: ImportedContact[];
|
|
79
|
+
entries: ImportedEntry[];
|
|
80
|
+
/** A trial balance: one line per account, to become the opening entry of a year. */
|
|
81
|
+
opening: ImportedLine[];
|
|
82
|
+
violations: Violation[];
|
|
83
|
+
}
|
|
84
|
+
export type Encoding = 'utf-8' | 'iso-8859-1';
|
|
85
|
+
export interface ReadOptions {
|
|
86
|
+
/**
|
|
87
|
+
* How bytes are decoded. Default `utf-8`, fatally: bytes that are not UTF-8
|
|
88
|
+
* are refused by name, never read as something else. A spreadsheet that
|
|
89
|
+
* saves in Latin-1 is said to be so, because every sequence of bytes is
|
|
90
|
+
* valid Latin-1 and a guess would always succeed.
|
|
91
|
+
*/
|
|
92
|
+
encoding?: Encoding;
|
|
93
|
+
/** Refused beyond this many bytes, before anything is read. Default 256 MiB. */
|
|
94
|
+
maxBytes?: number;
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,4CAA4C;AAC5C,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC;AAE7B,uDAAuD;AACvD,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC;AAE7B,sFAAsF;AACtF,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,2EAA2E;IAC3E,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB;AAED,2DAA2D;AAC3D,MAAM,WAAW,eAAe;IAC9B,iGAAiG;IACjG,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,sEAAsE;IACtE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,MAAM,WAAW,YAAY;IAC3B,8CAA8C;IAC9C,OAAO,EAAE,MAAM,CAAC;IAChB,yDAAyD;IACzD,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,OAAO,CAAC;IAChB,mEAAmE;IACnE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,mDAAmD;IACnD,cAAc,EAAE,OAAO,GAAG,IAAI,CAAC;IAC/B,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACxB,sEAAsE;IACtE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,aAAa;IAC5B,gEAAgE;IAChE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,wCAAwC;IACxC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,EAAE,OAAO,CAAC;IACd,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,sEAAsE;IACtE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CACpB;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,gDAAgD;IAChD,MAAM,EAAE,MAAM,CAAC;IACf,4FAA4F;IAC5F,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,oFAAoF;IACpF,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,UAAU,EAAE,SAAS,EAAE,CAAC;CACzB;AAED,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,YAAY,CAAC;AAE9C,MAAM,WAAW,WAAW;IAC1B;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a set of books is once it has been read out of the software that kept
|
|
3
|
+
* them: accounts, parties, entries and an opening balance. The shape is
|
|
4
|
+
* declared here and imported from nowhere — it names no table and no ledger.
|
|
5
|
+
*
|
|
6
|
+
* It is, field for field, the shape every reader of an accounting export in
|
|
7
|
+
* this repository declares for itself (`@ekwo-ai/fec`, and the readers beside
|
|
8
|
+
* this one), so whatever takes one takes the others. It is not a shared
|
|
9
|
+
* abstraction: none imports another, and a reader that has nothing to say for
|
|
10
|
+
* a part of it returns that part empty.
|
|
11
|
+
*
|
|
12
|
+
* Every amount is a **decimal string**, never negative: a line carries a debit
|
|
13
|
+
* or a credit, and the side is the sign. No figure passes through a `number`.
|
|
14
|
+
*/
|
|
15
|
+
export {};
|
|
16
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG"}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ekwo-ai/transaction-journal",
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "Reads a transaction journal saved as CSV — each transaction a run of rows under one date, type and number, closed by its total — with the list of accounts exported beside it, into plain objects in TypeScript: amounts as decimal strings, and what does not add up. Zero dependencies.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"transaction-journal",
|
|
7
|
+
"journal",
|
|
8
|
+
"general-ledger",
|
|
9
|
+
"csv",
|
|
10
|
+
"accounting",
|
|
11
|
+
"bookkeeping",
|
|
12
|
+
"migration",
|
|
13
|
+
"import",
|
|
14
|
+
"parser"
|
|
15
|
+
],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"author": "Ekwo AI",
|
|
18
|
+
"homepage": "https://github.com/Ekwo-ai/ekwo-os/tree/main/packages/formats/transaction-journal#readme",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/Ekwo-ai/ekwo-os.git",
|
|
22
|
+
"directory": "packages/formats/transaction-journal"
|
|
23
|
+
},
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/Ekwo-ai/ekwo-os/issues"
|
|
26
|
+
},
|
|
27
|
+
"type": "module",
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"README.md",
|
|
35
|
+
"LICENSE"
|
|
36
|
+
],
|
|
37
|
+
"main": "./dist/index.js",
|
|
38
|
+
"types": "./dist/index.d.ts",
|
|
39
|
+
"exports": {
|
|
40
|
+
".": {
|
|
41
|
+
"types": "./dist/index.d.ts",
|
|
42
|
+
"import": "./dist/index.js"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsc -p tsconfig.build.json",
|
|
47
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
48
|
+
"test": "vitest run"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/node": "^24.0.0",
|
|
52
|
+
"typescript": "^5.9.0",
|
|
53
|
+
"vitest": "^3.2.0"
|
|
54
|
+
}
|
|
55
|
+
}
|