diff --git a/kmymoney/plugins/checkprinting/checkprinting.cpp b/kmymoney/plugins/checkprinting/checkprinting.cpp index 84fcb5987..a529c121b 100644 --- a/kmymoney/plugins/checkprinting/checkprinting.cpp +++ b/kmymoney/plugins/checkprinting/checkprinting.cpp @@ -1,247 +1,262 @@ /*************************************************************************** * Copyright 2009 Cristian Onet onet.cristian@gmail.com * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see * ***************************************************************************/ -#include "checkprinting.h" - #include +#include "checkprinting.h" // QT includes #include #include #include #ifdef ENABLE_WEBENGINE #include #else #include #endif #include #include // KDE includes #include #include #include #include // KMyMoney includes #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneyinstitution.h" #include "mymoneymoney.h" #include "mymoneypayee.h" #include "mymoneysecurity.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneyutils.h" #include "viewinterface.h" #include "selectedtransactions.h" #include "numbertowords.h" #include "pluginsettings.h" #include "mymoneyenums.h" +#ifdef IS_APPIMAGE +#include +#include +#endif + struct CheckPrinting::Private { QAction* m_action; QString m_checkTemplateHTML; QStringList m_printedTransactionIdList; KMyMoneyRegister::SelectedTransactions m_transactions; }; CheckPrinting::CheckPrinting(QObject *parent, const QVariantList &args) : KMyMoneyPlugin::Plugin(parent, "checkprinting"/*must be the same as X-KDE-PluginInfo-Name*/), m_currentPrinter(nullptr) { Q_UNUSED(args); + const auto componentName = QLatin1String("checkprinting"); + const auto rcFileName = QLatin1String("checkprinting.rc"); // Tell the host application to load my GUI component - setComponentName("checkprinting", i18nc("It's about printing bank checks", "Check printing")); - setXMLFile("checkprinting.rc"); + setComponentName(componentName, i18nc("It's about printing bank checks", "Check printing")); + +#ifdef IS_APPIMAGE + const QString rcFilePath = QCoreApplication::applicationDirPath() + QLatin1String("/../share/kxmlgui5/") + componentName + QLatin1Char('/') + rcFileName; + setXMLFile(rcFilePath); + + const QString localRcFilePath = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation).first() + QLatin1Char('/') + componentName + QLatin1Char('/') + rcFileName; + setLocalXMLFile(localRcFilePath); +#else + setXMLFile(rcFileName); +#endif // For ease announce that we have been loaded. qDebug("Plugins: checkprinting loaded"); d = std::unique_ptr(new Private); // Create the actions of this plugin QString actionName = i18n("Print check"); d->m_action = actionCollection()->addAction("transaction_checkprinting", this, SLOT(slotPrintCheck())); d->m_action->setText(actionName); // wait until a transaction is selected before enabling the action d->m_action->setEnabled(false); d->m_printedTransactionIdList = PluginSettings::printedChecks(); readCheckTemplate(); //! @todo Christian: Replace #if 0 connect(KMyMoneyPlugin::PluginLoader::instance(), SIGNAL(configChanged(Plugin*)), this, SLOT(slotUpdateConfig())); #endif } /** * @internal Destructor is needed because destructor call of unique_ptr must be in this compile unit */ CheckPrinting::~CheckPrinting() { qDebug("Plugins: checkprinting unloaded"); } void CheckPrinting::plug() { connect(viewInterface(), &KMyMoneyPlugin::ViewInterface::transactionsSelected, this, &CheckPrinting::slotTransactionsSelected); } void CheckPrinting::unplug() { disconnect(viewInterface(), &KMyMoneyPlugin::ViewInterface::transactionsSelected, this, &CheckPrinting::slotTransactionsSelected); } void CheckPrinting::readCheckTemplate() { QString checkTemplateHTMLPath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, "checkprinting/check_template.html"); if (PluginSettings::checkTemplateFile().isEmpty()) { PluginSettings::setCheckTemplateFile(checkTemplateHTMLPath); PluginSettings::self()->save(); } QFile checkTemplateHTMLFile(PluginSettings::checkTemplateFile()); checkTemplateHTMLFile.open(QIODevice::ReadOnly); QTextStream stream(&checkTemplateHTMLFile); d->m_checkTemplateHTML = stream.readAll(); checkTemplateHTMLFile.close(); } bool CheckPrinting::canBePrinted(const KMyMoneyRegister::SelectedTransaction & selectedTransaction) const { MyMoneyFile* file = MyMoneyFile::instance(); bool isACheck = file->account(selectedTransaction.split().accountId()).accountType() == eMyMoney::Account::Type::Checkings && selectedTransaction.split().shares().isNegative(); return isACheck && d->m_printedTransactionIdList.contains(selectedTransaction.transaction().id()) == 0; } void CheckPrinting::markAsPrinted(const KMyMoneyRegister::SelectedTransaction & selectedTransaction) { d->m_printedTransactionIdList.append(selectedTransaction.transaction().id()); } void CheckPrinting::slotPrintCheck() { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyMoneyToWordsConverter converter; #ifdef ENABLE_WEBENGINE auto htmlPart = new QWebEngineView(); #else auto htmlPart = new KWebView(); #endif KMyMoneyRegister::SelectedTransactions::const_iterator it; for (it = d->m_transactions.constBegin(); it != d->m_transactions.constEnd(); ++it) { if (!canBePrinted(*it)) continue; // skip this check since it was already printed QString checkHTML = d->m_checkTemplateHTML; const MyMoneyAccount account = file->account((*it).split().accountId()); const MyMoneySecurity currency = file->currency(account.currencyId()); const MyMoneyInstitution institution = file->institution(file->account((*it).split().accountId()).institutionId()); // replace the predefined tokens // data about the user checkHTML.replace("$OWNER_NAME", file->user().name()); checkHTML.replace("$OWNER_ADDRESS", file->user().address()); checkHTML.replace("$OWNER_CITY", file->user().city()); checkHTML.replace("$OWNER_STATE", file->user().state()); // data about the account institution checkHTML.replace("$INSTITUTION_NAME", institution.name()); checkHTML.replace("$INSTITUTION_STREET", institution.street()); checkHTML.replace("$INSTITUTION_TELEPHONE", institution.telephone()); checkHTML.replace("$INSTITUTION_TOWN", institution.town()); checkHTML.replace("$INSTITUTION_CITY", institution.city()); checkHTML.replace("$INSTITUTION_POSTCODE", institution.postcode()); checkHTML.replace("$INSTITUTION_MANAGER", institution.manager()); // data about the transaction checkHTML.replace("$DATE", QLocale().toString((*it).transaction().postDate(), QLocale::ShortFormat)); checkHTML.replace("$CHECK_NUMBER", (*it).split().number()); checkHTML.replace("$PAYEE_NAME", file->payee((*it).split().payeeId()).name()); checkHTML.replace("$PAYEE_ADDRESS", file->payee((*it).split().payeeId()).address()); checkHTML.replace("$PAYEE_CITY", file->payee((*it).split().payeeId()).city()); checkHTML.replace("$PAYEE_POSTCODE", file->payee((*it).split().payeeId()).postcode()); checkHTML.replace("$PAYEE_STATE", file->payee((*it).split().payeeId()).state()); checkHTML.replace("$AMOUNT_STRING", converter.convert((*it).split().shares().abs(), currency.smallestAccountFraction())); checkHTML.replace("$AMOUNT_DECIMAL", MyMoneyUtils::formatMoney((*it).split().shares().abs(), currency)); checkHTML.replace("$MEMO", (*it).split().memo()); // print the check htmlPart->setHtml(checkHTML, QUrl("file://")); m_currentPrinter = new QPrinter(); QPointer dialog = new QPrintDialog(m_currentPrinter); dialog->setWindowTitle(QString()); if (dialog->exec() != QDialog::Accepted) { delete m_currentPrinter; m_currentPrinter = nullptr; continue; } else { #ifdef ENABLE_WEBENGINE htmlPart->page()->print(m_currentPrinter, [=] (bool) {delete m_currentPrinter; m_currentPrinter = nullptr;}); #else htmlPart->print(m_currentPrinter); #endif } delete dialog; // mark the transaction as printed markAsPrinted(*it); } PluginSettings::setPrintedChecks(d->m_printedTransactionIdList); delete htmlPart; } void CheckPrinting::slotTransactionsSelected(const KMyMoneyRegister::SelectedTransactions& transactions) { d->m_transactions = transactions; bool actionEnabled = false; // enable/disable the action depending if there are transactions selected or not // and whether they can be printed or not KMyMoneyRegister::SelectedTransactions::const_iterator it; for (it = d->m_transactions.constBegin(); it != d->m_transactions.constEnd(); ++it) { if (canBePrinted(*it)) { actionEnabled = true; break; } } d->m_action->setEnabled(actionEnabled); } // the plugin's configurations has changed void CheckPrinting::configurationChanged() { PluginSettings::self()->load(); // re-read the data because the configuration has changed readCheckTemplate(); d->m_printedTransactionIdList = PluginSettings::printedChecks(); } K_PLUGIN_FACTORY_WITH_JSON(CheckPrintingFactory, "checkprinting.json", registerPlugin();) #include "checkprinting.moc" diff --git a/kmymoney/plugins/csv/export/csvexporter.cpp b/kmymoney/plugins/csv/export/csvexporter.cpp index abaff2fba..b798f8ba4 100644 --- a/kmymoney/plugins/csv/export/csvexporter.cpp +++ b/kmymoney/plugins/csv/export/csvexporter.cpp @@ -1,110 +1,128 @@ /* * Copyright 2013-2014 Allan Anderson * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ +#include #include "csvexporter.h" // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "csvexportdlg.h" #include "csvwriter.h" #include "viewinterface.h" +#ifdef IS_APPIMAGE +#include +#include +#endif + CSVExporter::CSVExporter(QObject *parent, const QVariantList &args) : KMyMoneyPlugin::Plugin(parent, "csvexporter"/*must be the same as X-KDE-PluginInfo-Name*/) { Q_UNUSED(args); - setComponentName("csvexporter", i18n("CSV exporter")); - setXMLFile("csvexporter.rc"); + const auto componentName = QLatin1String("csvexporter"); + const auto rcFileName = QLatin1String("csvexporter.rc"); + setComponentName(componentName, i18n("CSV exporter")); + +#ifdef IS_APPIMAGE + const QString rcFilePath = QCoreApplication::applicationDirPath() + QLatin1String("/../share/kxmlgui5/") + componentName + QLatin1Char('/') + rcFileName; + setXMLFile(rcFilePath); + + const QString localRcFilePath = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation).first() + QLatin1Char('/') + componentName + QLatin1Char('/') + rcFileName; + setLocalXMLFile(localRcFilePath); +#else + setXMLFile(rcFileName); +#endif + createActions(); // For information, announce that we have been loaded. qDebug("Plugins: csvexporter loaded"); } CSVExporter::~CSVExporter() { qDebug("Plugins: csvexporter unloaded"); } void CSVExporter::createActions() { const auto &kpartgui = QStringLiteral("file_export_csv"); m_action = actionCollection()->addAction(kpartgui); m_action->setText(i18n("&CSV...")); connect(m_action, &QAction::triggered, this, &CSVExporter::slotCsvExport); connect(viewInterface(), &KMyMoneyPlugin::ViewInterface::viewStateChanged, action(qPrintable(kpartgui)), &QAction::setEnabled); } void CSVExporter::slotCsvExport() { m_dlg = new CsvExportDlg(); if (m_dlg->exec()) { if (okToWriteFile(QUrl::fromUserInput(m_dlg->filename()))) { m_dlg->setWindowTitle(i18nc("CSV Exporter dialog title", "CSV Exporter")); CsvWriter* writer = new CsvWriter; writer->m_plugin = this; connect(writer, &CsvWriter::signalProgress, m_dlg, &CsvExportDlg::slotStatusProgressBar); writer->write(m_dlg->filename(), m_dlg->accountId(), m_dlg->accountSelected(), m_dlg->categorySelected(), m_dlg->startDate(), m_dlg->endDate(), m_dlg->separator()); } } } bool CSVExporter::okToWriteFile(const QUrl &url) { // check if the file exists and warn the user bool reallySaveFile = true; bool fileExists = false; if (url.isValid()) { short int detailLevel = 0; // Lowest level: file/dir/symlink/none KIO::StatJob* statjob = KIO::stat(url, KIO::StatJob::SourceSide, detailLevel); bool noerror = statjob->exec(); if (noerror) { // We want a file fileExists = !statjob->statResult().isDir(); } } if (fileExists) { if (KMessageBox::warningYesNo(0, i18n("The file %1 already exists. Do you really want to overwrite it?", url.toDisplayString(QUrl::PreferLocalFile)), i18n("File already exists")) != KMessageBox::Yes) reallySaveFile = false; } return reallySaveFile; } K_PLUGIN_FACTORY_WITH_JSON(CSVExporterFactory, "csvexporter.json", registerPlugin();) #include "csvexporter.moc" diff --git a/kmymoney/plugins/csv/import/csvimporter.cpp b/kmymoney/plugins/csv/import/csvimporter.cpp index 5c820b7d9..e3e03cd0e 100644 --- a/kmymoney/plugins/csv/import/csvimporter.cpp +++ b/kmymoney/plugins/csv/import/csvimporter.cpp @@ -1,111 +1,129 @@ /* * Copyright 2010-2014 Allan Anderson * Copyright 2016-2018 Łukasz Wojniłowicz * Copyright 2018 Thomas Baumgart * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ +#include #include "csvimporter.h" // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "core/csvimportercore.h" #include "csvwizard.h" #include "statementinterface.h" #include "viewinterface.h" +#ifdef IS_APPIMAGE +#include +#include +#endif + CSVImporter::CSVImporter(QObject *parent, const QVariantList &args) : KMyMoneyPlugin::Plugin(parent, "csvimporter"/*must be the same as X-KDE-PluginInfo-Name*/) { Q_UNUSED(args); - setComponentName("csvimporter", i18n("CSV importer")); - setXMLFile("csvimporter.rc"); + const auto componentName = QLatin1String("csvimporter"); + const auto rcFileName = QLatin1String("csvimporter.rc"); + setComponentName(componentName, i18n("CSV importer")); + +#ifdef IS_APPIMAGE + const QString rcFilePath = QCoreApplication::applicationDirPath() + QLatin1String("/../share/kxmlgui5/") + componentName + QLatin1Char('/') + rcFileName; + setXMLFile(rcFilePath); + + const QString localRcFilePath = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation).first() + QLatin1Char('/') + componentName + QLatin1Char('/') + rcFileName; + setLocalXMLFile(localRcFilePath); +#else + setXMLFile(rcFileName); +#endif + createActions(); // For information, announce that we have been loaded. qDebug("Plugins: csvimporter loaded"); } CSVImporter::~CSVImporter() { qDebug("Plugins: csvimporter unloaded"); } void CSVImporter::createActions() { const auto &kpartgui = QStringLiteral("file_import_csv"); auto importAction = actionCollection()->addAction(kpartgui); importAction->setText(i18n("CSV...")); connect(importAction, &QAction::triggered, this, &CSVImporter::startWizardRun); connect(viewInterface(), &KMyMoneyPlugin::ViewInterface::viewStateChanged, action(qPrintable(kpartgui)), &QAction::setEnabled); } QString CSVImporter::formatName() const { return QLatin1String("CSV"); } QString CSVImporter::formatFilenameFilter() const { return "*.csv"; } bool CSVImporter::isMyFormat(const QString& filename) const { // filename is considered a CSV file if it can be opened // and the filename ends in ".csv" (case does not matter). QFile f(filename); return filename.endsWith(QLatin1String(".csv"), Qt::CaseInsensitive) && f.open(QIODevice::ReadOnly | QIODevice::Text); } void CSVImporter::startWizardRun() { import(QString()); } bool CSVImporter::import(const QString& filename) { QPointer wizard = new CSVWizard(this); wizard->presetFilename(filename); auto rc = false; if ((wizard->exec() == QDialog::Accepted) && wizard) { rc = !statementInterface()->import(wizard->statement(), false).isEmpty(); } wizard->deleteLater(); return rc; } QString CSVImporter::lastError() const { return QString(); } K_PLUGIN_FACTORY_WITH_JSON(CSVImporterFactory, "csvimporter.json", registerPlugin();) #include "csvimporter.moc" diff --git a/kmymoney/plugins/icalendar/export/icalendarexporter.cpp b/kmymoney/plugins/icalendar/export/icalendarexporter.cpp index 681ad06d4..e1bf2e92b 100644 --- a/kmymoney/plugins/icalendar/export/icalendarexporter.cpp +++ b/kmymoney/plugins/icalendar/export/icalendarexporter.cpp @@ -1,145 +1,162 @@ /*************************************************************************** * Copyright 2009 Cristian Onet onet.cristian@gmail.com * * * * This program is free software; you can redistribute it and/or * * modify it under the terms of the GNU General Public License as * * published by the Free Software Foundation; either version 2 of * * the License or (at your option) version 3 or any later version * * accepted by the membership of KDE e.V. (or its successor approved * * by the membership of KDE e.V.), which shall act as a proxy * * defined in Section 14 of version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see * ***************************************************************************/ +#include #include "icalendarexporter.h" #include #include #include // KDE includes #include #include #include #include #include // KMyMoney includes #include "mymoneyfile.h" #include "pluginloader.h" #include "schedulestoicalendar.h" #include "pluginsettings.h" #include "viewinterface.h" +#ifdef IS_APPIMAGE +#include +#include +#endif + struct iCalendarExporter::Private { QAction* m_action; QString m_profileName; QString m_iCalendarFileEntryName; KMMSchedulesToiCalendar m_exporter; }; iCalendarExporter::iCalendarExporter(QObject *parent, const QVariantList &args) : KMyMoneyPlugin::Plugin(parent, "icalendarexporter"/*must be the same as X-KDE-PluginInfo-Name*/), d(std::unique_ptr(new Private)) { Q_UNUSED(args); d->m_profileName = "iCalendarPlugin"; d->m_iCalendarFileEntryName = "iCalendarFile"; + const auto componentName = QLatin1String("icalendarexporter"); + const auto rcFileName = QLatin1String("icalendarexporter.rc"); // Tell the host application to load my GUI component - setComponentName("icalendarexporter", i18n("iCalendar exporter")); - setXMLFile("icalendarexporter.rc"); + setComponentName(componentName, i18n("iCalendar exporter")); + +#ifdef IS_APPIMAGE + const QString rcFilePath = QCoreApplication::applicationDirPath() + QLatin1String("/../share/kxmlgui5/") + componentName + QLatin1Char('/') + rcFileName; + setXMLFile(rcFilePath); + + const QString localRcFilePath = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation).first() + QLatin1Char('/') + componentName + QLatin1Char('/') + rcFileName; + setLocalXMLFile(localRcFilePath); +#else + setXMLFile(rcFileName); +#endif // For ease announce that we have been loaded. qDebug("Plugins: icalendarexporter loaded"); // Create the actions of this plugin QString actionName = i18n("Schedules to iCalendar"); QString icalFilePath; // Note the below code only exists to move existing settings to the new plugin specific config KConfigGroup config = KSharedConfig::openConfig()->group(d->m_profileName); icalFilePath = config.readEntry(d->m_iCalendarFileEntryName, icalFilePath); // read the settings PluginSettings::self()->load(); if (!icalFilePath.isEmpty()) { // move the old setting to the new config PluginSettings::setIcalendarFile(icalFilePath); PluginSettings::self()->save(); KSharedConfig::openConfig()->deleteGroup(d->m_profileName); } else { // read it from the new config icalFilePath = PluginSettings::icalendarFile(); } if (!icalFilePath.isEmpty()) actionName = i18n("Schedules to iCalendar [%1]", icalFilePath); const auto &kpartgui = QStringLiteral("file_export_icalendar"); d->m_action = actionCollection()->addAction(kpartgui); d->m_action->setText(actionName); connect(d->m_action, &QAction::triggered, this, &iCalendarExporter::slotFirstExport); connect(viewInterface(), &KMyMoneyPlugin::ViewInterface::viewStateChanged, action(qPrintable(kpartgui)), &QAction::setEnabled); } iCalendarExporter::~iCalendarExporter() { qDebug("Plugins: icalendarexporter unloaded"); } void iCalendarExporter::slotFirstExport() { QPointer fileDialog = new QFileDialog(d->m_action->parentWidget(), QString(), QString(), QString("%1|%2\n").arg("*.ics").arg(i18nc("ICS (Filefilter)", "iCalendar files"))); fileDialog->setAcceptMode(QFileDialog::AcceptSave); fileDialog->setWindowTitle(i18n("Export as")); if (fileDialog->exec() == QDialog::Accepted) { QUrl newURL = fileDialog->selectedUrls().front(); if (newURL.isLocalFile()) { PluginSettings::setIcalendarFile(newURL.toLocalFile()); PluginSettings::self()->save(); slotExport(); } } delete fileDialog; } void iCalendarExporter::slotExport() { QString icalFilePath = PluginSettings::icalendarFile(); if (!icalFilePath.isEmpty()) d->m_exporter.exportToFile(icalFilePath); } void iCalendarExporter::plug() { connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, this, &iCalendarExporter::slotExport); } void iCalendarExporter::unplug() { disconnect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, this, &iCalendarExporter::slotExport); } void iCalendarExporter::configurationChanged() { PluginSettings::self()->load(); // export the schedules because the configuration has changed QString icalFilePath = PluginSettings::icalendarFile(); if (!icalFilePath.isEmpty()) d->m_exporter.exportToFile(icalFilePath); } K_PLUGIN_FACTORY_WITH_JSON(iCalendarExporterFactory, "icalendarexporter.json", registerPlugin();) #include "icalendarexporter.moc" diff --git a/kmymoney/plugins/kbanking/kbanking.cpp b/kmymoney/plugins/kbanking/kbanking.cpp index 17d1ee63e..02d3b861e 100644 --- a/kmymoney/plugins/kbanking/kbanking.cpp +++ b/kmymoney/plugins/kbanking/kbanking.cpp @@ -1,1530 +1,1548 @@ /* * Copyright 2004 Martin Preuss aquamaniac@users.sourceforge.net * Copyright 2009 Cristian Onet onet.cristian@gmail.com * Copyright 2010-2018 Thomas Baumgart tbaumgart@kde.org * Copyright 2015 Christian David christian-david@web.de * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ + #include #include "kbanking.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include //! @todo remove @c #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Library Includes #include #include #include #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoney/onlinejob.h" #include "kbaccountsettings.h" #include "kbmapaccount.h" #include "mymoneyfile.h" #include "onlinejobadministration.h" #include "kmymoneyview.h" #include "kbpickstartdate.h" #include "mymoneyinstitution.h" #include "mymoneyexception.h" #include "gwenkdegui.h" #include "gwenhywfarqtoperators.h" #include "aqbankingkmmoperators.h" #include "mymoneystatement.h" #include "statementinterface.h" #include "viewinterface.h" #ifdef KMM_DEBUG // Added an option to open the chipTanDialog from the menu for debugging purposes #include "chiptandialog.h" #endif +#ifdef IS_APPIMAGE +#include +#include +#endif + class KBanking::Private { public: Private() : passwordCacheTimer(nullptr), jobList(), fileId() { QString gwenProxy = QString::fromLocal8Bit(qgetenv("GWEN_PROXY")); if (gwenProxy.isEmpty()) { std::unique_ptr cfg = std::unique_ptr(new KConfig("kioslaverc")); QRegExp exp("(\\w+://)?([^/]{2}.+:\\d+)"); QString proxy; KConfigGroup grp = cfg->group("Proxy Settings"); int type = grp.readEntry("ProxyType", 0); switch (type) { case 0: // no proxy break; case 1: // manual specified proxy = grp.readEntry("httpsProxy"); qDebug("KDE https proxy setting is '%s'", qPrintable(proxy)); if (exp.exactMatch(proxy)) { proxy = exp.cap(2); qDebug("Setting GWEN_PROXY to '%s'", qPrintable(proxy)); if (!qputenv("GWEN_PROXY", qPrintable(proxy))) { qDebug("Unable to setup GWEN_PROXY"); } } break; default: // other currently not supported qDebug("KDE proxy setting of type %d not supported", type); break; } } } /** * KMyMoney asks for accounts over and over again which causes a lot of "Job not supported with this account" error messages. * This function filters messages with that string. */ static int gwenLogHook(GWEN_GUI* gui, const char* domain, GWEN_LOGGER_LEVEL level, const char* message) { Q_UNUSED(gui); Q_UNUSED(domain); Q_UNUSED(level); const char* messageToFilter = "Job not supported with this account"; if (strstr(message, messageToFilter) != 0) return 1; return 0; } QTimer *passwordCacheTimer; QMap jobList; QString fileId; }; KBanking::KBanking(QObject *parent, const QVariantList &args) : OnlinePluginExtended(parent, "kbanking") , d(new Private) , m_configAction(nullptr) , m_importAction(nullptr) , m_kbanking(nullptr) , m_accountSettings(nullptr) { Q_UNUSED(args) qDebug("Plugins: kbanking loaded"); } KBanking::~KBanking() { delete d; qDebug("Plugins: kbanking unloaded"); } void KBanking::plug() { + const auto componentName = QLatin1String("kbanking"); + const auto rcFileName = QLatin1String("kbanking.rc"); m_kbanking = new KBankingExt(this, "KMyMoney"); d->passwordCacheTimer = new QTimer(this); d->passwordCacheTimer->setSingleShot(true); d->passwordCacheTimer->setInterval(60000); connect(d->passwordCacheTimer, &QTimer::timeout, this, &KBanking::slotClearPasswordCache); if (m_kbanking) { if (AB_Banking_HasConf4(m_kbanking->getCInterface())) { qDebug("KBankingPlugin: No AqB4 config found."); if (AB_Banking_HasConf3(m_kbanking->getCInterface())) { qDebug("KBankingPlugin: No AqB3 config found."); if (!AB_Banking_HasConf2(m_kbanking->getCInterface())) { qDebug("KBankingPlugin: AqB2 config found - converting."); AB_Banking_ImportConf2(m_kbanking->getCInterface()); } } else { qDebug("KBankingPlugin: AqB3 config found - converting."); AB_Banking_ImportConf3(m_kbanking->getCInterface()); } } //! @todo when is gwenKdeGui deleted? gwenKdeGui *gui = new gwenKdeGui(); GWEN_Gui_SetGui(gui->getCInterface()); GWEN_Logger_SetLevel(0, GWEN_LoggerLevel_Warning); if (m_kbanking->init() == 0) { // Tell the host application to load my GUI component - setComponentName("kbanking", "KBanking"); - setXMLFile("kbanking.rc"); + setComponentName(componentName, "KBanking"); + +#ifdef IS_APPIMAGE + const QString rcFilePath = QCoreApplication::applicationDirPath() + QLatin1String("/../share/kxmlgui5/") + componentName + QLatin1Char('/') + rcFileName; + setXMLFile(rcFilePath); + + const QString localRcFilePath = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation).first() + QLatin1Char('/') + componentName + QLatin1Char('/') + rcFileName; + setLocalXMLFile(localRcFilePath); + #else + setXMLFile(rcFileName); +#endif + qDebug("Plugins: kbanking plugged"); // get certificate handling and dialog settings management AB_Gui_Extend(gui->getCInterface(), m_kbanking->getCInterface()); // create actions createActions(); // load protocol conversion list loadProtocolConversion(); GWEN_Logger_SetLevel(AQBANKING_LOGDOMAIN, GWEN_LoggerLevel_Warning); GWEN_Gui_SetLogHookFn(GWEN_Gui_GetGui(), &KBanking::Private::gwenLogHook); } else { qWarning("Could not initialize KBanking online banking interface"); delete m_kbanking; m_kbanking = 0; } } } void KBanking::unplug() { d->passwordCacheTimer->deleteLater(); if (m_kbanking) { m_kbanking->fini(); delete m_kbanking; qDebug("Plugins: kbanking unplugged"); } } void KBanking::loadProtocolConversion() { if (m_kbanking) { m_protocolConversionMap = { {"aqhbci", "HBCI"}, {"aqofxconnect", "OFX"}, {"aqyellownet", "YellowNet"}, {"aqgeldkarte", "Geldkarte"}, {"aqdtaus", "DTAUS"} }; } } void KBanking::protocols(QStringList& protocolList) const { if (m_kbanking) { std::list list = m_kbanking->getActiveProviders(); std::list::iterator it; for (it = list.begin(); it != list.end(); ++it) { // skip the dummy if (*it == "aqnone") continue; QMap::const_iterator it_m; it_m = m_protocolConversionMap.find((*it).c_str()); if (it_m != m_protocolConversionMap.end()) protocolList << (*it_m); else protocolList << (*it).c_str(); } } } QWidget* KBanking::accountConfigTab(const MyMoneyAccount& acc, QString& name) { const MyMoneyKeyValueContainer& kvp = acc.onlineBankingSettings(); name = i18n("Online settings"); if (m_kbanking) { m_accountSettings = new KBAccountSettings(acc, 0); m_accountSettings->loadUi(kvp); return m_accountSettings; } QLabel* label = new QLabel(i18n("KBanking module not correctly initialized"), 0); label->setAlignment(Qt::AlignVCenter | Qt::AlignHCenter); return label; } MyMoneyKeyValueContainer KBanking::onlineBankingSettings(const MyMoneyKeyValueContainer& current) { MyMoneyKeyValueContainer kvp(current); kvp["provider"] = objectName().toLower(); if (m_accountSettings) { m_accountSettings->loadKvp(kvp); } return kvp; } void KBanking::createActions() { QAction *settings_aqbanking = actionCollection()->addAction("settings_aqbanking"); settings_aqbanking->setText(i18n("Configure Aq&Banking...")); connect(settings_aqbanking, &QAction::triggered, this, &KBanking::slotSettings); QAction *file_import_aqbanking = actionCollection()->addAction("file_import_aqbanking"); file_import_aqbanking->setText(i18n("AqBanking importer...")); connect(file_import_aqbanking, &QAction::triggered, this, &KBanking::slotImport); Q_CHECK_PTR(viewInterface()); connect(viewInterface(), &KMyMoneyPlugin::ViewInterface::viewStateChanged, action("file_import_aqbanking"), &QAction::setEnabled); #ifdef KMM_DEBUG QAction *openChipTanDialog = actionCollection()->addAction("open_chiptan_dialog"); openChipTanDialog->setText("Open ChipTan Dialog"); connect(openChipTanDialog, &QAction::triggered, [&](){ auto dlg = new chipTanDialog(); dlg->setHhdCode("0F04871100030333555414312C32331D"); dlg->setInfoText("

