File indexing completed on 2024-04-28 05:06:07

0001 /*
0002     SPDX-FileCopyrightText: 2000-2003 Michael Edwardes <mte@users.sourceforge.net>
0003     SPDX-FileCopyrightText: 2000-2003 Javier Campos Morales <javi_c@users.sourceforge.net>
0004     SPDX-FileCopyrightText: 2000-2003 Felix Rodriguez <frodriguez@users.sourceforge.net>
0005     SPDX-FileCopyrightText: 2000-2003 John C <thetacoturtle@users.sourceforge.net>
0006     SPDX-FileCopyrightText: 2000-2003 Thomas Baumgart <ipwizard@users.sourceforge.net>
0007     SPDX-FileCopyrightText: 2000-2003 Kevin Tambascio <ktambascio@users.sourceforge.net>
0008     SPDX-FileCopyrightText: 2017 Łukasz Wojniłowicz <lukasz.wojnilowicz@gmail.com>
0009     SPDX-License-Identifier: GPL-2.0-or-later
0010 */
0011 
0012 #include "kmymoneyutils.h"
0013 
0014 // ----------------------------------------------------------------------------
0015 // QT Includes
0016 
0017 #include <QAbstractButton>
0018 #include <QApplication>
0019 #include <QBitArray>
0020 #include <QFileInfo>
0021 #include <QGroupBox>
0022 #include <QIcon>
0023 #include <QList>
0024 #include <QPainter>
0025 #include <QPixmap>
0026 #include <QPixmapCache>
0027 #include <QRegularExpression>
0028 #include <QRegularExpressionMatch>
0029 #include <QTemporaryFile>
0030 #include <QWidget>
0031 #include <QWizard>
0032 #include <amountedit.h>
0033 #include <creditdebitedit.h>
0034 
0035 // ----------------------------------------------------------------------------
0036 // KDE Headers
0037 
0038 #include <KColorScheme>
0039 #include <KGuiItem>
0040 #include <KIO/StatJob>
0041 #include <KIO/StoredTransferJob>
0042 #include <KLazyLocalizedString>
0043 #include <KLocalizedString>
0044 #include <KMessageBox>
0045 #include <KStandardGuiItem>
0046 #include <KXmlGuiWindow>
0047 #include <kio_version.h>
0048 
0049 // ----------------------------------------------------------------------------
0050 // Project Includes
0051 
0052 #include "mymoneymoney.h"
0053 #include "mymoneyexception.h"
0054 #include "mymoneytransactionfilter.h"
0055 #include "mymoneyfile.h"
0056 #include "mymoneyaccount.h"
0057 #include "mymoneysecurity.h"
0058 #include "mymoneyschedule.h"
0059 #include "mymoneypayee.h"
0060 #include "mymoneytag.h"
0061 #include "mymoneyprice.h"
0062 #include "mymoneystatement.h"
0063 #include "mymoneyforecast.h"
0064 #include "mymoneysplit.h"
0065 #include "mymoneytransaction.h"
0066 #include "kmymoneysettings.h"
0067 #include "icons.h"
0068 #include "storageenums.h"
0069 #include "mymoneyenums.h"
0070 #include "kmymoneyplugin.h"
0071 #include "statusmodel.h"
0072 #include "journalmodel.h"
0073 #include "splitmodel.h"
0074 #include "accountsmodel.h"
0075 
0076 #include "kmmyesno.h"
0077 
0078 using namespace Icons;
0079 
0080 const QString KMyMoneyUtils::paymentMethodToString(eMyMoney::Schedule::PaymentType paymentType)
0081 {
0082     return i18n(MyMoneySchedule::paymentMethodToString(paymentType));
0083 }
0084 
0085 const QString KMyMoneyUtils::weekendOptionToString(eMyMoney::Schedule::WeekendOption weekendOption)
0086 {
0087     return i18n(MyMoneySchedule::weekendOptionToString(weekendOption).toLatin1());
0088 }
0089 
0090 const QString KMyMoneyUtils::scheduleTypeToString(eMyMoney::Schedule::Type type)
0091 {
0092     return i18nc("Scheduled transaction type", MyMoneySchedule::scheduleTypeToString(type).toLatin1());
0093 }
0094 
0095 KGuiItem KMyMoneyUtils::scheduleNewGuiItem()
0096 {
0097     KGuiItem splitGuiItem(i18n("&New Schedule..."),
0098                           Icons::get(Icon::DocumentNew),
0099                           i18n("Create a new schedule."),
0100                           i18n("Use this to create a new schedule."));
0101 
0102     return splitGuiItem;
0103 }
0104 
0105 KGuiItem KMyMoneyUtils::accountsFilterGuiItem()
0106 {
0107     KGuiItem splitGuiItem(i18n("&Filter"),
0108                           Icons::get(Icon::Filter),
0109                           i18n("Filter out accounts"),
0110                           i18n("Use this to filter out accounts"));
0111 
0112     return splitGuiItem;
0113 }
0114 
0115 const char* homePageItems[] = {
0116     kli18n("Scheduled payments").untranslatedText(),
0117     kli18n("Preferred accounts").untranslatedText(),
0118     kli18n("Payment accounts").untranslatedText(),
0119     kli18n("Favorite reports").untranslatedText(),
0120     kli18n("Forecast (schedule)").untranslatedText(),
0121     kli18n("Net worth forecast").untranslatedText(),
0122     kli18n("Forecast (history)").untranslatedText(), // unused, s.a. KSettingsHome::slotLoadItems()
0123     kli18n("Assets and Liabilities").untranslatedText(),
0124     kli18n("Budget").untranslatedText(),
0125     kli18n("CashFlow").untranslatedText(),
0126     // insert new items above this comment
0127     0,
0128 };
0129 
0130 const QString KMyMoneyUtils::homePageItemToString(const int idx)
0131 {
0132     QString rc;
0133     if (abs(idx) > 0 && abs(idx) < static_cast<int>(sizeof(homePageItems) / sizeof(homePageItems[0]))) {
0134         rc = i18n(homePageItems[abs(idx-1)]);
0135     }
0136     return rc;
0137 }
0138 
0139 int KMyMoneyUtils::stringToHomePageItem(const QString& txt)
0140 {
0141     int idx = 0;
0142     for (idx = 0; homePageItems[idx] != 0; ++idx) {
0143         if (txt == i18n(homePageItems[idx]))
0144             return idx + 1;
0145     }
0146     return 0;
0147 }
0148 
0149 bool KMyMoneyUtils::appendCorrectFileExt(QString& str, const QString& strExtToUse)
0150 {
0151     bool rc = false;
0152 
0153     if (!str.isEmpty()) {
0154         //find last . deliminator
0155         int nLoc = str.lastIndexOf('.');
0156         if (nLoc != -1) {
0157             QString strExt, strTemp;
0158             strTemp = str.left(nLoc + 1);
0159             strExt = str.right(str.length() - (nLoc + 1));
0160             if (strExt.indexOf(strExtToUse, 0, Qt::CaseInsensitive) == -1) {
0161                 // if the extension given contains a period, we remove ours
0162                 if (strExtToUse.indexOf('.') != -1)
0163                     strTemp = strTemp.left(strTemp.length() - 1);
0164                 //append extension to make complete file name
0165                 strTemp.append(strExtToUse);
0166                 str = strTemp;
0167                 rc = true;
0168             }
0169         } else {
0170             str.append(QLatin1Char('.'));
0171             str.append(strExtToUse);
0172             rc = true;
0173         }
0174     }
0175     return rc;
0176 }
0177 
0178 void KMyMoneyUtils::checkConstants()
0179 {
0180     // TODO: port to kf5
0181 #if 0
0182     Q_ASSERT(static_cast<int>(KLocale::ParensAround) == static_cast<int>(MyMoneyMoney::ParensAround));
0183     Q_ASSERT(static_cast<int>(KLocale::BeforeQuantityMoney) == static_cast<int>(MyMoneyMoney::BeforeQuantityMoney));
0184     Q_ASSERT(static_cast<int>(KLocale::AfterQuantityMoney) == static_cast<int>(MyMoneyMoney::AfterQuantityMoney));
0185     Q_ASSERT(static_cast<int>(KLocale::BeforeMoney) == static_cast<int>(MyMoneyMoney::BeforeMoney));
0186     Q_ASSERT(static_cast<int>(KLocale::AfterMoney) == static_cast<int>(MyMoneyMoney::AfterMoney));
0187 #endif
0188 }
0189 
0190 QString KMyMoneyUtils::getStylesheet(QString baseStylesheet)
0191 {
0192     if (baseStylesheet.isEmpty())
0193         baseStylesheet = QStandardPaths::locate(QStandardPaths::AppConfigLocation, "html/kmymoney.css");
0194 
0195     QColor tcolor = KColorScheme(QPalette::Active).foreground(KColorScheme::NormalText).color();
0196     QColor link = KColorScheme(QPalette::Active).foreground(KColorScheme::LinkText).color();
0197 
0198     QString css;
0199     css += QString(".row-even, .item0 { background-color: %1; color: %2 }\n")
0200                .arg(KMyMoneySettings::schemeColor(SchemeColor::ListBackground1).name(), tcolor.name());
0201     css += QString(".row-odd, .item1  { background-color: %1; color: %2 }\n")
0202                .arg(KMyMoneySettings::schemeColor(SchemeColor::ListBackground2).name(), tcolor.name());
0203     css += QString(".negativetext  { color: %1; }\n").arg(KMyMoneySettings::schemeColor(SchemeColor::Negative).name());
0204     css += QString("a { color: %1; }\n").arg(link.name());
0205 
0206     QFile cssFile(baseStylesheet);
0207     if (cssFile.open(QIODevice::ReadOnly)) {
0208         QTextStream cssStream(&cssFile);
0209         css += cssStream.readAll();
0210         cssFile.close();
0211     }
0212 
0213     return css;
0214 }
0215 
0216 const MyMoneySplit KMyMoneyUtils::stockSplit(const MyMoneyTransaction& t)
0217 {
0218     MyMoneySplit investmentAccountSplit;
0219     const auto splits = t.splits();
0220     for (const auto& split : splits) {
0221         if (!split.accountId().isEmpty()) {
0222             auto acc = MyMoneyFile::instance()->account(split.accountId());
0223             if (acc.isInvest()) {
0224                 return split;
0225             }
0226             // if we have a reference to an investment account, we remember it here
0227             if (acc.accountType() == eMyMoney::Account::Type::Investment)
0228                 investmentAccountSplit = split;
0229         }
0230     }
0231     // if we haven't found a stock split, we see if we've seen
0232     // an investment account on the way. If so, we return it.
0233     if (!investmentAccountSplit.id().isEmpty())
0234         return investmentAccountSplit;
0235 
0236     // if none was found, we return an empty split.
0237     return MyMoneySplit();
0238 }
0239 
0240 KMyMoneyUtils::transactionTypeE KMyMoneyUtils::transactionType(const MyMoneyTransaction& t)
0241 {
0242     if (!stockSplit(t).id().isEmpty())
0243         return InvestmentTransaction;
0244 
0245     if (t.splitCount() < 2) {
0246         return Unknown;
0247     } else if (t.splitCount() > 2) {
0248         // FIXME check for loan transaction here
0249         return SplitTransaction;
0250     }
0251     QString ida, idb;
0252     const auto & splits = t.splits();
0253     if (splits.size() > 0)
0254         ida = splits[0].accountId();
0255     if (splits.size() > 1)
0256         idb = splits[1].accountId();
0257     if (ida.isEmpty() || idb.isEmpty())
0258         return Unknown;
0259 
0260     MyMoneyAccount a, b;
0261     a = MyMoneyFile::instance()->account(ida);
0262     b = MyMoneyFile::instance()->account(idb);
0263     if ((a.accountGroup() == eMyMoney::Account::Type::Asset
0264             || a.accountGroup() == eMyMoney::Account::Type::Liability)
0265             && (b.accountGroup() == eMyMoney::Account::Type::Asset
0266                 || b.accountGroup() == eMyMoney::Account::Type::Liability))
0267         return Transfer;
0268     return Normal;
0269 }
0270 
0271 void KMyMoneyUtils::calculateAutoLoan(const MyMoneySchedule& schedule, MyMoneyTransaction& transaction, const QMap<QString, MyMoneyMoney>& balances)
0272 {
0273     try {
0274         MyMoneyForecast::calculateAutoLoan(schedule, transaction, balances);
0275     } catch (const MyMoneyException &e) {
0276         KMessageBox::detailedError(0, i18n("Unable to load schedule details"), QString::fromLatin1(e.what()));
0277     }
0278 }
0279 
0280 QString KMyMoneyUtils::nextCheckNumber(const MyMoneyAccount& acc)
0281 {
0282     return getAdjacentNumber(acc.value("lastNumberUsed"), 1);
0283 }
0284 
0285 QString KMyMoneyUtils::nextFreeCheckNumber(const MyMoneyAccount& acc)
0286 {
0287     auto file = MyMoneyFile::instance();
0288     auto num = acc.value("lastNumberUsed");
0289 
0290     if (num.isEmpty())
0291         num = QStringLiteral("1");
0292 
0293     // now check if this number has been used already
0294     if (file->checkNoUsed(acc.id(), num)) {
0295         // if a number has been entered which is immediately prior to
0296         // an existing number, the next new number produced would clash
0297         // so need to look ahead for free next number
0298         // we limit that to a number of tries which depends on the
0299         // number of splits in that account (we can't have more)
0300         MyMoneyTransactionFilter filter(acc.id());
0301         QList<MyMoneyTransaction> transactions;
0302         file->transactionList(transactions, filter);
0303         const int maxNumber = transactions.count();
0304         for (int i = 0; i < maxNumber; i++) {
0305             if (file->checkNoUsed(acc.id(), num)) {
0306                 //  increment and try again
0307                 num = getAdjacentNumber(num);
0308             } else {
0309                 //  found a free number
0310                 break;
0311             }
0312         }
0313     }
0314     return num;
0315 }
0316 
0317 QString KMyMoneyUtils::getAdjacentNumber(const QString& number, int offset)
0318 {
0319     // make sure the offset is either -1 or 1
0320     offset = (offset >= 0) ? 1 : -1;
0321 
0322     //                              +-#1--+ +#2++-#3-++-#4--+
0323     static const QRegularExpression exp(QString("(.*\\D)?(0*)(\\d+)(\\D.*)?"));
0324     QRegularExpressionMatch match = exp.match(number);
0325     if (match.hasMatch()) {
0326         return QStringLiteral("%1%2%3%4").arg(match.captured(1), match.captured(2), QString::number(match.captured(3).toULong() + offset), match.captured(4));
0327     }
0328     return QStringLiteral("1");
0329 }
0330 
0331 QString KMyMoneyUtils::reconcileStateToString(eMyMoney::Split::State flag, bool text)
0332 {
0333     QString txt;
0334     const QModelIndex idx = MyMoneyFile::instance()->statusModel()->index(static_cast<int>(flag), 0);
0335     if (idx.isValid()) {
0336         txt = idx.data(text ? eMyMoney::Model::SplitReconcileStatusRole : eMyMoney::Model::SplitReconcileFlagRole).toString();
0337     }
0338     return txt;
0339 }
0340 
0341 MyMoneyTransaction KMyMoneyUtils::scheduledTransaction(const MyMoneySchedule& schedule)
0342 {
0343     MyMoneyTransaction t = schedule.transaction();
0344 
0345     try {
0346         if (schedule.type() == eMyMoney::Schedule::Type::LoanPayment) {
0347             calculateAutoLoan(schedule, t, QMap<QString, MyMoneyMoney>());
0348         }
0349     } catch (const MyMoneyException &e) {
0350         qDebug("Unable to load schedule details for '%s' during transaction match: %s", qPrintable(schedule.name()), e.what());
0351     }
0352 
0353     t.clearId();
0354     t.setEntryDate(QDate());
0355     return t;
0356 }
0357 
0358 KXmlGuiWindow* KMyMoneyUtils::mainWindow()
0359 {
0360     const auto widgetList = QApplication::topLevelWidgets();
0361     for (QWidget* widget : qAsConst(widgetList)) {
0362         KXmlGuiWindow* result = dynamic_cast<KXmlGuiWindow*>(widget);
0363         if (result)
0364             return result;
0365     }
0366     return 0;
0367 }
0368 
0369 void KMyMoneyUtils::updateWizardButtons(QWizard* wizard)
0370 {
0371     // setup text on buttons
0372     wizard->setButtonText(QWizard::NextButton, i18nc("Go to next page of the wizard", "&Next"));
0373     wizard->setButtonText(QWizard::BackButton, KStandardGuiItem::back().text());
0374 
0375     // setup icons
0376     wizard->button(QWizard::FinishButton)->setIcon(KStandardGuiItem::ok().icon());
0377     wizard->button(QWizard::CancelButton)->setIcon(KStandardGuiItem::cancel().icon());
0378     wizard->button(QWizard::NextButton)->setIcon(KStandardGuiItem::forward(KStandardGuiItem::UseRTL).icon());
0379     wizard->button(QWizard::BackButton)->setIcon(KStandardGuiItem::back(KStandardGuiItem::UseRTL).icon());
0380 }
0381 
0382 void KMyMoneyUtils::dissectInvestmentTransaction(const QModelIndex &investSplitIdx, QModelIndex &assetAccountSplitIdx, SplitModel* feeSplitModel, SplitModel* interestSplitModel, MyMoneySecurity &security, MyMoneySecurity &currency, eMyMoney::Split::InvestmentTransactionType &transactionType)
0383 {
0384     // clear split models
0385     feeSplitModel->unload();
0386     interestSplitModel->unload();
0387 
0388     assetAccountSplitIdx = QModelIndex(); // set to none to check later if it was assigned
0389     const auto file = MyMoneyFile::instance();
0390 
0391     // collect the splits. split references the stock account and should already
0392     // be set up. assetAccountSplit references the corresponding asset account (maybe
0393     // empty), feeSplits is the list of all expenses and interestSplits
0394     // the list of all incomes
0395     auto idx = MyMoneyFile::baseModel()->mapToBaseSource(investSplitIdx);
0396     const auto list = idx.model()->match(idx.model()->index(0, 0), eMyMoney::Model::JournalTransactionIdRole,
0397                                          idx.data(eMyMoney::Model::JournalTransactionIdRole),
0398                                          -1,                         // all splits
0399                                          Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive | Qt::MatchRecursive));
0400     for (const auto& splitIdx : list) {
0401         auto accIdx = file->accountsModel()->indexById(splitIdx.data(eMyMoney::Model::SplitAccountIdRole).toString());
0402         const auto accountGroup = accIdx.data(eMyMoney::Model::AccountGroupRole).value<eMyMoney::Account::Type>();
0403         if (splitIdx.row() == idx.row()) {
0404             security = file->security(accIdx.data(eMyMoney::Model::AccountCurrencyIdRole).toString());
0405         } else if (accountGroup == eMyMoney::Account::Type::Expense) {
0406             feeSplitModel->appendSplit(file->journalModel()->itemByIndex(splitIdx).split());
0407         } else if (accountGroup == eMyMoney::Account::Type::Income) {
0408             interestSplitModel->appendSplit(file->journalModel()->itemByIndex(splitIdx).split());
0409         } else {
0410             if (!assetAccountSplitIdx.isValid()) { // first asset Account should be our requested brokerage account
0411                 assetAccountSplitIdx = splitIdx;
0412             } else if (idx.data(eMyMoney::Model::SplitValueRole).value<MyMoneyMoney>().isNegative()) { // the rest (if present) is handled as fee or interest
0413                 feeSplitModel->appendSplit(file->journalModel()->itemByIndex(splitIdx).split());
0414             } else if (idx.data(eMyMoney::Model::SplitValueRole).value<MyMoneyMoney>().isPositive()) {
0415                 interestSplitModel->appendSplit(file->journalModel()->itemByIndex(splitIdx).split());
0416             }
0417         }
0418     }
0419 
0420     // determine transaction type
0421     transactionType = idx.data(eMyMoney::Model::TransactionInvestementType).value<eMyMoney::Split::InvestmentTransactionType>();
0422 
0423     currency.setTradingSymbol("???");
0424     try {
0425         currency = file->security(file->journalModel()->itemByIndex(idx).transaction().commodity());
0426     } catch (const MyMoneyException &) {
0427     }
0428 }
0429 
0430 void KMyMoneyUtils::processPriceList(const MyMoneyStatement &st)
0431 {
0432     auto file = MyMoneyFile::instance();
0433     QHash<QString, MyMoneySecurity> secBySymbol;
0434     QHash<QString, MyMoneySecurity> secByName;
0435 
0436     const auto securityList = file->securityList();
0437     for (const auto& sec : securityList) {
0438         secBySymbol[sec.tradingSymbol()] = sec;
0439         secByName[sec.name()] = sec;
0440     }
0441 
0442     for (const auto& stPrice : st.m_listPrices) {
0443         auto currency = file->baseCurrency().id();
0444         QString security;
0445 
0446         if (!stPrice.m_strCurrency.isEmpty()) {
0447             security = stPrice.m_strSecurity;
0448             currency = stPrice.m_strCurrency;
0449         } else if (secBySymbol.contains(stPrice.m_strSecurity)) {
0450             security = secBySymbol[stPrice.m_strSecurity].id();
0451             currency = file->security(file->security(security).tradingCurrency()).id();
0452         } else if (secByName.contains(stPrice.m_strSecurity)) {
0453             security = secByName[stPrice.m_strSecurity].id();
0454             currency = file->security(file->security(security).tradingCurrency()).id();
0455         } else
0456             return;
0457 
0458         MyMoneyPrice price(security,
0459                            currency,
0460                            stPrice.m_date,
0461                            stPrice.m_amount, stPrice.m_sourceName.isEmpty() ? i18n("Prices Importer") : stPrice.m_sourceName);
0462         file->addPrice(price);
0463     }
0464 }
0465 
0466 void KMyMoneyUtils::deleteSecurity(const MyMoneySecurity& security, QWidget* parent)
0467 {
0468     QString msg, msg2;
0469     QString dontAsk, dontAsk2;
0470     if (security.isCurrency()) {
0471         msg = i18n("<p>Do you really want to remove the currency <b>%1</b> from the file?</p>", security.name());
0472         msg2 = i18n("<p>All exchange rates for currency <b>%1</b> will be lost.</p><p>Do you still want to continue?</p>", security.name());
0473         dontAsk = "DeleteCurrency";
0474         dontAsk2 = "DeleteCurrencyRates";
0475     } else {
0476         msg = i18n("<p>Do you really want to remove the %1 <b>%2</b> from the file?</p>", MyMoneySecurity::securityTypeToString(security.securityType()), security.name());
0477         msg2 = i18n("<p>All price quotes for %1 <b>%2</b> will be lost.</p><p>Do you still want to continue?</p>", MyMoneySecurity::securityTypeToString(security.securityType()), security.name());
0478         dontAsk = "DeleteSecurity";
0479         dontAsk2 = "DeleteSecurityPrices";
0480     }
0481     if (KMessageBox::questionTwoActions(parent, msg, i18n("Delete security"), KMMYesNo::yes(), KMMYesNo::no(), dontAsk) == KMessageBox::PrimaryAction) {
0482         MyMoneyFileTransaction ft;
0483         auto file = MyMoneyFile::instance();
0484 
0485         QBitArray skip((int)eStorage::Reference::Count);
0486         skip.fill(true);
0487         skip.clearBit((int)eStorage::Reference::Price);
0488         if (file->isReferenced(security, skip)) {
0489             if (KMessageBox::questionTwoActions(parent, msg2, i18n("Delete prices"), KMMYesNo::yes(), KMMYesNo::no(), dontAsk2) == KMessageBox::PrimaryAction) {
0490                 try {
0491                     QString secID = security.id();
0492                     const auto priceList = file->priceList();
0493                     for (const auto& priceEntry : priceList) {
0494                         const MyMoneyPrice& price = priceEntry.first();
0495                         if (price.from() == secID || price.to() == secID)
0496                             file->removePrice(price);
0497                     }
0498                     ft.commit();
0499                     ft.restart();
0500                 } catch (const MyMoneyException &) {
0501                     qDebug("Cannot delete price");
0502                     return;
0503                 }
0504             } else
0505                 return;
0506         }
0507         try {
0508             if (security.isCurrency())
0509                 file->removeCurrency(security);
0510             else
0511                 file->removeSecurity(security);
0512             ft.commit();
0513         } catch (const MyMoneyException &) {
0514         }
0515     }
0516 }
0517 
0518 bool KMyMoneyUtils::fileExists(const QUrl &url)
0519 {
0520     bool fileExists = false;
0521     if (url.isValid()) {
0522         if (url.isLocalFile() || url.scheme().isEmpty()) {
0523             QFileInfo check_file(url.toLocalFile());
0524             fileExists = check_file.exists() && check_file.isFile();
0525 
0526         } else {
0527             auto statjob = KIO::statDetails(url, KIO::StatJob::SourceSide, KIO::StatNoDetails);
0528             bool noerror = statjob->exec();
0529             if (noerror) {
0530                 // We want a file
0531                 fileExists = !statjob->statResult().isDir();
0532             }
0533             statjob->kill();
0534         }
0535     }
0536     return fileExists;
0537 }
0538 
0539 QString KMyMoneyUtils::downloadFile(const QUrl &url)
0540 {
0541     QString filename;
0542     KIO::StoredTransferJob *transferjob = KIO::storedGet (url);
0543 //  KJobWidgets::setWindow(transferjob, this);
0544     if (! transferjob->exec()) {
0545         KMessageBox::detailedError(nullptr,
0546                                    i18n("Error while loading file '%1'.", url.url()),
0547                                    transferjob->errorString(),
0548                                    i18n("File access error"));
0549         return filename;
0550     }
0551 
0552     QTemporaryFile file;
0553     file.setAutoRemove(false);
0554     file.open();
0555     file.write(transferjob->data());
0556     filename = file.fileName();
0557     file.close();
0558     return filename;
0559 }
0560 
0561 std::tuple<bool, QString> KMyMoneyUtils::newPayee(const QString& newnameBase)
0562 {
0563     bool doit = true;
0564     QString id;
0565 
0566     if (newnameBase != i18n("New Payee")) {
0567         // Ask the user if that is what he intended to do?
0568         const auto msg = i18n("<qt>Do you want to add <b>%1</b> as payee/receiver?</qt>", newnameBase);
0569 
0570         if (KMessageBox::questionTwoActions(nullptr, msg, i18n("New payee/receiver"), KMMYesNo::yes(), KMMYesNo::no(), "NewPayee")
0571             == KMessageBox::SecondaryAction) {
0572             doit = false;
0573             // we should not keep the 'no' setting because that can confuse people like
0574             // I have seen in some usability tests. So we just delete it right away.
0575             KSharedConfigPtr kconfig = KSharedConfig::openConfig();
0576             if (kconfig) {
0577                 kconfig->group(QLatin1String("Notification Messages")).deleteEntry(QLatin1String("NewPayee"));
0578             }
0579         }
0580     }
0581 
0582     if (doit) {
0583         MyMoneyFileTransaction ft;
0584         try {
0585             QString newname(newnameBase);
0586             // adjust name until a unique name has been created
0587             int count = 0;
0588 
0589             for (;;) {
0590                 try {
0591                     const auto payee = MyMoneyFile::instance()->payeeByName(newname);
0592                     if (payee.id().isEmpty())
0593                         break;
0594                     newname = QString::fromLatin1("%1 [%2]").arg(newnameBase).arg(++count);
0595                 } catch (const MyMoneyException &) {
0596                     break;
0597                 }
0598             }
0599 
0600             MyMoneyPayee p;
0601             p.setName(newname);
0602             p.setMatchData(eMyMoney::Payee::MatchType::NameExact, true, QStringList());
0603             MyMoneyFile::instance()->addPayee(p);
0604             id = p.id();
0605             ft.commit();
0606         } catch (const MyMoneyException &e) {
0607             KMessageBox::detailedError(nullptr, i18n("Unable to add payee"), QString::fromLatin1(e.what()));
0608             doit = false;
0609         }
0610     }
0611     return std::make_tuple(doit, id);
0612 }
0613 
0614 std::tuple<bool, QString> KMyMoneyUtils::newTag(const QString& newnameBase)
0615 {
0616     bool doit = true;
0617     QString id;
0618 
0619     if (newnameBase != i18n("New Tag")) {
0620         // Ask the user if that is what he intended to do?
0621         const auto msg = i18n("<qt>Do you want to add <b>%1</b> as tag?</qt>", newnameBase);
0622 
0623         if (KMessageBox::questionTwoActions(nullptr, msg, i18n("New tag"), KMMYesNo::yes(), KMMYesNo::no(), "NewTag") == KMessageBox::SecondaryAction) {
0624             doit = false;
0625             // we should not keep the 'no' setting because that can confuse people like
0626             // I have seen in some usability tests. So we just delete it right away.
0627             KSharedConfigPtr kconfig = KSharedConfig::openConfig();
0628             if (kconfig) {
0629                 kconfig->group(QLatin1String("Notification Messages")).deleteEntry(QLatin1String("NewTag"));
0630             }
0631         }
0632     }
0633 
0634     if (doit) {
0635         MyMoneyFileTransaction ft;
0636         try {
0637             QString newname(newnameBase);
0638             // adjust name until a unique name has been created
0639             int count = 0;
0640             for (;;) {
0641                 try {
0642                     if (MyMoneyFile::instance()->tagByName(newname).id().isEmpty()) {
0643                         break;
0644                     }
0645                     newname = QString::fromLatin1("%1 [%2]").arg(newnameBase).arg(++count);
0646                 } catch (const MyMoneyException &) {
0647                     break;
0648                 }
0649             }
0650 
0651             MyMoneyTag ta;
0652             ta.setName(newname);
0653             MyMoneyFile::instance()->addTag(ta);
0654             id = ta.id();
0655             ft.commit();
0656         } catch (const MyMoneyException &e) {
0657             KMessageBox::detailedError(nullptr, i18n("Unable to add tag"), QString::fromLatin1(e.what()));
0658         }
0659     }
0660     return std::make_tuple(doit, id);
0661 }
0662 
0663 void KMyMoneyUtils::newInstitution(MyMoneyInstitution& institution)
0664 {
0665     auto file = MyMoneyFile::instance();
0666 
0667     MyMoneyFileTransaction ft;
0668 
0669     try {
0670         file->addInstitution(institution);
0671         ft.commit();
0672 
0673     } catch (const MyMoneyException &e) {
0674         KMessageBox::information(nullptr, i18n("Cannot add institution: %1", QString::fromLatin1(e.what())));
0675     }
0676 }
0677 
0678 QDebug KMyMoneyUtils::debug()
0679 {
0680     return qDebug() << QDateTime::currentDateTime().toString(QStringLiteral("HH:mm:ss.zzz"));
0681 }
0682 
0683 MyMoneyForecast KMyMoneyUtils::forecast()
0684 {
0685     MyMoneyForecast forecast;
0686 
0687     // override object defaults with those of the application
0688     forecast.setForecastCycles(KMyMoneySettings::forecastCycles());
0689     forecast.setAccountsCycle(KMyMoneySettings::forecastAccountCycle());
0690     forecast.setHistoryStartDate(QDate::currentDate().addDays(-forecast.forecastCycles()*forecast.accountsCycle()));
0691     forecast.setHistoryEndDate(QDate::currentDate().addDays(-1));
0692     forecast.setForecastDays(KMyMoneySettings::forecastDays());
0693     forecast.setBeginForecastDay(KMyMoneySettings::beginForecastDay());
0694     forecast.setForecastMethod(KMyMoneySettings::forecastMethod());
0695     forecast.setHistoryMethod(KMyMoneySettings::historyMethod());
0696     forecast.setIncludeFutureTransactions(KMyMoneySettings::includeFutureTransactions());
0697     forecast.setIncludeScheduledTransactions(KMyMoneySettings::includeScheduledTransactions());
0698 
0699     return forecast;
0700 }
0701 
0702 bool KMyMoneyUtils::canUpdateAllAccounts()
0703 {
0704     const auto file = MyMoneyFile::instance();
0705     auto rc = false;
0706 
0707     QList<MyMoneyAccount> accList;
0708     file->accountList(accList);
0709     QList<MyMoneyAccount>::const_iterator it_a;
0710     auto it_p = pPlugins.online.constEnd();
0711     for (it_a = accList.constBegin(); (it_p == pPlugins.online.constEnd()) && (it_a != accList.constEnd()); ++it_a) {
0712         if ((*it_a).hasOnlineMapping()) {
0713             // check if provider is available
0714             it_p = pPlugins.online.constFind((*it_a).onlineBankingSettings().value("provider").toLower());
0715             if (it_p != pPlugins.online.constEnd()) {
0716                 QStringList protocols;
0717                 (*it_p)->protocols(protocols);
0718                 if (!protocols.isEmpty()) {
0719                     rc = true;
0720                     break;
0721                 }
0722             }
0723         }
0724     }
0725     return rc;
0726 }
0727 
0728 void KMyMoneyUtils::showStatementImportResult(const QStringList& resultMessages, uint statementCount)
0729 {
0730     KMessageBox::informationList(nullptr,
0731                                  i18np("One statement has been processed with the following results:",
0732                                        "%1 statements have been processed with the following results:",
0733                                        statementCount),
0734                                  !resultMessages.isEmpty() ?
0735                                  resultMessages :
0736                                  QStringList { i18np("No new transaction has been imported.", "No new transactions have been imported.", statementCount) },
0737                                  i18n("Statement import statistics"));
0738 }
0739 
0740 QString KMyMoneyUtils::normalizeNumericString(const qreal& val, const QLocale& loc, const char f, const int prec)
0741 {
0742     static const QRegularExpression trailingZeroesRegex("0+$");
0743     return loc.toString(val, f, prec).remove(loc.groupSeparator()).remove(trailingZeroesRegex).remove(QRegularExpression("\\" + loc.decimalPoint() + "$"));
0744 }
0745 
0746 QStringList KMyMoneyUtils::tabOrder(const QString& name, const QStringList& defaultTabOrder)
0747 {
0748     KSharedConfigPtr config = KSharedConfig::openConfig();
0749     KConfigGroup grp = config->group(QLatin1String("TabOrder"));
0750     return grp.readEntry(name, defaultTabOrder);
0751 }
0752 
0753 #if 0
0754 static QString widgetName(QWidget* w)
0755 {
0756     return w->objectName().isEmpty() ? w->metaObject()->className() : w->objectName();
0757 }
0758 
0759 static void dumpFocusChain(QWidget* w, QWidget* end, int additionalTabstops = 0, bool forward = true)
0760 {
0761     QString txt;
0762     int loopValid = 80;
0763     int trailing = -1;
0764     do {
0765         const auto policy = w->focusPolicy();
0766         if (policy == Qt::TabFocus || policy == Qt::StrongFocus ) {
0767             if (!txt.isEmpty()) {
0768                 txt += forward ? QLatin1String(" -> ") : QLatin1String(" <- ");
0769             }
0770             txt += widgetName(w);
0771             --loopValid;
0772             w = forward ? w->nextInFocusChain() : w->previousInFocusChain();
0773             if (w == end && (trailing < 0)) {
0774                 trailing = additionalTabstops+1;
0775                 loopValid = trailing;
0776             }
0777         }
0778         --trailing;
0779     } while ((loopValid > 0) && (trailing != 0));
0780 
0781     if (end) {
0782         txt += forward ? QLatin1String(" -> ") : QLatin1String(" <- ");
0783         txt += widgetName(w);
0784     }
0785 
0786     qDebug() << txt;
0787 }
0788 #endif
0789 
0790 void KMyMoneyUtils::setupTabOrder(QWidget* parent, const QStringList& tabOrder)
0791 {
0792     const auto widgetCount = tabOrder.count();
0793     if (widgetCount > 0) {
0794         auto prev = parent->findChild<QWidget*>(tabOrder.at(0));
0795         for (int i = 1; (prev != nullptr) && (i < widgetCount); ++i) {
0796             const auto next = parent->findChild<QWidget*>(tabOrder.at(i));
0797             if (next) {
0798                 parent->setTabOrder(prev, next);
0799                 prev = next;
0800             } else {
0801                 qDebug() << tabOrder.at(i) << "not found :(";
0802             }
0803         }
0804     }
0805 }
0806 
0807 void KMyMoneyUtils::storeTabOrder(const QString& name, const QStringList& tabOrder)
0808 {
0809     KSharedConfigPtr config = KSharedConfig::openConfig();
0810     KConfigGroup grp = config->group(QLatin1String("TabOrder"));
0811     grp.writeEntry(name, tabOrder);
0812 }
0813 
0814 bool KMyMoneyUtils::tabFocusHelper(QWidget* topLevelWidget, bool next)
0815 {
0816     const auto reason = next ? Qt::TabFocusReason : Qt::BacktabFocusReason;
0817     const auto tabOrder = topLevelWidget->property("kmm_currenttaborder").toStringList();
0818 
0819     if (tabOrder.isEmpty())
0820         return false;
0821 
0822     auto focusWidget = topLevelWidget->focusWidget();
0823 
0824     enum firstOrLastVisible {
0825         FirstVisible,
0826         LastVisible,
0827     };
0828 
0829     auto findFirstOrLastVisible = [&](firstOrLastVisible type) {
0830         const int ofs = (type == FirstVisible) ? 1 : -1;
0831         int idx = (type == FirstVisible) ? 0 : tabOrder.count() - 1;
0832         for (; idx >= 0 && idx < tabOrder.count(); idx += ofs) {
0833             auto w = topLevelWidget->findChild<QWidget*>(tabOrder.at(idx));
0834             // in case of embedded transaction editors, we may search
0835             // for a widget that is known to the parent
0836             if (!w) {
0837                 auto parent = topLevelWidget->parentWidget();
0838                 while (true) {
0839                     w = parent->findChild<QWidget*>(tabOrder.at(idx));
0840                     if (!w && qobject_cast<QGroupBox*>(parent)) {
0841                         parent = parent->parentWidget();
0842                         continue;
0843                     }
0844                     break;
0845                 }
0846             }
0847             if (w && w->isVisible() && w->isEnabled()) {
0848                 return w;
0849             }
0850         }
0851         return static_cast<QWidget*>(nullptr);
0852     };
0853 
0854     auto selectWidget = [&](QWidget* w) {
0855         if (w) {
0856             // if we point to a constructed widget (e.g. ButtonBox) we
0857             // need to select the last widget if going backward
0858             if (reason == Qt::BacktabFocusReason && !w->findChildren<QWidget*>().isEmpty()) {
0859                 auto parent = w;
0860                 while (w->nextInFocusChain()->parentWidget() == parent) {
0861                     w = w->nextInFocusChain();
0862                 }
0863             }
0864             w->setFocus(reason);
0865         }
0866     };
0867 
0868     auto adjustToContainer = [&](const char* containerClass, const char* widgetClass) {
0869         if (focusWidget->qt_metacast(widgetClass) && focusWidget->parentWidget()->qt_metacast(containerClass) && (reason == Qt::BacktabFocusReason)) {
0870             if (focusWidget->previousInFocusChain() == focusWidget->parentWidget()) {
0871                 focusWidget = focusWidget->parentWidget();
0872             }
0873         }
0874     };
0875 
0876     // In case of a CreditDebitEdit widget and we leave from the left backwards,
0877     // we need to adjust the widget to point to the container widget.
0878     adjustToContainer("CreditDebitEdit", "AmountEdit");
0879     // In case of a QDialogButtonBox widget and we leave from the left backwards,
0880     // we need to adjust the widget to point to the container widget.
0881     // adjustToContainer("QDialogButtonBox", "QPushButton");
0882 
0883     if ((reason == Qt::BacktabFocusReason) && (findFirstOrLastVisible(FirstVisible) == focusWidget)) {
0884         selectWidget(findFirstOrLastVisible(LastVisible));
0885         return true;
0886 
0887     } else if ((reason == Qt::TabFocusReason) && (findFirstOrLastVisible(LastVisible) == focusWidget)) {
0888         selectWidget(findFirstOrLastVisible(FirstVisible));
0889         return true;
0890     }
0891     return false;
0892 }