diff --git a/kmymoney/kmymoneyutils.cpp b/kmymoney/kmymoneyutils.cpp index 2f0a655fd..e6d1a6a62 100644 --- a/kmymoney/kmymoneyutils.cpp +++ b/kmymoney/kmymoneyutils.cpp @@ -1,836 +1,844 @@ /*************************************************************************** kmymoneyutils.cpp - description ------------------- begin : Wed Feb 5 2003 copyright : (C) 2000-2003 by Michael Edwardes email : mte@users.sourceforge.net Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio (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. * * * ***************************************************************************/ #include "kmymoneyutils.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Headers #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneymoney.h" #include "mymoneyexception.h" #include "mymoneytransactionfilter.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneyschedule.h" #include "mymoneypayee.h" #include "mymoneytag.h" #include "mymoneyprice.h" #include "mymoneystatement.h" #include "mymoneyforecast.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "kmymoneysettings.h" #include "icons.h" #include "storageenums.h" #include "mymoneyenums.h" #include "kmymoneyplugin.h" using namespace Icons; KMyMoneyUtils::KMyMoneyUtils() { } KMyMoneyUtils::~KMyMoneyUtils() { } const QString KMyMoneyUtils::occurrenceToString(const eMyMoney::Schedule::Occurrence occurrence) { return i18nc("Frequency of schedule", MyMoneySchedule::occurrenceToString(occurrence).toLatin1()); } const QString KMyMoneyUtils::paymentMethodToString(eMyMoney::Schedule::PaymentType paymentType) { return i18nc("Scheduled Transaction payment type", MyMoneySchedule::paymentMethodToString(paymentType).toLatin1()); } const QString KMyMoneyUtils::weekendOptionToString(eMyMoney::Schedule::WeekendOption weekendOption) { return i18n(MyMoneySchedule::weekendOptionToString(weekendOption).toLatin1()); } const QString KMyMoneyUtils::scheduleTypeToString(eMyMoney::Schedule::Type type) { return i18nc("Scheduled transaction type", MyMoneySchedule::scheduleTypeToString(type).toLatin1()); } KGuiItem KMyMoneyUtils::scheduleNewGuiItem() { KGuiItem splitGuiItem(i18n("&New Schedule..."), Icons::get(Icon::DocumentNew), i18n("Create a new schedule."), i18n("Use this to create a new schedule.")); return splitGuiItem; } KGuiItem KMyMoneyUtils::accountsFilterGuiItem() { KGuiItem splitGuiItem(i18n("&Filter"), Icons::get(Icon::ViewFilter), i18n("Filter out accounts"), i18n("Use this to filter out accounts")); return splitGuiItem; } const char* homePageItems[] = { I18N_NOOP("Payments"), I18N_NOOP("Preferred accounts"), I18N_NOOP("Payment accounts"), I18N_NOOP("Favorite reports"), I18N_NOOP("Forecast (schedule)"), I18N_NOOP("Net worth forecast"), I18N_NOOP("Forecast (history)"), // unused, s.a. KSettingsHome::slotLoadItems() I18N_NOOP("Assets and Liabilities"), I18N_NOOP("Budget"), I18N_NOOP("CashFlow"), // insert new items above this comment 0 }; const QString KMyMoneyUtils::homePageItemToString(const int idx) { QString rc; if (abs(idx) > 0 && abs(idx) < static_cast(sizeof(homePageItems) / sizeof(homePageItems[0]))) { rc = i18n(homePageItems[abs(idx-1)]); } return rc; } int KMyMoneyUtils::stringToHomePageItem(const QString& txt) { int idx = 0; for (idx = 0; homePageItems[idx] != 0; ++idx) { if (txt == i18n(homePageItems[idx])) return idx + 1; } return 0; } bool KMyMoneyUtils::appendCorrectFileExt(QString& str, const QString& strExtToUse) { bool rc = false; if (!str.isEmpty()) { //find last . deliminator int nLoc = str.lastIndexOf('.'); if (nLoc != -1) { QString strExt, strTemp; strTemp = str.left(nLoc + 1); strExt = str.right(str.length() - (nLoc + 1)); if (strExt.indexOf(strExtToUse, 0, Qt::CaseInsensitive) == -1) { // if the extension given contains a period, we remove ours if (strExtToUse.indexOf('.') != -1) strTemp = strTemp.left(strTemp.length() - 1); //append extension to make complete file name strTemp.append(strExtToUse); str = strTemp; rc = true; } } else { str.append(QLatin1Char('.')); str.append(strExtToUse); rc = true; } } return rc; } void KMyMoneyUtils::checkConstants() { // TODO: port to kf5 #if 0 Q_ASSERT(static_cast(KLocale::ParensAround) == static_cast(MyMoneyMoney::ParensAround)); Q_ASSERT(static_cast(KLocale::BeforeQuantityMoney) == static_cast(MyMoneyMoney::BeforeQuantityMoney)); Q_ASSERT(static_cast(KLocale::AfterQuantityMoney) == static_cast(MyMoneyMoney::AfterQuantityMoney)); Q_ASSERT(static_cast(KLocale::BeforeMoney) == static_cast(MyMoneyMoney::BeforeMoney)); Q_ASSERT(static_cast(KLocale::AfterMoney) == static_cast(MyMoneyMoney::AfterMoney)); #endif } QString KMyMoneyUtils::variableCSS() { QColor tcolor = KColorScheme(QPalette::Active).foreground(KColorScheme::NormalText).color(); QColor link = KColorScheme(QPalette::Active).foreground(KColorScheme::LinkText).color(); QString css; css += "\n"; return css; } QString KMyMoneyUtils::findResource(QStandardPaths::StandardLocation type, const QString& filename) { QLocale locale; QString country; QString localeName = locale.bcp47Name(); QString language = localeName; // extract language and country from the bcp47name QRegularExpression regExp(QLatin1String("(\\w+)_(\\w+)")); QRegularExpressionMatch match = regExp.match(localeName); if (match.hasMatch()) { language = match.captured(1); country = match.captured(2); } QString rc; // check that the placeholder is present and set things up if (filename.indexOf("%1") != -1) { /// @fixme somehow I have the impression, that language and country /// mappings to the filename are not correct. This certainly must /// be overhauled at some point in time (ipwizard, 2017-10-22) QString mask = filename.arg("_%1.%2"); rc = QStandardPaths::locate(type, mask.arg(country, language)); // search the given resource if (rc.isEmpty()) { mask = filename.arg("_%1"); rc = QStandardPaths::locate(type, mask.arg(language)); } if (rc.isEmpty()) { // qDebug(QString("html/home_%1.html not found").arg(country).toLatin1()); rc = QStandardPaths::locate(type, mask.arg(country)); } if (rc.isEmpty()) { rc = QStandardPaths::locate(type, filename.arg("")); } } else { rc = QStandardPaths::locate(type, filename); } if (rc.isEmpty()) { qWarning("No resource found for (%s,%s)", qPrintable(QStandardPaths::displayName(type)), qPrintable(filename)); } return rc; } const MyMoneySplit KMyMoneyUtils::stockSplit(const MyMoneyTransaction& t) { MyMoneySplit investmentAccountSplit; foreach (const auto split, t.splits()) { if (!split.accountId().isEmpty()) { auto acc = MyMoneyFile::instance()->account(split.accountId()); if (acc.isInvest()) { return split; } // if we have a reference to an investment account, we remember it here if (acc.accountType() == eMyMoney::Account::Type::Investment) investmentAccountSplit = split; } } // if we haven't found a stock split, we see if we've seen // an investment account on the way. If so, we return it. if (!investmentAccountSplit.id().isEmpty()) return investmentAccountSplit; // if none was found, we return an empty split. return MyMoneySplit(); } KMyMoneyUtils::transactionTypeE KMyMoneyUtils::transactionType(const MyMoneyTransaction& t) { if (!stockSplit(t).id().isEmpty()) return InvestmentTransaction; if (t.splitCount() < 2) { return Unknown; } else if (t.splitCount() > 2) { // FIXME check for loan transaction here return SplitTransaction; } QString ida, idb; const auto & splits = t.splits(); if (splits.size() > 0) ida = splits[0].accountId(); if (splits.size() > 1) idb = splits[1].accountId(); if (ida.isEmpty() || idb.isEmpty()) return Unknown; MyMoneyAccount a, b; a = MyMoneyFile::instance()->account(ida); b = MyMoneyFile::instance()->account(idb); if ((a.accountGroup() == eMyMoney::Account::Type::Asset || a.accountGroup() == eMyMoney::Account::Type::Liability) && (b.accountGroup() == eMyMoney::Account::Type::Asset || b.accountGroup() == eMyMoney::Account::Type::Liability)) return Transfer; return Normal; } void KMyMoneyUtils::calculateAutoLoan(const MyMoneySchedule& schedule, MyMoneyTransaction& transaction, const QMap& balances) { try { MyMoneyForecast::calculateAutoLoan(schedule, transaction, balances); } catch (const MyMoneyException &e) { KMessageBox::detailedError(0, i18n("Unable to load schedule details"), QString::fromLatin1(e.what())); } } QString KMyMoneyUtils::nextCheckNumber(const MyMoneyAccount& acc) { return getAdjacentNumber(acc.value("lastNumberUsed"), 1); } QString KMyMoneyUtils::nextFreeCheckNumber(const MyMoneyAccount& acc) { auto file = MyMoneyFile::instance(); auto num = acc.value("lastNumberUsed"); if (num.isEmpty()) num = QStringLiteral("1"); // now check if this number has been used already if (file->checkNoUsed(acc.id(), num)) { // if a number has been entered which is immediately prior to // an existing number, the next new number produced would clash // so need to look ahead for free next number // we limit that to a number of tries which depends on the // number of splits in that account (we can't have more) MyMoneyTransactionFilter filter(acc.id()); QList transactions; file->transactionList(transactions, filter); const int maxNumber = transactions.count(); for (int i = 0; i < maxNumber; i++) { if (file->checkNoUsed(acc.id(), num)) { // increment and try again num = getAdjacentNumber(num); } else { // found a free number break; } } } return num; } QString KMyMoneyUtils::getAdjacentNumber(const QString& number, int offset) { // make sure the offset is either -1 or 1 offset = (offset >= 0) ? 1 : -1; QString num = number; // +-#1--+ +#2++-#3-++-#4--+ QRegExp exp(QString("(.*\\D)?(0*)(\\d+)(\\D.*)?")); if (exp.indexIn(num) != -1) { QString arg1 = exp.cap(1); QString arg2 = exp.cap(2); QString arg3 = QString::number(exp.cap(3).toULong() + offset); QString arg4 = exp.cap(4); num = QString("%1%2%3%4").arg(arg1, arg2, arg3, arg4); } else { num = QStringLiteral("1"); } return num; } quint64 KMyMoneyUtils::numericPart(const QString & num) { quint64 num64 = 0; QRegExp exp(QString("(.*\\D)?(0*)(\\d+)(\\D.*)?")); if (exp.indexIn(num) != -1) { // QString arg1 = exp.cap(1); QString arg2 = exp.cap(2); QString arg3 = QString::number(exp.cap(3).toULongLong()); // QString arg4 = exp.cap(4); num64 = QString("%2%3").arg(arg2, arg3).toULongLong(); } return num64; } QString KMyMoneyUtils::reconcileStateToString(eMyMoney::Split::State flag, bool text) { QString txt; if (text) { switch (flag) { case eMyMoney::Split::State::NotReconciled: txt = i18nc("Reconciliation state 'Not reconciled'", "Not reconciled"); break; case eMyMoney::Split::State::Cleared: txt = i18nc("Reconciliation state 'Cleared'", "Cleared"); break; case eMyMoney::Split::State::Reconciled: txt = i18nc("Reconciliation state 'Reconciled'", "Reconciled"); break; case eMyMoney::Split::State::Frozen: txt = i18nc("Reconciliation state 'Frozen'", "Frozen"); break; default: txt = i18nc("Unknown reconciliation state", "Unknown"); break; } } else { switch (flag) { case eMyMoney::Split::State::NotReconciled: break; case eMyMoney::Split::State::Cleared: txt = i18nc("Reconciliation flag C", "C"); break; case eMyMoney::Split::State::Reconciled: txt = i18nc("Reconciliation flag R", "R"); break; case eMyMoney::Split::State::Frozen: txt = i18nc("Reconciliation flag F", "F"); break; default: txt = i18nc("Flag for unknown reconciliation state", "?"); break; } } return txt; } MyMoneyTransaction KMyMoneyUtils::scheduledTransaction(const MyMoneySchedule& schedule) { MyMoneyTransaction t = schedule.transaction(); try { if (schedule.type() == eMyMoney::Schedule::Type::LoanPayment) { calculateAutoLoan(schedule, t, QMap()); } } catch (const MyMoneyException &e) { qDebug("Unable to load schedule details for '%s' during transaction match: %s", qPrintable(schedule.name()), e.what()); } t.clearId(); t.setEntryDate(QDate()); return t; } KXmlGuiWindow* KMyMoneyUtils::mainWindow() { foreach (QWidget *widget, QApplication::topLevelWidgets()) { KXmlGuiWindow* result = dynamic_cast(widget); if (result) return result; } return 0; } void KMyMoneyUtils::updateWizardButtons(QWizard* wizard) { // setup text on buttons wizard->setButtonText(QWizard::NextButton, i18nc("Go to next page of the wizard", "&Next")); wizard->setButtonText(QWizard::BackButton, KStandardGuiItem::back().text()); // setup icons wizard->button(QWizard::FinishButton)->setIcon(KStandardGuiItem::ok().icon()); wizard->button(QWizard::CancelButton)->setIcon(KStandardGuiItem::cancel().icon()); wizard->button(QWizard::NextButton)->setIcon(KStandardGuiItem::forward(KStandardGuiItem::UseRTL).icon()); wizard->button(QWizard::BackButton)->setIcon(KStandardGuiItem::back(KStandardGuiItem::UseRTL).icon()); } void KMyMoneyUtils::dissectTransaction(const MyMoneyTransaction& transaction, const MyMoneySplit& split, MyMoneySplit& assetAccountSplit, QList& feeSplits, QList& interestSplits, MyMoneySecurity& security, MyMoneySecurity& currency, eMyMoney::Split::InvestmentTransactionType& transactionType) { // collect the splits. split references the stock account and should already // be set up. assetAccountSplit references the corresponding asset account (maybe // empty), feeSplits is the list of all expenses and interestSplits // the list of all incomes assetAccountSplit = MyMoneySplit(); // set to none to check later if it was assigned auto file = MyMoneyFile::instance(); foreach (const auto tsplit, transaction.splits()) { auto acc = file->account(tsplit.accountId()); if (tsplit.id() == split.id()) { security = file->security(acc.currencyId()); } else if (acc.accountGroup() == eMyMoney::Account::Type::Expense) { feeSplits.append(tsplit); // feeAmount += tsplit.value(); } else if (acc.accountGroup() == eMyMoney::Account::Type::Income) { interestSplits.append(tsplit); // interestAmount += tsplit.value(); } else { if (assetAccountSplit == MyMoneySplit()) // first asset Account should be our requested brokerage account assetAccountSplit = tsplit; else if (tsplit.value().isNegative()) // the rest (if present) is handled as fee or interest feeSplits.append(tsplit); // and shouldn't be allowed to override assetAccountSplit else if (tsplit.value().isPositive()) interestSplits.append(tsplit); } } // determine transaction type if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::AddShares)) { transactionType = (!split.shares().isNegative()) ? eMyMoney::Split::InvestmentTransactionType::AddShares : eMyMoney::Split::InvestmentTransactionType::RemoveShares; } else if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares)) { transactionType = (!split.value().isNegative()) ? eMyMoney::Split::InvestmentTransactionType::BuyShares : eMyMoney::Split::InvestmentTransactionType::SellShares; } else if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Dividend)) { transactionType = eMyMoney::Split::InvestmentTransactionType::Dividend; } else if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::ReinvestDividend)) { transactionType = eMyMoney::Split::InvestmentTransactionType::ReinvestDividend; } else if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Yield)) { transactionType = eMyMoney::Split::InvestmentTransactionType::Yield; } else if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::SplitShares)) { transactionType = eMyMoney::Split::InvestmentTransactionType::SplitShares; } else if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::InterestIncome)) { transactionType = eMyMoney::Split::InvestmentTransactionType::InterestIncome; } else transactionType = eMyMoney::Split::InvestmentTransactionType::BuyShares; currency.setTradingSymbol("???"); try { currency = file->security(transaction.commodity()); } catch (const MyMoneyException &) { } } void KMyMoneyUtils::processPriceList(const MyMoneyStatement &st) { auto file = MyMoneyFile::instance(); QHash secBySymbol; QHash secByName; const auto securityList = file->securityList(); for (const auto& sec : securityList) { secBySymbol[sec.tradingSymbol()] = sec; secByName[sec.name()] = sec; } for (const auto& stPrice : st.m_listPrices) { auto currency = file->baseCurrency().id(); QString security; if (!stPrice.m_strCurrency.isEmpty()) { security = stPrice.m_strSecurity; currency = stPrice.m_strCurrency; } else if (secBySymbol.contains(stPrice.m_strSecurity)) { security = secBySymbol[stPrice.m_strSecurity].id(); currency = file->security(file->security(security).tradingCurrency()).id(); } else if (secByName.contains(stPrice.m_strSecurity)) { security = secByName[stPrice.m_strSecurity].id(); currency = file->security(file->security(security).tradingCurrency()).id(); } else return; MyMoneyPrice price(security, currency, stPrice.m_date, stPrice.m_amount, stPrice.m_sourceName.isEmpty() ? i18n("Prices Importer") : stPrice.m_sourceName); file->addPrice(price); } } void KMyMoneyUtils::deleteSecurity(const MyMoneySecurity& security, QWidget* parent) { QString msg, msg2; QString dontAsk, dontAsk2; if (security.isCurrency()) { msg = i18n("

