File indexing completed on 2024-06-16 04:47:19
0001 /*************************************************************************** 0002 * SPDX-FileCopyrightText: 2022 S. MANKOWSKI stephane@mankowski.fr 0003 * SPDX-FileCopyrightText: 2022 G. DE BURE support@mankowski.fr 0004 * SPDX-License-Identifier: GPL-3.0-or-later 0005 ***************************************************************************/ 0006 /** @file 0007 * This file is Skrooge plugin for CSV import / export. 0008 * 0009 * @author Stephane MANKOWSKI / Guillaume DE BURE 0010 */ 0011 #include "skgimportplugincsv.h" 0012 0013 #include <klocalizedstring.h> 0014 0015 #include <kpluginfactory.h> 0016 0017 #include <qcryptographichash.h> 0018 #include <qfileinfo.h> 0019 #include <qsavefile.h> 0020 #include <qregularexpression.h> 0021 0022 #include "skgbankincludes.h" 0023 #include "skgimportexportmanager.h" 0024 #include "skgservices.h" 0025 #include "skgtraces.h" 0026 0027 /** 0028 * This plugin factory. 0029 */ 0030 K_PLUGIN_CLASS_WITH_JSON(SKGImportPluginCsv, "metadata.json") 0031 0032 SKGImportPluginCsv::SKGImportPluginCsv(QObject* iImporter, const QVariantList& iArg) 0033 : SKGImportPlugin(iImporter), 0034 m_csvSeparator(QChar()), m_csvHeaderIndex(-1) 0035 { 0036 SKGTRACEINFUNC(10) 0037 Q_UNUSED(iArg) 0038 0039 m_importParameters[QStringLiteral("mapping_date")] = i18nc("This is a regular expression to find the column in a csv file. You should keep the ^ and translate the word", "^date"); 0040 m_importParameters[QStringLiteral("mapping_account")] = i18nc("This is a regular expression to find the column in a csv file. You should keep the ^ and translate the word", "^account"); 0041 m_importParameters[QStringLiteral("mapping_number")] = i18nc("This is a regular expression to find the column in a csv file. You should keep the ^ and | and translate the words", "^number|^num?ro"); 0042 m_importParameters[QStringLiteral("mapping_mode")] = i18nc("This is a regular expression to find the column in a csv file. You should keep the ^ and | and translate the words", "^mode|^type"); 0043 m_importParameters[QStringLiteral("mapping_payee")] = i18nc("This is a regular expression to find the column in a csv file. You should keep the ^ and | and translate the words", "^payee|^tiers"); 0044 m_importParameters[QStringLiteral("mapping_comment")] = i18nc("This is a regular expression to find the column in a csv file. You should keep the ^ and | and translate the words", "^comment|^libell?|^d?tail|^info"); 0045 m_importParameters[QStringLiteral("mapping_status")] = i18nc("This is a regular expression to find the column in a csv file. You should keep the ^ and | and translate the words", "^status|^pointage"); 0046 m_importParameters[QStringLiteral("mapping_bookmarked")] = i18nc("This is a regular expression to find the column in a csv file. You should keep the ^ and translate the word", "^bookmarked"); 0047 m_importParameters[QStringLiteral("mapping_category")] = i18nc("This is a regular expression to find the column in a csv file. You should keep the ^ and translate the word", "^cat\\w*gor\\w*"); 0048 m_importParameters[QStringLiteral("mapping_amount")] = i18nc("This is a regular expression to find the column in a csv file. You should keep the ^ and | and translate the words", "^value|^amount|^valeur|^montant|^credit|^debit"); 0049 m_importParameters[QStringLiteral("mapping_quantity")] = i18nc("This is a regular expression to find the column in a csv file. You should keep the ^ and translate the word", "^quantity"); 0050 m_importParameters[QStringLiteral("mapping_unit")] = i18nc("This is a regular expression to find the column in a csv file. You should keep the ^ and translate the word", "^unit"); 0051 m_importParameters[QStringLiteral("mapping_sign")] = i18nc("This is a regular expression to find the column in a csv file. You should keep the ^ and | and translate the words", "^sign|^sens"); 0052 m_importParameters[QStringLiteral("mapping_debit")] = i18nc("This is a regular expression to find the column in a csv file. You should keep the ^ and | and translate the words", "^-|^debit|^withdrawal"); 0053 m_importParameters[QStringLiteral("mapping_idgroup")] = i18nc("This is a regular expression to find the column in a csv file. You should keep the ^ and translate the word", "^idgroup"); 0054 m_importParameters[QStringLiteral("mapping_idtransaction")] = i18nc("This is a regular expression to find the column in a csv file. You should keep the ^ and translate the word", "^idtransaction"); 0055 m_importParameters[QStringLiteral("mapping_property")] = QString(); 0056 0057 m_importParameters[QStringLiteral("automatic_search_header")] = 'Y'; 0058 m_importParameters[QStringLiteral("header_position")] = '1'; 0059 m_importParameters[QStringLiteral("automatic_search_columns")] = 'Y'; 0060 m_importParameters[QStringLiteral("columns_positions")] = QString(); 0061 0062 m_importParameters[QStringLiteral("mode_csv_unit")] = 'N'; 0063 m_importParameters[QStringLiteral("mode_csv_rule")] = 'N'; 0064 0065 m_importParameters[QStringLiteral("date_format")] = QString(); 0066 } 0067 0068 SKGImportPluginCsv::~SKGImportPluginCsv() 0069 = default; 0070 0071 void SKGImportPluginCsv::setImportParameters(const QMap< QString, QString >& iParameters) 0072 { 0073 SKGImportPlugin::setImportParameters(iParameters); 0074 0075 if (m_importParameters.value(QStringLiteral("automatic_search_header")) == QStringLiteral("N")) { 0076 int header_position = SKGServices::stringToInt(m_importParameters.value(QStringLiteral("header_position"))); 0077 setCSVHeaderIndex(header_position); 0078 } 0079 0080 if (m_importParameters.value(QStringLiteral("automatic_search_columns")) == QStringLiteral("N")) { 0081 QStringList columns_positions = m_importParameters.value(QStringLiteral("columns_positions")).split('|'); 0082 if (m_csvHeaderIndex == -1) { 0083 m_csvHeaderIndex = 1; 0084 } 0085 setCSVMapping(&columns_positions); 0086 } 0087 } 0088 0089 bool SKGImportPluginCsv::isImportPossible() 0090 { 0091 SKGTRACEINFUNC(10) 0092 return isExportPossible(); 0093 } 0094 0095 QStringList SKGImportPluginCsv::getCSVMappingFromLine(const QString& iLine) 0096 { 0097 QStringList output; 0098 QString line = iLine.trimmed(); 0099 0100 // Split first line 0101 QStringList csvAttributes = SKGServices::splitCSVLine(line, getCSVSeparator(iLine), true); 0102 int nb = csvAttributes.count(); 0103 0104 for (int i = 0; i < nb; ++i) { 0105 QString att = csvAttributes.at(i).toLower(); 0106 0107 // Search if this csv column is mapping a std attribute 0108 QMapIterator<QString, QString> csvMapperIterator(m_importParameters); 0109 bool found = false; 0110 while (!found && csvMapperIterator.hasNext()) { 0111 csvMapperIterator.next(); 0112 0113 QString key = csvMapperIterator.key(); 0114 if (key.startsWith(QLatin1String("mapping_"))) { 0115 key = key.right(key.length() - 8); 0116 if (key != QStringLiteral("debit") && 0117 key != QStringLiteral("property") && 0118 !csvMapperIterator.value().isEmpty() && 0119 QRegularExpression(csvMapperIterator.value(), QRegularExpression::CaseInsensitiveOption).match(att).hasMatch() && 0120 (!output.contains(key) || key == QStringLiteral("comment") || key == QStringLiteral("category") || key == QStringLiteral("amount"))) { 0121 output.push_back(key); 0122 found = true; 0123 } 0124 } 0125 } 0126 0127 // Search if this csv column must be added as a property 0128 if (!found && 0129 !m_importParameters.value(QStringLiteral("mapping_property")).isEmpty() && 0130 QRegularExpression(m_importParameters.value(QStringLiteral("mapping_property")), QRegularExpression::CaseInsensitiveOption).match(att).hasMatch() && 0131 !output.contains(att)) { 0132 output.push_back(att); 0133 found = true; 0134 } 0135 0136 if (!found) { 0137 output.push_back(QString()); // To ignore this column 0138 } 0139 } 0140 return output; 0141 } 0142 0143 SKGError SKGImportPluginCsv::setCSVMapping(const QStringList* iCSVMapping) 0144 { 0145 SKGError err; 0146 SKGTRACEINFUNCRC(10, err) 0147 0148 m_csvMapping.clear(); 0149 0150 if (iCSVMapping == nullptr) { 0151 // Automatic build 0152 // Open file 0153 QFile file(m_importer->getLocalFileName()); 0154 if (Q_UNLIKELY(!file.open(QIODevice::ReadOnly))) { 0155 err.setReturnCode(ERR_INVALIDARG).setMessage(i18nc("Error message", "Open file '%1' failed", m_importer->getFileName().toDisplayString())); 0156 } else { 0157 QTextStream stream(&file); 0158 if (!m_importer->getCodec().isEmpty()) { 0159 stream.setCodec(m_importer->getCodec().toLatin1().constData()); 0160 } 0161 0162 // Ignore useless lines 0163 int headerIndex = getCSVHeaderIndex(); 0164 for (int i = 1; i < headerIndex; ++i) { 0165 stream.readLine(); 0166 } 0167 0168 // Get mapping 0169 if (!stream.atEnd()) { 0170 m_csvMapping = getCSVMappingFromLine(stream.readLine()); 0171 } else { 0172 err.setReturnCode(ERR_INVALIDARG); 0173 } 0174 0175 // close file 0176 file.close(); 0177 } 0178 } else { 0179 // Manual build 0180 m_csvMapping = *iCSVMapping; 0181 } 0182 0183 IFOK(err) { 0184 // Check if mandatory attributes have been found 0185 if (m_importParameters.value(QStringLiteral("mode_csv_rule")) == QStringLiteral("Y")) { 0186 if (!m_csvMapping.contains(QStringLiteral("payee")) || !m_csvMapping.contains(QStringLiteral("category"))) { 0187 err = SKGError(ERR_FAIL, i18nc("Error message", "Columns payee and category not found. Set import parameters in settings (Configure Skrooge… / Import/Export / CSV / Edit regular expressions…).")); 0188 } 0189 } else { 0190 if (!m_csvMapping.contains(QStringLiteral("date")) || !m_csvMapping.contains(QStringLiteral("amount"))) { 0191 err = SKGError(ERR_FAIL, i18nc("Error message", "Columns date and amount not found. Set import parameters in settings (Configure Skrooge… / Import/Export / CSV / Edit regular expressions…).")); 0192 } 0193 } 0194 } 0195 0196 return err; 0197 } 0198 0199 SKGError SKGImportPluginCsv::setCSVHeaderIndex(int iIndex) 0200 { 0201 SKGError err; 0202 SKGTRACEINFUNCRC(10, err) 0203 0204 if (iIndex == -1) { 0205 // Automatic build 0206 // Open file 0207 QFile file(m_importer->getLocalFileName()); 0208 if (Q_UNLIKELY(!file.open(QIODevice::ReadOnly))) { 0209 err.setReturnCode(ERR_INVALIDARG).setMessage(i18nc("Error message", "Open file '%1' failed", m_importer->getFileName().toDisplayString())); 0210 } else { 0211 QTextStream stream(&file); 0212 if (!m_importer->getCodec().isEmpty()) { 0213 stream.setCodec(m_importer->getCodec().toLatin1().constData()); 0214 } 0215 0216 int i = 1; 0217 m_csvHeaderIndex = -1; 0218 while (!stream.atEnd() && m_csvHeaderIndex == -1) { 0219 // Read line 0220 QStringList map = getCSVMappingFromLine(stream.readLine()); 0221 if (m_importParameters.value(QStringLiteral("mode_csv_rule")) == QStringLiteral("Y")) { 0222 if (map.contains(QStringLiteral("payee")) && map.contains(QStringLiteral("category"))) { 0223 m_csvHeaderIndex = i; 0224 } 0225 } else { 0226 if (map.contains(QStringLiteral("date")) && map.contains(QStringLiteral("amount"))) { 0227 m_csvHeaderIndex = i; 0228 } 0229 } 0230 0231 ++i; 0232 } 0233 0234 // close file 0235 file.close(); 0236 } 0237 } else { 0238 // Manual build 0239 m_csvHeaderIndex = iIndex; 0240 } 0241 0242 return err; 0243 } 0244 0245 int SKGImportPluginCsv::getCSVHeaderIndex() 0246 { 0247 SKGTRACEINFUNC(10) 0248 if (m_csvHeaderIndex == -1) { 0249 setCSVHeaderIndex(-1); 0250 } 0251 return m_csvHeaderIndex; 0252 } 0253 0254 QChar SKGImportPluginCsv::getCSVSeparator(const QString& iLine) 0255 { 0256 SKGTRACEINFUNC(10) 0257 if (m_csvSeparator.isNull()) { 0258 QStringList csvAttributes = SKGServices::splitCSVLine(iLine, ';', true, &m_csvSeparator); 0259 int nb = csvAttributes.count(); 0260 0261 // If the split fails, we try with another separator 0262 if (nb == 1) { 0263 csvAttributes = SKGServices::splitCSVLine(iLine, ',', true, &m_csvSeparator); 0264 nb = csvAttributes.count(); 0265 0266 // If the split fails, we try with another separator 0267 if (nb == 1) { 0268 csvAttributes = SKGServices::splitCSVLine(iLine, '\t', true, &m_csvSeparator); 0269 } 0270 } 0271 } 0272 return m_csvSeparator; 0273 } 0274 0275 SKGError SKGImportPluginCsv::importFile() 0276 { 0277 if (m_importParameters.value(QStringLiteral("mode_csv_rule")) == QStringLiteral("Y")) { 0278 return importCSVRule(); 0279 } 0280 if (m_importParameters.value(QStringLiteral("mode_csv_unit")) == QStringLiteral("Y")) { 0281 return importCSVUnit(); 0282 } 0283 if (m_importer->getDocument() == nullptr) { 0284 return SKGError(ERR_ABORT, i18nc("Error message", "Invalid parameters")); 0285 } 0286 0287 SKGError err; 0288 SKGTRACEINFUNCRC(2, err) 0289 // Begin transaction 0290 err = m_importer->getDocument()->beginTransaction("#INTERNAL#" % i18nc("Import step", "Import %1 file", "CSV"), 3); 0291 IFOK(err) { 0292 // Initialize some variables 0293 QDateTime now = QDateTime::currentDateTime(); 0294 QString postFix = SKGServices::dateToSqlString(now); 0295 0296 // Default mapping 0297 if (m_csvMapping.isEmpty()) { 0298 err = setCSVMapping(nullptr); 0299 IFOKDO(err, m_importer->getDocument()->sendMessage(i18nc("An information message", "Use automatic search of the columns"))) 0300 } 0301 IFOKDO(err, m_importer->getDocument()->sendMessage(i18nc("An information message", "Mapping used: %1", m_csvMapping.join(QStringLiteral("|"))))) 0302 0303 // Step 1 done 0304 IFOKDO(err, m_importer->getDocument()->stepForward(1)) 0305 0306 // Open file 0307 IFOK(err) { 0308 QFile file(m_importer->getLocalFileName()); 0309 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { 0310 err.setReturnCode(ERR_INVALIDARG).setMessage(i18nc("Error message", "Open file '%1' failed", m_importer->getFileName().toDisplayString())); 0311 } else { 0312 QTextStream stream(&file); 0313 if (!m_importer->getCodec().isEmpty()) { 0314 stream.setCodec(m_importer->getCodec().toLatin1().constData()); 0315 } 0316 0317 // Ignore useless lines 0318 int headerIndex = getCSVHeaderIndex(); 0319 if (headerIndex == -1) { 0320 err.setReturnCode(ERR_FAIL).setMessage(i18nc("Error message", "Header not found in CSV file")); 0321 } 0322 0323 for (int i = 1; i <= headerIndex; ++i) { 0324 stream.readLine(); 0325 } 0326 0327 QList<int> amountIndexes; 0328 int nb = m_csvMapping.count(); 0329 for (int i = 0; i < nb; ++i) { 0330 if (m_csvMapping.at(i) == QStringLiteral("amount")) { 0331 amountIndexes.push_back(i); 0332 } 0333 } 0334 int nbAmount = amountIndexes.count(); 0335 0336 // Get data column 0337 QStringList dates; 0338 QStringList lines; 0339 int posdate = -1; 0340 bool modeAutoCreditDebit = (!m_csvMapping.contains(QStringLiteral("sign")) && nbAmount == 2); 0341 IFOK(err) { 0342 posdate = m_csvMapping.indexOf(QStringLiteral("date")); 0343 if (posdate == -1) { 0344 posdate = m_csvMapping.indexOf(QStringLiteral("date1")); 0345 } 0346 if (posdate == -1) { 0347 posdate = m_csvMapping.indexOf(QStringLiteral("date2")); 0348 } 0349 if (posdate != -1) { 0350 QString currentLine; 0351 while (!stream.atEnd()) { 0352 // Read line 0353 QString line = stream.readLine().trimmed(); 0354 if (!line.isEmpty()) { 0355 if (line.startsWith(QLatin1String("\""))) { 0356 currentLine.clear(); 0357 } 0358 currentLine += line; 0359 0360 // Get date 0361 QStringList field = SKGServices::splitCSVLine(currentLine, getCSVSeparator(currentLine)); 0362 if (field.isEmpty()) { 0363 // This is a multi line csv filename 0364 } else { 0365 lines.push_back(currentLine); 0366 currentLine.clear(); 0367 0368 if (field.count() >= m_csvMapping.count()) { 0369 if (posdate < field.count()) { 0370 dates.push_back(field.value(posdate).remove(QStringLiteral(" 00:00:00")).trimmed()); 0371 } 0372 0373 // Check if all amounts are positive 0374 if (modeAutoCreditDebit) { 0375 for (int i = 0; modeAutoCreditDebit && i < nbAmount; ++i) { 0376 QString s = field.at(amountIndexes.at(i)).trimmed(); 0377 if (!s.isEmpty() && SKGServices::stringToDouble(s) < 0) { 0378 modeAutoCreditDebit = false; 0379 } 0380 } 0381 } 0382 } 0383 } 0384 } 0385 } 0386 } 0387 } 0388 0389 // close file 0390 file.close(); 0391 0392 // Select dateformat 0393 QString dateFormat = m_importParameters.value(QStringLiteral("date_format")); 0394 if (dateFormat.isEmpty()) { 0395 dateFormat = SKGServices::getDateFormat(dates); // Automatic detection 0396 } 0397 if (!err) { 0398 int posdate2 = m_csvMapping.indexOf(QStringLiteral("date2")); 0399 if (dateFormat.isEmpty()) { 0400 // Check if another "date" attribute exists 0401 if (posdate2 != -1) { 0402 m_csvMapping[posdate] = QLatin1String(""); 0403 0404 err = importFile(); 0405 0406 SKGENDTRANSACTION(m_importer->getDocument(), err) 0407 return err; 0408 } 0409 err.setReturnCode(ERR_FAIL).setMessage(i18nc("Error message", "Date format not supported")); 0410 } else { 0411 // Disable other date columns 0412 if (posdate2 != -1 && posdate2 != posdate) { 0413 m_csvMapping[posdate2] = QLatin1String(""); 0414 } 0415 } 0416 } 0417 IFOKDO(err, m_importer->getDocument()->sendMessage(i18nc("An information message", "Import of '%1' with code '%2' and date format '%3'", m_importer->getFileName().toDisplayString(), m_importer->getCodec(), dateFormat))) 0418 0419 // Step 2 done 0420 IFOKDO(err, m_importer->getDocument()->stepForward(2)) 0421 0422 // Treat all lines 0423 IFOK(err) { 0424 int nb2 = lines.size(); 0425 err = m_importer->getDocument()->beginTransaction("#INTERNAL#" % i18nc("Import step", "Import transactions"), nb2); 0426 0427 // Save last mapping used in a settings 0428 QString mappingDesc; 0429 int nbMap = m_csvMapping.count(); 0430 for (int i = 0; i < nbMap; ++i) { 0431 if (i != 0) { 0432 mappingDesc += '|'; 0433 } 0434 mappingDesc += m_csvMapping.at(i); 0435 } 0436 IFOKDO(err, m_importer->getDocument()->setParameter(QStringLiteral("SKG_LAST_CSV_MAPPING_USED"), mappingDesc)) 0437 0438 SKGUnitObject defUnit; 0439 SKGAccountObject defAccount; 0440 QMap<int, SKGOperationObject> mapGroup; 0441 QMap<int, SKGOperationObject> mapOperation; 0442 bool noUnitColumn = (m_csvMapping.indexOf(QStringLiteral("unit")) == -1); 0443 bool noAccountColumn = (m_csvMapping.indexOf(QStringLiteral("account")) == -1); 0444 bool emptyAccount = false; 0445 0446 for (int i = 0; !err && i < nb2; ++i) { 0447 QString currentCategory; 0448 SKGOperationObject currentOperation(m_importer->getDocument()); 0449 SKGSubOperationObject currentSubOperation(m_importer->getDocument()); 0450 0451 // Valuate mandatory attribute with default value 0452 if (noUnitColumn && i == 0) { 0453 err = m_importer->getDefaultUnit(defUnit); 0454 } 0455 IFOKDO(err, currentOperation.setUnit(defUnit)) 0456 0457 if (!err && noAccountColumn) { 0458 err = m_importer->getDefaultAccount(defAccount); 0459 if (i == 0) { 0460 emptyAccount = (defAccount.getNbOperation() == 0); 0461 } 0462 IFOKDO(err, currentOperation.setParentAccount(defAccount)) 0463 } 0464 0465 const QString& line = lines.at(i); 0466 QString skg_op_original_amount; 0467 QStringList atts = SKGServices::splitCSVLine(line, getCSVSeparator(line)); 0468 int nbcol = m_csvMapping.count(); 0469 if (atts.count() < nbcol) { 0470 if (i == nb2 - 1) { 0471 // This is a footer (see 408284) 0472 break; 0473 } 0474 err = SKGError(ERR_INVALIDARG, i18nc("Error message", "Invalid number of columns in line %1. Expected %2. Found %3.", 0475 headerIndex + i + 1, nbcol, atts.count())); 0476 } 0477 QString linecleaned; 0478 for (int z = 0; !err && z < nbcol; ++z) { 0479 if (!m_csvMapping[z].isEmpty()) { 0480 if (!linecleaned.isEmpty()) { 0481 linecleaned += getCSVSeparator(); 0482 } 0483 linecleaned += atts[z]; 0484 } 0485 } 0486 0487 QByteArray hash = QCryptographicHash::hash(linecleaned.toUtf8(), QCryptographicHash::Md5); 0488 0489 int initialBalance = 0; 0490 int idgroup = 0; 0491 int idtransaction = 0; 0492 QStringList propertiesAtt; 0493 QStringList propertiesVal; 0494 double sign = (modeAutoCreditDebit ? -1.0 : 1.0); 0495 bool amountSet = false; 0496 for (int c = 0; !err && c < nbcol; ++c) { 0497 QString col = m_csvMapping[c]; 0498 if (!col.isEmpty()) { 0499 QString val; 0500 if (c >= 0 && c < atts.count()) { 0501 val = atts.at(c).trimmed(); 0502 } 0503 if (col == QStringLiteral("date") || col == QStringLiteral("date1") || col == QStringLiteral("date2")) { 0504 QDate d = SKGServices::stringToTime(SKGServices::dateToSqlString(val.remove(QStringLiteral(" 00:00:00")), dateFormat)).date(); 0505 err = currentOperation.setDate(d); 0506 IFOKDO(err, currentSubOperation.setDate(d)) 0507 if (val == QStringLiteral("0000-00-00")) { 0508 initialBalance = 1; 0509 } 0510 } else if (col == QStringLiteral("number")) { 0511 if (!val.isEmpty()) { 0512 err = currentOperation.setNumber(val); 0513 } 0514 } else if (col == QStringLiteral("mode")) { 0515 if (val == QStringLiteral("1")) { 0516 val = i18nc("An transaction mode", "Transfer"); 0517 } else if (val == QStringLiteral("2")) { 0518 val = i18nc("An transaction mode", "Direct debit"); 0519 } else if (val == QStringLiteral("3")) { 0520 val = i18nc("An transaction mode", "Check"); 0521 } else if (val == QStringLiteral("4")) { 0522 val = i18nc("An transaction mode", "Deposit"); 0523 } else if (val == QStringLiteral("5")) { 0524 val = i18nc("An transaction mode", "Payback"); 0525 } else if (val == QStringLiteral("6")) { 0526 val = i18nc("An transaction mode", "Withdrawal"); 0527 } else if (val == QStringLiteral("7")) { 0528 val = i18nc("An transaction mode", "Card"); 0529 } else if (val == QStringLiteral("8")) { 0530 val = i18nc("An transaction mode", "Loan payment"); 0531 } else if (val == QStringLiteral("9")) { 0532 val = i18nc("An transaction mode", "Subscription"); 0533 } else if (val == QStringLiteral("0")) { 0534 val = QString(); 0535 } else if (val == QStringLiteral("10")) { 0536 val = i18nc("An transaction mode", "Cash deposit"); 0537 } else if (val == QStringLiteral("11")) { 0538 val = i18nc("An transaction mode", "Card summary"); 0539 } else if (val == QStringLiteral("12")) { 0540 val = i18nc("An transaction mode", "Deferred card"); 0541 } 0542 err = currentOperation.setMode(val); 0543 } else if (col == QStringLiteral("payee")) { 0544 SKGPayeeObject payeeObj; 0545 err = SKGPayeeObject::createPayee(m_importer->getDocument(), val, payeeObj); 0546 IFOKDO(err, currentOperation.setPayee(payeeObj)) 0547 } else if (col == QStringLiteral("comment")) { 0548 QString comment = currentOperation.getComment(); 0549 if (!comment.isEmpty()) { 0550 comment += ' '; 0551 } 0552 comment += val; 0553 err = currentOperation.setComment(comment); 0554 IFOKDO(err, currentSubOperation.setComment(comment)) 0555 } else if (col == QStringLiteral("status")) { 0556 err = currentOperation.setStatus(val == QStringLiteral("C") || val == QStringLiteral("Y") ? SKGOperationObject::CHECKED : val == QStringLiteral("P") ? SKGOperationObject::MARKED : SKGOperationObject::NONE); 0557 } else if (col == QStringLiteral("bookmarked")) { 0558 err = currentOperation.bookmark(val == QStringLiteral("Y")); 0559 } else if (col == QStringLiteral("idgroup")) { 0560 idgroup = SKGServices::stringToInt(val); 0561 } else if (col == QStringLiteral("idtransaction")) { 0562 idtransaction = SKGServices::stringToInt(val); 0563 } else if (col == QStringLiteral("amount")) { 0564 if (!val.isEmpty() && SKGServices::stringToDouble(val) != 0.0 && !amountSet) { 0565 amountSet = true; 0566 if (m_csvMapping.contains(QStringLiteral("quantity"))) { 0567 // 209705 vvvv 0568 skg_op_original_amount = val; 0569 // 209705 ^^^^ 0570 } else { 0571 err = currentSubOperation.setQuantity(sign * SKGServices::stringToDouble(val)); 0572 } 0573 } else if (modeAutoCreditDebit) { 0574 sign = 1.0; // Next one will be considered as a credit 0575 } 0576 } else if (col == QStringLiteral("quantity")) { 0577 err = currentSubOperation.setQuantity(SKGServices::stringToDouble(val)); 0578 } else if (col == QStringLiteral("sign")) { 0579 if (QRegularExpression(m_importParameters.value(QStringLiteral("mapping_debit")), QRegularExpression::CaseInsensitiveOption).match(val).hasMatch()) { 0580 sign = -1; 0581 double cval = currentSubOperation.getQuantity(); 0582 if (cval > 0) { 0583 err = currentSubOperation.setQuantity(-cval); 0584 } 0585 } 0586 } else if (col == QStringLiteral("unit")) { 0587 // Looking for unit 0588 SKGUnitObject unit(m_importer->getDocument()); 0589 if (val != defUnit.getName()) { // For performance 0590 err = unit.setName(val); 0591 IFOKDO(err, unit.setSymbol(val)) 0592 if (!err && unit.load().isFailed()) { 0593 err = unit.save(false); // Save only 0594 } 0595 0596 // This unit is now the default one, it is better for performance 0597 defUnit = unit; 0598 } else { 0599 unit = defUnit; 0600 } 0601 0602 SKGUnitValueObject unitval; 0603 IFOKDO(err, unit.addUnitValue(unitval)) 0604 IFOK(err) { 0605 int posAmount = m_csvMapping.indexOf(QStringLiteral("amount")); 0606 int posQuantity = m_csvMapping.indexOf(QStringLiteral("quantity")); 0607 if (posAmount != -1 && posQuantity != -1) { 0608 err = unitval.setQuantity(SKGServices::stringToDouble(atts.at(posAmount)) / SKGServices::stringToDouble(atts.at(posQuantity))); 0609 } else { 0610 err = unitval.setQuantity(1); 0611 } 0612 } 0613 IFOKDO(err, unitval.setDate(now.date())) 0614 IFOKDO(err, unitval.save()) 0615 IFOKDO(err, currentOperation.setUnit(unit)) 0616 } else if (col == QStringLiteral("account")) { 0617 // Looking for account 0618 if (val.isEmpty()) { 0619 err = m_importer->getDefaultAccount(defAccount); 0620 if (i == 0) { 0621 emptyAccount = (defAccount.getNbOperation() == 0); 0622 } 0623 } else if (val != defAccount.getName()) { // For performance 0624 SKGAccountObject account(m_importer->getDocument()); 0625 account.setName(val); 0626 err = account.load(); 0627 IFKO(err) { 0628 // Not found, we have to create one 0629 SKGBankObject bank(m_importer->getDocument()); 0630 QString name = i18nc("Noun", "Bank for import %1", postFix); 0631 err = bank.setName(name); 0632 if (!err && bank.load().isFailed()) { 0633 err = bank.save(false); // Save only 0634 IFOKDO(err, m_importer->getDocument()->sendMessage(i18nc("An information message", "Default bank '%1' created for import", name))) 0635 } 0636 IFOKDO(err, bank.addAccount(account)) 0637 IFOKDO(err, account.setName(val)) 0638 if (!err && account.load().isFailed()) { 0639 err = account.save(false); // Save only 0640 } 0641 } 0642 0643 // This account is now the default one, it is better for performance 0644 defAccount = account; 0645 } 0646 IFOKDO(err, currentOperation.setParentAccount(defAccount)) 0647 } else if (col == QStringLiteral("category")) { 0648 // Set Category 0649 if (!val.isEmpty()) { 0650 // Prepare val 0651 val.replace('/', OBJECTSEPARATOR); 0652 val.replace(':', OBJECTSEPARATOR); 0653 val.replace(',', OBJECTSEPARATOR); 0654 val.replace(';', OBJECTSEPARATOR); 0655 // Get previous category 0656 if (!currentCategory.isEmpty()) { 0657 val = currentCategory % OBJECTSEPARATOR % val; 0658 } 0659 currentCategory = val; 0660 0661 // Create and set category 0662 SKGCategoryObject Category; 0663 err = SKGCategoryObject::createPathCategory(m_importer->getDocument(), val, Category); 0664 IFOKDO(err, currentSubOperation.setCategory(Category)) 0665 0666 IFOK(err) { 0667 if (m_csvMapping.indexOf(QStringLiteral("payee")) == -1) { 0668 SKGPayeeObject payeeObj; 0669 QString p = m_importer->getDocument()->getCategoryForPayee(val, false); 0670 err = SKGPayeeObject::createPayee(m_importer->getDocument(), p, payeeObj); 0671 IFOKDO(err, currentOperation.setPayee(payeeObj)) 0672 } 0673 } 0674 } 0675 } else { 0676 // A property 0677 propertiesAtt.push_back(col); 0678 propertiesVal.push_back(val); 0679 } 0680 } 0681 } 0682 0683 if (!err && (initialBalance != 0)) { 0684 // Specific values for initial balance 0685 err = currentOperation.setStatus(SKGOperationObject::CHECKED); 0686 IFOKDO(err, currentOperation.setAttribute(QStringLiteral("d_date"), QStringLiteral("0000-00-00"))) 0687 IFOKDO(err, currentSubOperation.setAttribute(QStringLiteral("d_date"), QStringLiteral("0000-00-00"))) 0688 } 0689 IFOKDO(err, currentOperation.setImportID(hash.toHex())) 0690 IFOK(err) { 0691 if (idtransaction != 0) { 0692 if (mapOperation.contains(idtransaction)) { 0693 currentOperation = mapOperation[idtransaction]; 0694 skg_op_original_amount = QString(); 0695 } else { 0696 err = currentOperation.save(); 0697 mapOperation[idtransaction] = currentOperation; 0698 } 0699 } else { 0700 err = currentOperation.save(false); // Save only 0701 } 0702 } 0703 if (!err && idgroup != 0) { 0704 if (mapGroup.contains(idgroup) && currentOperation != mapGroup[idgroup]) { 0705 err = currentOperation.setGroupOperation(mapGroup[idgroup]); 0706 IFOKDO(err, currentOperation.save()) 0707 } 0708 mapGroup[idgroup] = currentOperation; 0709 } 0710 0711 IFOKDO(err, currentSubOperation.setParentOperation(currentOperation)) 0712 IFOKDO(err, currentSubOperation.save(false, false)) 0713 0714 // 209705 vvvv 0715 if (!err && !skg_op_original_amount.isEmpty()) { 0716 err = currentOperation.setProperty(QStringLiteral("SKG_OP_ORIGINAL_AMOUNT"), skg_op_original_amount); 0717 } 0718 // 209705 ^^^^ 0719 0720 // Add properties 0721 int nbp = propertiesAtt.count(); 0722 for (int p = 0; !err && p < nbp; ++p) { 0723 err = currentOperation.setProperty(propertiesAtt.at(p), propertiesVal.at(p)); 0724 } 0725 0726 if (!err && i % 500 == 0) { 0727 err = m_importer->getDocument()->executeSqliteOrder(QStringLiteral("ANALYZE")); 0728 } 0729 IFOKDO(err, m_importer->getDocument()->stepForward(i + 1)) 0730 } 0731 0732 0733 IFOK(err) { 0734 QString balance = m_importParameters.value(QStringLiteral("balance")); 0735 if (!balance.isEmpty()) { 0736 if (emptyAccount) { 0737 // Current amount 0738 double currentAmount = defAccount.getAmount(QDate::currentDate()); 0739 0740 // Update account 0741 IFOKDO(err, defAccount.setInitialBalance(SKGServices::stringToDouble(balance) - currentAmount, defUnit)) 0742 IFOKDO(err, defAccount.save()) 0743 IFOKDO(err, m_importer->getDocument()->sendMessage(i18nc("An information message", "The initial balance of '%1' has been set", defAccount.getName()), SKGDocument::Warning)) 0744 0745 } else { 0746 m_importer->getDocument()->sendMessage(i18nc("An information message", "The initial balance of '%1' has not been set because some transactions are already existing", defAccount.getName())); 0747 } 0748 } 0749 } 0750 0751 SKGENDTRANSACTION(m_importer->getDocument(), err) 0752 0753 // Lines treated 0754 IFOKDO(err, m_importer->getDocument()->stepForward(3)) 0755 } 0756 } 0757 } 0758 } 0759 SKGENDTRANSACTION(m_importer->getDocument(), err) 0760 0761 return err; 0762 } 0763 0764 0765 bool SKGImportPluginCsv::isExportPossible() 0766 { 0767 SKGTRACEINFUNC(10) 0768 return (m_importer->getDocument() == nullptr ? true : m_importer->getFileNameExtension() == QStringLiteral("CSV")); 0769 } 0770 0771 SKGError SKGImportPluginCsv::exportFile() 0772 { 0773 if (m_importer->getDocument() == nullptr) { 0774 return SKGError(ERR_ABORT, i18nc("Error message", "Invalid parameters")); 0775 } 0776 SKGError err; 0777 SKGTRACEINFUNCRC(2, err) 0778 0779 auto listUUIDs = SKGServices::splitCSVLine(m_exportParameters.value(QStringLiteral("uuid_of_selected_accounts_or_operations"))); 0780 0781 QString wc; 0782 for (const auto& uuid : qAsConst(listUUIDs)) { 0783 auto items = SKGServices::splitCSVLine(uuid, '-'); 0784 if (items.at(1) == QStringLiteral("operation")) { 0785 if (!wc.isEmpty()) { 0786 wc += QLatin1String(" OR "); 0787 } 0788 wc += " i_OPID=" + items.at(0); 0789 } else if (items.at(1) == QStringLiteral("account")) { 0790 if (!wc.isEmpty()) { 0791 wc += QLatin1String(" OR "); 0792 } 0793 wc += " rd_account_id=" + items.at(0); 0794 } 0795 } 0796 if (wc.isEmpty()) { 0797 wc = QStringLiteral("1=1"); 0798 } else { 0799 IFOKDO(err, m_importer->getDocument()->sendMessage(i18nc("An information message", "Only selected accounts and transactions have been exported"))) 0800 } 0801 0802 // Open file 0803 QSaveFile file(m_importer->getLocalFileName(false)); 0804 if (!file.open(QIODevice::WriteOnly)) { 0805 err.setReturnCode(ERR_INVALIDARG).setMessage(i18nc("Error message", "Save file '%1' failed", m_importer->getFileName().toDisplayString())); 0806 } else { 0807 QTextStream out(&file); 0808 if (!m_importer->getCodec().isEmpty()) { 0809 out.setCodec(m_importer->getCodec().toLatin1().constData()); 0810 } 0811 err = m_importer->getDocument()->dumpSelectSqliteOrder( 0812 QStringLiteral("SELECT v.d_date as date, v.t_BANK as bank, v.t_ACCOUNT as account, v.t_number as number, v.t_mode as mode, " 0813 "v.t_PAYEE as payee, v.t_REALCOMMENT as comment, PRINTF('%.'||u.i_nbdecimal||'f', v.f_REALQUANTITY) as quantity, " 0814 "v.t_UNIT as unit, PRINTF('%.'||u.i_nbdecimal||'f', v.f_REALCURRENTAMOUNT) as amount, v.t_TYPEEXPENSE as sign, v.t_REALCATEGORY as category, v.t_status as status, " 0815 "v.t_REALREFUND as tracker, v.t_bookmarked as bookmarked, v.i_SUBOPID id, v.i_OPID idtransaction, v.i_group_id idgroup " 0816 "FROM v_suboperation_consolidated as v, unit as u WHERE v.rc_unit_id=u.id AND (") % wc % ") ORDER BY v.d_date, v.i_OPID, v.i_SUBOPID", &out, SKGServices::DUMP_CSV); 0817 0818 // Close file 0819 file.commit(); 0820 } 0821 0822 return err; 0823 } 0824 0825 QString SKGImportPluginCsv::getMimeTypeFilter() const 0826 { 0827 return "*.csv|" % i18nc("A file format", "CSV file"); 0828 } 0829 0830 SKGError SKGImportPluginCsv::importCSVUnit() 0831 { 0832 SKGError err; 0833 SKGTRACEINFUNCRC(2, err) 0834 0835 if (m_importer->getDocument() != nullptr) { 0836 // Begin transaction 0837 err = m_importer->getDocument()->beginTransaction("#INTERNAL#" % i18nc("Import step", "Import units"), 3); 0838 IFOK(err) { 0839 // File name is the name of the unit 0840 QFileInfo fInfo(m_importer->getFileName().path()); 0841 QString unitName = fInfo.baseName(); 0842 0843 // Default mapping 0844 if (m_csvMapping.isEmpty()) { 0845 err = setCSVMapping(nullptr); 0846 IFOKDO(err, m_importer->getDocument()->sendMessage(i18nc("An information message", "Use automatic search of the columns"))) 0847 } 0848 0849 // Step 1 done 0850 IFOKDO(err, m_importer->getDocument()->stepForward(1)) 0851 0852 // Open file 0853 IFOK(err) { 0854 QFile file(m_importer->getLocalFileName()); 0855 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { 0856 err.setReturnCode(ERR_INVALIDARG).setMessage(i18nc("Error message", "Open file '%1' failed", m_importer->getFileName().toDisplayString())); 0857 } else { 0858 QTextStream stream(&file); 0859 if (!m_importer->getCodec().isEmpty()) { 0860 stream.setCodec(m_importer->getCodec().toLatin1().constData()); 0861 } 0862 0863 // Ignore useless lines 0864 int headerIndex = getCSVHeaderIndex(); 0865 for (int i = 1; i <= headerIndex; ++i) { 0866 stream.readLine(); 0867 } 0868 0869 // Get data column 0870 QStringList dates; 0871 QStringList lines; 0872 int posdate = m_csvMapping.indexOf(QStringLiteral("date")); 0873 if (posdate != -1) { 0874 while (!stream.atEnd()) { 0875 // Read line 0876 QString line = stream.readLine().trimmed(); 0877 if (!line.isEmpty()) { 0878 lines.push_back(line); 0879 0880 // Get date 0881 QStringList field = SKGServices::splitCSVLine(line, getCSVSeparator(line)); 0882 if (posdate < field.count()) { 0883 dates.push_back(field.at(posdate)); 0884 } 0885 } 0886 } 0887 } 0888 0889 // close file 0890 file.close(); 0891 0892 // Select dateformat 0893 QString dateFormat = SKGServices::getDateFormat(dates); 0894 if (dateFormat.isEmpty()) { 0895 err.setReturnCode(ERR_FAIL).setMessage(i18nc("Error message", "Date format not supported")); 0896 } 0897 IFOKDO(err, m_importer->getDocument()->sendMessage(i18nc("An information message", "Import of '%1' with codec '%2' and date format '%3'", m_importer->getFileName().toDisplayString(), m_importer->getCodec(), dateFormat))) 0898 0899 // Step 2 done 0900 IFOKDO(err, m_importer->getDocument()->stepForward(2)) 0901 0902 // Treat all lines 0903 IFOK(err) { 0904 int nb = lines.size(); 0905 err = m_importer->getDocument()->beginTransaction("#INTERNAL#" % i18nc("Import step", "Import units"), nb); 0906 0907 // Save last mapping used in a settings 0908 QString mappingDesc; 0909 int nbMap = m_csvMapping.count(); 0910 for (int i = 0; i < nbMap; ++i) { 0911 if (i != 0) { 0912 mappingDesc += '|'; 0913 } 0914 mappingDesc += m_csvMapping.at(i); 0915 } 0916 IFOKDO(err, m_importer->getDocument()->setParameter(QStringLiteral("SKG_LAST_CSV_UNIT_MAPPING_USED"), mappingDesc)) 0917 0918 int posdate2 = m_csvMapping.indexOf(QStringLiteral("date")); 0919 int posvalue = m_csvMapping.indexOf(QStringLiteral("amount")); 0920 if (posdate2 != -1 && posvalue != -1) { 0921 for (int i = 0; !err && i < nb; ++i) { 0922 QStringList atts = SKGServices::splitCSVLine(lines.at(i), getCSVSeparator(lines.at(i))); 0923 err = m_importer->getDocument()->addOrModifyUnitValue(unitName, 0924 SKGServices::stringToTime(SKGServices::dateToSqlString(atts.at(posdate2), dateFormat)).date(), 0925 SKGServices::stringToDouble(atts.at(posvalue))); 0926 0927 IFOKDO(err, m_importer->getDocument()->stepForward(i + 1)) 0928 } 0929 } 0930 0931 SKGENDTRANSACTION(m_importer->getDocument(), err) 0932 0933 // Lines treated 0934 IFOKDO(err, m_importer->getDocument()->stepForward(3)) 0935 } 0936 } 0937 } 0938 } 0939 SKGENDTRANSACTION(m_importer->getDocument(), err) 0940 } 0941 return err; 0942 } 0943 0944 SKGError SKGImportPluginCsv::importCSVRule() 0945 { 0946 SKGError err; 0947 SKGTRACEINFUNCRC(2, err) 0948 0949 if (m_importer->getDocument() != nullptr) { 0950 // Begin transaction 0951 err = m_importer->getDocument()->beginTransaction("#INTERNAL#" % i18nc("Import step", "Import rules"), 3); 0952 IFOK(err) { 0953 // Default mapping 0954 if (m_csvMapping.isEmpty()) { 0955 err = setCSVMapping(nullptr); 0956 IFOKDO(err, m_importer->getDocument()->sendMessage(i18nc("An information message", "Use automatic search of the columns"))) 0957 } 0958 0959 // Step 1 done 0960 IFOKDO(err, m_importer->getDocument()->stepForward(1)) 0961 0962 // Open file 0963 IFOK(err) { 0964 QFile file(m_importer->getLocalFileName()); 0965 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { 0966 err.setReturnCode(ERR_INVALIDARG).setMessage(i18nc("Error message", "Open file '%1' failed", m_importer->getFileName().toDisplayString())); 0967 } else { 0968 QTextStream stream(&file); 0969 if (!m_importer->getCodec().isEmpty()) { 0970 stream.setCodec(m_importer->getCodec().toLatin1().constData()); 0971 } 0972 0973 // Ignore useless lines 0974 int headerIndex = getCSVHeaderIndex(); 0975 for (int i = 1; i <= headerIndex; ++i) { 0976 stream.readLine(); 0977 } 0978 0979 // Get data column 0980 QStringList lines; 0981 while (!stream.atEnd()) { 0982 // Read line 0983 QString line = stream.readLine().trimmed(); 0984 if (!line.isEmpty()) { 0985 lines.push_back(line); 0986 } 0987 } 0988 0989 // close file 0990 file.close(); 0991 0992 IFOKDO(err, m_importer->getDocument()->sendMessage(i18nc("An information message", "Import of '%1' with codec '%2'", m_importer->getFileName().toDisplayString(), m_importer->getCodec()))) 0993 0994 // Step 2 done 0995 IFOKDO(err, m_importer->getDocument()->stepForward(2)) 0996 0997 // Treat all lines 0998 IFOK(err) { 0999 int nb = lines.size(); 1000 err = m_importer->getDocument()->beginTransaction("#INTERNAL#" % i18nc("Import step", "Import rules"), nb); 1001 1002 // Save last mapping used in a settings 1003 QString mappingDesc; 1004 int nbMap = m_csvMapping.count(); 1005 for (int i = 0; i < nbMap; ++i) { 1006 if (i != 0) { 1007 mappingDesc += '|'; 1008 } 1009 mappingDesc += m_csvMapping.at(i); 1010 } 1011 IFOKDO(err, m_importer->getDocument()->setParameter(QStringLiteral("SKG_LAST_CSV_RULE_MAPPING_USED"), mappingDesc)) 1012 1013 int pospayee = m_csvMapping.indexOf(QStringLiteral("payee")); 1014 int poscategory = m_csvMapping.indexOf(QStringLiteral("category")); 1015 if (pospayee != -1 && poscategory != -1) { 1016 for (int i = 0; !err && i < nb; ++i) { 1017 QStringList atts = SKGServices::splitCSVLine(lines.at(i), getCSVSeparator(lines.at(i))); 1018 1019 SKGRuleObject oRule; 1020 err = SKGRuleObject::createPayeeCategoryRule(m_importer->getDocument(), atts.at(pospayee), atts.at(poscategory), oRule); 1021 1022 IFOKDO(err, m_importer->getDocument()->stepForward(i + 1)) 1023 } 1024 } 1025 1026 SKGENDTRANSACTION(m_importer->getDocument(), err) 1027 1028 // Lines treated 1029 IFOKDO(err, m_importer->getDocument()->stepForward(3)) 1030 } 1031 } 1032 } 1033 } 1034 SKGENDTRANSACTION(m_importer->getDocument(), err) 1035 } 1036 return err; 1037 } 1038 1039 #include <skgimportplugincsv.moc>