@tasenor/common-node 1.12.0 → 1.13.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 (115) hide show
  1. package/dist/src/cli/cli.js.map +1 -1
  2. package/dist/src/cli/mini-util.d.ts +1 -1
  3. package/dist/src/cli/mini-util.js.map +1 -1
  4. package/dist/src/commands/account.js.map +1 -1
  5. package/dist/src/commands/balance.js.map +1 -1
  6. package/dist/src/commands/db.js.map +1 -1
  7. package/dist/src/commands/entry.js.map +1 -1
  8. package/dist/src/commands/import.js.map +1 -1
  9. package/dist/src/commands/importer.js +1 -1
  10. package/dist/src/commands/importer.js.map +1 -1
  11. package/dist/src/commands/index.js.map +1 -1
  12. package/dist/src/commands/period.js.map +1 -1
  13. package/dist/src/commands/plugin.js.map +1 -1
  14. package/dist/src/commands/report.js.map +1 -1
  15. package/dist/src/commands/settings.js.map +1 -1
  16. package/dist/src/commands/stock.js.map +1 -1
  17. package/dist/src/commands/tag.js.map +1 -1
  18. package/dist/src/commands/tx.js.map +1 -1
  19. package/dist/src/commands/user.js.map +1 -1
  20. package/dist/src/data/csv.d.ts +5 -0
  21. package/dist/src/data/csv.js +28 -0
  22. package/dist/src/data/csv.js.map +1 -0
  23. package/dist/src/data/index.d.ts +1 -0
  24. package/dist/src/data/index.js +1 -0
  25. package/dist/src/data/index.js.map +1 -1
  26. package/dist/src/data/utils.js.map +1 -1
  27. package/dist/src/database/BookkeeperImporter.d.ts +1 -0
  28. package/dist/src/database/BookkeeperImporter.js +13 -12
  29. package/dist/src/database/BookkeeperImporter.js.map +1 -1
  30. package/dist/src/database/DB.js +2 -1
  31. package/dist/src/database/DB.js.map +1 -1
  32. package/dist/src/export/Exporter.d.ts +2 -2
  33. package/dist/src/export/Exporter.js +0 -2
  34. package/dist/src/export/Exporter.js.map +1 -1
  35. package/dist/src/export/TasenorExporter.js.map +1 -1
  36. package/dist/src/export/TilitinExporter.js.map +1 -1
  37. package/dist/src/import/TextFileProcessHandler.js +2 -2
  38. package/dist/src/import/TextFileProcessHandler.js.map +1 -1
  39. package/dist/src/import/TransactionImportConnector.js.map +1 -1
  40. package/dist/src/import/TransactionImportHandler.d.ts +4 -26
  41. package/dist/src/import/TransactionImportHandler.js +8 -27
  42. package/dist/src/import/TransactionImportHandler.js.map +1 -1
  43. package/dist/src/import/TransactionRules.d.ts +1 -172
  44. package/dist/src/import/TransactionRules.js +8 -172
  45. package/dist/src/import/TransactionRules.js.map +1 -1
  46. package/dist/src/import/TransactionUI.js +16 -2
  47. package/dist/src/import/TransactionUI.js.map +1 -1
  48. package/dist/src/import/TransferAnalyzer.js +27 -6
  49. package/dist/src/import/TransferAnalyzer.js.map +1 -1
  50. package/dist/src/net/git.d.ts +10 -1
  51. package/dist/src/net/git.js +45 -13
  52. package/dist/src/net/git.js.map +1 -1
  53. package/dist/src/net/middleware.js.map +1 -1
  54. package/dist/src/net/tokens.js.map +1 -1
  55. package/dist/src/net/vault.js.map +1 -1
  56. package/dist/src/plugins/BackendPlugin.d.ts +1 -1
  57. package/dist/src/plugins/BackendPlugin.js.map +1 -1
  58. package/dist/src/plugins/DataPlugin.js.map +1 -1
  59. package/dist/src/plugins/ImportPlugin.d.ts +4 -1
  60. package/dist/src/plugins/ImportPlugin.js +2 -0
  61. package/dist/src/plugins/ImportPlugin.js.map +1 -1
  62. package/dist/src/plugins/LanguageBackendPlugin.d.ts +15 -0
  63. package/dist/src/plugins/LanguageBackendPlugin.js +18 -0
  64. package/dist/src/plugins/LanguageBackendPlugin.js.map +1 -0
  65. package/dist/src/plugins/ReportPlugin.d.ts +8 -3
  66. package/dist/src/plugins/ReportPlugin.js +38 -4
  67. package/dist/src/plugins/ReportPlugin.js.map +1 -1
  68. package/dist/src/plugins/ServicePlugin.js.map +1 -1
  69. package/dist/src/plugins/index.d.ts +1 -0
  70. package/dist/src/plugins/index.js +1 -0
  71. package/dist/src/plugins/index.js.map +1 -1
  72. package/dist/src/plugins/plugins.d.ts +2 -2
  73. package/dist/src/plugins/plugins.js +27 -5
  74. package/dist/src/plugins/plugins.js.map +1 -1
  75. package/dist/src/process/Process.js +4 -4
  76. package/dist/src/process/Process.js.map +1 -1
  77. package/dist/src/process/ProcessFile.js.map +1 -1
  78. package/dist/src/process/ProcessHandler.d.ts +0 -6
  79. package/dist/src/process/ProcessStep.js.map +1 -1
  80. package/dist/src/process/ProcessingSystem.d.ts +2 -8
  81. package/dist/src/process/ProcessingSystem.js +17 -31
  82. package/dist/src/process/ProcessingSystem.js.map +1 -1
  83. package/dist/src/reports/conversions.js +0 -3
  84. package/dist/src/reports/conversions.js.map +1 -1
  85. package/dist/src/server/ISPDemoServer.js.map +1 -1
  86. package/dist/src/server/api.js.map +1 -1
  87. package/dist/src/server/index.d.ts +1 -0
  88. package/dist/src/server/index.js +1 -0
  89. package/dist/src/server/index.js.map +1 -1
  90. package/dist/src/server/router.js.map +1 -1
  91. package/dist/src/server/server.d.ts +11 -0
  92. package/dist/src/server/server.js +39 -0
  93. package/dist/src/server/server.js.map +1 -0
  94. package/dist/src/system.d.ts +1 -1
  95. package/dist/src/system.js +8 -3
  96. package/dist/src/system.js.map +1 -1
  97. package/package.json +26 -27
  98. package/src/cli/mini-util.ts +6 -6
  99. package/src/commands/importer.ts +1 -1
  100. package/src/commands/index.ts +1 -1
  101. package/src/database/DB.ts +3 -1
  102. package/src/export/Exporter.ts +5 -7
  103. package/src/import/TransactionImportHandler.ts +9 -27
  104. package/src/import/TransactionRules.ts +8 -172
  105. package/src/import/TransactionUI.ts +16 -2
  106. package/src/import/TransferAnalyzer.ts +38 -7
  107. package/src/plugins/BackendPlugin.ts +1 -1
  108. package/src/plugins/ImportPlugin.ts +2 -2
  109. package/src/plugins/LanguageBackendPlugin.ts +22 -0
  110. package/src/plugins/ReportPlugin.ts +40 -4
  111. package/src/plugins/index.ts +1 -0
  112. package/src/process/Process.ts +4 -4
  113. package/src/process/ProcessHandler.ts +0 -7
  114. package/src/process/ProcessingSystem.ts +18 -31
  115. package/src/reports/conversions.ts +0 -3