Test Graphic for debugging

The encoded data is

Account Number: 335554
Amount: 1,23

"); connect(dlg, &QDialog::accepted, dlg, &chipTanDialog::deleteLater); connect(dlg, &QDialog::rejected, dlg, &chipTanDialog::deleteLater); dlg->show(); }); #endif } void KBanking::slotSettings() { if (m_kbanking) { GWEN_DIALOG* dlg = AB_SetupDialog_new(m_kbanking->getCInterface()); if (dlg == NULL) { DBG_ERROR(0, "Could not create setup dialog."); return; } if (GWEN_Gui_ExecDialog(dlg, 0) == 0) { DBG_ERROR(0, "Aborted by user"); GWEN_Dialog_free(dlg); return; } GWEN_Dialog_free(dlg); } } bool KBanking::mapAccount(const MyMoneyAccount& acc, MyMoneyKeyValueContainer& settings) { bool rc = false; if (m_kbanking && !acc.id().isEmpty()) { m_kbanking->askMapAccount(acc); // at this point, the account should be mapped // so we search it and setup the account reference in the KMyMoney object AB_ACCOUNT* ab_acc; ab_acc = aqbAccount(acc); if (ab_acc) { MyMoneyAccount a(acc); setupAccountReference(a, ab_acc); settings = a.onlineBankingSettings(); rc = true; } } return rc; } AB_ACCOUNT* KBanking::aqbAccount(const MyMoneyAccount& acc) const { if (m_kbanking == 0) { return 0; } // certainly looking for an expense or income account does not make sense at this point // so we better get out right away if (acc.isIncomeExpense()) { return 0; } AB_ACCOUNT *ab_acc = AB_Banking_GetAccountByAlias(m_kbanking->getCInterface(), m_kbanking->mappingId(acc).toUtf8().data()); // if the account is not found, we temporarily scan for the 'old' mapping (the one w/o the file id) // in case we find it, we setup the new mapping in addition on the fly. if (!ab_acc && acc.isAssetLiability()) { ab_acc = AB_Banking_GetAccountByAlias(m_kbanking->getCInterface(), acc.id().toUtf8().data()); if (ab_acc) { qDebug("Found old mapping for '%s' but not new. Setup new mapping", qPrintable(acc.name())); m_kbanking->setAccountAlias(ab_acc, m_kbanking->mappingId(acc).toUtf8().constData()); // TODO at some point in time, we should remove the old mapping } } return ab_acc; } AB_ACCOUNT* KBanking::aqbAccount(const QString& accountId) const { MyMoneyAccount account = MyMoneyFile::instance()->account(accountId); return aqbAccount(account); } QString KBanking::stripLeadingZeroes(const QString& s) const { QString rc(s); QRegExp exp("^(0*)([^0].*)"); if (exp.exactMatch(s)) { rc = exp.cap(2); } return rc; } void KBanking::setupAccountReference(const MyMoneyAccount& acc, AB_ACCOUNT* ab_acc) { MyMoneyKeyValueContainer kvp; if (ab_acc) { QString accountNumber = stripLeadingZeroes(AB_Account_GetAccountNumber(ab_acc)); QString routingNumber = stripLeadingZeroes(AB_Account_GetBankCode(ab_acc)); QString val = QString("%1-%2").arg(routingNumber, accountNumber); if (val != acc.onlineBankingSettings().value("kbanking-acc-ref")) { kvp.clear(); // make sure to keep our own previous settings const QMap& vals = acc.onlineBankingSettings().pairs(); QMap::const_iterator it_p; for (it_p = vals.begin(); it_p != vals.end(); ++it_p) { if (QString(it_p.key()).startsWith("kbanking-")) { kvp.setValue(it_p.key(), *it_p); } } kvp.setValue("kbanking-acc-ref", val); kvp.setValue("provider", objectName().toLower()); setAccountOnlineParameters(acc, kvp); } } else { // clear the connection setAccountOnlineParameters(acc, kvp); } } bool KBanking::accountIsMapped(const MyMoneyAccount& acc) { return aqbAccount(acc) != 0; } bool KBanking::updateAccount(const MyMoneyAccount& acc) { return updateAccount(acc, false); } bool KBanking::updateAccount(const MyMoneyAccount& acc, bool moreAccounts) { if (!m_kbanking) return false; bool rc = false; if (!acc.id().isEmpty()) { AB_JOB *job = 0; int rv; /* get AqBanking account */ AB_ACCOUNT *ba = aqbAccount(acc); // Update the connection between the KMyMoney account and the AqBanking equivalent. // If the account is not found anymore ba == 0 and the connection is removed. setupAccountReference(acc, ba); if (!ba) { KMessageBox::error(0, i18n("" "The given application account %1 " "has not been mapped to an online " "account." "", acc.name()), i18n("Account Not Mapped")); } else { bool enqueJob = true; if (acc.onlineBankingSettings().value("kbanking-txn-download") != "no") { /* create getTransactions job */ job = AB_JobGetTransactions_new(ba); rv = AB_Job_CheckAvailability(job); if (rv) { DBG_ERROR(0, "Job \"GetTransactions\" is not available (%d)", rv); KMessageBox::error(0, i18n("" "The update job is not supported by the " "bank/account/backend.\n" ""), i18n("Job not Available")); AB_Job_free(job); job = 0; } if (job) { int days = AB_JobGetTransactions_GetMaxStoreDays(job); QDate qd; if (days > 0) { GWEN_TIME *ti1; GWEN_TIME *ti2; ti1 = GWEN_CurrentTime(); ti2 = GWEN_Time_fromSeconds(GWEN_Time_Seconds(ti1) - (60 * 60 * 24 * days)); GWEN_Time_free(ti1); ti1 = ti2; int year, month, day; if (GWEN_Time_GetBrokenDownDate(ti1, &day, &month, &year)) { DBG_ERROR(0, "Bad date"); qd = QDate(); } else qd = QDate(year, month + 1, day); GWEN_Time_free(ti1); } // get last statement request date from application account object // and start from a few days before if the date is valid QDate lastUpdate = QDate::fromString(acc.value("lastImportedTransactionDate"), Qt::ISODate); if (lastUpdate.isValid()) lastUpdate = lastUpdate.addDays(-3); int dateOption = acc.onlineBankingSettings().value("kbanking-statementDate").toInt(); switch (dateOption) { case 0: // Ask user break; case 1: // No date qd = QDate(); break; case 2: // Last download qd = lastUpdate; break; case 3: // First possible // qd is already setup break; } // the pick start date option dialog is needed in // case the dateOption is 0 or the date option is > 1 // and the qd is invalid if (dateOption == 0 || (dateOption > 1 && !qd.isValid())) { QPointer psd = new KBPickStartDate(m_kbanking, qd, lastUpdate, acc.name(), lastUpdate.isValid() ? 2 : 3, 0, true); if (psd->exec() == QDialog::Accepted) { qd = psd->date(); } else { enqueJob = false; } delete psd; } if (enqueJob) { if (qd.isValid()) { GWEN_TIME *ti1; ti1 = GWEN_Time_new(qd.year(), qd.month() - 1, qd.day(), 0, 0, 0, 0); AB_JobGetTransactions_SetFromTime(job, ti1); GWEN_Time_free(ti1); } rv = m_kbanking->enqueueJob(job); if (rv) { DBG_ERROR(0, "Error %d", rv); KMessageBox::error(0, i18n("" "Could not enqueue the job.\n" ""), i18n("Error")); } } AB_Job_free(job); } } if (enqueJob) { /* create getBalance job */ job = AB_JobGetBalance_new(ba); rv = AB_Job_CheckAvailability(job); if (!rv) rv = m_kbanking->enqueueJob(job); else rv = 0; AB_Job_free(job); if (rv) { DBG_ERROR(0, "Error %d", rv); KMessageBox::error(0, i18n("" "Could not enqueue the job.\n" ""), i18n("Error")); } else { rc = true; emit queueChanged(); } } } } // make sure we have at least one job in the queue before sending it if (!moreAccounts && m_kbanking->getEnqueuedJobs().size() > 0) executeQueue(); return rc; } void KBanking::executeQueue() { if (m_kbanking && m_kbanking->getEnqueuedJobs().size() > 0) { AB_IMEXPORTER_CONTEXT *ctx; ctx = AB_ImExporterContext_new(); int rv = m_kbanking->executeQueue(ctx); if (!rv) { m_kbanking->importContext(ctx, 0); } else { DBG_ERROR(0, "Error: %d", rv); } AB_ImExporterContext_free(ctx); } } /** @todo improve error handling, e.g. by adding a .isValid to nationalTransfer * @todo use new onlineJob system */ void KBanking::sendOnlineJob(QList& jobs) { Q_CHECK_PTR(m_kbanking); m_onlineJobQueue.clear(); QList unhandledJobs; if (!jobs.isEmpty()) { foreach (onlineJob job, jobs) { if (sepaOnlineTransfer::name() == job.task()->taskName()) { onlineJobTyped typedJob(job); enqueTransaction(typedJob); job = typedJob; } else { job.addJobMessage(onlineJobMessage(eMyMoney::OnlineJob::MessageType::Error, "KBanking", "Cannot handle this request")); unhandledJobs.append(job); } m_onlineJobQueue.insert(m_kbanking->mappingId(job), job); } executeQueue(); } jobs = m_onlineJobQueue.values() + unhandledJobs; m_onlineJobQueue.clear(); } QStringList KBanking::availableJobs(QString accountId) { try { MyMoneyAccount acc = MyMoneyFile::instance()->account(accountId); QString id = MyMoneyFile::instance()->value("kmm-id"); if(id != d->fileId) { d->jobList.clear(); d->fileId = id; } } catch (const MyMoneyException &) { // Exception usually means account was not found return QStringList(); } if(d->jobList.contains(accountId)) { return d->jobList[accountId]; } QStringList list; AB_ACCOUNT* abAccount = aqbAccount(accountId); if (!abAccount) { return list; } // Check availableJobs // sepa transfer AB_JOB* abJob = AB_JobSepaTransfer_new(abAccount); if (AB_Job_CheckAvailability(abJob) == 0) list.append(sepaOnlineTransfer::name()); AB_Job_free(abJob); d->jobList[accountId] = list; return list; } /** @brief experimenting with QScopedPointer and aqBanking pointers */ class QScopedPointerAbJobDeleter { public: static void cleanup(AB_JOB* job) { AB_Job_free(job); } }; /** @brief experimenting with QScopedPointer and aqBanking pointers */ class QScopedPointerAbAccountDeleter { public: static void cleanup(AB_ACCOUNT* account) { AB_Account_free(account); } }; IonlineTaskSettings::ptr KBanking::settings(QString accountId, QString taskName) { AB_ACCOUNT* abAcc = aqbAccount(accountId); if (abAcc == 0) return IonlineTaskSettings::ptr(); if (sepaOnlineTransfer::name() == taskName) { // Get limits for sepaonlinetransfer QScopedPointer abJob(AB_JobSepaTransfer_new(abAcc)); if (AB_Job_CheckAvailability(abJob.data()) != 0) return IonlineTaskSettings::ptr(); const AB_TRANSACTION_LIMITS* limits = AB_Job_GetFieldLimits(abJob.data()); return AB_TransactionLimits_toSepaOnlineTaskSettings(limits).dynamicCast(); } return IonlineTaskSettings::ptr(); } bool KBanking::enqueTransaction(onlineJobTyped& job) { /* get AqBanking account */ const QString accId = job.constTask()->responsibleAccount(); AB_ACCOUNT *abAccount = aqbAccount(accId); if (!abAccount) { job.addJobMessage(onlineJobMessage(eMyMoney::OnlineJob::MessageType::Warning, "KBanking", i18n("" "The given application account %1 " "has not been mapped to an online " "account." "", MyMoneyFile::instance()->account(accId).name()))); return false; } //setupAccountReference(acc, ba); // needed? AB_JOB *abJob = AB_JobSepaTransfer_new(abAccount); int rv = AB_Job_CheckAvailability(abJob); if (rv) { qDebug("AB_ERROR_OFFSET is %i", AB_ERROR_OFFSET); job.addJobMessage(onlineJobMessage(eMyMoney::OnlineJob::MessageType::Error, "AqBanking", QString("Sepa credit transfers for account \"%1\" are not available, error code %2.").arg(MyMoneyFile::instance()->account(accId).name(), rv) ) ); return false; } AB_TRANSACTION *AbTransaction = AB_Transaction_new(); // Recipient payeeIdentifiers::ibanBic beneficiaryAcc = job.constTask()->beneficiaryTyped(); AB_Transaction_SetRemoteName(AbTransaction, GWEN_StringList_fromQString(beneficiaryAcc.ownerName())); AB_Transaction_SetRemoteIban(AbTransaction, beneficiaryAcc.electronicIban().toUtf8().constData()); AB_Transaction_SetRemoteBic(AbTransaction, beneficiaryAcc.fullStoredBic().toUtf8().constData()); // Origin Account AB_Transaction_SetLocalAccount(AbTransaction, abAccount); // Purpose QStringList qPurpose = job.constTask()->purpose().split('\n'); GWEN_STRINGLIST *purpose = GWEN_StringList_fromQStringList(qPurpose); AB_Transaction_SetPurpose(AbTransaction, purpose); GWEN_StringList_free(purpose); // Reference // AqBanking duplicates the string. This should be safe. AB_Transaction_SetEndToEndReference(AbTransaction, job.constTask()->endToEndReference().toUtf8().constData()); // Other Fields AB_Transaction_SetTextKey(AbTransaction, job.constTask()->textKey()); AB_Transaction_SetValue(AbTransaction, AB_Value_fromMyMoneyMoney(job.constTask()->value())); /** @todo LOW remove Debug info */ qDebug() << "SetTransaction: " << AB_Job_SetTransaction(abJob, AbTransaction); GWEN_DB_NODE *gwenNode = AB_Job_GetAppData(abJob); GWEN_DB_SetCharValue(gwenNode, GWEN_DB_FLAGS_DEFAULT, "kmmOnlineJobId", m_kbanking->mappingId(job).toLatin1().constData()); qDebug() << "Enqueue: " << m_kbanking->enqueueJob(abJob); //delete localAcc; return true; } void KBanking::startPasswordTimer() { if (d->passwordCacheTimer->isActive()) d->passwordCacheTimer->stop(); d->passwordCacheTimer->start(); } void KBanking::slotClearPasswordCache() { m_kbanking->clearPasswordCache(); } void KBanking::slotImport() { m_statementCount = 0; statementInterface()->resetMessages(); if (!m_kbanking->interactiveImport()) qWarning("Error on import dialog"); else statementInterface()->showMessages(m_statementCount); } bool KBanking::importStatement(const MyMoneyStatement& s) { m_statementCount++; return !statementInterface()->import(s).isEmpty(); } MyMoneyAccount KBanking::account(const QString& key, const QString& value) const { return statementInterface()->account(key, value); } void KBanking::setAccountOnlineParameters(const MyMoneyAccount& acc, const MyMoneyKeyValueContainer& kvps) const { return statementInterface()->setAccountOnlineParameters(acc, kvps); } KBankingExt::KBankingExt(KBanking* parent, const char* appname, const char* fname) : AB_Banking(appname, fname) , m_parent(parent) , _jobQueue(0) { m_sepaKeywords = {QString::fromUtf8("SEPA-BASISLASTSCHRIFT"), QString::fromUtf8("SEPA-ÜBERWEISUNG")}; } int KBankingExt::init() { int rv = AB_Banking::init(); if (rv < 0) return rv; rv = onlineInit(); if (rv) { fprintf(stderr, "Error on online init (%d).\n", rv); AB_Banking::fini(); return rv; } _jobQueue = AB_Job_List2_new(); return 0; } int KBankingExt::fini() { if (_jobQueue) { AB_Job_List2_FreeAll(_jobQueue); _jobQueue = 0; } const int rv = onlineFini(); if (rv) { AB_Banking::fini(); return rv; } return AB_Banking::fini(); } int KBankingExt::executeQueue(AB_IMEXPORTER_CONTEXT *ctx) { m_parent->startPasswordTimer(); int rv = AB_Banking::executeJobs(_jobQueue, ctx); if (rv != 0) { qDebug() << "Sending queue by aqbanking got error no " << rv; } /** check result of each job */ AB_JOB_LIST2_ITERATOR* jobIter = AB_Job_List2_First(_jobQueue); if (jobIter) { AB_JOB* abJob = AB_Job_List2Iterator_Data(jobIter); while (abJob) { GWEN_DB_NODE* gwenNode = AB_Job_GetAppData(abJob); if (gwenNode == 0) { qWarning("Executed AB_Job without KMyMoney id"); abJob = AB_Job_List2Iterator_Next(jobIter); break; } QString jobIdent = QString::fromUtf8(GWEN_DB_GetCharValue(gwenNode, "kmmOnlineJobId", 0, "")); onlineJob job = m_parent->m_onlineJobQueue.value(jobIdent); if (job.isNull()) { // It should not be possible that this will happen (only if AqBanking fails heavily). //! @todo correct exception text qWarning("Executed a job which was not in queue. Please inform the KMyMoney developers."); abJob = AB_Job_List2Iterator_Next(jobIter); continue; } AB_JOB_STATUS abStatus = AB_Job_GetStatus(abJob); if (abStatus == AB_Job_StatusSent || abStatus == AB_Job_StatusPending || abStatus == AB_Job_StatusFinished || abStatus == AB_Job_StatusError || abStatus == AB_Job_StatusUnknown) job.setJobSend(); if (abStatus == AB_Job_StatusFinished) job.setBankAnswer(eMyMoney::OnlineJob::sendingState::acceptedByBank); else if (abStatus == AB_Job_StatusError || abStatus == AB_Job_StatusUnknown) job.setBankAnswer(eMyMoney::OnlineJob::sendingState::sendingError); job.addJobMessage(onlineJobMessage(eMyMoney::OnlineJob::MessageType::Debug, "KBanking", "Job was processed")); m_parent->m_onlineJobQueue.insert(jobIdent, job); abJob = AB_Job_List2Iterator_Next(jobIter); } AB_Job_List2Iterator_free(jobIter); } AB_JOB_LIST2 *oldQ = _jobQueue; _jobQueue = AB_Job_List2_new(); AB_Job_List2_FreeAll(oldQ); emit m_parent->queueChanged(); m_parent->startPasswordTimer(); return rv; } void KBankingExt::clearPasswordCache() { /* clear password DB */ GWEN_Gui_SetPasswordStatus(NULL, NULL, GWEN_Gui_PasswordStatus_Remove, 0); } std::list KBankingExt::getEnqueuedJobs() { AB_JOB_LIST2 *ll; std::list rl; ll = _jobQueue; if (ll && AB_Job_List2_GetSize(ll)) { AB_JOB *j; AB_JOB_LIST2_ITERATOR *it; it = AB_Job_List2_First(ll); assert(it); j = AB_Job_List2Iterator_Data(it); assert(j); while (j) { rl.push_back(j); j = AB_Job_List2Iterator_Next(it); } AB_Job_List2Iterator_free(it); } return rl; } int KBankingExt::enqueueJob(AB_JOB *j) { assert(_jobQueue); assert(j); AB_Job_Attach(j); AB_Job_List2_PushBack(_jobQueue, j); return 0; } int KBankingExt::dequeueJob(AB_JOB *j) { assert(_jobQueue); AB_Job_List2_Remove(_jobQueue, j); AB_Job_free(j); emit m_parent->queueChanged(); return 0; } void KBankingExt::transfer() { //m_parent->transfer(); } bool KBankingExt::askMapAccount(const MyMoneyAccount& acc) { MyMoneyFile* file = MyMoneyFile::instance(); QString bankId; QString accountId; // extract some information about the bank. if we have a sortcode // (BLZ) we display it, otherwise the name is enough. try { const MyMoneyInstitution &bank = file->institution(acc.institutionId()); bankId = bank.name(); if (!bank.sortcode().isEmpty()) bankId = bank.sortcode(); } catch (const MyMoneyException &e) { // no bank assigned, we just leave the field empty } // extract account information. if we have an account number // we show it, otherwise the name will be displayed accountId = acc.number(); if (accountId.isEmpty()) accountId = acc.name(); // do the mapping. the return value of this method is either // true, when the user mapped the account or false, if he // decided to quit the dialog. So not really a great thing // to present some more information. KBMapAccount *w; w = new KBMapAccount(this, bankId.toUtf8().constData(), accountId.toUtf8().constData()); if (w->exec() == QDialog::Accepted) { AB_ACCOUNT *a; a = w->getAccount(); assert(a); DBG_NOTICE(0, "Mapping application account \"%s\" to " "online account \"%s/%s\"", qPrintable(acc.name()), AB_Account_GetBankCode(a), AB_Account_GetAccountNumber(a)); // TODO remove the following line once we don't need backward compatibility setAccountAlias(a, acc.id().toUtf8().constData()); qDebug("Setup mapping to '%s'", acc.id().toUtf8().constData()); setAccountAlias(a, mappingId(acc).toUtf8().constData()); qDebug("Setup mapping to '%s'", mappingId(acc).toUtf8().constData()); delete w; return true; } delete w; return false; } QString KBankingExt::mappingId(const MyMoneyObject& object) const { QString id = MyMoneyFile::instance()->storageId() + QLatin1Char('-') + object.id(); // AqBanking does not handle the enclosing parens, so we remove it id.remove('{'); id.remove('}'); return id; } bool KBankingExt::interactiveImport() { AB_IMEXPORTER_CONTEXT *ctx; GWEN_DIALOG *dlg; int rv; ctx = AB_ImExporterContext_new(); dlg = AB_ImporterDialog_new(getCInterface(), ctx, NULL); if (dlg == NULL) { DBG_ERROR(0, "Could not create importer dialog."); AB_ImExporterContext_free(ctx); return false; } rv = GWEN_Gui_ExecDialog(dlg, 0); if (rv == 0) { DBG_ERROR(0, "Aborted by user"); GWEN_Dialog_free(dlg); AB_ImExporterContext_free(ctx); return false; } if (!importContext(ctx, 0)) { DBG_ERROR(0, "Error on importContext"); GWEN_Dialog_free(dlg); AB_ImExporterContext_free(ctx); return false; } GWEN_Dialog_free(dlg); AB_ImExporterContext_free(ctx); return true; } const AB_ACCOUNT_STATUS* KBankingExt::_getAccountStatus(AB_IMEXPORTER_ACCOUNTINFO *ai) { const AB_ACCOUNT_STATUS *ast; const AB_ACCOUNT_STATUS *best; best = 0; ast = AB_ImExporterAccountInfo_GetFirstAccountStatus(ai); while (ast) { if (!best) best = ast; else { const GWEN_TIME *tiBest; const GWEN_TIME *ti; tiBest = AB_AccountStatus_GetTime(best); ti = AB_AccountStatus_GetTime(ast); if (!tiBest) { best = ast; } else { if (ti) { double d; /* we have two times, compare them */ d = GWEN_Time_Diff(ti, tiBest); if (d > 0) /* newer */ best = ast; } } } ast = AB_ImExporterAccountInfo_GetNextAccountStatus(ai); } /* while */ return best; } void KBankingExt::_xaToStatement(MyMoneyStatement &ks, const MyMoneyAccount& acc, const AB_TRANSACTION *t) { const GWEN_STRINGLIST *sl; QString s; QString memo; const char *p; const AB_VALUE *val; const GWEN_TIME *ti; const GWEN_TIME *startTime = 0; MyMoneyStatement::Transaction kt; unsigned long h; kt.m_fees = MyMoneyMoney(); // bank's transaction id p = AB_Transaction_GetFiId(t); if (p) kt.m_strBankID = QString("ID ") + QString::fromUtf8(p); // payee s.truncate(0); sl = AB_Transaction_GetRemoteName(t); if (sl) { GWEN_STRINGLISTENTRY *se; se = GWEN_StringList_FirstEntry(sl); while (se) { p = GWEN_StringListEntry_Data(se); assert(p); s += QString::fromUtf8(p); se = GWEN_StringListEntry_Next(se); } // while } kt.m_strPayee = s; // memo // The variable 's' contains the old method of extracting // the memo which added a linefeed after each part received // from AqBanking. The new variable 'memo' does not have // this inserted linefeed. We keep the variable 's' to // construct the hash-value to retrieve the reference s.truncate(0); sl = AB_Transaction_GetPurpose(t); if (sl) { GWEN_STRINGLISTENTRY *se; bool insertLineSep = false; se = GWEN_StringList_FirstEntry(sl); while (se) { p = GWEN_StringListEntry_Data(se); assert(p); if (insertLineSep) s += '\n'; insertLineSep = true; s += QString::fromUtf8(p).trimmed(); memo += QString::fromUtf8(p).trimmed(); se = GWEN_StringListEntry_Next(se); } // while // Sparda / Netbank hack: the software these banks use stores // parts of the payee name in the beginning of the purpose field // in case the payee name exceeds the 27 character limit. This is // the case, when one of the strings listed in m_sepaKeywords is part // of the purpose fields but does not start at the beginning. In this // case, the part leading up to the keyword is to be treated as the // tail of the payee. Also, a blank is inserted after the keyword. QSet::const_iterator itk; for (itk = m_sepaKeywords.constBegin(); itk != m_sepaKeywords.constEnd(); ++itk) { int idx = s.indexOf(*itk); if (idx >= 0) { if (idx > 0) { // re-add a possibly removed blank to name if (kt.m_strPayee.length() < 27) kt.m_strPayee += ' '; kt.m_strPayee += s.left(idx); s = s.mid(idx); } s = QString("%1 %2").arg(*itk).arg(s.mid((*itk).length())); // now do the same for 'memo' except for updating the payee idx = memo.indexOf(*itk); if (idx >= 0) { if (idx > 0) { memo = memo.mid(idx); } } memo = QString("%1 %2").arg(*itk).arg(memo.mid((*itk).length())); break; } } // in case we have some SEPA fields filled with information // we add them to the memo field p = AB_Transaction_GetEndToEndReference(t); if (p) { s += QString(", EREF: %1").arg(p); if(memo.length()) memo.append('\n'); memo.append(QString("EREF: %1").arg(p)); } p = AB_Transaction_GetCustomerReference(t); if (p) { s += QString(", CREF: %1").arg(p); if(memo.length()) memo.append('\n'); memo.append(QString("CREF: %1").arg(p)); } p = AB_Transaction_GetMandateId(t); if (p) { s += QString(", MREF: %1").arg(p); if(memo.length()) memo.append('\n'); memo.append(QString("MREF: %1").arg(p)); } p = AB_Transaction_GetCreditorSchemeId(t); if (p) { s += QString(", CRED: %1").arg(p); if(memo.length()) memo.append('\n'); memo.append(QString("CRED: %1").arg(p)); } p = AB_Transaction_GetOriginatorIdentifier(t); if (p) { s += QString(", DEBT: %1").arg(p); if(memo.length()) memo.append('\n'); memo.append(QString("DEBT: %1").arg(p)); } } const MyMoneyKeyValueContainer& kvp = acc.onlineBankingSettings(); // check if we need the version with or without linebreaks if (kvp.value("kbanking-memo-removelinebreaks").compare(QLatin1String("no"))) { kt.m_strMemo = memo; } else { kt.m_strMemo = s; } // calculate the hash code and start with the payee info // and append the memo field h = MyMoneyTransaction::hash(kt.m_strPayee.trimmed()); h = MyMoneyTransaction::hash(s, h); // see, if we need to extract the payee from the memo field QString rePayee = kvp.value("kbanking-payee-regexp"); if (!rePayee.isEmpty() && kt.m_strPayee.isEmpty()) { QString reMemo = kvp.value("kbanking-memo-regexp"); QStringList exceptions = kvp.value("kbanking-payee-exceptions").split(';', QString::SkipEmptyParts); bool needExtract = true; QStringList::const_iterator it_s; for (it_s = exceptions.constBegin(); needExtract && it_s != exceptions.constEnd(); ++it_s) { QRegExp exp(*it_s, Qt::CaseInsensitive); if (exp.indexIn(kt.m_strMemo) != -1) { needExtract = false; } } if (needExtract) { QRegExp expPayee(rePayee, Qt::CaseInsensitive); QRegExp expMemo(reMemo, Qt::CaseInsensitive); if (expPayee.indexIn(kt.m_strMemo) != -1) { kt.m_strPayee = expPayee.cap(1); if (expMemo.indexIn(kt.m_strMemo) != -1) { kt.m_strMemo = expMemo.cap(1); } } } } kt.m_strPayee = kt.m_strPayee.trimmed(); // date ti = AB_Transaction_GetDate(t); if (!ti) ti = AB_Transaction_GetValutaDate(t); if (ti) { int year, month, day; if (!startTime) startTime = ti; /*else { dead code if (GWEN_Time_Diff(ti, startTime) < 0) startTime = ti; }*/ if (!GWEN_Time_GetBrokenDownDate(ti, &day, &month, &year)) { kt.m_datePosted = QDate(year, month + 1, day); } } else { DBG_WARN(0, "No date for transaction"); } // value val = AB_Transaction_GetValue(t); if (val) { if (ks.m_strCurrency.isEmpty()) { p = AB_Value_GetCurrency(val); if (p) ks.m_strCurrency = p; } else { p = AB_Value_GetCurrency(val); if (p) s = p; if (ks.m_strCurrency.toLower() != s.toLower()) { // TODO: handle currency difference DBG_ERROR(0, "Mixed currencies currently not allowed"); } } kt.m_amount = MyMoneyMoney(AB_Value_GetValueAsDouble(val)); // The initial implementation of this feature was based on // a denominator of 100. Since the denominator might be // different nowadays, we make sure to use 100 for the // duplicate detection QString tmpVal = kt.m_amount.formatMoney(100, false); tmpVal.remove(QRegExp("[,\\.]")); tmpVal += QLatin1String("/100"); h = MyMoneyTransaction::hash(tmpVal, h); } else { DBG_WARN(0, "No value for transaction"); } if (startTime) { int year, month, day; if (!GWEN_Time_GetBrokenDownDate(startTime, &day, &month, &year)) { QDate d(year, month + 1, day); if (!ks.m_dateBegin.isValid()) ks.m_dateBegin = d; else if (d < ks.m_dateBegin) ks.m_dateBegin = d; if (!ks.m_dateEnd.isValid()) ks.m_dateEnd = d; else if (d > ks.m_dateEnd) ks.m_dateEnd = d; } } else { DBG_WARN(0, "No date in current transaction"); } // add information about remote account to memo in case we have something const char *remoteAcc = AB_Transaction_GetRemoteAccountNumber(t); const char *remoteBankCode = AB_Transaction_GetRemoteBankCode(t); if (remoteAcc && remoteBankCode) { kt.m_strMemo += QString("\n%1/%2").arg(remoteBankCode, remoteAcc); } // make hash value unique in case we don't have one already if (kt.m_strBankID.isEmpty()) { QString hashBase; hashBase.sprintf("%s-%07lx", qPrintable(kt.m_datePosted.toString(Qt::ISODate)), h); int idx = 1; QString hash; for (;;) { hash = QString("%1-%2").arg(hashBase).arg(idx); QMap::const_iterator it; it = m_hashMap.constFind(hash); if (it == m_hashMap.constEnd()) { m_hashMap[hash] = true; break; } ++idx; } kt.m_strBankID = QString("%1-%2").arg(acc.id()).arg(hash); } // store transaction ks.m_listTransactions += kt; } bool KBankingExt::importAccountInfo(AB_IMEXPORTER_ACCOUNTINFO *ai, uint32_t /*flags*/) { const char *p; DBG_INFO(0, "Importing account..."); // account number MyMoneyStatement ks; p = AB_ImExporterAccountInfo_GetAccountNumber(ai); if (p) { ks.m_strAccountNumber = m_parent->stripLeadingZeroes(p); } p = AB_ImExporterAccountInfo_GetBankCode(ai); if (p) { ks.m_strRoutingNumber = m_parent->stripLeadingZeroes(p); } MyMoneyAccount kacc = m_parent->account("kbanking-acc-ref", QString("%1-%2").arg(ks.m_strRoutingNumber, ks.m_strAccountNumber)); ks.m_accountId = kacc.id(); // account name p = AB_ImExporterAccountInfo_GetAccountName(ai); if (p) ks.m_strAccountName = p; // account type switch (AB_ImExporterAccountInfo_GetType(ai)) { case AB_AccountType_Bank: ks.m_eType = eMyMoney::Statement::Type::Savings; break; case AB_AccountType_CreditCard: ks.m_eType = eMyMoney::Statement::Type::CreditCard; break; case AB_AccountType_Checking: ks.m_eType = eMyMoney::Statement::Type::Checkings; break; case AB_AccountType_Savings: ks.m_eType = eMyMoney::Statement::Type::Savings; break; case AB_AccountType_Investment: ks.m_eType = eMyMoney::Statement::Type::Investment; break; case AB_AccountType_Cash: default: ks.m_eType = eMyMoney::Statement::Type::None; } // account status const AB_ACCOUNT_STATUS* ast = _getAccountStatus(ai); if (ast) { const AB_BALANCE *bal; bal = AB_AccountStatus_GetBookedBalance(ast); if (!bal) bal = AB_AccountStatus_GetNotedBalance(ast); if (bal) { const AB_VALUE* val = AB_Balance_GetValue(bal); if (val) { DBG_INFO(0, "Importing balance"); ks.m_closingBalance = AB_Value_toMyMoneyMoney(val); p = AB_Value_GetCurrency(val); if (p) ks.m_strCurrency = p; } const GWEN_TIME* ti = AB_Balance_GetTime(bal); if (ti) { int year, month, day; if (!GWEN_Time_GetBrokenDownDate(ti, &day, &month, &year)) ks.m_dateEnd = QDate(year, month + 1, day); } else { DBG_WARN(0, "No time for balance"); } } else { DBG_WARN(0, "No account balance"); } } else { DBG_WARN(0, "No account status"); } // clear hash map m_hashMap.clear(); // get all transactions const AB_TRANSACTION* t = AB_ImExporterAccountInfo_GetFirstTransaction(ai); while (t) { _xaToStatement(ks, kacc, t); t = AB_ImExporterAccountInfo_GetNextTransaction(ai); } // import them if (!m_parent->importStatement(ks)) { if (KMessageBox::warningYesNo(0, i18n("Error importing statement. Do you want to continue?"), i18n("Critical Error")) == KMessageBox::No) { DBG_ERROR(0, "User aborted"); return false; } } return true; } K_PLUGIN_FACTORY_WITH_JSON(KBankingFactory, "kbanking.json", registerPlugin();) #include "kbanking.moc" diff --git a/kmymoney/plugins/ofx/import/ofximporter.cpp b/kmymoney/plugins/ofx/import/ofximporter.cpp index beb3df440..701f77d70 100644 --- a/kmymoney/plugins/ofx/import/ofximporter.cpp +++ b/kmymoney/plugins/ofx/import/ofximporter.cpp @@ -1,888 +1,906 @@ /* * Copyright 2005 Ace Jones acejones@users.sourceforge.net * Copyright 2010-2018 Thomas Baumgart tbaumgart@kde.org * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ +#include #include "ofximporter.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include #include "konlinebankingstatus.h" #include "konlinebankingsetupwizard.h" #include "kofxdirectconnectdlg.h" #include "mymoneyaccount.h" #include "mymoneyexception.h" #include "mymoneystatement.h" #include "mymoneystatementreader.h" #include "statementinterface.h" #include "importinterface.h" #include "viewinterface.h" #include "ui_importoption.h" #include "kmymoneyutils.h" //#define DEBUG_LIBOFX +#ifdef IS_APPIMAGE +#include +#include +#endif + using KWallet::Wallet; class OFXImporter::Private { public: Private() : m_valid(false), m_preferName(PreferId), m_walletIsOpen(false), m_statusDlg(0), m_wallet(0), m_updateStartDate(QDate(1900,1,1)), m_timestampOffset(0) {} bool m_valid; enum NamePreference { PreferId = 0, PreferName, PreferMemo } m_preferName; bool m_walletIsOpen; QList m_statementlist; QList m_securitylist; QString m_fatalerror; QStringList m_infos; QStringList m_warnings; QStringList m_errors; KOnlineBankingStatus* m_statusDlg; Wallet *m_wallet; QDate m_updateStartDate; int m_timestampOffset; }; OFXImporter::OFXImporter(QObject *parent, const QVariantList &args) : KMyMoneyPlugin::Plugin(parent, "ofximporter"), /* * the string in the line above must be the same as * X-KDE-PluginInfo-Name and the provider name assigned in * OfxImporterPlugin::onlineBankingSettings() */ KMyMoneyPlugin::ImporterPlugin(), d(new Private) { Q_UNUSED(args) - setComponentName(QStringLiteral("ofximporter"), i18n("OFX Importer")); - setXMLFile(QStringLiteral("ofximporter.rc")); + const auto componentName = QLatin1String("ofximporter"); + const auto rcFileName = QLatin1String("ofximporter.rc"); + setComponentName(componentName, i18n("OFX Importer")); + +#ifdef IS_APPIMAGE + const QString rcFilePath = QCoreApplication::applicationDirPath() + QLatin1String("/../share/kxmlgui5/") + componentName + QLatin1Char('/') + rcFileName; + setXMLFile(rcFilePath); + + const QString localRcFilePath = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation).first() + QLatin1Char('/') + componentName + QLatin1Char('/') + rcFileName; + setLocalXMLFile(localRcFilePath); +#else + setXMLFile(rcFileName); +#endif + createActions(); // For ease announce that we have been loaded. qDebug("Plugins: ofximporter loaded"); } OFXImporter::~OFXImporter() { delete d; qDebug("Plugins: ofximporter unloaded"); } void OFXImporter::createActions() { const auto &kpartgui = QStringLiteral("file_import_ofx"); auto a = actionCollection()->addAction(kpartgui); a->setText(i18n("OFX...")); connect(a, &QAction::triggered, this, static_cast(&OFXImporter::slotImportFile)); connect(viewInterface(), &KMyMoneyPlugin::ViewInterface::viewStateChanged, action(qPrintable(kpartgui)), &QAction::setEnabled); } void OFXImporter::slotImportFile() { QWidget * widget = new QWidget; Ui_ImportOption* option = new Ui_ImportOption; option->setupUi(widget); QUrl url = importInterface()->selectFile(i18n("OFX import file selection"), QString(), QStringLiteral("*.ofx *.qfx *.ofc|OFX files (*.ofx *.qfx *.ofc);;*|All files (*)"), QFileDialog::ExistingFile, widget); d->m_preferName = static_cast(option->m_preferName->currentIndex()); if (url.isValid()) { if (isMyFormat(url.path())) { statementInterface()->resetMessages(); slotImportFile(url.path()); statementInterface()->showMessages(d->m_statementlist.count()); } else { KMessageBox::error(0, i18n("Unable to import %1 using the OFX importer plugin. This file is not the correct format.", url.toDisplayString()), i18n("Incorrect format")); } } delete option; delete widget; } QString OFXImporter::formatName() const { return QStringLiteral("OFX"); } QString OFXImporter::formatFilenameFilter() const { return QStringLiteral("*.ofx *.qfx *.ofc"); } bool OFXImporter::isMyFormat(const QString& filename) const { // filename is considered an Ofx file if it contains // the tag "" or "" in the first 20 lines. // which contain some data bool result = false; QFile f(filename); if (f.open(QIODevice::ReadOnly | QIODevice::Text)) { QTextStream ts(&f); int lineCount = 20; while (!ts.atEnd() && !result && lineCount != 0) { // get a line of data and remove all unnecessary whitepace chars QString line = ts.readLine().simplified(); if (line.contains(QStringLiteral(""), Qt::CaseInsensitive) || line.contains(QStringLiteral(""), Qt::CaseInsensitive)) result = true; // count only lines that contain some non white space chars if (!line.isEmpty()) lineCount--; } f.close(); } return result; } bool OFXImporter::import(const QString& filename) { d->m_fatalerror = i18n("Unable to parse file"); d->m_valid = false; d->m_errors.clear(); d->m_warnings.clear(); d->m_infos.clear(); d->m_statementlist.clear(); d->m_securitylist.clear(); QByteArray filename_deep = QFile::encodeName(filename); ofx_STATUS_msg = true; ofx_INFO_msg = true; ofx_WARNING_msg = true; ofx_ERROR_msg = true; #ifdef DEBUG_LIBOFX ofx_PARSER_msg = true; ofx_DEBUG_msg = true; ofx_DEBUG1_msg = true; ofx_DEBUG2_msg = true; ofx_DEBUG3_msg = true; ofx_DEBUG4_msg = true; ofx_DEBUG5_msg = true; #endif LibofxContextPtr ctx = libofx_get_new_context(); Q_CHECK_PTR(ctx); // Don't show the position that caused a message to be shown // This has no setter (see libofx.h) ofx_show_position = false; qDebug("setup callback routines"); ofx_set_transaction_cb(ctx, ofxTransactionCallback, this); ofx_set_statement_cb(ctx, ofxStatementCallback, this); ofx_set_account_cb(ctx, ofxAccountCallback, this); ofx_set_security_cb(ctx, ofxSecurityCallback, this); ofx_set_status_cb(ctx, ofxStatusCallback, this); qDebug("process data"); libofx_proc_file(ctx, filename_deep, AUTODETECT); qDebug("process data done"); libofx_free_context(ctx); if (d->m_valid) { d->m_fatalerror.clear(); d->m_valid = storeStatements(d->m_statementlist); } return d->m_valid; } QString OFXImporter::lastError() const { if (d->m_errors.count() == 0) return d->m_fatalerror; return d->m_errors.join(QStringLiteral("

")); } /* __________________________________________________________________________ * AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA * * Static callbacks for LibOFX * * YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY */ int OFXImporter::ofxTransactionCallback(struct OfxTransactionData data, void * pv) { // kDebug(2) << Q_FUNC_INFO; OFXImporter* pofx = reinterpret_cast(pv); MyMoneyStatement& s = pofx->back(); MyMoneyStatement::Transaction t; if (data.date_posted_valid) { QDateTime dt; dt.setTime_t(data.date_posted - pofx->d->m_timestampOffset * 60); t.m_datePosted = dt.date(); } else if (data.date_initiated_valid) { QDateTime dt; dt.setTime_t(data.date_initiated - pofx->d->m_timestampOffset * 60); t.m_datePosted = dt.date(); } if (t.m_datePosted.isValid()) { // verify the transaction date is one we want if (t.m_datePosted < pofx->d->m_updateStartDate) { //kDebug(0) << "discarding transaction dated" << qPrintable(t.m_datePosted.toString(Qt::ISODate)); return 0; } } if (data.amount_valid) { t.m_amount = MyMoneyMoney(data.amount, 1000); } if (data.check_number_valid) { t.m_strNumber = QString::fromUtf8(data.check_number); } if (data.fi_id_valid) { t.m_strBankID = QStringLiteral("ID ") + QString::fromUtf8(data.fi_id); } else if (data.reference_number_valid) { t.m_strBankID = QStringLiteral("REF ") + QString::fromUtf8(data.reference_number); } // Decide whether to use NAME, PAYEEID or MEMO to construct the payee bool validity[3] = {false, false, false}; QStringList values; switch (pofx->d->m_preferName) { case OFXImporter::Private::PreferId: // PAYEEID default: validity[0] = data.payee_id_valid; validity[1] = data.name_valid; validity[2] = data.memo_valid; values += QString::fromUtf8(data.payee_id); values += QString::fromUtf8(data.name); values += QString::fromUtf8(data.memo); break; case OFXImporter::Private::PreferName: // NAME validity[0] = data.name_valid; validity[1] = data.payee_id_valid; validity[2] = data.memo_valid; values += QString::fromUtf8(data.name); values += QString::fromUtf8(data.payee_id); values += QString::fromUtf8(data.memo); break; case OFXImporter::Private::PreferMemo: // MEMO validity[0] = data.memo_valid; validity[1] = data.payee_id_valid; validity[2] = data.name_valid; values += QString::fromUtf8(data.memo); values += QString::fromUtf8(data.payee_id); values += QString::fromUtf8(data.name); break; } // for investment transactions we don't use the meme as payee if (data.invtransactiontype_valid) { values.clear(); validity[0] = data.payee_id_valid; validity[1] = data.name_valid; validity[2] = false; values += QString::fromUtf8(data.payee_id); values += QString::fromUtf8(data.name); } for (int idx = 0; idx < 3; ++idx) { if (validity[idx]) { t.m_strPayee = values[idx]; break; } } // extract memo field if we haven't used it as payee if ((data.memo_valid) && (pofx->d->m_preferName != OFXImporter::Private::PreferMemo)) { t.m_strMemo = QString::fromUtf8(data.memo); } // If the payee or memo fields are blank, set them to // the other one which is NOT blank. (acejones) if (t.m_strPayee.isEmpty()) { // But we only create a payee for non-investment transactions (ipwizard) if (! t.m_strMemo.isEmpty() && data.invtransactiontype_valid == false) t.m_strPayee = t.m_strMemo; } else { if (t.m_strMemo.isEmpty()) t.m_strMemo = t.m_strPayee; } if (data.security_data_valid) { struct OfxSecurityData* secdata = data.security_data_ptr; if (secdata->ticker_valid) { t.m_strSymbol = QString::fromUtf8(secdata->ticker); } if (secdata->secname_valid) { t.m_strSecurity = QString::fromUtf8(secdata->secname); } } t.m_shares = MyMoneyMoney(); if (data.units_valid) { t.m_shares = MyMoneyMoney(data.units, 100000).reduce(); } t.m_price = MyMoneyMoney(); if (data.unitprice_valid) { t.m_price = MyMoneyMoney(data.unitprice, 100000).reduce(); } t.m_fees = MyMoneyMoney(); if (data.fees_valid) { t.m_fees += MyMoneyMoney(data.fees, 1000).reduce(); } if (data.commission_valid) { t.m_fees += MyMoneyMoney(data.commission, 1000).reduce(); } bool unhandledtype = false; QString type; if (data.invtransactiontype_valid) { switch (data.invtransactiontype) { case OFX_BUYDEBT: case OFX_BUYMF: case OFX_BUYOPT: case OFX_BUYOTHER: case OFX_BUYSTOCK: t.m_eAction = eMyMoney::Transaction::Action::Buy; break; case OFX_REINVEST: t.m_eAction = eMyMoney::Transaction::Action::ReinvestDividend; break; case OFX_SELLDEBT: case OFX_SELLMF: case OFX_SELLOPT: case OFX_SELLOTHER: case OFX_SELLSTOCK: t.m_eAction = eMyMoney::Transaction::Action::Sell; break; case OFX_INCOME: t.m_eAction = eMyMoney::Transaction::Action::CashDividend; // NOTE: With CashDividend, the amount of the dividend should // be in data.amount. Since I've never seen an OFX file with // cash dividends, this is an assumption on my part. (acejones) break; // // These types are all not handled. We will generate a warning for them. // case OFX_CLOSUREOPT: unhandledtype = true; type = QStringLiteral("CLOSUREOPT (Close a position for an option)"); break; case OFX_INVEXPENSE: unhandledtype = true; type = QStringLiteral("INVEXPENSE (Misc investment expense that is associated with a specific security)"); break; case OFX_JRNLFUND: unhandledtype = true; type = QStringLiteral("JRNLFUND (Journaling cash holdings between subaccounts within the same investment account)"); break; case OFX_MARGININTEREST: unhandledtype = true; type = QStringLiteral("MARGININTEREST (Margin interest expense)"); break; case OFX_RETOFCAP: unhandledtype = true; type = QStringLiteral("RETOFCAP (Return of capital)"); break; case OFX_SPLIT: unhandledtype = true; type = QStringLiteral("SPLIT (Stock or mutial fund split)"); break; case OFX_TRANSFER: unhandledtype = true; type = QStringLiteral("TRANSFER (Transfer holdings in and out of the investment account)"); break; default: unhandledtype = true; type = QString("UNKNOWN %1").arg(data.invtransactiontype); break; } } else t.m_eAction = eMyMoney::Transaction::Action::None; // In the case of investment transactions, the 'total' is supposed to the total amount // of the transaction. units * unitprice +/- commission. Easy, right? Sadly, it seems // some ofx creators do not follow this in all circumstances. Therefore, we have to double- // check the total here and adjust it if it's wrong. #if 0 // Even more sadly, this logic is BROKEN. It consistently results in bogus total // values, because of rounding errors in the price. A more through solution would // be to test if the commission alone is causing a discrepancy, and adjust in that case. if (data.invtransactiontype_valid && data.unitprice_valid) { double proper_total = t.m_dShares * data.unitprice + t.m_moneyFees; if (proper_total != t.m_moneyAmount) { pofx->addWarning(QString("Transaction %1 has an incorrect total of %2. Using calculated total of %3 instead.").arg(t.m_strBankID).arg(t.m_moneyAmount).arg(proper_total)); t.m_moneyAmount = proper_total; } } #endif if (unhandledtype) pofx->addWarning(QString("Transaction %1 has an unsupported type (%2).").arg(t.m_strBankID, type)); else s.m_listTransactions += t; // kDebug(2) << Q_FUNC_INFO << "return 0 "; return 0; } int OFXImporter::ofxStatementCallback(struct OfxStatementData data, void* pv) { // kDebug(2) << Q_FUNC_INFO; OFXImporter* pofx = reinterpret_cast(pv); MyMoneyStatement& s = pofx->back(); pofx->setValid(); if (data.currency_valid) { s.m_strCurrency = QString::fromUtf8(data.currency); } if (data.account_id_valid) { s.m_strAccountNumber = QString::fromUtf8(data.account_id); } if (data.date_start_valid) { QDateTime dt; dt.setTime_t(data.date_start - pofx->d->m_timestampOffset * 60); s.m_dateBegin = dt.date(); } if (data.date_end_valid) { QDateTime dt; dt.setTime_t(data.date_end - pofx->d->m_timestampOffset * 60); s.m_dateEnd = dt.date(); } if (data.ledger_balance_valid && data.ledger_balance_date_valid) { s.m_closingBalance = MyMoneyMoney(data.ledger_balance); QDateTime dt; dt.setTime_t(data.ledger_balance_date); s.m_dateEnd = dt.date(); } // kDebug(2) << Q_FUNC_INFO << " return 0"; return 0; } int OFXImporter::ofxAccountCallback(struct OfxAccountData data, void * pv) { // kDebug(2) << Q_FUNC_INFO; OFXImporter* pofx = reinterpret_cast(pv); pofx->addnew(); MyMoneyStatement& s = pofx->back(); // Having any account at all makes an ofx statement valid pofx->d->m_valid = true; if (data.account_id_valid) { s.m_strAccountName = QString::fromUtf8(data.account_name); s.m_strAccountNumber = QString::fromUtf8(data.account_id); } if (data.bank_id_valid) { s.m_strRoutingNumber = QString::fromUtf8(data.bank_id); } if (data.broker_id_valid) { s.m_strRoutingNumber = QString::fromUtf8(data.broker_id); } if (data.currency_valid) { s.m_strCurrency = QString::fromUtf8(data.currency); } if (data.account_type_valid) { switch (data.account_type) { case OfxAccountData::OFX_CHECKING : s.m_eType = eMyMoney::Statement::Type::Checkings; break; case OfxAccountData::OFX_SAVINGS : s.m_eType = eMyMoney::Statement::Type::Savings; break; case OfxAccountData::OFX_MONEYMRKT : s.m_eType = eMyMoney::Statement::Type::Investment; break; case OfxAccountData::OFX_CREDITLINE : s.m_eType = eMyMoney::Statement::Type::CreditCard; break; case OfxAccountData::OFX_CMA : s.m_eType = eMyMoney::Statement::Type::CreditCard; break; case OfxAccountData::OFX_CREDITCARD : s.m_eType = eMyMoney::Statement::Type::CreditCard; break; case OfxAccountData::OFX_INVESTMENT : s.m_eType = eMyMoney::Statement::Type::Investment; break; } } // ask KMyMoney for an account id s.m_accountId = pofx->account(QStringLiteral("kmmofx-acc-ref"), QString("%1-%2").arg(s.m_strRoutingNumber, s.m_strAccountNumber)).id(); // copy over the securities s.m_listSecurities = pofx->d->m_securitylist; // kDebug(2) << Q_FUNC_INFO << " return 0"; return 0; } int OFXImporter::ofxSecurityCallback(struct OfxSecurityData data, void* pv) { // kDebug(2) << Q_FUNC_INFO; OFXImporter* pofx = reinterpret_cast(pv); MyMoneyStatement::Security sec; if (data.unique_id_valid) { sec.m_strId = QString::fromUtf8(data.unique_id); } if (data.secname_valid) { sec.m_strName = QString::fromUtf8(data.secname); } if (data.ticker_valid) { sec.m_strSymbol = QString::fromUtf8(data.ticker); } pofx->d->m_securitylist += sec; return 0; } int OFXImporter::ofxStatusCallback(struct OfxStatusData data, void * pv) { // kDebug(2) << Q_FUNC_INFO; OFXImporter* pofx = reinterpret_cast(pv); QString message; // if we got this far, we know we were able to parse the file. // so if it fails after here it can only because there were no actual // accounts in the file! pofx->d->m_fatalerror = i18n("No accounts found."); if (data.ofx_element_name_valid) message.prepend(QString("%1: ").arg(QString::fromUtf8(data.ofx_element_name))); if (data.code_valid) message += QString("%1 (Code %2): %3").arg(QString::fromUtf8(data.name)).arg(data.code).arg(QString::fromUtf8(data.description)); if (data.server_message_valid) message += QString(" (%1)").arg(QString::fromUtf8(data.server_message)); if (data.severity_valid) { switch (data.severity) { case OfxStatusData::INFO: pofx->addInfo(message); break; case OfxStatusData::ERROR: pofx->addError(message); break; case OfxStatusData::WARN: pofx->addWarning(message); break; default: pofx->addWarning(message); pofx->addWarning(QStringLiteral("Previous message was an unknown type. 'WARNING' was assumed.")); break; } } // kDebug(2) << Q_FUNC_INFO << " return 0 "; return 0; } QStringList OFXImporter::importStatement(const MyMoneyStatement &s) { qDebug("OfxImporterPlugin::importStatement start"); return statementInterface()->import(s, false); } MyMoneyAccount OFXImporter::account(const QString& key, const QString& value) const { return statementInterface()->account(key, value); } void OFXImporter::protocols(QStringList& protocolList) const { protocolList.clear(); protocolList << QStringLiteral("OFX"); } QWidget* OFXImporter::accountConfigTab(const MyMoneyAccount& acc, QString& name) { name = i18n("Online settings"); d->m_statusDlg = new KOnlineBankingStatus(acc, 0); return d->m_statusDlg; } MyMoneyKeyValueContainer OFXImporter::onlineBankingSettings(const MyMoneyKeyValueContainer& current) { MyMoneyKeyValueContainer kvp(current); // keep the provider name in sync with the one found in kmm_ofximport.desktop kvp[QStringLiteral("provider")] = objectName().toLower(); if (d->m_statusDlg) { kvp.deletePair(QStringLiteral("appId")); kvp.deletePair(QStringLiteral("kmmofx-headerVersion")); kvp.deletePair(QStringLiteral("password")); d->m_wallet = openSynchronousWallet(); if (d->m_wallet && (d->m_wallet->hasFolder(KWallet::Wallet::PasswordFolder()) || d->m_wallet->createFolder(KWallet::Wallet::PasswordFolder())) && d->m_wallet->setFolder(KWallet::Wallet::PasswordFolder())) { QString key = OFX_PASSWORD_KEY(kvp.value(QStringLiteral("url")), kvp.value(QStringLiteral("uniqueId"))); if (d->m_statusDlg->m_storePassword->isChecked()) { d->m_wallet->writePassword(key, d->m_statusDlg->m_password->text()); } else { if (d->m_wallet->hasEntry(key)) { d->m_wallet->removeEntry(key); } } } else { if (d->m_statusDlg->m_storePassword->isChecked()) { kvp.setValue(QStringLiteral("password"), d->m_statusDlg->m_password->text()); } } if (!d->m_statusDlg->appId().isEmpty()) kvp.setValue(QStringLiteral("appId"), d->m_statusDlg->appId()); kvp.setValue(QStringLiteral("kmmofx-headerVersion"), d->m_statusDlg->headerVersion()); kvp.setValue(QStringLiteral("kmmofx-numRequestDays"), QString::number(d->m_statusDlg->m_numdaysSpin->value())); kvp.setValue(QStringLiteral("kmmofx-todayMinus"), QString::number(d->m_statusDlg->m_todayRB->isChecked())); kvp.setValue(QStringLiteral("kmmofx-lastUpdate"), QString::number(d->m_statusDlg->m_lastUpdateRB->isChecked())); kvp.setValue(QStringLiteral("kmmofx-pickDate"), QString::number(d->m_statusDlg->m_pickDateRB->isChecked())); kvp.setValue(QStringLiteral("kmmofx-specificDate"), d->m_statusDlg->m_specificDate->date().toString()); kvp.setValue(QStringLiteral("kmmofx-preferName"), QString::number(d->m_statusDlg->m_preferredPayee->currentIndex())); if (!d->m_statusDlg->m_clientUidEdit->text().isEmpty()) kvp.setValue(QStringLiteral("clientUid"), d->m_statusDlg->m_clientUidEdit->text()); else kvp.deletePair(QStringLiteral("clientUid")); if (d->m_statusDlg->m_timestampOffset->time().msecsSinceStartOfDay() == 0) { kvp.deletePair(QStringLiteral("kmmofx-timestampOffset")); } else { // get offset in minutes int offset = d->m_statusDlg->m_timestampOffset->time().msecsSinceStartOfDay() / 1000 / 60; if (d->m_statusDlg->m_timestampOffsetSign->currentText() == QStringLiteral("-")) { offset = -offset; } kvp.setValue(QStringLiteral("kmmofx-timestampOffset"), QString::number(offset)); } // get rid of pre 4.6 values kvp.deletePair(QStringLiteral("kmmofx-preferPayeeid")); } return kvp; } bool OFXImporter::mapAccount(const MyMoneyAccount& acc, MyMoneyKeyValueContainer& settings) { Q_UNUSED(acc); bool rc = false; QPointer wiz = new KOnlineBankingSetupWizard(0); if (wiz->isInit()) { if (wiz->exec() == QDialog::Accepted) { rc = wiz->chosenSettings(settings); } } delete wiz; return rc; } bool OFXImporter::updateAccount(const MyMoneyAccount& acc, bool moreAccounts) { Q_UNUSED(moreAccounts); qDebug("OfxImporterPlugin::updateAccount"); try { if (!acc.id().isEmpty()) { // Save the value of preferName to be used by ofxTransactionCallback d->m_preferName = static_cast(acc.onlineBankingSettings().value(QStringLiteral("kmmofx-preferName")).toInt()); QPointer dlg = new KOfxDirectConnectDlg(acc); connect(dlg.data(), &KOfxDirectConnectDlg::statementReady, this, static_cast(&OFXImporter::slotImportFile)); // get the date of the earliest transaction that we are interested in // from the settings for this account MyMoneyKeyValueContainer settings = acc.onlineBankingSettings(); if (!settings.value(QStringLiteral("provider")).isEmpty()) { if ((settings.value(QStringLiteral("kmmofx-todayMinus")).toInt() != 0) && !settings.value(QStringLiteral("kmmofx-numRequestDays")).isEmpty()) { //kDebug(0) << "start date = today minus"; d->m_updateStartDate = QDate::currentDate().addDays(-settings.value(QStringLiteral("kmmofx-numRequestDays")).toInt()); } else if ((settings.value(QStringLiteral("kmmofx-lastUpdate")).toInt() != 0) && !acc.value(QStringLiteral("lastImportedTransactionDate")).isEmpty()) { //kDebug(0) << "start date = last update"; d->m_updateStartDate = QDate::fromString(acc.value(QStringLiteral("lastImportedTransactionDate")), Qt::ISODate); } else if ((settings.value(QStringLiteral("kmmofx-pickDate")).toInt() != 0) && !settings.value(QStringLiteral("kmmofx-specificDate")).isEmpty()) { //kDebug(0) << "start date = pick date"; d->m_updateStartDate = QDate::fromString(settings.value(QStringLiteral("kmmofx-specificDate"))); } else { //kDebug(0) << "start date = today - 2 months"; d->m_updateStartDate = QDate::currentDate().addMonths(-2); } } d->m_timestampOffset = settings.value("kmmofx-timestampOffset").toInt(); //kDebug(0) << "ofx plugin: account" << acc.name() << "earliest transaction date to process =" << qPrintable(d->m_updateStartDate.toString(Qt::ISODate)); if (dlg->init()) dlg->exec(); delete dlg; // reset the earliest-interesting-transaction date to the non-specific account setting d->m_updateStartDate = QDate(1900,1,1); d->m_timestampOffset = 0; } } catch (const MyMoneyException &e) { KMessageBox::information(0 , i18n("Error connecting to bank: %1", QString::fromLatin1(e.what()))); } return false; } void OFXImporter::slotImportFile(const QString& url) { qDebug("OfxImporterPlugin::slotImportFile"); if (!import(url)) { KMessageBox::error(0, QString("%1").arg(i18n("

Unable to import '%1' using the OFX importer plugin. The plugin returned the following error:

%2

", url, lastError())), i18n("Importing error")); } } bool OFXImporter::storeStatements(const QList &statements) { if (statements.isEmpty()) return true; auto ok = true; auto abort = false; // FIXME Deal with warnings/errors coming back from plugins /*if ( ofx.errors().count() ) { if ( KMessageBox::warningContinueCancelList(this,i18n("The following errors were returned from your bank"),ofx.errors(),i18n("OFX Errors")) == KMessageBox::Cancel ) abort = true; } if ( ofx.warnings().count() ) { if ( KMessageBox::warningContinueCancelList(this,i18n("The following warnings were returned from your bank"),ofx.warnings(),i18n("OFX Warnings"),KStandardGuiItem::cont(),"ofxwarnings") == KMessageBox::Cancel ) abort = true; }*/ qDebug("OfxImporterPlugin::storeStatements() with %d statements called", statements.count()); for (const auto& statement : statements) { if (abort) break; if (importStatement(statement).isEmpty()) ok = false; } if (!ok) KMessageBox::error(nullptr, i18n("Importing process terminated unexpectedly."), i18n("Failed to import all statements.")); return ok; } void OFXImporter::addnew() { d->m_statementlist.push_back(MyMoneyStatement()); } MyMoneyStatement& OFXImporter::back() { return d->m_statementlist.back(); } bool OFXImporter::isValid() const { return d->m_valid; } void OFXImporter::setValid() { d->m_valid = true; } void OFXImporter::addInfo(const QString& _msg) { d->m_infos += _msg; } void OFXImporter::addWarning(const QString& _msg) { d->m_warnings += _msg; } void OFXImporter::addError(const QString& _msg) { d->m_errors += _msg; } const QStringList& OFXImporter::infos() const // krazy:exclude=spelling { return d->m_infos; } const QStringList& OFXImporter::warnings() const { return d->m_warnings; } const QStringList& OFXImporter::errors() const { return d->m_errors; } K_PLUGIN_FACTORY_WITH_JSON(OFXImporterFactory, "ofximporter.json", registerPlugin();) #include "ofximporter.moc" diff --git a/kmymoney/plugins/qif/export/qifexporter.cpp b/kmymoney/plugins/qif/export/qifexporter.cpp index ca14ab9a6..847fed300 100644 --- a/kmymoney/plugins/qif/export/qifexporter.cpp +++ b/kmymoney/plugins/qif/export/qifexporter.cpp @@ -1,85 +1,103 @@ /*************************************************************************** qifexporter.cpp ------------------- copyright : (C) 2017 by Łukasz Wojniłowicz email : lukasz.wojnilowicz@gmail.com ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ +#include #include "qifexporter.h" // ---------------------------------------------------------------------------- // QT Includes // ---------------------------------------------------------------------------- // KDE Includes #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "kexportdlg.h" #include "mymoneyqifwriter.h" #include "viewinterface.h" +#ifdef IS_APPIMAGE +#include +#include +#endif + QIFExporter::QIFExporter(QObject *parent, const QVariantList &args) : KMyMoneyPlugin::Plugin(parent, "qifexporter"/*must be the same as X-KDE-PluginInfo-Name*/) { Q_UNUSED(args); - setComponentName("qifexporter", i18n("QIF exporter")); - setXMLFile("qifexporter.rc"); + const auto componentName = QLatin1String("qifexporter"); + const auto rcFileName = QLatin1String("qifexporter.rc"); + setComponentName(componentName, i18n("QIF exporter")); + +#ifdef IS_APPIMAGE + const QString rcFilePath = QCoreApplication::applicationDirPath() + QLatin1String("/../share/kxmlgui5/") + componentName + QLatin1Char('/') + rcFileName; + setXMLFile(rcFilePath); + + const QString localRcFilePath = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation).first() + QLatin1Char('/') + componentName + QLatin1Char('/') + rcFileName; + setLocalXMLFile(localRcFilePath); +#else + setXMLFile(rcFileName); +#endif + createActions(); // For information, announce that we have been loaded. qDebug("Plugins: qifexporter loaded"); } QIFExporter::~QIFExporter() { qDebug("Plugins: qifexporter unloaded"); } void QIFExporter::createActions() { const auto &kpartgui = QStringLiteral("file_export_qif"); m_action = actionCollection()->addAction(kpartgui); m_action->setText(i18n("QIF...")); connect(m_action, &QAction::triggered, this, &QIFExporter::slotQifExport); connect(viewInterface(), &KMyMoneyPlugin::ViewInterface::viewStateChanged, action(qPrintable(kpartgui)), &QAction::setEnabled); } void QIFExporter::slotQifExport() { m_action->setEnabled(false); QPointer dlg = new KExportDlg(nullptr); if (dlg->exec() == QDialog::Accepted && dlg != nullptr) { // if (okToWriteFile(QUrl::fromLocalFile(dlg->filename()))) { MyMoneyQifWriter writer; connect(&writer, SIGNAL(signalProgress(int,int)), this, SLOT(slotStatusProgressBar(int,int))); writer.write(dlg->filename(), dlg->profile(), dlg->accountId(), dlg->accountSelected(), dlg->categorySelected(), dlg->startDate(), dlg->endDate()); // } } delete dlg; m_action->setEnabled(true); } K_PLUGIN_FACTORY_WITH_JSON(QIFExporterFactory, "qifexporter.json", registerPlugin();) #include "qifexporter.moc" diff --git a/kmymoney/plugins/qif/import/qifimporter.cpp b/kmymoney/plugins/qif/import/qifimporter.cpp index bbef21357..fb0557bde 100644 --- a/kmymoney/plugins/qif/import/qifimporter.cpp +++ b/kmymoney/plugins/qif/import/qifimporter.cpp @@ -1,111 +1,129 @@ /*************************************************************************** qifimporter.cpp ------------------- copyright : (C) 2017 by Łukasz Wojniłowicz email : lukasz.wojnilowicz@gmail.com ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ +#include #include "qifimporter.h" // ---------------------------------------------------------------------------- // QT Includes // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "kimportdlg.h" #include "mymoneyqifreader.h" #include "statementinterface.h" #include "viewinterface.h" +#ifdef IS_APPIMAGE +#include +#include +#endif + class MyMoneyStatement; QIFImporter::QIFImporter(QObject *parent, const QVariantList &args) : KMyMoneyPlugin::Plugin(parent, "qifimporter"/*must be the same as X-KDE-PluginInfo-Name*/) { Q_UNUSED(args); - setComponentName("qifimporter", i18n("QIF importer")); - setXMLFile("qifimporter.rc"); + const auto componentName = QLatin1String("qifimporter"); + const auto rcFileName = QLatin1String("qifimporter.rc"); + setComponentName(componentName, i18n("QIF importer")); + +#ifdef IS_APPIMAGE + const QString rcFilePath = QCoreApplication::applicationDirPath() + QLatin1String("/../share/kxmlgui5/csvexporter/") + rcFileName; + setXMLFile(rcFilePath); + + const QString localRcFilePath = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation).first() + QLatin1Char('/') + rcFileName; + setLocalXMLFile(localRcFilePath); +#else + setXMLFile(rcFileName); +#endif + createActions(); // For information, announce that we have been loaded. qDebug("Plugins: qifimporter loaded"); } QIFImporter::~QIFImporter() { qDebug("Plugins: qifimporter unloaded"); } void QIFImporter::createActions() { const auto &kpartgui = QStringLiteral("file_import_qif"); m_action = actionCollection()->addAction(kpartgui); m_action->setText(i18n("QIF...")); connect(m_action, &QAction::triggered, this, &QIFImporter::slotQifImport); connect(viewInterface(), &KMyMoneyPlugin::ViewInterface::viewStateChanged, action(qPrintable(kpartgui)), &QAction::setEnabled); } void QIFImporter::slotQifImport() { m_action->setEnabled(false); QPointer dlg = new KImportDlg(nullptr); if (dlg->exec() == QDialog::Accepted && dlg != nullptr) { m_qifReader = new MyMoneyQifReader; statementInterface()->resetMessages(); connect(m_qifReader, &MyMoneyQifReader::statementsReady, this, &QIFImporter::slotGetStatements); m_qifReader->setURL(dlg->file()); m_qifReader->setProfile(dlg->profile()); m_qifReader->setCategoryMapping(dlg->m_typeComboBox->currentIndex() == 0); const auto statementCount = m_qifReader->statementCount(); if (!m_qifReader->startImport()) delete m_qifReader; statementInterface()->showMessages(statementCount); } delete dlg; m_action->setEnabled(true); } bool QIFImporter::slotGetStatements(const QList &statements) { auto ret = true; QStringList importSummary; for (const auto& statement : statements) { const auto singleImportSummary = statementInterface()->import(statement); if (singleImportSummary.isEmpty()) ret = false; importSummary.append(singleImportSummary); } delete m_qifReader; if (!importSummary.isEmpty()) KMessageBox::informationList(nullptr, i18n("The statement has been processed with the following results:"), importSummary, i18n("Statement stats")); return ret; } K_PLUGIN_FACTORY_WITH_JSON(QIFImporterFactory, "qifimporter.json", registerPlugin();) #include "qifimporter.moc" diff --git a/kmymoney/plugins/sql/sqlstorage.cpp b/kmymoney/plugins/sql/sqlstorage.cpp index 1d6a0f6e3..b01b1278b 100644 --- a/kmymoney/plugins/sql/sqlstorage.cpp +++ b/kmymoney/plugins/sql/sqlstorage.cpp @@ -1,358 +1,376 @@ /*************************************************************************** sqlstorage.cpp ------------------- copyright : (C) 2018 by Łukasz Wojniłowicz email : lukasz.wojnilowicz@gmail.com ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ +#include #include "sqlstorage.h" #include #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "appinterface.h" #include "viewinterface.h" #include "kselectdatabasedlg.h" #include "kgeneratesqldlg.h" #include "mymoneyfile.h" #include "mymoneystoragesql.h" #include "mymoneyexception.h" #include "mymoneystoragemgr.h" #include "icons.h" #include "kmymoneysettings.h" #include "kmymoneyenums.h" +#ifdef IS_APPIMAGE +#include +#include +#endif + using namespace Icons; QUrlQuery SQLStorage::convertOldUrl(const QUrl& url) { const auto key = QLatin1String("driver"); // take care and convert some old url to their new counterpart QUrlQuery query(url); if (query.queryItemValue(key) == QLatin1String("QMYSQL3")) { // fix any old urls query.removeQueryItem(key); query.addQueryItem(key, QLatin1String("QMYSQL")); } else if (query.queryItemValue(key) == QLatin1String("QSQLITE3")) { query.removeQueryItem(key); query.addQueryItem(key, QLatin1String("QSQLITE")); } #ifdef ENABLE_SQLCIPHER // Reading unencrypted database with QSQLITE // while QSQLCIPHER is available causes crash. // QSQLCIPHER can read QSQLITE if (query.queryItemValue(key) == QLatin1String("QSQLITE")) { query.removeQueryItem(key); query.addQueryItem(key, QLatin1String("QSQLCIPHER")); } #endif return query; } SQLStorage::SQLStorage(QObject *parent, const QVariantList &args) : KMyMoneyPlugin::Plugin(parent, "sqlstorage"/*must be the same as X-KDE-PluginInfo-Name*/) { Q_UNUSED(args) - setComponentName("sqlstorage", i18n("SQL storage")); - setXMLFile("sqlstorage.rc"); + const auto componentName = QLatin1String("sqlstorage"); + const auto rcFileName = QLatin1String("sqlstorage.rc"); + setComponentName(componentName, i18n("SQL storage")); + +#ifdef IS_APPIMAGE + const QString rcFilePath = QCoreApplication::applicationDirPath() + QLatin1String("/../share/kxmlgui5/") + componentName + QLatin1Char('/') + rcFileName; + setXMLFile(rcFilePath); + + const QString localRcFilePath = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation).first() + QLatin1Char('/') + componentName + QLatin1Char('/') + rcFileName; + setLocalXMLFile(localRcFilePath); +#else + setXMLFile(rcFileName); +#endif + createActions(); // For information, announce that we have been loaded. qDebug("Plugins: sqlstorage loaded"); } SQLStorage::~SQLStorage() { qDebug("Plugins: sqlstorage unloaded"); } MyMoneyStorageMgr *SQLStorage::open(const QUrl &url) { if (url.scheme() != QLatin1String("sql")) return nullptr; auto storage = new MyMoneyStorageMgr; auto reader = std::make_unique(storage, url); QUrl dbURL(url); if (dbURL.password().isEmpty()) { // check if a password is needed. it may be if the URL came from the last/recent file list QPointer dialog = new KSelectDatabaseDlg(QIODevice::ReadWrite, dbURL); if (!dialog->checkDrivers()) { delete dialog; return nullptr; } QUrlQuery query = convertOldUrl(dbURL); // if we need to supply a password, then show the dialog // otherwise it isn't needed if ((query.queryItemValue("secure").toLower() == QLatin1String("yes")) && dbURL.password().isEmpty()) { if (dialog->exec() == QDialog::Accepted && dialog != nullptr) { dbURL = dialog->selectedURL(); } else { delete dialog; return nullptr; } } delete dialog; } bool retry = true; while (retry) { switch (reader->open(dbURL, QIODevice::ReadWrite)) { case 0: // opened okay retry = false; break; case 1: // permanent error KMessageBox::detailedError(nullptr, i18n("Cannot open database %1\n", dbURL.toDisplayString()), reader->lastError()); delete storage; return nullptr; case -1: // retryable error if (KMessageBox::warningYesNo(nullptr, reader->lastError(), PACKAGE) == KMessageBox::No) { delete storage; return nullptr; } else { QUrlQuery query(dbURL); const QString optionKey = QLatin1String("options"); QString options = query.queryItemValue(optionKey); if(!options.isEmpty()) { options += QLatin1Char(','); } options += QLatin1String("override"); query.removeQueryItem(QLatin1String("mode")); query.removeQueryItem(optionKey); query.addQueryItem(optionKey, options); dbURL.setQuery(query); } break; case 2: // bad password case 3: // unsupported operation delete storage; return nullptr; } } // single user mode; read some of the data into memory // FIXME - readFile no longer relevant? // tried removing it but then got no indication that loading was complete // also, didn't show home page // reader->setProgressCallback(&KMyMoneyView::progressCallback); if (!reader->readFile()) { KMessageBox::detailedError(nullptr, i18n("An unrecoverable error occurred while reading the database"), reader->lastError().toLatin1(), i18n("Database malfunction")); delete storage; return nullptr; } // reader->setProgressCallback(0); return storage; } bool SQLStorage::save(const QUrl &url) { auto rc = false; if (!appInterface()->fileOpen()) { KMessageBox::error(nullptr, i18n("Tried to access a file when it has not been opened")); return (rc); } auto writer = new MyMoneyStorageSql(MyMoneyFile::instance()->storage(), url); writer->open(url, QIODevice::WriteOnly); // writer->setProgressCallback(&KMyMoneyView::progressCallback); if (!writer->writeFile()) { KMessageBox::detailedError(nullptr, i18n("An unrecoverable error occurred while writing to the database.\n" "It may well be corrupt."), writer->lastError().toLatin1(), i18n("Database malfunction")); rc = false; } else { rc = true; } writer->setProgressCallback(0); delete writer; return rc; } bool SQLStorage::saveAs() { auto rc = false; QUrl oldUrl; // in event of it being a database, ensure that all data is read into storage for saveas if (appInterface()->isDatabase()) oldUrl = appInterface()->filenameURL().isEmpty() ? appInterface()->lastOpenedURL() : appInterface()->filenameURL(); QPointer dialog = new KSelectDatabaseDlg(QIODevice::WriteOnly); QUrl url = oldUrl; if (!dialog->checkDrivers()) { delete dialog; return rc; } while (oldUrl == url && dialog->exec() == QDialog::Accepted && dialog != 0) { url = dialog->selectedURL(); // If the protocol is SQL for the old and new, and the hostname and database names match // Let the user know that the current database cannot be saved on top of itself. if (url.scheme() == "sql" && oldUrl.scheme() == "sql" && oldUrl.host() == url.host() && QUrlQuery(oldUrl).queryItemValue("driver") == QUrlQuery(url).queryItemValue("driver") && oldUrl.path().right(oldUrl.path().length() - 1) == url.path().right(url.path().length() - 1)) { KMessageBox::sorry(nullptr, i18n("Cannot save to current database.")); } else { try { rc = saveAsDatabase(url); } catch (const MyMoneyException &e) { KMessageBox::sorry(nullptr, i18n("Cannot save to current database: %1", QString::fromLatin1(e.what()))); } } } delete dialog; if (rc) { //KRecentFilesAction *p = dynamic_cast(action("file_open_recent")); //if(p) appInterface()->addToRecentFiles(url); appInterface()->writeLastUsedFile(url.toDisplayString(QUrl::PreferLocalFile)); appInterface()->writeFilenameURL(url); } return rc; } eKMyMoney::StorageType SQLStorage::storageType() const { return eKMyMoney::StorageType::SQL; } QString SQLStorage::fileExtension() const { return i18n("Database files (*.db *.sql)"); } void SQLStorage::createActions() { m_openDBaction = actionCollection()->addAction("open_database"); m_openDBaction->setText(i18n("Open database...")); m_openDBaction->setIcon(Icons::get(Icon::SVNUpdate)); connect(m_openDBaction, &QAction::triggered, this, &SQLStorage::slotOpenDatabase); m_generateDB = actionCollection()->addAction("tools_generate_sql"); m_generateDB->setText(i18n("Generate Database SQL")); connect(m_generateDB, &QAction::triggered, this, &SQLStorage::slotGenerateSql); } void SQLStorage::slotOpenDatabase() { QPointer dialog = new KSelectDatabaseDlg(QIODevice::ReadWrite); if (!dialog->checkDrivers()) { delete dialog; return; } if (dialog->exec() == QDialog::Accepted && dialog != 0) { auto url = dialog->selectedURL(); QUrl newurl = url; if ((newurl.scheme() == QLatin1String("sql"))) { QUrlQuery query = convertOldUrl(newurl); newurl.setQuery(query); // check if a password is needed. it may be if the URL came from the last/recent file list dialog = new KSelectDatabaseDlg(QIODevice::ReadWrite, newurl); if (!dialog->checkDrivers()) { delete dialog; return; } // if we need to supply a password, then show the dialog // otherwise it isn't needed if ((query.queryItemValue("secure").toLower() == QLatin1String("yes")) && newurl.password().isEmpty()) { if (dialog->exec() == QDialog::Accepted && dialog != nullptr) { newurl = dialog->selectedURL(); } else { delete dialog; return; } } delete dialog; } appInterface()->slotFileOpenRecent(newurl); } delete dialog; } void SQLStorage::slotGenerateSql() { QPointer editor = new KGenerateSqlDlg(nullptr); editor->setObjectName("Generate Database SQL"); editor->exec(); delete editor; } bool SQLStorage::saveAsDatabase(const QUrl &url) { auto writer = new MyMoneyStorageSql(MyMoneyFile::instance()->storage(), url); auto canWrite = false; switch (writer->open(url, QIODevice::WriteOnly)) { case 0: canWrite = true; break; case -1: // dbase already has data, see if he wants to clear it out if (KMessageBox::warningContinueCancel(nullptr, i18n("Database contains data which must be removed before using Save As.\n" "Do you wish to continue?"), "Database not empty") == KMessageBox::Continue) { if (writer->open(url, QIODevice::WriteOnly, true) == 0) canWrite = true; } else { delete writer; return false; } break; case 2: // bad password case 3: // unsupported operation delete writer; return false; } if (canWrite) { delete writer; save(url); return true; } else { KMessageBox::detailedError(nullptr, i18n("Cannot open or create database %1.\n" "Retry Save As Database and click Help" " for further info.", url.toDisplayString()), writer->lastError()); delete writer; return false; } } K_PLUGIN_FACTORY_WITH_JSON(SQLStorageFactory, "sqlstorage.json", registerPlugin();) #include "sqlstorage.moc" diff --git a/kmymoney/plugins/weboob/weboob.cpp b/kmymoney/plugins/weboob/weboob.cpp index c6a89e6ca..7262c2541 100644 --- a/kmymoney/plugins/weboob/weboob.cpp +++ b/kmymoney/plugins/weboob/weboob.cpp @@ -1,223 +1,240 @@ /* * This file is part of KMyMoney, A Personal Finance Manager by KDE * Copyright (C) 2014-2015 Romain Bignon * Copyright (C) 2014-2015 Florent Fourcot * Copyright (C) 2016 Christian David * (C) 2017 by Łukasz Wojniłowicz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ +#include #include "weboob.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include // ---------------------------------------------------------------------------- // Project Includes #include "mapaccountwizard.h" #include "accountsettings.h" #include "weboobinterface.h" #include "mymoneyaccount.h" #include "mymoneykeyvaluecontainer.h" #include "mymoneystatement.h" #include "statementinterface.h" +#ifdef IS_APPIMAGE +#include +#include +#endif + class WeboobPrivate { public: WeboobPrivate() { } ~WeboobPrivate() { } WeboobInterface weboob; QFutureWatcher watcher; std::unique_ptr progress; AccountSettings* accountSettings; }; Weboob::Weboob(QObject *parent, const QVariantList &args) : KMyMoneyPlugin::Plugin(parent, "weboob"), d_ptr(new WeboobPrivate) { Q_UNUSED(args) - setComponentName("weboob", i18n("Weboob")); - setXMLFile("weboob.rc"); + const auto componentName = QLatin1String("weboob"); + const auto rcFileName = QLatin1String("weboob.rc"); + setComponentName(componentName, i18n("Weboob")); + +#ifdef IS_APPIMAGE + const QString rcFilePath = QCoreApplication::applicationDirPath() + QLatin1String("/../share/kxmlgui5/") + componentName + QLatin1Char('/') + rcFileName; + setXMLFile(rcFilePath); + + const QString localRcFilePath = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation).first() + QLatin1Char('/') + componentName + QLatin1Char('/') + rcFileName; + setLocalXMLFile(localRcFilePath); +#else + setXMLFile(rcFileName); +#endif qDebug("Plugins: weboob loaded"); } Weboob::~Weboob() { Q_D(Weboob); delete d; qDebug("Plugins: weboob unloaded"); } void Weboob::plug() { Q_D(Weboob); connect(&d->watcher, &QFutureWatcher::finished, this, &Weboob::gotAccount); } void Weboob::unplug() { Q_D(Weboob); disconnect(&d->watcher, &QFutureWatcher::finished, this, &Weboob::gotAccount); } void Weboob::protocols(QStringList& protocolList) const { protocolList << "weboob"; } QWidget* Weboob::accountConfigTab(const MyMoneyAccount& account, QString& tabName) { Q_D(Weboob); const MyMoneyKeyValueContainer& kvp = account.onlineBankingSettings(); tabName = i18n("Weboob configuration"); d->accountSettings = new AccountSettings(account, 0); d->accountSettings->loadUi(kvp); return d->accountSettings; } MyMoneyKeyValueContainer Weboob::onlineBankingSettings(const MyMoneyKeyValueContainer& current) { Q_D(Weboob); MyMoneyKeyValueContainer kvp(current); kvp["provider"] = objectName().toLower(); if (d->accountSettings) { d->accountSettings->loadKvp(kvp); } return kvp; } bool Weboob::mapAccount(const MyMoneyAccount& acc, MyMoneyKeyValueContainer& onlineBankingSettings) { Q_D(Weboob); Q_UNUSED(acc); bool rc = false; QPointer w = new MapAccountWizard(nullptr, &d->weboob); if (w->exec() == QDialog::Accepted && w != nullptr) { onlineBankingSettings.setValue("wb-backend", w->currentBackend()); onlineBankingSettings.setValue("wb-id", w->currentAccount()); onlineBankingSettings.setValue("wb-max", "0"); rc = true; } delete w; return rc; } bool Weboob::updateAccount(const MyMoneyAccount& kacc, bool moreAccounts) { Q_D(Weboob); Q_UNUSED(moreAccounts); QString bname = kacc.onlineBankingSettings().value("wb-backend"); QString id = kacc.onlineBankingSettings().value("wb-id"); QString max = kacc.onlineBankingSettings().value("wb-max"); d->progress = std::make_unique(nullptr); d->progress->setWindowTitle(i18n("Connecting to bank...")); d->progress->setLabelText(i18n("Retrieving transactions...")); d->progress->setModal(true); d->progress->setCancelButton(nullptr); d->progress->setMinimum(0); d->progress->setMaximum(0); d->progress->setMinimumDuration(0); QFuture future = QtConcurrent::run(&d->weboob, &WeboobInterface::getAccount, bname, id, max); d->watcher.setFuture(future); d->progress->exec(); d->progress.reset(); return true; } void Weboob::gotAccount() { Q_D(Weboob); WeboobInterface::Account acc = d->watcher.result(); MyMoneyAccount kacc = statementInterface()->account("wb-id", acc.id); MyMoneyStatement ks; ks.m_accountId = kacc.id(); ks.m_strAccountName = acc.name; ks.m_closingBalance = acc.balance; if (acc.transactions.length() > 0) ks.m_dateEnd = acc.transactions.front().date; #if 0 switch (acc.type) { case Weboob::Account::TYPE_CHECKING: ks.m_eType = MyMoneyStatement::etCheckings; break; case Weboob::Account::TYPE_SAVINGS: ks.m_eType = MyMoneyStatement::etSavings; break; case Weboob::Account::TYPE_MARKET: ks.m_eType = MyMoneyStatement::etInvestment; break; case Weboob::Account::TYPE_DEPOSIT: case Weboob::Account::TYPE_LOAN: case Weboob::Account::TYPE_JOINT: case Weboob::Account::TYPE_UNKNOWN: break; } #endif for (QListIterator it(acc.transactions); it.hasNext();) { WeboobInterface::Transaction tr = it.next(); MyMoneyStatement::Transaction kt; kt.m_strBankID = QLatin1String("ID ") + tr.id; kt.m_datePosted = tr.rdate; kt.m_amount = tr.amount; kt.m_strMemo = tr.raw; kt.m_strPayee = tr.label; ks.m_listTransactions += kt; } statementInterface()->import(ks); d->progress->hide(); } K_PLUGIN_FACTORY_WITH_JSON(WeboobFactory, "weboob.json", registerPlugin();) #include "weboob.moc" diff --git a/packaging/linux/appimage/build-image.sh b/packaging/linux/appimage/build-image.sh index bda3bfd29..46fbc8c34 100755 --- a/packaging/linux/appimage/build-image.sh +++ b/packaging/linux/appimage/build-image.sh @@ -1,85 +1,95 @@ #!/bin/bash # Halt on errors and be verbose about what we are doing set -e set -x # Read in our parameters export BUILD_PREFIX=$1 export KMYMONEY_SOURCES=$2 # Save some frequently referenced locations in variables for ease of use / updating export APPDIR=$BUILD_PREFIX/kmymoney.appdir -export PLUGINS=$APPDIR/usr/lib/plugins/kmymoney/ +export PLUGINS=$APPDIR/usr/lib/plugins/ +export APPIMAGEPLUGINS=$APPDIR/usr/plugins/ # qjsonparser, used to add metadata to the plugins needs to work in a en_US.UTF-8 environment. # That's not always the case, so make sure it is export LC_ALL=en_US.UTF-8 export LANG=en_us.UTF-8 # We want to use $prefix/deps/usr/ for all our dependencies export DEPS_INSTALL_PREFIX=$BUILD_PREFIX/deps/usr/ export DOWNLOADS_DIR=$BUILD_PREFIX/downloads/ # Setup variables needed to help everything find what we built export LD_LIBRARY_PATH=$DEPS_INSTALL_PREFIX/lib/:$DEPS_INSTALL_PREFIX/lib/x86_64-linux-gnu/:$APPDIR/usr/lib/:$LD_LIBRARY_PATH export PATH=$DEPS_INSTALL_PREFIX/bin/:$PATH export PKG_CONFIG_PATH=$DEPS_INSTALL_PREFIX/share/pkgconfig/:$DEPS_INSTALL_PREFIX/lib/pkgconfig/:/usr/lib/pkgconfig/:$PKG_CONFIG_PATH export CMAKE_PREFIX_PATH=$DEPS_INSTALL_PREFIX:$CMAKE_PREFIX_PATH # Switch over to our build prefix cd $BUILD_PREFIX # # Now we can get the process started! # # Step 0: place the translations where ki18n and Qt look for them if [ -d $APPDIR/usr/share/locale ] ; then mv $APPDIR/usr/share/locale $APPDIR/usr/share/kmymoney fi # Step 1: Copy over all the resources provided by dependencies that we need cp -r $DEPS_INSTALL_PREFIX/share/locale $APPDIR/usr/share/kmymoney cp -r $DEPS_INSTALL_PREFIX/share/kf5 $APPDIR/usr/share cp -r $DEPS_INSTALL_PREFIX/share/mime $APPDIR/usr/share cp -r $DEPS_INSTALL_PREFIX/translations $APPDIR/usr/ # Step 2: Relocate x64 binaries from the architecture specific directory as required for Appimages mv $APPDIR/usr/lib/x86_64-linux-gnu/* $APPDIR/usr/lib rm -rf $APPDIR/usr/lib/x86_64-linux-gnu/ # Step 3: Update the rpath in the various plugins we have to make sure they'll be loadable in an Appimage context -for lib in $PLUGINS/*.so*; do - patchelf --set-rpath '$ORIGIN/../..' $lib; +for lib in $PLUGINS/kmymoney/*.so*; do + patchelf --set-rpath '$ORIGIN/../../lib' $lib; done -# Step 4: Build the image!!! +# Step 4: Move plugins to loadable location in AppImage + +# Make sure our plugin directory already exists +if [ ! -d $APPIMAGEPLUGINS ] ; then + mkdir -p $APPIMAGEPLUGINS +fi + +mv $PLUGINS/* $APPIMAGEPLUGINS + +# Step 5: Build the image!!! linuxdeployqt $APPDIR/usr/share/applications/org.kde.kmymoney.desktop \ -executable=$APPDIR/usr/bin/kmymoney \ -qmldir=$DEPS_INSTALL_PREFIX/qml \ -verbose=2 \ -bundle-non-qt-libs \ -appimage \ -exclude-libs=libnss3.so,libnssutil3.so # Step 5: Find out what version of KMyMoney we built and give the Appimage a proper name cd $BUILD_PREFIX/kmymoney-build KMYMONEY_VERSION=$(grep "KMyMoney VERSION" CMakeLists.txt | cut -d '"' -f 2) # Also find out the revision of Git we built # Then use that to generate a combined name we'll distribute cd $KMYMONEY_SOURCES if [[ -d .git ]]; then GIT_REVISION=$(git rev-parse --short HEAD) VERSION=$KMYMONEY_VERSION-$GIT_REVISION else VERSION=$KMYMONEY_VERSION fi # Return to our build root cd $BUILD_PREFIX # Generate a new name for the Appimage file and rename it accordingly APPIMAGE=kmymoney-"$VERSION"-x86_64.appimage mv KMyMoney-x86_64.AppImage $APPIMAGE