@tasenor/common-node 1.9.116 → 1.9.118
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/commands/report.js +12 -8
- package/dist/src/commands/report.js.map +1 -1
- package/dist/src/database/DB.js +1 -4
- package/dist/src/database/DB.js.map +1 -1
- package/dist/src/import/TransactionImportHandler.js +16 -0
- package/dist/src/import/TransactionImportHandler.js.map +1 -1
- package/dist/src/import/TransactionRules.d.ts +1 -0
- package/dist/src/import/TransactionRules.js +3 -1
- package/dist/src/import/TransactionRules.js.map +1 -1
- package/dist/src/import/TransferAnalyzer.js +2 -2
- package/dist/src/import/TransferAnalyzer.js.map +1 -1
- package/dist/src/plugins/ReportPlugin.d.ts +19 -17
- package/dist/src/plugins/ReportPlugin.js +38 -25
- package/dist/src/plugins/ReportPlugin.js.map +1 -1
- package/dist/src/process/ProcessFile.d.ts +4 -0
- package/dist/src/process/ProcessFile.js +8 -0
- package/dist/src/process/ProcessFile.js.map +1 -1
- package/dist/src/process/ProcessHandler.d.ts +7 -2
- package/dist/src/process/ProcessHandler.js +10 -1
- package/dist/src/process/ProcessHandler.js.map +1 -1
- package/dist/src/process/ProcessingSystem.js +9 -2
- package/dist/src/process/ProcessingSystem.js.map +1 -1
- package/dist/src/reports/conversions.js +16 -6
- package/dist/src/reports/conversions.js.map +1 -1
- package/package.json +4 -4
- package/src/commands/report.ts +13 -9
- package/src/database/DB.ts +1 -5
- package/src/import/TransactionImportHandler.ts +17 -0
- package/src/import/TransactionRules.ts +3 -1
- package/src/import/TransferAnalyzer.ts +2 -2
- package/src/plugins/ReportPlugin.ts +58 -42
- package/src/process/ProcessFile.ts +9 -0
- package/src/process/ProcessHandler.ts +12 -2
- package/src/process/ProcessingSystem.ts +8 -2
- package/src/reports/conversions.ts +16 -7
package/src/commands/report.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { sprintf } from 'sprintf-js'
|
|
3
3
|
import { Command } from '.'
|
|
4
4
|
import { ArgumentParser } from 'argparse'
|
|
5
|
-
import { Report, ReportColumnDefinition } from '@tasenor/common'
|
|
5
|
+
import { Report, ReportColumnDefinition, isReportItem } from '@tasenor/common'
|
|
6
6
|
|
|
7
7
|
class ReportCommand extends Command {
|
|
8
8
|
|
|
@@ -53,7 +53,7 @@ class ReportCommand extends Command {
|
|
|
53
53
|
|
|
54
54
|
// Meta data.
|
|
55
55
|
if ('meta' in data) {
|
|
56
|
-
Object.keys(report.meta
|
|
56
|
+
Object.keys(report.meta).forEach((meta) => console.log(`${meta}: ${(report.meta)[meta]}`))
|
|
57
57
|
console.log()
|
|
58
58
|
}
|
|
59
59
|
|
|
@@ -71,13 +71,16 @@ class ReportCommand extends Command {
|
|
|
71
71
|
// Render each report line.
|
|
72
72
|
for (const item of report.data) {
|
|
73
73
|
line = []
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
74
|
+
if (isReportItem(item)) {
|
|
75
|
+
for (const column of columns) {
|
|
76
|
+
const text = {
|
|
77
|
+
id: () => item.id,
|
|
78
|
+
name: () => item.name,
|
|
79
|
+
currency: () => item.values && item.values[column.name] !== undefined && sprintf('%.2f', (item.values[column.name] as number || 0) / 100),
|
|
80
|
+
numeric: () => item.values && item.values[column.name] !== undefined && sprintf('%f', (item.values[column.name] || 0))
|
|
81
|
+
}[column.type]()
|
|
82
|
+
line.push(text || '')
|
|
83
|
+
}
|
|
81
84
|
}
|
|
82
85
|
lines.push(line)
|
|
83
86
|
}
|
|
@@ -102,6 +105,7 @@ class ReportCommand extends Command {
|
|
|
102
105
|
}
|
|
103
106
|
return
|
|
104
107
|
}
|
|
108
|
+
|
|
105
109
|
throw new Error('Default output not implented.')
|
|
106
110
|
}
|
|
107
111
|
|
package/src/database/DB.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Crypto, DatabaseName, error, Hostname, ID, isDatabaseName, log, Secret, Url } from '@tasenor/common'
|
|
1
|
+
import { Crypto, DatabaseName, delay, error, Hostname, ID, isDatabaseName, log, Secret, Url } from '@tasenor/common'
|
|
2
2
|
import { randomString, vault } from '..'
|
|
3
3
|
import knex, { Knex } from 'knex'
|
|
4
4
|
import { types } from 'pg'
|
|
@@ -245,10 +245,6 @@ const migrate = async (masterDb: KnexDatabase, name: DatabaseName, migrations: s
|
|
|
245
245
|
* Apply migrations to the master database.
|
|
246
246
|
*/
|
|
247
247
|
const migrateMaster = async (migrations: string): Promise<void> => {
|
|
248
|
-
function delay(time) {
|
|
249
|
-
return new Promise(resolve => setTimeout(resolve, time))
|
|
250
|
-
}
|
|
251
|
-
|
|
252
248
|
log('Migrating master database.')
|
|
253
249
|
|
|
254
250
|
const conf = await getMasterConfig()
|
|
@@ -188,6 +188,22 @@ export class TransactionImportHandler extends TextFileProcessHandler {
|
|
|
188
188
|
async segmentationPostProcess(state: ImportStateText<'segmented'>): Promise<ImportStateText<'segmented'>> {
|
|
189
189
|
const shared: Record<SegmentId, Record<string, string>> = {}
|
|
190
190
|
|
|
191
|
+
// Remap fields, if defined.
|
|
192
|
+
if (this.importOptions.fieldRemapping) {
|
|
193
|
+
Object.keys(this.importOptions.fieldRemapping).forEach(old => {
|
|
194
|
+
const replace = (this.importOptions.fieldRemapping && this.importOptions.fieldRemapping[old]) || ''
|
|
195
|
+
for (const fileName of Object.keys(state.files)) {
|
|
196
|
+
for (let n = 0; n < state.files[fileName].lines.length; n++) {
|
|
197
|
+
const { columns } = state.files[fileName].lines[n]
|
|
198
|
+
if (columns[old] !== undefined) {
|
|
199
|
+
columns[replace] = columns[old]
|
|
200
|
+
delete columns[old]
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
})
|
|
205
|
+
}
|
|
206
|
+
|
|
191
207
|
for (const fileName of Object.keys(state.files)) {
|
|
192
208
|
// Build standard fields.
|
|
193
209
|
const { textField, totalAmountField } = this.importOptions
|
|
@@ -765,6 +781,7 @@ export class TransactionImportHandler extends TextFileProcessHandler {
|
|
|
765
781
|
debug('EXECUTION', res.transactions)
|
|
766
782
|
const hasOld = await this.system.connector.resultExists(process.id, res)
|
|
767
783
|
if (hasOld) {
|
|
784
|
+
log(`Duplicate transaction found ${JSON.stringify(res.transactions)}.`)
|
|
768
785
|
const allow = await this.UI.getBoolean(process.config, 'allowIdenticalTx', 'Allow creation of identical transactions that has been already created.')
|
|
769
786
|
if (!allow) {
|
|
770
787
|
for (const tx of res.transactions || []) {
|
|
@@ -272,6 +272,7 @@ export class TransactionRules {
|
|
|
272
272
|
* * `options` - the options of the current rule we are evaluating
|
|
273
273
|
* * `text` - original text of the corresponding line
|
|
274
274
|
* * `lineNumber` - original line number of the corresponding line
|
|
275
|
+
* * `version` - version number of the source format found
|
|
275
276
|
* If the filter match is found, then questions are provided to UI unless already
|
|
276
277
|
* answered. The reponses to the questions are passed to the any further evaluations.
|
|
277
278
|
*/
|
|
@@ -326,7 +327,8 @@ export class TransactionRules {
|
|
|
326
327
|
rule,
|
|
327
328
|
options: rule.options || {},
|
|
328
329
|
text: line.text,
|
|
329
|
-
lineNumber: line.line
|
|
330
|
+
lineNumber: line.line,
|
|
331
|
+
version: this.handler.version
|
|
330
332
|
}
|
|
331
333
|
|
|
332
334
|
const singleMatch = rule.options && rule.options.singleMatch
|
|
@@ -379,7 +379,7 @@ export class TransferAnalyzer {
|
|
|
379
379
|
values.giveAsset = myEntry[0].amount < 0 ? myEntry[0].asset : otherEntry[0].asset
|
|
380
380
|
} else if (weHave(['dividend', 'income'], ['currency', 'statement']) || weHave(['tax', 'dividend', 'income'], ['currency', 'statement'])) {
|
|
381
381
|
kind = 'dividend'
|
|
382
|
-
} else if (weHave(['income'], ['currency', 'statement']) || weHave(['income', 'tax'], ['currency', 'statement'])) {
|
|
382
|
+
} else if (weHave(['income'], ['currency', 'statement']) || weHave(['income'], ['crypto', 'statement']) || weHave(['income', 'tax'], ['currency', 'statement'])) {
|
|
383
383
|
kind = 'income'
|
|
384
384
|
const statementEntry = shouldHaveOne('income', 'statement')
|
|
385
385
|
values.name = await this.getTranslation(`income-${statementEntry.asset}`)
|
|
@@ -1003,7 +1003,7 @@ export class TransferAnalyzer {
|
|
|
1003
1003
|
? transfers.transfers.filter(t => t.reason === 'trade' && t.value && t.value > 0)
|
|
1004
1004
|
: transfers.transfers.filter(t => t.reason === 'trade' && t.value && t.value < 0))
|
|
1005
1005
|
if (soldAsset.length !== 1) {
|
|
1006
|
-
throw new BadState(`Did not found unique asset that was
|
|
1006
|
+
throw new BadState(`Did not found unique asset that was traded from ${JSON.stringify(transfers.transfers)}`)
|
|
1007
1007
|
}
|
|
1008
1008
|
let reason: AssetTransferReason
|
|
1009
1009
|
let asset: VATTarget
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
2
2
|
import fs from 'fs'
|
|
3
|
-
import { data2csv } from '..'
|
|
3
|
+
import { KnexDatabase, data2csv } from '..'
|
|
4
4
|
import dayjs from 'dayjs'
|
|
5
5
|
import quarterOfYear from 'dayjs/plugin/quarterOfYear'
|
|
6
|
-
import { ReportOptions, ReportID, ReportFlagName, ReportItem, ReportQueryParams, ReportLine, AccountNumber, ReportColumnDefinition, PeriodModel, ReportFormat, Language } from '@tasenor/common'
|
|
6
|
+
import { ReportOptions, ReportID, ReportFlagName, ReportItem, ReportQueryParams, ReportLine, AccountNumber, ReportColumnDefinition, PeriodModel, ReportFormat, Language, PK, ReportMeta, ReportData, ReportTotals, Report } from '@tasenor/common'
|
|
7
7
|
import { BackendPlugin } from './BackendPlugin'
|
|
8
8
|
|
|
9
9
|
dayjs.extend(quarterOfYear)
|
|
@@ -24,7 +24,7 @@ export class ReportPlugin extends BackendPlugin {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
* Read in report
|
|
27
|
+
* Read in report structure file.
|
|
28
28
|
*/
|
|
29
29
|
getReportStructure(id: ReportID, lang: Language) : ReportFormat | undefined {
|
|
30
30
|
const path = this.filePath(`${id}-${lang}.tsv`)
|
|
@@ -44,7 +44,7 @@ export class ReportPlugin extends BackendPlugin {
|
|
|
44
44
|
* Check if the given report is provided by this plugin.
|
|
45
45
|
* @param id
|
|
46
46
|
*/
|
|
47
|
-
hasReport(id) {
|
|
47
|
+
hasReport(id: ReportID) {
|
|
48
48
|
return this.formats.includes(id)
|
|
49
49
|
}
|
|
50
50
|
|
|
@@ -62,7 +62,7 @@ export class ReportPlugin extends BackendPlugin {
|
|
|
62
62
|
* Return UI option definitions for the given report.
|
|
63
63
|
* @param id
|
|
64
64
|
*/
|
|
65
|
-
getReportOptions(id): ReportOptions {
|
|
65
|
+
getReportOptions(id: ReportID): ReportOptions {
|
|
66
66
|
return {}
|
|
67
67
|
}
|
|
68
68
|
|
|
@@ -85,7 +85,7 @@ export class ReportPlugin extends BackendPlugin {
|
|
|
85
85
|
* @param flags
|
|
86
86
|
* @returns
|
|
87
87
|
*/
|
|
88
|
-
flags2item(flags: ReportFlagName[]) {
|
|
88
|
+
flags2item(flags: ReportFlagName[]): ReportItem {
|
|
89
89
|
const item: ReportItem = {}
|
|
90
90
|
flags.forEach(flag => {
|
|
91
91
|
if (flag) {
|
|
@@ -124,13 +124,13 @@ export class ReportPlugin extends BackendPlugin {
|
|
|
124
124
|
* @param entries
|
|
125
125
|
* @param options
|
|
126
126
|
*/
|
|
127
|
-
async getColumns(id, entries, options: ReportOptions, settings): Promise<ReportColumnDefinition[]> {
|
|
127
|
+
async getColumns(id: ReportID, entries: ReportData[], options: ReportOptions, settings: ReportMeta): Promise<ReportColumnDefinition[]> {
|
|
128
128
|
if (!options.periods) {
|
|
129
|
-
throw new Error(`Need periods to define columns ${JSON.stringify(options)}`)
|
|
129
|
+
throw new Error(`Need option 'periods' to define columns in ${JSON.stringify(options)}`)
|
|
130
130
|
}
|
|
131
131
|
const columns: ReportColumnDefinition[] = options.periods.map((period) => {
|
|
132
132
|
return {
|
|
133
|
-
type: '
|
|
133
|
+
type: 'currency',
|
|
134
134
|
name: 'period' + period.id,
|
|
135
135
|
title: this.columnTitle(id, period, options)
|
|
136
136
|
}
|
|
@@ -157,20 +157,27 @@ export class ReportPlugin extends BackendPlugin {
|
|
|
157
157
|
* Force some options, if needed.
|
|
158
158
|
* @returns
|
|
159
159
|
*/
|
|
160
|
-
forceOptions(options) {
|
|
160
|
+
forceOptions(options: ReportQueryParams): ReportQueryParams {
|
|
161
161
|
return {
|
|
162
162
|
negateAssetAndProfit: false, // A flag to multiply by -1 entries from asset and profit types of accounts.
|
|
163
163
|
addPreviousPeriod: false // A flag to define if the previous period should be displayed for comparison.
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Optional extra SQL limitation for SQL used to gather raw data. See constructSqlQuery().
|
|
169
|
+
*/
|
|
170
|
+
async extraSQLCondition(): Promise<string | null> {
|
|
171
|
+
return null
|
|
172
|
+
}
|
|
173
|
+
|
|
167
174
|
/**
|
|
168
175
|
* Construct a SQL for the report query.
|
|
169
176
|
* @param db
|
|
170
177
|
* @param options
|
|
171
178
|
* @returns A knex query prepared.
|
|
172
179
|
*/
|
|
173
|
-
async constructSqlQuery(db, options, settings) {
|
|
180
|
+
async constructSqlQuery(db: KnexDatabase, options: ReportQueryParams, settings: ReportMeta) {
|
|
174
181
|
// Construct value negator.
|
|
175
182
|
let negateSql = '(CASE debit WHEN true THEN 1 ELSE -1 END)'
|
|
176
183
|
if (options.negateAssetAndProfit) {
|
|
@@ -180,7 +187,7 @@ export class ReportPlugin extends BackendPlugin {
|
|
|
180
187
|
// Find periods.
|
|
181
188
|
const periodIds = [options.periodId]
|
|
182
189
|
if (options.addPreviousPeriod) {
|
|
183
|
-
const recentPeriods = await db.select('*').from('period').where('id', '<=', options.periodId).orderBy('end_date', 'desc').limit(2)
|
|
190
|
+
const recentPeriods = await db.select('*').from('period').where('id', '<=', options.periodId + '').orderBy('end_date', 'desc').limit(2)
|
|
184
191
|
if (recentPeriods.length > 1) {
|
|
185
192
|
periodIds.push(recentPeriods[1].id)
|
|
186
193
|
}
|
|
@@ -196,24 +203,32 @@ export class ReportPlugin extends BackendPlugin {
|
|
|
196
203
|
'account.type',
|
|
197
204
|
'account.number',
|
|
198
205
|
db.raw(`CAST(ROUND(${negateSql} * entry.amount * 100) AS BIGINT) AS amount`),
|
|
199
|
-
'entry.description'
|
|
206
|
+
'entry.description',
|
|
207
|
+
'entry.data'
|
|
200
208
|
)
|
|
201
209
|
.from('entry')
|
|
202
210
|
.leftJoin('account', 'account.id', 'entry.account_id')
|
|
203
211
|
.leftJoin('document', 'document.id', 'entry.document_id')
|
|
204
|
-
.whereIn('document.period_id', periodIds)
|
|
212
|
+
.whereIn('document.period_id', periodIds as PK[])
|
|
205
213
|
|
|
206
214
|
// Limit by account, if given.
|
|
207
215
|
if (options.accountId) {
|
|
208
216
|
sqlQuery = sqlQuery.andWhere('account.id', '=', options.accountId)
|
|
209
217
|
}
|
|
210
218
|
|
|
219
|
+
// Add extras, if any.
|
|
220
|
+
const extras = await this.extraSQLCondition()
|
|
221
|
+
if (extras) {
|
|
222
|
+
sqlQuery = sqlQuery.andWhere(db.raw(extras))
|
|
223
|
+
}
|
|
224
|
+
|
|
211
225
|
// Tune ordering.
|
|
212
226
|
sqlQuery = (sqlQuery
|
|
213
227
|
.orderBy('document.date')
|
|
214
228
|
.orderBy('document.number')
|
|
215
229
|
.orderBy('document.id')
|
|
216
|
-
.orderBy('entry.row_number')
|
|
230
|
+
.orderBy('entry.row_number')
|
|
231
|
+
.orderBy('entry.id'))
|
|
217
232
|
|
|
218
233
|
return sqlQuery
|
|
219
234
|
}
|
|
@@ -239,9 +254,9 @@ export class ReportPlugin extends BackendPlugin {
|
|
|
239
254
|
* * `needLocalization` if set, value should be localized, i.e. translated via Localization component in ui.
|
|
240
255
|
* * `name` Title of the entry.
|
|
241
256
|
* * `number` Account number if the entry is an account.
|
|
242
|
-
* * `
|
|
257
|
+
* * `values` An object with entry for each column mapping name of the columnt to the value to display.
|
|
243
258
|
*/
|
|
244
|
-
async renderReport(db, id, options: ReportQueryParams = {}) {
|
|
259
|
+
async renderReport(db: KnexDatabase, id: ReportID, options: ReportQueryParams = {}): Promise<Report> {
|
|
245
260
|
|
|
246
261
|
// Add report forced options.
|
|
247
262
|
Object.assign(options, this.forceOptions(options))
|
|
@@ -262,12 +277,12 @@ export class ReportPlugin extends BackendPlugin {
|
|
|
262
277
|
options.format = this.getReportStructure(id, options.lang || 'en')
|
|
263
278
|
|
|
264
279
|
// Prepare query.
|
|
265
|
-
const q =
|
|
266
|
-
let entries = await q
|
|
280
|
+
const q = this.constructSqlQuery(db, options, settings)
|
|
281
|
+
let entries = await q as ReportData[]
|
|
267
282
|
|
|
268
283
|
// Process big ints.
|
|
269
284
|
for (const entry of entries) {
|
|
270
|
-
entry.amount = parseInt(entry.amount)
|
|
285
|
+
entry.amount = parseInt(entry.amount + '')
|
|
271
286
|
}
|
|
272
287
|
|
|
273
288
|
// Apply query filtering.
|
|
@@ -275,7 +290,7 @@ export class ReportPlugin extends BackendPlugin {
|
|
|
275
290
|
|
|
276
291
|
// We have now relevant entries collected. Use plugin features next.
|
|
277
292
|
const columns: ReportColumnDefinition[] = await this.getColumns(id, entries, options as ReportOptions, settings)
|
|
278
|
-
let data = this.preProcess(id, entries, options, settings, columns)
|
|
293
|
+
let data = this.preProcess(id, entries, options, settings, columns) as ReportLine[]
|
|
279
294
|
data = this.postProcess(id, data, options, settings, columns)
|
|
280
295
|
const report = {
|
|
281
296
|
format: id,
|
|
@@ -302,7 +317,7 @@ export class ReportPlugin extends BackendPlugin {
|
|
|
302
317
|
* @param options
|
|
303
318
|
* @param settings
|
|
304
319
|
*/
|
|
305
|
-
doFiltering(id, entries, options, settings) {
|
|
320
|
+
doFiltering(id: ReportID, entries: ReportData[], options: ReportQueryParams, settings: ReportMeta) {
|
|
306
321
|
let filter = (entry) => true
|
|
307
322
|
|
|
308
323
|
if (options.quarter1) {
|
|
@@ -323,7 +338,7 @@ export class ReportPlugin extends BackendPlugin {
|
|
|
323
338
|
* @param options
|
|
324
339
|
* @param columns
|
|
325
340
|
*/
|
|
326
|
-
preProcess(id, entries, options, settings, columns) {
|
|
341
|
+
preProcess(id: ReportID, entries: ReportData[], options: ReportQueryParams, settings: ReportMeta, columns: ReportColumnDefinition[]): ReportLine[] {
|
|
327
342
|
throw new Error(`Report plugin ${this.constructor.name} does not implement preProcess().`)
|
|
328
343
|
}
|
|
329
344
|
|
|
@@ -336,12 +351,12 @@ export class ReportPlugin extends BackendPlugin {
|
|
|
336
351
|
* @param columns Column definitions.
|
|
337
352
|
* @returns
|
|
338
353
|
*/
|
|
339
|
-
postProcess(id, data, options, settings, columns) {
|
|
354
|
+
postProcess(id: ReportID, data: ReportLine[], options: ReportQueryParams, settings: ReportMeta, columns: ReportColumnDefinition[]): ReportLine[] {
|
|
340
355
|
return data
|
|
341
356
|
}
|
|
342
357
|
|
|
343
358
|
/**
|
|
344
|
-
* A helper to combine final report from pre-processed material for reports using text description.
|
|
359
|
+
* A helper to combine final report from pre-processed material for reports using TSV text description file.
|
|
345
360
|
* @param accountNumbers A set of all account numbers found.
|
|
346
361
|
* @param accountNames A mapping from account numbers to their names.
|
|
347
362
|
* @param columnNames A list of column names.
|
|
@@ -349,8 +364,8 @@ export class ReportPlugin extends BackendPlugin {
|
|
|
349
364
|
* @param totals A mapping from account numbers their total balance.
|
|
350
365
|
* @returns
|
|
351
366
|
*/
|
|
352
|
-
parseAndCombineReport(accountNumbers: AccountNumber[], accountNames, columns, format, totals) {
|
|
353
|
-
const columnNames = columns.filter((col) => col.type === '
|
|
367
|
+
parseAndCombineReport(accountNumbers: AccountNumber[], accountNames: Record<AccountNumber, string>, columns: ReportColumnDefinition[], format: ReportFormat, totals: ReportTotals): ReportLine[] {
|
|
368
|
+
const columnNames = columns.filter((col) => col.type === 'currency').map((col) => col.name)
|
|
354
369
|
|
|
355
370
|
// Parse report and construct format.
|
|
356
371
|
const allAccounts: AccountNumber[] = Array.from(accountNumbers).sort()
|
|
@@ -359,9 +374,9 @@ export class ReportPlugin extends BackendPlugin {
|
|
|
359
374
|
if (/^#/.test(line)) {
|
|
360
375
|
return
|
|
361
376
|
}
|
|
362
|
-
let [
|
|
363
|
-
numbers =
|
|
364
|
-
flags =
|
|
377
|
+
let [accNumbers, text, accFlags] = line.split('\t')
|
|
378
|
+
const numbers = accNumbers.split(' ')
|
|
379
|
+
const flags: Set<ReportFlagName> = accFlags ? new Set(accFlags.trim().split(/\s+/) as ReportFlagName[]) : new Set()
|
|
365
380
|
const tab = text ? text.replace(/^(_*).*/, '$1').length : 0
|
|
366
381
|
text = text ? text.replace(/^_+/, '') : ''
|
|
367
382
|
|
|
@@ -376,10 +391,10 @@ export class ReportPlugin extends BackendPlugin {
|
|
|
376
391
|
}
|
|
377
392
|
|
|
378
393
|
// Split the line and reset variables.
|
|
379
|
-
const
|
|
380
|
-
columnNames.forEach((column) => (
|
|
394
|
+
const values: Record<string, number | null> = {}
|
|
395
|
+
columnNames.forEach((column) => (values[column] = null))
|
|
381
396
|
let unused = true
|
|
382
|
-
const item: ReportItem = { tab, ...this.flags2item(flags) }
|
|
397
|
+
const item: ReportItem = { tab, ...this.flags2item([...flags]) }
|
|
383
398
|
|
|
384
399
|
// Collect all totals inside any of the account number ranges.
|
|
385
400
|
for (let i = 0; i < numbers.length; i++) {
|
|
@@ -391,7 +406,7 @@ export class ReportPlugin extends BackendPlugin {
|
|
|
391
406
|
if (number >= from && number < to) {
|
|
392
407
|
unused = false
|
|
393
408
|
if (totals[column][number] !== undefined) {
|
|
394
|
-
|
|
409
|
+
values[column] = (values[column] || 0) + totals[column][number]
|
|
395
410
|
}
|
|
396
411
|
}
|
|
397
412
|
})
|
|
@@ -402,7 +417,7 @@ export class ReportPlugin extends BackendPlugin {
|
|
|
402
417
|
if (!item.accountDetails) {
|
|
403
418
|
if (item.required || !unused) {
|
|
404
419
|
item.name = text
|
|
405
|
-
item.
|
|
420
|
+
item.values = values
|
|
406
421
|
ret.push(item)
|
|
407
422
|
}
|
|
408
423
|
}
|
|
@@ -415,20 +430,20 @@ export class ReportPlugin extends BackendPlugin {
|
|
|
415
430
|
const to = parts[1]
|
|
416
431
|
allAccounts.forEach((number) => {
|
|
417
432
|
if (number >= from && number < to) {
|
|
418
|
-
const item = { tab, ...this.flags2item(flags) }
|
|
433
|
+
const item = { tab, ...this.flags2item([...flags]) }
|
|
419
434
|
item.isAccount = true
|
|
420
435
|
delete item.accountDetails
|
|
421
436
|
item.name = accountNames[number]
|
|
422
437
|
item.number = number
|
|
423
|
-
item.
|
|
438
|
+
item.values = {}
|
|
424
439
|
columnNames.forEach((column) => {
|
|
425
|
-
if (!item.
|
|
426
|
-
item.
|
|
440
|
+
if (!item.values) {
|
|
441
|
+
item.values = {}
|
|
427
442
|
}
|
|
428
443
|
if (totals[column][number] === undefined) {
|
|
429
|
-
item.
|
|
444
|
+
item.values[column] = null
|
|
430
445
|
} else {
|
|
431
|
-
item.
|
|
446
|
+
item.values[column] = totals[column][number] + 0
|
|
432
447
|
}
|
|
433
448
|
})
|
|
434
449
|
ret.push(item)
|
|
@@ -438,6 +453,7 @@ export class ReportPlugin extends BackendPlugin {
|
|
|
438
453
|
}
|
|
439
454
|
})
|
|
440
455
|
|
|
441
|
-
|
|
456
|
+
// TODO: Define stuff so that need no type hint.
|
|
457
|
+
return ret as ReportLine[]
|
|
442
458
|
}
|
|
443
459
|
}
|
|
@@ -166,4 +166,13 @@ export class ProcessFile {
|
|
|
166
166
|
|
|
167
167
|
throw new InvalidFile(`An encoding '${this.encoding}' is not yet supported.`)
|
|
168
168
|
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Set the new content for this file (as UTF-8).
|
|
172
|
+
*/
|
|
173
|
+
set(content: string): void {
|
|
174
|
+
this.encoding = 'utf-8'
|
|
175
|
+
this.data = clone(content)
|
|
176
|
+
this._decoded = undefined
|
|
177
|
+
}
|
|
169
178
|
}
|
|
@@ -10,7 +10,10 @@ import { Directions, ImportAction, ImportState, ProcessConfig } from '@tasenor/c
|
|
|
10
10
|
export class ProcessHandler {
|
|
11
11
|
|
|
12
12
|
system: ProcessingSystem
|
|
13
|
+
// Name of the process handler.
|
|
13
14
|
name: string
|
|
15
|
+
// Source data version (if more than one).
|
|
16
|
+
version?: number = undefined
|
|
14
17
|
|
|
15
18
|
constructor(name: string) {
|
|
16
19
|
this.name = name
|
|
@@ -25,13 +28,20 @@ export class ProcessHandler {
|
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
/**
|
|
28
|
-
* Check if we are able to handle the given file.
|
|
31
|
+
* Check if we are able to handle the given file. Can be boolean or version number of data format.
|
|
29
32
|
* @param file
|
|
30
33
|
*/
|
|
31
|
-
canHandle(file: ProcessFile): boolean {
|
|
34
|
+
canHandle(file: ProcessFile): boolean | number {
|
|
32
35
|
throw new NotImplemented(`A handler '${this.name}' cannot check file '${file.name}', since canHandle() is not implemented.`)
|
|
33
36
|
}
|
|
34
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Initialization hook called after file(s) has been loaded.
|
|
40
|
+
*/
|
|
41
|
+
async init(files: ProcessFile[]): Promise<ProcessFile[]> {
|
|
42
|
+
return files
|
|
43
|
+
}
|
|
44
|
+
|
|
35
45
|
/**
|
|
36
46
|
* Check if we are able to append the given file to the process.
|
|
37
47
|
* @param file
|
|
@@ -92,7 +92,13 @@ export class ProcessingSystem {
|
|
|
92
92
|
let selectedHandler: ProcessHandler | null = null
|
|
93
93
|
for (const handler of Object.values(this.handlers)) {
|
|
94
94
|
try {
|
|
95
|
-
|
|
95
|
+
const version = handler.canHandle(processFile)
|
|
96
|
+
if (version) {
|
|
97
|
+
if (version === true) {
|
|
98
|
+
handler.version = 1
|
|
99
|
+
} else {
|
|
100
|
+
handler.version = version
|
|
101
|
+
}
|
|
96
102
|
selectedHandler = handler
|
|
97
103
|
break
|
|
98
104
|
}
|
|
@@ -109,7 +115,6 @@ export class ProcessingSystem {
|
|
|
109
115
|
// Check if the handler accepts the rest of the files.
|
|
110
116
|
for (let i = 1; i < files.length; i++) {
|
|
111
117
|
const processFile = new ProcessFile(files[i])
|
|
112
|
-
// await processFile.save(this.db)
|
|
113
118
|
if (!selectedHandler.canAppend(processFile)) {
|
|
114
119
|
await process.crashed(new InvalidArgument(`The file ${files[i].name} of type ${files[i].type} cannot be appended to handler.`))
|
|
115
120
|
return process
|
|
@@ -117,6 +122,7 @@ export class ProcessingSystem {
|
|
|
117
122
|
process.addFile(processFile)
|
|
118
123
|
await processFile.save(this.db)
|
|
119
124
|
}
|
|
125
|
+
process.files = await selectedHandler.init(process.files)
|
|
120
126
|
|
|
121
127
|
// Create initial step using the handler.
|
|
122
128
|
let state
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ReportQueryParams } from '@tasenor/common'
|
|
1
|
+
import { ReportQueryParams, strRound } from '@tasenor/common'
|
|
2
2
|
import json2csv from 'json2csv'
|
|
3
3
|
import { sprintf } from 'sprintf-js'
|
|
4
4
|
|
|
@@ -13,14 +13,23 @@ export function data2csv(report, options: ReportQueryParams) {
|
|
|
13
13
|
|
|
14
14
|
const render = {
|
|
15
15
|
id: (column, entry) => entry.id,
|
|
16
|
-
name: (column, entry) => `${entry.isAccount ? entry.number + ' ' : ''}${entry.name}`,
|
|
16
|
+
name: (column, entry) => entry.name === undefined ? '' : `${entry.isAccount ? entry.number + ' ' : ''}${entry.name}`,
|
|
17
17
|
text: (column, entry) => entry[column.name],
|
|
18
|
-
|
|
18
|
+
// TODO: Here and rendering we could use heuristic string rounding, i.e. get rid of ..3999999 -> ..4
|
|
19
|
+
// Need to share function with rendering.
|
|
20
|
+
numeric: (column, entry) => (entry.values &&
|
|
19
21
|
!entry.hideTotal &&
|
|
20
|
-
entry.
|
|
21
|
-
!isNaN(entry.
|
|
22
|
-
entry.
|
|
23
|
-
? (entry.
|
|
22
|
+
entry.values[column.name] !== '' &&
|
|
23
|
+
!isNaN(entry.values[column.name]) &&
|
|
24
|
+
entry.values[column.name] !== undefined)
|
|
25
|
+
? (entry.values[column.name] === null ? '—' : strRound(entry.values[column.name]))
|
|
26
|
+
: '',
|
|
27
|
+
currency: (column, entry) => (entry.values &&
|
|
28
|
+
!entry.hideTotal &&
|
|
29
|
+
entry.values[column.name] !== '' &&
|
|
30
|
+
!isNaN(entry.values[column.name]) &&
|
|
31
|
+
entry.values[column.name] !== undefined)
|
|
32
|
+
? (entry.values[column.name] === null ? '—' : sprintf('%.2f', entry.values[column.name] / 100))
|
|
24
33
|
: ''
|
|
25
34
|
}
|
|
26
35
|
|