LCOV - code coverage report
Current view: top level - _classes/storage - file_picker.dart Coverage Total Hit
Test: lcov.info Lines: 7.8 % 64 5
Test Date: 2026-11-14 15:03:01 Functions: - 0 0

            Line data    Source code
       1              : // Copyright 2023 The terCAD team. All rights reserved.
       2              : // Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file.
       3              : 
       4              : import 'dart:typed_data';
       5              : 
       6              : import 'package:app_finance/_classes/herald/app_locale.dart';
       7              : import 'package:app_finance/_classes/storage/file_parser.dart';
       8              : import 'package:app_finance/_classes/storage/statement_parser.dart';
       9              : import 'package:app_finance/_mixins/file/file_import_mixin.dart';
      10              : import 'package:csv/csv.dart';
      11              : 
      12              : typedef FileScope = List<List<dynamic>>;
      13              : 
      14              : class FilePicker with FileImportMixin {
      15              :   static const csvFormat = 'csv';
      16              :   static const qifFormat = 'qif';
      17              :   static const ofxFormat = 'ofx';
      18              :   static const fileFormats = [csvFormat, qifFormat, ofxFormat, ...StatementParser.formats];
      19              :   final List<String> ext;
      20              :   List<String> columnMap = [
      21              :     FileParser.attrBillUuid,
      22              :     FileParser.attrBillAmount,
      23              :     FileParser.attrBillComment,
      24              :     FileParser.attrCategoryName,
      25              :     FileParser.attrBillDate,
      26              :     FileParser.attrBillType,
      27              :   ];
      28              :   final List<String> header = [
      29              :     AppLocale.labels.uuid,
      30              :     AppLocale.labels.expense,
      31              :     AppLocale.labels.description,
      32              :     AppLocale.labels.budget,
      33              :     AppLocale.labels.balanceDate,
      34              :     AppLocale.labels.flowTypeTooltip,
      35              :   ];
      36              : 
      37              :   // Pending transactions a statement file holds, left out of the import
      38              :   int pending = 0;
      39              : 
      40            1 :   FilePicker(this.ext);
      41              : 
      42            1 :   FileScope parseStatement(String format, Uint8List bytes) {
      43            1 :     final (:rows, :pending) = StatementParser.parse(format, bytes);
      44            1 :     this.pending = pending;
      45            2 :     columnMap = [
      46              :       FileParser.attrBillUuid,
      47              :       FileParser.attrBillAmount,
      48              :       FileParser.attrBillComment,
      49              :       FileParser.attrBillDate,
      50              :       FileParser.attrBillType,
      51              :       FileParser.attrBillCurrency,
      52              :       '',
      53              :     ];
      54              :     return rows;
      55              :   }
      56              : 
      57            0 :   FileScope _parseCsv(String content, String splitter) {
      58            0 :     final result = CsvToListConverter(eol: splitter).convert(content);
      59            0 :     columnMap = List<String>.filled(result.first.length, '');
      60              :     return result;
      61              :   }
      62              : 
      63            0 :   FileScope _parseQif(String content, String splitter) {
      64            0 :     final scope = content.split(splitter);
      65              :     int idx = 1;
      66            0 :     Map<String, int> mapping = {
      67              :       'N': 0, // "Number" for the transaction
      68              :       'T': 1, // "Total" amount
      69              :       'P': 2, // "Payee"
      70              :       'L': 3, // "Category/Account Line"
      71              :       'D': 4, // "Date"
      72              :     };
      73              :     int billType = 5;
      74            0 :     FileScope result = [header];
      75            0 :     result.add(List<dynamic>.filled(header.length, null));
      76            0 :     for (int i = 0; i < scope.length; i++) {
      77            0 :       if (scope[i].isEmpty) {
      78              :         continue;
      79              :       }
      80            0 :       final key = scope[i].substring(0, 1);
      81            0 :       final value = scope[i].substring(1);
      82            0 :       if (key == '^') {
      83            0 :         idx++;
      84            0 :         result.add(List<dynamic>.filled(header.length, null));
      85              :         continue;
      86              :       }
      87            0 :       int? pos = mapping[key];
      88              :       if (pos != null) {
      89            0 :         if (key == 'T') {
      90            0 :           result[idx][pos] = (double.tryParse(value) ?? 0.0).abs().toString();
      91            0 :           result[idx][billType] = (double.tryParse(value) ?? 0) > 0 ? AppLocale.labels.flowTypeInvoice : '';
      92              :         } else {
      93            0 :           result[idx][pos] = value;
      94              :         }
      95              :       }
      96              :     }
      97            0 :     if (result.last.every((element) => element == null)) {
      98            0 :       result.removeLast();
      99              :     }
     100              :     return result;
     101              :   }
     102              : 
     103            0 :   FileScope _parseOfx(String content) {
     104            0 :     FileScope result = [header];
     105              :     int amountType = 1;
     106              :     int dateType = 4;
     107              :     int billType = 5;
     108            0 :     Map<String, int> mapping = {
     109              :       'FITID': 0, // Unique ID
     110              :       'TRNAMT': amountType, // Amount
     111              :       'NAME': 2, // Description
     112              :       'DTPOSTED': dateType, // Date
     113              :     };
     114              : 
     115            0 :     final data = content.split('<STMTTRN>');
     116            0 :     for (int i = 1; i < data.length; i++) {
     117            0 :       final tmp = List<dynamic>.filled(7, null);
     118            0 :       for (var key in mapping.keys) {
     119            0 :         final regexp = RegExp(r'(?<=<' + key + r'>)(.*?)(?=<)');
     120            0 :         final match = regexp.firstMatch(data[i]);
     121              :         if (match != null) {
     122            0 :           int pos = mapping[key]!;
     123            0 :           tmp[pos] = match.group(0)!;
     124            0 :           if (pos == dateType) {
     125            0 :             List<String> date = tmp[pos].split('');
     126            0 :             if (date.length > 8) {
     127            0 :               date.insert(8, 'T');
     128            0 :               tmp[pos] = date.join('');
     129              :             }
     130            0 :           } else if (pos == amountType) {
     131            0 :             double nm = double.tryParse(tmp[pos]) ?? 0.0;
     132            0 :             tmp[pos] = nm.abs().toString();
     133            0 :             tmp[billType] = nm > 0 ? AppLocale.labels.flowTypeInvoice : AppLocale.labels.bill;
     134              :           }
     135              :         }
     136              :       }
     137            0 :       result.add(tmp);
     138              :     }
     139              :     return result;
     140              :   }
     141              : 
     142            0 :   Future<FileScope?> pickFile() async {
     143            0 :     final format = ext.first;
     144            0 :     if (StatementParser.isStatement(format)) {
     145            0 :       final bytes = await importFileBytes(StatementParser.extensions[format]!);
     146              :       if (bytes == null) {
     147            0 :         throw Exception(AppLocale.labels.missingContent);
     148              :       }
     149            0 :       return parseStatement(format, bytes);
     150              :     }
     151            0 :     String? content = await importFile(ext);
     152              :     if (content != null) {
     153            0 :       final splitter = content.contains('\r\n') ? '\r\n' : '\n';
     154            0 :       switch (ext.first) {
     155            0 :         case csvFormat:
     156            0 :           return _parseCsv(content, splitter);
     157            0 :         case qifFormat:
     158            0 :           return _parseQif(content, splitter);
     159            0 :         case ofxFormat:
     160            0 :           return _parseOfx(content.replaceAll(splitter, ''));
     161              :       }
     162              :     } else {
     163            0 :       throw Exception(AppLocale.labels.missingContent);
     164              :     }
     165              :     return null;
     166              :   }
     167              : }
        

Generated by: LCOV version 2.0-1