@@ -11,33 +11,7 @@ import { BadState, InvalidFile, NotImplemented, SystemError } from '../error'
11
11
  import clone from 'clone'
12
12
 
13
13
  /**
14
- * ## Importing
15
- *
16
14
  * This module has types used in the importing data to the system using Interactive Stateful Process.
17
- *
18
- * ### Importing Process
19
- *
20
- * The import processing goes through the following stages:
21
- *
22
- * ```mermaid
23
- * flowchart LR
24
- * START --> segmentation
25
- * segmentation --> classification
26
- * classification --> analyze
27
- * analyze --> execution
28
- * execution --> END
29
- * ```
30
- *
31
- * 1. **Segmentation**: Split incoming data to smaller pieces so that parts of the original data are grouped
32
- * together under unique *segmentation ID*. Each group present some meaningful unit of information
33
- * belonging together called *segment*.
34
- * 2. **Classification**: For each segment, determine which *class* it belongs to. Classes are some
35
- * defined set of qualities describing similar segments.
36
- * 3. **Analyze**: Based on the classification, additional information can be collected and/or calculated
37
- * for each segment.
38
- * 4. **Execution**: Once information for each segment is complete, we can execute database insertions,
39
- * REST API calls or whatever appropriate actions based on the segement data.
40
- *
41
15
  */
