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,416 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What was paid, what was bought, and what belongs to everybody else.
|
|
3
|
+
*
|
|
4
|
+
* A combined trial balance still carries the parent's investment in each
|
|
5
|
+
* subsidiary as an asset *and* the subsidiary's own share capital as equity,
|
|
6
|
+
* which is the same money on the page twice: the investment is a claim on
|
|
7
|
+
* exactly the net assets that are already there line by line. Both come out,
|
|
8
|
+
* and what is left over is goodwill — the part of the price that bought
|
|
9
|
+
* something the subsidiary's balance sheet does not carry.
|
|
10
|
+
*
|
|
11
|
+
* goodwill = consideration + non-controlling interest at acquisition
|
|
12
|
+
* - the net assets acquired
|
|
13
|
+
*
|
|
14
|
+
* The non-controlling interest belongs in that sum because goodwill is a
|
|
15
|
+
* measure of what the whole company was worth against what its identifiable
|
|
16
|
+
* net assets were worth, and the group only bought part of it. How the
|
|
17
|
+
* non-controlling interest is measured is a choice the group makes per
|
|
18
|
+
* acquisition and it changes the goodwill figure:
|
|
19
|
+
*
|
|
20
|
+
* - **Proportionate**: its share of the identifiable net assets. Goodwill is
|
|
21
|
+
* then only the group's own, and the balance sheet carries none of the
|
|
22
|
+
* goodwill attributable to the outside shareholders.
|
|
23
|
+
* - **Fair value**: what the outside stake was actually worth on the day,
|
|
24
|
+
* which is usually more, and the difference is the goodwill attributable to
|
|
25
|
+
* the non-controlling interest.
|
|
26
|
+
*
|
|
27
|
+
* A price below the net assets acquired is a bargain purchase, and the excess
|
|
28
|
+
* is a gain in the income statement rather than a negative asset. Negative
|
|
29
|
+
* goodwill sitting in the balance sheet would be asserting that the group owns
|
|
30
|
+
* something worth less than nothing.
|
|
31
|
+
*
|
|
32
|
+
* Afterwards, the two sides share what the subsidiary earns in the ratio of
|
|
33
|
+
* their interests. The non-controlling interest's claim at any date is its
|
|
34
|
+
* share of the net assets then, plus whatever goodwill was attributed to it at
|
|
35
|
+
* acquisition — a formulation that needs no roll-forward and cannot drift.
|
|
36
|
+
*
|
|
37
|
+
* On an indirect holding this uses the direct method: a company held 75% by a
|
|
38
|
+
* subsidiary that is itself held 80% has a 40% non-controlling interest from
|
|
39
|
+
* the group's point of view, and its goodwill is measured against the price
|
|
40
|
+
* the subsidiary paid. The alternative treatment, which scales the cost of the
|
|
41
|
+
* investment by the group's interest in the buyer, is a known gap.
|
|
42
|
+
*
|
|
43
|
+
* ## Which currency the arithmetic happens in
|
|
44
|
+
*
|
|
45
|
+
* All of it happens in the currency the acquired company keeps its books in,
|
|
46
|
+
* and the answer is then translated into the group's. That ordering is not a
|
|
47
|
+
* matter of taste. Goodwill on a foreign operation is an asset *of that
|
|
48
|
+
* operation*: what the extra money bought — a name, a position, people who
|
|
49
|
+
* stay — is a thing in that country, priced in that currency, and the group
|
|
50
|
+
* owning it does not move it. So the consideration comes the other way for the
|
|
51
|
+
* purposes of this sum, into the subsidiary's currency at the rate on the day,
|
|
52
|
+
* and the net assets read from its own books need no translating because they
|
|
53
|
+
* are already there.
|
|
54
|
+
*
|
|
55
|
+
* Doing it the other way round — translating each input into the group's
|
|
56
|
+
* currency and taking the residual there — gives a figure that cannot be
|
|
57
|
+
* retranslated afterwards, because there is no functional-currency amount left
|
|
58
|
+
* to retranslate, and one that carries three roundings instead of one: each
|
|
59
|
+
* input is rounded on its way across and goodwill is defined as the difference
|
|
60
|
+
* between them, so the error lands in the one figure with nothing to absorb it.
|
|
61
|
+
*
|
|
62
|
+
* A group reporting in the currency its subsidiary already keeps is untouched
|
|
63
|
+
* by any of this: no rate is looked up and nothing moves.
|
|
64
|
+
*/
|
|
65
|
+
import { Money } from "../money/money.js";
|
|
66
|
+
import { compareDates, date as parseDate, } from "../ledger/date.js";
|
|
67
|
+
import { trialBalance } from "../ledger/trialBalance.js";
|
|
68
|
+
import { Interest } from "./interest.js";
|
|
69
|
+
import { GroupError } from "./structure.js";
|
|
70
|
+
import { GROUP_ACCOUNTS } from "./accounts.js";
|
|
71
|
+
/**
|
|
72
|
+
* The net assets of an entity on a date, read from its own books.
|
|
73
|
+
*
|
|
74
|
+
* Assets less liabilities, which in the signed convention is simply the sum of
|
|
75
|
+
* the asset and liability rows: the trial balance's other rows are the claims
|
|
76
|
+
* on them.
|
|
77
|
+
*/
|
|
78
|
+
export function netAssets(ledger, asAt, currency) {
|
|
79
|
+
const tb = trialBalance(ledger, { currency, asAt, includeZero: true });
|
|
80
|
+
return tb.rows
|
|
81
|
+
.filter((row) => row.type === "asset" || row.type === "liability")
|
|
82
|
+
.reduce((running, row) => running.plus(row.signed), Money.zero(currency));
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* One amount into another currency at a given day's rate.
|
|
86
|
+
*
|
|
87
|
+
* `why` finishes the sentence "there is no rate on <date>" in the error, so a
|
|
88
|
+
* caller who has not supplied a quote is told which figure needed it and which
|
|
89
|
+
* way it was going rather than being handed a bare lookup failure.
|
|
90
|
+
*/
|
|
91
|
+
function convertOn(amount, into, options, on, what) {
|
|
92
|
+
if (amount.currency.code === into.code)
|
|
93
|
+
return amount;
|
|
94
|
+
try {
|
|
95
|
+
return options.rates
|
|
96
|
+
.lookup(amount.currency, into, on)
|
|
97
|
+
.rate.convert(amount, options.rounding ?? "half-even");
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
throw new GroupError(`${what} is in ${amount.currency.code} and has to be stated in ${into.code}, ` +
|
|
101
|
+
`but there is no rate on ${on}: ${error.message}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* A functional-currency measurement and its presentation-currency statement.
|
|
106
|
+
*
|
|
107
|
+
* The translation happens here and only here, so every figure on an
|
|
108
|
+
* `Acquisition` is one rounding away from the amount it was measured as.
|
|
109
|
+
*/
|
|
110
|
+
function measured(functional, options, on, what) {
|
|
111
|
+
return Object.freeze({
|
|
112
|
+
functional,
|
|
113
|
+
presentation: convertOn(functional, options.presentation, options, on, what),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Work out goodwill and the non-controlling interest at acquisition.
|
|
118
|
+
*
|
|
119
|
+
* Everything is measured on the day control was obtained, in the acquired
|
|
120
|
+
* company's own currency, and translated at that day's rate. The date is the
|
|
121
|
+
* acquisition date because that is when the bargain was struck: a rate that
|
|
122
|
+
* moved afterwards changes what the group's investment is worth, not what it
|
|
123
|
+
* paid. The currency is the subsidiary's because goodwill is an asset of the
|
|
124
|
+
* subsidiary; see the note at the top of this file.
|
|
125
|
+
*/
|
|
126
|
+
export function acquisitionOf(group, ledgers, input, options) {
|
|
127
|
+
const entity = group.get(input.entity);
|
|
128
|
+
if (entity.code === group.parent) {
|
|
129
|
+
throw new GroupError(`${entity.code} is the parent company; nobody acquired it`);
|
|
130
|
+
}
|
|
131
|
+
if (!entity.controlled) {
|
|
132
|
+
throw new GroupError(`${entity.code} is not consolidated, so there is no acquisition to account for. ` +
|
|
133
|
+
`It is held ${entity.effective.toPercentString(4)} and carried at cost.`);
|
|
134
|
+
}
|
|
135
|
+
const acquiredText = input.acquired ?? entity.acquired;
|
|
136
|
+
if (acquiredText === null || acquiredText === undefined) {
|
|
137
|
+
throw new GroupError(`${entity.code} needs the date control was obtained: without it there is no way ` +
|
|
138
|
+
`to tell pre-acquisition reserves from post-acquisition ones.`);
|
|
139
|
+
}
|
|
140
|
+
const acquired = parseDate(acquiredText);
|
|
141
|
+
const measurement = input.nciMeasurement ?? "proportionate";
|
|
142
|
+
const rounding = options.rounding ?? "half-even";
|
|
143
|
+
const functional = entity.currency;
|
|
144
|
+
const zero = Money.zero(functional);
|
|
145
|
+
// Everything below is in the acquired company's own currency.
|
|
146
|
+
const consideration = convertOn(input.consideration, functional, options, acquired, `The consideration for ${entity.code}`);
|
|
147
|
+
let netAssetsAcquired;
|
|
148
|
+
let netAssetsSupplied;
|
|
149
|
+
if (input.netAssetsAtAcquisition !== undefined) {
|
|
150
|
+
netAssetsAcquired = convertOn(input.netAssetsAtAcquisition, functional, options, acquired, `The net assets acquired with ${entity.code}`);
|
|
151
|
+
netAssetsSupplied = true;
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
const ledger = ledgers instanceof Map
|
|
155
|
+
? ledgers.get(entity.code)
|
|
156
|
+
: ledgers[entity.code];
|
|
157
|
+
if (ledger === undefined) {
|
|
158
|
+
throw new GroupError(`No books for ${entity.code}, so its net assets cannot be read`);
|
|
159
|
+
}
|
|
160
|
+
netAssetsAcquired = netAssets(ledger, acquired, functional);
|
|
161
|
+
netAssetsSupplied = false;
|
|
162
|
+
}
|
|
163
|
+
const nci = entity.nonControlling;
|
|
164
|
+
const nciShareOfNetAssets = nci.share(netAssetsAcquired, rounding);
|
|
165
|
+
let nciAtAcquisition;
|
|
166
|
+
if (measurement === "fair-value") {
|
|
167
|
+
if (input.nciFairValue === undefined) {
|
|
168
|
+
throw new GroupError(`${entity.code} measures its non-controlling interest at fair value, which is a ` +
|
|
169
|
+
`figure only the group has: supply nciFairValue or use the proportionate method.`);
|
|
170
|
+
}
|
|
171
|
+
nciAtAcquisition = convertOn(input.nciFairValue, functional, options, acquired, `The fair value of the non-controlling interest in ${entity.code}`);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
if (input.nciFairValue !== undefined) {
|
|
175
|
+
throw new GroupError(`${entity.code} gives a fair value for its non-controlling interest but measures it ` +
|
|
176
|
+
`proportionately, so the figure would be ignored. Say which is meant.`);
|
|
177
|
+
}
|
|
178
|
+
nciAtAcquisition = nciShareOfNetAssets;
|
|
179
|
+
}
|
|
180
|
+
const excess = consideration.plus(nciAtAcquisition).minus(netAssetsAcquired);
|
|
181
|
+
const goodwill = excess.isNegative ? zero : excess;
|
|
182
|
+
const bargainGain = excess.isNegative ? excess.negated() : zero;
|
|
183
|
+
const nciGoodwill = nciAtAcquisition.minus(nciShareOfNetAssets);
|
|
184
|
+
const considerationIn = measured(consideration, options, acquired, `The consideration for ${entity.code}`);
|
|
185
|
+
const netAssetsAcquiredIn = measured(netAssetsAcquired, options, acquired, `The net assets acquired with ${entity.code}`);
|
|
186
|
+
const nciAtAcquisitionIn = measured(nciAtAcquisition, options, acquired, `The non-controlling interest in ${entity.code} at acquisition`);
|
|
187
|
+
const goodwillIn = measured(goodwill, options, acquired, `Goodwill on ${entity.code}`);
|
|
188
|
+
const bargainGainIn = measured(bargainGain, options, acquired, `The bargain purchase gain on ${entity.code}`);
|
|
189
|
+
const nciGoodwillIn = measured(nciGoodwill, options, acquired, `The outside stake's goodwill on ${entity.code}`);
|
|
190
|
+
return Object.freeze({
|
|
191
|
+
entity: entity.code,
|
|
192
|
+
acquired,
|
|
193
|
+
groupInterest: entity.effective,
|
|
194
|
+
nonControllingInterest: nci,
|
|
195
|
+
measurement,
|
|
196
|
+
functional,
|
|
197
|
+
presentation: options.presentation,
|
|
198
|
+
consideration: considerationIn.presentation,
|
|
199
|
+
considerationIn,
|
|
200
|
+
netAssetsAcquired: netAssetsAcquiredIn.presentation,
|
|
201
|
+
netAssetsAcquiredIn,
|
|
202
|
+
netAssetsSupplied,
|
|
203
|
+
nciAtAcquisition: nciAtAcquisitionIn.presentation,
|
|
204
|
+
nciAtAcquisitionIn,
|
|
205
|
+
goodwill: goodwillIn.presentation,
|
|
206
|
+
goodwillIn,
|
|
207
|
+
bargainGain: bargainGainIn.presentation,
|
|
208
|
+
bargainGainIn,
|
|
209
|
+
nciGoodwill: nciGoodwillIn.presentation,
|
|
210
|
+
nciGoodwillIn,
|
|
211
|
+
investmentAccount: input.investmentAccount ?? GROUP_ACCOUNTS.investment,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
export function goodwillAt(acquisition, options, on) {
|
|
215
|
+
const rounding = options.rounding ?? "half-even";
|
|
216
|
+
const grossFunctional = acquisition.goodwillIn.functional;
|
|
217
|
+
const grossNciFunctional = acquisition.nciGoodwillIn.functional;
|
|
218
|
+
const sameCurrency = acquisition.functional.code === acquisition.presentation.code;
|
|
219
|
+
const zeroFunctional = Money.zero(acquisition.functional);
|
|
220
|
+
const closingRate = (when, what) => {
|
|
221
|
+
try {
|
|
222
|
+
return options.rates.lookup(acquisition.functional, acquisition.presentation, when).rate;
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
throw new GroupError(`${what} on ${acquisition.entity} is ${acquisition.functional.code} and has to be ` +
|
|
226
|
+
`stated in ${acquisition.presentation.code}, but there is no rate ` +
|
|
227
|
+
`on ${when}: ${error.message}`);
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
// The impairment comes off the measurement before any rate touches it: what
|
|
231
|
+
// is retranslated is what is left. Events after the carrying date are not
|
|
232
|
+
// yet news, and events inside the reporting period are this period's charge
|
|
233
|
+
// as well as part of the accumulated total.
|
|
234
|
+
const events = [...(options.impairments ?? [])]
|
|
235
|
+
.map((declared) => {
|
|
236
|
+
if (declared.entity !== acquisition.entity) {
|
|
237
|
+
throw new GroupError(`An impairment of goodwill on ${declared.entity} was given to ${acquisition.entity}`);
|
|
238
|
+
}
|
|
239
|
+
const when = parseDate(declared.on);
|
|
240
|
+
if (compareDates(when, acquisition.acquired) < 0) {
|
|
241
|
+
throw new GroupError(`Goodwill on ${acquisition.entity} is written off on ${when}, before it arose on ` +
|
|
242
|
+
`${acquisition.acquired}. There was nothing there to write off.`);
|
|
243
|
+
}
|
|
244
|
+
const amount = convertOn(declared.amount, acquisition.functional, {
|
|
245
|
+
rates: options.rates,
|
|
246
|
+
presentation: acquisition.presentation,
|
|
247
|
+
...(options.rounding === undefined ? {} : { rounding: options.rounding }),
|
|
248
|
+
}, when, `The impairment of goodwill on ${acquisition.entity}`);
|
|
249
|
+
if (amount.isNegative) {
|
|
250
|
+
throw new GroupError(`Goodwill on ${acquisition.entity} is written back up by ${amount.negated()} on ` +
|
|
251
|
+
`${when}. An impairment once recognised stays recognised.`);
|
|
252
|
+
}
|
|
253
|
+
return { when, amount };
|
|
254
|
+
})
|
|
255
|
+
.filter((event) => compareDates(event.when, on) <= 0)
|
|
256
|
+
.sort((a, b) => compareDates(a.when, b.when));
|
|
257
|
+
const inPeriod = (when) => options.period === undefined ||
|
|
258
|
+
(compareDates(when, options.period.from) >= 0 && compareDates(when, options.period.to) <= 0);
|
|
259
|
+
let impairedFunctional = zeroFunctional;
|
|
260
|
+
let chargedFunctional = zeroFunctional;
|
|
261
|
+
let nciImpairedFunctional = zeroFunctional;
|
|
262
|
+
let nciChargedFunctional = zeroFunctional;
|
|
263
|
+
// Each event's loss takes the rate on its own day, so the sum of them is
|
|
264
|
+
// what the income statement has carried over the asset's life and the
|
|
265
|
+
// residual against the closing rate is the rate's doing and nothing else.
|
|
266
|
+
let recognisedAtTheTime = Money.zero(acquisition.presentation);
|
|
267
|
+
let nciRecognisedAtTheTime = Money.zero(acquisition.presentation);
|
|
268
|
+
let chargeLoss = Money.zero(acquisition.presentation);
|
|
269
|
+
let nciChargeLoss = Money.zero(acquisition.presentation);
|
|
270
|
+
for (const event of events) {
|
|
271
|
+
// The stake bears its ownership fraction of the loss, not its share of the
|
|
272
|
+
// goodwill: a loss of the period is shared the way every result is. Where
|
|
273
|
+
// the stake was measured proportionately it has no goodwill on the balance
|
|
274
|
+
// sheet and so no recognised share of a write-down of it.
|
|
275
|
+
const nciAmount = acquisition.measurement === "fair-value"
|
|
276
|
+
? acquisition.nonControllingInterest.share(event.amount, rounding)
|
|
277
|
+
: zeroFunctional;
|
|
278
|
+
impairedFunctional = impairedFunctional.plus(event.amount);
|
|
279
|
+
nciImpairedFunctional = nciImpairedFunctional.plus(nciAmount);
|
|
280
|
+
if (impairedFunctional.greaterThan(grossFunctional)) {
|
|
281
|
+
throw new GroupError(`${impairedFunctional} of goodwill is written off ${acquisition.entity} by ${event.when}, ` +
|
|
282
|
+
`which only has ${grossFunctional}. Goodwill carried at a negative amount is not a ` +
|
|
283
|
+
`smaller error than the one being corrected.`);
|
|
284
|
+
}
|
|
285
|
+
if (nciImpairedFunctional.greaterThan(grossNciFunctional)) {
|
|
286
|
+
throw new GroupError(`The outside stake's share of the impairments on ${acquisition.entity} reaches ` +
|
|
287
|
+
`${nciImpairedFunctional} by ${event.when}, more than the ${grossNciFunctional} of ` +
|
|
288
|
+
`goodwill attributed to it. Writing its claim below what it was given would charge ` +
|
|
289
|
+
`it for goodwill it never had.`);
|
|
290
|
+
}
|
|
291
|
+
const eventRate = sameCurrency ? null : closingRate(event.when, "The impairment of goodwill");
|
|
292
|
+
const atTheTime = eventRate === null ? event.amount : eventRate.convert(event.amount, rounding);
|
|
293
|
+
const nciAtTheTime = eventRate === null ? nciAmount : eventRate.convert(nciAmount, rounding);
|
|
294
|
+
recognisedAtTheTime = recognisedAtTheTime.plus(atTheTime);
|
|
295
|
+
nciRecognisedAtTheTime = nciRecognisedAtTheTime.plus(nciAtTheTime);
|
|
296
|
+
if (inPeriod(event.when)) {
|
|
297
|
+
chargedFunctional = chargedFunctional.plus(event.amount);
|
|
298
|
+
nciChargedFunctional = nciChargedFunctional.plus(nciAmount);
|
|
299
|
+
chargeLoss = chargeLoss.plus(atTheTime);
|
|
300
|
+
nciChargeLoss = nciChargeLoss.plus(nciAtTheTime);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
void nciChargedFunctional;
|
|
304
|
+
const functional = grossFunctional.minus(impairedFunctional);
|
|
305
|
+
const nciFunctional = grossNciFunctional.minus(nciImpairedFunctional);
|
|
306
|
+
let rate = null;
|
|
307
|
+
let carrying = functional;
|
|
308
|
+
let nciCarrying = nciFunctional;
|
|
309
|
+
if (!sameCurrency) {
|
|
310
|
+
rate = closingRate(on, "Goodwill");
|
|
311
|
+
carrying = rate.convert(functional, rounding);
|
|
312
|
+
nciCarrying = rate.convert(nciFunctional, rounding);
|
|
313
|
+
}
|
|
314
|
+
// What the asset was recognised at, less everything taken out of it through
|
|
315
|
+
// the income statement over its life, each write-off at the rate on its own
|
|
316
|
+
// day. Whatever is left between that and the closing-rate carrying amount is
|
|
317
|
+
// the rate's doing and nothing else, which is what sends it to the reserve.
|
|
318
|
+
//
|
|
319
|
+
// It falls out of this that a write-off recognised at the closing rate leaves
|
|
320
|
+
// the exchange difference exactly as it would have been with no write-off at
|
|
321
|
+
// all: the asset is smaller and so is the figure it is measured against, by
|
|
322
|
+
// the same amount. That is the test that says the two effects are apart.
|
|
323
|
+
const atAcquisition = acquisition.goodwill.minus(recognisedAtTheTime);
|
|
324
|
+
const nciAtAcquisition = acquisition.nciGoodwill.minus(nciRecognisedAtTheTime);
|
|
325
|
+
const exchangeDifference = carrying.minus(atAcquisition);
|
|
326
|
+
const nciExchangeDifference = nciCarrying.minus(nciAtAcquisition);
|
|
327
|
+
return Object.freeze({
|
|
328
|
+
entity: acquisition.entity,
|
|
329
|
+
on,
|
|
330
|
+
functional,
|
|
331
|
+
grossFunctional,
|
|
332
|
+
impairedFunctional,
|
|
333
|
+
carrying,
|
|
334
|
+
atAcquisition,
|
|
335
|
+
exchangeDifference,
|
|
336
|
+
impairmentLoss: chargeLoss,
|
|
337
|
+
nciImpairmentLoss: nciChargeLoss,
|
|
338
|
+
groupImpairmentLoss: chargeLoss.minus(nciChargeLoss),
|
|
339
|
+
recognisedToDate: recognisedAtTheTime,
|
|
340
|
+
chargedFunctional,
|
|
341
|
+
nciFunctional,
|
|
342
|
+
nciCarrying,
|
|
343
|
+
nciAtAcquisition,
|
|
344
|
+
nciExchangeDifference,
|
|
345
|
+
groupExchangeDifference: exchangeDifference.minus(nciExchangeDifference),
|
|
346
|
+
rate,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
/** Every acquisition in a group, in the structure's order. */
|
|
350
|
+
export function acquisitions(group, ledgers, inputs, options) {
|
|
351
|
+
const seen = new Set();
|
|
352
|
+
for (const input of inputs) {
|
|
353
|
+
if (seen.has(input.entity)) {
|
|
354
|
+
throw new GroupError(`${input.entity} is acquired twice in the same group`);
|
|
355
|
+
}
|
|
356
|
+
seen.add(input.entity);
|
|
357
|
+
}
|
|
358
|
+
const missing = group
|
|
359
|
+
.consolidated()
|
|
360
|
+
.filter((e) => e.code !== group.parent && !seen.has(e.code))
|
|
361
|
+
.map((e) => e.code);
|
|
362
|
+
if (missing.length > 0) {
|
|
363
|
+
throw new GroupError(`No acquisition for ${missing.join(", ")}. Without what was paid there is no goodwill ` +
|
|
364
|
+
`and no way to eliminate the investment against the equity it bought.`);
|
|
365
|
+
}
|
|
366
|
+
const order = new Map(group.order.map((code, index) => [code, index]));
|
|
367
|
+
return Object.freeze([...inputs]
|
|
368
|
+
.sort((a, b) => (order.get(a.entity) ?? 0) - (order.get(b.entity) ?? 0))
|
|
369
|
+
.map((input) => acquisitionOf(group, ledgers, input, options)));
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* The workings behind goodwill, as a reader would check them.
|
|
373
|
+
*
|
|
374
|
+
* Where the acquired company keeps its books in another currency the figures
|
|
375
|
+
* are shown in both: the subsidiary's currency, which is where the subtraction
|
|
376
|
+
* happened, and the group's beside it. Showing only the translated column
|
|
377
|
+
* would leave a reader adding up three sterling figures that do not quite make
|
|
378
|
+
* the fourth, because each of them is a rounding of something else.
|
|
379
|
+
*/
|
|
380
|
+
export function renderAcquisition(acquisition) {
|
|
381
|
+
const lines = [];
|
|
382
|
+
const foreign = acquisition.functional.code !== acquisition.presentation.code;
|
|
383
|
+
const label = (text, amount, negated = false) => {
|
|
384
|
+
const take = (money) => (negated ? money.negated() : money).toDecimalString();
|
|
385
|
+
const presentation = take(amount.presentation).padStart(14);
|
|
386
|
+
if (!foreign)
|
|
387
|
+
return ` ${text.padEnd(44)}${presentation}`;
|
|
388
|
+
return ` ${text.padEnd(44)}${take(amount.functional).padStart(14)}${presentation}`;
|
|
389
|
+
};
|
|
390
|
+
lines.push(`${acquisition.entity} — acquired ${acquisition.acquired}, ` +
|
|
391
|
+
`${acquisition.groupInterest.toPercentString(4)} to the group, ` +
|
|
392
|
+
`non-controlling interest at ${acquisition.measurement}`);
|
|
393
|
+
if (foreign) {
|
|
394
|
+
lines.push(` ${"".padEnd(44)}${acquisition.functional.code.padStart(14)}` +
|
|
395
|
+
`${acquisition.presentation.code.padStart(14)}`);
|
|
396
|
+
}
|
|
397
|
+
lines.push(label("Consideration transferred", acquisition.considerationIn));
|
|
398
|
+
lines.push(label("Non-controlling interest at acquisition", acquisition.nciAtAcquisitionIn));
|
|
399
|
+
lines.push(label(`Net assets acquired${acquisition.netAssetsSupplied ? " (supplied)" : ""}`, acquisition.netAssetsAcquiredIn, true));
|
|
400
|
+
if (acquisition.bargainGain.isZero) {
|
|
401
|
+
lines.push(label("Goodwill", acquisition.goodwillIn));
|
|
402
|
+
if (!acquisition.nciGoodwill.isZero) {
|
|
403
|
+
lines.push(label(" of which attributable to the outside stake", acquisition.nciGoodwillIn));
|
|
404
|
+
}
|
|
405
|
+
if (foreign) {
|
|
406
|
+
lines.push(` Measured in ${acquisition.functional.code} and translated at the rate on ` +
|
|
407
|
+
`${acquisition.acquired}: goodwill on a foreign operation is an asset of that ` +
|
|
408
|
+
`operation.`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
lines.push(label("Gain on a bargain purchase", acquisition.bargainGainIn, true));
|
|
413
|
+
lines.push(" A price below the net assets acquired is a gain, not a negative asset.");
|
|
414
|
+
}
|
|
415
|
+
return lines.join("\n");
|
|
416
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bringing several sets of books onto one page.
|
|
3
|
+
*
|
|
4
|
+
* Aggregation is the mechanical half of consolidation and it has to happen
|
|
5
|
+
* before any of the interesting half can. Nothing is eliminated here and
|
|
6
|
+
* nothing is apportioned: every controlled entity's trial balance is restated
|
|
7
|
+
* into the group's presentation currency and added account by account, in
|
|
8
|
+
* full, including the parts of it that belong to somebody else. A subsidiary
|
|
9
|
+
* held 60% contributes 100% of its assets, because the group controls all of
|
|
10
|
+
* them; that the other 40% of the equity belongs to outside shareholders is
|
|
11
|
+
* settled later, as a claim, and not by adding up less of the balance sheet.
|
|
12
|
+
*
|
|
13
|
+
* Three things are worth being careful about.
|
|
14
|
+
*
|
|
15
|
+
* The first is that each entity's translation carries its own adjustment — the
|
|
16
|
+
* residual of translating assets at one rate, income at another and equity at
|
|
17
|
+
* a third — and those adjustments do not cancel. They are kept per entity as
|
|
18
|
+
* well as in total, because the group's translation reserve is the sum of the
|
|
19
|
+
* subsidiaries' and a reader is entitled to see which subsidiary moved it.
|
|
20
|
+
*
|
|
21
|
+
* The second is that not every entity's trial balance is read on the same day.
|
|
22
|
+
* A company the group sold in September has a balance sheet the group must
|
|
23
|
+
* account for and it is September's, not December's; asking its books what
|
|
24
|
+
* they say in December would be asking about somebody else's company. So each
|
|
25
|
+
* entity carries the date it was read at, and for every company still in the
|
|
26
|
+
* group that date is the reporting date.
|
|
27
|
+
*
|
|
28
|
+
* The third is that the same account code can mean different things in books
|
|
29
|
+
* that were never kept on one chart. Nothing here can fix that, but it can say
|
|
30
|
+
* so: where two entities give a code different names, the disagreement is
|
|
31
|
+
* reported rather than resolved by whichever entity happened to be first.
|
|
32
|
+
*/
|
|
33
|
+
import type { Currency } from "../money/currency.js";
|
|
34
|
+
import { Money } from "../money/money.js";
|
|
35
|
+
import type { RoundingMode } from "../money/rounding.js";
|
|
36
|
+
import type { AccountType } from "../accounts/types.js";
|
|
37
|
+
import type { CalendarDate, DateRange } from "../ledger/date.js";
|
|
38
|
+
import type { Ledger } from "../ledger/ledger.js";
|
|
39
|
+
import type { AverageMethod } from "../fx/average.js";
|
|
40
|
+
import type { RateTable } from "../fx/table.js";
|
|
41
|
+
import { type Translation } from "../fx/translate.js";
|
|
42
|
+
import { type GroupStructure } from "./structure.js";
|
|
43
|
+
import { type ControlWindow } from "./timeline.js";
|
|
44
|
+
/** The books of each entity, by entity code. */
|
|
45
|
+
export type EntityLedgers = ReadonlyMap<string, Ledger> | Readonly<Record<string, Ledger>>;
|
|
46
|
+
export interface CombinedRow {
|
|
47
|
+
readonly account: string;
|
|
48
|
+
readonly name: string;
|
|
49
|
+
readonly type: AccountType | null;
|
|
50
|
+
/** The line as the group reads it, in the presentation currency. */
|
|
51
|
+
readonly total: Money;
|
|
52
|
+
/** What each entity put into it. Entities contributing nothing are absent. */
|
|
53
|
+
readonly byEntity: ReadonlyMap<string, Money>;
|
|
54
|
+
}
|
|
55
|
+
export interface EntityContribution {
|
|
56
|
+
readonly entity: string;
|
|
57
|
+
readonly functional: Currency;
|
|
58
|
+
readonly translation: Translation;
|
|
59
|
+
/** That entity's share of the group's translation reserve. */
|
|
60
|
+
readonly translationAdjustment: Money;
|
|
61
|
+
/** How much of the reporting period the group controlled it for. */
|
|
62
|
+
readonly control: ControlWindow;
|
|
63
|
+
/**
|
|
64
|
+
* Whether that window was applied. False only when the caller asked for the
|
|
65
|
+
* whole period regardless, so a reader is never shown a window beside
|
|
66
|
+
* figures that ignore it.
|
|
67
|
+
*/
|
|
68
|
+
readonly windowApplied: boolean;
|
|
69
|
+
/**
|
|
70
|
+
* The result taken to reserves before translating, because it was earned
|
|
71
|
+
* before the group controlled the company. Nil for an entity held all period.
|
|
72
|
+
*/
|
|
73
|
+
readonly preAcquisitionResult: Money;
|
|
74
|
+
/**
|
|
75
|
+
* The date this entity's own books were read at. The reporting date for a
|
|
76
|
+
* company still in the group; the date control was lost for one that is not.
|
|
77
|
+
*/
|
|
78
|
+
readonly readAt: CalendarDate;
|
|
79
|
+
}
|
|
80
|
+
export interface NameConflict {
|
|
81
|
+
readonly account: string;
|
|
82
|
+
readonly names: readonly {
|
|
83
|
+
entity: string;
|
|
84
|
+
name: string;
|
|
85
|
+
}[];
|
|
86
|
+
}
|
|
87
|
+
export interface Aggregation {
|
|
88
|
+
readonly presentation: Currency;
|
|
89
|
+
readonly asAt: CalendarDate;
|
|
90
|
+
readonly period: DateRange;
|
|
91
|
+
readonly entities: readonly EntityContribution[];
|
|
92
|
+
readonly rows: readonly CombinedRow[];
|
|
93
|
+
/** The sum of the entities' translation adjustments. */
|
|
94
|
+
readonly translationAdjustment: Money;
|
|
95
|
+
readonly totalDebit: Money;
|
|
96
|
+
readonly totalCredit: Money;
|
|
97
|
+
readonly balanced: boolean;
|
|
98
|
+
/** Entities in the structure that were not added in, and why. */
|
|
99
|
+
readonly excluded: readonly {
|
|
100
|
+
entity: string;
|
|
101
|
+
reason: string;
|
|
102
|
+
}[];
|
|
103
|
+
readonly nameConflicts: readonly NameConflict[];
|
|
104
|
+
}
|
|
105
|
+
export interface AggregationOptions {
|
|
106
|
+
rates: RateTable;
|
|
107
|
+
/** Balance sheet date, and the date of the closing rate. */
|
|
108
|
+
asAt: string;
|
|
109
|
+
/** The reporting period the average rate covers. */
|
|
110
|
+
period?: DateRange;
|
|
111
|
+
averageMethod?: AverageMethod;
|
|
112
|
+
equityBasis?: "historical" | "closing";
|
|
113
|
+
/**
|
|
114
|
+
* Accounts to hold at the rate on the day each movement happened. The
|
|
115
|
+
* investment in a subsidiary belongs here: it is carried at what was paid,
|
|
116
|
+
* and what was paid is a fact about the day of the purchase.
|
|
117
|
+
*/
|
|
118
|
+
historicalAccounts?: readonly string[];
|
|
119
|
+
rounding?: RoundingMode;
|
|
120
|
+
includeZero?: boolean;
|
|
121
|
+
/**
|
|
122
|
+
* Where an entity's pre-acquisition result goes when its books are closed at
|
|
123
|
+
* the date control was obtained. Retained earnings by default.
|
|
124
|
+
*/
|
|
125
|
+
reserves?: string;
|
|
126
|
+
/**
|
|
127
|
+
* Add an entity acquired part-way through the period in full anyway, as it
|
|
128
|
+
* was before. Off by default: the honest answer is that only the part of the
|
|
129
|
+
* period the group controlled the company for is the group's.
|
|
130
|
+
*/
|
|
131
|
+
wholePeriodRegardless?: boolean;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Translate and add every controlled entity's trial balance.
|
|
135
|
+
*
|
|
136
|
+
* An entity in the structure with no books is an error rather than a zero: a
|
|
137
|
+
* consolidation that quietly left out a subsidiary because its file was
|
|
138
|
+
* missing would balance perfectly and be wrong.
|
|
139
|
+
*/
|
|
140
|
+
export declare function aggregate(group: GroupStructure, ledgers: EntityLedgers, options: AggregationOptions): Aggregation;
|
|
141
|
+
/** A row's contribution from one entity, or zero. */
|
|
142
|
+
export declare function contribution(row: CombinedRow, entity: string): Money;
|
|
143
|
+
export declare function renderAggregation(result: Aggregation): string;
|