tallyd 0.0.0-stage → 0.1.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 +1493 -2
- package/dist/accounts/chart.d.ts +96 -0
- package/dist/accounts/chart.js +244 -0
- package/dist/accounts/index.d.ts +3 -0
- package/dist/accounts/index.js +3 -0
- package/dist/accounts/standard.d.ts +12 -0
- package/dist/accounts/standard.js +95 -0
- package/dist/accounts/types.d.ts +20 -0
- package/dist/accounts/types.js +40 -0
- package/dist/cli/args.d.ts +45 -0
- package/dist/cli/args.js +147 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +2 -0
- package/dist/cli/main.d.ts +6 -0
- package/dist/cli/main.js +25 -0
- package/dist/cli/run.d.ts +34 -0
- package/dist/cli/run.js +1660 -0
- package/dist/dashboard/charts.d.ts +38 -0
- package/dist/dashboard/charts.js +173 -0
- package/dist/dashboard/index.d.ts +3 -0
- package/dist/dashboard/index.js +3 -0
- package/dist/dashboard/model.d.ts +137 -0
- package/dist/dashboard/model.js +190 -0
- package/dist/dashboard/render.d.ts +23 -0
- package/dist/dashboard/render.js +231 -0
- package/dist/dashboard/script.d.ts +28 -0
- package/dist/dashboard/script.js +704 -0
- package/dist/dashboard/styles.d.ts +14 -0
- package/dist/dashboard/styles.js +726 -0
- package/dist/demo/disposal.d.ts +80 -0
- package/dist/demo/disposal.js +215 -0
- package/dist/demo/evidence.d.ts +153 -0
- package/dist/demo/evidence.js +348 -0
- package/dist/demo/foreign.d.ts +29 -0
- package/dist/demo/foreign.js +113 -0
- package/dist/demo/foreignReport.d.ts +10 -0
- package/dist/demo/foreignReport.js +89 -0
- package/dist/demo/generator.d.ts +108 -0
- package/dist/demo/generator.js +445 -0
- package/dist/demo/group.d.ts +70 -0
- package/dist/demo/group.js +195 -0
- package/dist/demo/groupPeriods.d.ts +27 -0
- package/dist/demo/groupPeriods.js +125 -0
- package/dist/demo/groupReport.d.ts +13 -0
- package/dist/demo/groupReport.js +87 -0
- package/dist/demo/ingest.d.ts +6 -0
- package/dist/demo/ingest.js +61 -0
- package/dist/demo/main.d.ts +6 -0
- package/dist/demo/main.js +7 -0
- package/dist/demo/mainDisposal.d.ts +1 -0
- package/dist/demo/mainDisposal.js +2 -0
- package/dist/demo/mainEvidence.d.ts +8 -0
- package/dist/demo/mainEvidence.js +9 -0
- package/dist/demo/mainForeign.d.ts +6 -0
- package/dist/demo/mainForeign.js +7 -0
- package/dist/demo/mainGroup.d.ts +6 -0
- package/dist/demo/mainGroup.js +7 -0
- package/dist/demo/mainGroupPeriods.d.ts +6 -0
- package/dist/demo/mainGroupPeriods.js +7 -0
- package/dist/demo/mainIngest.d.ts +6 -0
- package/dist/demo/mainIngest.js +7 -0
- package/dist/demo/mainMidYear.d.ts +6 -0
- package/dist/demo/mainMidYear.js +7 -0
- package/dist/demo/mainReconcile.d.ts +6 -0
- package/dist/demo/mainReconcile.js +7 -0
- package/dist/demo/mainStatements.d.ts +6 -0
- package/dist/demo/mainStatements.js +7 -0
- package/dist/demo/midYear.d.ts +51 -0
- package/dist/demo/midYear.js +162 -0
- package/dist/demo/month.d.ts +13 -0
- package/dist/demo/month.js +142 -0
- package/dist/demo/random.d.ts +48 -0
- package/dist/demo/random.js +94 -0
- package/dist/demo/receivables.d.ts +36 -0
- package/dist/demo/receivables.js +104 -0
- package/dist/demo/reconcile.d.ts +20 -0
- package/dist/demo/reconcile.js +104 -0
- package/dist/demo/report.d.ts +6 -0
- package/dist/demo/report.js +26 -0
- package/dist/demo/statement.d.ts +13 -0
- package/dist/demo/statement.js +51 -0
- package/dist/demo/statements.d.ts +6 -0
- package/dist/demo/statements.js +29 -0
- package/dist/demo/supplierRun.d.ts +46 -0
- package/dist/demo/supplierRun.js +94 -0
- package/dist/demo/unrealisedEvidence.d.ts +51 -0
- package/dist/demo/unrealisedEvidence.js +183 -0
- package/dist/fx/average.d.ts +60 -0
- package/dist/fx/average.js +139 -0
- package/dist/fx/document.d.ts +60 -0
- package/dist/fx/document.js +205 -0
- package/dist/fx/exposure.d.ts +108 -0
- package/dist/fx/exposure.js +262 -0
- package/dist/fx/index.d.ts +8 -0
- package/dist/fx/index.js +8 -0
- package/dist/fx/rate.d.ts +77 -0
- package/dist/fx/rate.js +200 -0
- package/dist/fx/revaluation.d.ts +92 -0
- package/dist/fx/revaluation.js +197 -0
- package/dist/fx/settlement.d.ts +84 -0
- package/dist/fx/settlement.js +162 -0
- package/dist/fx/table.d.ts +120 -0
- package/dist/fx/table.js +271 -0
- package/dist/fx/translate.d.ts +126 -0
- package/dist/fx/translate.js +258 -0
- package/dist/group/accounts.d.ts +74 -0
- package/dist/group/accounts.js +179 -0
- package/dist/group/acquisition.d.ts +324 -0
- package/dist/group/acquisition.js +416 -0
- package/dist/group/aggregate.d.ts +143 -0
- package/dist/group/aggregate.js +288 -0
- package/dist/group/comparative.d.ts +102 -0
- package/dist/group/comparative.js +166 -0
- package/dist/group/consolidate.d.ts +196 -0
- package/dist/group/consolidate.js +635 -0
- package/dist/group/disposal.d.ts +181 -0
- package/dist/group/disposal.js +233 -0
- package/dist/group/document.d.ts +41 -0
- package/dist/group/document.js +326 -0
- package/dist/group/index.d.ts +12 -0
- package/dist/group/index.js +12 -0
- package/dist/group/intercompany.d.ts +136 -0
- package/dist/group/intercompany.js +238 -0
- package/dist/group/interest.d.ts +109 -0
- package/dist/group/interest.js +224 -0
- package/dist/group/movement.d.ts +206 -0
- package/dist/group/movement.js +432 -0
- package/dist/group/structure.d.ts +151 -0
- package/dist/group/structure.js +327 -0
- package/dist/group/timeline.d.ts +109 -0
- package/dist/group/timeline.js +169 -0
- package/dist/group/unrealised.d.ts +165 -0
- package/dist/group/unrealised.js +314 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/ledger/close.d.ts +75 -0
- package/dist/ledger/close.js +142 -0
- package/dist/ledger/date.d.ts +48 -0
- package/dist/ledger/date.js +141 -0
- package/dist/ledger/entry.d.ts +150 -0
- package/dist/ledger/entry.js +264 -0
- package/dist/ledger/index.d.ts +6 -0
- package/dist/ledger/index.js +6 -0
- package/dist/ledger/ledger.d.ts +101 -0
- package/dist/ledger/ledger.js +308 -0
- package/dist/ledger/serialise.d.ts +68 -0
- package/dist/ledger/serialise.js +204 -0
- package/dist/ledger/trialBalance.d.ts +49 -0
- package/dist/ledger/trialBalance.js +103 -0
- package/dist/money/currency.d.ts +37 -0
- package/dist/money/currency.js +67 -0
- package/dist/money/index.d.ts +3 -0
- package/dist/money/index.js +3 -0
- package/dist/money/money.d.ts +106 -0
- package/dist/money/money.js +300 -0
- package/dist/money/rounding.d.ts +41 -0
- package/dist/money/rounding.js +117 -0
- package/dist/reconcile/accuracy.d.ts +65 -0
- package/dist/reconcile/accuracy.js +162 -0
- package/dist/reconcile/assignment.d.ts +83 -0
- package/dist/reconcile/assignment.js +326 -0
- package/dist/reconcile/bankView.d.ts +65 -0
- package/dist/reconcile/bankView.js +86 -0
- package/dist/reconcile/bridge.d.ts +76 -0
- package/dist/reconcile/bridge.js +129 -0
- package/dist/reconcile/candidates.d.ts +84 -0
- package/dist/reconcile/candidates.js +174 -0
- package/dist/reconcile/decisions.d.ts +116 -0
- package/dist/reconcile/decisions.js +168 -0
- package/dist/reconcile/index.d.ts +12 -0
- package/dist/reconcile/index.js +12 -0
- package/dist/reconcile/matcher.d.ts +89 -0
- package/dist/reconcile/matcher.js +306 -0
- package/dist/reconcile/memory.d.ts +120 -0
- package/dist/reconcile/memory.js +281 -0
- package/dist/reconcile/posting.d.ts +185 -0
- package/dist/reconcile/posting.js +481 -0
- package/dist/reconcile/scoring.d.ts +98 -0
- package/dist/reconcile/scoring.js +319 -0
- package/dist/reconcile/similarity.d.ts +88 -0
- package/dist/reconcile/similarity.js +272 -0
- package/dist/reconcile/subsetSum.d.ts +61 -0
- package/dist/reconcile/subsetSum.js +136 -0
- package/dist/reports/ageing.d.ts +75 -0
- package/dist/reports/ageing.js +149 -0
- package/dist/reports/balanceSheet.d.ts +57 -0
- package/dist/reports/balanceSheet.js +107 -0
- package/dist/reports/incomeStatement.d.ts +59 -0
- package/dist/reports/incomeStatement.js +132 -0
- package/dist/reports/index.d.ts +4 -0
- package/dist/reports/index.js +4 -0
- package/dist/reports/period.d.ts +37 -0
- package/dist/reports/period.js +78 -0
- package/dist/statement/amount.d.ts +55 -0
- package/dist/statement/amount.js +211 -0
- package/dist/statement/columns.d.ts +39 -0
- package/dist/statement/columns.js +221 -0
- package/dist/statement/csv.d.ts +50 -0
- package/dist/statement/csv.js +185 -0
- package/dist/statement/dates.d.ts +52 -0
- package/dist/statement/dates.js +204 -0
- package/dist/statement/duplicates.d.ts +51 -0
- package/dist/statement/duplicates.js +101 -0
- package/dist/statement/import.d.ts +52 -0
- package/dist/statement/import.js +172 -0
- package/dist/statement/index.d.ts +20 -0
- package/dist/statement/index.js +32 -0
- package/dist/statement/line.d.ts +60 -0
- package/dist/statement/line.js +73 -0
- package/dist/statement/ofx.d.ts +47 -0
- package/dist/statement/ofx.js +199 -0
- package/package.json +72 -4
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The chart of accounts: an immutable, validated account tree.
|
|
3
|
+
*
|
|
4
|
+
* Validation happens once, at construction. Downstream code that holds a
|
|
5
|
+
* `ChartOfAccounts` can assume every parent exists, no cycle is present, child
|
|
6
|
+
* types agree with their parent, and any account it is handed is postable —
|
|
7
|
+
* because the alternative would have thrown before the object existed.
|
|
8
|
+
*/
|
|
9
|
+
import type { Currency } from "../money/currency.js";
|
|
10
|
+
import { type AccountType, type NormalBalance } from "./types.js";
|
|
11
|
+
export interface AccountDefinition {
|
|
12
|
+
/** Stable identifier, conventionally numeric ("1000") but any string works. */
|
|
13
|
+
code: string;
|
|
14
|
+
name: string;
|
|
15
|
+
type: AccountType;
|
|
16
|
+
/** Code of the parent account. Omit for a root. */
|
|
17
|
+
parent?: string;
|
|
18
|
+
/** Currency for this account. Defaults to the chart's currency. */
|
|
19
|
+
currency?: Currency | string;
|
|
20
|
+
/**
|
|
21
|
+
* A placeholder groups children but cannot itself be posted to — the usual
|
|
22
|
+
* treatment for "Current Assets" as opposed to "Barclays Current Account".
|
|
23
|
+
*/
|
|
24
|
+
placeholder?: boolean;
|
|
25
|
+
/** A closed account keeps its history but rejects new postings. */
|
|
26
|
+
closed?: boolean;
|
|
27
|
+
description?: string;
|
|
28
|
+
}
|
|
29
|
+
export interface Account {
|
|
30
|
+
readonly code: string;
|
|
31
|
+
readonly name: string;
|
|
32
|
+
readonly type: AccountType;
|
|
33
|
+
readonly parent: string | null;
|
|
34
|
+
readonly currency: Currency;
|
|
35
|
+
readonly placeholder: boolean;
|
|
36
|
+
readonly closed: boolean;
|
|
37
|
+
readonly description: string;
|
|
38
|
+
readonly normalBalance: NormalBalance;
|
|
39
|
+
/** Depth from the root; roots are 0. */
|
|
40
|
+
readonly depth: number;
|
|
41
|
+
/** Colon-joined names from the root, e.g. "Assets:Current:Barclays". */
|
|
42
|
+
readonly path: string;
|
|
43
|
+
readonly children: readonly string[];
|
|
44
|
+
}
|
|
45
|
+
export declare class ChartError extends Error {
|
|
46
|
+
constructor(message: string);
|
|
47
|
+
}
|
|
48
|
+
export declare class UnknownAccountError extends ChartError {
|
|
49
|
+
readonly code: string;
|
|
50
|
+
constructor(code: string);
|
|
51
|
+
}
|
|
52
|
+
export declare class AccountNotPostableError extends ChartError {
|
|
53
|
+
readonly code: string;
|
|
54
|
+
constructor(code: string, reason: string);
|
|
55
|
+
}
|
|
56
|
+
export declare class ChartOfAccounts {
|
|
57
|
+
private readonly accounts;
|
|
58
|
+
readonly defaultCurrency: Currency;
|
|
59
|
+
readonly roots: readonly string[];
|
|
60
|
+
private constructor();
|
|
61
|
+
static build(definitions: readonly AccountDefinition[], options?: {
|
|
62
|
+
currency?: Currency | string;
|
|
63
|
+
}): ChartOfAccounts;
|
|
64
|
+
has(code: string): boolean;
|
|
65
|
+
/** Look up an account, throwing if it does not exist. */
|
|
66
|
+
get(code: string): Account;
|
|
67
|
+
find(code: string): Account | undefined;
|
|
68
|
+
get size(): number;
|
|
69
|
+
/** Every account, in depth-first order from each sorted root. */
|
|
70
|
+
list(): readonly Account[];
|
|
71
|
+
ofType(type: AccountType): readonly Account[];
|
|
72
|
+
/** Accounts with no children — the ones that normally receive postings. */
|
|
73
|
+
leaves(): readonly Account[];
|
|
74
|
+
children(code: string): readonly Account[];
|
|
75
|
+
/** All descendants of `code`, excluding `code` itself. */
|
|
76
|
+
descendants(code: string): readonly Account[];
|
|
77
|
+
/** `code` and all its descendants — the set a rolled-up balance covers. */
|
|
78
|
+
subtree(code: string): readonly Account[];
|
|
79
|
+
/** Ancestors from the immediate parent up to the root. */
|
|
80
|
+
ancestors(code: string): readonly Account[];
|
|
81
|
+
isDescendantOf(code: string, ancestor: string): boolean;
|
|
82
|
+
/** Resolve by colon-delimited path, e.g. "Assets:Current:Barclays". */
|
|
83
|
+
findByPath(path: string): Account | undefined;
|
|
84
|
+
/**
|
|
85
|
+
* Assert that an account can receive a posting, returning it. Placeholders
|
|
86
|
+
* and closed accounts are rejected here rather than at posting time so the
|
|
87
|
+
* error names the account and the reason.
|
|
88
|
+
*/
|
|
89
|
+
assertPostable(code: string): Account;
|
|
90
|
+
isPostable(code: string): boolean;
|
|
91
|
+
/** A new chart with extra accounts, re-validated from scratch. */
|
|
92
|
+
extend(definitions: readonly AccountDefinition[]): ChartOfAccounts;
|
|
93
|
+
toDefinitions(): AccountDefinition[];
|
|
94
|
+
/** Indented text rendering, handy in the CLI and in test failure output. */
|
|
95
|
+
render(): string;
|
|
96
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The chart of accounts: an immutable, validated account tree.
|
|
3
|
+
*
|
|
4
|
+
* Validation happens once, at construction. Downstream code that holds a
|
|
5
|
+
* `ChartOfAccounts` can assume every parent exists, no cycle is present, child
|
|
6
|
+
* types agree with their parent, and any account it is handed is postable —
|
|
7
|
+
* because the alternative would have thrown before the object existed.
|
|
8
|
+
*/
|
|
9
|
+
import { currency as lookupCurrency } from "../money/currency.js";
|
|
10
|
+
import { isAccountType, normalBalanceOf } from "./types.js";
|
|
11
|
+
export class ChartError extends Error {
|
|
12
|
+
constructor(message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "ChartError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export class UnknownAccountError extends ChartError {
|
|
18
|
+
code;
|
|
19
|
+
constructor(code) {
|
|
20
|
+
super(`No such account: ${code}`);
|
|
21
|
+
this.code = code;
|
|
22
|
+
this.name = "UnknownAccountError";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export class AccountNotPostableError extends ChartError {
|
|
26
|
+
code;
|
|
27
|
+
constructor(code, reason) {
|
|
28
|
+
super(`Account ${code} is not postable: ${reason}`);
|
|
29
|
+
this.code = code;
|
|
30
|
+
this.name = "AccountNotPostableError";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export class ChartOfAccounts {
|
|
34
|
+
accounts;
|
|
35
|
+
defaultCurrency;
|
|
36
|
+
roots;
|
|
37
|
+
constructor(accounts, defaultCurrency, roots) {
|
|
38
|
+
this.accounts = accounts;
|
|
39
|
+
this.defaultCurrency = defaultCurrency;
|
|
40
|
+
this.roots = Object.freeze([...roots]);
|
|
41
|
+
Object.freeze(this);
|
|
42
|
+
}
|
|
43
|
+
static build(definitions, options = {}) {
|
|
44
|
+
const defaultCurrency = typeof options.currency === "string"
|
|
45
|
+
? lookupCurrency(options.currency)
|
|
46
|
+
: (options.currency ?? lookupCurrency("GBP"));
|
|
47
|
+
// Pass 1 — shape and uniqueness.
|
|
48
|
+
const byCode = new Map();
|
|
49
|
+
for (const def of definitions) {
|
|
50
|
+
if (def.code.trim() === "")
|
|
51
|
+
throw new ChartError("Account code must not be blank");
|
|
52
|
+
if (def.name.trim() === "") {
|
|
53
|
+
throw new ChartError(`Account ${def.code} must have a name`);
|
|
54
|
+
}
|
|
55
|
+
if (!isAccountType(def.type)) {
|
|
56
|
+
throw new ChartError(`Account ${def.code} has unknown type: ${String(def.type)}`);
|
|
57
|
+
}
|
|
58
|
+
if (byCode.has(def.code))
|
|
59
|
+
throw new ChartError(`Duplicate account code: ${def.code}`);
|
|
60
|
+
byCode.set(def.code, def);
|
|
61
|
+
}
|
|
62
|
+
// Pass 2 — parent links, cycles and type agreement.
|
|
63
|
+
const childrenOf = new Map();
|
|
64
|
+
for (const def of definitions)
|
|
65
|
+
childrenOf.set(def.code, []);
|
|
66
|
+
for (const def of definitions) {
|
|
67
|
+
if (def.parent === undefined)
|
|
68
|
+
continue;
|
|
69
|
+
const parent = byCode.get(def.parent);
|
|
70
|
+
if (parent === undefined) {
|
|
71
|
+
throw new ChartError(`Account ${def.code} names a missing parent: ${def.parent}`);
|
|
72
|
+
}
|
|
73
|
+
if (parent.type !== def.type) {
|
|
74
|
+
throw new ChartError(`Account ${def.code} is ${def.type} but its parent ${parent.code} is ${parent.type}`);
|
|
75
|
+
}
|
|
76
|
+
childrenOf.get(def.parent).push(def.code);
|
|
77
|
+
}
|
|
78
|
+
for (const def of definitions) {
|
|
79
|
+
const seen = new Set([def.code]);
|
|
80
|
+
let cursor = def.parent;
|
|
81
|
+
while (cursor !== undefined) {
|
|
82
|
+
if (seen.has(cursor)) {
|
|
83
|
+
throw new ChartError(`Account hierarchy contains a cycle at ${cursor}`);
|
|
84
|
+
}
|
|
85
|
+
seen.add(cursor);
|
|
86
|
+
cursor = byCode.get(cursor)?.parent;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Pass 3 — materialise, computing depth and path top-down.
|
|
90
|
+
const accounts = new Map();
|
|
91
|
+
const roots = definitions.filter((d) => d.parent === undefined).map((d) => d.code);
|
|
92
|
+
const visit = (code, depth, prefix) => {
|
|
93
|
+
const def = byCode.get(code);
|
|
94
|
+
const children = childrenOf.get(code).slice().sort();
|
|
95
|
+
const path = prefix === "" ? def.name : `${prefix}:${def.name}`;
|
|
96
|
+
const accountCurrency = def.currency === undefined
|
|
97
|
+
? defaultCurrency
|
|
98
|
+
: typeof def.currency === "string"
|
|
99
|
+
? lookupCurrency(def.currency)
|
|
100
|
+
: def.currency;
|
|
101
|
+
accounts.set(code, Object.freeze({
|
|
102
|
+
code,
|
|
103
|
+
name: def.name,
|
|
104
|
+
type: def.type,
|
|
105
|
+
parent: def.parent ?? null,
|
|
106
|
+
currency: accountCurrency,
|
|
107
|
+
placeholder: def.placeholder ?? children.length > 0,
|
|
108
|
+
closed: def.closed ?? false,
|
|
109
|
+
description: def.description ?? "",
|
|
110
|
+
normalBalance: normalBalanceOf(def.type),
|
|
111
|
+
depth,
|
|
112
|
+
path,
|
|
113
|
+
children: Object.freeze(children),
|
|
114
|
+
}));
|
|
115
|
+
for (const child of children)
|
|
116
|
+
visit(child, depth + 1, path);
|
|
117
|
+
};
|
|
118
|
+
for (const root of roots)
|
|
119
|
+
visit(root, 0, "");
|
|
120
|
+
return new ChartOfAccounts(accounts, defaultCurrency, roots.slice().sort());
|
|
121
|
+
}
|
|
122
|
+
// ------------------------------------------------------------------ lookups
|
|
123
|
+
has(code) {
|
|
124
|
+
return this.accounts.has(code);
|
|
125
|
+
}
|
|
126
|
+
/** Look up an account, throwing if it does not exist. */
|
|
127
|
+
get(code) {
|
|
128
|
+
const account = this.accounts.get(code);
|
|
129
|
+
if (account === undefined)
|
|
130
|
+
throw new UnknownAccountError(code);
|
|
131
|
+
return account;
|
|
132
|
+
}
|
|
133
|
+
find(code) {
|
|
134
|
+
return this.accounts.get(code);
|
|
135
|
+
}
|
|
136
|
+
get size() {
|
|
137
|
+
return this.accounts.size;
|
|
138
|
+
}
|
|
139
|
+
/** Every account, in depth-first order from each sorted root. */
|
|
140
|
+
list() {
|
|
141
|
+
const out = [];
|
|
142
|
+
const walk = (code) => {
|
|
143
|
+
const account = this.get(code);
|
|
144
|
+
out.push(account);
|
|
145
|
+
for (const child of account.children)
|
|
146
|
+
walk(child);
|
|
147
|
+
};
|
|
148
|
+
for (const root of this.roots)
|
|
149
|
+
walk(root);
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
ofType(type) {
|
|
153
|
+
return this.list().filter((a) => a.type === type);
|
|
154
|
+
}
|
|
155
|
+
/** Accounts with no children — the ones that normally receive postings. */
|
|
156
|
+
leaves() {
|
|
157
|
+
return this.list().filter((a) => a.children.length === 0);
|
|
158
|
+
}
|
|
159
|
+
children(code) {
|
|
160
|
+
return this.get(code).children.map((c) => this.get(c));
|
|
161
|
+
}
|
|
162
|
+
/** All descendants of `code`, excluding `code` itself. */
|
|
163
|
+
descendants(code) {
|
|
164
|
+
const out = [];
|
|
165
|
+
const walk = (current) => {
|
|
166
|
+
for (const child of this.get(current).children) {
|
|
167
|
+
out.push(this.get(child));
|
|
168
|
+
walk(child);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
walk(code);
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
/** `code` and all its descendants — the set a rolled-up balance covers. */
|
|
175
|
+
subtree(code) {
|
|
176
|
+
return [this.get(code), ...this.descendants(code)];
|
|
177
|
+
}
|
|
178
|
+
/** Ancestors from the immediate parent up to the root. */
|
|
179
|
+
ancestors(code) {
|
|
180
|
+
const out = [];
|
|
181
|
+
let cursor = this.get(code).parent;
|
|
182
|
+
while (cursor !== null) {
|
|
183
|
+
const account = this.get(cursor);
|
|
184
|
+
out.push(account);
|
|
185
|
+
cursor = account.parent;
|
|
186
|
+
}
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
189
|
+
isDescendantOf(code, ancestor) {
|
|
190
|
+
return this.ancestors(code).some((a) => a.code === ancestor);
|
|
191
|
+
}
|
|
192
|
+
/** Resolve by colon-delimited path, e.g. "Assets:Current:Barclays". */
|
|
193
|
+
findByPath(path) {
|
|
194
|
+
return this.list().find((a) => a.path === path);
|
|
195
|
+
}
|
|
196
|
+
// ----------------------------------------------------------------- postings
|
|
197
|
+
/**
|
|
198
|
+
* Assert that an account can receive a posting, returning it. Placeholders
|
|
199
|
+
* and closed accounts are rejected here rather than at posting time so the
|
|
200
|
+
* error names the account and the reason.
|
|
201
|
+
*/
|
|
202
|
+
assertPostable(code) {
|
|
203
|
+
const account = this.get(code);
|
|
204
|
+
if (account.placeholder) {
|
|
205
|
+
throw new AccountNotPostableError(code, "it is a placeholder for its children");
|
|
206
|
+
}
|
|
207
|
+
if (account.closed)
|
|
208
|
+
throw new AccountNotPostableError(code, "it is closed");
|
|
209
|
+
return account;
|
|
210
|
+
}
|
|
211
|
+
isPostable(code) {
|
|
212
|
+
const account = this.find(code);
|
|
213
|
+
return account !== undefined && !account.placeholder && !account.closed;
|
|
214
|
+
}
|
|
215
|
+
// ------------------------------------------------------------------ derived
|
|
216
|
+
/** A new chart with extra accounts, re-validated from scratch. */
|
|
217
|
+
extend(definitions) {
|
|
218
|
+
return ChartOfAccounts.build([...this.toDefinitions(), ...definitions], {
|
|
219
|
+
currency: this.defaultCurrency,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
toDefinitions() {
|
|
223
|
+
return this.list().map((a) => {
|
|
224
|
+
const def = {
|
|
225
|
+
code: a.code,
|
|
226
|
+
name: a.name,
|
|
227
|
+
type: a.type,
|
|
228
|
+
currency: a.currency,
|
|
229
|
+
placeholder: a.placeholder,
|
|
230
|
+
closed: a.closed,
|
|
231
|
+
description: a.description,
|
|
232
|
+
};
|
|
233
|
+
if (a.parent !== null)
|
|
234
|
+
def.parent = a.parent;
|
|
235
|
+
return def;
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
/** Indented text rendering, handy in the CLI and in test failure output. */
|
|
239
|
+
render() {
|
|
240
|
+
return this.list()
|
|
241
|
+
.map((a) => `${" ".repeat(a.depth)}${a.code} ${a.name}${a.placeholder ? " ·" : ""}`)
|
|
242
|
+
.join("\n");
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { type AccountType, type NormalBalance, ACCOUNT_TYPES, normalBalanceOf, debitSign, isBalanceSheet, isTemporary, isAccountType, } from "./types.js";
|
|
2
|
+
export { type Account, type AccountDefinition, ChartOfAccounts, ChartError, UnknownAccountError, AccountNotPostableError, } from "./chart.js";
|
|
3
|
+
export { STANDARD_ACCOUNTS, standardChart } from "./standard.js";
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { ACCOUNT_TYPES, normalBalanceOf, debitSign, isBalanceSheet, isTemporary, isAccountType, } from "./types.js";
|
|
2
|
+
export { ChartOfAccounts, ChartError, UnknownAccountError, AccountNotPostableError, } from "./chart.js";
|
|
3
|
+
export { STANDARD_ACCOUNTS, standardChart } from "./standard.js";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small-business chart of accounts, used as the default in the CLI and as
|
|
3
|
+
* fixture data in tests.
|
|
4
|
+
*
|
|
5
|
+
* The codes follow the usual 1000/2000/3000/4000/5000 blocks so the ordering
|
|
6
|
+
* of a trial balance printed by code matches the order of a balance sheet.
|
|
7
|
+
*/
|
|
8
|
+
import type { AccountDefinition } from "./chart.js";
|
|
9
|
+
import { ChartOfAccounts } from "./chart.js";
|
|
10
|
+
import type { Currency } from "../money/currency.js";
|
|
11
|
+
export declare const STANDARD_ACCOUNTS: readonly AccountDefinition[];
|
|
12
|
+
export declare function standardChart(currency?: Currency | string): ChartOfAccounts;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small-business chart of accounts, used as the default in the CLI and as
|
|
3
|
+
* fixture data in tests.
|
|
4
|
+
*
|
|
5
|
+
* The codes follow the usual 1000/2000/3000/4000/5000 blocks so the ordering
|
|
6
|
+
* of a trial balance printed by code matches the order of a balance sheet.
|
|
7
|
+
*/
|
|
8
|
+
import { ChartOfAccounts } from "./chart.js";
|
|
9
|
+
export const STANDARD_ACCOUNTS = [
|
|
10
|
+
// 1000 — assets
|
|
11
|
+
{ code: "1000", name: "Assets", type: "asset", placeholder: true },
|
|
12
|
+
{ code: "1100", name: "Current Assets", type: "asset", parent: "1000", placeholder: true },
|
|
13
|
+
{
|
|
14
|
+
code: "1110",
|
|
15
|
+
name: "Bank",
|
|
16
|
+
type: "asset",
|
|
17
|
+
parent: "1100",
|
|
18
|
+
description: "Operating current account, reconciled against statements",
|
|
19
|
+
},
|
|
20
|
+
{ code: "1120", name: "Petty Cash", type: "asset", parent: "1100" },
|
|
21
|
+
{
|
|
22
|
+
code: "1130",
|
|
23
|
+
name: "Accounts Receivable",
|
|
24
|
+
type: "asset",
|
|
25
|
+
parent: "1100",
|
|
26
|
+
description: "Invoiced but unpaid",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
code: "1140",
|
|
30
|
+
name: "Undeposited Funds",
|
|
31
|
+
type: "asset",
|
|
32
|
+
parent: "1100",
|
|
33
|
+
description: "Captured by the processor, not yet settled to the bank",
|
|
34
|
+
},
|
|
35
|
+
{ code: "1150", name: "Prepaid Expenses", type: "asset", parent: "1100" },
|
|
36
|
+
{ code: "1200", name: "Fixed Assets", type: "asset", parent: "1000", placeholder: true },
|
|
37
|
+
{ code: "1210", name: "Equipment", type: "asset", parent: "1200" },
|
|
38
|
+
{
|
|
39
|
+
code: "1220",
|
|
40
|
+
name: "Accumulated Depreciation",
|
|
41
|
+
type: "asset",
|
|
42
|
+
parent: "1200",
|
|
43
|
+
description: "Contra-asset; carries a credit balance",
|
|
44
|
+
},
|
|
45
|
+
// 2000 — liabilities
|
|
46
|
+
{ code: "2000", name: "Liabilities", type: "liability", placeholder: true },
|
|
47
|
+
{ code: "2100", name: "Accounts Payable", type: "liability", parent: "2000" },
|
|
48
|
+
{ code: "2200", name: "VAT Payable", type: "liability", parent: "2000" },
|
|
49
|
+
{ code: "2300", name: "Payroll Liabilities", type: "liability", parent: "2000" },
|
|
50
|
+
{ code: "2400", name: "Credit Card", type: "liability", parent: "2000" },
|
|
51
|
+
// 3000 — equity
|
|
52
|
+
{ code: "3000", name: "Equity", type: "equity", placeholder: true },
|
|
53
|
+
{ code: "3100", name: "Share Capital", type: "equity", parent: "3000" },
|
|
54
|
+
{ code: "3200", name: "Retained Earnings", type: "equity", parent: "3000" },
|
|
55
|
+
{ code: "3300", name: "Drawings", type: "equity", parent: "3000" },
|
|
56
|
+
// 4000 — income
|
|
57
|
+
{ code: "4000", name: "Income", type: "income", placeholder: true },
|
|
58
|
+
{ code: "4100", name: "Sales", type: "income", parent: "4000" },
|
|
59
|
+
{ code: "4200", name: "Consulting", type: "income", parent: "4000" },
|
|
60
|
+
{ code: "4300", name: "Interest Income", type: "income", parent: "4000" },
|
|
61
|
+
{
|
|
62
|
+
code: "4400",
|
|
63
|
+
name: "Foreign Exchange Gain",
|
|
64
|
+
type: "income",
|
|
65
|
+
parent: "4000",
|
|
66
|
+
description: "Where a balance retranslated or settled in your favour lands",
|
|
67
|
+
},
|
|
68
|
+
// 5000 — expenses
|
|
69
|
+
{ code: "5000", name: "Expenses", type: "expense", placeholder: true },
|
|
70
|
+
{ code: "5100", name: "Cost of Sales", type: "expense", parent: "5000" },
|
|
71
|
+
{ code: "5200", name: "Salaries", type: "expense", parent: "5000" },
|
|
72
|
+
{ code: "5300", name: "Rent", type: "expense", parent: "5000" },
|
|
73
|
+
{ code: "5400", name: "Software", type: "expense", parent: "5000" },
|
|
74
|
+
{
|
|
75
|
+
code: "5500",
|
|
76
|
+
name: "Payment Processing Fees",
|
|
77
|
+
type: "expense",
|
|
78
|
+
parent: "5000",
|
|
79
|
+
description: "The gap that makes a deposit fail to match its invoice",
|
|
80
|
+
},
|
|
81
|
+
{ code: "5600", name: "Travel", type: "expense", parent: "5000" },
|
|
82
|
+
{ code: "5700", name: "Professional Fees", type: "expense", parent: "5000" },
|
|
83
|
+
{ code: "5800", name: "Bank Charges", type: "expense", parent: "5000" },
|
|
84
|
+
{ code: "5900", name: "Depreciation", type: "expense", parent: "5000" },
|
|
85
|
+
{
|
|
86
|
+
code: "5950",
|
|
87
|
+
name: "Foreign Exchange Loss",
|
|
88
|
+
type: "expense",
|
|
89
|
+
parent: "5000",
|
|
90
|
+
description: "The other side of 4400, kept separate so the P&L shows both",
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
export function standardChart(currency = "GBP") {
|
|
94
|
+
return ChartOfAccounts.build(STANDARD_ACCOUNTS, { currency });
|
|
95
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Account classification and normal balances.
|
|
3
|
+
*
|
|
4
|
+
* The five statement classes determine which side of a posting increases an
|
|
5
|
+
* account. Getting this wrong is the single most common source of a report
|
|
6
|
+
* that balances arithmetically but reads backwards, so the mapping lives in
|
|
7
|
+
* one table and everything else derives from it.
|
|
8
|
+
*/
|
|
9
|
+
export type AccountType = "asset" | "liability" | "equity" | "income" | "expense";
|
|
10
|
+
export declare const ACCOUNT_TYPES: readonly AccountType[];
|
|
11
|
+
/** The side on which an account's balance normally sits. */
|
|
12
|
+
export type NormalBalance = "debit" | "credit";
|
|
13
|
+
export declare function normalBalanceOf(type: AccountType): NormalBalance;
|
|
14
|
+
/** +1 when a debit increases this type, -1 when a debit decreases it. */
|
|
15
|
+
export declare function debitSign(type: AccountType): 1 | -1;
|
|
16
|
+
/** Accounts that appear on the balance sheet rather than the P&L. */
|
|
17
|
+
export declare function isBalanceSheet(type: AccountType): boolean;
|
|
18
|
+
/** Accounts closed out to retained earnings at period end. */
|
|
19
|
+
export declare function isTemporary(type: AccountType): boolean;
|
|
20
|
+
export declare function isAccountType(value: string): value is AccountType;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Account classification and normal balances.
|
|
3
|
+
*
|
|
4
|
+
* The five statement classes determine which side of a posting increases an
|
|
5
|
+
* account. Getting this wrong is the single most common source of a report
|
|
6
|
+
* that balances arithmetically but reads backwards, so the mapping lives in
|
|
7
|
+
* one table and everything else derives from it.
|
|
8
|
+
*/
|
|
9
|
+
export const ACCOUNT_TYPES = [
|
|
10
|
+
"asset",
|
|
11
|
+
"liability",
|
|
12
|
+
"equity",
|
|
13
|
+
"income",
|
|
14
|
+
"expense",
|
|
15
|
+
];
|
|
16
|
+
const NORMAL_BALANCE = {
|
|
17
|
+
asset: "debit",
|
|
18
|
+
expense: "debit",
|
|
19
|
+
liability: "credit",
|
|
20
|
+
equity: "credit",
|
|
21
|
+
income: "credit",
|
|
22
|
+
};
|
|
23
|
+
export function normalBalanceOf(type) {
|
|
24
|
+
return NORMAL_BALANCE[type];
|
|
25
|
+
}
|
|
26
|
+
/** +1 when a debit increases this type, -1 when a debit decreases it. */
|
|
27
|
+
export function debitSign(type) {
|
|
28
|
+
return NORMAL_BALANCE[type] === "debit" ? 1 : -1;
|
|
29
|
+
}
|
|
30
|
+
/** Accounts that appear on the balance sheet rather than the P&L. */
|
|
31
|
+
export function isBalanceSheet(type) {
|
|
32
|
+
return type === "asset" || type === "liability" || type === "equity";
|
|
33
|
+
}
|
|
34
|
+
/** Accounts closed out to retained earnings at period end. */
|
|
35
|
+
export function isTemporary(type) {
|
|
36
|
+
return type === "income" || type === "expense";
|
|
37
|
+
}
|
|
38
|
+
export function isAccountType(value) {
|
|
39
|
+
return ACCOUNT_TYPES.includes(value);
|
|
40
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Argument parsing, written rather than installed.
|
|
3
|
+
*
|
|
4
|
+
* A dependency would do this, and for a tool this size the dependency is the
|
|
5
|
+
* larger risk: everything a ledger CLI needs — long flags, `--flag=value` and
|
|
6
|
+
* `--flag value`, short aliases, repeated values, `--` to stop parsing — is a
|
|
7
|
+
* page of code that can be tested against the awkward cases instead of
|
|
8
|
+
* trusted.
|
|
9
|
+
*
|
|
10
|
+
* The awkward cases are the point. `--account -1100` should be a value, not a
|
|
11
|
+
* missing flag followed by a stray negative number. `--as-at=2026-08-31` and
|
|
12
|
+
* `--as-at 2026-08-31` must mean the same thing. An unknown flag must be an
|
|
13
|
+
* error rather than silently ignored, because a typo in `--as-at` that reports
|
|
14
|
+
* the wrong period without complaining is exactly the failure a report tool
|
|
15
|
+
* cannot afford.
|
|
16
|
+
*/
|
|
17
|
+
export type FlagKind = "string" | "boolean" | "list";
|
|
18
|
+
export interface FlagSpec {
|
|
19
|
+
readonly kind: FlagKind;
|
|
20
|
+
/** Single-character alias, without the dash. */
|
|
21
|
+
readonly short?: string;
|
|
22
|
+
readonly describe: string;
|
|
23
|
+
readonly placeholder?: string;
|
|
24
|
+
}
|
|
25
|
+
export type FlagSpecs = Readonly<Record<string, FlagSpec>>;
|
|
26
|
+
export interface ParsedArgs {
|
|
27
|
+
readonly positional: readonly string[];
|
|
28
|
+
readonly flags: Readonly<Record<string, string | boolean | readonly string[]>>;
|
|
29
|
+
}
|
|
30
|
+
export declare class ArgumentError extends Error {
|
|
31
|
+
constructor(message: string);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Parse `argv` against a flag table.
|
|
35
|
+
*
|
|
36
|
+
* Everything after a bare `--` is positional, however it looks. Anything not
|
|
37
|
+
* recognised is an error rather than a shrug.
|
|
38
|
+
*/
|
|
39
|
+
export declare function parseArgs(argv: readonly string[], specs: FlagSpecs): ParsedArgs;
|
|
40
|
+
export declare function stringFlag(parsed: ParsedArgs, name: string): string | undefined;
|
|
41
|
+
export declare function requiredFlag(parsed: ParsedArgs, name: string): string;
|
|
42
|
+
export declare function booleanFlag(parsed: ParsedArgs, name: string): boolean;
|
|
43
|
+
export declare function listFlag(parsed: ParsedArgs, name: string): readonly string[];
|
|
44
|
+
/** Render a flag table as aligned help text. */
|
|
45
|
+
export declare function renderFlags(specs: FlagSpecs, indent?: string): string;
|