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.
Files changed (213) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1493 -2
  3. package/dist/accounts/chart.d.ts +96 -0
  4. package/dist/accounts/chart.js +244 -0
  5. package/dist/accounts/index.d.ts +3 -0
  6. package/dist/accounts/index.js +3 -0
  7. package/dist/accounts/standard.d.ts +12 -0
  8. package/dist/accounts/standard.js +95 -0
  9. package/dist/accounts/types.d.ts +20 -0
  10. package/dist/accounts/types.js +40 -0
  11. package/dist/cli/args.d.ts +45 -0
  12. package/dist/cli/args.js +147 -0
  13. package/dist/cli/index.d.ts +2 -0
  14. package/dist/cli/index.js +2 -0
  15. package/dist/cli/main.d.ts +6 -0
  16. package/dist/cli/main.js +25 -0
  17. package/dist/cli/run.d.ts +34 -0
  18. package/dist/cli/run.js +1660 -0
  19. package/dist/dashboard/charts.d.ts +38 -0
  20. package/dist/dashboard/charts.js +173 -0
  21. package/dist/dashboard/index.d.ts +3 -0
  22. package/dist/dashboard/index.js +3 -0
  23. package/dist/dashboard/model.d.ts +137 -0
  24. package/dist/dashboard/model.js +190 -0
  25. package/dist/dashboard/render.d.ts +23 -0
  26. package/dist/dashboard/render.js +231 -0
  27. package/dist/dashboard/script.d.ts +28 -0
  28. package/dist/dashboard/script.js +704 -0
  29. package/dist/dashboard/styles.d.ts +14 -0
  30. package/dist/dashboard/styles.js +726 -0
  31. package/dist/demo/disposal.d.ts +80 -0
  32. package/dist/demo/disposal.js +215 -0
  33. package/dist/demo/evidence.d.ts +153 -0
  34. package/dist/demo/evidence.js +348 -0
  35. package/dist/demo/foreign.d.ts +29 -0
  36. package/dist/demo/foreign.js +113 -0
  37. package/dist/demo/foreignReport.d.ts +10 -0
  38. package/dist/demo/foreignReport.js +89 -0
  39. package/dist/demo/generator.d.ts +108 -0
  40. package/dist/demo/generator.js +445 -0
  41. package/dist/demo/group.d.ts +70 -0
  42. package/dist/demo/group.js +195 -0
  43. package/dist/demo/groupPeriods.d.ts +27 -0
  44. package/dist/demo/groupPeriods.js +125 -0
  45. package/dist/demo/groupReport.d.ts +13 -0
  46. package/dist/demo/groupReport.js +87 -0
  47. package/dist/demo/ingest.d.ts +6 -0
  48. package/dist/demo/ingest.js +61 -0
  49. package/dist/demo/main.d.ts +6 -0
  50. package/dist/demo/main.js +7 -0
  51. package/dist/demo/mainDisposal.d.ts +1 -0
  52. package/dist/demo/mainDisposal.js +2 -0
  53. package/dist/demo/mainEvidence.d.ts +8 -0
  54. package/dist/demo/mainEvidence.js +9 -0
  55. package/dist/demo/mainForeign.d.ts +6 -0
  56. package/dist/demo/mainForeign.js +7 -0
  57. package/dist/demo/mainGroup.d.ts +6 -0
  58. package/dist/demo/mainGroup.js +7 -0
  59. package/dist/demo/mainGroupPeriods.d.ts +6 -0
  60. package/dist/demo/mainGroupPeriods.js +7 -0
  61. package/dist/demo/mainIngest.d.ts +6 -0
  62. package/dist/demo/mainIngest.js +7 -0
  63. package/dist/demo/mainMidYear.d.ts +6 -0
  64. package/dist/demo/mainMidYear.js +7 -0
  65. package/dist/demo/mainReconcile.d.ts +6 -0
  66. package/dist/demo/mainReconcile.js +7 -0
  67. package/dist/demo/mainStatements.d.ts +6 -0
  68. package/dist/demo/mainStatements.js +7 -0
  69. package/dist/demo/midYear.d.ts +51 -0
  70. package/dist/demo/midYear.js +162 -0
  71. package/dist/demo/month.d.ts +13 -0
  72. package/dist/demo/month.js +142 -0
  73. package/dist/demo/random.d.ts +48 -0
  74. package/dist/demo/random.js +94 -0
  75. package/dist/demo/receivables.d.ts +36 -0
  76. package/dist/demo/receivables.js +104 -0
  77. package/dist/demo/reconcile.d.ts +20 -0
  78. package/dist/demo/reconcile.js +104 -0
  79. package/dist/demo/report.d.ts +6 -0
  80. package/dist/demo/report.js +26 -0
  81. package/dist/demo/statement.d.ts +13 -0
  82. package/dist/demo/statement.js +51 -0
  83. package/dist/demo/statements.d.ts +6 -0
  84. package/dist/demo/statements.js +29 -0
  85. package/dist/demo/supplierRun.d.ts +46 -0
  86. package/dist/demo/supplierRun.js +94 -0
  87. package/dist/demo/unrealisedEvidence.d.ts +51 -0
  88. package/dist/demo/unrealisedEvidence.js +183 -0
  89. package/dist/fx/average.d.ts +60 -0
  90. package/dist/fx/average.js +139 -0
  91. package/dist/fx/document.d.ts +60 -0
  92. package/dist/fx/document.js +205 -0
  93. package/dist/fx/exposure.d.ts +108 -0
  94. package/dist/fx/exposure.js +262 -0
  95. package/dist/fx/index.d.ts +8 -0
  96. package/dist/fx/index.js +8 -0
  97. package/dist/fx/rate.d.ts +77 -0
  98. package/dist/fx/rate.js +200 -0
  99. package/dist/fx/revaluation.d.ts +92 -0
  100. package/dist/fx/revaluation.js +197 -0
  101. package/dist/fx/settlement.d.ts +84 -0
  102. package/dist/fx/settlement.js +162 -0
  103. package/dist/fx/table.d.ts +120 -0
  104. package/dist/fx/table.js +271 -0
  105. package/dist/fx/translate.d.ts +126 -0
  106. package/dist/fx/translate.js +258 -0
  107. package/dist/group/accounts.d.ts +74 -0
  108. package/dist/group/accounts.js +179 -0
  109. package/dist/group/acquisition.d.ts +324 -0
  110. package/dist/group/acquisition.js +416 -0
  111. package/dist/group/aggregate.d.ts +143 -0
  112. package/dist/group/aggregate.js +288 -0
  113. package/dist/group/comparative.d.ts +102 -0
  114. package/dist/group/comparative.js +166 -0
  115. package/dist/group/consolidate.d.ts +196 -0
  116. package/dist/group/consolidate.js +635 -0
  117. package/dist/group/disposal.d.ts +181 -0
  118. package/dist/group/disposal.js +233 -0
  119. package/dist/group/document.d.ts +41 -0
  120. package/dist/group/document.js +326 -0
  121. package/dist/group/index.d.ts +12 -0
  122. package/dist/group/index.js +12 -0
  123. package/dist/group/intercompany.d.ts +136 -0
  124. package/dist/group/intercompany.js +238 -0
  125. package/dist/group/interest.d.ts +109 -0
  126. package/dist/group/interest.js +224 -0
  127. package/dist/group/movement.d.ts +206 -0
  128. package/dist/group/movement.js +432 -0
  129. package/dist/group/structure.d.ts +151 -0
  130. package/dist/group/structure.js +327 -0
  131. package/dist/group/timeline.d.ts +109 -0
  132. package/dist/group/timeline.js +169 -0
  133. package/dist/group/unrealised.d.ts +165 -0
  134. package/dist/group/unrealised.js +314 -0
  135. package/dist/index.d.ts +10 -0
  136. package/dist/index.js +10 -0
  137. package/dist/ledger/close.d.ts +75 -0
  138. package/dist/ledger/close.js +142 -0
  139. package/dist/ledger/date.d.ts +48 -0
  140. package/dist/ledger/date.js +141 -0
  141. package/dist/ledger/entry.d.ts +150 -0
  142. package/dist/ledger/entry.js +264 -0
  143. package/dist/ledger/index.d.ts +6 -0
  144. package/dist/ledger/index.js +6 -0
  145. package/dist/ledger/ledger.d.ts +101 -0
  146. package/dist/ledger/ledger.js +308 -0
  147. package/dist/ledger/serialise.d.ts +68 -0
  148. package/dist/ledger/serialise.js +204 -0
  149. package/dist/ledger/trialBalance.d.ts +49 -0
  150. package/dist/ledger/trialBalance.js +103 -0
  151. package/dist/money/currency.d.ts +37 -0
  152. package/dist/money/currency.js +67 -0
  153. package/dist/money/index.d.ts +3 -0
  154. package/dist/money/index.js +3 -0
  155. package/dist/money/money.d.ts +106 -0
  156. package/dist/money/money.js +300 -0
  157. package/dist/money/rounding.d.ts +41 -0
  158. package/dist/money/rounding.js +117 -0
  159. package/dist/reconcile/accuracy.d.ts +65 -0
  160. package/dist/reconcile/accuracy.js +162 -0
  161. package/dist/reconcile/assignment.d.ts +83 -0
  162. package/dist/reconcile/assignment.js +326 -0
  163. package/dist/reconcile/bankView.d.ts +65 -0
  164. package/dist/reconcile/bankView.js +86 -0
  165. package/dist/reconcile/bridge.d.ts +76 -0
  166. package/dist/reconcile/bridge.js +129 -0
  167. package/dist/reconcile/candidates.d.ts +84 -0
  168. package/dist/reconcile/candidates.js +174 -0
  169. package/dist/reconcile/decisions.d.ts +116 -0
  170. package/dist/reconcile/decisions.js +168 -0
  171. package/dist/reconcile/index.d.ts +12 -0
  172. package/dist/reconcile/index.js +12 -0
  173. package/dist/reconcile/matcher.d.ts +89 -0
  174. package/dist/reconcile/matcher.js +306 -0
  175. package/dist/reconcile/memory.d.ts +120 -0
  176. package/dist/reconcile/memory.js +281 -0
  177. package/dist/reconcile/posting.d.ts +185 -0
  178. package/dist/reconcile/posting.js +481 -0
  179. package/dist/reconcile/scoring.d.ts +98 -0
  180. package/dist/reconcile/scoring.js +319 -0
  181. package/dist/reconcile/similarity.d.ts +88 -0
  182. package/dist/reconcile/similarity.js +272 -0
  183. package/dist/reconcile/subsetSum.d.ts +61 -0
  184. package/dist/reconcile/subsetSum.js +136 -0
  185. package/dist/reports/ageing.d.ts +75 -0
  186. package/dist/reports/ageing.js +149 -0
  187. package/dist/reports/balanceSheet.d.ts +57 -0
  188. package/dist/reports/balanceSheet.js +107 -0
  189. package/dist/reports/incomeStatement.d.ts +59 -0
  190. package/dist/reports/incomeStatement.js +132 -0
  191. package/dist/reports/index.d.ts +4 -0
  192. package/dist/reports/index.js +4 -0
  193. package/dist/reports/period.d.ts +37 -0
  194. package/dist/reports/period.js +78 -0
  195. package/dist/statement/amount.d.ts +55 -0
  196. package/dist/statement/amount.js +211 -0
  197. package/dist/statement/columns.d.ts +39 -0
  198. package/dist/statement/columns.js +221 -0
  199. package/dist/statement/csv.d.ts +50 -0
  200. package/dist/statement/csv.js +185 -0
  201. package/dist/statement/dates.d.ts +52 -0
  202. package/dist/statement/dates.js +204 -0
  203. package/dist/statement/duplicates.d.ts +51 -0
  204. package/dist/statement/duplicates.js +101 -0
  205. package/dist/statement/import.d.ts +52 -0
  206. package/dist/statement/import.js +172 -0
  207. package/dist/statement/index.d.ts +20 -0
  208. package/dist/statement/index.js +32 -0
  209. package/dist/statement/line.d.ts +60 -0
  210. package/dist/statement/line.js +73 -0
  211. package/dist/statement/ofx.d.ts +47 -0
  212. package/dist/statement/ofx.js +199 -0
  213. 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;