42
16
  export class TransactionImportHandler extends TextFileProcessHandler {
43
17
 
@@ -180,6 +154,13 @@ export class TransactionImportHandler extends TextFileProcessHandler {
180
154
  return newState
181
155
  }
182
156
 
157
+ /**
158
+ * Hook to post process column values.
159
+ */
160
+ async segmentationColumnPostProcess(columns: Record<string, unknown>): Promise<Record<string, unknown>> {
161
+ return columns
162
+ }
163
+
183
164
  /**
184
165
  * Hook to do some post proccessing for segmentation process. Collects standard fields.
185
166
  * @param state
@@ -208,7 +189,7 @@ export class TransactionImportHandler extends TextFileProcessHandler {
208
189
  // Build standard fields.
209
190
  const { textField, totalAmountField } = this.importOptions
210
191
  for (let n = 0; n < state.files[fileName].lines.length; n++) {
211
- const { columns, segmentId } = state.files[fileName].lines[n]
192
+ let { columns, segmentId } = state.files[fileName].lines[n]
212
193
  for (const name of this.importOptions.requiredFields) {
213
194
  if (columns[name] === undefined) {
214
195
  columns[name] = ''
@@ -238,6 +219,7 @@ export class TransactionImportHandler extends TextFileProcessHandler {
238
219
  if (totalAmountField) {
239
220
  columns._totalAmountField = columns[totalAmountField]
240
221
  }
222
+ columns = await this.segmentationColumnPostProcess(columns) as Record<string, string>
241
223
  }
242
224
  }
243
225
 
@@ -5,178 +5,7 @@ import clone from 'clone'
5
5
  import { BadState, SystemError } from '../error'
6
6
 
7
7
  /**
8
- * ## Transaction rule system
9
- *
10
- * The classification of the import data uses rule system describing how to transform segmented
11
- * data to *transfers*, i.e. generic description of bookkeeping events. Initially in the beginning
12
- * of the processing the settings and rules defined for the particular importer are copied to the
13
- * initial state of the process. During the processing we may ask questions and add more information
14
- * to the process.
15
- *
16
- * So the structure of the configration is
17
- * ```json
18
- * {
19
- * "language": "fi",
20
- * "currency": "EUR",
21
- * ...
22
- * "account.income.currency.EUR": "1910",
23
- * ...
24
- * "rules": [...],
25
- * "questions": {...},
26
- * "answers": {...}
27
- * }
28
- * ```
29
- * There are
30
- * 1. Generic universal settings like *language* or *currency*.
31
- * 2. Then there is an account and import setting configuration that has been possibly resolved during the import process
32
- * by asking from user, but which will also apply universally afterwards and are copied to the future import
33
- * configuration.
34
- * 3. **Rules** section defines how to map segments to transfers.
35
- * 4. **Question** section defines UI questions to resolve some cases, that always require user interaction and
36
- * cannot be resolved automatically.
37
- * 5. **Answers** section is a collection of responses to questions stored by each segment. They are not universally
38
- * copied to the importer, but are only relevant the current import only.
39
- *
40
- * ### Settings
41
- *
42
- * The following general settings are used
43
- * - `currency` - A main currency of the bookkeeping database.
44
- * - `language` - A translation language for the imported texts.
45
- * - `tags.*.*.*` - A list of tags to be added for every transaction descriptions. Also some specific tags
46
- * can be specified, since `*.*.*` uses the same convention than account configurations.
47
- *
48
- * Accounts are defined as the following.
49
- * - `account.<reason>.<type>.<asset>` - Defines the account number to be used for the given purpose.
50
- * Parts can be `'*'` to allow any purpose. Otherwise they are
51
- * explained in more detail in {@link TransferAnalyzer}.
52
- *
53
- * Miscellaneous optional settings:
54
- * - `isTradeFeePartOfTotal` If set to `true`, assume that trading fee is included in the total.
55
- * Otherwise it is assumed to be paid on the top of the total.
56
- * - `recordDeposits` If set to false, skip deposits.
57
- * - `recordWithdrawals` If set to false, skip withdrawls.
58
- * - `allowShortSelling` If set, allow short selling, i.e. selling assets we don't have.
59
- *
60
- * #### Example
61
- * ```json
62
- * {
63
- * "currency": "EUR",
64
- * "language": "en",
65
- * "tags.*.*.*": ["Lynx"],
66
- * "account.deposit.currency.EUR": "1918",
67
- * "account.deposit.external.EUR": "9999",
68
- * "account.withdrawal.currency.EUR": "1918",
69
- * "account.withdrawal.external.EUR": "9999",
70
- * "account.expense.statement.INTEREST_EXPENSE": "9550",
71
- * "account.expense.currency.EUR": "1918",
72
- *
73
- * "rules": [],
74
- * "questions": [],
75
- * "answers": {}
76
- * }
77
- * ```
78
- *
79
- * ### Rules
80
- *
81
- * Rules sections is a list of rule definitions of form
82
- * ```json
83
- * {
84
- * "name": "Name of the rule",
85
- * "filter": "<expression>",
86
- * "comment": "<optional description>",
87
- * "options": {
88
- * <optional flags>
89
- * },
90
- * "result": [
91
- * <transfer1>, <transfer2>...
92
- * ]
93
- * }
94
- * ```
95
- * The *name* is any string describing the rule. Rules are used so that each segment resulting from the segmentation
96
- * step are handled in the order of their timestamps. Lines belonging to the segment are offered one by one to the
97
- * *filter* expression and if returning true, the entries in *result* are concatenated together. Each entry in the
98
- * result is a *transfer description*.
99
- *
100
- * The filtering and result expressions has various variables set during the processing. All variables from
101
- * the segmentation is included. Typically they are the same as the column names in the CSV file for example.
102
- * See {@link TransactionRules.classifyLines} for other variables available.
103
- *
104
- * The structure of transfers are explained in {@link TransferAnalyzer}.
105
- *
106
- * The syntax of the filter and result is explained in {@link RulesEngine}.
107
- *
108
- * Currently one one boolean option is supported: `singleMatch` which means that matching any of the lines in the
109
- * segment suffices and the parsing result is returned immediately when matching rule is found. The rest of the
110
- * lines are ignored. It is useful when for example using `sum(lines, 'field')` to gather values from all lines
111
- * of the segment at once.
112
- *
113
- * ### Questions
114
- *
115
- * There are situation, where importer cannot deduct some part of the transfer automatically. In that case we can
116
- * define a question that needs to be answered every time, when the matching rule has been found. For example
117
- * we may determine based on the transaction data that it is related to computers but we want to know the exact
118
- * type of the purchase. Then we can define a question
119
- * ```json
120
- * {
121
- * "name": "Computer purchase",
122
- * "label": "What category is the purchase",
123
- * "ask": {
124
- * "Hardware equipment": "HARDWARE",
125
- * "Software": "SOFTWARE"
126
- * }
127
- * },
128
- * ```
129
- *
130
- * The question can be used in the transfer as explained in {@link TransferAnalyzer}.
131
- *
132
- * Different question types are documented in {@link TransactionUI.parseQuery}.
133
- *
134
- * ### Answers
135
- *
136
- * This section collects answers given earlier during the processing. They are grouped per segment ID per transfer.
137
- * For example
138
- * ```json
139
- * "d3e89d9af37dda4609bed94770fc5c52be946175": {
140
- * "type": "HARDWARE"
141
- * },
142
- * ```
143
- * It may contain also complete transaction definition, which will override all parsing
144
- * ```
145
- * "581e46d024678ddcddc01ae36369bf6fc54f16b2": {
146
- * "transfers": [
147
- * {
148
- * "data": {
149
- * "text": "Payment for something"
150
- * },
151
- * "type": "account",
152
- * "asset": "8650",
153
- * "amount": 59.27,
154
- * "reason": "expense"
155
- * },
156
- * {
157
- * "type": "account",
158
- * "asset": "3020",
159
- * "amount": -59.27,
160
- * "reason": "expense"
161
- * }
162
- * ]
163
- * }
164
- * ```
165
- * There is also a global answer section applied to all imports. If an asset has changed its name, it can be
166
- * stored like this in the empty segment ID:
167
- * ```
168
- * "": {
169
- * "asset-renaming": [
170
- * {
171
- * "date": "<YYYY-MM-DD>"
172
- * "type": "stock",
173
- * "old": "<OLD ASSET>"
174
- * "new": "<NEW ASSET>"
175
- * }
176
- * ]
177
- * }
178
- * }
179
- * ```
8
+ * Rule handler.
180
9
  */
181
10
  export class TransactionRules {
182
11
  // TODO: Could also define extension from ProcessConfig configuration type definition and start building precise description.
@@ -391,6 +220,13 @@ export class TransactionRules {
391
220
 
392
221
  // Decide the error when passing through without finding an answer.
393
222
  if (matched) {
223
+ if (await this.UI.getBoolean(config, 'allowEmptyResults', 'Match was found but actually no transfers found. Should we allow this and ignore the match?')) {
224
+ return {
225
+ type: 'transfers',
226
+ transfers: [],
227
+ transactions: [],
228
+ }
229
+ }
394
230
  throw new Error(`Found matches but the result list is empty for ${JSON.stringify(lines)}.`)
395
231
  }
396
232
  throw new Error(`Could not find rules matching ${JSON.stringify(lines)}.`)
@@ -88,7 +88,21 @@ export class TransactionUI {
88
88
  const ans = await this.getSegmentAnswer(config, segment, `hasBeenRenamed.${type}.${asset}`) as undefined | boolean
89
89
 
90
90
  if (ans === undefined) {
91
- throw new AskUI(await this.message(`Asset renaming question not implemented (avoid error for now by setting answer 'hasBeenRenamed.${type}.${asset}' for segment '${segment.id}').`, 'error'))
91
+ const text = await this.getTranslation("We don't seem to have any history for an asset '{asset}' of type '{type}'. Has it been renamed at some point?", config.language as Language)
92
+ const msg = await this.message(text.replace('{asset}', asset).replace('{type}', type), 'info')
93
+ throw new AskUI({
94
+ type: 'flat',
95
+ elements: [
96
+ msg,
97
+ {
98
+ type: 'yesno',
99
+ name: `answer.${segment.id}.hasBeenRenamed.${type}.${asset}`,
100
+ label: '',
101
+ actions: {}
102
+ },
103
+ await this.submit('Continue', 2, config.language as Language)
104
+ ]
105
+ })
92
106
  }
93
107
  return ans
94
108
  }
@@ -166,7 +180,7 @@ export class TransactionUI {
166
180
  if (defaultAccount) {
167
181
  ui.defaultValue = defaultAccount
168
182
  } else {
169
- const canditates = await this.deps.getAccountCanditates(account, { ...config, plugin: config.handlers instanceof Array && config.handlers.length ? config.handlers[0] as PluginCode : undefined })
183
+ const canditates = await this.deps.getAccountCanditates(account, { ...config, plugin: config.handler })
170
184
  if (canditates.length) {
171
185
  ui.defaultValue = canditates[0]
172
186
  if (canditates.length > 1) {
@@ -394,7 +394,7 @@ export class TransferAnalyzer {
394
394
  kind = 'investment'
395
395
  const statementEntry = shouldHaveOne('investment', 'statement')
396
396
  values.name = await this.getTranslation(`income-${statementEntry.asset}`)
397
- } else if (weHave(['expense'], ['currency', 'statement']) || weHave(['expense', 'tax'], ['currency', 'statement'])) {
397
+ } else if (weHave(['expense'], ['currency', 'statement']) || weHave(['expense'], ['crypto', 'statement']) || weHave(['expense', 'tax'], ['currency', 'statement'])) {
398
398
  kind = 'expense'
399
399
  const statementEntry = shouldHaveOne('expense', 'statement')
400
400
  values.name = await this.getTranslation(`expense-${statementEntry.asset}`)
@@ -722,6 +722,7 @@ export class TransferAnalyzer {
722
722
  // Ot at least pass through to the short selling question.
723
723
  const renamed = await this.UI.askedRenamingOrThrow(this.config, segment, transfer.type, transfer.asset)
724
724
  if (renamed === true) {
725
+ // TODO: Need to implement this.
725
726
  throw new SystemError(`Something went wrong. Asset ${transfer.type} ${transfer.asset} has been renamed but we did not encounter actual transaction for the renaming.`)
726
727
  }
727
728
  }
@@ -741,6 +742,9 @@ export class TransferAnalyzer {
741
742
  // Normal selling is valued by the value of the asset in our stock.
742
743
  transfer.value = Math.round(transferAmount * (value / amount))
743
744
  if (!transfer.value) {
745
+ if (isNaN(transfer.value)) {
746
+ throw new SystemError(`Calculation of value for ${transfer.type} ${transfer.asset} failed (transferAmount=${JSON.stringify(transferAmount)}, value=${JSON.stringify(value)}, amount=${JSON.stringify(amount)}.`)
747
+ }
744
748
  throw new SystemError(`Asset ${transfer.type} ${transfer.asset} have no value left when trading on ${segment.time}.`)
745
749
  }
746
750
  }
@@ -885,7 +889,14 @@ export class TransferAnalyzer {
885
889
  } else {
886
890
  throw new Error(`Handling non-fee '${nonFee}' not implemented.`)
887
891
  }
888
- feeIsMissingFromTotal = !await this.UI.getBoolean(config, variable, 'Is transaction fee of type {type} already included in the {reason} total?'.replace('{type}', `${feeType}`).replace('{reason}', await this.getTranslation(`reason-${nonFee}`)))
892
+
893
+ feeIsMissingFromTotal = !await this.UI.getBoolean(
894
+ config,
895
+ variable,
896
+ 'Is transaction fee of type {type} already included in the {reason} total?'.replace('{type}',
897
+ `${feeType}`).replace('{reason}',
898
+ await this.getTranslation(`reason-${nonFee}`))
899
+ )
889
900
 
890
901
  // Adjust asset transfers by the fee paid as asset itself, when they are missing from transfer total.
891
902
  if (feeIsMissingFromTotal) {
@@ -976,6 +987,7 @@ export class TransferAnalyzer {
976
987
  delete feesToDeduct[transfer.asset]
977
988
  }
978
989
  this.setData(transfer, data)
990
+ // TODO: Might actually need short-stock and short-crypto separately.
979
991
  const type = transfer.type === 'short' ? 'stock' : transfer.type
980
992
  await this.changeStock(segment.time, type, transfer.asset, transfer.amount, transfer.value)
981
993
  }
@@ -1002,9 +1014,11 @@ export class TransferAnalyzer {
1002
1014
  const soldAsset: AssetTransfer[] = (kind === 'short-buy'
1003
1015
  ? transfers.transfers.filter(t => t.reason === 'trade' && t.value && t.value > 0)
1004
1016
  : transfers.transfers.filter(t => t.reason === 'trade' && t.value && t.value < 0))
1017
+
1005
1018
  if (soldAsset.length !== 1) {
1006
- throw new BadState(`Did not found unique asset that was traded from ${JSON.stringify(transfers.transfers)}`)
1019
+ throw new BadState(`Did not found unique asset that was given out from ${JSON.stringify(transfers.transfers)}`)
1007
1020
  }
1021
+
1008
1022
  let reason: AssetTransferReason
1009
1023
  let asset: VATTarget
1010
1024
  if (total > 0) {
@@ -1127,6 +1141,16 @@ export class TransferAnalyzer {
1127
1141
  amount: transfer.value === undefined ? 0 : transfer.value,
1128
1142
  description
1129
1143
  }
1144
+ // Some accounts are not detectable in the beginning. Try once more.
1145
+ if (!txEntry.account) {
1146
+ const acc = await this.getAccount(transfer.reason, transfer.type, transfer.asset, segment.id)
1147
+ if (acc) {
1148
+ txEntry.account = acc
1149
+ }
1150
+ }
1151
+ if (!txEntry.account) {
1152
+ txEntry.account = await this.UI.throwGetAccount(this.config, `${transfer.reason}.${transfer.type}.${transfer.asset}` as AccountAddress)
1153
+ }
1130
1154
  if (!txEntry.account) {
1131
1155
  throw new SystemError(`Cannot find account ${transfer.reason}.${transfer.type}.${transfer.asset} for entry ${JSON.stringify(txEntry)}`)
1132
1156
  }
@@ -1339,10 +1363,14 @@ export class TransferAnalyzer {
1339
1363
  if (!isTransactionImportConnector(this.handler.system.connector)) {
1340
1364
  throw new SystemError('Connector used is not a transaction import connector.')
1341
1365
  }
1342
- const account: AccountNumber = await this.getAccount('trade', type, asset) as AccountNumber
1366
+ let account: AccountNumber = await this.getAccount('trade', type, asset) as AccountNumber
1367
+ if (!account) {
1368
+ account = await this.UI.throwGetAccount(this.config, `trade.${type}.${asset}` as AccountAddress)
1369
+ }
1343
1370
  if (!account) {
1344
- throw new Error(`Unable to find account for ${type} ${asset}.`)
1371
+ throw new Error(`Unable to find account for 'trade.${type}.${asset}'.`)
1345
1372
  }
1373
+
1346
1374
  // If no records yet, fetch it using the connector.
1347
1375
  if (!this.stocks[account]) {
1348
1376
  this.stocks[account] = new StockBookkeeping(`Account ${account}`)
@@ -1368,9 +1396,12 @@ export class TransferAnalyzer {
1368
1396
  // Force reading the stock initial status.
1369
1397
  await this.getStock(time, type, asset)
1370
1398
 
1371
- const account = await this.getAccount('trade', type, asset)
1399
+ let account = await this.getAccount('trade', type, asset)
1400
+ if (!account) {
1401
+ account = await this.UI.throwGetAccount(this.config, `trade.${type}.${asset}` as AccountAddress)
1402
+ }
1372
1403
  if (!account) {
1373
- throw new Error(`Unable to find account for ${type} ${asset}.`)
1404
+ throw new Error(`Unable to find account for 'trade.${type}.${asset}'.`)
1374
1405
  }
1375
1406
  if (!this.stocks[account]) {
1376
1407
  this.stocks[account] = new StockBookkeeping(`Account ${account}`)
@@ -20,7 +20,7 @@ export class BackendPlugin {
20
20
  public description: string
21
21
  public path: string
22
22
  public languages: Record<string, Record<string, string>>
23
- private catalog?: BackendCatalog
23
+ protected catalog?: BackendCatalog
24
24
 
25
25
  constructor() {
26
26
  this.id = null
@@ -1,4 +1,4 @@
1
- import { log } from '@tasenor/common'
1
+ import { ImportRule, log } from '@tasenor/common'
2
2
  import fs from 'fs'
3
3
  import { TransactionImportHandler } from '../import/TransactionImportHandler'
4
4
  import { TransactionUI } from '../import/TransactionUI'
@@ -205,7 +205,7 @@ export class ImportPlugin extends BackendPlugin {
205
205
  * Load and return default rules from the JSON-rules file.
206
206
  * @returns
207
207
  */
208
- getRules(): JSON {
208
+ getRules(): ImportRule[] {
209
209
  const path = this.filePath('rules.json')
210
210
  log(`Reading rules ${path}.`)
211
211
  return JSON.parse(fs.readFileSync(path).toString('utf-8')).rules
@@ -0,0 +1,22 @@
1
+ import { BackendCatalog, Language, LanguageHandler } from '@tasenor/common'
2
+ import { BackendPlugin } from '.'
3
+
4
+ /**
5
+ * A plugin providing translations for a language in backend.
6
+ */
7
+ export class LanguageBackendPlugin extends BackendPlugin {
8
+ handler: LanguageHandler
9
+ declare protected catalog?: BackendCatalog
10
+
11
+ constructor(handler: LanguageHandler) {
12
+ super()
13
+ this.handler = handler
14
+ this.languages = handler.languages
15
+ }
16
+
17
+ getLanguages(): Set<Language> { return this.handler.getLanguages() }
18
+ flag(language: Language) { return this.handler.flag(language) }
19
+ date2str(date): string { return this.handler.date2str(date) }
20
+ time2str(date): string { return this.handler.time2str(date) }
21
+ str2date(date, sample: Date | undefined): string | undefined { return this.handler.str2date(date, sample) }
22
+ }
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable @typescript-eslint/no-unused-vars */
2
2
  import fs from 'fs'
3
- import { KnexDatabase, data2csv } from '..'
3
+ import { KnexDatabase, LanguageBackendPlugin, data2csv } from '..'
4
4
  import dayjs from 'dayjs'
5
5
  import quarterOfYear from 'dayjs/plugin/quarterOfYear'
6
6
  import { ReportOptions, ReportID, ReportFlagName, ReportItem, ReportQueryParams, ReportLine, AccountNumber, ReportColumnDefinition, PeriodModel, ReportFormat, Language, PK, ReportMeta, ReportData, ReportTotals, Report } from '@tasenor/common'
@@ -16,10 +16,13 @@ export class ReportPlugin extends BackendPlugin {
16
16
  private formats: ReportID[]
17
17
  // Is set, allow this report only on DBs having those accounting schemes.
18
18
  protected schemes: Set<string> | undefined
19
+ // Store translation plugin references.
20
+ protected languagePlugins: Partial<Record<Language, LanguageBackendPlugin>>
19
21
 
20
22
  constructor(...formats: ReportID[]) {
21
23
  super()
22
24
  this.formats = formats
25
+ this.languagePlugins = {}
23
26
  this.schemes = undefined
24
27
  }
25
28
 
@@ -234,6 +237,29 @@ export class ReportPlugin extends BackendPlugin {
234
237
  return sqlQuery
235
238
  }
236
239
 
240
+ /**
241
+ * Scan string for `{...}` sub-strings and translate parts.
242
+ */
243
+ translate(text: string, lang: Language): string {
244
+ let match
245
+ do {
246
+ match = /(\{(\d\d\d\d-\d\d-\d\d)\})/.exec(text)
247
+ if (match) {
248
+ if (!this.languagePlugins[lang]) {
249
+ this.languagePlugins[lang] = this.catalog?.getLanguagePlugin(lang) as LanguageBackendPlugin | undefined
250
+ }
251
+ text = text.replace(match[1], this.languagePlugins[lang]?.date2str(match[2]) || match[2])
252
+ } else {
253
+ match = /(\{(.*?)\})/.exec(text)
254
+ if (match) {
255
+ text = text.replace(match[1], this.t(match[2], lang))
256
+ }
257
+ }
258
+ } while (match)
259
+
260
+ return text
261
+ }
262
+
237
263
  /**
238
264
  * Construct a report data for the report.
239
265
  * @param db
@@ -252,7 +278,7 @@ export class ReportPlugin extends BackendPlugin {
252
278
  * * `useRemainingColumns` if set, extend this column index to use all the rest columns in the row.
253
279
  * * `accountDetails` if true, after this are summarized accounts under this entry.
254
280
  * * `isAccount` if true, this is an account entry.
255
- * * `needLocalization` if set, value should be localized, i.e. translated via Localization component in ui.
281
+ * * `needLocalization` if set, value should be localized, i.e. translated according to the language selected.
256
282
  * * `name` Title of the entry.
257
283
  * * `number` Account number if the entry is an account.
258
284
  * * `values` An object with entry for each column mapping name of the columnt to the value to display.
@@ -289,8 +315,13 @@ export class ReportPlugin extends BackendPlugin {
289
315
  // Apply query filtering.
290
316
  entries = this.doFiltering(id, entries, options, settings)
291
317
 
292
- // We have now relevant entries collected. Use plugin features next.
318
+ // Construct columns.
293
319
  const columns: ReportColumnDefinition[] = await this.getColumns(id, entries, options as ReportOptions, settings)
320
+ columns.forEach(column => {
321
+ column.title = this.translate(column.title, options.lang || 'en')
322
+ })
323
+
324
+ // We have now relevant entries collected. Use plugin features next.
294
325
  let data = await this.preProcess(id, entries, options, settings, columns) as ReportLine[]
295
326
  data = await this.postProcess(id, data, options, settings, columns)
296
327
  const report = {
@@ -344,7 +375,7 @@ export class ReportPlugin extends BackendPlugin {
344
375
  }
345
376
 
346
377
  /**
347
- * Do post processing for report data before sending it.
378
+ * Do post processing for report data before sending it. By default, do translations.
348
379
  * @param id Report type.
349
380
  * @param data Calculated report data
350
381
  * @param options Report options.
@@ -353,6 +384,11 @@ export class ReportPlugin extends BackendPlugin {
353
384
  * @returns
354
385
  */
355
386
  async postProcess(id: ReportID, data: ReportLine[], options: ReportQueryParams, settings: ReportMeta, columns: ReportColumnDefinition[]): Promise<ReportLine[]> {
387
+ data.forEach(item => {
388
+ if ('needLocalization' in item && item.needLocalization && item.name) {
389
+ item.name = this.translate(item.name, options.lang || 'en')
390
+ }
391
+ })
356
392
  return data
357
393
  }
358
394
 
@@ -4,6 +4,7 @@
4
4
  export * from './BackendPlugin'
5
5
  export * from './DataPlugin'
6
6
  export * from './ImportPlugin'
7
+ export * from './LanguageBackendPlugin'
7
8
  export * from './ReportPlugin'
8
9
  export * from './SchemePlugin'
9
10
  export * from './ServicePlugin'
@@ -109,7 +109,7 @@ export class Process {
109
109
  */
110
110
  async proceedToState(action: ImportAction, state: ImportState): Promise<void> {
111
111
  const current = await this.getCurrentStep()
112
- const handler = this.system.getHandler(current.handler)
112
+ const handler = this.system.handler
113
113
  current.action = action
114
114
  current.finished = new Date()
115
115
  current.save()
@@ -220,7 +220,7 @@ export class Process {
220
220
  await this.updateStatus()
221
221
  break
222
222
  }
223
- const handler = this.system.getHandler(step.handler)
223
+ const handler = this.system.handler
224
224
  const state = clone(step.state)
225
225
  const action = clone(step.directions.action)
226
226
  try {
@@ -327,7 +327,7 @@ export class Process {
327
327
  async input(action: ImportAction): Promise<void> {
328
328
  this.system.logger.info(`Handling input ${JSON.stringify(action)} on process ${this}.`)
329
329
  const step = await this.getCurrentStep()
330
- const handler = this.system.getHandler(step.handler)
330
+ const handler = this.system.handler
331
331
  let nextState
332
332
  try {
333
333
  nextState = await handler.action(this, action, clone(step.state), this.files)
@@ -349,7 +349,7 @@ export class Process {
349
349
  }
350
350
  const step = await this.getCurrentStep()
351
351
  this.system.logger.info(`Attempt of rolling back '${step}' from '${this}'.`)
352
- const handler = this.system.getHandler(step.handler)
352
+ const handler = this.system.handler
353
353
  await handler.rollback(this, this.state)
354
354
  const current = await this.getCurrentStep()
355
355
  current.action = { rollback: true }
@@ -95,10 +95,3 @@ export class ProcessHandler {
95
95
  throw new NotImplemented(`A handler '${this.name}' does not implement rollback()`)
96
96
  }
97
97
  }
98
-
99
- /**
100
- * A collection of process handlers.
101
- */
102
- export type ProcessHandlerMap = {
103
- [key: string]: ProcessHandler
104
- }