Do you really want to remove the currency %1 from the file?

", security.name()); msg2 = i18n("

All exchange rates for currency %1 will be lost.

Do you still want to continue?

", security.name()); dontAsk = "DeleteCurrency"; dontAsk2 = "DeleteCurrencyRates"; } else { msg = i18n("

Do you really want to remove the %1 %2 from the file?

", MyMoneySecurity::securityTypeToString(security.securityType()), security.name()); msg2 = i18n("

All price quotes for %1 %2 will be lost.

Do you still want to continue?

", MyMoneySecurity::securityTypeToString(security.securityType()), security.name()); dontAsk = "DeleteSecurity"; dontAsk2 = "DeleteSecurityPrices"; } if (KMessageBox::questionYesNo(parent, msg, i18n("Delete security"), KStandardGuiItem::yes(), KStandardGuiItem::no(), dontAsk) == KMessageBox::Yes) { MyMoneyFileTransaction ft; auto file = MyMoneyFile::instance(); QBitArray skip((int)eStorage::Reference::Count); skip.fill(true); skip.clearBit((int)eStorage::Reference::Price); if (file->isReferenced(security, skip)) { if (KMessageBox::questionYesNo(parent, msg2, i18n("Delete prices"), KStandardGuiItem::yes(), KStandardGuiItem::no(), dontAsk2) == KMessageBox::Yes) { try { QString secID = security.id(); foreach (auto priceEntry, file->priceList()) { const MyMoneyPrice& price = priceEntry.first(); if (price.from() == secID || price.to() == secID) file->removePrice(price); } ft.commit(); ft.restart(); } catch (const MyMoneyException &) { qDebug("Cannot delete price"); return; } } else return; } try { if (security.isCurrency()) file->removeCurrency(security); else file->removeSecurity(security); ft.commit(); } catch (const MyMoneyException &) { } } } bool KMyMoneyUtils::fileExists(const QUrl &url) { bool fileExists = false; if (url.isValid()) { if (url.isLocalFile() || url.scheme().isEmpty()) { QFileInfo check_file(url.toLocalFile()); fileExists = check_file.exists() && check_file.isFile(); } else { 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(); } statjob->kill(); } } return fileExists; } QString KMyMoneyUtils::downloadFile(const QUrl &url) { QString filename; KIO::StoredTransferJob *transferjob = KIO::storedGet (url); // KJobWidgets::setWindow(transferjob, this); if (! transferjob->exec()) { KMessageBox::detailedError(nullptr, i18n("Error while loading file '%1'.", url.url()), transferjob->errorString(), i18n("File access error")); return filename; } QTemporaryFile file; file.setAutoRemove(false); file.open(); file.write(transferjob->data()); filename = file.fileName(); file.close(); return filename; } bool KMyMoneyUtils::newPayee(const QString& newnameBase, QString& id) { bool doit = true; if (newnameBase != i18n("New Payee")) { // Ask the user if that is what he intended to do? const auto msg = i18n("Do you want to add %1 as payer/receiver?", newnameBase); if (KMessageBox::questionYesNo(nullptr, msg, i18n("New payee/receiver"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "NewPayee") == KMessageBox::No) { doit = false; // we should not keep the 'no' setting because that can confuse people like // I have seen in some usability tests. So we just delete it right away. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); if (kconfig) { kconfig->group(QLatin1String("Notification Messages")).deleteEntry(QLatin1String("NewPayee")); } } } if (doit) { MyMoneyFileTransaction ft; try { QString newname(newnameBase); // adjust name until a unique name has been created int count = 0; for (;;) { try { MyMoneyFile::instance()->payeeByName(newname); newname = QString::fromLatin1("%1 [%2]").arg(newnameBase).arg(++count); } catch (const MyMoneyException &) { break; } } MyMoneyPayee p; p.setName(newname); MyMoneyFile::instance()->addPayee(p); id = p.id(); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(nullptr, i18n("Unable to add payee"), QString::fromLatin1(e.what())); doit = false; } } return doit; } void KMyMoneyUtils::newTag(const QString& newnameBase, QString& id) { bool doit = true; if (newnameBase != i18n("New Tag")) { // Ask the user if that is what he intended to do? const auto msg = i18n("Do you want to add %1 as tag?", newnameBase); if (KMessageBox::questionYesNo(nullptr, msg, i18n("New tag"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "NewTag") == KMessageBox::No) { doit = false; // we should not keep the 'no' setting because that can confuse people like // I have seen in some usability tests. So we just delete it right away. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); if (kconfig) { kconfig->group(QLatin1String("Notification Messages")).deleteEntry(QLatin1String("NewTag")); } } } if (doit) { MyMoneyFileTransaction ft; try { QString newname(newnameBase); // adjust name until a unique name has been created int count = 0; for (;;) { try { MyMoneyFile::instance()->tagByName(newname); newname = QString::fromLatin1("%1 [%2]").arg(newnameBase).arg(++count); } catch (const MyMoneyException &) { break; } } MyMoneyTag ta; ta.setName(newname); MyMoneyFile::instance()->addTag(ta); id = ta.id(); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(nullptr, i18n("Unable to add tag"), QString::fromLatin1(e.what())); } } } void KMyMoneyUtils::newInstitution(MyMoneyInstitution& institution) { auto file = MyMoneyFile::instance(); MyMoneyFileTransaction ft; try { file->addInstitution(institution); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::information(nullptr, i18n("Cannot add institution: %1", QString::fromLatin1(e.what()))); } } QDebug KMyMoneyUtils::debug() { return qDebug() << QDateTime::currentDateTime().toString(QStringLiteral("HH:mm:ss.zzz")); } MyMoneyForecast KMyMoneyUtils::forecast() { MyMoneyForecast forecast; // override object defaults with those of the application forecast.setForecastCycles(KMyMoneySettings::forecastCycles()); forecast.setAccountsCycle(KMyMoneySettings::forecastAccountCycle()); forecast.setHistoryStartDate(QDate::currentDate().addDays(-forecast.forecastCycles()*forecast.accountsCycle())); forecast.setHistoryEndDate(QDate::currentDate().addDays(-1)); forecast.setForecastDays(KMyMoneySettings::forecastDays()); forecast.setBeginForecastDay(KMyMoneySettings::beginForecastDay()); forecast.setForecastMethod(KMyMoneySettings::forecastMethod()); forecast.setHistoryMethod(KMyMoneySettings::historyMethod()); forecast.setIncludeFutureTransactions(KMyMoneySettings::includeFutureTransactions()); forecast.setIncludeScheduledTransactions(KMyMoneySettings::includeScheduledTransactions()); return forecast; } bool KMyMoneyUtils::canUpdateAllAccounts() { const auto file = MyMoneyFile::instance(); auto rc = false; if (!file->storageAttached()) return rc; QList accList; file->accountList(accList); QList::const_iterator it_a; auto it_p = pPlugins.online.constEnd(); for (it_a = accList.constBegin(); (it_p == pPlugins.online.constEnd()) && (it_a != accList.constEnd()); ++it_a) { if ((*it_a).hasOnlineMapping()) { // check if provider is available it_p = pPlugins.online.constFind((*it_a).onlineBankingSettings().value("provider").toLower()); if (it_p != pPlugins.online.constEnd()) { QStringList protocols; (*it_p)->protocols(protocols); if (!protocols.isEmpty()) { rc = true; break; } } } } return rc; } void KMyMoneyUtils::showStatementImportResult(const QStringList& resultMessages, uint statementCount) { KMessageBox::informationList(nullptr, i18np("One statement has been processed with the following results:", "%1 statements have been processed with the following results:", statementCount), !resultMessages.isEmpty() ? resultMessages : QStringList { i18np("No new transaction has been imported.", "No new transactions have been imported.", statementCount) }, i18n("Statement import statistics")); } + +QString KMyMoneyUtils::normalizeNumericString(const qreal& val, const QLocale& loc, const char f, const int prec) +{ + return loc.toString(val, f, prec) + .remove(loc.groupSeparator()) + .remove(QRegularExpression("0+$")) + .remove(QRegularExpression("\\" + loc.decimalPoint() + "$")); +} diff --git a/kmymoney/kmymoneyutils.h b/kmymoney/kmymoneyutils.h index 90012025d..d7078f6da 100644 --- a/kmymoney/kmymoneyutils.h +++ b/kmymoney/kmymoneyutils.h @@ -1,400 +1,421 @@ /*************************************************************************** kmymoneyutils.h - description ------------------- begin : Wed Feb 5 2003 copyright : (C) 2000-2003 by Michael Edwardes email : mte@users.sourceforge.net Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio ***************************************************************************/ /*************************************************************************** * * * 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. * * * ***************************************************************************/ #ifndef KMYMONEYUTILS_H #define KMYMONEYUTILS_H // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include // ---------------------------------------------------------------------------- // KDE Headers // ---------------------------------------------------------------------------- // Project Includes class QIcon; /** * @author Thomas Baumgart */ static QString m_lastNumberUsed; class QPixmap; class QWizard; class QWidget; class KGuiItem; class KXmlGuiWindow; class MyMoneyMoney; class MyMoneyAccount; class MyMoneySecurity; class MyMoneySchedule; class MyMoneySplit; class MyMoneyTransaction; class MyMoneyStatement; class MyMoneyInstitution; class MyMoneyForecast; namespace eMyMoney { namespace Schedule { enum class Occurrence; enum class PaymentType; enum class WeekendOption; enum class Type; } namespace Split { enum class State; enum class InvestmentTransactionType; } } class KMyMoneyUtils { public: enum transactionTypeE { /** * Unknown transaction type (e.g. used for a transaction with only * a single split) */ Unknown, /** * A 'normal' transaction is one that consists out two splits: one * referencing an income/expense account, the other referencing * an asset/liability account. */ Normal, /** * A transfer denotes a transaction consisting of two splits. * Both of the splits reference an asset/liability * account. */ Transfer, /** * Whenever a transaction consists of more than 2 splits, * it is treated as 'split transaction'. */ SplitTransaction, /** * This transaction denotes a specific transaction where * a loan account is involved. Usually, a special dialog * is used to modify this transaction. */ LoanPayment, /** * This transaction denotes a specific transaction where * an investment is involved. Usually, a special dialog * is used to modify this transaction. */ InvestmentTransaction }; static const int maxHomePageItems = 5; KMyMoneyUtils(); ~KMyMoneyUtils(); /** * This method is used to convert the occurrence type from its * internal representation into a human readable format. * * @param occurrence numerical representation of the MyMoneySchedule * occurrence type * * @return QString representing the human readable format translated according to the language catalog * * @sa MyMoneySchedule::occurrenceToString() * * @deprecated Use i18n(MyMoneySchedule::occurrenceToString(occurrence)) instead */ static const QString occurrenceToString(const eMyMoney::Schedule::Occurrence occurrence); /** * This method is used to convert the payment type from its * internal representation into a human readable format. * * @param paymentType numerical representation of the MyMoneySchedule * payment type * * @return QString representing the human readable format translated according to the language catalog * * @sa MyMoneySchedule::paymentMethodToString() */ static const QString paymentMethodToString(eMyMoney::Schedule::PaymentType paymentType); /** * This method is used to convert the schedule weekend option from its * internal representation into a human readable format. * * @param weekendOption numerical representation of the MyMoneySchedule * weekend option * * @return QString representing the human readable format translated according to the language catalog * * @sa MyMoneySchedule::weekendOptionToString() */ static const QString weekendOptionToString(eMyMoney::Schedule::WeekendOption weekendOption); /** * This method is used to convert the schedule type from its * internal representation into a human readable format. * * @param type numerical representation of the MyMoneySchedule * schedule type * * @return QString representing the human readable format translated according to the language catalog * * @sa MyMoneySchedule::scheduleTypeToString() */ static const QString scheduleTypeToString(eMyMoney::Schedule::Type type); /** * This method is used to convert a numeric index of an item * represented on the home page into its string form. * * @param idx numeric index of item * * @return QString with text of this item */ static const QString homePageItemToString(const int idx); /** * This method is used to convert the name of a home page item * to its internal numerical representation * * @param txt QString reference of the items name * * @retval 0 @p txt is unknown * @retval >0 numeric value for @p txt */ static int stringToHomePageItem(const QString& txt); /** * Retrieve a KDE KGuiItem for the new schedule button. * * @return The KGuiItem that can be used to display the icon and text */ static KGuiItem scheduleNewGuiItem(); /** * Retrieve a KDE KGuiItem for the account filter button * * @return The KGuiItem that can be used to display the icon and text */ static KGuiItem accountsFilterGuiItem(); /** * This method adds the file extension passed as argument @p extension * to the end of the file name passed as argument @p name if it is not present. * If @p name contains an extension it will be removed. * * @param name filename to be checked * @param extension extension to be added (w/o the dot) * * @retval true if @p name was changed * @retval false if @p name remained unchanged */ static bool appendCorrectFileExt(QString& name, const QString& extension); /** * Check that internal MyMoney engine constants use the same * values as the KDE constants. */ static void checkConstants(); static QString variableCSS(); /** * This method searches a KDE specific resource and applies country and * language settings during the search. Therefore, the parameter @p filename must contain * the characters '%1' which gets replaced with the language/country values. * * The search is performed in the following order (stopped immediately if a file was found): * - @c \%1 is replaced with _\.\ * - @c \%1 is replaced with _\ * - @c \%1 is replaced with _\ * - @c \%1 is replaced with the empty string * * @c \ and @c \ denote the respective KDE settings. * * Example: The KDE settings for country is Spain (es) and language is set * to Galician (gl). The code for looking up a file looks like this: * * @code * * : * QString fname = KMyMoneyUtils::findResource("appdata", "html/home%1.html") * : * * @endcode * * The method calls KStandardDirs::findResource() with the following values for the * parameter @p filename: * * - html/home_es.gl.html * - html/home_gl.html * - html/home_es.html * - html/home.html * * @note See KStandardDirs::findResource() for details on the parameters */ static QString findResource(QStandardPaths::StandardLocation type, const QString& filename); /** * This method returns the split referencing a stock account if * one exists in the transaction passed as @p t. If none is present * in @p t, an empty MyMoneySplit() object will be returned. * * @param t transaction to be checked for a stock account * @return MyMoneySplit object referencing a stock account or an * empty MyMoneySplit object. */ static const MyMoneySplit stockSplit(const MyMoneyTransaction& t); /** * This method analyses the splits of a transaction and returns * the type of transaction. Possible values are defined by the * KMyMoneyUtils::transactionTypeE enum. * * @param t const reference to the transaction * * @return KMyMoneyUtils::transactionTypeE value of the action */ static transactionTypeE transactionType(const MyMoneyTransaction& t); /** * This method modifies a scheduled loan transaction such that all * references to automatic calculated values are resolved to actual values. * * @param schedule const reference to the schedule the transaction is based on * @param transaction reference to the transaction to be checked and modified * @param balances QMap of (account-id,balance) pairs to be used as current balance * for the calculation of interest. If map is empty, the engine * will be interrogated for current balances. */ static void calculateAutoLoan(const MyMoneySchedule& schedule, MyMoneyTransaction& transaction, const QMap& balances); /** * Returns the next check number for account @a acc. No check is performed, if the * number is already in use. */ static QString nextCheckNumber(const MyMoneyAccount& acc); /** * Returns the next check free number for account @a acc. */ static QString nextFreeCheckNumber(const MyMoneyAccount& acc); // static void setLastNumberUsed(const QString& num); // static QString lastNumberUsed(); /** * Returns previous number if offset is -1 or * the following number if offset is 1. */ static QString getAdjacentNumber(const QString& number, int offset = 1); /** * remove any non-numeric characters from check number * to allow validity check */ static quint64 numericPart(const QString & num); /** * Returns the text representing the reconcile flag. If @a text is @p true * then the full text will be returned otherwise a short form (usually one character). */ static QString reconcileStateToString(eMyMoney::Split::State flag, bool text = false); /** * Returns the transaction for @a schedule. In case of a loan payment the * transaction will be modified by calculateAutoLoan(). * The ID of the transaction as well as the entryDate will be reset. * * @returns adjusted transaction */ static MyMoneyTransaction scheduledTransaction(const MyMoneySchedule& schedule); /** * This method replaces the deprecated QApplication::mainWidget() from Qt 3.x. * It assumes that there is only one KXmlGuiWindow in the application, and * returns it. * * @return the first KXmlGuiWindow found in QApplication::topLevelWidgets() */ static KXmlGuiWindow* mainWindow(); /** * This method sets the button text and icons to the KDE standard ones * for the QWizard passed as argument. */ static void updateWizardButtons(QWizard *); static void dissectTransaction(const MyMoneyTransaction& transaction, const MyMoneySplit& split, MyMoneySplit& assetAccountSplit, QList& feeSplits, QList& interestSplits, MyMoneySecurity& security, MyMoneySecurity& currency, eMyMoney::Split::InvestmentTransactionType& transactionType); static void processPriceList(const MyMoneyStatement& st); /** * This method deletes security and associated price list but asks beforehand. */ static void deleteSecurity(const MyMoneySecurity &security, QWidget *parent = nullptr); /** * Check whether the url links to an existing file or not * @returns whether the file exists or not */ static bool fileExists(const QUrl &url); static QString downloadFile(const QUrl &url); static bool newPayee(const QString& newnameBase, QString& id); static void newTag(const QString& newnameBase, QString& id); /** * Creates a new institution entry in the MyMoneyFile engine * * @param institution MyMoneyInstitution object containing the data of * the institution to be created. */ static void newInstitution(MyMoneyInstitution& institution); static QDebug debug(); static MyMoneyForecast forecast(); static bool canUpdateAllAccounts(); static void showStatementImportResult(const QStringList& resultMessages, uint statementCount); + + /** + * This method returns a double converted into a QString + * after removing any group separators, trailing zeros, + * and decimal separators. + * + * @param val reference to a qreal value to be converted + * @param loc reference to a Qlocale converter + * @param f format specifier: + * e - format as [-]9.9e[+|-]999 + * E - format as [-]9.9E[+|-]999 + * f - format as [-]9.9 + * g - use e or f format, whichever is the most concise + * G - use E or f format, whichever is the most concise + * @param prec precision representing the number of digits + * after the decimal point ('e', 'E' and 'f' formats) + * or the maximum number of significant digits + * (trailing zeroes are omitted) ('g' and 'G' formats) + * @return QString object containing the converted value + */ + static QString normalizeNumericString(const qreal& val, const QLocale& loc, const char f = 'g', const int prec = 6); }; #endif diff --git a/kmymoney/plugins/views/reports/reporttabimpl.cpp b/kmymoney/plugins/views/reports/reporttabimpl.cpp index b80e3c094..41bab6dc6 100644 --- a/kmymoney/plugins/views/reports/reporttabimpl.cpp +++ b/kmymoney/plugins/views/reports/reporttabimpl.cpp @@ -1,351 +1,438 @@ /* This file is part of the KDE project Copyright (C) 2009 Laurent Montel (C) 2017 by Łukasz Wojniłowicz Copyright 2018 Michael Kiefer + Copyright 2020 Robert Szczesiak This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "reporttabimpl.h" #include +#include + +#include "kmymoneyutils.h" #include "daterangedlg.h" #include "ui_reporttabgeneral.h" #include "ui_reporttabrowcolpivot.h" #include "ui_reporttabrowcolquery.h" #include "ui_reporttabchart.h" #include "ui_reporttabrange.h" #include "ui_reporttabcapitalgain.h" #include "ui_reporttabperformance.h" #include "mymoney/mymoneyreport.h" #include "mymoneyenums.h" ReportTabGeneral::ReportTabGeneral(QWidget *parent) : QWidget(parent) { ui = new Ui::ReportTabGeneral; ui->setupUi(this); } ReportTabGeneral::~ReportTabGeneral() { delete ui; } ReportTabRowColPivot::ReportTabRowColPivot(QWidget *parent) : QWidget(parent) { ui = new Ui::ReportTabRowColPivot; ui->setupUi(this); } ReportTabRowColPivot::~ReportTabRowColPivot() { delete ui; } ReportTabRowColQuery::ReportTabRowColQuery(QWidget *parent) : QWidget(parent) { ui = new Ui::ReportTabRowColQuery; ui->setupUi(this); ui->buttonGroup1->setExclusive(false); ui->buttonGroup1->setId(ui->m_checkMemo, 0); ui->buttonGroup1->setId(ui->m_checkShares, 1); ui->buttonGroup1->setId(ui->m_checkPrice, 2); ui->buttonGroup1->setId(ui->m_checkReconciled, 3); ui->buttonGroup1->setId(ui->m_checkAccount, 4); ui->buttonGroup1->setId(ui->m_checkNumber, 5); ui->buttonGroup1->setId(ui->m_checkPayee, 6); ui->buttonGroup1->setId(ui->m_checkCategory, 7); ui->buttonGroup1->setId(ui->m_checkAction, 8); ui->buttonGroup1->setId(ui->m_checkBalance, 9); connect(ui->m_checkHideTransactions, &QAbstractButton::toggled, this, &ReportTabRowColQuery::slotHideTransactionsChanged); } void ReportTabRowColQuery::slotHideTransactionsChanged(bool checked) { if (checked) // toggle m_checkHideSplitDetails only if it's mandatory ui->m_checkHideSplitDetails->setChecked(checked); ui->m_checkHideSplitDetails->setEnabled(!checked); // hiding transactions without hiding splits isn't allowed } ReportTabRowColQuery::~ReportTabRowColQuery() { delete ui; } ReportTabChart::ReportTabChart(QWidget *parent) : QWidget(parent) { ui = new Ui::ReportTabChart; ui->setupUi(this); ui->m_comboType->addItem(i18nc("type of graphic chart", "Line"), static_cast(eMyMoney::Report::ChartType::Line)); ui->m_comboType->addItem(i18nc("type of graphic chart", "Bar"), static_cast(eMyMoney::Report::ChartType::Bar)); ui->m_comboType->addItem(i18nc("type of graphic chart", "Stacked Bar"), static_cast(eMyMoney::Report::ChartType::StackedBar)); ui->m_comboType->addItem(i18nc("type of graphic chart", "Pie"), static_cast(eMyMoney::Report::ChartType::Pie)); ui->m_comboType->addItem(i18nc("type of graphic chart", "Ring"), static_cast(eMyMoney::Report::ChartType::Ring)); connect(ui->m_comboType, static_cast(&QComboBox::currentIndexChanged), this, &ReportTabChart::slotChartTypeChanged); emit ui->m_comboType->currentIndexChanged(ui->m_comboType->currentIndex()); ui->m_comboPalette->addItem(i18nc("type of graphic palette", "Use application setting"), static_cast(eMyMoney::Report::ChartPalette::Application)); ui->m_comboPalette->addItem(i18nc("type of graphic palette", "Default"), static_cast(eMyMoney::Report::ChartPalette::Default)); ui->m_comboPalette->addItem(i18nc("type of graphic palette", "Rainbow"), static_cast(eMyMoney::Report::ChartPalette::Rainbow)); ui->m_comboPalette->addItem(i18nc("type of graphic palette", "Subdued"), static_cast(eMyMoney::Report::ChartPalette::Subdued)); } ReportTabChart::~ReportTabChart() { delete ui; } void ReportTabChart::slotChartTypeChanged(int index) { if (index == static_cast(eMyMoney::Report::ChartType::Pie) || index == static_cast(eMyMoney::Report::ChartType::Ring)) { ui->m_checkCHGridLines->setText(i18n("Show circular grid lines")); ui->m_checkSVGridLines->setText(i18n("Show sagittal grid lines")); ui->m_logYaxis->setChecked(false); ui->m_logYaxis->setEnabled(false); ui->m_negExpenses->setChecked(false); ui->m_negExpenses->setEnabled(false); } else { ui->m_checkCHGridLines->setText(i18n("Show horizontal grid lines")); ui->m_checkSVGridLines->setText(i18n("Show vertical grid lines")); ui->m_logYaxis->setEnabled(true); ui->m_negExpenses->setEnabled(true); } } void ReportTabChart::setNegExpenses(bool set) { // logarithm on negative numbers does not make sense, so disable it if (set) { ui->m_logYaxis->setChecked(false); ui->m_logYaxis->setEnabled(false); } else { ui->m_logYaxis->setEnabled(true); } } ReportTabRange::ReportTabRange(QWidget *parent) : QWidget(parent), - ui(new Ui::ReportTabRange) + ui(new Ui::ReportTabRange), + m_logYaxis(false) { ui->setupUi(this); m_dateRange = new DateRangeDlg; ui->dateRangeGrid->addWidget(m_dateRange, 0, 0, 1, 2); connect(ui->m_yLabelsPrecision, static_cast(&QSpinBox::valueChanged), this, &ReportTabRange::slotYLabelsPrecisionChanged); emit ui->m_yLabelsPrecision->valueChanged(ui->m_yLabelsPrecision->value()); connect(ui->m_dataRangeStart, &QLineEdit::editingFinished, this, &ReportTabRange::slotEditingFinishedStart); connect(ui->m_dataRangeEnd, &QLineEdit::editingFinished, this, &ReportTabRange::slotEditingFinishedEnd); connect(ui->m_dataMajorTick, &QLineEdit::editingFinished, this, &ReportTabRange::slotEditingFinishedMajor); connect(ui->m_dataMinorTick, &QLineEdit::editingFinished, this, &ReportTabRange::slotEditingFinishedMinor); connect(ui->m_dataLock, static_cast(&QComboBox::currentIndexChanged), this, &ReportTabRange::slotDataLockChanged); emit ui->m_dataLock->currentIndexChanged(ui->m_dataLock->currentIndex()); } ReportTabRange::~ReportTabRange() { delete ui; } void ReportTabRange::setRangeLogarythmic(bool set) { // major and minor tick have no influence if axis is logarithmic so hide them if (set) { ui->lblDataMajorTick->hide(); ui->lblDataMinorTick->hide(); ui->m_dataMajorTick->hide(); ui->m_dataMinorTick->hide(); + + m_logYaxis = true; } else { ui->lblDataMajorTick->show(); ui->lblDataMinorTick->show(); ui->m_dataMajorTick->show(); ui->m_dataMinorTick->show(); + + m_logYaxis = false; } + + updateDataRangeValidators(ui->m_yLabelsPrecision->value()); // update data range validators and re-validate } -void ReportTabRange::slotEditingFinished(EDimension dim) +void ReportTabRange::updateDataRangeValidators(const int& precision) { - qreal dataRangeStart = locale().toDouble(ui->m_dataRangeStart->text()); - qreal dataRangeEnd = locale().toDouble(ui->m_dataRangeEnd->text()); - qreal dataMajorTick = locale().toDouble(ui->m_dataMajorTick->text()); - qreal dataMinorTick = locale().toDouble(ui->m_dataMinorTick->text()); - if (dataRangeEnd < dataRangeStart) { // end must be higher than start - if (dim == eRangeEnd) { - ui->m_dataRangeStart->setText(ui->m_dataRangeEnd->text()); - dataRangeStart = dataRangeEnd; - } else { - ui->m_dataRangeEnd->setText(ui->m_dataRangeStart->text()); - dataRangeEnd = dataRangeStart; + ui->m_dataRangeStart->setValidator(nullptr); + ui->m_dataRangeEnd->setValidator(nullptr); + + QDoubleValidator *dbValStart; + QDoubleValidator *dbValEnd; + if (m_logYaxis) { + dbValStart = new MyLogarithmicDoubleValidator(precision, qPow(10, -precision), ui->m_dataRangeStart); + dbValEnd = new MyLogarithmicDoubleValidator(precision, qPow(10, -precision + 4), ui->m_dataRangeEnd); + } else { // the validator will be used by two QLineEdit objects so let this tab be their parent + dbValStart = new MyDoubleValidator(precision, this); + dbValEnd = dbValStart; } - } - if ((dataRangeStart != 0 || dataRangeEnd != 0)) { // if data range isn't going to be reset - if ((dataRangeEnd - dataRangeStart) < dataMajorTick) // major tick cannot be greater than data range - dataMajorTick = dataRangeEnd - dataRangeStart; - if (dataMajorTick != 0 && // if major tick isn't going to be reset - dataMajorTick < (dataRangeEnd - dataRangeStart) * 0.01) // constraint major tick to be greater or equal to 1% of data range - dataMajorTick = (dataRangeEnd - dataRangeStart) * 0.01; + ui->m_dataRangeStart->setValidator(dbValStart); + ui->m_dataRangeEnd->setValidator(dbValEnd); - //set precision of major tick to be greater by 1 - ui->m_dataMajorTick->setText(locale().toString(dataMajorTick, 'f', ui->m_yLabelsPrecision->value() + 1).remove(locale().groupSeparator()).remove(QRegularExpression("0+$")).remove(QRegularExpression("\\" + locale().decimalPoint() + "$"))); - } - - if (dataMajorTick < dataMinorTick) { // major tick must be higher than minor - if (dim == eMinorTick) { - ui->m_dataMajorTick->setText(ui->m_dataMinorTick->text()); - dataMajorTick = dataMinorTick; - } else { - ui->m_dataMinorTick->setText(ui->m_dataMajorTick->text()); - dataMinorTick = dataMajorTick; + QString dataRangeStart = ui->m_dataRangeStart->text(); + QString dataRangeEnd = ui->m_dataRangeEnd->text(); + if (!ui->m_dataRangeStart->hasAcceptableInput()) { + dbValStart->fixup(dataRangeStart); + ui->m_dataRangeStart->setText(dataRangeStart); } - } + if (ui->m_dataRangeEnd->hasAcceptableInput()) { + dbValEnd->fixup(dataRangeEnd); + ui->m_dataRangeEnd->setText(dataRangeEnd); + } +} - if (dataMinorTick < dataMajorTick * 0.1) { // constraint minor tick to be greater or equal to 10% of major tick, and set precision to be greater by 1 - dataMinorTick = dataMajorTick * 0.1; - ui->m_dataMinorTick->setText(locale().toString(dataMinorTick, 'f', ui->m_yLabelsPrecision->value() + 1).remove(locale().groupSeparator()).remove(QRegularExpression("0+$")).remove(QRegularExpression("\\" + locale().decimalPoint() + "$"))); - } +void ReportTabRange::slotEditingFinished(EDimension dim) +{ + qreal dataRangeStart = locale().toDouble(ui->m_dataRangeStart->text()); + qreal dataRangeEnd = locale().toDouble(ui->m_dataRangeEnd->text()); + + if (dataRangeEnd < dataRangeStart) { // end must be higher than start + if (dim == eRangeEnd) { + ui->m_dataRangeStart->setText(ui->m_dataRangeEnd->text()); + dataRangeStart = dataRangeEnd; + } else { + ui->m_dataRangeEnd->setText(ui->m_dataRangeStart->text()); + dataRangeEnd = dataRangeStart; + } + } + if (!m_logYaxis) { // major and minor ticks only have influence when axis is linear + qreal dataMajorTick = locale().toDouble(ui->m_dataMajorTick->text()); + qreal dataMinorTick = locale().toDouble(ui->m_dataMinorTick->text()); + if ((dataRangeStart != 0 || dataRangeEnd != 0)) { // if data range isn't going to be reset + if ((dataRangeEnd - dataRangeStart) < dataMajorTick) // major tick cannot be greater than data range + dataMajorTick = dataRangeEnd - dataRangeStart; + + if (dataMajorTick != 0 && // if major tick isn't going to be reset + dataMajorTick < (dataRangeEnd - dataRangeStart) * 0.01) // constraint major tick to be greater or equal to 1% of data range + dataMajorTick = (dataRangeEnd - dataRangeStart) * 0.01; + + //set precision of major tick to be greater by 1 + ui->m_dataMajorTick->setText(locale().toString(dataMajorTick, 'f', ui->m_yLabelsPrecision->value() + 1).remove(locale().groupSeparator()).remove(QRegularExpression("0+$")).remove(QRegularExpression("\\" + locale().decimalPoint() + "$"))); + } + + if (dataMajorTick < dataMinorTick) { // major tick must be higher than minor + if (dim == eMinorTick) { + ui->m_dataMajorTick->setText(ui->m_dataMinorTick->text()); + dataMajorTick = dataMinorTick; + } else { + ui->m_dataMinorTick->setText(ui->m_dataMajorTick->text()); + dataMinorTick = dataMajorTick; + } + } + + if (dataMinorTick < dataMajorTick * 0.1) { // constraint minor tick to be greater or equal to 10% of major tick, and set precision to be greater by 1 + dataMinorTick = dataMajorTick * 0.1; + ui->m_dataMinorTick->setText(locale().toString(dataMinorTick, 'f', ui->m_yLabelsPrecision->value() + 1).remove(locale().groupSeparator()).remove(QRegularExpression("0+$")).remove(QRegularExpression("\\" + locale().decimalPoint() + "$"))); + } + } } void ReportTabRange::slotEditingFinishedStart() { slotEditingFinished(eRangeStart); } void ReportTabRange::slotEditingFinishedEnd() { slotEditingFinished(eRangeEnd); } void ReportTabRange::slotEditingFinishedMajor() { slotEditingFinished(eMajorTick); } void ReportTabRange::slotEditingFinishedMinor() { slotEditingFinished(eMinorTick); } void ReportTabRange::slotYLabelsPrecisionChanged(const int& value) { - ui->m_dataRangeStart->setValidator(0); - ui->m_dataRangeEnd->setValidator(0); - ui->m_dataMajorTick->setValidator(0); - ui->m_dataMinorTick->setValidator(0); + ui->m_dataMajorTick->setValidator(0); + ui->m_dataMinorTick->setValidator(0); + + MyDoubleValidator *dblVal2 = new MyDoubleValidator(value + 1); + ui->m_dataMajorTick->setValidator(dblVal2); + ui->m_dataMinorTick->setValidator(dblVal2); - MyDoubleValidator *dblVal = new MyDoubleValidator(value); - ui->m_dataRangeStart->setValidator(dblVal); - ui->m_dataRangeEnd->setValidator(dblVal); - MyDoubleValidator *dblVal2 = new MyDoubleValidator(value + 1); - ui->m_dataMajorTick->setValidator(dblVal2); - ui->m_dataMinorTick->setValidator(dblVal2); + updateDataRangeValidators(value); } void ReportTabRange::slotDataLockChanged(int index) { if (index == static_cast(eMyMoney::Report::DataLock::Automatic)) { ui->m_dataRangeStart->setText(QStringLiteral("0")); ui->m_dataRangeEnd->setText(QStringLiteral("0")); ui->m_dataMajorTick->setText(QStringLiteral("0")); ui->m_dataMinorTick->setText(QStringLiteral("0")); ui->m_dataRangeStart->setEnabled(false); ui->m_dataRangeEnd->setEnabled(false); ui->m_dataMajorTick->setEnabled(false); ui->m_dataMinorTick->setEnabled(false); } else { ui->m_dataRangeStart->setEnabled(true); ui->m_dataRangeEnd->setEnabled(true); ui->m_dataMajorTick->setEnabled(true); ui->m_dataMinorTick->setEnabled(true); } } ReportTabCapitalGain::ReportTabCapitalGain(QWidget *parent) : QWidget(parent) { ui = new Ui::ReportTabCapitalGain; ui->setupUi(this); connect(ui->m_investmentSum, static_cast(&QComboBox::currentIndexChanged), this, &ReportTabCapitalGain::slotInvestmentSumChanged); } ReportTabCapitalGain::~ReportTabCapitalGain() { delete ui; } void ReportTabCapitalGain::slotInvestmentSumChanged(int index) { Q_UNUSED(index); if (ui->m_investmentSum->currentData() == static_cast(eMyMoney::Report::InvestmentSum::Owned)) { ui->m_settlementPeriod->setValue(0); ui->m_settlementPeriod->setEnabled(false); ui->m_showSTLTCapitalGains->setChecked(false); ui->m_showSTLTCapitalGains->setEnabled(false); ui->m_termSeparator->setEnabled(false); } else { ui->m_settlementPeriod->setEnabled(true); ui->m_showSTLTCapitalGains->setEnabled(true); ui->m_termSeparator->setEnabled(true); } } ReportTabPerformance::ReportTabPerformance(QWidget *parent) : QWidget(parent) { ui = new Ui::ReportTabPerformance; ui->setupUi(this); } ReportTabPerformance::~ReportTabPerformance() { delete ui; } MyDoubleValidator::MyDoubleValidator(int decimals, QObject * parent) : QDoubleValidator(0, 0, decimals, parent) { } QValidator::State MyDoubleValidator::validate(QString &s, int &i) const { Q_UNUSED(i); if (s.isEmpty() || s == "-") { return QValidator::Intermediate; } QChar decimalPoint = locale().decimalPoint(); if(s.indexOf(decimalPoint) != -1) { int charsAfterPoint = s.length() - s.indexOf(decimalPoint) - 1; if (charsAfterPoint > decimals()) { return QValidator::Invalid; } } bool ok; locale().toDouble(s, &ok); if (ok) { return QValidator::Acceptable; } else { return QValidator::Invalid; } } + +MyLogarithmicDoubleValidator::MyLogarithmicDoubleValidator(const int decimals, const qreal defaultValue, QObject *parent) + : QDoubleValidator(qPow(10, -decimals), 0, decimals, parent) +{ + m_defaultText = KMyMoneyUtils::normalizeNumericString(defaultValue, locale(), 'f', decimals); +} + +QValidator::State MyLogarithmicDoubleValidator::validate(QString &s, int &i) const +{ + Q_UNUSED(i); + if (s.isEmpty() || s == QStringLiteral("0")) { + return QValidator::Intermediate; + } + + QChar decimalPoint = locale().decimalPoint(); + + // start numbering placeholders with a two-digit number to avoid + // interpreting the following zero as part of the placeholder index + const QRegularExpression re((QStringLiteral("^0\\%110{0,%12}$") + .arg(decimalPoint) + .arg(decimals() - 1))); + if (re.match(s).hasMatch()) + return QValidator::Intermediate; + + if (s.indexOf(decimalPoint) != -1) { + int charsAfterPoint = s.length() - s.indexOf(decimalPoint) - 1; + + if (charsAfterPoint > decimals()) { + return QValidator::Invalid; + } + } + + bool ok; + const qreal result = locale().toDouble(s, &ok); + + if (ok && result >= bottom()) { + return QValidator::Acceptable; + } else { + return QValidator::Invalid; + } +} + +void MyLogarithmicDoubleValidator::fixup(QString &input) const +{ + input = m_defaultText; +} diff --git a/kmymoney/plugins/views/reports/reporttabimpl.h b/kmymoney/plugins/views/reports/reporttabimpl.h index 1dc9613a8..1b05b3c44 100644 --- a/kmymoney/plugins/views/reports/reporttabimpl.h +++ b/kmymoney/plugins/views/reports/reporttabimpl.h @@ -1,149 +1,168 @@ /* This file is part of the KDE project Copyright (C) 2009 Laurent Montel (C) 2017 by Łukasz Wojniłowicz 2018 by Michael Kiefer This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef REPORTTABIMPL_H #define REPORTTABIMPL_H #include #include class DateRangeDlg; namespace Ui { class ReportTabGeneral; class ReportTabRowColPivot; class ReportTabRowColQuery; class ReportTabChart; class ReportTabRange; class ReportTabCapitalGain; class ReportTabPerformance; } class ReportTabGeneral : public QWidget { Q_DISABLE_COPY(ReportTabGeneral) public: explicit ReportTabGeneral(QWidget *parent); ~ReportTabGeneral(); Ui::ReportTabGeneral* ui; }; class ReportTabRowColPivot : public QWidget { Q_DISABLE_COPY(ReportTabRowColPivot) public: explicit ReportTabRowColPivot(QWidget *parent); ~ReportTabRowColPivot(); Ui::ReportTabRowColPivot* ui; }; class ReportTabRowColQuery : public QWidget { Q_OBJECT Q_DISABLE_COPY(ReportTabRowColQuery) public: explicit ReportTabRowColQuery(QWidget *parent); ~ReportTabRowColQuery(); Ui::ReportTabRowColQuery* ui; private Q_SLOTS: void slotHideTransactionsChanged(bool checked); }; class ReportTabChart : public QWidget { Q_OBJECT Q_DISABLE_COPY(ReportTabChart) public: explicit ReportTabChart(QWidget *parent); ~ReportTabChart(); Ui::ReportTabChart* ui; void setNegExpenses(bool set); private Q_SLOTS: void slotChartTypeChanged(int index); }; class ReportTabRange : public QWidget { Q_OBJECT Q_DISABLE_COPY(ReportTabRange) public: explicit ReportTabRange(QWidget *parent); ~ReportTabRange(); Ui::ReportTabRange* ui; DateRangeDlg *m_dateRange; void setRangeLogarythmic(bool set); private: enum EDimension { eRangeStart = 0, eRangeEnd, eMajorTick, eMinorTick}; + bool m_logYaxis; + /** + * Update data range start and data range end text validators + * and re-validate the contents of those text fields against the updated validator. + * If re-validation fails, arbitrary default values will be set depending on vertical axis type. + * This fucntion should be called when vertical axis type or labels precision changed. + */ + void updateDataRangeValidators(const int& precision); private Q_SLOTS: void slotEditingFinished(EDimension dim); void slotEditingFinishedStart(); void slotEditingFinishedEnd(); void slotEditingFinishedMajor(); void slotEditingFinishedMinor(); void slotYLabelsPrecisionChanged(const int &value); void slotDataLockChanged(int index); }; class ReportTabCapitalGain : public QWidget { Q_OBJECT Q_DISABLE_COPY(ReportTabCapitalGain) public: explicit ReportTabCapitalGain(QWidget *parent); ~ReportTabCapitalGain(); Ui::ReportTabCapitalGain* ui; private Q_SLOTS: void slotInvestmentSumChanged(int index); }; class ReportTabPerformance : public QWidget { public: explicit ReportTabPerformance(QWidget *parent); ~ReportTabPerformance(); Ui::ReportTabPerformance* ui; }; class MyDoubleValidator : public QDoubleValidator { public: explicit MyDoubleValidator(int decimals, QObject * parent = 0); QValidator::State validate(QString &s, int &i) const final override; }; + +class MyLogarithmicDoubleValidator : public QDoubleValidator +{ +public: + explicit MyLogarithmicDoubleValidator(const int decimals, const qreal defaultValue, QObject *parent = nullptr); + + QValidator::State validate(QString &s, int &i) const final Q_DECL_OVERRIDE; + void fixup(QString &input) const final Q_DECL_OVERRIDE; +private: + QString m_defaultText; +}; #endif /* REPORTTABIMPL_H */