@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.
Files changed (35) hide show
  1. package/dist/src/commands/report.js +12 -8
  2. package/dist/src/commands/report.js.map +1 -1
  3. package/dist/src/database/DB.js +1 -4
  4. package/dist/src/database/DB.js.map +1 -1
  5. package/dist/src/import/TransactionImportHandler.js +16 -0
  6. package/dist/src/import/TransactionImportHandler.js.map +1 -1
  7. package/dist/src/import/TransactionRules.d.ts +1 -0
  8. package/dist/src/import/TransactionRules.js +3 -1
  9. package/dist/src/import/TransactionRules.js.map +1 -1
  10. package/dist/src/import/TransferAnalyzer.js +2 -2
  11. package/dist/src/import/TransferAnalyzer.js.map +1 -1
  12. package/dist/src/plugins/ReportPlugin.d.ts +19 -17
  13. package/dist/src/plugins/ReportPlugin.js +38 -25
  14. package/dist/src/plugins/ReportPlugin.js.map +1 -1
  15. package/dist/src/process/ProcessFile.d.ts +4 -0
  16. package/dist/src/process/ProcessFile.js +8 -0
  17. package/dist/src/process/ProcessFile.js.map +1 -1
  18. package/dist/src/process/ProcessHandler.d.ts +7 -2
  19. package/dist/src/process/ProcessHandler.js +10 -1
  20. package/dist/src/process/ProcessHandler.js.map +1 -1
  21. package/dist/src/process/ProcessingSystem.js +9 -2
  22. package/dist/src/process/ProcessingSystem.js.map +1 -1
  23. package/dist/src/reports/conversions.js +16 -6
  24. package/dist/src/reports/conversions.js.map +1 -1
  25. package/package.json +4 -4
  26. package/src/commands/report.ts +13 -9
  27. package/src/database/DB.ts +1 -5
  28. package/src/import/TransactionImportHandler.ts +17 -0
  29. package/src/import/TransactionRules.ts +3 -1
  30. package/src/import/TransferAnalyzer.ts +2 -2
  31. package/src/plugins/ReportPlugin.ts +58 -42
  32. package/src/process/ProcessFile.ts +9 -0
  33. package/src/process/ProcessHandler.ts +12 -2
  34. package/src/process/ProcessingSystem.ts +8 -2
  35. package/src/reports/conversions.ts +16 -7
@@ -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 as Record<string, unknown>).forEach((meta) => console.log(`${meta}: ${(report.meta as Record<string, unknown>)[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
- for (const column of columns) {
75
- const text = {
76
- id: () => item.id,
77
- name: () => item.name,
78
- numeric: () => item.amounts && item.amounts[column.name] !== undefined && sprintf('%.2f', (item.amounts[column.name] || 0) / 100)
79
- }[column.type]()
80
- line.push(text || '')
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
 
@@ -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 sold from ${JSON.stringify(transfers.transfers)}`)
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 struture file.
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: 'numeric',
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
- * * `amounts` An object with entry for each column mapping name of the columnt to the value to display.
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 = await this.constructSqlQuery(db, options, settings)
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 === 'numeric').map((col) => col.name)
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 [numbers, text, flags] = line.split('\t')
363
- numbers = numbers.split(' ')
364
- flags = flags ? new Set(flags.trim().split(/\s+/)) : new Set()
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 amounts: Record<string, number | null> = {}
380
- columnNames.forEach((column) => (amounts[column] = null))
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
- amounts[column] += totals[column][number]
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.amounts = amounts
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.amounts = {}
438
+ item.values = {}
424
439
  columnNames.forEach((column) => {
425
- if (!item.amounts) {
426
- item.amounts = {}
440
+ if (!item.values) {
441
+ item.values = {}
427
442
  }
428
443
  if (totals[column][number] === undefined) {
429
- item.amounts[column] = null
444
+ item.values[column] = null
430
445
  } else {
431
- item.amounts[column] = totals[column][number] + 0
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
- return ret
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
- if (handler.canHandle(processFile)) {
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
- numeric: (column, entry) => (entry.amounts &&
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.amounts[column.name] !== '' &&
21
- !isNaN(entry.amounts[column.name]) &&
22
- entry.amounts[column.name] !== undefined)
23
- ? (entry.amounts[column.name] === null ? '—' : sprintf('%.2f', entry.amounts[column.name] / 100))
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