File indexing completed on 2025-02-09 06:04:39
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 BACKEND import / export. 0008 * 0009 * @author Stephane MANKOWSKI / Guillaume DE BURE 0010 */ 0011 #include "skgimportpluginbackend.h" 0012 0013 #include <qapplication.h> 0014 #include <qdir.h> 0015 #include <qdiriterator.h> 0016 #include <qfile.h> 0017 #include <qfileinfo.h> 0018 #include <qprocess.h> 0019 #include <qstandardpaths.h> 0020 #include <qtconcurrentmap.h> 0021 #include <qregularexpression.h> 0022 0023 #include <kaboutdata.h> 0024 #include <klocalizedstring.h> 0025 #include <kpluginfactory.h> 0026 #include <qvariant.h> 0027 0028 #include "skgbankincludes.h" 0029 #include "skgimportexportmanager.h" 0030 #include "skgservices.h" 0031 #include "skgtraces.h" 0032 0033 /** 0034 * This plugin factory. 0035 */ 0036 K_PLUGIN_CLASS_WITH_JSON(SKGImportPluginBackend, "metadata.json") 0037 0038 SKGImportPluginBackend::SKGImportPluginBackend(QObject* iImporter, const QVariantList& iArg) 0039 : SKGImportPlugin(iImporter) 0040 { 0041 SKGTRACEINFUNC(10) 0042 Q_UNUSED(iArg) 0043 0044 m_listBackends = KServiceTypeTrader::self()->query(QStringLiteral("Skrooge/Import/Backend")); 0045 SKGTRACEL(10) << m_listBackends.count() << " plugins found" << SKGENDL; 0046 } 0047 0048 SKGImportPluginBackend::~SKGImportPluginBackend() 0049 = default; 0050 0051 QExplicitlySharedDataPointer<KService> SKGImportPluginBackend::getService() const 0052 { 0053 for (const auto& service : m_listBackends) { 0054 #ifdef SKG_KF_5102 0055 if (service->property(QStringLiteral("X-Krunner-ID"), QMetaType::QString).toString().toUpper() == m_importer->getFileNameExtension()) { 0056 return service; 0057 } 0058 #else 0059 if (service->property(QStringLiteral("X-Krunner-ID"), QVariant::String).toString().toUpper() == m_importer->getFileNameExtension()) { 0060 return service; 0061 } 0062 #endif 0063 } 0064 return QExplicitlySharedDataPointer<KService>(nullptr); 0065 } 0066 0067 QString SKGImportPluginBackend::getParameter(const QString& iAttribute) 0068 { 0069 auto service = getService(); 0070 #ifdef SKG_KF_5102 0071 auto output = service->property(iAttribute, QMetaType::QString).toString(); 0072 #else 0073 auto output = service->property(iAttribute, QVariant::String).toString(); 0074 #endif 0075 QMap<QString, QString> parameters = this->getImportParameters(); 0076 0077 for (int i = 1; i <= 10; ++i) { 0078 QString param = "parameter" + SKGServices::intToString(i); 0079 if (output.contains(QStringLiteral("%") % param)) { 0080 output = output.replace(QStringLiteral("%") % param, parameters.value(param)); 0081 } 0082 } 0083 0084 return output; 0085 } 0086 0087 bool SKGImportPluginBackend::isImportPossible() 0088 { 0089 SKGTRACEINFUNC(10) 0090 return (m_importer->getDocument() == nullptr ? true : getService().data() != nullptr); 0091 } 0092 0093 struct download { 0094 download(int iNbToDownload, QString iDate, QString iCmd, QString iPwd, QString iPath) 0095 : m_nbToDownload(iNbToDownload), m_date(std::move(iDate)), m_cmd(std::move(iCmd)), m_pwd(std::move(iPwd)), m_path(std::move(iPath)) 0096 { 0097 } 0098 0099 using result_type = QString; 0100 0101 QString operator()(const QString& iAccountId) 0102 { 0103 QString file = m_path % "/" % iAccountId % ".csv"; 0104 // Build cmd 0105 QString cmd = m_cmd; 0106 cmd = cmd.replace(QStringLiteral("%2"), SKGServices::intToString(m_nbToDownload)).replace(QStringLiteral("%1"), iAccountId).replace(QStringLiteral("%3"), m_pwd).replace(QStringLiteral("%4"), m_date); 0107 0108 // Execute 0109 QProcess p; 0110 cmd = SKGServices::getFullPathCommandLine(cmd); 0111 SKGTRACEL(10) << "Execute: " << cmd << SKGENDL; 0112 p.setStandardOutputFile(file); 0113 0114 int retry = 0; 0115 do { 0116 p.start(QStringLiteral("/bin/bash"), QStringList() << QStringLiteral("-c") << cmd); 0117 if (p.waitForFinished(1000 * 60 * 2)) { 0118 if (p.exitCode() == 0) { 0119 return iAccountId; 0120 } 0121 SKGTRACE << i18nc("A warning message", "WARNING: The command %1 failed with code %2 (Retry %3)", cmd, p.exitCode(), retry + 1) << SKGENDL; 0122 0123 } else { 0124 SKGTRACE << i18nc("A warning message", "WARNING: The command %1 failed due to a time out (Retry %2)", cmd, retry + 1) << SKGENDL; 0125 p.terminate(); 0126 p.kill(); 0127 } 0128 ++retry; 0129 } while (retry < 6); 0130 0131 QString errorMsg = i18nc("Error message", "The following command line failed with code %2:\n'%1'", cmd, p.exitCode()); 0132 SKGTRACE << errorMsg << SKGENDL; 0133 0134 return QStringLiteral("ERROR:") + errorMsg; 0135 } 0136 0137 int m_nbToDownload; 0138 QString m_date; 0139 QString m_cmd; 0140 QString m_pwd; 0141 QString m_path; 0142 }; 0143 0144 SKGError SKGImportPluginBackend::importFile() 0145 { 0146 if (m_importer->getDocument() == nullptr) { 0147 return SKGError(ERR_ABORT, i18nc("Error message", "Invalid parameters")); 0148 } 0149 SKGError err; 0150 SKGTRACEINFUNCRC(2, err) 0151 0152 SKGBEGINPROGRESSTRANSACTION(*m_importer->getDocument(), i18nc("Noun, name of the user action", "Import with %1", "Backend"), err, 3) 0153 QString bankendName = m_importer->getFileNameExtension().toLower(); 0154 0155 // Get parameters 0156 QMap<QString, QString> parameters = this->getImportParameters(); 0157 QString pwd = parameters[QStringLiteral("password")]; 0158 0159 // Get list of accounts 0160 QStringList backendAccounts; 0161 QMap<QString, QString> backendAccountsBalance; 0162 QMap<QString, QString> backendAccountsName; 0163 QString csvfile = m_tempDir.path() % "/skrooge_backend.csv"; 0164 QString cmd = getParameter(QStringLiteral("X-SKROOGE-getaccounts")).replace(QStringLiteral("%3"), pwd); 0165 QProcess p; 0166 cmd = SKGServices::getFullPathCommandLine(cmd); 0167 SKGTRACEL(10) << "Execute: " << cmd << SKGENDL; 0168 p.setStandardOutputFile(csvfile); 0169 p.start(QStringLiteral("/bin/bash"), QStringList() << QStringLiteral("-c") << cmd); 0170 if (!p.waitForFinished(1000 * 60 * 2) || p.exitCode() != 0) { 0171 err.setReturnCode(ERR_FAIL).setMessage(i18nc("Error message", "The following command line failed with code %2:\n'%1'", cmd, p.exitCode())); 0172 } else { 0173 QFile file(csvfile); 0174 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { 0175 err.setReturnCode(ERR_INVALIDARG).setMessage(i18nc("Error message", "Open file '%1' failed", csvfile)); 0176 } else { 0177 QRegularExpression reggetaccounts(getParameter(QStringLiteral("X-SKROOGE-getaccountid"))); 0178 QRegularExpression reggetaccountbalance(getParameter(QStringLiteral("X-SKROOGE-getaccountbalance"))); 0179 QRegularExpression reggetaccountname(getParameter(QStringLiteral("X-SKROOGE-getaccountname"))); 0180 0181 QTextStream stream(&file); 0182 stream.readLine(); // To avoid header 0183 QStringList backendAccountsUniqueId; 0184 while (!stream.atEnd()) { 0185 // Read line 0186 QString line = stream.readLine().trimmed(); 0187 SKGTRACEL(10) << "Read line: " << line << SKGENDL; 0188 0189 // Get account id 0190 auto match = reggetaccounts.match(line); 0191 if (match.hasMatch()) { 0192 QString accountid = match.captured(1); 0193 QString uniqueid = SKGServices::splitCSVLine(accountid, QLatin1Char('@')).at(0); 0194 0195 if (!backendAccounts.contains(accountid) && !backendAccountsUniqueId.contains(uniqueid)) { 0196 backendAccounts.push_back(accountid); 0197 backendAccountsUniqueId.push_back(uniqueid); 0198 0199 // Get account balance 0200 match = reggetaccountbalance.match(line); 0201 if (match.hasMatch()) { 0202 backendAccountsBalance[accountid] = match.captured(1); 0203 } else { 0204 backendAccountsBalance[accountid] = '0'; 0205 } 0206 0207 // Get account name 0208 match = reggetaccountname.match(line); 0209 if (match.hasMatch()) { 0210 backendAccountsName[accountid] = match.captured(1); 0211 } else { 0212 backendAccountsName[accountid] = QLatin1String(""); 0213 } 0214 } 0215 } else { 0216 // This is an error 0217 err.setReturnCode(ERR_FAIL).setMessage(line).addError(ERR_FAIL, i18nc("Error message", "Impossible to find the account id with the regular expression '%1' in line '%2'", getParameter(QStringLiteral("X-SKROOGE-getaccountid")), line)); 0218 break; 0219 } 0220 } 0221 0222 // close file 0223 file.close(); 0224 file.remove(); 0225 } 0226 } 0227 0228 // Download transactions 0229 IFOKDO(err, m_importer->getDocument()->stepForward(1, i18nc("Progress message", "Download transactions"))) 0230 IFOK(err) { 0231 if (backendAccounts.isEmpty()) { 0232 err.setReturnCode(ERR_FAIL).setMessage(i18nc("Error message", "Your backend '%1' seems to be not well configure because no account has been found.", bankendName)); 0233 } else { 0234 // Compute the begin date for download 0235 QDate lastDownload = SKGServices::stringToTime(m_importer->getDocument()->getParameter("SKG_LAST_" % bankendName.toUpper() % "_IMPORT_DATE")).date(); 0236 QString lastList = m_importer->getDocument()->getParameter("SKG_LAST_" % bankendName.toUpper() % "_IMPORT_LIST"); 0237 QString currentList = backendAccounts.join(QStringLiteral(";")); 0238 0239 int nbToDownload = 0; 0240 QString fromDate; 0241 if (currentList != lastList || !lastDownload.isValid()) { 0242 nbToDownload = 99999; 0243 fromDate = QStringLiteral("2000-01-01"); 0244 } else { 0245 nbToDownload = qMax(lastDownload.daysTo(QDate::currentDate()) * 10, qint64(20)); 0246 fromDate = SKGServices::dateToSqlString(lastDownload.addDays(-4)); 0247 } 0248 0249 // Download 0250 QStringList listDownloadedId; 0251 QString bulk = getParameter(QStringLiteral("X-SKROOGE-getbulk")); 0252 QString cmddownload; 0253 if (!bulk.isEmpty()) { 0254 // mode bulk 0255 SKGTRACEL(10) << "Mode getbulk" << SKGENDL; 0256 QProcess pbulk; 0257 QString cmd = bulk.replace(QStringLiteral("%1"), m_tempDir.path()); 0258 cmd = SKGServices::getFullPathCommandLine(cmd); 0259 cmddownload = cmd; 0260 SKGTRACEL(10) << "Execute: " << cmd << SKGENDL; 0261 pbulk.start(QStringLiteral("/bin/bash"), QStringList() << QStringLiteral("-c") << cmd); 0262 if (!pbulk.waitForFinished(1000 * 60 * 2) || pbulk.exitCode() != 0) { 0263 err.setReturnCode(ERR_FAIL).setMessage(i18nc("Error message", "The following command line failed with code %2:\n'%1'", cmd, pbulk.exitCode())); 0264 } else { 0265 SKGTRACEL(10) << "Searching csv files " << SKGENDL; 0266 QDirIterator it(m_tempDir.path(), QStringList() << QStringLiteral("*.csv")); 0267 while (it.hasNext()) { 0268 auto id = QFileInfo(it.next()).baseName(); 0269 listDownloadedId.push_back(id); 0270 0271 SKGTRACEL(10) << "Find id: " << id << SKGENDL; 0272 } 0273 } 0274 } else { 0275 // mode getoperations 0276 SKGTRACEL(10) << "Mode getoperations" << SKGENDL; 0277 cmddownload = getParameter(QStringLiteral("X-SKROOGE-getoperations")); 0278 QFuture<QString> f = QtConcurrent::mapped(backendAccounts, download(nbToDownload, fromDate, cmddownload, pwd, m_tempDir.path())); 0279 f.waitForFinished(); 0280 listDownloadedId = f.results(); 0281 } 0282 listDownloadedId.removeAll(QLatin1String("")); 0283 // Build list of errors 0284 QStringList errors; 0285 int nb = listDownloadedId.count(); 0286 errors.reserve(nb); 0287 for (int i = nb - 1; i >= 0; --i) { 0288 auto item = listDownloadedId.value(i); 0289 if (item.startsWith(QLatin1String("ERROR:"))) { 0290 listDownloadedId.removeAt(i); 0291 errors.push_back(item.right(item.length() - 6)); 0292 } 0293 } 0294 0295 // Check 0296 IFOK(err) { 0297 bool checkOK = true; 0298 int nb = listDownloadedId.count(); 0299 if (errors.count() != 0) { 0300 // Some accounts have not been downloaded 0301 if (nb == 0) { 0302 err = SKGError(ERR_FAIL, i18nc("Error message", "No accounts downloaded with the following command:\n%1\nCheck your backend installation.", cmddownload)); 0303 } else { 0304 // Warning 0305 m_importer->getDocument()->sendMessage(i18nc("Warning message", "Some accounts have not been downloaded. %1", errors.join(QStringLiteral(". "))), SKGDocument::Warning); 0306 } 0307 SKGTRACEL(10) << errors.count() << " accounts not imported => checkOK=false" << SKGENDL; 0308 checkOK = false; 0309 } 0310 0311 // import 0312 IFOKDO(err, m_importer->getDocument()->stepForward(2, i18nc("Progress message", "Import"))) 0313 if (!err && (nb != 0)) { 0314 // import 0315 SKGBEGINPROGRESSTRANSACTION(*m_importer->getDocument(), "#INTERNAL#" % i18nc("Noun, name of the user action", "Import one account with %1", "Backend"), err, nb) 0316 0317 // Get all messages 0318 SKGDocument::SKGMessageList messages; 0319 IFOKDO(err, m_importer->getDocument()->getMessages(m_importer->getDocument()->getCurrentTransaction(), messages, true)) 0320 0321 auto extension = getParameter(QStringLiteral("X-SKROOGE-extension")).toLower(); 0322 if (extension.length() == 0) { 0323 extension = "csv"; 0324 } 0325 0326 // Import all files 0327 for (int i = 0; !err && i < nb; ++i) { 0328 // Rename the imported name 0329 QString file = m_tempDir.path() % "/" % listDownloadedId.at(i) % ".csv"; 0330 if (!listDownloadedId.at(i).contains(QStringLiteral("-")) && !backendAccountsName[listDownloadedId.at(i)].isEmpty()) { 0331 QString newFileName = m_tempDir.path() % "/" % backendAccountsName[listDownloadedId.at(i)] % '-' % listDownloadedId.at(i) % "." % extension; 0332 if (QFile::rename(file, newFileName)) { 0333 file = newFileName; 0334 } 0335 } 0336 0337 // Import 0338 SKGImportExportManager imp1(m_importer->getDocument(), QUrl::fromLocalFile(file)); 0339 imp1.setAutomaticValidation(m_importer->automaticValidation()); 0340 imp1.setAutomaticApplyRules(m_importer->automaticApplyRules()); 0341 // This option is not used with backend import 0342 imp1.setSinceLastImportDate(false); 0343 imp1.setCodec(m_importer->getCodec()); 0344 0345 if (extension == "csv") { 0346 QMap<QString, QString> newParameters = imp1.getImportParameters(); 0347 newParameters[QStringLiteral("automatic_search_header")] = 'N'; 0348 newParameters[QStringLiteral("header_position")] = '1'; 0349 newParameters[QStringLiteral("automatic_search_columns")] = 'N'; 0350 newParameters[QStringLiteral("columns_positions")] = getParameter(QStringLiteral("X-SKROOGE-csvcolumns")); 0351 newParameters[QStringLiteral("mode_csv_unit")] = 'N'; 0352 newParameters[QStringLiteral("mode_csv_rule")] = 'N'; 0353 newParameters[QStringLiteral("balance")] = backendAccountsBalance[listDownloadedId.at(i)]; 0354 newParameters[QStringLiteral("donotfinalize")] = 'Y'; 0355 imp1.setImportParameters(newParameters); 0356 } 0357 IFOKDO(err, imp1.importFile()) 0358 0359 if (!backendAccountsBalance[listDownloadedId.at(i)].isEmpty()) { 0360 SKGAccountObject act; 0361 IFOKDO(err, imp1.getDefaultAccount(act)) 0362 m_importer->addAccountToCheck(act, SKGServices::stringToDouble(backendAccountsBalance[listDownloadedId.at(i)])); 0363 } 0364 IFOKDO(err, m_importer->getDocument()->stepForward(i + 1)) 0365 } 0366 0367 // Remove all temporary files 0368 for (int i = 0; i < nb; ++i) { 0369 QString file = m_tempDir.path() % "/" % listDownloadedId.at(i) % ".csv"; 0370 QFile::remove(file); 0371 } 0372 0373 // Reset message 0374 IFOKDO(err, m_importer->getDocument()->removeMessages(m_importer->getDocument()->getCurrentTransaction())) 0375 int nbm = messages.count(); 0376 for (int j = 0; j < nbm; ++j) { 0377 SKGDocument::SKGMessage msg = messages.at(j); 0378 m_importer->getDocument()->sendMessage(msg.Text, msg.Type, msg.Action); 0379 } 0380 0381 // Finalize import 0382 IFOKDO(err, m_importer->finalizeImportation()) 0383 0384 // Disable std finalisation 0385 QMap<QString, QString> parameters = m_importer->getImportParameters(); 0386 parameters[QStringLiteral("donotfinalize")] = 'Y'; 0387 m_importer->setImportParameters(parameters); 0388 0389 // Check balances of accounts 0390 auto accountsToCheck = m_importer->getAccountsToCheck(); 0391 int nb = accountsToCheck.count(); 0392 for (int i = 0; !err && i < nb; ++i) { 0393 // Get the account to check 0394 auto act = accountsToCheck[i].first; 0395 auto targetBalance = accountsToCheck[i].second; 0396 auto soluces = act.getPossibleReconciliations(targetBalance, false); 0397 if (soluces.isEmpty()) { 0398 SKGTRACEL(10) << "Account " << listDownloadedId.at(i) << " not reconciliable => checkOK=false" << SKGENDL; 0399 checkOK = false; 0400 } 0401 } 0402 0403 if (checkOK) { 0404 // Last import is memorized only in case of 100% success 0405 IFOKDO(err, m_importer->getDocument()->setParameter("SKG_LAST_" % bankendName.toUpper() % "_IMPORT_DATE", SKGServices::dateToSqlString(QDateTime::currentDateTime()))) 0406 IFOKDO(err, m_importer->getDocument()->setParameter("SKG_LAST_" % bankendName.toUpper() % "_IMPORT_LIST", currentList)) 0407 } else { 0408 // Remove last import for next import 0409 IFOKDO(err, m_importer->getDocument()->setParameter("SKG_LAST_" % bankendName.toUpper() % "_IMPORT_DATE", QLatin1String(""))) 0410 IFOKDO(err, m_importer->getDocument()->setParameter("SKG_LAST_" % bankendName.toUpper() % "_IMPORT_LIST", QLatin1String(""))) 0411 } 0412 } 0413 IFOKDO(err, m_importer->getDocument()->stepForward(3)) 0414 } 0415 } 0416 } 0417 0418 return err; 0419 } 0420 0421 QString SKGImportPluginBackend::getMimeTypeFilter() const 0422 { 0423 return QLatin1String(""); 0424 } 0425 0426 #include <skgimportpluginbackend.moc>