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 'package:app_finance/_classes/herald/app_locale.dart'; 5 : import 'package:app_finance/_classes/storage/file_parser.dart'; 6 : import 'package:app_finance/_mixins/file/file_import_mixin.dart'; 7 : import 'package:csv/csv.dart'; 8 : import 'package:xml/xml.dart'; 9 : 10 : typedef FileScope = List<List<dynamic>>; 11 : 12 : class FilePicker with FileImportMixin { 13 : static const csvFormat = 'csv'; 14 : static const qifFormat = 'qif'; 15 : static const ofxFormat = 'ofx'; 16 : static const fileFormats = [csvFormat, qifFormat, ofxFormat]; 17 : final List<String> ext; 18 : List<String> columnMap = [ 19 : FileParser.attrBillUuid, 20 : FileParser.attrBillAmount, 21 : FileParser.attrBillComment, 22 : FileParser.attrCategoryName, 23 : FileParser.attrBillDate, 24 : FileParser.attrBillType, 25 : ]; 26 : final header = [ 27 : AppLocale.labels.uuid, 28 : AppLocale.labels.expense, 29 : AppLocale.labels.description, 30 : AppLocale.labels.budget, 31 : AppLocale.labels.balanceDate, 32 : AppLocale.labels.flowTypeTooltip, 33 : ]; 34 : 35 0 : FilePicker(this.ext); 36 : 37 0 : FileScope _parseCsv(String content, String splitter) { 38 0 : final result = CsvToListConverter(eol: splitter).convert(content); 39 0 : columnMap = List<String>.filled(result.first.length, ''); 40 : return result; 41 : } 42 : 43 0 : FileScope _parseQif(String content, String splitter) { 44 0 : final scope = content.split(splitter); 45 : int idx = 1; 46 0 : Map<String, int> mapping = { 47 : 'N': 0, // "Number" for the transaction 48 : 'T': 1, // "Total" amount 49 : 'P': 2, // "Payee" 50 : 'L': 3, // "Category/Account Line" 51 : 'D': 4, // "Date" 52 : }; 53 : int billType = 5; 54 0 : FileScope result = [header]; 55 0 : result.add(List<dynamic>.filled(header.length, null)); 56 0 : for (int i = 0; i < scope.length; i++) { 57 0 : if (scope[i].isEmpty) { 58 : continue; 59 : } 60 0 : final key = scope[i].substring(0, 1); 61 0 : final value = scope[i].substring(1); 62 0 : if (key == '^') { 63 0 : idx++; 64 0 : result.add(List<dynamic>.filled(header.length, null)); 65 : continue; 66 : } 67 0 : int? pos = mapping[key]; 68 : if (pos != null) { 69 0 : if (key == 'T') { 70 0 : result[idx][pos] = (double.tryParse(value) ?? 0.0).abs().toString(); 71 0 : result[idx][billType] = (double.tryParse(value) ?? 0) > 0 ? AppLocale.labels.flowTypeInvoice : ''; 72 : } else { 73 0 : result[idx][pos] = value; 74 : } 75 : } 76 : } 77 0 : if (result.last.every((element) => element == null)) { 78 0 : result.removeLast(); 79 : } 80 : return result; 81 : } 82 : 83 0 : FileScope _parseOfx(String content) { 84 0 : FileScope result = [header]; 85 0 : final xmlData = XmlDocument.parse(content); 86 : int amountType = 1; 87 : int dateType = 4; 88 : int billType = 5; 89 0 : Map<String, int> mapping = { 90 : 'FITID': 0, // Unique ID 91 : 'TRNAMT': amountType, // Amount 92 : 'NAME': 2, // Description 93 : 'DTPOSTED': dateType, // Date 94 : }; 95 0 : for (XmlElement node in xmlData.findAllElements('STMTTRN')) { 96 0 : final tmp = List<dynamic>.filled(6, null); 97 0 : for (XmlElement element in node.childElements) { 98 0 : int? pos = mapping[element.name.toString()]; 99 0 : String value = element.innerText; 100 : if (pos != null) { 101 0 : if (pos == dateType) { 102 0 : List<String> date = value.split(''); 103 0 : date.insert(8, 'T'); 104 0 : tmp[pos] = date.join(''); 105 0 : } else if (pos == amountType) { 106 0 : double nm = double.tryParse(value) ?? 0.0; 107 0 : tmp[pos] = nm.abs().toString(); 108 0 : tmp[billType] = nm > 0 ? AppLocale.labels.flowTypeInvoice : ''; 109 : } else { 110 0 : tmp[pos] = value; 111 : } 112 : } 113 : } 114 0 : result.add(tmp); 115 : } 116 : return result; 117 : } 118 : 119 0 : Future<FileScope?> pickFile() async { 120 0 : String? content = await importFile(ext); 121 : if (content != null) { 122 0 : final splitter = content.contains('\r\n') ? '\r\n' : '\n'; 123 0 : switch (ext.first) { 124 0 : case csvFormat: 125 0 : return _parseCsv(content, splitter); 126 0 : case qifFormat: 127 0 : return _parseQif(content, splitter); 128 0 : case ofxFormat: 129 0 : return _parseOfx(content); 130 : } 131 : } else { 132 0 : throw Exception(AppLocale.labels.missingContent); 133 : } 134 : return null; 135 : } 136 : }