@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.
- package/dist/src/cli/cli.js.map +1 -1
- package/dist/src/cli/mini-util.d.ts +1 -1
- package/dist/src/cli/mini-util.js.map +1 -1
- package/dist/src/commands/account.js.map +1 -1
- package/dist/src/commands/balance.js.map +1 -1
- package/dist/src/commands/db.js.map +1 -1
- package/dist/src/commands/entry.js.map +1 -1
- package/dist/src/commands/import.js.map +1 -1
- package/dist/src/commands/importer.js +1 -1
- package/dist/src/commands/importer.js.map +1 -1
- package/dist/src/commands/index.js.map +1 -1
- package/dist/src/commands/period.js.map +1 -1
- package/dist/src/commands/plugin.js.map +1 -1
- package/dist/src/commands/report.js.map +1 -1
- package/dist/src/commands/settings.js.map +1 -1
- package/dist/src/commands/stock.js.map +1 -1
- package/dist/src/commands/tag.js.map +1 -1
- package/dist/src/commands/tx.js.map +1 -1
- package/dist/src/commands/user.js.map +1 -1
- package/dist/src/data/csv.d.ts +5 -0
- package/dist/src/data/csv.js +28 -0
- package/dist/src/data/csv.js.map +1 -0
- package/dist/src/data/index.d.ts +1 -0
- package/dist/src/data/index.js +1 -0
- package/dist/src/data/index.js.map +1 -1
- package/dist/src/data/utils.js.map +1 -1
- package/dist/src/database/BookkeeperImporter.d.ts +1 -0
- package/dist/src/database/BookkeeperImporter.js +13 -12
- package/dist/src/database/BookkeeperImporter.js.map +1 -1
- package/dist/src/database/DB.js +2 -1
- package/dist/src/database/DB.js.map +1 -1
- package/dist/src/export/Exporter.d.ts +2 -2
- package/dist/src/export/Exporter.js +0 -2
- package/dist/src/export/Exporter.js.map +1 -1
- package/dist/src/export/TasenorExporter.js.map +1 -1
- package/dist/src/export/TilitinExporter.js.map +1 -1
- package/dist/src/import/TextFileProcessHandler.js +2 -2
- package/dist/src/import/TextFileProcessHandler.js.map +1 -1
- package/dist/src/import/TransactionImportConnector.js.map +1 -1
- package/dist/src/import/TransactionImportHandler.d.ts +4 -26
- package/dist/src/import/TransactionImportHandler.js +8 -27
- package/dist/src/import/TransactionImportHandler.js.map +1 -1
- package/dist/src/import/TransactionRules.d.ts +1 -172
- package/dist/src/import/TransactionRules.js +8 -172
- package/dist/src/import/TransactionRules.js.map +1 -1
- package/dist/src/import/TransactionUI.js +16 -2
- package/dist/src/import/TransactionUI.js.map +1 -1
- package/dist/src/import/TransferAnalyzer.js +27 -6
- package/dist/src/import/TransferAnalyzer.js.map +1 -1
- package/dist/src/net/git.d.ts +10 -1
- package/dist/src/net/git.js +45 -13
- package/dist/src/net/git.js.map +1 -1
- package/dist/src/net/middleware.js.map +1 -1
- package/dist/src/net/tokens.js.map +1 -1
- package/dist/src/net/vault.js.map +1 -1
- package/dist/src/plugins/BackendPlugin.d.ts +1 -1
- package/dist/src/plugins/BackendPlugin.js.map +1 -1
- package/dist/src/plugins/DataPlugin.js.map +1 -1
- package/dist/src/plugins/ImportPlugin.d.ts +4 -1
- package/dist/src/plugins/ImportPlugin.js +2 -0
- package/dist/src/plugins/ImportPlugin.js.map +1 -1
- package/dist/src/plugins/LanguageBackendPlugin.d.ts +15 -0
- package/dist/src/plugins/LanguageBackendPlugin.js +18 -0
- package/dist/src/plugins/LanguageBackendPlugin.js.map +1 -0
- package/dist/src/plugins/ReportPlugin.d.ts +8 -3
- package/dist/src/plugins/ReportPlugin.js +38 -4
- package/dist/src/plugins/ReportPlugin.js.map +1 -1
- package/dist/src/plugins/ServicePlugin.js.map +1 -1
- package/dist/src/plugins/index.d.ts +1 -0
- package/dist/src/plugins/index.js +1 -0
- package/dist/src/plugins/index.js.map +1 -1
- package/dist/src/plugins/plugins.d.ts +2 -2
- package/dist/src/plugins/plugins.js +27 -5
- package/dist/src/plugins/plugins.js.map +1 -1
- package/dist/src/process/Process.js +4 -4
- package/dist/src/process/Process.js.map +1 -1
- package/dist/src/process/ProcessFile.js.map +1 -1
- package/dist/src/process/ProcessHandler.d.ts +0 -6
- package/dist/src/process/ProcessStep.js.map +1 -1
- package/dist/src/process/ProcessingSystem.d.ts +2 -8
- package/dist/src/process/ProcessingSystem.js +17 -31
- package/dist/src/process/ProcessingSystem.js.map +1 -1
- package/dist/src/reports/conversions.js +0 -3
- package/dist/src/reports/conversions.js.map +1 -1
- package/dist/src/server/ISPDemoServer.js.map +1 -1
- package/dist/src/server/api.js.map +1 -1
- package/dist/src/server/index.d.ts +1 -0
- package/dist/src/server/index.js +1 -0
- package/dist/src/server/index.js.map +1 -1
- package/dist/src/server/router.js.map +1 -1
- package/dist/src/server/server.d.ts +11 -0
- package/dist/src/server/server.js +39 -0
- package/dist/src/server/server.js.map +1 -0
- package/dist/src/system.d.ts +1 -1
- package/dist/src/system.js +8 -3
- package/dist/src/system.js.map +1 -1
- package/package.json +26 -27
- package/src/cli/mini-util.ts +6 -6
- package/src/commands/importer.ts +1 -1
- package/src/commands/index.ts +1 -1
- package/src/database/DB.ts +3 -1
- package/src/export/Exporter.ts +5 -7
- package/src/import/TransactionImportHandler.ts +9 -27
- package/src/import/TransactionRules.ts +8 -172
- package/src/import/TransactionUI.ts +16 -2
- package/src/import/TransferAnalyzer.ts +38 -7
- package/src/plugins/BackendPlugin.ts +1 -1
- package/src/plugins/ImportPlugin.ts +2 -2
- package/src/plugins/LanguageBackendPlugin.ts +22 -0
- package/src/plugins/ReportPlugin.ts +40 -4
- package/src/plugins/index.ts +1 -0
- package/src/process/Process.ts +4 -4
- package/src/process/ProcessHandler.ts +0 -7
- package/src/process/ProcessingSystem.ts +18 -31
- 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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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}`)
|
|
@@ -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():
|
|
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
|
|
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
|
-
//
|
|
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
|
|
package/src/plugins/index.ts
CHANGED
package/src/process/Process.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
}
|