diff --git a/kmymoney/dialogs/investactivities.cpp b/kmymoney/dialogs/investactivities.cpp index ec932cafe..b7545f1d8 100644 --- a/kmymoney/dialogs/investactivities.cpp +++ b/kmymoney/dialogs/investactivities.cpp @@ -1,977 +1,977 @@ /*************************************************************************** investactivities.cpp ---------- begin : Fri Dec 15 2006 copyright : (C) 2006 by Thomas Baumgart email : Thomas Baumgart (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 "investactivities.h" // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "investtransactioneditor.h" #include "mymoneymoney.h" #include "kmymoneycategory.h" #include "kmymoneyedit.h" #include "kmymoneyaccountselector.h" #include "kmymoneycompletion.h" #include #include "mymoneyfile.h" #include "mymoneysplit.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "dialogenums.h" #include "mymoneyenums.h" using namespace Invest; using namespace KMyMoneyRegister; class Invest::ActivityPrivate { Q_DISABLE_COPY(ActivityPrivate) public: ActivityPrivate() : m_parent(nullptr), m_memoChanged(false) { } InvestTransactionEditor *m_parent; QMap m_priceInfo; bool m_memoChanged; QString m_memoText; }; Activity::Activity(InvestTransactionEditor* editor) : d_ptr(new ActivityPrivate) { Q_D(Activity); d->m_memoChanged = false; d->m_parent = editor; } Activity::~Activity() { Q_D(Activity); delete d; } bool& Activity::memoChanged() { Q_D(Activity); return d->m_memoChanged; } QString& Activity::memoText() { Q_D(Activity); return d->m_memoText; } bool Activity::isComplete(QString& reason) const { Q_D(const Activity); Q_UNUSED(reason) auto rc = false; auto security = dynamic_cast(haveWidget("security")); if (security && !security->currentText().isEmpty()) { rc = (security->selector()->contains(security->currentText()) || (isMultiSelection() && d->m_memoChanged)); } return rc; } QWidget* Activity::haveWidget(const QString& name) const { Q_D(const Activity); return d->m_parent->haveWidget(name); } bool Activity::haveAssetAccount() const { auto rc = true; auto cat = dynamic_cast(haveWidget("asset-account")); if (!cat) return false; if (!isMultiSelection()) rc = !cat->currentText().isEmpty(); if (rc && !cat->currentText().isEmpty()) rc = cat->selector()->contains(cat->currentText()); return rc; } bool Activity::haveCategoryAndAmount(const QString& category, const QString& amount, bool optional) const { Q_D(const Activity); auto cat = dynamic_cast(haveWidget(category)); auto rc = true; if (cat && !cat->currentText().isEmpty()) { rc = cat->selector()->contains(cat->currentText()) || cat->isSplitTransaction(); if (rc && !amount.isEmpty() && !isMultiSelection()) { if (cat->isSplitTransaction()) { QList::const_iterator split; QList::const_iterator splitEnd; if (category == "fee-account") { split = d->m_parent->feeSplits().cbegin(); splitEnd = d->m_parent->feeSplits().cend(); } else if (category == "interest-account") { split = d->m_parent->interestSplits().cbegin(); splitEnd = d->m_parent->interestSplits().cend(); } for (; split != splitEnd; ++split) { if ((*split).value().isZero()) rc = false; } } else { if (auto valueWidget = dynamic_cast(haveWidget(amount))) rc = !valueWidget->value().isZero(); } } } else if (!isMultiSelection() && !optional) { rc = false; } return rc; } bool Activity::haveFees(bool optional) const { return haveCategoryAndAmount("fee-account", "fee-amount", optional); } bool Activity::haveInterest(bool optional) const { return haveCategoryAndAmount("interest-account", "interest-amount", optional); } bool Activity::haveShares() const { if (auto amount = dynamic_cast(haveWidget("shares"))) { if (isMultiSelection() && amount->value().isZero()) return true; return !amount->value().isZero(); } return false; } bool Activity::havePrice() const { if (auto amount = dynamic_cast(haveWidget("price"))) { if (isMultiSelection() && amount->value().isZero()) return true; return !amount->value().isZero(); } return false; } bool Activity::isMultiSelection() const { Q_D(const Activity); return d->m_parent->isMultiSelection(); } bool Activity::createCategorySplits(const MyMoneyTransaction& t, KMyMoneyCategory* cat, KMyMoneyEdit* amount, MyMoneyMoney factor, QList&splits, const QList& osplits) const { Q_D(const Activity); auto rc = true; if (!isMultiSelection() || (isMultiSelection() && !cat->currentText().isEmpty())) { if (!cat->isSplitTransaction()) { splits.clear(); MyMoneySplit s1; QString categoryId; categoryId = cat->selectedItem(); if (!categoryId.isEmpty()) { s1.setAccountId(categoryId); s1.setValue(amount->value() * factor); if (!s1.value().isZero()) { rc = d->m_parent->setupPrice(t, s1); } splits.append(s1); } } else { splits = osplits; } } return rc; } void Activity::createAssetAccountSplit(MyMoneySplit& split, const MyMoneySplit& stockSplit) const { auto cat = dynamic_cast(haveWidget("asset-account")); - if (!isMultiSelection() || (isMultiSelection() && cat && !cat->currentText().isEmpty())) { + if (cat && (!isMultiSelection() || (isMultiSelection() && !cat->currentText().isEmpty()))) { auto categoryId = cat->selectedItem(); split.setAccountId(categoryId); } split.setMemo(stockSplit.memo()); } MyMoneyMoney Activity::sumSplits(const MyMoneySplit& s0, const QList& feeSplits, const QList& interestSplits) const { auto total = s0.value(); foreach (const auto feeSplit, feeSplits) total += feeSplit.value(); foreach (const auto interestSplit, interestSplits) total += interestSplit.value(); return total; } void Activity::setLabelText(const QString& idx, const QString& txt) const { auto w = dynamic_cast(haveWidget(idx)); if (w) { w->setText(txt); } else { if (KMyMoneySettings::transactionForm()) { // labels are only used in the transaction form qDebug("Unknown QLabel named '%s'", qPrintable(idx)); } } } void Activity::preloadAssetAccount() { Q_D(Activity); auto cat = dynamic_cast(haveWidget("asset-account")); if (cat && cat->isVisible()) { if (cat->currentText().isEmpty()) { MyMoneyAccount acc = MyMoneyFile::instance()->accountByName(i18n("%1 (Brokerage)", d->m_parent->account().name())); if (!acc.id().isEmpty()) { bool blocked = cat->signalsBlocked(); // block signals, so that the focus does not go crazy cat->blockSignals(true); cat->completion()->setSelected(acc.id()); cat->slotItemSelected(acc.id()); cat->blockSignals(blocked); } } } } void Activity::setWidgetVisibility(const QStringList& widgetIds, bool visible) const { for (QStringList::const_iterator it_w = widgetIds.constBegin(); it_w != widgetIds.constEnd(); ++it_w) { auto w = haveWidget(*it_w); if (w) { if (visible) { w->show(); } else { w->hide(); } } } } eDialogs::PriceMode Activity::priceMode() const { Q_D(const Activity); return d->m_parent->priceMode(); } QString Activity::priceLabel() const { QString label; if (priceMode() == eDialogs::PriceMode::Price) { label = i18n("Price"); } else if (priceMode() == eDialogs::PriceMode::PricePerShare) { label = i18n("Price/share"); } else if (priceMode() == eDialogs::PriceMode::PricePerTransaction) { label = i18n("Transaction amount"); } return label; } Buy::Buy(InvestTransactionEditor* editor) : Activity(editor) { } Buy::~Buy() { } eMyMoney::Split::InvestmentTransactionType Buy::type() const { return eMyMoney::Split::InvestmentTransactionType::BuyShares; } void Buy::showWidgets() const { static const QStringList visibleWidgetIds = QStringList() << "asset-account" << "shares" << "price" << "total" << "interest-account" << "fee-account"; setWidgetVisibility(visibleWidgetIds, true); setLabelText("interest-amount-label", i18n("Interest")); setLabelText("interest-label", i18n("Interest")); setLabelText("fee-label", i18n("Fees")); setLabelText("asset-label", i18n("Account")); setLabelText("shares-label", i18n("Shares")); if (dynamic_cast(haveWidget("price-label"))) setLabelText("price-label", priceLabel()); setLabelText("total-label", i18nc("Total value", "Total")); } bool Buy::isComplete(QString& reason) const { auto rc = Activity::isComplete(reason); rc &= haveAssetAccount(); rc &= haveFees(true); rc &= haveShares(); rc &= havePrice(); return rc; } bool Buy::createTransaction(MyMoneyTransaction& t, MyMoneySplit& s0, MyMoneySplit& assetAccountSplit, QList& feeSplits, QList& m_feeSplits, QList& interestSplits, QList& m_interestSplits, MyMoneySecurity& security, MyMoneySecurity& currency) { Q_D(Activity); Q_UNUSED(m_interestSplits); Q_UNUSED(security); Q_UNUSED(currency); QString reason; if (!isComplete(reason)) return false; auto sharesEdit = dynamic_cast(haveWidget("shares")); auto priceEdit = dynamic_cast(haveWidget("price")); s0.setAction(eMyMoney::Split::InvestmentTransactionType::BuyShares); MyMoneyMoney shares = s0.shares(); MyMoneyMoney price; if (!s0.shares().isZero()) price = (s0.value() / s0.shares()).reduce(); - if (!isMultiSelection() || (isMultiSelection() && sharesEdit && !sharesEdit->value().isZero())) { + if (sharesEdit && (!isMultiSelection() || (isMultiSelection() && !sharesEdit->value().isZero()))) { shares = sharesEdit->value().abs(); s0.setShares(shares); s0.setValue((shares * price).reduce()); s0.setPrice(price); } - if (!isMultiSelection() || (isMultiSelection() && priceEdit && !priceEdit->value().isZero())) { + if (priceEdit && (!isMultiSelection() || (isMultiSelection() && !priceEdit->value().isZero()))) { price = priceEdit->value().abs(); if (priceMode() == eDialogs::PriceMode::PricePerTransaction) { s0.setValue(price.reduce()); if (!s0.shares().isZero()) s0.setPrice((price / s0.shares()).reduce()); } else { s0.setValue((shares * price).reduce()); s0.setPrice(price); } } auto feeAccountWidget = dynamic_cast(haveWidget("fee-account")); auto feeAmountWidget = dynamic_cast(haveWidget("fee-amount")); if (!feeAccountWidget || !feeAmountWidget || !createCategorySplits(t, feeAccountWidget, feeAmountWidget, MyMoneyMoney::ONE, feeSplits, m_feeSplits)) return false; createAssetAccountSplit(assetAccountSplit, s0); MyMoneyMoney total = sumSplits(s0, feeSplits, QList()); // Clear any leftover value from previous Dividend. interestSplits.clear(); assetAccountSplit.setValue(-total); if (!d->m_parent->setupPrice(t, assetAccountSplit)) return false; return true; } Sell::Sell(InvestTransactionEditor* editor) : Activity(editor) { } Sell::~Sell() { } eMyMoney::Split::InvestmentTransactionType Sell::type() const { return eMyMoney::Split::InvestmentTransactionType::SellShares; } void Sell::showWidgets() const { Q_D(const Activity); static const QStringList visibleWidgetIds = QStringList() << "asset-account" << "interest-amount" << "shares" << "price" << "total" << "interest-account" << "fee-account"; setWidgetVisibility(visibleWidgetIds, true); if (auto shareEdit = dynamic_cast(haveWidget("shares"))) shareEdit->setPrecision(MyMoneyMoney::denomToPrec(d->m_parent->security().smallestAccountFraction())); setLabelText("interest-amount-label", i18n("Interest")); setLabelText("interest-label", i18n("Interest")); setLabelText("fee-label", i18n("Fees")); setLabelText("asset-label", i18n("Account")); setLabelText("shares-label", i18n("Shares")); if (dynamic_cast(haveWidget("price-label"))) setLabelText("price-label", priceLabel()); setLabelText("total-label", i18nc("Total value", "Total")); } bool Sell::isComplete(QString& reason) const { auto rc = Activity::isComplete(reason); rc &= haveAssetAccount(); rc &= haveFees(true); rc &= haveInterest(true); rc &= haveShares(); rc &= havePrice(); return rc; } bool Sell::createTransaction(MyMoneyTransaction& t, MyMoneySplit& s0, MyMoneySplit& assetAccountSplit, QList& feeSplits, QList& m_feeSplits, QList& interestSplits, QList& m_interestSplits, MyMoneySecurity& security, MyMoneySecurity& currency) { Q_D(Activity); Q_UNUSED(m_interestSplits); Q_UNUSED(security); Q_UNUSED(currency); QString reason; if (!isComplete(reason)) return false; auto sharesEdit = dynamic_cast(haveWidget("shares")); auto priceEdit = dynamic_cast(haveWidget("price")); s0.setAction(eMyMoney::Split::InvestmentTransactionType::BuyShares); MyMoneyMoney shares = s0.shares(); MyMoneyMoney price; if (!s0.shares().isZero()) price = (s0.value() / s0.shares()).reduce(); - if (!isMultiSelection() || (isMultiSelection() && sharesEdit && !sharesEdit->value().isZero())) { + if (sharesEdit && (!isMultiSelection() || (isMultiSelection() && !sharesEdit->value().isZero()))) { shares = -sharesEdit->value().abs(); s0.setShares(shares); s0.setValue((shares * price).reduce()); s0.setPrice(price); } - if (!isMultiSelection() || (isMultiSelection() && priceEdit && !priceEdit->value().isZero())) { + if (priceEdit && (!isMultiSelection() || (isMultiSelection() && !priceEdit->value().isZero()))) { price = priceEdit->value().abs(); if (priceMode() == eDialogs::PriceMode::PricePerTransaction) { price = -price; s0.setValue(price.reduce()); if (!s0.shares().isZero()) s0.setPrice((price / s0.shares()).reduce()); } else { s0.setValue((shares * price).reduce()); s0.setPrice(price); } } auto feeAccountWidget = dynamic_cast(haveWidget("fee-account")); auto feeAmountWidget = dynamic_cast(haveWidget("fee-amount")); if (!feeAccountWidget || !feeAmountWidget || !createCategorySplits(t, feeAccountWidget, feeAmountWidget, MyMoneyMoney::ONE, feeSplits, m_feeSplits)) return false; auto interestAccountWidget = dynamic_cast(haveWidget("interest-account")); auto interestAmountWidget = dynamic_cast(haveWidget("interest-amount")); if (!interestAccountWidget || !interestAmountWidget || !createCategorySplits(t, interestAccountWidget, interestAmountWidget, MyMoneyMoney::MINUS_ONE, interestSplits, m_interestSplits)) return false; createAssetAccountSplit(assetAccountSplit, s0); MyMoneyMoney total = sumSplits(s0, feeSplits, interestSplits); assetAccountSplit.setValue(-total); if (!d->m_parent->setupPrice(t, assetAccountSplit)) return false; return true; } Div::Div(InvestTransactionEditor* editor) : Activity(editor) { } Div::~Div() { } eMyMoney::Split::InvestmentTransactionType Div::type() const { return eMyMoney::Split::InvestmentTransactionType::Dividend; } void Div::showWidgets() const { static const QStringList visibleWidgetIds = QStringList() << "asset-account" << "interest-amount" << "total" << "interest-account" << "fee-account"; setWidgetVisibility(visibleWidgetIds, true); static const QStringList hiddenWidgetIds = QStringList() << "shares" << "price"; setWidgetVisibility(hiddenWidgetIds, false); setLabelText("interest-amount-label", i18n("Interest")); setLabelText("interest-label", i18n("Interest")); setLabelText("fee-label", i18n("Fees")); setLabelText("asset-label", i18n("Account")); setLabelText("total-label", i18nc("Total value", "Total")); } bool Div::isComplete(QString& reason) const { Q_UNUSED(reason) auto rc = Activity::isComplete(reason); rc &= haveAssetAccount(); rc &= haveCategoryAndAmount("interest-account", QString(), false); rc &= haveInterest(false); return rc; } bool Div::createTransaction(MyMoneyTransaction& t, MyMoneySplit& s0, MyMoneySplit& assetAccountSplit, QList& feeSplits, QList& m_feeSplits, QList& interestSplits, QList& m_interestSplits, MyMoneySecurity& security, MyMoneySecurity& currency) { Q_D(Activity); Q_UNUSED(m_feeSplits); Q_UNUSED(security); Q_UNUSED(currency); QString reason; if (!isComplete(reason)) return false; s0.setAction(eMyMoney::Split::InvestmentTransactionType::Dividend); // for dividends, we only use the stock split as a marker MyMoneyMoney shares; s0.setShares(shares); s0.setValue(shares); s0.setPrice(MyMoneyMoney::ONE); auto feeAccountWidget = dynamic_cast(haveWidget("fee-account")); auto feeAmountWidget = dynamic_cast(haveWidget("fee-amount")); if (!feeAccountWidget || !feeAmountWidget || !createCategorySplits(t, feeAccountWidget, feeAmountWidget, MyMoneyMoney::ONE, feeSplits, m_feeSplits)) return false; auto interestAccountWidget = dynamic_cast(haveWidget("interest-account")); auto interestAmountWidget = dynamic_cast(haveWidget("interest-amount")); if (!interestAccountWidget || !interestAmountWidget || !createCategorySplits(t, interestAccountWidget, interestAmountWidget, MyMoneyMoney::MINUS_ONE, interestSplits, m_interestSplits)) return false; createAssetAccountSplit(assetAccountSplit, s0); MyMoneyMoney total = sumSplits(s0, feeSplits, interestSplits); assetAccountSplit.setValue(-total); if (!d->m_parent->setupPrice(t, assetAccountSplit)) return false; return true; } Reinvest::Reinvest(InvestTransactionEditor* editor) : Activity(editor) { } Reinvest::~Reinvest() { } eMyMoney::Split::InvestmentTransactionType Reinvest::type() const { return eMyMoney::Split::InvestmentTransactionType::ReinvestDividend; } void Reinvest::showWidgets() const { Q_D(const Activity); static const QStringList visibleWidgetIds = QStringList() << "price" << "interest-account"; setWidgetVisibility(visibleWidgetIds, true); if (auto shareEdit = dynamic_cast(haveWidget("shares"))) { shareEdit->show(); shareEdit->setPrecision(MyMoneyMoney::denomToPrec(d->m_parent->security().smallestAccountFraction())); } if (auto intAmount = dynamic_cast(haveWidget("interest-amount"))) { intAmount->hide(); setLabelText("interest-amount-label", QString()); intAmount->setValue(MyMoneyMoney()); } setLabelText("interest-label", i18n("Interest")); setLabelText("shares-label", i18n("Shares")); if (dynamic_cast(haveWidget("price-label"))) setLabelText("price-label", priceLabel()); setLabelText("total-label", i18nc("Total value", "Total")); } bool Reinvest::isComplete(QString& reason) const { auto rc = Activity::isComplete(reason); rc &= haveCategoryAndAmount("interest-account", QString(), false); rc &= haveShares(); rc &= havePrice(); return rc; } bool Reinvest::createTransaction(MyMoneyTransaction& t, MyMoneySplit& s0, MyMoneySplit& assetAccountSplit, QList& feeSplits, QList& m_feeSplits, QList& interestSplits, QList& m_interestSplits, MyMoneySecurity& security, MyMoneySecurity& currency) { Q_D(Activity); Q_UNUSED(assetAccountSplit); Q_UNUSED(security); Q_UNUSED(currency); Q_UNUSED(m_feeSplits) QString reason; if (!isComplete(reason)) return false; auto sharesEdit = dynamic_cast(haveWidget("shares")); auto priceEdit = dynamic_cast(haveWidget("price")); s0.setAction(eMyMoney::Split::InvestmentTransactionType::ReinvestDividend); MyMoneyMoney shares = s0.shares(); MyMoneyMoney price; if (!s0.shares().isZero()) price = (s0.value() / s0.shares()).reduce(); - if (!isMultiSelection() || (isMultiSelection() && sharesEdit && !sharesEdit->value().isZero())) { + if (sharesEdit && (!isMultiSelection() || (isMultiSelection() && !sharesEdit->value().isZero()))) { shares = sharesEdit->value().abs(); s0.setShares(shares); s0.setValue((shares * price).reduce()); s0.setPrice(price); } - if (!isMultiSelection() || (isMultiSelection() && priceEdit && !priceEdit->value().isZero())) { + if (priceEdit && (!isMultiSelection() || (isMultiSelection() && !priceEdit->value().isZero()))) { price = priceEdit->value().abs(); if (priceMode() == eDialogs::PriceMode::PricePerTransaction) { s0.setValue(price.reduce()); if (!s0.shares().isZero()) s0.setPrice((price / s0.shares()).reduce()); } else { s0.setValue((shares * price).reduce()); s0.setPrice(price); } } auto interestAccountWidget = dynamic_cast(haveWidget("interest-account")); auto interestAmountWidget = dynamic_cast(haveWidget("interest-amount")); if (!interestAccountWidget || !interestAmountWidget || !createCategorySplits(t, interestAccountWidget, interestAmountWidget, MyMoneyMoney::MINUS_ONE, interestSplits, m_interestSplits)) return false; if (interestSplits.count() != 1) { qDebug("more or less than one interest split in Reinvest::createTransaction. Not created."); return false; } assetAccountSplit.setAccountId(QString()); MyMoneySplit& s1 = interestSplits[0]; MyMoneyMoney total = sumSplits(s0, feeSplits, QList()); s1.setValue(-total); if (!d->m_parent->setupPrice(t, s1)) return false; return true; } Add::Add(InvestTransactionEditor* editor) : Activity(editor) { } Add::~Add() { } eMyMoney::Split::InvestmentTransactionType Add::type() const { return eMyMoney::Split::InvestmentTransactionType::AddShares; } void Add::showWidgets() const { Q_D(const Activity); if (auto shareEdit = dynamic_cast(haveWidget("shares"))) { shareEdit->show(); shareEdit->setPrecision(MyMoneyMoney::denomToPrec(d->m_parent->security().smallestAccountFraction())); } setLabelText("shares-label", i18n("Shares")); } bool Add::isComplete(QString& reason) const { auto rc = Activity::isComplete(reason); rc &= haveShares(); return rc; } bool Add::createTransaction(MyMoneyTransaction& t, MyMoneySplit& s0, MyMoneySplit& assetAccountSplit, QList& feeSplits, QList& m_feeSplits, QList& interestSplits, QList& m_interestSplits, MyMoneySecurity& security, MyMoneySecurity& currency) { Q_UNUSED(t); Q_UNUSED(assetAccountSplit); Q_UNUSED(m_feeSplits); Q_UNUSED(m_interestSplits); Q_UNUSED(security); Q_UNUSED(currency); QString reason; if (!isComplete(reason)) return false; auto sharesEdit = dynamic_cast(haveWidget("shares")); s0.setAction(eMyMoney::Split::InvestmentTransactionType::AddShares); if (sharesEdit) s0.setShares(sharesEdit->value().abs()); s0.setValue(MyMoneyMoney()); s0.setPrice(MyMoneyMoney()); assetAccountSplit.setValue(MyMoneyMoney());// Clear any leftover value from previous Dividend. feeSplits.clear(); interestSplits.clear(); return true; } Remove::Remove(InvestTransactionEditor* editor) : Activity(editor) { } Remove::~Remove() { } eMyMoney::Split::InvestmentTransactionType Remove::type() const { return eMyMoney::Split::InvestmentTransactionType::RemoveShares; } void Remove::showWidgets() const { Q_D(const Activity); if (auto shareEdit = dynamic_cast(haveWidget("shares"))) { shareEdit->show(); shareEdit->setPrecision(MyMoneyMoney::denomToPrec(d->m_parent->security().smallestAccountFraction())); } setLabelText("shares-label", i18n("Shares")); } bool Remove::isComplete(QString& reason) const { auto rc = Activity::isComplete(reason); rc &= haveShares(); return rc; } bool Remove::createTransaction(MyMoneyTransaction& t, MyMoneySplit& s0, MyMoneySplit& assetAccountSplit, QList& feeSplits, QList& m_feeSplits, QList& interestSplits, QList& m_interestSplits, MyMoneySecurity& security, MyMoneySecurity& currency) { Q_UNUSED(t); Q_UNUSED(assetAccountSplit); Q_UNUSED(m_feeSplits); Q_UNUSED(m_interestSplits); Q_UNUSED(security); Q_UNUSED(currency); QString reason; if (!isComplete(reason)) return false; s0.setAction(eMyMoney::Split::InvestmentTransactionType::AddShares); if (auto sharesEdit = dynamic_cast(haveWidget("shares"))) s0.setShares(-(sharesEdit->value().abs())); s0.setValue(MyMoneyMoney()); s0.setPrice(MyMoneyMoney()); assetAccountSplit.setValue(MyMoneyMoney());// Clear any leftover value from previous Dividend. feeSplits.clear(); interestSplits.clear(); return true; } Invest::Split::Split(InvestTransactionEditor* editor) : Activity(editor) { } Invest::Split::~Split() { } eMyMoney::Split::InvestmentTransactionType Invest::Split::type() const { return eMyMoney::Split::InvestmentTransactionType::SplitShares; } void Invest::Split::showWidgets() const { // TODO do we need a special split ratio widget? // TODO maybe yes, currently the precision is the one of the fraction and might differ from it if (auto shareEdit = dynamic_cast(haveWidget("shares"))) { shareEdit->show(); shareEdit->setPrecision(-1); } setLabelText("shares-label", i18n("Ratio 1/")); } bool Invest::Split::isComplete(QString& reason) const { auto rc = Activity::isComplete(reason); rc &= haveShares(); return rc; } bool Invest::Split::createTransaction(MyMoneyTransaction& t, MyMoneySplit& s0, MyMoneySplit& assetAccountSplit, QList& feeSplits, QList& m_feeSplits, QList& interestSplits, QList& m_interestSplits, MyMoneySecurity& security, MyMoneySecurity& currency) { Q_UNUSED(t); Q_UNUSED(assetAccountSplit); Q_UNUSED(m_feeSplits); Q_UNUSED(m_interestSplits); Q_UNUSED(security); Q_UNUSED(currency); auto sharesEdit = dynamic_cast(haveWidget("shares")); KMyMoneyCategory* cat; cat = dynamic_cast(haveWidget("interest-account")); if (cat) cat->parentWidget()->hide(); cat = dynamic_cast(haveWidget("fee-account")); if (cat) cat->parentWidget()->hide(); s0.setAction(eMyMoney::Split::InvestmentTransactionType::SplitShares); if (sharesEdit) s0.setShares(sharesEdit->value().abs()); s0.setValue(MyMoneyMoney()); s0.setPrice(MyMoneyMoney()); feeSplits.clear(); interestSplits.clear(); return true; } IntInc::IntInc(InvestTransactionEditor* editor) : Activity(editor) { } IntInc::~IntInc() { } eMyMoney::Split::InvestmentTransactionType IntInc::type() const { return eMyMoney::Split::InvestmentTransactionType::InterestIncome; } void IntInc::showWidgets() const { static const QStringList visibleWidgetIds = QStringList() << "asset-account" << "interest-amount" << "total" << "interest-account" << "fee-account"; setWidgetVisibility(visibleWidgetIds, true); static const QStringList hiddenWidgetIds = QStringList() << "shares" << "price" << "fee-amount"; setWidgetVisibility(hiddenWidgetIds, false); setLabelText("interest-amount-label", i18n("Interest")); setLabelText("interest-label", i18n("Interest")); setLabelText("fee-label", i18n("Fees")); setLabelText("asset-label", i18n("Account")); setLabelText("total-label", i18nc("Total value", "Total")); } bool IntInc::isComplete(QString& reason) const { Q_UNUSED(reason) auto rc = Activity::isComplete(reason); rc &= haveAssetAccount(); rc &= haveCategoryAndAmount("interest-account", QString(), false); rc &= haveInterest(false); return rc; } bool IntInc::createTransaction(MyMoneyTransaction& t, MyMoneySplit& s0, MyMoneySplit& assetAccountSplit, QList& feeSplits, QList& m_feeSplits, QList& interestSplits, QList& m_interestSplits, MyMoneySecurity& security, MyMoneySecurity& currency) { Q_D(Activity); Q_UNUSED(security); Q_UNUSED(currency); QString reason; if (!isComplete(reason)) return false; s0.setAction(eMyMoney::Split::InvestmentTransactionType::InterestIncome); // for dividends, we only use the stock split as a marker MyMoneyMoney shares; s0.setShares(shares); s0.setValue(shares); s0.setPrice(MyMoneyMoney::ONE); auto feeAccountWidget = dynamic_cast(haveWidget("fee-account")); auto feeAmountWidget = dynamic_cast(haveWidget("fee-amount")); if (!feeAccountWidget || !feeAmountWidget || !createCategorySplits(t, feeAccountWidget, feeAmountWidget, MyMoneyMoney::ONE, feeSplits, m_feeSplits)) return false; auto interestAccountWidget = dynamic_cast(haveWidget("interest-account")); auto interestAmountWidget = dynamic_cast(haveWidget("interest-amount")); if (!interestAccountWidget || !interestAmountWidget || !createCategorySplits(t, interestAccountWidget, interestAmountWidget, MyMoneyMoney::MINUS_ONE, interestSplits, m_interestSplits)) return false; createAssetAccountSplit(assetAccountSplit, s0); MyMoneyMoney total = sumSplits(s0, feeSplits, interestSplits); assetAccountSplit.setValue(-total); if (!d->m_parent->setupPrice(t, assetAccountSplit)) return false; return true; } diff --git a/kmymoney/dialogs/investtransactioneditor.cpp b/kmymoney/dialogs/investtransactioneditor.cpp index c0f4ab10f..1e012d1f7 100644 --- a/kmymoney/dialogs/investtransactioneditor.cpp +++ b/kmymoney/dialogs/investtransactioneditor.cpp @@ -1,1254 +1,1254 @@ /*************************************************************************** investtransactioneditor.cpp ---------- begin : Fri Dec 15 2006 copyright : (C) 2006 by Thomas Baumgart email : Thomas Baumgart (C) 2017, 2018 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 "investtransactioneditor.h" #include "transactioneditor_p.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneyreconcilecombo.h" #include "kmymoneyactivitycombo.h" #include "kmymoneytagcombo.h" #include "ktagcontainer.h" #include "investtransaction.h" #include "selectedtransactions.h" #include "transactioneditorcontainer.h" #include "kmymoneycategory.h" #include "kmymoneydateinput.h" #include "kmymoneyedit.h" #include "kmymoneyaccountselector.h" #include "kmymoneymvccombo.h" #include "mymoneyfile.h" #include "mymoneyexception.h" #include "mymoneysecurity.h" #include "mymoneyprice.h" #include "ksplittransactiondlg.h" #include "kcurrencycalculator.h" #include "kmymoneysettings.h" #include "investactivities.h" #include "kmymoneycompletion.h" #include "dialogenums.h" using namespace eMyMoney; using namespace KMyMoneyRegister; using namespace KMyMoneyTransactionForm; using namespace Invest; class InvestTransactionEditorPrivate : public TransactionEditorPrivate { Q_DISABLE_COPY(InvestTransactionEditorPrivate) Q_DECLARE_PUBLIC(InvestTransactionEditor) friend class Invest::Activity; public: InvestTransactionEditorPrivate(InvestTransactionEditor* qq) : TransactionEditorPrivate(qq), m_activity(0), m_transactionType(eMyMoney::Split::InvestmentTransactionType::BuyShares) { m_phonyAccount = MyMoneyAccount("Phony-ID", MyMoneyAccount()); } ~InvestTransactionEditorPrivate() { delete m_activity; } void hideCategory(const QString& name) { Q_Q(InvestTransactionEditor); if (auto cat = dynamic_cast(q->haveWidget(name))) { cat->hide(); cat->splitButton()->hide(); } } void activityFactory(eMyMoney::Split::InvestmentTransactionType type) { Q_Q(InvestTransactionEditor); if (!m_activity || type != m_activity->type()) { delete m_activity; switch (type) { default: case eMyMoney::Split::InvestmentTransactionType::BuyShares: m_activity = new Buy(q); break; case eMyMoney::Split::InvestmentTransactionType::SellShares: m_activity = new Sell(q); break; case eMyMoney::Split::InvestmentTransactionType::Dividend: case eMyMoney::Split::InvestmentTransactionType::Yield: m_activity = new Div(q); break; case eMyMoney::Split::InvestmentTransactionType::ReinvestDividend: m_activity = new Reinvest(q); break; case eMyMoney::Split::InvestmentTransactionType::AddShares: m_activity = new Add(q); break; case eMyMoney::Split::InvestmentTransactionType::RemoveShares: m_activity = new Remove(q); break; case eMyMoney::Split::InvestmentTransactionType::SplitShares: m_activity = new Invest::Split(q); break; case eMyMoney::Split::InvestmentTransactionType::InterestIncome: m_activity = new IntInc(q); break; } } } MyMoneyMoney subtotal(const QList& splits) const { MyMoneyMoney sum; foreach (const auto split, splits) sum += split.value(); return sum; } /** * This method creates a transaction to be used for the split fee/interest editor. * It has a reference to a phony account and the splits contained in @a splits . */ bool createPseudoTransaction(MyMoneyTransaction& t, const QList& splits) { t.removeSplits(); MyMoneySplit split; split.setAccountId(m_phonyAccount.id()); split.setValue(-subtotal(splits)); split.setShares(split.value()); t.addSplit(split); m_phonySplit = split; foreach (const auto it_s, splits) { split = it_s; split.clearId(); t.addSplit(split); } return true; } /** * Convenience method used by slotEditInterestSplits() and slotEditFeeSplits(). * * @param categoryWidgetName name of the category widget * @param amountWidgetName name of the amount widget * @param splits the splits that make up the transaction to be edited * @param isIncome @c false for fees, @c true for interest * @param slotEditSplits name of the slot to be connected to the focusIn signal of the * category widget named @p categoryWidgetName in case of multiple splits * in @p splits . */ int editSplits(const QString& categoryWidgetName, const QString& amountWidgetName, QList& splits, bool isIncome, const char* slotEditSplits) { Q_Q(InvestTransactionEditor); int rc = QDialog::Rejected; if (!m_openEditSplits) { // only get in here in a single instance m_openEditSplits = true; // force focus change to update all data auto category = dynamic_cast(m_editWidgets[categoryWidgetName]); if (!category) return rc; QWidget* w = category->splitButton(); if (w) w->setFocus(); auto amount = dynamic_cast(q->haveWidget(amountWidgetName)); if (!amount) return rc; MyMoneyTransaction transaction; transaction.setCommodity(m_currency.id()); if (splits.count() == 0 && !category->selectedItem().isEmpty()) { MyMoneySplit s; s.setAccountId(category->selectedItem()); s.setShares(amount->value()); s.setValue(s.shares()); splits << s; } // use the transactions commodity as the currency indicator for the splits // this is used to allow some useful setting for the fractions in the amount fields try { m_phonyAccount.setCurrencyId(m_transaction.commodity()); m_phonyAccount.fraction(MyMoneyFile::instance()->security(m_transaction.commodity())); } catch (const MyMoneyException &) { qDebug("Unable to setup precision"); } if (createPseudoTransaction(transaction, splits)) { MyMoneyMoney value; QPointer dlg = new KSplitTransactionDlg(transaction, m_phonySplit, m_phonyAccount, false, isIncome, MyMoneyMoney(), m_priceInfo, m_regForm); // q->connect(dlg, SIGNAL(newCategory(MyMoneyAccount&)), q, SIGNAL(newCategory(MyMoneyAccount&))); if ((rc = dlg->exec()) == QDialog::Accepted) { transaction = dlg->transaction(); // collect splits out of the transaction splits.clear(); MyMoneyMoney fees; foreach (const auto split, transaction.splits()) { if (split.accountId() == m_phonyAccount.id()) continue; splits << split; fees += split.shares(); } if (isIncome) fees = -fees; QString categoryId; q->setupCategoryWidget(category, splits, categoryId, slotEditSplits); amount->setValue(fees); q->slotUpdateTotalAmount(); } delete dlg; } // focus jumps into the memo field if ((w = q->haveWidget("memo")) != 0) { w->setFocus(); } m_openEditSplits = false; } return rc; } void updatePriceMode(const MyMoneySplit& split = MyMoneySplit()) { Q_Q(InvestTransactionEditor); if (auto label = dynamic_cast(q->haveWidget("price-label"))) { auto sharesEdit = dynamic_cast(q->haveWidget("shares")); auto priceEdit = dynamic_cast(q->haveWidget("price")); if (!sharesEdit || !priceEdit) return; MyMoneyMoney price; if (!split.id().isEmpty()) price = split.price().reduce(); else price = priceEdit->value().abs(); if (q->priceMode() == eDialogs::PriceMode::PricePerTransaction) { priceEdit->setPrecision(m_currency.pricePrecision()); label->setText(i18n("Transaction amount")); if (!sharesEdit->value().isZero()) priceEdit->setValue(sharesEdit->value().abs() * price); } else if (q->priceMode() == eDialogs::PriceMode::PricePerShare) { priceEdit->setPrecision(m_security.pricePrecision()); label->setText(i18n("Price/Share")); priceEdit->setValue(price); } else priceEdit->setValue(price); } } Activity* m_activity; MyMoneyAccount m_phonyAccount; MyMoneySplit m_phonySplit; MyMoneySplit m_assetAccountSplit; QList m_interestSplits; QList m_feeSplits; MyMoneySecurity m_security; MyMoneySecurity m_currency; eMyMoney::Split::InvestmentTransactionType m_transactionType; }; InvestTransactionEditor::InvestTransactionEditor() : TransactionEditor(*new InvestTransactionEditorPrivate(this)) { Q_D(InvestTransactionEditor); d->m_transactionType = eMyMoney::Split::InvestmentTransactionType::UnknownTransactionType; } InvestTransactionEditor::~InvestTransactionEditor() { } InvestTransactionEditor::InvestTransactionEditor(TransactionEditorContainer* regForm, KMyMoneyRegister::InvestTransaction* item, const KMyMoneyRegister::SelectedTransactions& list, const QDate& lastPostDate) : TransactionEditor(*new InvestTransactionEditorPrivate(this), regForm, item, list, lastPostDate) { Q_D(InvestTransactionEditor); // after the gometries of the container are updated hide the widgets which are not needed by the current activity connect(d->m_regForm, &TransactionEditorContainer::geometriesUpdated, this, &InvestTransactionEditor::slotTransactionContainerGeometriesUpdated); // dissect the transaction into its type, splits, currency, security etc. KMyMoneyUtils::dissectTransaction(d->m_transaction, d->m_split, d->m_assetAccountSplit, d->m_feeSplits, d->m_interestSplits, d->m_security, d->m_currency, d->m_transactionType); // determine initial activity object d->activityFactory(d->m_transactionType); } void InvestTransactionEditor::createEditWidgets() { Q_D(InvestTransactionEditor); auto activity = new KMyMoneyActivityCombo(); d->m_editWidgets["activity"] = activity; connect(activity, &KMyMoneyActivityCombo::activitySelected, this, &InvestTransactionEditor::slotUpdateActivity); connect(activity, &KMyMoneyActivityCombo::activitySelected, this, &InvestTransactionEditor::slotUpdateButtonState); d->m_editWidgets["postdate"] = new KMyMoneyDateInput; auto security = new KMyMoneySecurity; security->setPlaceholderText(i18n("Security")); d->m_editWidgets["security"] = security; connect(security, &KMyMoneyCombo::itemSelected, this, &InvestTransactionEditor::slotUpdateSecurity); connect(security, &QComboBox::editTextChanged, this, &InvestTransactionEditor::slotUpdateButtonState); connect(security, &KMyMoneyCombo::createItem, this, &InvestTransactionEditor::slotCreateSecurity); connect(security, &KMyMoneyCombo::objectCreation, this, &InvestTransactionEditor::objectCreation); auto asset = new KMyMoneyCategory(false, nullptr); asset->setPlaceholderText(i18n("Asset account")); d->m_editWidgets["asset-account"] = asset; connect(asset, &QComboBox::editTextChanged, this, &InvestTransactionEditor::slotUpdateButtonState); connect(asset, &KMyMoneyCombo::objectCreation, this, &InvestTransactionEditor::objectCreation); auto fees = new KMyMoneyCategory(true, nullptr); fees->setPlaceholderText(i18n("Fees")); d->m_editWidgets["fee-account"] = fees; connect(fees, &KMyMoneyCombo::itemSelected, this, &InvestTransactionEditor::slotUpdateFeeCategory); connect(fees, &QComboBox::editTextChanged, this, &InvestTransactionEditor::slotUpdateButtonState); connect(fees, &QComboBox::editTextChanged, this, &InvestTransactionEditor::slotUpdateFeeVisibility); connect(fees, &KMyMoneyCombo::createItem, this, &InvestTransactionEditor::slotCreateFeeCategory); connect(fees, &KMyMoneyCombo::objectCreation, this, &InvestTransactionEditor::objectCreation); connect(fees->splitButton(), &QAbstractButton::clicked, this, &InvestTransactionEditor::slotEditFeeSplits); auto interest = new KMyMoneyCategory(true, nullptr); interest->setPlaceholderText(i18n("Interest")); d->m_editWidgets["interest-account"] = interest; connect(interest, &KMyMoneyCombo::itemSelected, this, &InvestTransactionEditor::slotUpdateInterestCategory); connect(interest, &QComboBox::editTextChanged, this, &InvestTransactionEditor::slotUpdateButtonState); connect(interest, &QComboBox::editTextChanged, this, &InvestTransactionEditor::slotUpdateInterestVisibility); connect(interest, &KMyMoneyCombo::createItem, this, &InvestTransactionEditor::slotCreateInterestCategory); connect(interest, &KMyMoneyCombo::objectCreation, this, &InvestTransactionEditor::objectCreation); connect(interest->splitButton(), &QAbstractButton::clicked, this, &InvestTransactionEditor::slotEditInterestSplits); auto tag = new KTagContainer; tag->tagCombo()->setPlaceholderText(i18n("Tag")); tag->tagCombo()->setObjectName(QLatin1String("Tag")); d->m_editWidgets["tag"] = tag; connect(tag->tagCombo(), &QComboBox::editTextChanged, this, &InvestTransactionEditor::slotUpdateButtonState); connect(tag->tagCombo(), &KMyMoneyMVCCombo::createItem, this, &InvestTransactionEditor::slotNewTag); connect(tag->tagCombo(), &KMyMoneyMVCCombo::objectCreation, this, &InvestTransactionEditor::objectCreation); auto memo = new KTextEdit; memo->setTabChangesFocus(true); d->m_editWidgets["memo"] = memo; connect(memo, &QTextEdit::textChanged, this, &InvestTransactionEditor::slotUpdateInvestMemoState); connect(memo, &QTextEdit::textChanged, this, &InvestTransactionEditor::slotUpdateButtonState); d->m_activity->memoText().clear(); d->m_activity->memoChanged() = false; KMyMoneyEdit* value = new KMyMoneyEdit; value->setPlaceholderText(i18n("Shares")); value->setResetButtonVisible(false); d->m_editWidgets["shares"] = value; connect(value, &KMyMoneyEdit::textChanged, this, &InvestTransactionEditor::slotUpdateButtonState); connect(value, &KMyMoneyEdit::valueChanged, this, &InvestTransactionEditor::slotUpdateTotalAmount); value = new KMyMoneyEdit; value->setPlaceholderText(i18n("Price")); value->setResetButtonVisible(false); d->m_editWidgets["price"] = value; connect(value, &KMyMoneyEdit::textChanged, this, &InvestTransactionEditor::slotUpdateButtonState); connect(value, &KMyMoneyEdit::valueChanged, this, &InvestTransactionEditor::slotUpdateTotalAmount); value = new KMyMoneyEdit; // TODO once we have the selected transactions as array of Transaction // we can allow multiple splits for fee and interest value->setResetButtonVisible(false); d->m_editWidgets["fee-amount"] = value; connect(value, &KMyMoneyEdit::textChanged, this, &InvestTransactionEditor::slotUpdateButtonState); connect(value, &KMyMoneyEdit::valueChanged, this, &InvestTransactionEditor::slotUpdateTotalAmount); value = new KMyMoneyEdit; // TODO once we have the selected transactions as array of Transaction // we can allow multiple splits for fee and interest value->setResetButtonVisible(false); d->m_editWidgets["interest-amount"] = value; connect(value, &KMyMoneyEdit::textChanged, this, &InvestTransactionEditor::slotUpdateButtonState); connect(value, &KMyMoneyEdit::valueChanged, this, &InvestTransactionEditor::slotUpdateTotalAmount); auto reconcile = new KMyMoneyReconcileCombo; d->m_editWidgets["status"] = reconcile; connect(reconcile, &KMyMoneyMVCCombo::itemSelected, this, &InvestTransactionEditor::slotUpdateButtonState); KMyMoneyRegister::QWidgetContainer::iterator it_w; for (it_w = d->m_editWidgets.begin(); it_w != d->m_editWidgets.end(); ++it_w) { (*it_w)->installEventFilter(this); } QLabel* label; d->m_editWidgets["activity-label"] = label = new QLabel(i18n("Activity")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["postdate-label"] = label = new QLabel(i18n("Date")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["security-label"] = label = new QLabel(i18n("Security")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["shares-label"] = label = new QLabel(i18n("Shares")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["asset-label"] = label = new QLabel(i18n("Account")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["price-label"] = label = new QLabel(i18n("Price/share")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["fee-label"] = label = new QLabel(i18n("Fees")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["fee-amount-label"] = label = new QLabel(""); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["interest-label"] = label = new QLabel(i18n("Interest")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["interest-amount-label"] = label = new QLabel(i18n("Interest")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["memo-label"] = label = new QLabel(i18n("Memo")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["total"] = label = new QLabel(""); label->setAlignment(Qt::AlignVCenter | Qt::AlignRight); d->m_editWidgets["total-label"] = label = new QLabel(i18nc("Total value", "Total")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["status-label"] = label = new QLabel(i18n("Status")); label->setAlignment(Qt::AlignVCenter); // if we don't have more than 1 selected transaction, we don't need // the "don't change" item in some of the combo widgets if (d->m_transactions.count() < 2) { reconcile->removeDontCare(); } } int InvestTransactionEditor::slotEditFeeSplits() { Q_D(InvestTransactionEditor); return d->editSplits("fee-account", "fee-amount", d->m_feeSplits, false, SLOT(slotEditFeeSplits())); } int InvestTransactionEditor::slotEditInterestSplits() { Q_D(InvestTransactionEditor); return d->editSplits("interest-account", "interest-amount", d->m_interestSplits, true, SLOT(slotEditInterestSplits())); } void InvestTransactionEditor::slotCreateSecurity(const QString& name, QString& id) { Q_D(InvestTransactionEditor); MyMoneyAccount acc; QRegExp exp("([^:]+)"); if (exp.indexIn(name) != -1) { acc.setName(exp.cap(1)); slotNewInvestment(acc, d->m_account); // return id id = acc.id(); if (!id.isEmpty()) { slotUpdateSecurity(id); } } } void InvestTransactionEditor::slotCreateFeeCategory(const QString& name, QString& id) { MyMoneyAccount acc; acc.setName(name); slotNewCategory(acc, MyMoneyFile::instance()->expense()); // return id id = acc.id(); } void InvestTransactionEditor::slotUpdateFeeCategory(const QString& id) { haveWidget("fee-amount")->setDisabled(id.isEmpty()); } void InvestTransactionEditor::slotUpdateFeeVisibility(const QString& txt) { Q_D(InvestTransactionEditor); static const QSet transactionTypesWithoutFee = QSet() << eMyMoney::Split::InvestmentTransactionType::AddShares << eMyMoney::Split::InvestmentTransactionType::RemoveShares << eMyMoney::Split::InvestmentTransactionType::SplitShares; auto feeAmount = dynamic_cast(haveWidget("fee-amount")); if (!feeAmount) return; feeAmount->setHidden(txt.isEmpty()); auto l = dynamic_cast(haveWidget("fee-amount-label")); auto fee = dynamic_cast(haveWidget("fee-account")); if (!fee) return; const auto hideFee = txt.isEmpty() || transactionTypesWithoutFee.contains(d->m_activity->type()); // no fee expected so hide if (hideFee) { if (l) { l->setText(""); } feeAmount->hide(); fee->splitButton()->hide(); } else { if (l) { l->setText(i18n("Fee Amount")); } feeAmount->show(); fee->splitButton()->show(); } } void InvestTransactionEditor::slotUpdateInterestCategory(const QString& id) { haveWidget("interest-amount")->setDisabled(id.isEmpty()); } void InvestTransactionEditor::slotUpdateInterestVisibility(const QString& txt) { Q_D(InvestTransactionEditor); static const QSet transactionTypesWithInterest = QSet() << eMyMoney::Split::InvestmentTransactionType::BuyShares << eMyMoney::Split::InvestmentTransactionType::SellShares << eMyMoney::Split::InvestmentTransactionType::Dividend << eMyMoney::Split::InvestmentTransactionType::InterestIncome << eMyMoney::Split::InvestmentTransactionType::Yield; QWidget* w = haveWidget("interest-amount"); w->setHidden(txt.isEmpty()); auto l = dynamic_cast(haveWidget("interest-amount-label")); auto interest = dynamic_cast(haveWidget("interest-account")); const auto showInterest = !txt.isEmpty() && transactionTypesWithInterest.contains(d->m_activity->type()); if (interest && showInterest) { interest->splitButton()->show(); w->show(); if (l) l->setText(i18n("Interest")); } else { if (interest) { interest->splitButton()->hide(); w->hide(); if (l) l->setText(QString()); } } } void InvestTransactionEditor::slotCreateInterestCategory(const QString& name, QString& id) { MyMoneyAccount acc; acc.setName(name); slotNewCategory(acc, MyMoneyFile::instance()->income()); id = acc.id(); } void InvestTransactionEditor::slotReloadEditWidgets() { Q_D(InvestTransactionEditor); auto interest = dynamic_cast(haveWidget("interest-account")); auto fees = dynamic_cast(haveWidget("fee-account")); auto security = dynamic_cast(haveWidget("security")); if (!interest || !fees || !security) return; AccountSet aSet; QString id; // interest-account aSet.clear(); aSet.addAccountGroup(Account::Type::Income); aSet.load(interest->selector()); setupCategoryWidget(interest, d->m_interestSplits, id, SLOT(slotEditInterestSplits())); // fee-account aSet.clear(); aSet.addAccountGroup(Account::Type::Expense); aSet.load(fees->selector()); setupCategoryWidget(fees, d->m_feeSplits, id, SLOT(slotEditFeeSplits())); // security aSet.clear(); aSet.load(security->selector(), i18n("Security"), d->m_account.accountList(), true); } void InvestTransactionEditor::loadEditWidgets(eWidgets::eRegister::Action) { loadEditWidgets(); } void InvestTransactionEditor::loadEditWidgets() { Q_D(InvestTransactionEditor); QString id; auto postDate = dynamic_cast(haveWidget("postdate")); auto reconcile = dynamic_cast(haveWidget("status")); auto security = dynamic_cast(haveWidget("security")); auto activity = dynamic_cast(haveWidget("activity")); auto asset = dynamic_cast(haveWidget("asset-account")); auto memo = dynamic_cast(d->m_editWidgets["memo"]); KMyMoneyEdit* value; auto interest = dynamic_cast(haveWidget("interest-account")); auto fees = dynamic_cast(haveWidget("fee-account")); if (!postDate || !reconcile || !security || !activity || !asset || !memo || !interest || !fees) return; // check if the current transaction has a reference to an equity account auto haveEquityAccount = false; foreach (const auto split, d->m_transaction.splits()) { auto acc = MyMoneyFile::instance()->account(split.accountId()); if (acc.accountType() == Account::Type::Equity) { haveEquityAccount = true; break; } } // asset-account AccountSet aSet; aSet.clear(); aSet.addAccountType(Account::Type::Checkings); aSet.addAccountType(Account::Type::Savings); aSet.addAccountType(Account::Type::Cash); aSet.addAccountType(Account::Type::Asset); aSet.addAccountType(Account::Type::Currency); aSet.addAccountType(Account::Type::CreditCard); if (KMyMoneySettings::expertMode() || haveEquityAccount) aSet.addAccountGroup(Account::Type::Equity); aSet.load(asset->selector()); // security security->setSuppressObjectCreation(false); // allow object creation on the fly aSet.clear(); aSet.load(security->selector(), i18n("Security"), d->m_account.accountList(), true); // memo memo->setText(d->m_split.memo()); d->m_activity->memoText() = d->m_split.memo(); d->m_activity->memoChanged() = false; if (!isMultiSelection()) { // date if (d->m_transaction.postDate().isValid()) postDate->setDate(d->m_transaction.postDate()); else if (d->m_lastPostDate.isValid()) postDate->setDate(d->m_lastPostDate); else postDate->setDate(QDate::currentDate()); // security (but only if it's not the investment account) if (d->m_split.accountId() != d->m_account.id()) { security->completion()->setSelected(d->m_split.accountId()); security->slotItemSelected(d->m_split.accountId()); } // activity activity->setActivity(d->m_activity->type()); slotUpdateActivity(activity->activity()); asset->completion()->setSelected(d->m_assetAccountSplit.accountId()); asset->slotItemSelected(d->m_assetAccountSplit.accountId()); // interest-account aSet.clear(); aSet.addAccountGroup(Account::Type::Income); aSet.load(interest->selector()); setupCategoryWidget(interest, d->m_interestSplits, id, SLOT(slotEditInterestSplits())); slotUpdateInterestVisibility(interest->currentText()); // fee-account aSet.clear(); aSet.addAccountGroup(Account::Type::Expense); aSet.load(fees->selector()); setupCategoryWidget(fees, d->m_feeSplits, id, SLOT(slotEditFeeSplits())); slotUpdateFeeVisibility(fees->currentText()); // shares // don't set the value if the number of shares is zero so that // we can see the hint value = dynamic_cast(haveWidget("shares")); if (!value) return; if (typeid(*(d->m_activity)) != typeid(Invest::Split(this))) value->setPrecision(MyMoneyMoney::denomToPrec(d->m_security.smallestAccountFraction())); else value->setPrecision(-1); if (!d->m_split.shares().isZero()) value->setValue(d->m_split.shares().abs()); // price d->updatePriceMode(d->m_split); // fee amount value = dynamic_cast(haveWidget("fee-amount")); if (!value) return; value->setValue(d->subtotal(d->m_feeSplits)); // interest amount value = dynamic_cast(haveWidget("interest-amount")); if (!value) return; value->setValue(-d->subtotal(d->m_interestSplits)); // total slotUpdateTotalAmount(); // status if (d->m_split.reconcileFlag() == eMyMoney::Split::State::Unknown) d->m_split.setReconcileFlag(eMyMoney::Split::State::NotReconciled); reconcile->setState(d->m_split.reconcileFlag()); } else { postDate->loadDate(QDate()); reconcile->setState(eMyMoney::Split::State::Unknown); // We don't allow to change the activity activity->setActivity(d->m_activity->type()); slotUpdateActivity(activity->activity()); activity->setDisabled(true); // scan the list of selected transactions and check that they have // the same activity. KMyMoneyRegister::SelectedTransactions::iterator it_t = d->m_transactions.begin(); const QString& action = d->m_item->split().action(); bool isNegative = d->m_item->split().shares().isNegative(); bool allSameActivity = true; for (it_t = d->m_transactions.begin(); allSameActivity && (it_t != d->m_transactions.end()); ++it_t) { allSameActivity = (action == (*it_t).split().action() && (*it_t).split().shares().isNegative() == isNegative); } QStringList fields; fields << "shares" << "price" << "fee-amount" << "interest-amount"; QStringList::const_iterator it_f; for (it_f = fields.constBegin(); it_f != fields.constEnd(); ++it_f) { value = dynamic_cast(haveWidget((*it_f))); if (!value) return; value->setText(""); value->setAllowEmpty(); } // if we have transactions with different activities, disable some more widgets if (!allSameActivity) { fields << "asset-account" << "fee-account" << "interest-account"; QStringList::const_iterator it_f; for (it_f = fields.constBegin(); it_f != fields.constEnd(); ++it_f) { haveWidget(*it_f)->setDisabled(true); } } } } QWidget* InvestTransactionEditor::firstWidget() const { return nullptr; // let the creator use the first widget in the tab order } bool InvestTransactionEditor::isComplete(QString& reason) const { Q_D(const InvestTransactionEditor); reason.clear(); return d->m_activity->isComplete(reason); } void InvestTransactionEditor::slotUpdateSecurity(const QString& stockId) { Q_D(InvestTransactionEditor); auto file = MyMoneyFile::instance(); MyMoneyAccount stock = file->account(stockId); d->m_security = file->security(stock.currencyId()); d->m_currency = file->security(d->m_security.tradingCurrency()); bool currencyKnown = !d->m_currency.id().isEmpty(); if (!currencyKnown) { d->m_currency.setTradingSymbol("???"); } else { auto sharesWidget = dynamic_cast(haveWidget("shares")); if (sharesWidget) { if (typeid(*(d->m_activity)) != typeid(Invest::Split(this))) sharesWidget->setPrecision(MyMoneyMoney::denomToPrec(d->m_security.smallestAccountFraction())); else sharesWidget->setPrecision(-1); } } d->updatePriceMode(); d->m_activity->preloadAssetAccount(); haveWidget("shares")->setEnabled(currencyKnown); haveWidget("price")->setEnabled(currencyKnown); haveWidget("fee-amount")->setEnabled(currencyKnown); haveWidget("interest-amount")->setEnabled(currencyKnown); slotUpdateTotalAmount(); slotUpdateButtonState(); resizeForm(); } bool InvestTransactionEditor::fixTransactionCommodity(const MyMoneyAccount& /* account */) { return true; } void InvestTransactionEditor::totalAmount(MyMoneyMoney& amount) const { auto activityCombo = dynamic_cast(haveWidget("activity")); auto sharesEdit = dynamic_cast(haveWidget("shares")); auto priceEdit = dynamic_cast(haveWidget("price")); auto feesEdit = dynamic_cast(haveWidget("fee-amount")); auto interestEdit = dynamic_cast(haveWidget("interest-amount")); if (!activityCombo || !sharesEdit || !priceEdit || !feesEdit || !interestEdit) return; if (priceMode() == eDialogs::PriceMode::PricePerTransaction) amount = priceEdit->value().abs(); else amount = sharesEdit->value().abs() * priceEdit->value().abs(); if (feesEdit->isVisible()) { MyMoneyMoney fee = feesEdit->value(); MyMoneyMoney factor(-1, 1); switch (activityCombo->activity()) { case eMyMoney::Split::InvestmentTransactionType::BuyShares: case eMyMoney::Split::InvestmentTransactionType::ReinvestDividend: factor = MyMoneyMoney::ONE; break; default: break; } amount += (fee * factor); } if (interestEdit->isVisible()) { MyMoneyMoney interest = interestEdit->value(); MyMoneyMoney factor(1, 1); switch (activityCombo->activity()) { case eMyMoney::Split::InvestmentTransactionType::BuyShares: factor = MyMoneyMoney::MINUS_ONE; break; default: break; } amount += (interest * factor); } } void InvestTransactionEditor::slotUpdateTotalAmount() { Q_D(InvestTransactionEditor); auto total = dynamic_cast(haveWidget("total")); if (total && total->isVisible()) { MyMoneyMoney amount; totalAmount(amount); total->setText(amount.convert(d->m_currency.smallestAccountFraction(), d->m_security.roundingMethod()) .formatMoney(d->m_currency.tradingSymbol(), MyMoneyMoney::denomToPrec(d->m_currency.smallestAccountFraction()))); } } void InvestTransactionEditor::slotTransactionContainerGeometriesUpdated() { Q_D(InvestTransactionEditor); // when the geometries of the transaction container are updated some edit widgets that were // previously hidden are being shown (see QAbstractItemView::updateEditorGeometries) so we // need to update the activity with the current activity in order to show only the widgets // which are needed by the current activity if (d->m_editWidgets.isEmpty()) return; slotUpdateActivity(d->m_activity->type()); } void InvestTransactionEditor::slotUpdateActivity(eMyMoney::Split::InvestmentTransactionType activity) { Q_D(InvestTransactionEditor); // create new activity object if required d->activityFactory(activity); // hide all dynamic widgets d->hideCategory("interest-account"); d->hideCategory("fee-account"); QStringList dynwidgets; dynwidgets << "total-label" << "asset-label" << "fee-label" << "fee-amount-label" << "interest-label" << "interest-amount-label" << "price-label" << "shares-label"; // hiding labels works by clearing them. hide() does not do the job // as the underlying text in the QTable object will shine through QStringList::const_iterator it_s; for (it_s = dynwidgets.constBegin(); it_s != dynwidgets.constEnd(); ++it_s) { QLabel* w = dynamic_cast(haveWidget(*it_s)); if (w) w->setText(" "); } // real widgets can be hidden dynwidgets.clear(); dynwidgets << "asset-account" << "interest-amount" << "fee-amount" << "shares" << "price" << "total"; for (it_s = dynwidgets.constBegin(); it_s != dynwidgets.constEnd(); ++it_s) { QWidget* w = haveWidget(*it_s); if (w) w->hide(); } d->m_activity->showWidgets(); d->m_activity->preloadAssetAccount(); if (auto cat = dynamic_cast(haveWidget("interest-account"))) { if (cat->parentWidget()->isVisible()) slotUpdateInterestVisibility(cat->currentText()); else cat->splitButton()->hide(); } if (auto cat = dynamic_cast(haveWidget("fee-account"))) { if (cat->parentWidget()->isVisible()) slotUpdateFeeVisibility(cat->currentText()); else cat->splitButton()->hide(); } } eDialogs::PriceMode InvestTransactionEditor::priceMode() const { Q_D(const InvestTransactionEditor); eDialogs::PriceMode mode = static_cast(eDialogs::PriceMode::Price); auto sec = dynamic_cast(d->m_editWidgets["security"]); QString accId; if (sec && !sec->currentText().isEmpty()) { accId = sec->selectedItem(); if (accId.isEmpty()) accId = d->m_account.id(); } while (!accId.isEmpty() && mode == eDialogs::PriceMode::Price) { auto acc = MyMoneyFile::instance()->account(accId); if (acc.value("priceMode").isEmpty()) accId = acc.parentAccountId(); else mode = static_cast(acc.value("priceMode").toInt()); } // if mode is still then use that if (mode == eDialogs::PriceMode::Price) mode = eDialogs::PriceMode::PricePerShare; return mode; } MyMoneySecurity InvestTransactionEditor::security() const { Q_D(const InvestTransactionEditor); return d->m_security; } QList InvestTransactionEditor::feeSplits() const { Q_D(const InvestTransactionEditor); return d->m_feeSplits; } QList InvestTransactionEditor::interestSplits() const { Q_D(const InvestTransactionEditor); return d->m_interestSplits; } bool InvestTransactionEditor::setupPrice(const MyMoneyTransaction& t, MyMoneySplit& split) { Q_D(InvestTransactionEditor); auto file = MyMoneyFile::instance(); auto acc = file->account(split.accountId()); MyMoneySecurity toCurrency(file->security(acc.currencyId())); int fract = acc.fraction(); if (acc.currencyId() != t.commodity()) { if (acc.currencyId().isEmpty()) acc.setCurrencyId(t.commodity()); QMap::Iterator it_p; QString key = t.commodity() + '-' + acc.currencyId(); it_p = d->m_priceInfo.find(key); // if it's not found, then collect it from the user first MyMoneyMoney price; if (it_p == d->m_priceInfo.end()) { MyMoneySecurity fromCurrency = file->security(t.commodity()); MyMoneyMoney fromValue, toValue; fromValue = split.value(); const MyMoneyPrice &priceInfo = MyMoneyFile::instance()->price(fromCurrency.id(), toCurrency.id(), t.postDate()); toValue = split.value() * priceInfo.rate(toCurrency.id()); QPointer calc = new KCurrencyCalculator(fromCurrency, toCurrency, fromValue, toValue, t.postDate(), fract, d->m_regForm); if (calc->exec() == QDialog::Rejected) { delete calc; return false; } price = calc->price(); delete calc; d->m_priceInfo[key] = price; } else { price = (*it_p); } // update shares if the transaction commodity is the currency // of the current selected account split.setShares(split.value() * price); } else { split.setShares(split.value()); } return true; } bool InvestTransactionEditor::createTransaction(MyMoneyTransaction& t, const MyMoneyTransaction& torig, const MyMoneySplit& sorig, bool /* skipPriceDialog */) { Q_D(InvestTransactionEditor); auto file = MyMoneyFile::instance(); // we start with the previous values, make sure we can add them later on t = torig; MyMoneySplit s0 = sorig; s0.clearId(); auto sec = dynamic_cast(d->m_editWidgets["security"]); - if (!isMultiSelection() || (isMultiSelection() && sec && !sec->currentText().isEmpty())) { + if (sec && (!isMultiSelection() || (isMultiSelection() && !sec->currentText().isEmpty()))) { QString securityId = sec->selectedItem(); if (!securityId.isEmpty()) { s0.setAccountId(securityId); MyMoneyAccount stockAccount = file->account(securityId); QString currencyId = stockAccount.currencyId(); MyMoneySecurity security = file->security(currencyId); t.setCommodity(security.tradingCurrency()); } else { s0.setAccountId(d->m_account.id()); t.setCommodity(d->m_account.currencyId()); } } // extract price info from original transaction d->m_priceInfo.clear(); if (!torig.id().isEmpty()) { foreach (const auto split, torig.splits()) { if (split.id() != sorig.id()) { auto cat = file->account(split.accountId()); if (cat.currencyId() != d->m_account.currencyId()) { if (cat.currencyId().isEmpty()) cat.setCurrencyId(d->m_account.currencyId()); if (!split.shares().isZero() && !split.value().isZero()) { d->m_priceInfo[cat.currencyId()] = (split.shares() / split.value()).reduce(); } } } } } t.removeSplits(); auto postDate = dynamic_cast(d->m_editWidgets["postdate"]); if (postDate && postDate->date().isValid()) { t.setPostDate(postDate->date()); } // memo and number field are special: if we have multiple transactions selected // and the edit field is empty, we treat it as "not modified". // FIXME a better approach would be to have a 'dirty' flag with the widgets // which identifies if the originally loaded value has been modified // by the user auto memo = dynamic_cast(d->m_editWidgets["memo"]); if (memo) { if (!isMultiSelection() || (isMultiSelection() && d->m_activity->memoChanged())) s0.setMemo(memo->toPlainText()); } MyMoneySplit assetAccountSplit; QList feeSplits; QList interestSplits; MyMoneySecurity security, currency; eMyMoney::Split::InvestmentTransactionType transactionType; // extract the splits from the original transaction KMyMoneyUtils::dissectTransaction(torig, sorig, assetAccountSplit, feeSplits, interestSplits, security, currency, transactionType); // check if the trading currency is the same if the security has changed // in case it differs, check that we have a price (request from user) // and convert all splits // TODO // do the conversions here // TODO // keep the current activity object and create a new one // that can be destroyed later on auto activity = d->m_activity; d->m_activity = 0; // make sure we create a new one d->activityFactory(activity->type()); // if the activity is not set in the combo widget, we keep // the one which is used in the original transaction auto activityCombo = dynamic_cast(haveWidget("activity")); if (activityCombo && activityCombo->activity() == eMyMoney::Split::InvestmentTransactionType::UnknownTransactionType) { d->activityFactory(transactionType); } // if we mark the split reconciled here, we'll use today's date if no reconciliation date is given auto status = dynamic_cast(d->m_editWidgets["status"]); if (status && status->state() != eMyMoney::Split::State::Unknown) s0.setReconcileFlag(status->state()); if (s0.reconcileFlag() == eMyMoney::Split::State::Reconciled && !s0.reconcileDate().isValid()) s0.setReconcileDate(QDate::currentDate()); // call the creation logic for the current selected activity bool rc = d->m_activity->createTransaction(t, s0, assetAccountSplit, feeSplits, d->m_feeSplits, interestSplits, d->m_interestSplits, security, currency); // now switch back to the original activity delete d->m_activity; d->m_activity = activity; // add the splits to the transaction if (rc) { if (security.name().isEmpty()) // new transaction has no security filled... security = file->security(file->account(s0.accountId()).currencyId()); // ...so fetch it from s0 split QList resultSplits; // concatenates splits for easy processing if (!assetAccountSplit.accountId().isEmpty()) resultSplits.append(assetAccountSplit); if (!feeSplits.isEmpty()) resultSplits.append(feeSplits); if (!interestSplits.isEmpty()) resultSplits.append(interestSplits); AlkValue::RoundingMethod roundingMethod = AlkValue::RoundRound; if (security.roundingMethod() != AlkValue::RoundNever) roundingMethod = security.roundingMethod(); int currencyFraction = currency.smallestAccountFraction(); int securityFraction = security.smallestAccountFraction(); // assuming that all non-stock splits are monetary foreach (auto split, resultSplits) { split.clearId(); split.setShares(MyMoneyMoney(split.shares().convertDenominator(currencyFraction, roundingMethod))); split.setValue(MyMoneyMoney(split.value().convertDenominator(currencyFraction, roundingMethod))); t.addSplit(split); } s0.setShares(MyMoneyMoney(s0.shares().convertDenominator(securityFraction, roundingMethod))); // only shares variable from stock split isn't evaluated in currency s0.setValue(MyMoneyMoney(s0.value().convertDenominator(currencyFraction, roundingMethod))); t.addSplit(s0); } return rc; } void InvestTransactionEditor::setupFinalWidgets() { addFinalWidget(haveWidget("memo")); } void InvestTransactionEditor::slotUpdateInvestMemoState() { Q_D(InvestTransactionEditor); auto memo = dynamic_cast(d->m_editWidgets["memo"]); if (memo) { d->m_activity->memoChanged() = (memo->toPlainText() != d->m_activity->memoText()); } } diff --git a/kmymoney/dialogs/kenterscheduledlg.cpp b/kmymoney/dialogs/kenterscheduledlg.cpp index bc657ac9d..6631472d5 100644 --- a/kmymoney/dialogs/kenterscheduledlg.cpp +++ b/kmymoney/dialogs/kenterscheduledlg.cpp @@ -1,385 +1,387 @@ /*************************************************************************** kenterscheduledlg.cpp ------------------- begin : Sat Apr 7 2007 copyright : (C) 2007 by Thomas Baumgart email : Thomas Baumgart (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 "kenterscheduledlg.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "ui_kenterscheduledlg.h" #include "tabbar.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneymoney.h" #include "mymoneyschedule.h" #include "register.h" #include "transactionform.h" #include "transaction.h" #include "selectedtransactions.h" #include "transactioneditor.h" #include "kmymoneyutils.h" #include "kmymoneylineedit.h" #include "kmymoneydateinput.h" #include "knewaccountdlg.h" #include "knewinvestmentwizard.h" #include "mymoneyexception.h" #include "icons/icons.h" #include "mymoneyenums.h" #include "dialogenums.h" #include "widgetenums.h" using namespace Icons; class KEnterScheduleDlgPrivate { Q_DISABLE_COPY(KEnterScheduleDlgPrivate) public: KEnterScheduleDlgPrivate() : ui(new Ui::KEnterScheduleDlg), m_item(nullptr), m_showWarningOnce(true), m_extendedReturnCode(eDialogs::ScheduleResultCode::Cancel) { } ~KEnterScheduleDlgPrivate() { delete ui; } Ui::KEnterScheduleDlg *ui; MyMoneySchedule m_schedule; KMyMoneyRegister::Transaction* m_item; QWidgetList m_tabOrderWidgets; bool m_showWarningOnce; eDialogs::ScheduleResultCode m_extendedReturnCode; }; KEnterScheduleDlg::KEnterScheduleDlg(QWidget *parent, const MyMoneySchedule& schedule) : QDialog(parent), d_ptr(new KEnterScheduleDlgPrivate) { Q_D(KEnterScheduleDlg); d->ui->setupUi(this); d->m_schedule = schedule; d->m_extendedReturnCode = eDialogs::ScheduleResultCode::Enter; d->ui->buttonOk->setIcon(Icons::get(Icon::KeyEnter)); d->ui->buttonSkip->setIcon(Icons::get(Icon::MediaSeekForward)); KGuiItem::assign(d->ui->buttonCancel, KStandardGuiItem::cancel()); KGuiItem::assign(d->ui->buttonHelp, KStandardGuiItem::help()); d->ui->buttonIgnore->setHidden(true); d->ui->buttonSkip->setHidden(true); // make sure, we have a tabbar with the form KMyMoneyTransactionForm::TabBar* tabbar = d->ui->m_form->getTabBar(d->ui->m_form->parentWidget()); // we never need to see the register d->ui->m_register->hide(); // ... setup the form ... d->ui->m_form->setupForm(d->m_schedule.account()); // ... and the register ... d->ui->m_register->clear(); // ... now add the transaction to register and form ... MyMoneyTransaction t = transaction(); d->m_item = KMyMoneyRegister::Register::transactionFactory(d->ui->m_register, t, d->m_schedule.transaction().splits().isEmpty() ? MyMoneySplit() : d->m_schedule.transaction().splits().front(), 0); d->ui->m_register->selectItem(d->m_item); // show the account row d->m_item->setShowRowInForm(0, true); d->ui->m_form->slotSetTransaction(d->m_item); // no need to see the tabbar tabbar->hide(); // setup name and type d->ui->m_scheduleName->setText(d->m_schedule.name()); d->ui->m_type->setText(KMyMoneyUtils::scheduleTypeToString(d->m_schedule.type())); connect(d->ui->buttonHelp, &QAbstractButton::clicked, this, &KEnterScheduleDlg::slotShowHelp); connect(d->ui->buttonIgnore, &QAbstractButton::clicked, this, &KEnterScheduleDlg::slotIgnore); connect(d->ui->buttonSkip, &QAbstractButton::clicked, this, &KEnterScheduleDlg::slotSkip); } KEnterScheduleDlg::~KEnterScheduleDlg() { Q_D(KEnterScheduleDlg); delete d; } eDialogs::ScheduleResultCode KEnterScheduleDlg::resultCode() const { Q_D(const KEnterScheduleDlg); if (result() == QDialog::Accepted) return d->m_extendedReturnCode; return eDialogs::ScheduleResultCode::Cancel; } void KEnterScheduleDlg::showExtendedKeys(bool visible) { Q_D(KEnterScheduleDlg); d->ui->buttonIgnore->setVisible(visible); d->ui->buttonSkip->setVisible(visible); } void KEnterScheduleDlg::slotIgnore() { Q_D(KEnterScheduleDlg); d->m_extendedReturnCode = eDialogs::ScheduleResultCode::Ignore; accept(); } void KEnterScheduleDlg::slotSkip() { Q_D(KEnterScheduleDlg); d->m_extendedReturnCode = eDialogs::ScheduleResultCode::Skip; accept(); } MyMoneyTransaction KEnterScheduleDlg::transaction() { Q_D(KEnterScheduleDlg); auto t = d->m_schedule.transaction(); try { if (d->m_schedule.type() == eMyMoney::Schedule::Type::LoanPayment) { KMyMoneyUtils::calculateAutoLoan(d->m_schedule, t, QMap()); } } catch (const MyMoneyException &e) { KMessageBox::detailedError(this, i18n("Unable to load schedule details"), e.what()); } t.clearId(); t.setEntryDate(QDate()); return t; } QDate KEnterScheduleDlg::date(const QDate& _date) const { Q_D(const KEnterScheduleDlg); auto date(_date); return d->m_schedule.adjustedDate(date, d->m_schedule.weekendOption()); } void KEnterScheduleDlg::resizeEvent(QResizeEvent* ev) { Q_UNUSED(ev) Q_D(KEnterScheduleDlg); d->ui->m_register->resize((int)eWidgets::eTransaction::Column::Detail); d->ui->m_form->resize((int)eWidgets::eTransactionForm::Column::Value1); QDialog::resizeEvent(ev); } void KEnterScheduleDlg::slotSetupSize() { resize(width(), minimumSizeHint().height()); } int KEnterScheduleDlg::exec() { Q_D(KEnterScheduleDlg); if (d->m_showWarningOnce) { d->m_showWarningOnce = false; KMessageBox::information(this, QString("") + i18n("

Please check that all the details in the following dialog are correct and press OK.

Editable data can be changed and can either be applied to just this occurrence or for all subsequent occurrences for this schedule. (You will be asked what you intend after pressing OK in the following dialog)

") + QString("
"), i18n("Enter scheduled transaction"), "EnterScheduleDlgInfo"); } // force the initial height to be as small as possible QTimer::singleShot(0, this, SLOT(slotSetupSize())); return QDialog::exec(); } TransactionEditor* KEnterScheduleDlg::startEdit() { Q_D(KEnterScheduleDlg); KMyMoneyRegister::SelectedTransactions list(d->ui->m_register); - TransactionEditor* editor = d->m_item->createEditor(d->ui->m_form, list, QDate()); - editor->setScheduleInfo(d->m_schedule.name()); - editor->setPaymentMethod(d->m_schedule.paymentType()); + auto editor = d->m_item->createEditor(d->ui->m_form, list, QDate()); + if (editor) { + editor->setScheduleInfo(d->m_schedule.name()); + editor->setPaymentMethod(d->m_schedule.paymentType()); + } // check that we use the same transaction commodity in all selected transactions // if not, we need to update this in the editor's list. The user can also bail out // of this operation which means that we have to stop editing here. if (editor) { if (!editor->fixTransactionCommodity(d->m_schedule.account())) { // if the user wants to quit, we need to destroy the editor // and bail out delete editor; editor = 0; } } if (editor) { connect(editor, &TransactionEditor::transactionDataSufficient, d->ui->buttonOk, &QWidget::setEnabled); connect(editor, &TransactionEditor::escapePressed, d->ui->buttonCancel, &QAbstractButton::animateClick); connect(editor, &TransactionEditor::returnPressed, d->ui->buttonOk, &QAbstractButton::animateClick); connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, editor, &TransactionEditor::slotReloadEditWidgets); // connect(editor, SIGNAL(finishEdit(KMyMoneyRegister::SelectedTransactions)), this, SLOT(slotLeaveEditMode(KMyMoneyRegister::SelectedTransactions))); connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, editor, &TransactionEditor::slotReloadEditWidgets); // create the widgets, place them in the parent and load them with data // setup tab order d->m_tabOrderWidgets.clear(); eWidgets::eRegister::Action action = eWidgets::eRegister::Action::Withdrawal; switch (d->m_schedule.type()) { case eMyMoney::Schedule::Type::Transfer: action = eWidgets::eRegister::Action::Transfer; break; case eMyMoney::Schedule::Type::Deposit: action = eWidgets::eRegister::Action::Deposit; break; case eMyMoney::Schedule::Type::LoanPayment: switch (d->m_schedule.paymentType()) { case eMyMoney::Schedule::PaymentType::DirectDeposit: case eMyMoney::Schedule::PaymentType::ManualDeposit: action = eWidgets::eRegister::Action::Deposit; break; default: break; } break; default: break; } editor->setup(d->m_tabOrderWidgets, d->m_schedule.account(), action); MyMoneyTransaction t = d->m_schedule.transaction(); QString num = t.splits().first().number(); QWidget* w = editor->haveWidget("number"); if (d->m_schedule.paymentType() == eMyMoney::Schedule::PaymentType::WriteChecque) { auto file = MyMoneyFile::instance(); if (file->checkNoUsed(d->m_schedule.account().id(), num)) { // increment and try again num = KMyMoneyUtils::getAdjacentNumber(num); } num = KMyMoneyUtils::nextCheckNumber(d->m_schedule.account()); KMyMoneyUtils::updateLastNumberUsed(d->m_schedule.account(), num); d->m_schedule.account().setValue("lastNumberUsed", num); if (w) if (auto numberWidget = dynamic_cast(w)) numberWidget->loadText(num); } else { // if it's not a check, then we need to clear // a possibly assigned check number if (w) if (auto numberWidget = dynamic_cast(w)) numberWidget->loadText(QString()); } Q_ASSERT(!d->m_tabOrderWidgets.isEmpty()); // editor->setup() leaves the tabbar as the last widget in the stack, but we // need it as first here. So we move it around. w = editor->haveWidget("tabbar"); if (w) { int idx = d->m_tabOrderWidgets.indexOf(w); if (idx != -1) { d->m_tabOrderWidgets.removeAt(idx); d->m_tabOrderWidgets.push_front(w); } } // don't forget our three buttons d->m_tabOrderWidgets.append(d->ui->buttonOk); d->m_tabOrderWidgets.append(d->ui->buttonCancel); d->m_tabOrderWidgets.append(d->ui->buttonHelp); for (auto i = 0; i < d->m_tabOrderWidgets.size(); ++i) { QWidget* w = d->m_tabOrderWidgets.at(i); if (w) { w->installEventFilter(this); w->installEventFilter(editor); } } // Check if the editor has some preference on where to set the focus // If not, set the focus to the first widget in the tab order QWidget* focusWidget = editor->firstWidget(); if (!focusWidget) focusWidget = d->m_tabOrderWidgets.first(); focusWidget->setFocus(); // Make sure, we use the adjusted date if (auto dateEdit = dynamic_cast(editor->haveWidget("postdate"))) dateEdit->setDate(d->m_schedule.adjustedNextDueDate()); } return editor; } bool KEnterScheduleDlg::focusNextPrevChild(bool next) { Q_D(KEnterScheduleDlg); auto rc = false; auto w = qApp->focusWidget(); int currentWidgetIndex = d->m_tabOrderWidgets.indexOf(w); while (w && currentWidgetIndex == -1) { // qDebug("'%s' not in list, use parent", w->className()); w = w->parentWidget(); currentWidgetIndex = d->m_tabOrderWidgets.indexOf(w); } if (currentWidgetIndex != -1) { // if(w) qDebug("tab order is at '%s'", w->className()); currentWidgetIndex += next ? 1 : -1; if (currentWidgetIndex < 0) currentWidgetIndex = d->m_tabOrderWidgets.size() - 1; else if (currentWidgetIndex >= d->m_tabOrderWidgets.size()) currentWidgetIndex = 0; w = d->m_tabOrderWidgets[currentWidgetIndex]; // qDebug("currentWidgetIndex = %d, w = %p", currentWidgetIndex, w); if (((w->focusPolicy() & Qt::TabFocus) == Qt::TabFocus) && w->isVisible() && w->isEnabled()) { // qDebug("Selecting '%s' as focus", w->className()); w->setFocus(); rc = true; } } return rc; } void KEnterScheduleDlg::slotShowHelp() { KHelpClient::invokeHelp("details.schedules.entering"); } diff --git a/kmymoney/dialogs/kequitypriceupdatedlg.cpp b/kmymoney/dialogs/kequitypriceupdatedlg.cpp index 41efeb809..c71e5a697 100644 --- a/kmymoney/dialogs/kequitypriceupdatedlg.cpp +++ b/kmymoney/dialogs/kequitypriceupdatedlg.cpp @@ -1,814 +1,818 @@ /*************************************************************************** kequitypriceupdatedlg.cpp - description ------------------- begin : Mon Sep 1 2003 copyright : (C) 2000-2003 by Michael Edwardes email : mte@users.sourceforge.net Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio Ace Jones (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 "kequitypriceupdatedlg.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "ui_kequitypriceupdatedlg.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneyprice.h" #include "webpricequote.h" #include "kequitypriceupdateconfdlg.h" #include "kmymoneyutils.h" #include "mymoneyexception.h" #include "dialogenums.h" #define WEBID_COL 0 #define NAME_COL 1 #define PRICE_COL 2 #define DATE_COL 3 #define KMMID_COL 4 #define SOURCE_COL 5 class KEquityPriceUpdateDlgPrivate { Q_DISABLE_COPY(KEquityPriceUpdateDlgPrivate) Q_DECLARE_PUBLIC(KEquityPriceUpdateDlg) public: explicit KEquityPriceUpdateDlgPrivate(KEquityPriceUpdateDlg *qq) : q_ptr(qq), ui(new Ui::KEquityPriceUpdateDlg), m_fUpdateAll(false), m_updatingPricePolicy(eDialogs::UpdatePrice::All) { } ~KEquityPriceUpdateDlgPrivate() { delete ui; } void init(const QString& securityId) { Q_Q(KEquityPriceUpdateDlg); ui->setupUi(q); m_fUpdateAll = false; QStringList headerList; headerList << i18n("ID") << i18nc("Equity name", "Name") << i18n("Price") << i18n("Date"); ui->lvEquityList->header()->setSortIndicator(0, Qt::AscendingOrder); ui->lvEquityList->setColumnWidth(NAME_COL, 125); // This is a "get it up and running" hack. Will replace this in the future. headerList << i18nc("Internal identifier", "Internal ID") << i18nc("Online quote source", "Source"); ui->lvEquityList->setColumnWidth(KMMID_COL, 0); ui->lvEquityList->setHeaderLabels(headerList); ui->lvEquityList->setSelectionMode(QAbstractItemView::MultiSelection); ui->lvEquityList->setAllColumnsShowFocus(true); ui->btnUpdateAll->setEnabled(false); auto file = MyMoneyFile::instance(); // // Add each price pair that we know about // // send in securityId == "XXX YYY" to get a single-shot update for XXX to YYY. // for consistency reasons, this accepts the same delimiters as WebPriceQuote::launch() QRegExp splitrx("([0-9a-z\\.]+)[^a-z0-9]+([0-9a-z\\.]+)", Qt::CaseInsensitive); MyMoneySecurityPair currencyIds; if (splitrx.indexIn(securityId) != -1) { currencyIds = MyMoneySecurityPair(splitrx.cap(1), splitrx.cap(2)); } MyMoneyPriceList prices = file->priceList(); for (MyMoneyPriceList::ConstIterator it_price = prices.constBegin(); it_price != prices.constEnd(); ++it_price) { const MyMoneySecurityPair& pair = it_price.key(); if (file->security(pair.first).isCurrency() && (securityId.isEmpty() || (pair == currencyIds))) { const MyMoneyPriceEntries& entries = (*it_price); if (entries.count() > 0 && entries.begin().key() <= QDate::currentDate()) { addPricePair(pair, false); ui->btnUpdateAll->setEnabled(true); } } } // // Add each investment // QList securities = file->securityList(); for (QList::const_iterator it = securities.constBegin(); it != securities.constEnd(); ++it) { if (!(*it).isCurrency() && (securityId.isEmpty() || ((*it).id() == securityId)) && !(*it).value("kmm-online-source").isEmpty() ) { addInvestment(*it); ui->btnUpdateAll->setEnabled(true); } } // if list is empty, add the request price pair if (ui->lvEquityList->invisibleRootItem()->childCount() == 0) { addPricePair(currencyIds, true); } q->connect(ui->btnUpdateSelected, &QAbstractButton::clicked, q, &KEquityPriceUpdateDlg::slotUpdateSelectedClicked); q->connect(ui->btnUpdateAll, &QAbstractButton::clicked, q, &KEquityPriceUpdateDlg::slotUpdateAllClicked); q->connect(ui->m_fromDate, &KMyMoneyDateInput::dateChanged, q, &KEquityPriceUpdateDlg::slotDateChanged); q->connect(ui->m_toDate, &KMyMoneyDateInput::dateChanged, q, &KEquityPriceUpdateDlg::slotDateChanged); q->connect(&m_webQuote, &WebPriceQuote::csvquote, q, &KEquityPriceUpdateDlg::slotReceivedCSVQuote); q->connect(&m_webQuote, &WebPriceQuote::quote, q, &KEquityPriceUpdateDlg::slotReceivedQuote); q->connect(&m_webQuote, &WebPriceQuote::failed, q, &KEquityPriceUpdateDlg::slotQuoteFailed); q->connect(&m_webQuote, &WebPriceQuote::status, q, &KEquityPriceUpdateDlg::logStatusMessage); q->connect(&m_webQuote, &WebPriceQuote::error, q, &KEquityPriceUpdateDlg::logErrorMessage); q->connect(ui->lvEquityList, &QTreeWidget::itemSelectionChanged, q, &KEquityPriceUpdateDlg::slotUpdateSelection); q->connect(ui->btnConfigure, &QAbstractButton::clicked, q, &KEquityPriceUpdateDlg::slotConfigureClicked); if (!securityId.isEmpty()) { ui->btnUpdateSelected->hide(); ui->btnUpdateAll->hide(); // delete layout1; QTimer::singleShot(100, q, SLOT(slotUpdateAllClicked())); } // Hide OK button until we have received the first update ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); q->slotUpdateSelection(); // previous versions of this dialog allowed to store a "Don't ask again" switch. // Since we don't support it anymore, we just get rid of it KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup grp = config->group("Notification Messages"); grp.deleteEntry("KEquityPriceUpdateDlg::slotQuoteFailed::Price Update Failed"); grp.sync(); grp = config->group("Equity Price Update"); int policyValue = grp.readEntry("PriceUpdatingPolicy", (int)eDialogs::UpdatePrice::Missing); if (policyValue > (int)eDialogs::UpdatePrice::Ask || policyValue < (int)eDialogs::UpdatePrice::All) m_updatingPricePolicy = eDialogs::UpdatePrice::Missing; else m_updatingPricePolicy = static_cast(policyValue); } void addPricePair(const MyMoneySecurityPair& pair, bool dontCheckExistance) { auto file = MyMoneyFile::instance(); const auto symbol = QString::fromLatin1("%1 > %2").arg(pair.first, pair.second); const auto id = QString::fromLatin1("%1 %2").arg(pair.first, pair.second); // Check that the pair does not already exist if (ui->lvEquityList->findItems(id, Qt::MatchExactly, KMMID_COL).empty()) { const MyMoneyPrice &pr = file->price(pair.first, pair.second); if (pr.source() != QLatin1String("KMyMoney")) { bool keep = true; if ((pair.first == file->baseCurrency().id()) || (pair.second == file->baseCurrency().id())) { const QString& foreignCurrency = file->foreignCurrency(pair.first, pair.second); // check that the foreign currency is still in use QList::const_iterator it_a; QList list; file->accountList(list); for (it_a = list.constBegin(); !dontCheckExistance && it_a != list.constEnd(); ++it_a) { // if it's an account denominated in the foreign currency // keep it if (((*it_a).currencyId() == foreignCurrency) && !(*it_a).isClosed()) break; // if it's an investment traded in the foreign currency // keep it if ((*it_a).isInvest() && !(*it_a).isClosed()) { MyMoneySecurity sec = file->security((*it_a).currencyId()); if (sec.tradingCurrency() == foreignCurrency) break; } } // if it is in use, it_a is not equal to list.end() if (it_a == list.constEnd() && !dontCheckExistance) keep = false; } if (keep) { auto item = new QTreeWidgetItem(); item->setText(WEBID_COL, symbol); item->setText(NAME_COL, i18n("%1 units in %2", pair.first, pair.second)); if (pr.isValid()) { MyMoneySecurity fromCurrency = file->currency(pair.second); MyMoneySecurity toCurrency = file->currency(pair.first); item->setText(PRICE_COL, pr.rate(pair.second).formatMoney(fromCurrency.tradingSymbol(), toCurrency.pricePrecision())); item->setText(DATE_COL, pr.date().toString(Qt::ISODate)); } item->setText(KMMID_COL, id); item->setText(SOURCE_COL, "KMyMoney Currency"); // This string value should not be localized ui->lvEquityList->invisibleRootItem()->addChild(item); } } } } void addInvestment(const MyMoneySecurity& inv) { const auto id = inv.id(); // Check that the pair does not already exist if (ui->lvEquityList->findItems(id, Qt::MatchExactly, KMMID_COL).empty()) { auto file = MyMoneyFile::instance(); // check that the security is still in use QList::const_iterator it_a; QList list; file->accountList(list); for (it_a = list.constBegin(); it_a != list.constEnd(); ++it_a) { if ((*it_a).isInvest() && ((*it_a).currencyId() == inv.id()) && !(*it_a).isClosed()) break; } // if it is in use, it_a is not equal to list.end() if (it_a != list.constEnd()) { QString webID; WebPriceQuoteSource onlineSource(inv.value("kmm-online-source")); if (onlineSource.m_webIDBy == WebPriceQuoteSource::identifyBy::IdentificationNumber) webID = inv.value("kmm-security-id"); // insert ISIN number... else if (onlineSource.m_webIDBy == WebPriceQuoteSource::identifyBy::Name) webID = inv.name(); // ...or name... else webID = inv.tradingSymbol(); // ...or symbol QTreeWidgetItem* item = new QTreeWidgetItem(); item->setForeground(WEBID_COL, KColorScheme(QPalette::Normal).foreground(KColorScheme::NormalText)); if (webID.isEmpty()) { webID = i18n("[No identifier]"); item->setForeground(WEBID_COL, KColorScheme(QPalette::Normal).foreground(KColorScheme::NegativeText)); } item->setText(WEBID_COL, webID); item->setText(NAME_COL, inv.name()); MyMoneySecurity currency = file->currency(inv.tradingCurrency()); const MyMoneyPrice &pr = file->price(id.toUtf8(), inv.tradingCurrency()); if (pr.isValid()) { item->setText(PRICE_COL, pr.rate(currency.id()).formatMoney(currency.tradingSymbol(), inv.pricePrecision())); item->setText(DATE_COL, pr.date().toString(Qt::ISODate)); } item->setText(KMMID_COL, id); if (inv.value("kmm-online-quote-system") == "Finance::Quote") item->setText(SOURCE_COL, QString("Finance::Quote %1").arg(inv.value("kmm-online-source"))); else item->setText(SOURCE_COL, inv.value("kmm-online-source")); ui->lvEquityList->invisibleRootItem()->addChild(item); // If this investment is denominated in a foreign currency, ensure that // the appropriate price pair is also on the list if (currency.id() != file->baseCurrency().id()) { addPricePair(MyMoneySecurityPair(currency.id(), file->baseCurrency().id()), false); } } } } KEquityPriceUpdateDlg *q_ptr; Ui::KEquityPriceUpdateDlg *ui; bool m_fUpdateAll; eDialogs::UpdatePrice m_updatingPricePolicy; WebPriceQuote m_webQuote; }; KEquityPriceUpdateDlg::KEquityPriceUpdateDlg(QWidget *parent, const QString& securityId) : QDialog(parent), d_ptr(new KEquityPriceUpdateDlgPrivate(this)) { Q_D(KEquityPriceUpdateDlg); d->init(securityId); } KEquityPriceUpdateDlg::KEquityPriceUpdateDlg(QWidget *parent) : KEquityPriceUpdateDlg(parent, QString()) { } KEquityPriceUpdateDlg::~KEquityPriceUpdateDlg() { Q_D(KEquityPriceUpdateDlg); auto config = KSharedConfig::openConfig(); auto grp = config->group("Equity Price Update"); grp.writeEntry("PriceUpdatingPolicy", static_cast(d->m_updatingPricePolicy)); grp.sync(); delete d; } void KEquityPriceUpdateDlg::logErrorMessage(const QString& message) { logStatusMessage(QString("") + message + QString("")); } void KEquityPriceUpdateDlg::logStatusMessage(const QString& message) { Q_D(KEquityPriceUpdateDlg); d->ui->lbStatus->append(message); } MyMoneyPrice KEquityPriceUpdateDlg::price(const QString& id) const { Q_D(const KEquityPriceUpdateDlg); MyMoneyPrice price; QTreeWidgetItem* item = nullptr; QList foundItems = d->ui->lvEquityList->findItems(id, Qt::MatchExactly, KMMID_COL); if (! foundItems.empty()) item = foundItems.at(0); if (item) { MyMoneyMoney rate(item->text(PRICE_COL)); if (!rate.isZero()) { QString id = item->text(KMMID_COL).toUtf8(); // if the ID has a space, then this is TWO ID's, so it's a currency quote if (id.contains(" ")) { QStringList ids = id.split(' ', QString::SkipEmptyParts); QString fromid = ids[0].toUtf8(); QString toid = ids[1].toUtf8(); price = MyMoneyPrice(fromid, toid, QDate().fromString(item->text(DATE_COL), Qt::ISODate), rate, item->text(SOURCE_COL)); } else // otherwise, it's a security quote { MyMoneySecurity security = MyMoneyFile::instance()->security(id); price = MyMoneyPrice(id, security.tradingCurrency(), QDate().fromString(item->text(DATE_COL), Qt::ISODate), rate, item->text(SOURCE_COL)); } } } return price; } void KEquityPriceUpdateDlg::storePrices() { Q_D(KEquityPriceUpdateDlg); // update the new prices into the equities auto file = MyMoneyFile::instance(); QString name; MyMoneyFileTransaction ft; try { for (auto i = 0; i < d->ui->lvEquityList->invisibleRootItem()->childCount(); ++i) { QTreeWidgetItem* item = d->ui->lvEquityList->invisibleRootItem()->child(i); // turn on signals before we modify the last entry in the list file->blockSignals(i < d->ui->lvEquityList->invisibleRootItem()->childCount() - 1); MyMoneyMoney rate(item->text(PRICE_COL)); if (!rate.isZero()) { QString id = item->text(KMMID_COL); QString fromid; QString toid; // if the ID has a space, then this is TWO ID's, so it's a currency quote if (id.contains(QLatin1Char(' '))) { QStringList ids = id.split(QLatin1Char(' '), QString::SkipEmptyParts); fromid = ids.at(0); toid = ids.at(1); name = QString::fromLatin1("%1 --> %2").arg(fromid, toid); } else { // otherwise, it's a security quote MyMoneySecurity security = file->security(id); name = security.name(); fromid = id; toid = security.tradingCurrency(); } // TODO (Ace) Better handling of the case where there is already a price // for this date. Currently, it just overrides the old value. Really it // should check to see if the price is the same and prompt the user. file->addPrice(MyMoneyPrice(fromid, toid, QDate::fromString(item->text(DATE_COL), Qt::ISODate), rate, item->text(SOURCE_COL))); } } ft.commit(); } catch (const MyMoneyException &) { qDebug("Unable to add price information for %s", qPrintable(name)); } } void KEquityPriceUpdateDlg::slotConfigureClicked() { Q_D(KEquityPriceUpdateDlg); QPointer dlg = new EquityPriceUpdateConfDlg(d->m_updatingPricePolicy); if (dlg->exec() == QDialog::Accepted) d->m_updatingPricePolicy = dlg->policy(); delete dlg; } void KEquityPriceUpdateDlg::slotUpdateSelection() { Q_D(KEquityPriceUpdateDlg); // Only enable the update button if there is a selection d->ui->btnUpdateSelected->setEnabled(false); if (! d->ui->lvEquityList->selectedItems().empty()) d->ui->btnUpdateSelected->setEnabled(true); } void KEquityPriceUpdateDlg::slotUpdateSelectedClicked() { Q_D(KEquityPriceUpdateDlg); // disable sorting while the update is running to maintain the current order of items on which // the update process depends and which could be changed with sorting enabled due to the updated values d->ui->lvEquityList->setSortingEnabled(false); auto item = d->ui->lvEquityList->invisibleRootItem()->child(0); auto skipCnt = 1; while (item && !item->isSelected()) { item = d->ui->lvEquityList->invisibleRootItem()->child(skipCnt); ++skipCnt; } d->m_webQuote.setDate(d->ui->m_fromDate->date(), d->ui->m_toDate->date()); if (item) { d->ui->prgOnlineProgress->setMaximum(1 + d->ui->lvEquityList->invisibleRootItem()->childCount()); d->ui->prgOnlineProgress->setValue(skipCnt); d->m_webQuote.launch(item->text(WEBID_COL), item->text(KMMID_COL), item->text(SOURCE_COL)); } else { logErrorMessage("No security selected."); } } void KEquityPriceUpdateDlg::slotUpdateAllClicked() { Q_D(KEquityPriceUpdateDlg); // disable sorting while the update is running to maintain the current order of items on which // the update process depends and which could be changed with sorting enabled due to the updated values d->ui->lvEquityList->setSortingEnabled(false); QTreeWidgetItem* item = d->ui->lvEquityList->invisibleRootItem()->child(0); if (item) { d->ui->prgOnlineProgress->setMaximum(1 + d->ui->lvEquityList->invisibleRootItem()->childCount()); d->ui->prgOnlineProgress->setValue(1); d->m_fUpdateAll = true; d->m_webQuote.launch(item->text(WEBID_COL), item->text(KMMID_COL), item->text(SOURCE_COL)); } else { logErrorMessage("Security list is empty."); } } void KEquityPriceUpdateDlg::slotDateChanged() { Q_D(KEquityPriceUpdateDlg); d->ui->m_fromDate->blockSignals(true); d->ui->m_toDate->blockSignals(true); if (d->ui->m_toDate->date() > QDate::currentDate()) d->ui->m_toDate->setDate(QDate::currentDate()); - if (d->ui->m_toDate->date() < d->ui->m_fromDate->date()) + if (d->ui->m_fromDate->date() > d->ui->m_toDate->date()) d->ui->m_fromDate->setDate(d->ui->m_toDate->date()); d->ui->m_fromDate->blockSignals(false); d->ui->m_toDate->blockSignals(false); } void KEquityPriceUpdateDlg::slotQuoteFailed(const QString& _kmmID, const QString& _webID) { Q_D(KEquityPriceUpdateDlg); auto foundItems = d->ui->lvEquityList->findItems(_kmmID, Qt::MatchExactly, KMMID_COL); QTreeWidgetItem* item = nullptr; if (! foundItems.empty()) item = foundItems.at(0); // Give the user some options int result; - if (_kmmID.contains(" ")) - result = KMessageBox::warningContinueCancel(this, i18n("Failed to retrieve an exchange rate for %1 from %2. It will be skipped this time.", _webID, item->text(SOURCE_COL)), i18n("Price Update Failed")); - else if (!item) + if (_kmmID.contains(" ")) { + if (item) + result = KMessageBox::warningContinueCancel(this, i18n("Failed to retrieve an exchange rate for %1 from %2. It will be skipped this time.", _webID, item->text(SOURCE_COL)), i18n("Price Update Failed")); + else + return; + } else if (!item) { return; - else + } else { result = KMessageBox::questionYesNoCancel(this, QString::fromLatin1("%1").arg(i18n("Failed to retrieve a quote for %1 from %2. Press No to remove the online price source from this security permanently, Yes to continue updating this security during future price updates or Cancel to stop the current update operation.", _webID, item->text(SOURCE_COL))), i18n("Price Update Failed"), KStandardGuiItem::yes(), KStandardGuiItem::no()); + } if (result == KMessageBox::No) { // Disable price updates for this security MyMoneyFileTransaction ft; try { // Get this security (by ID) MyMoneySecurity security = MyMoneyFile::instance()->security(_kmmID.toUtf8()); // Set the quote source to blank security.setValue("kmm-online-source", QString()); security.setValue("kmm-online-quote-system", QString()); // Re-commit the security MyMoneyFile::instance()->modifySecurity(security); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::error(this, QString("") + i18n("Cannot update security %1: %2", _webID, e.what()) + QString(""), i18n("Price Update Failed")); } } // As long as the user doesn't want to cancel, move on! if (result != KMessageBox::Cancel) { QTreeWidgetItem* next = nullptr; d->ui->prgOnlineProgress->setValue(d->ui->prgOnlineProgress->value() + 1); item->setSelected(false); // launch the NEXT one ... in case of m_fUpdateAll == false, we // need to parse the list to find the next selected one next = d->ui->lvEquityList->invisibleRootItem()->child(d->ui->lvEquityList->invisibleRootItem()->indexOfChild(item) + 1); if (!d->m_fUpdateAll) { while (next && !next->isSelected()) { d->ui->prgOnlineProgress->setValue(d->ui->prgOnlineProgress->value() + 1); next = d->ui->lvEquityList->invisibleRootItem()->child(d->ui->lvEquityList->invisibleRootItem()->indexOfChild(next) + 1); } } if (next) { d->m_webQuote.launch(next->text(WEBID_COL), next->text(KMMID_COL), next->text(SOURCE_COL)); } else { finishUpdate(); } } else { finishUpdate(); } } void KEquityPriceUpdateDlg::slotReceivedCSVQuote(const QString& _kmmID, const QString& _webID, MyMoneyStatement& st) { Q_D(KEquityPriceUpdateDlg); auto foundItems = d->ui->lvEquityList->findItems(_kmmID, Qt::MatchExactly, KMMID_COL); QTreeWidgetItem* item = nullptr; if (! foundItems.empty()) item = foundItems.at(0); QTreeWidgetItem* next = nullptr; if (item) { auto file = MyMoneyFile::instance(); MyMoneySecurity fromCurrency, toCurrency; if (!_kmmID.contains(QLatin1Char(' '))) { try { toCurrency = MyMoneyFile::instance()->security(_kmmID); fromCurrency = MyMoneyFile::instance()->security(toCurrency.tradingCurrency()); } catch (const MyMoneyException &) { fromCurrency = toCurrency = MyMoneySecurity(); } } else { QRegExp splitrx("([0-9a-z\\.]+)[^a-z0-9]+([0-9a-z\\.]+)", Qt::CaseInsensitive); if (splitrx.indexIn(_kmmID) != -1) { try { fromCurrency = MyMoneyFile::instance()->security(splitrx.cap(2).toUtf8()); toCurrency = MyMoneyFile::instance()->security(splitrx.cap(1).toUtf8()); } catch (const MyMoneyException &) { fromCurrency = toCurrency = MyMoneySecurity(); } } } if (d->m_updatingPricePolicy != eDialogs::UpdatePrice::All) { QStringList qSources = WebPriceQuote::quoteSources(); for (auto it = st.m_listPrices.begin(); it != st.m_listPrices.end();) { MyMoneyPrice storedPrice = file->price(toCurrency.id(), fromCurrency.id(), (*it).m_date, true); bool priceValid = storedPrice.isValid(); if (!priceValid) ++it; else { switch(d->m_updatingPricePolicy) { case eDialogs::UpdatePrice::Missing: it = st.m_listPrices.erase(it); break; case eDialogs::UpdatePrice::Downloaded: if (!qSources.contains(storedPrice.source())) it = st.m_listPrices.erase(it); else ++it; break; case eDialogs::UpdatePrice::SameSource: if (storedPrice.source().compare((*it).m_sourceName) != 0) it = st.m_listPrices.erase(it); else ++it; break; case eDialogs::UpdatePrice::Ask: { auto result = KMessageBox::questionYesNoCancel(this, i18n("For %1 on %2 price %3 already exists.
" "Do you want to replace it with %4?", storedPrice.from(), storedPrice.date().toString(Qt::ISODate), QString().setNum(storedPrice.rate(storedPrice.to()).toDouble(), 'g', 10), QString().setNum((*it).m_amount.toDouble(), 'g', 10)), i18n("Price Already Exists")); switch(result) { - case KStandardGuiItem::Yes: + case KMessageBox::ButtonCode::Yes: ++it; break; - case KStandardGuiItem::No: + case KMessageBox::ButtonCode::No: it = st.m_listPrices.erase(it); break; default: - case KStandardGuiItem::Cancel: + case KMessageBox::ButtonCode::Cancel: finishUpdate(); return; break; } break; } default: ++it; break; } } } } if (!st.m_listPrices.isEmpty()) { MyMoneyFileTransaction ft; KMyMoneyUtils::processPriceList(st); ft.commit(); // latest price could be in the last or in the first row MyMoneyStatement::Price priceClass; if (st.m_listPrices.first().m_date > st.m_listPrices.last().m_date) priceClass = st.m_listPrices.first(); else priceClass = st.m_listPrices.last(); // update latest price in dialog if applicable auto latestDate = QDate::fromString(item->text(DATE_COL),Qt::ISODate); if (latestDate <= priceClass.m_date && priceClass.m_amount.isPositive()) { item->setText(PRICE_COL, priceClass.m_amount.formatMoney(fromCurrency.tradingSymbol(), toCurrency.pricePrecision())); item->setText(DATE_COL, priceClass.m_date.toString(Qt::ISODate)); item->setText(SOURCE_COL, priceClass.m_sourceName); } logStatusMessage(i18n("Price for %1 updated (id %2)", _webID, _kmmID)); // make sure to make OK button available } d->ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); d->ui->prgOnlineProgress->setValue(d->ui->prgOnlineProgress->value() + 1); item->setSelected(false); // launch the NEXT one ... in case of m_fUpdateAll == false, we // need to parse the list to find the next selected one next = d->ui->lvEquityList->invisibleRootItem()->child(d->ui->lvEquityList->invisibleRootItem()->indexOfChild(item) + 1); if (!d->m_fUpdateAll) { while (next && !next->isSelected()) { d->ui->prgOnlineProgress->setValue(d->ui->prgOnlineProgress->value() + 1); next = d->ui->lvEquityList->invisibleRootItem()->child(d->ui->lvEquityList->invisibleRootItem()->indexOfChild(next) + 1); } } } else { logErrorMessage(i18n("Received a price for %1 (id %2), but this symbol is not on the list. Aborting entire update.", _webID, _kmmID)); } if (next) { d->m_webQuote.launch(next->text(WEBID_COL), next->text(KMMID_COL), next->text(SOURCE_COL)); } else { finishUpdate(); } } void KEquityPriceUpdateDlg::slotReceivedQuote(const QString& _kmmID, const QString& _webID, const QDate& _date, const double& _price) { Q_D(KEquityPriceUpdateDlg); auto foundItems = d->ui->lvEquityList->findItems(_kmmID, Qt::MatchExactly, KMMID_COL); QTreeWidgetItem* item = nullptr; if (! foundItems.empty()) item = foundItems.at(0); QTreeWidgetItem* next = 0; if (item) { if (_price > 0.0f && _date.isValid()) { QDate date = _date; if (date > QDate::currentDate()) date = QDate::currentDate(); MyMoneyMoney price = MyMoneyMoney::ONE; QString id = _kmmID.toUtf8(); MyMoneySecurity fromCurrency, toCurrency; if (_kmmID.contains(" ") == 0) { MyMoneySecurity security = MyMoneyFile::instance()->security(id); QString factor = security.value("kmm-online-factor"); if (!factor.isEmpty()) { price = price * MyMoneyMoney(factor); } try { toCurrency = MyMoneyFile::instance()->security(id); fromCurrency = MyMoneyFile::instance()->security(toCurrency.tradingCurrency()); } catch (const MyMoneyException &) { fromCurrency = toCurrency = MyMoneySecurity(); } } else { QRegExp splitrx("([0-9a-z\\.]+)[^a-z0-9]+([0-9a-z\\.]+)", Qt::CaseInsensitive); if (splitrx.indexIn(_kmmID) != -1) { try { fromCurrency = MyMoneyFile::instance()->security(splitrx.cap(2).toUtf8()); toCurrency = MyMoneyFile::instance()->security(splitrx.cap(1).toUtf8()); } catch (const MyMoneyException &) { fromCurrency = toCurrency = MyMoneySecurity(); } } } price *= MyMoneyMoney(_price, MyMoneyMoney::precToDenom(toCurrency.pricePrecision())); item->setText(PRICE_COL, price.formatMoney(fromCurrency.tradingSymbol(), toCurrency.pricePrecision())); item->setText(DATE_COL, date.toString(Qt::ISODate)); logStatusMessage(i18n("Price for %1 updated (id %2)", _webID, _kmmID)); // make sure to make OK button available d->ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); } else { logErrorMessage(i18n("Received an invalid price for %1, unable to update.", _webID)); } d->ui->prgOnlineProgress->setValue(d->ui->prgOnlineProgress->value() + 1); item->setSelected(false); // launch the NEXT one ... in case of m_fUpdateAll == false, we // need to parse the list to find the next selected one next = d->ui->lvEquityList->invisibleRootItem()->child(d->ui->lvEquityList->invisibleRootItem()->indexOfChild(item) + 1); if (!d->m_fUpdateAll) { while (next && !next->isSelected()) { d->ui->prgOnlineProgress->setValue(d->ui->prgOnlineProgress->value() + 1); next = d->ui->lvEquityList->invisibleRootItem()->child(d->ui->lvEquityList->invisibleRootItem()->indexOfChild(next) + 1); } } } else { logErrorMessage(i18n("Received a price for %1 (id %2), but this symbol is not on the list. Aborting entire update.", _webID, _kmmID)); } if (next) { d->m_webQuote.launch(next->text(WEBID_COL), next->text(KMMID_COL), next->text(SOURCE_COL)); } else { finishUpdate(); } } void KEquityPriceUpdateDlg::finishUpdate() { Q_D(KEquityPriceUpdateDlg); // we've run past the end, reset to the default value. d->m_fUpdateAll = false; // force progress bar to show 100% d->ui->prgOnlineProgress->setValue(d->ui->prgOnlineProgress->maximum()); // re-enable the sorting that was disabled during the update process d->ui->lvEquityList->setSortingEnabled(true); } // Make sure, that these definitions are only used within this file // this does not seem to be necessary, but when building RPMs the // build option 'final' is used and all CPP files are concatenated. // So it could well be, that in another CPP file these definitions // are also used. #undef WEBID_COL #undef NAME_COL #undef PRICE_COL #undef DATE_COL #undef KMMID_COL #undef SOURCE_COL diff --git a/kmymoney/dialogs/kselecttransactionsdlg.cpp b/kmymoney/dialogs/kselecttransactionsdlg.cpp index 7d152ab1a..81c56b999 100644 --- a/kmymoney/dialogs/kselecttransactionsdlg.cpp +++ b/kmymoney/dialogs/kselecttransactionsdlg.cpp @@ -1,204 +1,204 @@ /*************************************************************************** kselecttransactionsdlg.cpp ------------------- begin : Wed May 16 2007 copyright : (C) 2007 by Thomas Baumgart email : ipwizard@users.sourceforge.net (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 "kselecttransactionsdlg.h" #include "kselecttransactionsdlg_p.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include // ---------------------------------------------------------------------------- // Project Includes #include "ui_kselecttransactionsdlg.h" #include "mymoneyaccount.h" #include "selectedtransactions.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "transaction.h" #include "kmymoneysettings.h" #include "widgetenums.h" KSelectTransactionsDlg::KSelectTransactionsDlg(const MyMoneyAccount& _account, QWidget* parent) : QDialog(parent), d_ptr(new KSelectTransactionsDlgPrivate) { Q_D(KSelectTransactionsDlg); d->m_account = _account; d->ui->setupUi(this); // setup descriptive texts setWindowTitle(i18n("Select Transaction")); d->ui->m_description->setText(i18n("Select a transaction and press the OK button or use Cancel to select none.")); // clear current register contents d->ui->m_register->clear(); // no selection possible d->ui->m_register->setSelectionMode(QTableWidget::SingleSelection); // setup header font auto font = KMyMoneySettings::listHeaderFontEx(); QFontMetrics fm(font); auto height = fm.lineSpacing() + 6; d->ui->m_register->horizontalHeader()->setMinimumHeight(height); d->ui->m_register->horizontalHeader()->setMaximumHeight(height); d->ui->m_register->horizontalHeader()->setFont(font); // setup cell font font = KMyMoneySettings::listCellFontEx(); d->ui->m_register->setFont(font); // ... setup the register columns ... d->ui->m_register->setupRegister(d->m_account); // setup buttons // default is to need at least one transaction selected d->ui->buttonBox->button(QDialogButtonBox::Ok)->setDisabled(true); // catch some events from the register d->ui->m_register->installEventFilter(this); connect(d->ui->m_register, &KMyMoneyRegister::Register::transactionsSelected, this, &KSelectTransactionsDlg::slotEnableOk); connect(d->ui->m_register, &KMyMoneyRegister::Register::editTransaction, this, &QDialog::accept); connect(d->ui->buttonBox, &QDialogButtonBox::helpRequested, this, &KSelectTransactionsDlg::slotHelp); } KSelectTransactionsDlg::~KSelectTransactionsDlg() { Q_D(KSelectTransactionsDlg); delete d; } void KSelectTransactionsDlg::slotEnableOk(const KMyMoneyRegister::SelectedTransactions& list) { Q_D(KSelectTransactionsDlg); d->ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(list.count() != 0); } void KSelectTransactionsDlg::addTransaction(const MyMoneyTransaction& t) { Q_D(KSelectTransactionsDlg); foreach (const auto split, t.splits()) { if (split.accountId() == d->m_account.id()) { KMyMoneyRegister::Transaction* tr = KMyMoneyRegister::Register::transactionFactory(d->ui->m_register, t, split, 0); // force full detail display tr->setNumRowsRegister(tr->numRowsRegister(true)); break; } } } int KSelectTransactionsDlg::exec() { Q_D(KSelectTransactionsDlg); d->ui->m_register->updateRegister(true); d->ui->m_register->update(); d->ui->m_register->setFocus(); return QDialog::exec(); } void KSelectTransactionsDlg::slotHelp() { // KHelpClient::invokeHelp("details.ledgers.match"); } void KSelectTransactionsDlg::showEvent(QShowEvent* event) { Q_D(KSelectTransactionsDlg); QDialog::showEvent(event); d->ui->m_register->resize((int)eWidgets::eTransaction::Column::Detail, true); } void KSelectTransactionsDlg::resizeEvent(QResizeEvent* ev) { Q_D(KSelectTransactionsDlg); // don't forget the resizer QDialog::resizeEvent(ev); // resize the register d->ui->m_register->resize((int)eWidgets::eTransaction::Column::Detail, true); } MyMoneyTransaction KSelectTransactionsDlg::transaction() const { Q_D(const KSelectTransactionsDlg); MyMoneyTransaction t; QList list; list = d->ui->m_register->selectedItems(); if (list.count()) { if (auto _t = dynamic_cast(list[0])) t = _t->transaction(); } return t; } KMyMoneyRegister::Register* KSelectTransactionsDlg::getRegister() { Q_D(KSelectTransactionsDlg); return d->ui->m_register; } bool KSelectTransactionsDlg::eventFilter(QObject* o, QEvent* e) { Q_D(KSelectTransactionsDlg); auto rc = false; - QKeyEvent* k = nullptr; if (o == d->ui->m_register) { switch (e->type()) { case QEvent::KeyPress: - k = dynamic_cast(e); - if ((k && k->modifiers() & Qt::KeyboardModifierMask) == 0 - || (k && k->modifiers() & Qt::KeypadModifier) != 0) { - switch (k->key()) { - case Qt::Key_Return: - case Qt::Key_Enter: - if (d->ui->buttonBox->button(QDialogButtonBox::Ok)->isEnabled()) { - accept(); - rc = true; - } - // tricky fall through here - default: - break; + if (auto k = dynamic_cast(e)) { + if ((k->modifiers() & Qt::KeyboardModifierMask) == 0 + || (k->modifiers() & Qt::KeypadModifier) != 0) { + switch (k->key()) { + case Qt::Key_Return: + case Qt::Key_Enter: + if (d->ui->buttonBox->button(QDialogButtonBox::Ok)->isEnabled()) { + accept(); + rc = true; + } + // tricky fall through here + default: + break; + } } } // tricky fall through here default: break; } } return rc; } diff --git a/kmymoney/dialogs/stdtransactioneditor.cpp b/kmymoney/dialogs/stdtransactioneditor.cpp index 11c0dd14f..cd43fd7d9 100644 --- a/kmymoney/dialogs/stdtransactioneditor.cpp +++ b/kmymoney/dialogs/stdtransactioneditor.cpp @@ -1,1662 +1,1665 @@ /*************************************************************************** stdtransactioneditor.cpp ---------- begin : Wed Jun 07 2006 copyright : (C) 2006 by Thomas Baumgart email : Thomas Baumgart (C) 2017, 2018 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 "stdtransactioneditor.h" #include "transactioneditor_p.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneyreconcilecombo.h" #include "kmymoneycashflowcombo.h" #include "kmymoneypayeecombo.h" #include "kmymoneytagcombo.h" #include "ktagcontainer.h" #include "tabbar.h" #include "kmymoneycategory.h" #include "kmymoneymvccombo.h" #include "kmymoneydateinput.h" #include "kmymoneyedit.h" #include "kmymoneylineedit.h" #include "kmymoneyaccountselector.h" #include "mymoneyfile.h" #include "mymoneypayee.h" #include "mymoneytag.h" #include "kmymoneyutils.h" #include "kmymoneycompletion.h" #include "transaction.h" #include "transactionform.h" #include "mymoneytransactionfilter.h" #include "kmymoneysettings.h" #include "transactioneditorcontainer.h" #include "ksplittransactiondlg.h" #include "kcurrencycalculator.h" #include "kselecttransactionsdlg.h" #include "widgetenums.h" using namespace eWidgets; using namespace KMyMoneyRegister; using namespace KMyMoneyTransactionForm; class StdTransactionEditorPrivate : public TransactionEditorPrivate { Q_DISABLE_COPY(StdTransactionEditorPrivate) public: StdTransactionEditorPrivate(StdTransactionEditor *qq) : TransactionEditorPrivate(qq), m_inUpdateVat(false) { } ~StdTransactionEditorPrivate() { } MyMoneyMoney m_shares; bool m_inUpdateVat; }; StdTransactionEditor::StdTransactionEditor() : TransactionEditor(*new StdTransactionEditorPrivate(this)) { } StdTransactionEditor::StdTransactionEditor(TransactionEditorContainer* regForm, KMyMoneyRegister::Transaction* item, const KMyMoneyRegister::SelectedTransactions& list, const QDate& lastPostDate) : TransactionEditor(*new StdTransactionEditorPrivate(this), regForm, item, list, lastPostDate) { } StdTransactionEditor::~StdTransactionEditor() { } void StdTransactionEditor::createEditWidgets() { Q_D(StdTransactionEditor); // we only create the account widget in case it is needed // to avoid confusion in the tab order later on. if (d->m_item->showRowInForm(0)) { auto account = new KMyMoneyCategory; account->setPlaceholderText(i18n("Account")); account->setObjectName(QLatin1String("Account")); d->m_editWidgets["account"] = account; connect(account, &QComboBox::editTextChanged, this, &StdTransactionEditor::slotUpdateButtonState); connect(account, &KMyMoneyCombo::itemSelected, this, &StdTransactionEditor::slotUpdateAccount); } auto payee = new KMyMoneyPayeeCombo; payee->setPlaceholderText(i18n("Payer/Receiver")); payee->setObjectName(QLatin1String("Payee")); d->m_editWidgets["payee"] = payee; connect(payee, &KMyMoneyMVCCombo::createItem, this, &StdTransactionEditor::slotNewPayee); connect(payee, &KMyMoneyMVCCombo::objectCreation, this, &StdTransactionEditor::objectCreation); connect(payee, &KMyMoneyMVCCombo::itemSelected, this, &StdTransactionEditor::slotUpdatePayee); connect(payee, &QComboBox::editTextChanged, this, &StdTransactionEditor::slotUpdateButtonState); auto category = new KMyMoneyCategory(true, nullptr); category->setPlaceholderText(i18n("Category/Account")); category->setObjectName(QLatin1String("Category/Account")); d->m_editWidgets["category"] = category; connect(category, &KMyMoneyCombo::itemSelected, this, &StdTransactionEditor::slotUpdateCategory); connect(category, &QComboBox::editTextChanged, this, &StdTransactionEditor::slotUpdateButtonState); connect(category, &KMyMoneyCombo::createItem, this, &StdTransactionEditor::slotCreateCategory); connect(category, &KMyMoneyCombo::objectCreation, this, &StdTransactionEditor::objectCreation); connect(category->splitButton(), &QAbstractButton::clicked, this, &StdTransactionEditor::slotEditSplits); // initially disable the split button since we don't have an account set if (category->splitButton()) category->splitButton()->setDisabled(d->m_account.id().isEmpty()); auto tag = new KTagContainer; tag->tagCombo()->setPlaceholderText(i18n("Tag")); tag->tagCombo()->setObjectName(QLatin1String("Tag")); d->m_editWidgets["tag"] = tag; connect(tag->tagCombo(), &KMyMoneyMVCCombo::createItem, this, &StdTransactionEditor::slotNewTag); connect(tag->tagCombo(), &KMyMoneyMVCCombo::objectCreation, this, &StdTransactionEditor::objectCreation); auto memo = new KTextEdit; memo->setObjectName(QLatin1String("Memo")); memo->setTabChangesFocus(true); connect(memo, &QTextEdit::textChanged, this, &StdTransactionEditor::slotUpdateMemoState); connect(memo, &QTextEdit::textChanged, this, &StdTransactionEditor::slotUpdateButtonState); d->m_editWidgets["memo"] = memo; d->m_memoText.clear(); d->m_memoChanged = false; bool showNumberField = true; switch (d->m_account.accountType()) { case eMyMoney::Account::Type::Savings: case eMyMoney::Account::Type::Cash: case eMyMoney::Account::Type::Loan: case eMyMoney::Account::Type::AssetLoan: case eMyMoney::Account::Type::Asset: case eMyMoney::Account::Type::Liability: case eMyMoney::Account::Type::Equity: showNumberField = KMyMoneySettings::alwaysShowNrField(); break; case eMyMoney::Account::Type::Income: case eMyMoney::Account::Type::Expense: showNumberField = false; break; default: break; } if (showNumberField) { auto number = new KMyMoneyLineEdit; number->setPlaceholderText(i18n("Number")); number->setObjectName(QLatin1String("Number")); d->m_editWidgets["number"] = number; connect(number, &KMyMoneyLineEdit::lineChanged, this, &StdTransactionEditor::slotNumberChanged); // number->installEventFilter(this); } auto postDate = new KMyMoneyDateInput; d->m_editWidgets["postdate"] = postDate; postDate->setObjectName(QLatin1String("PostDate")); connect(postDate, &KMyMoneyDateInput::dateChanged, this, &StdTransactionEditor::slotUpdateButtonState); postDate->setDate(QDate()); auto value = new KMyMoneyEdit; d->m_editWidgets["amount"] = value; value->setObjectName(QLatin1String("Amount")); value->setResetButtonVisible(false); connect(value, &KMyMoneyEdit::valueChanged, this, &StdTransactionEditor::slotUpdateAmount); connect(value, &KMyMoneyEdit::textChanged, this, &StdTransactionEditor::slotUpdateButtonState); value = new KMyMoneyEdit; d->m_editWidgets["payment"] = value; value->setObjectName(QLatin1String("Payment")); value->setResetButtonVisible(false); connect(value, &KMyMoneyEdit::valueChanged, this, &StdTransactionEditor::slotUpdatePayment); connect(value, &KMyMoneyEdit::textChanged, this, &StdTransactionEditor::slotUpdateButtonState); value = new KMyMoneyEdit; d->m_editWidgets["deposit"] = value; value->setObjectName(QLatin1String("Deposit")); value->setResetButtonVisible(false); connect(value, &KMyMoneyEdit::valueChanged, this, &StdTransactionEditor::slotUpdateDeposit); connect(value, &KMyMoneyEdit::textChanged, this, &StdTransactionEditor::slotUpdateButtonState); auto cashflow = new KMyMoneyCashFlowCombo(d->m_account.accountGroup(), nullptr); d->m_editWidgets["cashflow"] = cashflow; cashflow->setObjectName(QLatin1String("Cashflow")); connect(cashflow, &KMyMoneyCashFlowCombo::directionSelected, this, &StdTransactionEditor::slotUpdateCashFlow); auto reconcile = new KMyMoneyReconcileCombo; d->m_editWidgets["status"] = reconcile; reconcile->setObjectName(QLatin1String("Reconcile")); KMyMoneyRegister::QWidgetContainer::iterator it_w; for (it_w = d->m_editWidgets.begin(); it_w != d->m_editWidgets.end(); ++it_w) { (*it_w)->installEventFilter(this); } // if we don't have more than 1 selected transaction, we don't need // the "don't change" item in some of the combo widgets if (!isMultiSelection()) { reconcile->removeDontCare(); cashflow->removeDontCare(); } QLabel* label; d->m_editWidgets["account-label"] = label = new QLabel(i18n("Account")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["category-label"] = label = new QLabel(i18n("Category")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["tag-label"] = label = new QLabel(i18n("Tags")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["memo-label"] = label = new QLabel(i18n("Memo")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["number-label"] = label = new QLabel(i18n("Number")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["date-label"] = label = new QLabel(i18n("Date")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["amount-label"] = label = new QLabel(i18n("Amount")); label->setAlignment(Qt::AlignVCenter); d->m_editWidgets["status-label"] = label = new QLabel(i18n("Status")); label->setAlignment(Qt::AlignVCenter); // create a copy of tabbar above the form (if we are created for a form) auto form = dynamic_cast(d->m_regForm); if (form) { auto tabbar = new KMyMoneyTransactionForm::TabBar; d->m_editWidgets["tabbar"] = tabbar; tabbar->setObjectName(QLatin1String("TabBar")); tabbar->copyTabs(form->getTabBar()); connect(tabbar, &KMyMoneyTransactionForm::TabBar::tabCurrentChanged, this, &StdTransactionEditor::slotUpdateAction); connect(tabbar, &KMyMoneyTransactionForm::TabBar::tabCurrentChanged, this, &TransactionEditor::operationTypeChanged); } setupPrecision(); } void StdTransactionEditor::setupCategoryWidget(QString& categoryId) { Q_D(StdTransactionEditor); if (auto categoryWidget = dynamic_cast(d->m_editWidgets["category"])) TransactionEditor::setupCategoryWidget(categoryWidget, d->m_splits, categoryId, SLOT(slotEditSplits())); if (d->m_splits.count() == 1) d->m_shares = d->m_splits[0].shares(); } bool StdTransactionEditor::isTransfer(const QString& accId1, const QString& accId2) const { if (accId1.isEmpty() || accId2.isEmpty()) return false; return MyMoneyFile::instance()->account(accId1).isIncomeExpense() == MyMoneyFile::instance()->account(accId2).isIncomeExpense(); } void StdTransactionEditor::loadEditWidgets(eRegister::Action action) { Q_D(StdTransactionEditor); // don't kick off VAT processing from here d->m_inUpdateVat = true; QMap::const_iterator it_w; QWidget* w; AccountSet aSet; // load the account widget if (auto account = dynamic_cast(haveWidget("account"))) { aSet.addAccountGroup(eMyMoney::Account::Type::Asset); aSet.addAccountGroup(eMyMoney::Account::Type::Liability); aSet.removeAccountType(eMyMoney::Account::Type::AssetLoan); aSet.removeAccountType(eMyMoney::Account::Type::CertificateDep); aSet.removeAccountType(eMyMoney::Account::Type::Investment); aSet.removeAccountType(eMyMoney::Account::Type::Stock); aSet.removeAccountType(eMyMoney::Account::Type::MoneyMarket); aSet.removeAccountType(eMyMoney::Account::Type::Loan); aSet.load(account->selector()); account->completion()->setSelected(d->m_account.id()); account->slotItemSelected(d->m_account.id()); } // load the payee widget auto payee = dynamic_cast(d->m_editWidgets["payee"]); if (payee) payee->loadPayees(MyMoneyFile::instance()->payeeList()); // load the category widget auto category = dynamic_cast(d->m_editWidgets["category"]); if (category) disconnect(category, &KMyMoneyCategory::focusIn, this, &StdTransactionEditor::slotEditSplits); // load the tag widget //auto tag = dynamic_cast(m_editWidgets["tag"]); auto tag = dynamic_cast(d->m_editWidgets["tag"]); if (tag) tag->loadTags(MyMoneyFile::instance()->tagList()); // check if the current transaction has a reference to an equity account auto haveEquityAccount = false; foreach (const auto split, d->m_transaction.splits()) { auto acc = MyMoneyFile::instance()->account(split.accountId()); if (acc.accountType() == eMyMoney::Account::Type::Equity) { haveEquityAccount = true; break; } } aSet.clear(); aSet.addAccountGroup(eMyMoney::Account::Type::Asset); aSet.addAccountGroup(eMyMoney::Account::Type::Liability); aSet.addAccountGroup(eMyMoney::Account::Type::Income); aSet.addAccountGroup(eMyMoney::Account::Type::Expense); if (KMyMoneySettings::expertMode() || haveEquityAccount) aSet.addAccountGroup(eMyMoney::Account::Type::Equity); aSet.removeAccountType(eMyMoney::Account::Type::CertificateDep); aSet.removeAccountType(eMyMoney::Account::Type::Investment); aSet.removeAccountType(eMyMoney::Account::Type::Stock); aSet.removeAccountType(eMyMoney::Account::Type::MoneyMarket); if (category) aSet.load(category->selector()); // if an account is specified then remove it from the widget so that the user // cannot create a transfer with from and to account being the same account if (!d->m_account.id().isEmpty()) category->selector()->removeItem(d->m_account.id()); // also show memo text if isMultiSelection() if (auto memoWidget = dynamic_cast(d->m_editWidgets["memo"])) memoWidget->setText(d->m_split.memo()); // need to know if it changed d->m_memoText = d->m_split.memo(); d->m_memoChanged = false; if (!isMultiSelection()) { if (auto dateWidget = dynamic_cast(d->m_editWidgets["postdate"])) { if (d->m_transaction.postDate().isValid()) dateWidget->setDate(d->m_transaction.postDate()); else if (d->m_lastPostDate.isValid()) dateWidget->setDate(d->m_lastPostDate); else dateWidget->setDate(QDate::currentDate()); } if ((w = haveWidget("number")) != 0) { if (auto lineEdit = dynamic_cast(w)) lineEdit->loadText(d->m_split.number()); if (d->m_transaction.id().isEmpty() // new transaction && dynamic_cast(w)->text().isEmpty() // no number filled in && d->m_account.accountType() == eMyMoney::Account::Type::Checkings // checkings account && KMyMoneySettings::autoIncCheckNumber() // and auto inc number turned on? && action != eRegister::Action::Deposit // only transfers or withdrawals && d->m_paymentMethod == eMyMoney::Schedule::PaymentType::WriteChecque) {// only for WriteChecque assignNextNumber(); } } if (auto statusWidget = dynamic_cast(d->m_editWidgets["status"])) statusWidget->setState(d->m_split.reconcileFlag()); QString payeeId = d->m_split.payeeId(); if (payee && !payeeId.isEmpty()) payee->setSelectedItem(payeeId); QList t = d->m_split.tagIdList(); if (tag && !t.isEmpty()) for (auto i = 0; i < t.size(); ++i) tag->addTagWidget(t[i]); d->m_splits.clear(); if (d->m_transaction.splitCount() < 2) { category->completion()->setSelected(QString()); } else { foreach (const auto split, d->m_transaction.splits()) { if (split == d->m_split) continue; d->m_splits.append(split); } } QString categoryId; setupCategoryWidget(categoryId); if ((w = haveWidget("cashflow")) != 0) { if (auto cashflow = dynamic_cast(w)) cashflow->setDirection(!d->m_split.value().isPositive() ? eRegister::CashFlowDirection::Payment : eRegister::CashFlowDirection::Deposit); // include isZero case } if ((w = haveWidget("category-label")) != 0) { if (auto categoryLabel = dynamic_cast(w)) { if (isTransfer(d->m_split.accountId(), categoryId)) { if (d->m_split.value().isPositive()) categoryLabel->setText(i18n("Transfer from")); else categoryLabel->setText(i18n("Transfer to")); } } } MyMoneyMoney value = d->m_split.shares(); if (haveWidget("deposit")) { auto depositWidget = dynamic_cast(d->m_editWidgets["deposit"]); auto paymentWidget = dynamic_cast(d->m_editWidgets["payment"]); - if (!depositWidget || !paymentWidget) { + if (depositWidget && paymentWidget) { if (d->m_split.shares().isNegative()) { depositWidget->loadText(QString()); paymentWidget->setValue(value.abs()); } else { depositWidget->setValue(value.abs()); paymentWidget->loadText(QString()); } } } if ((w = haveWidget("amount")) != 0) { if (auto amountWidget = dynamic_cast(w)) amountWidget->setValue(value.abs()); } slotUpdateCategory(categoryId); // try to preset for specific action if a new transaction is being started if (d->m_transaction.id().isEmpty()) { if ((w = haveWidget("category-label")) != 0) { auto tabbar = dynamic_cast(haveWidget("tabbar")); if (action == eRegister::Action::None) { if (tabbar) { action = static_cast(tabbar->currentIndex()); } } if (action != eRegister::Action::None) { if (auto categoryLabel = dynamic_cast(w)) { if (action == eRegister::Action::Transfer) { if (d->m_split.value().isPositive()) categoryLabel->setText(i18n("Transfer from")); else categoryLabel->setText(i18n("Transfer to")); } } if ((w = haveWidget("cashflow")) != 0) { if (auto cashflow = dynamic_cast(w)) { if (action == eRegister::Action::Deposit || (action == eRegister::Action::Transfer && d->m_split.value().isPositive())) cashflow->setDirection(eRegister::CashFlowDirection::Deposit); else cashflow->setDirection(eRegister::CashFlowDirection::Payment); } } if (tabbar) { tabbar->setCurrentIndex((int)action); } } } } else { if (auto tabbar = dynamic_cast(haveWidget("tabbar"))) { if (!isTransfer(d->m_split.accountId(), categoryId)) tabbar->setCurrentIndex(d->m_split.value().isNegative() ? (int)eRegister::Action::Withdrawal : (int)eRegister::Action::Deposit); else tabbar->setCurrentIndex((int)eRegister::Action::Transfer); } } } else { // isMultiSelection() if (auto postDateWidget = dynamic_cast(d->m_editWidgets["postdate"])) postDateWidget->loadDate(QDate()); if (auto statusWidget = dynamic_cast(d->m_editWidgets["status"])) statusWidget->setState(eMyMoney::Split::State::Unknown); if (haveWidget("deposit")) { if (auto depositWidget = dynamic_cast(d->m_editWidgets["deposit"])) { depositWidget->loadText(QString()); depositWidget->setAllowEmpty(); } if (auto paymentWidget = dynamic_cast(d->m_editWidgets["payment"])) { paymentWidget->loadText(QString()); paymentWidget->setAllowEmpty(); } } if ((w = haveWidget("amount")) != 0) { if (auto amountWidget = dynamic_cast(w)) { amountWidget->loadText(QString()); amountWidget->setAllowEmpty(); } } slotUpdateAction((int)action); if ((w = haveWidget("tabbar")) != 0) { w->setEnabled(false); } - category->completion()->setSelected(QString()); + if (category && category->completion()) + category->completion()->setSelected(QString()); } // allow kick off VAT processing again d->m_inUpdateVat = false; } void StdTransactionEditor::loadEditWidgets() { loadEditWidgets(eRegister::Action::None); } QWidget* StdTransactionEditor::firstWidget() const { Q_D(const StdTransactionEditor); - QWidget* w = 0; + QWidget* w = nullptr; if (d->m_initialAction != eRegister::Action::None) { w = haveWidget("payee"); } return w; } void StdTransactionEditor::slotReloadEditWidgets() { Q_D(StdTransactionEditor); // reload category widget if (auto category = dynamic_cast(d->m_editWidgets["category"])) { QString categoryId = category->selectedItem(); AccountSet aSet; aSet.addAccountGroup(eMyMoney::Account::Type::Asset); aSet.addAccountGroup(eMyMoney::Account::Type::Liability); aSet.addAccountGroup(eMyMoney::Account::Type::Income); aSet.addAccountGroup(eMyMoney::Account::Type::Expense); if (KMyMoneySettings::expertMode()) aSet.addAccountGroup(eMyMoney::Account::Type::Equity); aSet.load(category->selector()); // if an account is specified then remove it from the widget so that the user // cannot create a transfer with from and to account being the same account if (!d->m_account.id().isEmpty()) category->selector()->removeItem(d->m_account.id()); if (!categoryId.isEmpty()) category->setSelectedItem(categoryId); } // reload payee widget if (auto payee = dynamic_cast(d->m_editWidgets["payee"])) { QString payeeId = payee->selectedItem(); payee->loadPayees(MyMoneyFile::instance()->payeeList()); if (!payeeId.isEmpty()) { payee->setSelectedItem(payeeId); } } // reload tag widget if (auto tag = dynamic_cast(d->m_editWidgets["tag"])) { QString tagId = tag->tagCombo()->selectedItem(); tag->loadTags(MyMoneyFile::instance()->tagList()); if (!tagId.isEmpty()) { tag->RemoveAllTagWidgets(); tag->addTagWidget(tagId); } } } void StdTransactionEditor::slotUpdatePayee(const QString& payeeId) { Q_D(StdTransactionEditor); // we have a new payee assigned to this transaction. // in case there is no category assigned, no value entered and no // memo available, we search for the last transaction of this payee // in the account. if (d->m_transaction.id().isEmpty() && d->m_splits.count() == 0 && KMyMoneySettings::autoFillTransaction() != 0) { // check if category is empty if (auto category = dynamic_cast(d->m_editWidgets["category"])) { QStringList list; category->selectedItems(list); if (!list.isEmpty()) return; } // check if memo is empty auto memo = dynamic_cast(d->m_editWidgets["memo"]); if (memo && !memo->toPlainText().isEmpty()) return; // check if all value fields are empty KMyMoneyEdit* amount; QStringList fields; fields << "amount" << "payment" << "deposit"; QStringList::const_iterator it_f; for (it_f = fields.constBegin(); it_f != fields.constEnd(); ++it_f) { amount = dynamic_cast(haveWidget(*it_f)); if (amount && !amount->value().isZero()) return; } #if 0 // Tony mentioned, that autofill does not work when he changed the date. Well, // that certainly makes sense when you enter transactions in register mode as // opposed to form mode, because the date field is located prior to the date // field in the tab order of the widgets and the user might have already // changed it. // // So I commented out the code that checks the date but left it in for reference. // (ipwizard, 2008-04-07) // check if date has been altered by user auto postDate = dynamic_cast(m_editWidgets["postdate"]); if (postDate && (m_lastPostDate.isValid() && (postDate->date() != m_lastPostDate)) || (!m_lastPostDate.isValid() && (postDate->date() != QDate::currentDate()))) return; #endif // if we got here, we have to autofill autoFill(payeeId); } // If payee has associated default account (category), set that now. const MyMoneyPayee& payeeObj = MyMoneyFile::instance()->payee(payeeId); if (payeeObj.defaultAccountEnabled()) { if (auto category = dynamic_cast(d->m_editWidgets["category"])) category->slotItemSelected(payeeObj.defaultAccountId()); } } MyMoneyMoney StdTransactionEditor::shares(const MyMoneyTransaction& t) const { Q_D(const StdTransactionEditor); MyMoneyMoney result; foreach (const auto split, t.splits()) { if (split.accountId() == d->m_account.id()) { result += split.shares(); } } return result; } struct uniqTransaction { const MyMoneyTransaction* t; int cnt; }; void StdTransactionEditor::autoFill(const QString& payeeId) { Q_D(StdTransactionEditor); QList > list; MyMoneyTransactionFilter filter(d->m_account.id()); filter.addPayee(payeeId); MyMoneyFile::instance()->transactionList(list, filter); if (!list.empty()) { // ok, we found at least one previous transaction. now we clear out // what we have collected so far and add those splits from // the previous transaction. QList >::const_iterator it_t; QMap uniqList; // collect the transactions and see if we have any duplicates for (it_t = list.constBegin(); it_t != list.constEnd(); ++it_t) { QString key = (*it_t).first.accountSignature(); int cnt = 0; QMap::iterator it_u; do { QString ukey = QString("%1-%2").arg(key).arg(cnt); it_u = uniqList.find(ukey); if (it_u == uniqList.end()) { uniqList[ukey].t = &((*it_t).first); uniqList[ukey].cnt = 1; } else if (KMyMoneySettings::autoFillTransaction() == 1) { // we already have a transaction with this signature. we must // now check, if we should really treat it as a duplicate according // to the value comparison delta. MyMoneyMoney s1 = shares(*((*it_u).t)); MyMoneyMoney s2 = shares((*it_t).first); if (s2.abs() > s1.abs()) { MyMoneyMoney t(s1); s1 = s2; s2 = t; } MyMoneyMoney diff; if (s2.isZero()) diff = s1.abs(); else diff = ((s1 - s2) / s2).convert(10000); if (diff.isPositive() && diff <= MyMoneyMoney(KMyMoneySettings::autoFillDifference(), 100)) { uniqList[ukey].t = &((*it_t).first); break; // end while loop } } else if (KMyMoneySettings::autoFillTransaction() == 2) { (*it_u).cnt++; break; // end while loop } ++cnt; } while (it_u != uniqList.end()); } MyMoneyTransaction t; if (KMyMoneySettings::autoFillTransaction() != 2) { #if 0 // I removed this code to allow cancellation of an autofill if // it does not match even if there is only a single matching // transaction for the payee in question. In case, we want to revert // to the old behavior, don't forget to uncomment the closing // brace further down in the code as well. (ipwizard 2009-01-16) if (uniqList.count() == 1) { t = list.last().first; } else { #endif QPointer dlg = new KSelectTransactionsDlg(d->m_account, d->m_regForm); dlg->setWindowTitle(i18n("Select autofill transaction")); QMap::const_iterator it_u; for (it_u = uniqList.constBegin(); it_u != uniqList.constEnd(); ++it_u) { dlg->addTransaction(*(*it_u).t); } auto tRegister = dlg->getRegister(); // setup sort order tRegister->setSortOrder("1,-9,-4"); // sort the transactions according to the sort setting tRegister->sortItems(); // and select the last item if (tRegister->lastItem()) tRegister->selectItem(tRegister->lastItem()); if (dlg->exec() == QDialog::Accepted) { t = dlg->transaction(); } #if 0 } #endif } else { int maxCnt = 0; QMap::const_iterator it_u; for (it_u = uniqList.constBegin(); it_u != uniqList.constEnd(); ++it_u) { if ((*it_u).cnt > maxCnt) { t = *(*it_u).t; maxCnt = (*it_u).cnt; } } } if (t != MyMoneyTransaction()) { d->m_transaction.removeSplits(); d->m_split = MyMoneySplit(); MyMoneySplit otherSplit; foreach (const auto split, t.splits()) { MyMoneySplit s(split); s.setReconcileFlag(eMyMoney::Split::State::NotReconciled); s.setReconcileDate(QDate()); s.clearId(); s.setBankID(QString()); // older versions of KMyMoney used to set the action // we don't need this anymore if (s.action() != MyMoneySplit::actionName(eMyMoney::Split::Action::Amortization) && s.action() != MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)) { s.setAction(QString()); } // FIXME update check number. The old comment contained // // // If a check number is already specified by the user it is // used. If the input field is empty and the previous transaction // contains a checknumber, the next usable check number will be assigned // to the transaction. // auto editNr = dynamic_cast(haveWidget("number")); if (editNr && !editNr->text().isEmpty()) { s.setNumber(editNr->text()); } else if (!s.number().isEmpty()) { s.setNumber(KMyMoneyUtils::nextCheckNumber(d->m_account)); } // if the memos should not be used with autofill or // if the transaction has exactly two splits, remove // the memo text of the split that does not reference // the current account. This allows the user to change // the autofilled memo text which will then also be used // in this split. See createTransaction() for this logic. if ((s.accountId() != d->m_account.id() && t.splitCount() == 2) || !KMyMoneySettings::autoFillUseMemos()) s.setMemo(QString()); d->m_transaction.addSplit(s); if (s.accountId() == d->m_account.id() && d->m_split == MyMoneySplit()) { d->m_split = s; } else { otherSplit = s; } } // make sure to extract the right action eRegister::Action action; action = d->m_split.shares().isNegative() ? eRegister::Action::Withdrawal : eRegister::Action::Deposit; if (d->m_transaction.splitCount() == 2) { auto acc = MyMoneyFile::instance()->account(otherSplit.accountId()); if (acc.isAssetLiability()) action = eRegister::Action::Transfer; } // now setup the widgets with the new data but keep the date if (auto postdateWidget = dynamic_cast(d->m_editWidgets["postdate"])) { auto date = postdateWidget->date(); loadEditWidgets(action); postdateWidget->setDate(date); } } } // focus jumps into the category field QWidget* w; if ((w = haveWidget("payee")) != 0) { w->setFocus(); } } void StdTransactionEditor::slotUpdateAction(int action) { Q_D(StdTransactionEditor); auto tabbar = dynamic_cast(haveWidget("tabbar")); if (tabbar) { auto categoryLabel = dynamic_cast(haveWidget("category-label")); auto cashflow = dynamic_cast(d->m_editWidgets["cashflow"]); if (!categoryLabel || !cashflow) return; switch (action) { case (int)eRegister::Action::Deposit: categoryLabel->setText(i18n("Category")); cashflow->setDirection(eRegister::CashFlowDirection::Deposit); break; case (int)eRegister::Action::Transfer: if (d->m_split.shares().isNegative()) { cashflow->setDirection(eRegister::CashFlowDirection::Payment); categoryLabel->setText(i18n("Transfer to")); } else { cashflow->setDirection(eRegister::CashFlowDirection::Deposit); categoryLabel->setText(i18n("Transfer from")); } tabbar->setCurrentIndex((int)eRegister::Action::Transfer); slotUpdateCashFlow(cashflow->direction()); break; case (int)eRegister::Action::Withdrawal: categoryLabel->setText(i18n("Category")); cashflow->setDirection(eRegister::CashFlowDirection::Payment); break; } resizeForm(); } } void StdTransactionEditor::slotUpdateCashFlow(eRegister::CashFlowDirection dir) { auto categoryLabel = dynamic_cast(haveWidget("category-label")); if (auto cashflow = dynamic_cast(haveWidget("cashflow"))) cashflow->setDirection(dir); // qDebug("Update cashflow to %d", dir); if (categoryLabel) { auto tabbar = dynamic_cast(haveWidget("tabbar")); if (!tabbar) return; // no transaction form if (categoryLabel->text() != i18n("Category")) { tabbar->setCurrentIndex((int)eRegister::Action::Transfer); if (dir == eRegister::CashFlowDirection::Deposit) { categoryLabel->setText(i18n("Transfer from")); } else { categoryLabel->setText(i18n("Transfer to")); } resizeForm(); } else { if (dir == eRegister::CashFlowDirection::Deposit) tabbar->setCurrentIndex((int)eRegister::Action::Deposit); else tabbar->setCurrentIndex((int)eRegister::Action::Withdrawal); } } } void StdTransactionEditor::slotUpdateCategory(const QString& id) { Q_D(StdTransactionEditor); auto categoryLabel = dynamic_cast(haveWidget("category-label")); // qDebug("Update category to %s", qPrintable(id)); if (categoryLabel) { auto tabbar = dynamic_cast(haveWidget("tabbar")); auto amount = dynamic_cast(d->m_editWidgets["amount"]); auto val = amount ? amount->value() : MyMoneyMoney(); if (categoryLabel->text() == i18n("Transfer from")) { val = -val; } else { val = val.abs(); } if (tabbar) { tabbar->setTabEnabled((int)eRegister::Action::Transfer, true); tabbar->setTabEnabled((int)eRegister::Action::Deposit, true); tabbar->setTabEnabled((int)eRegister::Action::Withdrawal, true); } bool disableTransferTab = false; if (!id.isEmpty()) { auto acc = MyMoneyFile::instance()->account(id); if (acc.isAssetLiability() || acc.accountGroup() == eMyMoney::Account::Type::Equity) { if (tabbar) { tabbar->setCurrentIndex((int)eRegister::Action::Transfer); tabbar->setTabEnabled((int)eRegister::Action::Deposit, false); tabbar->setTabEnabled((int)eRegister::Action::Withdrawal, false); } auto cashflow = dynamic_cast(d->m_editWidgets["cashflow"]); if (val.isZero()) { if (cashflow && (cashflow->direction() == eRegister::CashFlowDirection::Deposit)) { categoryLabel->setText(i18n("Transfer from")); } else { categoryLabel->setText(i18n("Transfer to")); } } else if (val.isNegative()) { categoryLabel->setText(i18n("Transfer from")); - cashflow->setDirection(eRegister::CashFlowDirection::Deposit); + if (cashflow) + cashflow->setDirection(eRegister::CashFlowDirection::Deposit); } else categoryLabel->setText(i18n("Transfer to")); } else { disableTransferTab = true; categoryLabel->setText(i18n("Category")); } updateAmount(val); } else { //id.isEmpty() if (auto category = dynamic_cast(d->m_editWidgets["category"])) disableTransferTab = !category->currentText().isEmpty(); categoryLabel->setText(i18n("Category")); } if (tabbar) { if (disableTransferTab) { // set the proper tab before disabling the currently active tab if (tabbar->currentIndex() == (int)eRegister::Action::Transfer) { tabbar->setCurrentIndex(val.isPositive() ? (int)eRegister::Action::Withdrawal : (int)eRegister::Action::Deposit); } tabbar->setTabEnabled((int)eRegister::Action::Transfer, false); } tabbar->update(); } resizeForm(); } updateVAT(false); } void StdTransactionEditor::slotUpdatePayment(const QString& txt) { Q_D(StdTransactionEditor); MyMoneyMoney val(txt); auto depositWidget = dynamic_cast(d->m_editWidgets["deposit"]); auto paymentWidget = dynamic_cast(d->m_editWidgets["payment"]); if (!depositWidget || !paymentWidget) return; if (val.isNegative()) { depositWidget->setValue(val.abs()); paymentWidget->clearText(); } else { depositWidget->clearText(); } updateVAT(); } void StdTransactionEditor::slotUpdateDeposit(const QString& txt) { Q_D(StdTransactionEditor); MyMoneyMoney val(txt); auto depositWidget = dynamic_cast(d->m_editWidgets["deposit"]); auto paymentWidget = dynamic_cast(d->m_editWidgets["payment"]); if (!depositWidget || !paymentWidget) return; if (val.isNegative()) { paymentWidget->setValue(val.abs()); depositWidget->clearText(); } else { paymentWidget->clearText(); } updateVAT(); } void StdTransactionEditor::slotUpdateAmount(const QString& txt) { // qDebug("Update amount to %s", qPrintable(txt)); MyMoneyMoney val(txt); updateAmount(val); updateVAT(true); } void StdTransactionEditor::updateAmount(const MyMoneyMoney& val) { // we don't do anything if we have multiple transactions selected if (isMultiSelection()) return; Q_D(StdTransactionEditor); auto categoryLabel = dynamic_cast(haveWidget("category-label")); if (categoryLabel) { if (auto cashflow = dynamic_cast(d->m_editWidgets["cashflow"])) { if (!val.isPositive()) { // fixes BUG321317 if (categoryLabel->text() != i18n("Category")) { if (cashflow->direction() == eRegister::CashFlowDirection::Payment) { categoryLabel->setText(i18n("Transfer to")); } } else { slotUpdateCashFlow(cashflow->direction()); } if (auto amountWidget = dynamic_cast(d->m_editWidgets["amount"])) amountWidget->setValue(val.abs()); } else { if (categoryLabel->text() != i18n("Category")) { if (cashflow->direction() == eRegister::CashFlowDirection::Payment) { categoryLabel->setText(i18n("Transfer to")); } else { categoryLabel->setText(i18n("Transfer from")); cashflow->setDirection(eRegister::CashFlowDirection::Deposit); // editing with +ve shows 'from' not 'pay to' } } if (auto amountWidget = dynamic_cast(d->m_editWidgets["amount"])) amountWidget->setValue(val.abs()); } } } } void StdTransactionEditor::updateVAT(bool amountChanged) { Q_D(StdTransactionEditor); // make sure that we don't do this recursively if (d->m_inUpdateVat) return; // we don't do anything if we have multiple transactions selected if (isMultiSelection()) return; // if auto vat assignment for this account is turned off // we don't care about taxes if (d->m_account.value("NoVat") == "Yes") return; // more splits than category and tax are not supported if (d->m_splits.count() > 2) return; // in order to do anything, we need an amount MyMoneyMoney amount, newAmount; bool amountOk; amount = amountFromWidget(&amountOk); if (!amountOk) return; // If the transaction has a tax and a category split, remove the tax split if (d->m_splits.count() == 2) { newAmount = removeVatSplit(); if (d->m_splits.count() == 2) // not removed? return; } else if (auto category = dynamic_cast(d->m_editWidgets["category"])) { // otherwise, we need a category if (category->selectedItem().isEmpty()) return; // if no VAT account is associated with this category/account, then we bail out MyMoneyAccount cat = MyMoneyFile::instance()->account(category->selectedItem()); if (cat.value("VatAccount").isEmpty()) return; newAmount = amount; } // seems we have everything we need if (amountChanged) newAmount = amount; MyMoneyTransaction transaction; if (createTransaction(transaction, d->m_transaction, d->m_split)) { if (addVatSplit(transaction, newAmount)) { d->m_transaction = transaction; if (!d->m_transaction.splits().isEmpty()) d->m_split = d->m_transaction.splits().front(); loadEditWidgets(); // if we made this a split transaction, then move the // focus to the memo field if (qApp->focusWidget() == haveWidget("category")) { QWidget* w = haveWidget("memo"); if (w) w->setFocus(); } } } } bool StdTransactionEditor::addVatSplit(MyMoneyTransaction& tr, const MyMoneyMoney& amount) { if (tr.splitCount() != 2) return false; Q_D(StdTransactionEditor); auto file = MyMoneyFile::instance(); // extract the category split from the transaction MyMoneyAccount category = file->account(tr.splitByAccount(d->m_account.id(), false).accountId()); return file->addVATSplit(tr, d->m_account, category, amount); } MyMoneyMoney StdTransactionEditor::removeVatSplit() { Q_D(StdTransactionEditor); // we only deal with splits that have three splits if (d->m_splits.count() != 2) return amountFromWidget(); MyMoneySplit c; // category split MyMoneySplit t; // tax split auto netValue = false; foreach (const auto split , d->m_splits) { auto acc = MyMoneyFile::instance()->account(split.accountId()); if (!acc.value("VatAccount").isEmpty()) { netValue = (acc.value("VatAmount").toLower() == "net"); c = split; } else if (!acc.value("VatRate").isEmpty()) { t = split; } } // bail out if not all splits are setup if (c.id().isEmpty() || t.id().isEmpty()) return amountFromWidget(); MyMoneyMoney amount; // reduce the splits if (netValue) { amount = -c.shares(); } else { amount = -(c.shares() + t.shares()); } // remove tax split from the list, ... d->m_splits.clear(); d->m_splits.append(c); // ... make sure that the widget is updated ... // block the signals to avoid popping up the split editor dialog // for nothing d->m_editWidgets["category"]->blockSignals(true); QString id; setupCategoryWidget(id); d->m_editWidgets["category"]->blockSignals(false); // ... and return the updated amount return amount; } bool StdTransactionEditor::isComplete(QString& reason) const { Q_D(const StdTransactionEditor); reason.clear(); QMap::const_iterator it_w; auto postDate = dynamic_cast(d->m_editWidgets["postdate"]); if (postDate) { QDate accountOpeningDate = d->m_account.openingDate(); for (QList::const_iterator it_s = d->m_splits.constBegin(); it_s != d->m_splits.constEnd(); ++it_s) { const MyMoneyAccount& acc = MyMoneyFile::instance()->account((*it_s).accountId()); // compute the newest opening date of all accounts involved in the transaction if (acc.openingDate() > accountOpeningDate) accountOpeningDate = acc.openingDate(); } // check the selected category in case m_splits hasn't been updated yet auto category = dynamic_cast(d->m_editWidgets["category"]); if (category && !category->selectedItem().isEmpty()) { MyMoneyAccount cat = MyMoneyFile::instance()->account(category->selectedItem()); if (cat.openingDate() > accountOpeningDate) accountOpeningDate = cat.openingDate(); } if (postDate->date().isValid() && (postDate->date() < accountOpeningDate)) { postDate->markAsBadDate(true, KMyMoneySettings::schemeColor(SchemeColor::Negative)); reason = i18n("Cannot enter transaction with postdate prior to account's opening date."); postDate->setToolTip(reason); return false; } postDate->markAsBadDate(); postDate->setToolTip(""); } for (it_w = d->m_editWidgets.begin(); it_w != d->m_editWidgets.end(); ++it_w) { auto payee = dynamic_cast(*it_w); auto tagContainer = dynamic_cast(*it_w); auto category = dynamic_cast(*it_w); auto amount = dynamic_cast(*it_w); auto reconcile = dynamic_cast(*it_w); auto cashflow = dynamic_cast(*it_w); auto memo = dynamic_cast(*it_w); if (payee && !(payee->currentText().isEmpty())) break; if (category && !category->lineEdit()->text().isEmpty()) break; if (amount && !(amount->value().isZero())) break; // the following widgets are only checked if we are editing multiple transactions if (isMultiSelection()) { if (auto tabbar = dynamic_cast(haveWidget("tabbar"))) tabbar->setEnabled(true); if (reconcile && reconcile->state() != eMyMoney::Split::State::Unknown) break; if (cashflow && cashflow->direction() != eRegister::CashFlowDirection::Unknown) break; if (postDate && postDate->date().isValid() && (postDate->date() >= d->m_account.openingDate())) break; if (memo && d->m_memoChanged) break; if (tagContainer && !(tagContainer->selectedTags().isEmpty())) // Tag is optional field break; } } return it_w != d->m_editWidgets.end(); } void StdTransactionEditor::slotCreateCategory(const QString& name, QString& id) { Q_D(StdTransactionEditor); MyMoneyAccount acc, parent; acc.setName(name); auto cashflow = dynamic_cast(haveWidget("cashflow")); if (cashflow) { // form based input if (cashflow->direction() == eRegister::CashFlowDirection::Deposit) parent = MyMoneyFile::instance()->income(); else parent = MyMoneyFile::instance()->expense(); } else if (haveWidget("deposit")) { // register based input if (auto deposit = dynamic_cast(d->m_editWidgets["deposit"])) { if (deposit->value().isPositive()) parent = MyMoneyFile::instance()->income(); else parent = MyMoneyFile::instance()->expense(); } } else parent = MyMoneyFile::instance()->expense(); // TODO extract possible first part of a hierarchy and check if it is one // of our top categories. If so, remove it and select the parent // according to this information. slotNewCategory(acc, parent); // return id id = acc.id(); } int StdTransactionEditor::slotEditSplits() { Q_D(StdTransactionEditor); int rc = QDialog::Rejected; if (!d->m_openEditSplits) { // only get in here in a single instance d->m_openEditSplits = true; // force focus change to update all data - QWidget* w = dynamic_cast(d->m_editWidgets["category"])->splitButton(); + auto categoryWidget = dynamic_cast(d->m_editWidgets["category"]); + QWidget* w = categoryWidget ? categoryWidget->splitButton() : nullptr; if (w) w->setFocus(); auto amount = dynamic_cast(haveWidget("amount")); auto deposit = dynamic_cast(haveWidget("deposit")); auto payment = dynamic_cast(haveWidget("payment")); KMyMoneyCashFlowCombo* cashflow = 0; eRegister::CashFlowDirection dir = eRegister::CashFlowDirection::Unknown; bool isValidAmount = false; if (amount) { isValidAmount = amount->lineedit()->text().length() != 0; if ((cashflow = dynamic_cast(haveWidget("cashflow")))) dir = cashflow->direction(); } else { if (deposit) { if (deposit->lineedit()->text().length() != 0) { isValidAmount = true; dir = eRegister::CashFlowDirection::Deposit; } } if (payment) { if (payment->lineedit()->text().length() != 0) { isValidAmount = true; dir = eRegister::CashFlowDirection::Payment; } } if (!deposit || !payment) { qDebug("Internal error: deposit(%p) & payment(%p) widgets not found but required", deposit, payment); return rc; } } if (dir == eRegister::CashFlowDirection::Unknown) dir = eRegister::CashFlowDirection::Payment; MyMoneyTransaction transaction; if (createTransaction(transaction, d->m_transaction, d->m_split)) { MyMoneyMoney value; QPointer dlg = new KSplitTransactionDlg(transaction, transaction.splits().isEmpty() ? MyMoneySplit() : transaction.splits().front(), d->m_account, isValidAmount, dir == eRegister::CashFlowDirection::Deposit, MyMoneyMoney(), d->m_priceInfo, d->m_regForm); connect(dlg.data(), &KSplitTransactionDlg::objectCreation, this, &StdTransactionEditor::objectCreation); connect(dlg.data(), &KSplitTransactionDlg::createCategory, this, &StdTransactionEditor::slotNewCategory); if ((rc = dlg->exec()) == QDialog::Accepted) { d->m_transaction = dlg->transaction(); if (!d->m_transaction.splits().isEmpty()) d->m_split = d->m_transaction.splits().front(); loadEditWidgets(); } delete dlg; } // focus jumps into the tag field if ((w = haveWidget("tag")) != 0) { w->setFocus(); } d->m_openEditSplits = false; } return rc; } void StdTransactionEditor::checkPayeeInSplit(MyMoneySplit& s, const QString& payeeId) { if (s.accountId().isEmpty()) return; auto acc = MyMoneyFile::instance()->account(s.accountId()); if (acc.isIncomeExpense()) { s.setPayeeId(payeeId); } else { if (s.payeeId().isEmpty()) s.setPayeeId(payeeId); } } MyMoneyMoney StdTransactionEditor::amountFromWidget(bool* update) const { Q_D(const StdTransactionEditor); bool updateValue = false; MyMoneyMoney value; auto cashflow = dynamic_cast(haveWidget("cashflow")); if (cashflow) { // form based input if (auto amount = dynamic_cast(d->m_editWidgets["amount"])) { // if both fields do not contain changes -> no need to update if (cashflow->direction() != eRegister::CashFlowDirection::Unknown && !amount->lineedit()->text().isEmpty()) updateValue = true; value = amount->value(); if (cashflow->direction() == eRegister::CashFlowDirection::Payment) value = -value; } } else if (haveWidget("deposit")) { // register based input auto deposit = dynamic_cast(d->m_editWidgets["deposit"]); auto payment = dynamic_cast(d->m_editWidgets["payment"]); - if (deposit || payment) { + if (deposit && payment) { // if both fields do not contain text -> no need to update if (!(deposit->lineedit()->text().isEmpty() && payment->lineedit()->text().isEmpty())) updateValue = true; if (deposit->value().isPositive()) value = deposit->value(); else value = -(payment->value()); } } if (update) *update = updateValue; // determine the max fraction for this account and // adjust the value accordingly return value.convert(d->m_account.fraction()); } bool StdTransactionEditor::createTransaction(MyMoneyTransaction& t, const MyMoneyTransaction& torig, const MyMoneySplit& sorig, bool skipPriceDialog) { Q_D(StdTransactionEditor); // extract price info from original transaction d->m_priceInfo.clear(); if (!torig.id().isEmpty()) { foreach (const auto split, torig.splits()) { if (split.id() != sorig.id()) { MyMoneyAccount cat = MyMoneyFile::instance()->account(split.accountId()); if (cat.currencyId() != d->m_account.currencyId()) { if (!split.shares().isZero() && !split.value().isZero()) { d->m_priceInfo[cat.currencyId()] = (split.shares() / split.value()).reduce(); } } } } } t = torig; t.removeSplits(); t.setCommodity(d->m_account.currencyId()); auto postDate = dynamic_cast(d->m_editWidgets["postdate"]); if (postDate && postDate->date().isValid()) { t.setPostDate(postDate->date()); } // we start with the previous values, make sure we can add them later on MyMoneySplit s0 = sorig; s0.clearId(); // make sure we reference this account here s0.setAccountId(d->m_account.id()); // memo and number field are special: if we have multiple transactions selected // and the edit field is empty, we treat it as "not modified". // FIXME a better approach would be to have a 'dirty' flag with the widgets // which identifies if the originally loaded value has been modified // by the user auto memo = dynamic_cast(d->m_editWidgets["memo"]); if (memo) { if (!isMultiSelection() || (isMultiSelection() && d->m_memoChanged)) s0.setMemo(memo->toPlainText()); } if (auto number = dynamic_cast(haveWidget("number"))) { if (!isMultiSelection() || (isMultiSelection() && !number->text().isEmpty())) s0.setNumber(number->text()); } auto payee = dynamic_cast(d->m_editWidgets["payee"]); QString payeeId; - if (!isMultiSelection() || (isMultiSelection() && payee && !payee->currentText().isEmpty())) { + if (payee && (!isMultiSelection() || (isMultiSelection() && !payee->currentText().isEmpty()))) { payeeId = payee->selectedItem(); s0.setPayeeId(payeeId); } //KMyMoneyTagCombo* tag = dynamic_cast(m_editWidgets["tag"]); auto tag = dynamic_cast(d->m_editWidgets["tag"]); - if (!isMultiSelection() || (isMultiSelection() && tag && !tag->selectedTags().isEmpty())) { + if (tag && (!isMultiSelection() || (isMultiSelection() && !tag->selectedTags().isEmpty()))) { s0.setTagIdList(tag->selectedTags()); } bool updateValue; MyMoneyMoney value = amountFromWidget(&updateValue); if (updateValue) { // for this account, the shares and value is the same s0.setValue(value); s0.setShares(value); } else { value = s0.value(); } // if we mark the split reconciled here, we'll use today's date if no reconciliation date is given auto status = dynamic_cast(d->m_editWidgets["status"]); if (status && status->state() != eMyMoney::Split::State::Unknown) s0.setReconcileFlag(status->state()); if (s0.reconcileFlag() == eMyMoney::Split::State::Reconciled && !s0.reconcileDate().isValid()) s0.setReconcileDate(QDate::currentDate()); checkPayeeInSplit(s0, payeeId); // add the split to the transaction t.addSplit(s0); // if we have no other split we create it // if we have none or only one other split, we reconstruct it here // if we have more than one other split, we take them as they are // make sure to perform all those changes on a local copy QList splits = d->m_splits; MyMoneySplit s1; if (splits.isEmpty()) { s1.setMemo(s0.memo()); splits.append(s1); // make sure we will fill the value and share fields later on updateValue = true; } // FIXME in multiSelection we currently only support transactions with one // or two splits. So we check the original transaction and extract the other // split or create it if (isMultiSelection()) { if (torig.splitCount() == 2) { foreach (const auto split, torig.splits()) { if (split.id() == sorig.id()) continue; s1 = split; s1.clearId(); break; } } } else { if (splits.count() == 1) { s1 = splits[0]; s1.clearId(); } } if (isMultiSelection() || splits.count() == 1) { auto category = dynamic_cast(d->m_editWidgets["category"]); - if (!isMultiSelection() || (isMultiSelection() && category && !category->currentText().isEmpty())) { + if (category && (!isMultiSelection() || (isMultiSelection() && !category->currentText().isEmpty()))) { s1.setAccountId(category->selectedItem()); } // if the first split has a memo but the second split is empty, // we just copy the memo text over if (memo) { if (!isMultiSelection() || (isMultiSelection() && !memo->toPlainText().isEmpty())) { // if the memo is filled, we check if the // account referenced by s1 is a regular account or a category. // in case of a regular account, we just leave the memo as is // in case of a category we simply copy the new value over the old. // in case we don't even have an account id, we just skip because // the split will be removed later on anyway. if (!s1.memo().isEmpty() && s1.memo() != s0.memo()) { if (!s1.accountId().isEmpty()) { auto acc = MyMoneyFile::instance()->account(s1.accountId()); if (acc.isIncomeExpense()) s1.setMemo(s0.memo()); else if (KMessageBox::questionYesNo(d->m_regForm, i18n("Do you want to replace memo

%1

with memo

%2

in the other split?", s1.memo(), s0.memo()), i18n("Copy memo"), KStandardGuiItem::yes(), KStandardGuiItem::no(), QStringLiteral("CopyMemoOver")) == KMessageBox::Yes) s1.setMemo(s0.memo()); } } else { s1.setMemo(s0.memo()); } } } if (updateValue && !s1.accountId().isEmpty()) { s1.setValue(-value); MyMoneyMoney shares; if (!skipPriceDialog) { if (!KCurrencyCalculator::setupSplitPrice(shares, t, s1, d->m_priceInfo, d->m_regForm)) return false; } else { MyMoneyAccount cat = MyMoneyFile::instance()->account(s1.accountId()); if (d->m_priceInfo.find(cat.currencyId()) != d->m_priceInfo.end()) { shares = (s1.value() * d->m_priceInfo[cat.currencyId()]).reduce().convert(cat.fraction()); } else shares = s1.value(); } s1.setShares(shares); } checkPayeeInSplit(s1, payeeId); if (!s1.accountId().isEmpty()) t.addSplit(s1); } else { foreach (const auto split, splits) { s1 = split; s1.clearId(); checkPayeeInSplit(s1, payeeId); t.addSplit(s1); } } return true; } void StdTransactionEditor::setupFinalWidgets() { addFinalWidget(haveWidget("deposit")); addFinalWidget(haveWidget("payment")); addFinalWidget(haveWidget("amount")); addFinalWidget(haveWidget("status")); } void StdTransactionEditor::slotUpdateAccount(const QString& id) { Q_D(StdTransactionEditor); TransactionEditor::slotUpdateAccount(id); auto category = dynamic_cast(d->m_editWidgets["category"]); if (category && category->splitButton()) { category->splitButton()->setDisabled(id.isEmpty()); } } diff --git a/kmymoney/dialogs/transactioneditor.cpp b/kmymoney/dialogs/transactioneditor.cpp index 14458c923..8b9626abc 100644 --- a/kmymoney/dialogs/transactioneditor.cpp +++ b/kmymoney/dialogs/transactioneditor.cpp @@ -1,853 +1,853 @@ /*************************************************************************** transactioneditor.cpp ---------- begin : Wed Jun 07 2006 copyright : (C) 2006 by Thomas Baumgart email : Thomas Baumgart (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 "transactioneditor.h" #include "transactioneditor_p.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneytagcombo.h" #include "knewinvestmentwizard.h" #include "knewaccountdlg.h" #include "ktagcontainer.h" #include "tabbar.h" #include "mymoneyutils.h" #include "mymoneyexception.h" #include "kmymoneycategory.h" #include "kmymoneymvccombo.h" #include "kmymoneyedit.h" #include "kmymoneylineedit.h" #include "mymoneyfile.h" #include "mymoneyprice.h" #include "mymoneysecurity.h" #include "kmymoneyutils.h" #include "kmymoneycompletion.h" #include "transaction.h" #include "transactionform.h" #include "kmymoneysettings.h" #include "transactioneditorcontainer.h" #include "kcurrencycalculator.h" #include "icons.h" using namespace KMyMoneyRegister; using namespace KMyMoneyTransactionForm; using namespace Icons; TransactionEditor::TransactionEditor() : d_ptr(new TransactionEditorPrivate(this)) { Q_D(TransactionEditor); d->init(); } TransactionEditor::TransactionEditor(TransactionEditorPrivate &dd, TransactionEditorContainer* regForm, KMyMoneyRegister::Transaction* item, const KMyMoneyRegister::SelectedTransactions& list, const QDate& lastPostDate) : d_ptr(&dd) // d_ptr(new TransactionEditorPrivate) { Q_D(TransactionEditor); d->m_paymentMethod = eMyMoney::Schedule::PaymentType::Any; d->m_transactions = list; d->m_regForm = regForm; d->m_item = item; d->m_transaction = item->transaction(); d->m_split = item->split(); d->m_lastPostDate = lastPostDate; d->m_initialAction = eWidgets::eRegister::Action::None; d->m_openEditSplits = false; d->m_memoChanged = false; d->m_item->startEditMode(); connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, this, static_cast(&TransactionEditor::slotUpdateAccount)); } TransactionEditor::TransactionEditor(TransactionEditorPrivate &dd) : d_ptr(&dd) { Q_D(TransactionEditor); d->init(); } TransactionEditor::~TransactionEditor() { Q_D(TransactionEditor); // Make sure the widgets do not send out signals to the editor anymore // After all, the editor is about to die //disconnect first tagCombo: auto w = dynamic_cast(haveWidget("tag")); if (w && w->tagCombo()) { w->tagCombo()->disconnect(this); } QMap::iterator it_w; for (it_w = d->m_editWidgets.begin(); it_w != d->m_editWidgets.end(); ++it_w) { (*it_w)->disconnect(this); } d->m_regForm->removeEditWidgets(d->m_editWidgets); d->m_item->leaveEditMode(); emit finishEdit(d->m_transactions); } void TransactionEditor::slotUpdateAccount(const QString& id) { Q_D(TransactionEditor); d->m_account = MyMoneyFile::instance()->account(id); setupPrecision(); } void TransactionEditor::slotUpdateAccount() { Q_D(TransactionEditor); // reload m_account as it might have been changed d->m_account = MyMoneyFile::instance()->account(d->m_account.id()); setupPrecision(); } void TransactionEditor::setupPrecision() { Q_D(TransactionEditor); const int prec = (d->m_account.id().isEmpty()) ? 2 : MyMoneyMoney::denomToPrec(d->m_account.fraction()); QStringList widgets = QString("amount,deposit,payment").split(','); QStringList::const_iterator it_w; for (it_w = widgets.constBegin(); it_w != widgets.constEnd(); ++it_w) { QWidget * w; if ((w = haveWidget(*it_w)) != 0) { if (auto precisionWidget = dynamic_cast(w)) precisionWidget->setPrecision(prec); } } } void TransactionEditor::setup(QWidgetList& tabOrderWidgets, const MyMoneyAccount& account, eWidgets::eRegister::Action action) { Q_D(TransactionEditor); d->m_account = account; d->m_initialAction = action; createEditWidgets(); d->m_regForm->arrangeEditWidgets(d->m_editWidgets, d->m_item); d->m_regForm->tabOrder(tabOrderWidgets, d->m_item); QWidget* w = haveWidget("tabbar"); if (w) { tabOrderWidgets.append(w); auto tabbar = dynamic_cast(w); if ((tabbar) && (action == eWidgets::eRegister::Action::None)) { action = static_cast(tabbar->currentIndex()); } } loadEditWidgets(action); // remove all unused widgets and don't forget to remove them // from the tab order list as well d->m_editWidgets.removeOrphans(); QWidgetList::iterator it_w; const QWidgetList editWidgets(d->m_editWidgets.values()); for (it_w = tabOrderWidgets.begin(); it_w != tabOrderWidgets.end();) { if (editWidgets.contains(*it_w)) { ++it_w; } else { // before we remove the widget, we make sure it's not a part of a known one. // these could be a direct child in case of KMyMoneyDateInput and KMyMoneyEdit // where we store the pointer to the surrounding frame in editWidgets // or the parent is called "KMyMoneyCategoryFrame" if (*it_w) { if (editWidgets.contains((*it_w)->parentWidget()) || ((*it_w)->parentWidget() && (*it_w)->parentWidget()->objectName() == QLatin1String("KMyMoneyCategoryFrame"))) { ++it_w; } else { // qDebug("Remove '%s' from taborder", qPrintable((*it_w)->objectName())); it_w = tabOrderWidgets.erase(it_w); } } else { it_w = tabOrderWidgets.erase(it_w); } } } clearFinalWidgets(); setupFinalWidgets(); slotUpdateButtonState(); } void TransactionEditor::setup(QWidgetList& tabOrderWidgets, const MyMoneyAccount& account) { setup(tabOrderWidgets, account, eWidgets::eRegister::Action::None); } void TransactionEditor::setup(QWidgetList& tabOrderWidgets) { setup(tabOrderWidgets, MyMoneyAccount(), eWidgets::eRegister::Action::None); } MyMoneyAccount TransactionEditor::account() const { Q_D(const TransactionEditor); return d->m_account; } void TransactionEditor::setScheduleInfo(const QString& si) { Q_D(TransactionEditor); d->m_scheduleInfo = si; } void TransactionEditor::setPaymentMethod(eMyMoney::Schedule::PaymentType pm) { Q_D(TransactionEditor); d->m_paymentMethod = pm; } void TransactionEditor::clearFinalWidgets() { Q_D(TransactionEditor); d->m_finalEditWidgets.clear(); } void TransactionEditor::addFinalWidget(const QWidget* w) { Q_D(TransactionEditor); if (w) { d->m_finalEditWidgets << w; } } void TransactionEditor::slotReloadEditWidgets() { } bool TransactionEditor::eventFilter(QObject* o, QEvent* e) { Q_D(TransactionEditor); bool rc = false; if (o == haveWidget("number")) { if (e->type() == QEvent::MouseButtonDblClick) { emit assignNumber(); rc = true; } } // if the object is a widget, the event is a key press event and // the object is one of our edit widgets, then .... auto numberWiget = dynamic_cast(o); if (o->isWidgetType() && (e->type() == QEvent::KeyPress) && numberWiget && d->m_editWidgets.values().contains(numberWiget)) { auto k = dynamic_cast(e); - if ((k && k->modifiers() & Qt::KeyboardModifierMask) == 0 - || (k && k->modifiers() & Qt::KeypadModifier) != 0) { + if ((k && (k->modifiers() & Qt::KeyboardModifierMask)) == 0 + || (k && (k->modifiers() & Qt::KeypadModifier)) != 0) { bool isFinal = false; QList::const_iterator it_w; switch (k->key()) { case Qt::Key_Return: case Qt::Key_Enter: // we check, if the object is one of the m_finalEditWidgets and if it's // a KMyMoneyEdit object that the value is not 0. If any of that is the // case, it's the final object. In other cases, we convert the enter // key into a TAB key to move between the fields. Of course, we only need // to do this as long as the appropriate option is set. In all other cases, // we treat the return/enter key as such. if (KMyMoneySettings::enterMovesBetweenFields()) { for (it_w = d->m_finalEditWidgets.constBegin(); !isFinal && it_w != d->m_finalEditWidgets.constEnd(); ++it_w) { if (*it_w == o) { if (auto widget = dynamic_cast(*it_w)) { isFinal = !(widget->value().isZero()); } else isFinal = true; } } } else isFinal = true; // for the non-final objects, we treat the return key as a TAB if (!isFinal) { QKeyEvent evt(e->type(), Qt::Key_Tab, k->modifiers(), QString(), k->isAutoRepeat(), k->count()); QApplication::sendEvent(o, &evt); // in case of a category item and the split button is visible // send a second event so that we get passed the button. auto widget = dynamic_cast(o); if (widget && widget->splitButton()) QApplication::sendEvent(o, &evt); } else { QTimer::singleShot(0, this, SIGNAL(returnPressed())); } // don't process any further rc = true; break; case Qt::Key_Escape: QTimer::singleShot(0, this, SIGNAL(escapePressed())); break; } } } return rc; } void TransactionEditor::slotNumberChanged(const QString& txt) { Q_D(TransactionEditor); auto next = txt; QString schedInfo; if (!d->m_scheduleInfo.isEmpty()) { schedInfo = i18n("
Processing schedule for %1.
", d->m_scheduleInfo); } while (MyMoneyFile::instance()->checkNoUsed(d->m_account.id(), next)) { if (KMessageBox::questionYesNo(d->m_regForm, QString("") + schedInfo + i18n("
Check number %1 has already been used in account %2.
" "
Do you want to replace it with the next available number?
", next, d->m_account.name()) + QString("
"), i18n("Duplicate number")) == KMessageBox::Yes) { assignNextNumber(); next = KMyMoneyUtils::nextCheckNumber(d->m_account); } else if (auto number = dynamic_cast(haveWidget("number"))) { number->setText(QString()); break; } } } void TransactionEditor::slotUpdateMemoState() { Q_D(TransactionEditor); KTextEdit* memo = dynamic_cast(d->m_editWidgets["memo"]); if (memo) { d->m_memoChanged = (memo->toPlainText() != d->m_memoText); } } void TransactionEditor::slotUpdateButtonState() { QString reason; emit transactionDataSufficient(isComplete(reason)); } QWidget* TransactionEditor::haveWidget(const QString& name) const { Q_D(const TransactionEditor); return d->m_editWidgets.haveWidget(name); } int TransactionEditor::slotEditSplits() { return QDialog::Rejected; } void TransactionEditor::setTransaction(const MyMoneyTransaction& t, const MyMoneySplit& s) { Q_D(TransactionEditor); d->m_transaction = t; d->m_split = s; loadEditWidgets(); } bool TransactionEditor::isMultiSelection() const { Q_D(const TransactionEditor); return d->m_transactions.count() > 1; } bool TransactionEditor::fixTransactionCommodity(const MyMoneyAccount& account) { Q_D(TransactionEditor); bool rc = true; bool firstTimeMultiCurrency = true; d->m_account = account; auto file = MyMoneyFile::instance(); // determine the max fraction for this account MyMoneySecurity sec = file->security(d->m_account.currencyId()); int fract = d->m_account.fraction(); // scan the list of selected transactions KMyMoneyRegister::SelectedTransactions::iterator it_t; for (it_t = d->m_transactions.begin(); (rc == true) && (it_t != d->m_transactions.end()); ++it_t) { // there was a time when the schedule editor did not setup the transaction commodity // let's give a helping hand here for those old schedules if ((*it_t).transaction().commodity().isEmpty()) (*it_t).transaction().setCommodity(d->m_account.currencyId()); // we need to check things only if a different commodity is used if (d->m_account.currencyId() != (*it_t).transaction().commodity()) { MyMoneySecurity osec = file->security((*it_t).transaction().commodity()); switch ((*it_t).transaction().splitCount()) { case 0: // new transaction, guess nothing's here yet ;) break; case 1: try { // make sure, that the value is equal to the shares, don't forget our own copy MyMoneySplit& splitB = (*it_t).split(); // reference usage wanted here if (d->m_split == splitB) d->m_split.setValue(splitB.shares()); splitB.setValue(splitB.shares()); (*it_t).transaction().modifySplit(splitB); } catch (const MyMoneyException &e) { qDebug("Unable to update commodity to second splits currency in %s: '%s'", qPrintable((*it_t).transaction().id()), qPrintable(e.what())); } break; case 2: // If we deal with multiple currencies we make sure, that for // transactions with two splits, the transaction's commodity is the // currency of the currently selected account. This saves us from a // lot of grieve later on. We just have to switch the // transactions commodity. Let's assume the following scenario: // - transactions commodity is CA // - splitB and account's currencyId is CB // - splitA is of course in CA (otherwise we have a real problem) // - Value is V in both splits // - Shares in splitB is SB // - Shares in splitA is SA (and equal to V) // // We do the following: // - change transactions commodity to CB // - set V in both splits to SB // - modify the splits in the transaction try { // retrieve the splits MyMoneySplit& splitB = (*it_t).split(); // reference usage wanted here MyMoneySplit splitA = (*it_t).transaction().splitByAccount(d->m_account.id(), false); // - set V in both splits to SB. Don't forget our own copy if (d->m_split == splitB) { d->m_split.setValue(splitB.shares()); } splitB.setValue(splitB.shares()); splitA.setValue(-splitB.shares()); (*it_t).transaction().modifySplit(splitA); (*it_t).transaction().modifySplit(splitB); } catch (const MyMoneyException &e) { qDebug("Unable to update commodity to second splits currency in %s: '%s'", qPrintable((*it_t).transaction().id()), qPrintable(e.what())); } break; default: // TODO: use new logic by adjusting all splits by the price // extracted from the selected split. Inform the user that // this will happen and allow him to stop the processing (rc = false) try { QString msg; if (firstTimeMultiCurrency) { firstTimeMultiCurrency = false; if (!isMultiSelection()) { msg = i18n("This transaction has more than two splits and is originally based on a different currency (%1). Using this account to modify the transaction may result in rounding errors. Do you want to continue?", osec.name()); } else { msg = i18n("At least one of the selected transactions has more than two splits and is originally based on a different currency (%1). Using this account to modify the transactions may result in rounding errors. Do you want to continue?", osec.name()); } if (KMessageBox::warningContinueCancel(0, QString("%1").arg(msg)) == KMessageBox::Cancel) { rc = false; } } if (rc == true) { MyMoneyMoney price; if (!(*it_t).split().shares().isZero() && !(*it_t).split().value().isZero()) price = (*it_t).split().shares() / (*it_t).split().value(); MyMoneySplit& mySplit = (*it_t).split(); foreach (const auto split, (*it_t).transaction().splits()) { auto s = split; if (s == mySplit) { s.setValue(s.shares()); if (mySplit == d->m_split) { d->m_split = s; } mySplit = s; } else { s.setValue((s.value() * price).convert(fract)); } (*it_t).transaction().modifySplit(s); } } } catch (const MyMoneyException &e) { qDebug("Unable to update commodity of split currency in %s: '%s'", qPrintable((*it_t).transaction().id()), qPrintable(e.what())); } break; } // set the transaction's ommodity to this account's currency (*it_t).transaction().setCommodity(d->m_account.currencyId()); // update our copy of the transaction that has the focus if ((*it_t).transaction().id() == d->m_transaction.id()) { d->m_transaction = (*it_t).transaction(); } } } return rc; } void TransactionEditor::assignNextNumber() { Q_D(TransactionEditor); if (canAssignNumber()) { auto number = dynamic_cast(haveWidget("number")); QString num = KMyMoneyUtils::nextCheckNumber(d->m_account); bool showMessage = true; int rc = KMessageBox::No; QString schedInfo; if (!d->m_scheduleInfo.isEmpty()) { schedInfo = i18n("
Processing schedule for %1.
", d->m_scheduleInfo); } while (MyMoneyFile::instance()->checkNoUsed(d->m_account.id(), num)) { if (showMessage) { rc = KMessageBox::questionYesNo(d->m_regForm, QString("") + schedInfo + i18n("Check number %1 has already been used in account %2." "
Do you want to replace it with the next available number?
", num, d->m_account.name()) + QString("
"), i18n("Duplicate number")); showMessage = false; } if (rc == KMessageBox::Yes) { num = KMyMoneyUtils::nextCheckNumber(d->m_account); KMyMoneyUtils::updateLastNumberUsed(d->m_account, num); d->m_account.setValue("lastNumberUsed", num); if (number) number->loadText(num); } else { num = QString(); break; } } if (number) number->setText(num); } } bool TransactionEditor::canAssignNumber() const { if (dynamic_cast(haveWidget("number"))) return true; return false; } void TransactionEditor::setupCategoryWidget(KMyMoneyCategory* category, const QList& splits, QString& categoryId, const char* splitEditSlot, bool /* allowObjectCreation */) { disconnect(category, SIGNAL(focusIn()), this, splitEditSlot); #if 0 // FIXME must deal with the logic that suppressObjectCreation is // automatically turned off when the createItem() signal is connected if (allowObjectCreation) category->setSuppressObjectCreation(false); #endif switch (splits.count()) { case 0: categoryId.clear(); if (!category->currentText().isEmpty()) { // category->clearEditText(); // don't clear as could be from another widget - Bug 322768 // make sure, we don't see the selector category->completion()->hide(); } category->completion()->setSelected(QString()); break; case 1: categoryId = splits[0].accountId(); category->completion()->setSelected(categoryId); category->slotItemSelected(categoryId); break; default: categoryId.clear(); category->setSplitTransaction(); connect(category, SIGNAL(focusIn()), this, splitEditSlot); #if 0 // FIXME must deal with the logic that suppressObjectCreation is // automatically turned off when the createItem() signal is connected if (allowObjectCreation) category->setSuppressObjectCreation(true); #endif break; } } bool TransactionEditor::enterTransactions(QString& newId, bool askForSchedule, bool suppressBalanceWarnings) { Q_D(TransactionEditor); newId.clear(); auto file = MyMoneyFile::instance(); // make sure to run through all stuff that is tied to 'focusout events'. d->m_regForm->parentWidget()->setFocus(); QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 10); // we don't need to update our widgets anymore, so we just disconnect the signal disconnect(file, &MyMoneyFile::dataChanged, this, &TransactionEditor::slotReloadEditWidgets); KMyMoneyRegister::SelectedTransactions::iterator it_t; MyMoneyTransaction t; bool newTransactionCreated = false; // make sure, that only a single new transaction can be created. // we need to update m_transactions to contain the new transaction // which is then stored in the variable t when we leave the loop. // m_transactions will be sent out in finishEdit() and forces // the new transaction to be selected in the ledger view // collect the transactions to be stored in the engine in a local // list first, so that the user has a chance to interrupt the storage // process QList list; auto storeTransactions = true; // collect transactions for (it_t = d->m_transactions.begin(); storeTransactions && !newTransactionCreated && it_t != d->m_transactions.end(); ++it_t) { storeTransactions = createTransaction(t, (*it_t).transaction(), (*it_t).split()); // if the transaction was created successfully, append it to the list if (storeTransactions) list.append(t); // if we created a new transaction keep that in mind if (t.id().isEmpty()) newTransactionCreated = true; } // if not interrupted by user, continue to store them in the engine if (storeTransactions) { auto i = 0; emit statusMsg(i18n("Storing transactions")); emit statusProgress(0, list.count()); MyMoneyFileTransaction ft; try { QMap minBalanceEarly; QMap minBalanceAbsolute; QMap maxCreditEarly; QMap maxCreditAbsolute; QMap accountIds; for (MyMoneyTransaction& transaction : list) { // if we have a categorization, make sure we remove // the 'imported' flag automagically if (transaction.splitCount() > 1) transaction.setImported(false); // create information about min and max balances foreach (const auto split, transaction.splits()) { auto acc = file->account(split.accountId()); accountIds[acc.id()] = true; MyMoneyMoney balance = file->balance(acc.id()); if (!acc.value("minBalanceEarly").isEmpty()) { minBalanceEarly[acc.id()] = balance < MyMoneyMoney(acc.value("minBalanceEarly")); } if (!acc.value("minBalanceAbsolute").isEmpty()) { minBalanceAbsolute[acc.id()] = balance < MyMoneyMoney(acc.value("minBalanceAbsolute")); minBalanceEarly[acc.id()] = false; } if (!acc.value("maxCreditEarly").isEmpty()) { maxCreditEarly[acc.id()] = balance < MyMoneyMoney(acc.value("maxCreditEarly")); } if (!acc.value("maxCreditAbsolute").isEmpty()) { maxCreditAbsolute[acc.id()] = balance < MyMoneyMoney(acc.value("maxCreditAbsolute")); maxCreditEarly[acc.id()] = false; } } if (transaction.id().isEmpty()) { bool enter = true; if (askForSchedule && transaction.postDate() > QDate::currentDate()) { KGuiItem enterButton(i18n("&Enter"), Icons::get(Icon::DialogOK), i18n("Accepts the entered data and stores it"), i18n("Use this to enter the transaction into the ledger.")); KGuiItem scheduleButton(i18n("&Schedule"), Icons::get(Icon::AppointmentNew), i18n("Accepts the entered data and stores it as schedule"), i18n("Use this to schedule the transaction for later entry into the ledger.")); enter = KMessageBox::questionYesNo(d->m_regForm, QString("%1").arg(i18n("The transaction you are about to enter has a post date in the future.

Do you want to enter it in the ledger or add it to the schedules?")), i18nc("Dialog caption for 'Enter or schedule' dialog", "Enter or schedule?"), enterButton, scheduleButton, "EnterOrScheduleTransactionInFuture") == KMessageBox::Yes; } if (enter) { // add new transaction file->addTransaction(transaction); // pass the newly assigned id on to the caller newId = transaction.id(); // refresh account object for transactional changes // refresh account and transaction object because they might have changed d->m_account = file->account(d->m_account.id()); t = transaction; // if a new transaction has a valid number, keep it with the account d->keepNewNumber(transaction); } else { // turn object creation on, so that moving the focus does // not screw up the dialog that might be popping up emit objectCreation(true); emit scheduleTransaction(transaction, eMyMoney::Schedule::Occurrence::Once); emit objectCreation(false); newTransactionCreated = false; } // send out the post date of this transaction emit lastPostDateUsed(transaction.postDate()); } else { // modify existing transaction // its number might have been edited // bearing in mind it could contain alpha characters d->keepNewNumber(transaction); file->modifyTransaction(transaction); } } emit statusProgress(i++, 0); // update m_transactions to contain the newly created transaction so that // it is selected as the current one // we need to do that before we commit the transaction to the engine // as we need it during the update of the views that is caused by committing already. if (newTransactionCreated) { d->m_transactions.clear(); MyMoneySplit s; // a transaction w/o a single split should not exist and adding it // should throw an exception in MyMoneyFile::addTransaction, but we // remain on the save side of things to check for it if (t.splitCount() > 0) s = t.splits().front(); KMyMoneyRegister::SelectedTransaction st(t, s, QString()); d->m_transactions.append(st); } // Save pricing information foreach (const auto split, t.splits()) { if ((split.action() != "Buy") && (split.action() != "Reinvest")) { continue; } QString id = split.accountId(); auto acc = file->account(id); MyMoneySecurity sec = file->security(acc.currencyId()); MyMoneyPrice price(acc.currencyId(), sec.tradingCurrency(), t.postDate(), split.price(), "Transaction"); file->addPrice(price); break; } ft.commit(); // now analyze the balances and spit out warnings to the user QMap::const_iterator it_a; if (!suppressBalanceWarnings) { for (it_a = accountIds.constBegin(); it_a != accountIds.constEnd(); ++it_a) { QString msg; auto acc = file->account(it_a.key()); MyMoneyMoney balance = file->balance(acc.id()); const MyMoneySecurity& sec = file->security(acc.currencyId()); QString key; key = "minBalanceEarly"; if (!acc.value(key).isEmpty()) { if (minBalanceEarly[acc.id()] == false && balance < MyMoneyMoney(acc.value(key))) { msg = QString("%1").arg(i18n("The balance of account %1 dropped below the warning balance of %2.", acc.name(), MyMoneyUtils::formatMoney(MyMoneyMoney(acc.value(key)), acc, sec))); } } key = "minBalanceAbsolute"; if (!acc.value(key).isEmpty()) { if (minBalanceAbsolute[acc.id()] == false && balance < MyMoneyMoney(acc.value(key))) { msg = QString("%1").arg(i18n("The balance of account %1 dropped below the minimum balance of %2.", acc.name(), MyMoneyUtils::formatMoney(MyMoneyMoney(acc.value(key)), acc, sec))); } } key = "maxCreditEarly"; if (!acc.value(key).isEmpty()) { if (maxCreditEarly[acc.id()] == false && balance < MyMoneyMoney(acc.value(key))) { msg = QString("%1").arg(i18n("The balance of account %1 dropped below the maximum credit warning limit of %2.", acc.name(), MyMoneyUtils::formatMoney(MyMoneyMoney(acc.value(key)), acc, sec))); } } key = "maxCreditAbsolute"; if (!acc.value(key).isEmpty()) { if (maxCreditAbsolute[acc.id()] == false && balance < MyMoneyMoney(acc.value(key))) { msg = QString("%1").arg(i18n("The balance of account %1 dropped below the maximum credit limit of %2.", acc.name(), MyMoneyUtils::formatMoney(MyMoneyMoney(acc.value(key)), acc, sec))); } } if (!msg.isEmpty()) { emit balanceWarning(d->m_regForm, acc, msg); } } } } catch (const MyMoneyException &e) { qDebug("Unable to store transaction within engine: %s", qPrintable(e.what())); newTransactionCreated = false; } emit statusProgress(-1, -1); emit statusMsg(QString()); } return storeTransactions; } void TransactionEditor::resizeForm() { Q_D(TransactionEditor); // force resizeing of the columns in the form if (auto form = dynamic_cast(d->m_regForm)) QMetaObject::invokeMethod(form, "resize", Qt::QueuedConnection, QGenericReturnArgument(), Q_ARG(int, (int)eWidgets::eTransactionForm::Column::Value1)); } void TransactionEditor::slotNewPayee(const QString& newnameBase, QString& id) { KMyMoneyUtils::newPayee(newnameBase, id); } void TransactionEditor::slotNewTag(const QString& newnameBase, QString& id) { KMyMoneyUtils::newTag(newnameBase, id); } void TransactionEditor::slotNewCategory(MyMoneyAccount& account, const MyMoneyAccount& parent) { KNewAccountDlg::newCategory(account, parent); } void TransactionEditor::slotNewInvestment(MyMoneyAccount& account, const MyMoneyAccount& parent) { KNewInvestmentWizard::newInvestment(account, parent); } diff --git a/kmymoney/kmymoney.cpp b/kmymoney/kmymoney.cpp index a240802cf..35f4db2e4 100644 --- a/kmymoney/kmymoney.cpp +++ b/kmymoney/kmymoney.cpp @@ -1,4332 +1,4335 @@ /*************************************************************************** kmymoney.cpp ------------------- copyright : (C) 2000 by Michael Edwardes (C) 2007 by Thomas Baumgart (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 #include "kmymoney.h" // ---------------------------------------------------------------------------- // Std C++ / STL Includes #include #include #include // ---------------------------------------------------------------------------- // QT Includes #include #include // only for performance tests #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef KF5Holidays_FOUND #include #include #endif // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneysettings.h" #include "kmymoneyadaptor.h" #include "dialogs/settings/ksettingskmymoney.h" #include "dialogs/kbackupdlg.h" #include "dialogs/kenterscheduledlg.h" #include "dialogs/kconfirmmanualenterdlg.h" #include "dialogs/kmymoneypricedlg.h" #include "dialogs/kcurrencyeditdlg.h" #include "dialogs/kequitypriceupdatedlg.h" #include "dialogs/kmymoneyfileinfodlg.h" #include "dialogs/kfindtransactiondlg.h" #include "dialogs/knewbankdlg.h" #include "wizards/newinvestmentwizard/knewinvestmentwizard.h" #include "dialogs/knewaccountdlg.h" #include "dialogs/editpersonaldatadlg.h" #include "dialogs/kcurrencycalculator.h" #include "dialogs/keditscheduledlg.h" #include "wizards/newloanwizard/keditloanwizard.h" #include "dialogs/kpayeereassigndlg.h" #include "dialogs/kcategoryreassigndlg.h" #include "wizards/endingbalancedlg/kendingbalancedlg.h" #include "dialogs/kbalancechartdlg.h" #include "dialogs/kloadtemplatedlg.h" #include "dialogs/kgpgkeyselectiondlg.h" #include "dialogs/ktemplateexportdlg.h" #include "dialogs/transactionmatcher.h" #include "wizards/newuserwizard/knewuserwizard.h" #include "wizards/newaccountwizard/knewaccountwizard.h" #include "dialogs/kbalancewarning.h" #include "widgets/kmymoneyaccountselector.h" #include "widgets/kmymoneypayeecombo.h" #include "widgets/onlinejobmessagesview.h" #include "widgets/amountedit.h" #include "widgets/kmymoneyedit.h" #include "widgets/kmymoneymvccombo.h" #include "views/kmymoneyview.h" #include "views/konlinejoboutbox.h" #include "models/onlinejobmessagesmodel.h" #include "models/models.h" #include "models/accountsmodel.h" #include "models/equitiesmodel.h" #include "models/securitiesmodel.h" #include "mymoney/mymoneyobject.h" #include "mymoney/mymoneyfile.h" #include "mymoney/mymoneyinstitution.h" #include "mymoney/mymoneyaccount.h" #include "mymoney/mymoneyaccountloan.h" #include "mymoney/mymoneysecurity.h" #include "mymoney/mymoneypayee.h" #include "mymoney/mymoneyprice.h" #include "mymoney/mymoneytag.h" #include "mymoney/mymoneybudget.h" #include "mymoney/mymoneyreport.h" #include "mymoney/mymoneysplit.h" #include "mymoney/mymoneyutils.h" #include "mymoney/mymoneystatement.h" #include "mymoney/mymoneyforecast.h" #include "mymoney/mymoneytransactionfilter.h" #include "mymoney/onlinejobmessage.h" #include "converter/mymoneystatementreader.h" #include "converter/mymoneytemplate.h" #include "plugins/interfaces/kmmappinterface.h" #include "plugins/interfaces/kmmviewinterface.h" #include "plugins/interfaces/kmmstatementinterface.h" #include "plugins/interfaces/kmmimportinterface.h" #include "plugins/interfaceloader.h" #include "plugins/onlinepluginextended.h" #include "pluginloader.h" #include "tasks/credittransfer.h" #include "icons/icons.h" #include "misc/webconnect.h" #include "storage/mymoneystoragemgr.h" #include "storage/mymoneystoragexml.h" #include "storage/mymoneystoragebin.h" #include "storage/mymoneystorageanon.h" #include #include "transactioneditor.h" #include "konlinetransferform.h" #include #include #include "kmymoneyutils.h" #include "kcreditswindow.h" #include "ledgerdelegate.h" #include "storageenums.h" #include "mymoneyenums.h" #include "dialogenums.h" #include "menuenums.h" #include "misc/platformtools.h" #ifdef KMM_DEBUG #include "mymoney/storage/mymoneystoragedump.h" #include "mymoneytracer.h" #endif using namespace Icons; using namespace eMenu; static constexpr KCompressionDevice::CompressionType const& COMPRESSION_TYPE = KCompressionDevice::GZip; static constexpr char recoveryKeyId[] = "0xD2B08440"; static constexpr char recoveryKeyId2[] = "59B0F826D2B08440"; // define the default period to warn about an expiring recoverkey to 30 days // but allows to override this setting during build time #ifndef RECOVER_KEY_EXPIRATION_WARNING #define RECOVER_KEY_EXPIRATION_WARNING 30 #endif QHash pActions; QHash pMenus; enum backupStateE { BACKUP_IDLE = 0, BACKUP_MOUNTING, BACKUP_COPYING, BACKUP_UNMOUNTING }; class KMyMoneyApp::Private { public: Private(KMyMoneyApp *app) : q(app), m_ft(0), m_moveToAccountSelector(0), m_statementXMLindex(0), m_balanceWarning(0), m_backupState(backupStateE::BACKUP_IDLE), m_backupResult(0), m_backupMount(0), m_ignoreBackupExitCode(false), m_fileOpen(false), m_fmode(QFileDevice::ReadUser | QFileDevice::WriteUser), + m_fileType(KMyMoneyApp::KmmXML), m_myMoneyView(nullptr), m_startDialog(false), m_progressBar(nullptr), m_statusLabel(nullptr), m_searchDlg(nullptr), m_autoSaveEnabled(true), m_autoSaveTimer(nullptr), m_progressTimer(nullptr), m_autoSavePeriod(0), m_inAutoSaving(false), m_transactionEditor(nullptr), m_endingBalanceDlg(nullptr), m_saveEncrypted(nullptr), m_additionalKeyLabel(nullptr), m_additionalKeyButton(nullptr), m_recentFiles(nullptr), #ifdef KF5Holidays_FOUND m_holidayRegion(0), #endif m_applicationIsReady(true), m_webConnect(new WebConnect(app)) { // since the days of the week are from 1 to 7, // and a day of the week is used to index this bit array, // resize the array to 8 elements (element 0 is left unused) m_processingDays.resize(8); } void closeFile(); void unlinkStatementXML(); void moveInvestmentTransaction(const QString& fromId, const QString& toId, const MyMoneyTransaction& t); QList > automaticReconciliation(const MyMoneyAccount &account, const QList > &transactions, const MyMoneyMoney &amount); /** * The public interface. */ KMyMoneyApp * const q; MyMoneyFileTransaction* m_ft; KMyMoneyAccountSelector* m_moveToAccountSelector; int m_statementXMLindex; KBalanceWarning* m_balanceWarning; /** the configuration object of the application */ KSharedConfigPtr m_config; /** * @brief Structure of plugins objects by their interfaces */ KMyMoneyPlugin::Container m_plugins; /** * The following variable represents the state while crafting a backup. * It can have the following values * * - IDLE: the default value if not performing a backup * - MOUNTING: when a mount command has been issued * - COPYING: when a copy command has been issued * - UNMOUNTING: when an unmount command has been issued */ backupStateE m_backupState; /** * This variable keeps the result of the backup operation. */ int m_backupResult; /** * This variable is set, when the user selected to mount/unmount * the backup volume. */ bool m_backupMount; /** * Flag for internal run control */ bool m_ignoreBackupExitCode; bool m_fileOpen; QFileDevice::Permissions m_fmode; KMyMoneyApp::fileTypeE m_fileType; KProcess m_proc; /// A pointer to the view holding the tabs. KMyMoneyView *m_myMoneyView; /// The URL of the file currently being edited when open. QUrl m_fileName; bool m_startDialog; QString m_mountpoint; QProgressBar* m_progressBar; QTime m_lastUpdate; QLabel* m_statusLabel; // allows multiple imports to be launched trough web connect and to be executed sequentially QQueue m_importUrlsQueue; KFindTransactionDlg* m_searchDlg; // This is Auto Saving related bool m_autoSaveEnabled; QTimer* m_autoSaveTimer; QTimer* m_progressTimer; int m_autoSavePeriod; bool m_inAutoSaving; // pointer to the current transaction editor TransactionEditor* m_transactionEditor; // Reconciliation dialog KEndingBalanceDlg* m_endingBalanceDlg; // Pointer to the combo box used for key selection during // File/Save as KComboBox* m_saveEncrypted; // id's that need to be remembered QString m_accountGoto, m_payeeGoto; QStringList m_additionalGpgKeys; QLabel* m_additionalKeyLabel; QPushButton* m_additionalKeyButton; KRecentFilesAction* m_recentFiles; #ifdef KF5Holidays_FOUND // used by the calendar interface for schedules KHolidays::HolidayRegion* m_holidayRegion; #endif QBitArray m_processingDays; QMap m_holidayMap; QStringList m_consistencyCheckResult; bool m_applicationIsReady; WebConnect* m_webConnect; // methods void consistencyCheck(bool alwaysDisplayResults); static void setThemedCSS(); void copyConsistencyCheckResults(); void saveConsistencyCheckResults(); void checkAccountName(const MyMoneyAccount& _acc, const QString& name) const { auto file = MyMoneyFile::instance(); if (_acc.name() != name) { MyMoneyAccount acc(_acc); acc.setName(name); file->modifyAccount(acc); } } /** * This method updates names of currencies from file to localized names */ void updateCurrencyNames() { auto file = MyMoneyFile::instance(); MyMoneyFileTransaction ft; QList storedCurrencies = MyMoneyFile::instance()->currencyList(); QList availableCurrencies = MyMoneyFile::instance()->availableCurrencyList(); QStringList currencyIDs; foreach (auto currency, availableCurrencies) currencyIDs.append(currency.id()); try { foreach (auto currency, storedCurrencies) { int i = currencyIDs.indexOf(currency.id()); if (i != -1 && availableCurrencies.at(i).name() != currency.name()) { currency.setName(availableCurrencies.at(i).name()); file->modifyCurrency(currency); } } ft.commit(); } catch (const MyMoneyException &e) { qDebug("Error %s updating currency names", qPrintable(e.what())); } } void updateAccountNames() { // make sure we setup the name of the base accounts in translated form try { MyMoneyFileTransaction ft; const auto file = MyMoneyFile::instance(); checkAccountName(file->asset(), i18n("Asset")); checkAccountName(file->liability(), i18n("Liability")); checkAccountName(file->income(), i18n("Income")); checkAccountName(file->expense(), i18n("Expense")); checkAccountName(file->equity(), i18n("Equity")); ft.commit(); } catch (const MyMoneyException &) { } } void ungetString(QIODevice *qfile, char *buf, int len) { buf = &buf[len-1]; while (len--) { qfile->ungetChar(*buf--); } } bool applyFileFixes() { const auto blocked = MyMoneyFile::instance()->blockSignals(true); KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup grp = config->group("General Options"); // For debugging purposes, we can turn off the automatic fix manually // by setting the entry in kmymoneyrc to true grp = config->group("General Options"); if (grp.readEntry("SkipFix", false) != true) { MyMoneyFileTransaction ft; try { // Check if we have to modify the file before we allow to work with it auto s = MyMoneyFile::instance()->storage(); while (s->fileFixVersion() < s->currentFixVersion()) { qDebug("%s", qPrintable((QString("testing fileFixVersion %1 < %2").arg(s->fileFixVersion()).arg(s->currentFixVersion())))); switch (s->fileFixVersion()) { case 0: fixFile_0(); s->setFileFixVersion(1); break; case 1: fixFile_1(); s->setFileFixVersion(2); break; case 2: fixFile_2(); s->setFileFixVersion(3); break; case 3: fixFile_3(); s->setFileFixVersion(4); break; // add new levels above. Don't forget to increase currentFixVersion() for all // the storage backends this fix applies to default: throw MYMONEYEXCEPTION(i18n("Unknown fix level in input file")); } } ft.commit(); } catch (const MyMoneyException &) { MyMoneyFile::instance()->blockSignals(blocked); return false; } } else { qDebug("Skipping automatic transaction fix!"); } MyMoneyFile::instance()->blockSignals(blocked); return true; } void connectStorageToModels() { q->connect(MyMoneyFile::instance(), &MyMoneyFile::objectAdded, Models::instance()->accountsModel(), &AccountsModel::slotObjectAdded); q->connect(MyMoneyFile::instance(), &MyMoneyFile::objectModified, Models::instance()->accountsModel(), &AccountsModel::slotObjectModified); q->connect(MyMoneyFile::instance(), &MyMoneyFile::objectRemoved, Models::instance()->accountsModel(), &AccountsModel::slotObjectRemoved); q->connect(MyMoneyFile::instance(), &MyMoneyFile::balanceChanged, Models::instance()->accountsModel(), &AccountsModel::slotBalanceOrValueChanged); q->connect(MyMoneyFile::instance(), &MyMoneyFile::valueChanged, Models::instance()->accountsModel(), &AccountsModel::slotBalanceOrValueChanged); q->connect(MyMoneyFile::instance(), &MyMoneyFile::objectAdded, Models::instance()->institutionsModel(), &InstitutionsModel::slotObjectAdded); q->connect(MyMoneyFile::instance(), &MyMoneyFile::objectModified, Models::instance()->institutionsModel(), &InstitutionsModel::slotObjectModified); q->connect(MyMoneyFile::instance(), &MyMoneyFile::objectRemoved, Models::instance()->institutionsModel(), &InstitutionsModel::slotObjectRemoved); q->connect(MyMoneyFile::instance(), &MyMoneyFile::balanceChanged, Models::instance()->institutionsModel(), &AccountsModel::slotBalanceOrValueChanged); q->connect(MyMoneyFile::instance(), &MyMoneyFile::valueChanged, Models::instance()->institutionsModel(), &AccountsModel::slotBalanceOrValueChanged); q->connect(MyMoneyFile::instance(), &MyMoneyFile::objectAdded, Models::instance()->equitiesModel(), &EquitiesModel::slotObjectAdded); q->connect(MyMoneyFile::instance(), &MyMoneyFile::objectModified, Models::instance()->equitiesModel(), &EquitiesModel::slotObjectModified); q->connect(MyMoneyFile::instance(), &MyMoneyFile::objectRemoved, Models::instance()->equitiesModel(), &EquitiesModel::slotObjectRemoved); q->connect(MyMoneyFile::instance(), &MyMoneyFile::balanceChanged, Models::instance()->equitiesModel(), &EquitiesModel::slotBalanceOrValueChanged); q->connect(MyMoneyFile::instance(), &MyMoneyFile::valueChanged, Models::instance()->equitiesModel(), &EquitiesModel::slotBalanceOrValueChanged); q->connect(MyMoneyFile::instance(), &MyMoneyFile::objectAdded, Models::instance()->securitiesModel(), &SecuritiesModel::slotObjectAdded); q->connect(MyMoneyFile::instance(), &MyMoneyFile::objectModified, Models::instance()->securitiesModel(), &SecuritiesModel::slotObjectModified); q->connect(MyMoneyFile::instance(), &MyMoneyFile::objectRemoved, Models::instance()->securitiesModel(), &SecuritiesModel::slotObjectRemoved); } void disconnectStorageFromModels() { q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectAdded, Models::instance()->accountsModel(), &AccountsModel::slotObjectAdded); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectModified, Models::instance()->accountsModel(), &AccountsModel::slotObjectModified); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectRemoved, Models::instance()->accountsModel(), &AccountsModel::slotObjectRemoved); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::balanceChanged, Models::instance()->accountsModel(), &AccountsModel::slotBalanceOrValueChanged); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::valueChanged, Models::instance()->accountsModel(), &AccountsModel::slotBalanceOrValueChanged); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectAdded, Models::instance()->institutionsModel(), &InstitutionsModel::slotObjectAdded); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectModified, Models::instance()->institutionsModel(), &InstitutionsModel::slotObjectModified); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectRemoved, Models::instance()->institutionsModel(), &InstitutionsModel::slotObjectRemoved); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::balanceChanged, Models::instance()->institutionsModel(), &AccountsModel::slotBalanceOrValueChanged); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::valueChanged, Models::instance()->institutionsModel(), &AccountsModel::slotBalanceOrValueChanged); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectAdded, Models::instance()->equitiesModel(), &EquitiesModel::slotObjectAdded); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectModified, Models::instance()->equitiesModel(), &EquitiesModel::slotObjectModified); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectRemoved, Models::instance()->equitiesModel(), &EquitiesModel::slotObjectRemoved); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::balanceChanged, Models::instance()->equitiesModel(), &EquitiesModel::slotBalanceOrValueChanged); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::valueChanged, Models::instance()->equitiesModel(), &EquitiesModel::slotBalanceOrValueChanged); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectAdded, Models::instance()->securitiesModel(), &SecuritiesModel::slotObjectAdded); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectModified, Models::instance()->securitiesModel(), &SecuritiesModel::slotObjectModified); q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::objectRemoved, Models::instance()->securitiesModel(), &SecuritiesModel::slotObjectRemoved); } /** * This method is used after a file or database has been * read into storage, and performs various initialization tasks * * @retval true all went okay * @retval false an exception occurred during this process */ bool initializeStorage() { const auto blocked = MyMoneyFile::instance()->blockSignals(true); updateAccountNames(); updateCurrencyNames(); selectBaseCurrency(); // setup the standard precision AmountEdit::setStandardPrecision(MyMoneyMoney::denomToPrec(MyMoneyFile::instance()->baseCurrency().smallestAccountFraction())); KMyMoneyEdit::setStandardPrecision(MyMoneyMoney::denomToPrec(MyMoneyFile::instance()->baseCurrency().smallestAccountFraction())); if (!applyFileFixes()) return false; MyMoneyFile::instance()->blockSignals(blocked); emit q->kmmFilePlugin(KMyMoneyApp::postOpen); Models::instance()->fileOpened(); connectStorageToModels(); // inform everyone about new data MyMoneyFile::instance()->forceDataChanged(); q->slotCheckSchedules(); m_myMoneyView->slotFileOpened(); onlineJobAdministration::instance()->updateActions(); return true; } /** * This method attaches an empty storage object to the MyMoneyFile * object. It calls removeStorage() to remove a possibly attached * storage object. */ void newStorage() { removeStorage(); auto file = MyMoneyFile::instance(); file->attachStorage(new MyMoneyStorageMgr); } /** * This method removes an attached storage from the MyMoneyFile * object. */ void removeStorage() { auto file = MyMoneyFile::instance(); auto p = file->storage(); if (p) { file->detachStorage(p); delete p; } } /** * if no base currency is defined, start the dialog and force it to be set */ void selectBaseCurrency() { auto file = MyMoneyFile::instance(); // check if we have a base currency. If not, we need to select one QString baseId; try { baseId = MyMoneyFile::instance()->baseCurrency().id(); } catch (const MyMoneyException &e) { qDebug("%s", qPrintable(e.what())); } if (baseId.isEmpty()) { QPointer dlg = new KCurrencyEditDlg(q); // connect(dlg, SIGNAL(selectBaseCurrency(MyMoneySecurity)), this, SLOT(slotSetBaseCurrency(MyMoneySecurity))); dlg->exec(); delete dlg; } try { baseId = MyMoneyFile::instance()->baseCurrency().id(); } catch (const MyMoneyException &e) { qDebug("%s", qPrintable(e.what())); } if (!baseId.isEmpty()) { // check that all accounts have a currency QList list; file->accountList(list); QList::Iterator it; // don't forget those standard accounts list << file->asset(); list << file->liability(); list << file->income(); list << file->expense(); list << file->equity(); for (it = list.begin(); it != list.end(); ++it) { QString cid; try { if (!(*it).currencyId().isEmpty() || (*it).currencyId().length() != 0) cid = MyMoneyFile::instance()->currency((*it).currencyId()).id(); } catch (const MyMoneyException& e) { qDebug() << QLatin1String("Account") << (*it).id() << (*it).name() << e.what(); } if (cid.isEmpty()) { (*it).setCurrencyId(baseId); MyMoneyFileTransaction ft; try { file->modifyAccount(*it); ft.commit(); } catch (const MyMoneyException &e) { qDebug("Unable to setup base currency in account %s (%s): %s", qPrintable((*it).name()), qPrintable((*it).id()), qPrintable(e.what())); } } } } } /** * Calls MyMoneyFile::readAllData which reads a MyMoneyFile into appropriate * data structures in memory. The return result is examined to make sure no * errors occurred whilst parsing. * * @param url The URL to read from. * If no protocol is specified, file:// is assumed. * * @return Whether the read was successful. */ bool openNondatabase(const QUrl &url) { if (!url.isValid()) throw MYMONEYEXCEPTION(QString::fromLatin1("Invalid URL %1").arg(qPrintable(url.url()))); QString fileName; auto downloadedFile = false; if (url.isLocalFile()) { fileName = url.toLocalFile(); } else { fileName = KMyMoneyUtils::downloadFile(url); downloadedFile = true; } if (!KMyMoneyUtils::fileExists(QUrl::fromLocalFile(fileName))) throw MYMONEYEXCEPTION(QString::fromLatin1("Error opening the file.\n" "Requested file: '%1'.\n" "Downloaded file: '%2'").arg(qPrintable(url.url()), fileName)); QFile file(fileName); if (!file.open(QIODevice::ReadOnly)) throw MYMONEYEXCEPTION(QString::fromLatin1("Cannot read the file: %1").arg(fileName)); QByteArray qbaFileHeader(2, '\0'); const auto sFileToShort = QString::fromLatin1("File %1 is too short.").arg(fileName); if (file.read(qbaFileHeader.data(), 2) != 2) throw MYMONEYEXCEPTION(sFileToShort); file.close(); // There's a problem with the KFilterDev and KGPGFile classes: // One supports the at(n) member but not ungetch() together with // read() and the other does not provide an at(n) method but // supports read() that considers the ungetch() buffer. QFile // supports everything so this is not a problem. We solve the problem // for now by keeping track of which method can be used. auto haveAt = true; auto isEncrypted = false; emit q->kmmFilePlugin(preOpen); QIODevice* qfile = nullptr; QString sFileHeader(qbaFileHeader); if (sFileHeader == QString("\037\213")) { // gzipped? qfile = new KCompressionDevice(fileName, COMPRESSION_TYPE); } else if (sFileHeader == QString("--") || // PGP ASCII armored? sFileHeader == QString("\205\001") || // PGP binary? sFileHeader == QString("\205\002")) { // PGP binary? if (KGPGFile::GPGAvailable()) { qfile = new KGPGFile(fileName); haveAt = false; isEncrypted = true; } else { throw MYMONEYEXCEPTION(QString::fromLatin1("%1").arg(i18n("GPG is not available for decryption of file %1", fileName))); } } else { // we can't use file directly, as we delete qfile later on qfile = new QFile(file.fileName()); } if (!qfile->open(QIODevice::ReadOnly)) { delete qfile; throw MYMONEYEXCEPTION(QString::fromLatin1("Cannot read the file: %1").arg(fileName)); } qbaFileHeader.resize(8); if (qfile->read(qbaFileHeader.data(), 8) != 8) throw MYMONEYEXCEPTION(sFileToShort); if (haveAt) qfile->seek(0); else ungetString(qfile, qbaFileHeader.data(), 8); // Ok, we got the first block of 8 bytes. Read in the two // unsigned long int's by preserving endianess. This is // achieved by reading them through a QDataStream object qint32 magic0, magic1; QDataStream s(&qbaFileHeader, QIODevice::ReadOnly); s >> magic0; s >> magic1; // If both magic numbers match (we actually read in the // text 'KMyMoney' then we assume a binary file and // construct a reader for it. Otherwise, we construct // an XML reader object. // // The expression magic0 < 30 is only used to create // a binary reader if we assume an old binary file. This // should be removed at some point. An alternative is to // check the beginning of the file against an pattern // of the XML file (e.g. '?%1").arg(i18n("File %1 contains the old binary format used by KMyMoney. Please use an older version of KMyMoney (0.8.x) that still supports this format to convert it to the new XML based format.", fileName))); } // Scan the first 70 bytes to see if we find something // we know. For now, we support our own XML format and // GNUCash XML format. If the file is smaller, then it // contains no valid data and we reject it anyway. qbaFileHeader.resize(70); if (qfile->read(qbaFileHeader.data(), 70) != 70) throw MYMONEYEXCEPTION(sFileToShort); if (haveAt) qfile->seek(0); else ungetString(qfile, qbaFileHeader.data(), 70); IMyMoneyOperationsFormat* pReader = nullptr; QRegExp kmyexp(""); QRegExp gncexp("formatName().compare(QLatin1String("GNC")) == 0) { pReader = plugin->reader(); break; } } if (!pReader) { KMessageBox::error(q, i18n("Couldn't find suitable plugin to read your storage.")); return false; } m_fileType = KMyMoneyApp::GncXML; } else { throw MYMONEYEXCEPTION(QString::fromLatin1("%1").arg(i18n("File %1 contains an unknown file format.", fileName))); } // disconnect the current storga manager from the engine MyMoneyFile::instance()->detachStorage(); // create a new empty storage object auto storage = new MyMoneyStorageMgr; // attach the storage before reading the file, since the online // onlineJobAdministration object queries the engine during // loading. MyMoneyFile::instance()->attachStorage(storage); pReader->setProgressCallback(&KMyMoneyApp::progressCallback); pReader->readFile(qfile, storage); pReader->setProgressCallback(0); delete pReader; qfile->close(); delete qfile; // if a temporary file was downloaded, then it will be removed // with the next call. Otherwise, it stays untouched on the local // filesystem. if (downloadedFile) QFile::remove(fileName); // encapsulate transactions to the engine to be able to commit/rollback MyMoneyFileTransaction ft; // make sure we setup the encryption key correctly if (isEncrypted && MyMoneyFile::instance()->value("kmm-encryption-key").isEmpty()) MyMoneyFile::instance()->setValue("kmm-encryption-key", KMyMoneySettings::gpgRecipientList().join(",")); ft.commit(); return true; } /** * This method is called from readFile to open a database file which * is to be processed in 'proper' database mode, i.e. in-place updates * * @param dbaseURL pseudo-QUrl representation of database * * @retval true Database opened successfully * @retval false Could not open or read database */ bool openDatabase(const QUrl &url) { // open the database auto pStorage = MyMoneyFile::instance()->storage(); if (!pStorage) pStorage = new MyMoneyStorageMgr; auto rc = false; auto pluginFound = false; for (const auto& plugin : m_plugins.storage) { if (plugin->formatName().compare(QLatin1String("SQL")) == 0) { rc = plugin->open(pStorage, url); pluginFound = true; break; } } if(!pluginFound) KMessageBox::error(q, i18n("Couldn't find suitable plugin to read your storage.")); if(!rc) { removeStorage(); delete pStorage; return false; } if (pStorage) { MyMoneyFile::instance()->detachStorage(); MyMoneyFile::instance()->attachStorage(pStorage); } return true; } /** * Close the currently opened file and create an empty new file. * * @see MyMoneyFile */ void newFile() { closeFile(); m_fileType = KMyMoneyApp::KmmXML; // assume native type until saved m_fileOpen = true; } /** * Saves the data into permanent storage using the XML format. * * @param url The URL to save into. * If no protocol is specified, file:// is assumed. * @param keyList QString containing a comma separated list of keys * to be used for encryption. If @p keyList is empty, * the file will be saved unencrypted (the default) * * @retval false save operation failed * @retval true save operation was successful */ bool saveFile(const QUrl &url, const QString& keyList = QString()) { QString filename = url.path(); if (!m_fileOpen) { KMessageBox::error(q, i18n("Tried to access a file when it has not been opened")); return false; } emit q->kmmFilePlugin(KMyMoneyApp::preSave); std::unique_ptr storageWriter; // If this file ends in ".ANON.XML" then this should be written using the // anonymous writer. bool plaintext = filename.right(4).toLower() == ".xml"; if (filename.right(9).toLower() == ".anon.xml") storageWriter = std::make_unique(); else storageWriter = std::make_unique(); // actually, url should be the parameter to this function // but for now, this would involve too many changes bool rc = true; try { if (! url.isValid()) { throw MYMONEYEXCEPTION(i18n("Malformed URL '%1'", url.url())); } if (url.isLocalFile()) { filename = url.toLocalFile(); try { const unsigned int nbak = KMyMoneySettings::autoBackupCopies(); if (nbak) { KBackup::numberedBackupFile(filename, QString(), QStringLiteral("~"), nbak); } saveToLocalFile(filename, storageWriter.get(), plaintext, keyList); } catch (const MyMoneyException &) { throw MYMONEYEXCEPTION(i18n("Unable to write changes to '%1'", filename)); } } else { QTemporaryFile tmpfile; tmpfile.open(); // to obtain the name tmpfile.close(); saveToLocalFile(tmpfile.fileName(), storageWriter.get(), plaintext, keyList); Q_CONSTEXPR int permission = -1; QFile file(tmpfile.fileName()); file.open(QIODevice::ReadOnly); KIO::StoredTransferJob *putjob = KIO::storedPut(file.readAll(), url, permission, KIO::JobFlag::Overwrite); if (!putjob->exec()) { throw MYMONEYEXCEPTION(i18n("Unable to upload to '%1'.
%2", url.toDisplayString(), putjob->errorString())); } file.close(); } m_fileType = KMyMoneyApp::KmmXML; } catch (const MyMoneyException &e) { KMessageBox::error(q, e.what()); MyMoneyFile::instance()->setDirty(); rc = false; } emit q->kmmFilePlugin(postSave); return rc; } /** * This method is used by saveFile() to store the data * either directly in the destination file if it is on * the local file system or in a temporary file when * the final destination is reached over a network * protocol (e.g. FTP) * * @param localFile the name of the local file * @param writer pointer to the formatter * @param plaintext whether to override any compression & encryption settings * @param keyList QString containing a comma separated list of keys to be used for encryption * If @p keyList is empty, the file will be saved unencrypted * * @note This method will close the file when it is written. */ void saveToLocalFile(const QString& localFile, IMyMoneyOperationsFormat* pWriter, bool plaintext, const QString& keyList) { // Check GPG encryption bool encryptFile = true; bool encryptRecover = false; if (!keyList.isEmpty()) { if (!KGPGFile::GPGAvailable()) { KMessageBox::sorry(q, i18n("GPG does not seem to be installed on your system. Please make sure that GPG can be found using the standard search path. This time, encryption is disabled."), i18n("GPG not found")); encryptFile = false; } else { if (KMyMoneySettings::encryptRecover()) { encryptRecover = true; if (!KGPGFile::keyAvailable(QString(recoveryKeyId))) { KMessageBox::sorry(q, i18n("

You have selected to encrypt your data also with the KMyMoney recover key, but the key with id

%1

has not been found in your keyring at this time. Please make sure to import this key into your keyring. You can find it on the KMyMoney web-site. This time your data will not be encrypted with the KMyMoney recover key.

", QString(recoveryKeyId)), i18n("GPG Key not found")); encryptRecover = false; } } for(const QString& key: keyList.split(',', QString::SkipEmptyParts)) { if (!KGPGFile::keyAvailable(key)) { KMessageBox::sorry(q, i18n("

You have specified to encrypt your data for the user-id

%1.

Unfortunately, a valid key for this user-id was not found in your keyring. Please make sure to import a valid key for this user-id. This time, encryption is disabled.

", key), i18n("GPG Key not found")); encryptFile = false; break; } } if (encryptFile == true) { QString msg = i18n("

You have configured to save your data in encrypted form using GPG. Make sure you understand that you might lose all your data if you encrypt it, but cannot decrypt it later on. If unsure, answer No.

"); if (KMessageBox::questionYesNo(q, msg, i18n("Store GPG encrypted"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "StoreEncrypted") == KMessageBox::No) { encryptFile = false; } } } } // Create a temporary file if needed QString writeFile = localFile; QTemporaryFile tmpFile; if (QFile::exists(localFile)) { tmpFile.open(); writeFile = tmpFile.fileName(); tmpFile.close(); } /** * @brief Automatically restore settings when scope is left */ struct restorePreviousSettingsHelper { restorePreviousSettingsHelper() : m_signalsWereBlocked{MyMoneyFile::instance()->signalsBlocked()} { MyMoneyFile::instance()->blockSignals(true); } ~restorePreviousSettingsHelper() { MyMoneyFile::instance()->blockSignals(m_signalsWereBlocked); } const bool m_signalsWereBlocked; } restoreHelper; MyMoneyFileTransaction ft; MyMoneyFile::instance()->deletePair("kmm-encryption-key"); std::unique_ptr device; if (!keyList.isEmpty() && encryptFile && !plaintext) { std::unique_ptr kgpg = std::unique_ptr(new KGPGFile{writeFile}); if (kgpg) { for(const QString& key: keyList.split(',', QString::SkipEmptyParts)) { kgpg->addRecipient(key.toLatin1()); } if (encryptRecover) { kgpg->addRecipient(recoveryKeyId); } MyMoneyFile::instance()->setValue("kmm-encryption-key", keyList); device = std::unique_ptr(kgpg.release()); } } else { QFile *file = new QFile(writeFile); // The second parameter of KCompressionDevice means that KCompressionDevice will delete the QFile object device = std::unique_ptr(new KCompressionDevice{file, true, (plaintext) ? KCompressionDevice::None : COMPRESSION_TYPE}); } ft.commit(); if (!device || !device->open(QIODevice::WriteOnly)) { throw MYMONEYEXCEPTION(i18n("Unable to open file '%1' for writing.", localFile)); } pWriter->setProgressCallback(&KMyMoneyApp::progressCallback); pWriter->writeFile(device.get(), MyMoneyFile::instance()->storage()); device->close(); // Check for errors if possible, only possible for KGPGFile QFileDevice *fileDevice = qobject_cast(device.get()); if (fileDevice && fileDevice->error() != QFileDevice::NoError) { throw MYMONEYEXCEPTION(i18n("Failure while writing to '%1'", localFile)); } if (writeFile != localFile) { // This simple comparison is possible because the strings are equal if no temporary file was created. // If a temporary file was created, it is made in a way that the name is definitely different. So no // symlinks etc. have to be evaluated. if (!QFile::remove(localFile) || !QFile::rename(writeFile, localFile)) throw MYMONEYEXCEPTION(i18n("Failure while writing to '%1'", localFile)); } QFile::setPermissions(localFile, m_fmode); pWriter->setProgressCallback(0); } /** * Call this to see if the MyMoneyFile contains any unsaved data. * * @retval true if any data has been modified but not saved * @retval false otherwise */ bool dirty() { if (!m_fileOpen) return false; return MyMoneyFile::instance()->dirty(); } /* DO NOT ADD code to this function or any of it's called ones. Instead, create a new function, fixFile_n, and modify the initializeStorage() logic above to call it */ void fixFile_3() { // make sure each storage object contains a (unique) id MyMoneyFile::instance()->storageId(); } void fixFile_2() { auto file = MyMoneyFile::instance(); MyMoneyTransactionFilter filter; filter.setReportAllSplits(false); QList transactionList; file->transactionList(transactionList, filter); // scan the transactions and modify transactions with two splits // which reference an account and a category to have the memo text // of the account. auto count = 0; foreach (const auto transaction, transactionList) { if (transaction.splitCount() == 2) { QString accountId; QString categoryId; QString accountMemo; QString categoryMemo; foreach (const auto split, transaction.splits()) { auto acc = file->account(split.accountId()); if (acc.isIncomeExpense()) { categoryId = split.id(); categoryMemo = split.memo(); } else { accountId = split.id(); accountMemo = split.memo(); } } if (!accountId.isEmpty() && !categoryId.isEmpty() && accountMemo != categoryMemo) { MyMoneyTransaction t(transaction); MyMoneySplit s(t.splitById(categoryId)); s.setMemo(accountMemo); t.modifySplit(s); file->modifyTransaction(t); ++count; } } } qDebug("%d transactions fixed in fixFile_2", count); } void fixFile_1() { // we need to fix reports. If the account filter list contains // investment accounts, we need to add the stock accounts to the list // as well if we don't have the expert mode enabled if (!KMyMoneySettings::expertMode()) { try { QList reports = MyMoneyFile::instance()->reportList(); QList::iterator it_r; for (it_r = reports.begin(); it_r != reports.end(); ++it_r) { QStringList list; (*it_r).accounts(list); QStringList missing; QStringList::const_iterator it_a, it_b; for (it_a = list.constBegin(); it_a != list.constEnd(); ++it_a) { auto acc = MyMoneyFile::instance()->account(*it_a); if (acc.accountType() == eMyMoney::Account::Type::Investment) { foreach (const auto accountID, acc.accountList()) { if (!list.contains(accountID)) { missing.append(accountID); } } } } if (!missing.isEmpty()) { (*it_r).addAccount(missing); MyMoneyFile::instance()->modifyReport(*it_r); } } } catch (const MyMoneyException &) { } } } #if 0 if (!m_accountsView->allItemsSelected()) { // retrieve a list of selected accounts QStringList list; m_accountsView->selectedItems(list); // if we're not in expert mode, we need to make sure // that all stock accounts for the selected investment // account are also selected if (!KMyMoneySettings::expertMode()) { QStringList missing; QStringList::const_iterator it_a, it_b; for (it_a = list.begin(); it_a != list.end(); ++it_a) { auto acc = MyMoneyFile::instance()->account(*it_a); if (acc.accountType() == Account::Type::Investment) { foreach (const auto accountID, acc.accountList()) { if (!list.contains(accountID)) { missing.append(accountID); } } } } list += missing; } m_filter.addAccount(list); } #endif void fixFile_0() { /* (Ace) I am on a crusade against file fixups. Whenever we have to fix the * file, it is really a warning. So I'm going to print a debug warning, and * then go track them down when I see them to figure out how they got saved * out needing fixing anyway. */ auto file = MyMoneyFile::instance(); QList accountList; file->accountList(accountList); QList::Iterator it_a; QList scheduleList = file->scheduleList(); QList::Iterator it_s; MyMoneyAccount equity = file->equity(); MyMoneyAccount asset = file->asset(); bool equityListEmpty = equity.accountList().count() == 0; for (it_a = accountList.begin(); it_a != accountList.end(); ++it_a) { if ((*it_a).accountType() == eMyMoney::Account::Type::Loan || (*it_a).accountType() == eMyMoney::Account::Type::AssetLoan) { fixLoanAccount_0(*it_a); } // until early before 0.8 release, the equity account was not saved to // the file. If we have an equity account with no sub-accounts but // find and equity account that has equity() as it's parent, we reparent // this account. Need to move it to asset() first, because otherwise // MyMoneyFile::reparent would act as NOP. if (equityListEmpty && (*it_a).accountType() == eMyMoney::Account::Type::Equity) { if ((*it_a).parentAccountId() == equity.id()) { auto acc = *it_a; // tricky, force parent account to be empty so that we really // can re-parent it acc.setParentAccountId(QString()); file->reparentAccount(acc, equity); qDebug() << Q_FUNC_INFO << " fixed account " << acc.id() << " reparented to " << equity.id(); } } } for (it_s = scheduleList.begin(); it_s != scheduleList.end(); ++it_s) { fixSchedule_0(*it_s); } fixTransactions_0(); } void fixSchedule_0(MyMoneySchedule sched) { MyMoneyTransaction t = sched.transaction(); QList splitList = t.splits(); QList::ConstIterator it_s; bool updated = false; try { // Check if the splits contain valid data and set it to // be valid. for (it_s = splitList.constBegin(); it_s != splitList.constEnd(); ++it_s) { // the first split is always the account on which this transaction operates // and if the transaction commodity is not set, we take this if (it_s == splitList.constBegin() && t.commodity().isEmpty()) { qDebug() << Q_FUNC_INFO << " " << t.id() << " has no commodity"; try { auto acc = MyMoneyFile::instance()->account((*it_s).accountId()); t.setCommodity(acc.currencyId()); updated = true; } catch (const MyMoneyException &) { } } // make sure the account exists. If not, remove the split try { MyMoneyFile::instance()->account((*it_s).accountId()); } catch (const MyMoneyException &) { qDebug() << Q_FUNC_INFO << " " << sched.id() << " " << (*it_s).id() << " removed, because account '" << (*it_s).accountId() << "' does not exist."; t.removeSplit(*it_s); updated = true; } if ((*it_s).reconcileFlag() != eMyMoney::Split::State::NotReconciled) { qDebug() << Q_FUNC_INFO << " " << sched.id() << " " << (*it_s).id() << " should be 'not reconciled'"; MyMoneySplit split = *it_s; split.setReconcileDate(QDate()); split.setReconcileFlag(eMyMoney::Split::State::NotReconciled); t.modifySplit(split); updated = true; } // the schedule logic used to operate only on the value field. // This is now obsolete. if ((*it_s).shares().isZero() && !(*it_s).value().isZero()) { MyMoneySplit split = *it_s; split.setShares(split.value()); t.modifySplit(split); updated = true; } } // If there have been changes, update the schedule and // the engine data. if (updated) { sched.setTransaction(t); MyMoneyFile::instance()->modifySchedule(sched); } } catch (const MyMoneyException &e) { qWarning("Unable to update broken schedule: %s", qPrintable(e.what())); } } void fixLoanAccount_0(MyMoneyAccount acc) { if (acc.value("final-payment").isEmpty() || acc.value("term").isEmpty() || acc.value("periodic-payment").isEmpty() || acc.value("loan-amount").isEmpty() || acc.value("interest-calculation").isEmpty() || acc.value("schedule").isEmpty() || acc.value("fixed-interest").isEmpty()) { KMessageBox::information(q, i18n("

The account \"%1\" was previously created as loan account but some information is missing.

The new loan wizard will be started to collect all relevant information.

Please use KMyMoney version 0.8.7 or later and earlier than version 0.9 to correct the problem.

" , acc.name()), i18n("Account problem")); throw MYMONEYEXCEPTION("Fix LoanAccount0 not supported anymore"); } } void fixTransactions_0() { auto file = MyMoneyFile::instance(); QList scheduleList = file->scheduleList(); MyMoneyTransactionFilter filter; filter.setReportAllSplits(false); QList transactionList; file->transactionList(transactionList, filter); QList::Iterator it_x; QStringList interestAccounts; KMSTATUS(i18n("Fix transactions")); q->slotStatusProgressBar(0, scheduleList.count() + transactionList.count()); int cnt = 0; // scan the schedules to find interest accounts for (it_x = scheduleList.begin(); it_x != scheduleList.end(); ++it_x) { MyMoneyTransaction t = (*it_x).transaction(); QList::ConstIterator it_s; QStringList accounts; bool hasDuplicateAccounts = false; foreach (const auto split, t.splits()) { if (accounts.contains(split.accountId())) { hasDuplicateAccounts = true; qDebug() << Q_FUNC_INFO << " " << t.id() << " has multiple splits with account " << split.accountId(); } else { accounts << split.accountId(); } if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)) { if (interestAccounts.contains(split.accountId()) == 0) { interestAccounts << split.accountId(); } } } if (hasDuplicateAccounts) { fixDuplicateAccounts_0(t); } ++cnt; if (!(cnt % 10)) q->slotStatusProgressBar(cnt); } // scan the transactions and modify loan transactions for (auto& transaction : transactionList) { QString defaultAction; QList splits = transaction.splits(); QStringList accounts; // check if base commodity is set. if not, set baseCurrency if (transaction.commodity().isEmpty()) { qDebug() << Q_FUNC_INFO << " " << transaction.id() << " has no base currency"; transaction.setCommodity(file->baseCurrency().id()); file->modifyTransaction(transaction); } bool isLoan = false; // Determine default action if (transaction.splitCount() == 2) { // check for transfer int accountCount = 0; MyMoneyMoney val; foreach (const auto split, splits) { auto acc = file->account(split.accountId()); if (acc.accountGroup() == eMyMoney::Account::Type::Asset || acc.accountGroup() == eMyMoney::Account::Type::Liability) { val = split.value(); accountCount++; if (acc.accountType() == eMyMoney::Account::Type::Loan || acc.accountType() == eMyMoney::Account::Type::AssetLoan) isLoan = true; } else break; } if (accountCount == 2) { if (isLoan) defaultAction = MyMoneySplit::actionName(eMyMoney::Split::Action::Amortization); else defaultAction = MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer); } else { if (val.isNegative()) defaultAction = MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal); else defaultAction = MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit); } } isLoan = false; foreach (const auto split, splits) { auto acc = file->account(split.accountId()); MyMoneyMoney val = split.value(); if (acc.accountGroup() == eMyMoney::Account::Type::Asset || acc.accountGroup() == eMyMoney::Account::Type::Liability) { if (!val.isPositive()) { defaultAction = MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal); break; } else { defaultAction = MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit); break; } } } #if 0 // Check for correct actions in transactions referencing credit cards bool needModify = false; // The action fields are actually not used anymore in the ledger view logic // so we might as well skip this whole thing here! for (it_s = splits.begin(); needModify == false && it_s != splits.end(); ++it_s) { auto acc = file->account((*it_s).accountId()); MyMoneyMoney val = (*it_s).value(); if (acc.accountType() == Account::Type::CreditCard) { if (val < 0 && (*it_s).action() != MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal) && (*it_s).action() != MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer)) needModify = true; if (val >= 0 && (*it_s).action() != MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit) && (*it_s).action() != MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer)) needModify = true; } } // (Ace) Extended the #endif down to cover this conditional, because as-written // it will ALWAYS be skipped. if (needModify == true) { for (it_s = splits.begin(); it_s != splits.end(); ++it_s) { (*it_s).setAction(defaultAction); transaction.modifySplit(*it_s); file->modifyTransaction(transaction); } splits = transaction.splits(); // update local copy qDebug("Fixed credit card assignment in %s", transaction.id().data()); } #endif // Check for correct assignment of ActionInterest in all splits // and check if there are any duplicates in this transactions for (auto& split : splits) { MyMoneyAccount splitAccount = file->account(split.accountId()); if (!accounts.contains(split.accountId())) { accounts << split.accountId(); } // if this split references an interest account, the action // must be of type ActionInterest if (interestAccounts.contains(split.accountId())) { if (split.action() != MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)) { qDebug() << Q_FUNC_INFO << " " << transaction.id() << " contains an interest account (" << split.accountId() << ") but does not have ActionInterest"; split.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)); transaction.modifySplit(split); file->modifyTransaction(transaction); qDebug("Fixed interest action in %s", qPrintable(transaction.id())); } // if it does not reference an interest account, it must not be // of type ActionInterest } else { if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)) { qDebug() << Q_FUNC_INFO << " " << transaction.id() << " does not contain an interest account so it should not have ActionInterest"; split.setAction(defaultAction); transaction.modifySplit(split); file->modifyTransaction(transaction); qDebug("Fixed interest action in %s", qPrintable(transaction.id())); } } // check that for splits referencing an account that has // the same currency as the transactions commodity the value // and shares field are the same. if (transaction.commodity() == splitAccount.currencyId() && split.value() != split.shares()) { qDebug() << Q_FUNC_INFO << " " << transaction.id() << " " << split.id() << " uses the transaction currency, but shares != value"; split.setShares(split.value()); transaction.modifySplit(split); file->modifyTransaction(transaction); } // fix the shares and values to have the correct fraction if (!splitAccount.isInvest()) { try { int fract = splitAccount.fraction(); if (split.shares() != split.shares().convert(fract)) { qDebug("adjusting fraction in %s,%s", qPrintable(transaction.id()), qPrintable(split.id())); split.setShares(split.shares().convert(fract)); split.setValue(split.value().convert(fract)); transaction.modifySplit(split); file->modifyTransaction(transaction); } } catch (const MyMoneyException &) { qDebug("Missing security '%s', split not altered", qPrintable(splitAccount.currencyId())); } } } ++cnt; if (!(cnt % 10)) q->slotStatusProgressBar(cnt); } q->slotStatusProgressBar(-1, -1); } void fixDuplicateAccounts_0(MyMoneyTransaction& t) { qDebug("Duplicate account in transaction %s", qPrintable(t.id())); } }; KMyMoneyApp::KMyMoneyApp(QWidget* parent) : KXmlGuiWindow(parent), d(new Private(this)) { #ifdef KMM_DBUS new KmymoneyAdaptor(this); QDBusConnection::sessionBus().registerObject("/KMymoney", this); QDBusConnection::sessionBus().interface()->registerService( "org.kde.kmymoney-" + QString::number(platformTools::processId()), QDBusConnectionInterface::DontQueueService); #endif // Register the main engine types used as meta-objects qRegisterMetaType("MyMoneyMoney"); qRegisterMetaType("MyMoneySecurity"); // preset the pointer because we need it during the course of this constructor kmymoney = this; d->m_config = KSharedConfig::openConfig(); d->setThemedCSS(); MyMoneyTransactionFilter::setFiscalYearStart(KMyMoneySettings::firstFiscalMonth(), KMyMoneySettings::firstFiscalDay()); updateCaption(true); QFrame* frame = new QFrame; frame->setFrameStyle(QFrame::NoFrame); // values for margin (11) and spacing(6) taken from KDialog implementation QBoxLayout* layout = new QBoxLayout(QBoxLayout::TopToBottom, frame); layout->setContentsMargins(2, 2, 2, 2); layout->setSpacing(6); { #ifdef Q_OS_WIN QString themeName = QLatin1Literal("system"); // using QIcon::setThemeName on Craft build system causes icons to disappear #else QString themeName = KMyMoneySettings::iconsTheme(); // get theme user wants #endif if (!themeName.isEmpty() && themeName != QLatin1Literal("system")) // if it isn't default theme then set it QIcon::setThemeName(themeName); Icons::setIconThemeNames(QIcon::themeName()); // get whatever theme user ends up with and hope our icon names will fit that theme } initStatusBar(); pActions = initActions(); pMenus = initMenus(); d->newStorage(); d->m_myMoneyView = new KMyMoneyView(this/*the global variable kmymoney is not yet assigned. So we pass it here*/); layout->addWidget(d->m_myMoneyView, 10); connect(d->m_myMoneyView, &KMyMoneyView::aboutToChangeView, this, &KMyMoneyApp::slotResetSelections); connect(d->m_myMoneyView, SIGNAL(currentPageChanged(KPageWidgetItem*,KPageWidgetItem*)), this, SLOT(slotUpdateActions())); connect(d->m_myMoneyView, &KMyMoneyView::statusMsg, this, &KMyMoneyApp::slotStatusMsg); connect(d->m_myMoneyView, &KMyMoneyView::statusProgress, this, &KMyMoneyApp::slotStatusProgressBar); /////////////////////////////////////////////////////////////////// // call inits to invoke all other construction parts readOptions(); // now initialize the plugin structure createInterfaces(); KMyMoneyPlugin::pluginHandling(KMyMoneyPlugin::Action::Load, d->m_plugins, this, guiFactory()); onlineJobAdministration::instance()->setOnlinePlugins(d->m_plugins.extended); d->m_myMoneyView->setOnlinePlugins(d->m_plugins.online); d->m_myMoneyView->setStoragePlugins(d->m_plugins.storage); setCentralWidget(frame); connect(&d->m_proc, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(slotBackupHandleEvents())); // force to show the home page if the file is closed connect(pActions[Action::ViewTransactionDetail], &QAction::toggled, d->m_myMoneyView, &KMyMoneyView::slotShowTransactionDetail); d->m_backupState = BACKUP_IDLE; QLocale locale; int weekStart = locale.firstDayOfWeek(); int weekEnd = weekStart-1; if (weekEnd < Qt::Monday) { weekEnd = Qt::Sunday; } bool startFirst = (weekStart < weekEnd); for (int i = 0; i < 8; ++i) { if (startFirst) d->m_processingDays.setBit(i, (i >= weekStart && i <= weekEnd)); else d->m_processingDays.setBit(i, (i >= weekStart || i <= weekEnd)); } d->m_autoSaveTimer = new QTimer(this); d->m_progressTimer = new QTimer(this); connect(d->m_autoSaveTimer, SIGNAL(timeout()), this, SLOT(slotAutoSave())); connect(d->m_progressTimer, SIGNAL(timeout()), this, SLOT(slotStatusProgressDone())); // make sure, we get a note when the engine changes state connect(MyMoneyFile::instance(), SIGNAL(dataChanged()), this, SLOT(slotDataChanged())); // connect the WebConnect server connect(d->m_webConnect, SIGNAL(gotUrl(QUrl)), this, SLOT(webConnect(QUrl))); // make sure we have a balance warning object d->m_balanceWarning = new KBalanceWarning(this); // setup the initial configuration slotUpdateConfiguration(QString()); // kickstart date change timer slotDateChanged(); connect(this, SIGNAL(fileLoaded(QUrl)), onlineJobAdministration::instance(), SLOT(updateOnlineTaskProperties())); } KMyMoneyApp::~KMyMoneyApp() { d->removeStorage(); // delete cached objects since they are in the way // when unloading the plugins onlineJobAdministration::instance()->clearCaches(); // we need to unload all plugins before we destroy anything else KMyMoneyPlugin::pluginHandling(KMyMoneyPlugin::Action::Unload, d->m_plugins, this, guiFactory()); delete d->m_searchDlg; delete d->m_transactionEditor; delete d->m_endingBalanceDlg; delete d->m_moveToAccountSelector; #ifdef KF5Holidays_FOUND delete d->m_holidayRegion; #endif delete d; } QUrl KMyMoneyApp::lastOpenedURL() { QUrl url = d->m_startDialog ? QUrl() : d->m_fileName; if (!url.isValid()) { url = QUrl::fromUserInput(readLastUsedFile()); } ready(); return url; } void KMyMoneyApp::slotObjectDestroyed(QObject* o) { if (o == d->m_moveToAccountSelector) { d->m_moveToAccountSelector = 0; } } void KMyMoneyApp::slotInstallConsistencyCheckContextMenu() { // this code relies on the implementation of KMessageBox::informationList to add a context menu to that list, // please adjust it if it's necessary or rewrite the way the consistency check results are displayed if (QWidget* dialog = QApplication::activeModalWidget()) { if (QListWidget* widget = dialog->findChild()) { // give the user a hint that the data can be saved widget->setToolTip(i18n("This is the consistency check log, use the context menu to copy or save it.")); widget->setWhatsThis(widget->toolTip()); widget->setContextMenuPolicy(Qt::CustomContextMenu); connect(widget, SIGNAL(customContextMenuRequested(QPoint)), SLOT(slotShowContextMenuForConsistencyCheck(QPoint))); } } } void KMyMoneyApp::slotShowContextMenuForConsistencyCheck(const QPoint &pos) { // allow the user to save the consistency check results if (QWidget* widget = qobject_cast< QWidget* >(sender())) { QMenu contextMenu(widget); QAction* copy = new QAction(i18n("Copy to clipboard"), widget); QAction* save = new QAction(i18n("Save to file"), widget); contextMenu.addAction(copy); contextMenu.addAction(save); QAction *result = contextMenu.exec(widget->mapToGlobal(pos)); if (result == copy) { // copy the consistency check results to the clipboard d->copyConsistencyCheckResults(); } else if (result == save) { // save the consistency check results to a file d->saveConsistencyCheckResults(); } } } QHash KMyMoneyApp::initMenus() { QHash lutMenus; const QHash menuNames { {Menu::Institution, QStringLiteral("institution_context_menu")}, {Menu::Account, QStringLiteral("account_context_menu")}, {Menu::Schedule, QStringLiteral("schedule_context_menu")}, {Menu::Category, QStringLiteral("category_context_menu")}, {Menu::Tag, QStringLiteral("tag_context_menu")}, {Menu::Payee, QStringLiteral("payee_context_menu")}, {Menu::Investment, QStringLiteral("investment_context_menu")}, {Menu::Transaction, QStringLiteral("transaction_context_menu")}, {Menu::MoveTransaction, QStringLiteral("transaction_move_menu")}, {Menu::MarkTransaction, QStringLiteral("transaction_mark_menu")}, {Menu::MarkTransactionContext, QStringLiteral("transaction_context_mark_menu")}, {Menu::OnlineJob, QStringLiteral("onlinejob_context_menu")} }; for (auto it = menuNames.cbegin(); it != menuNames.cend(); ++it) lutMenus.insert(it.key(), qobject_cast(factory()->container(it.value(), this))); return lutMenus; } QHash KMyMoneyApp::initActions() { auto aC = actionCollection(); // ************* // Adding standard actions // ************* KStandardAction::openNew(this, &KMyMoneyApp::slotFileNew, aC); KStandardAction::open(this, &KMyMoneyApp::slotFileOpen, aC); d->m_recentFiles = KStandardAction::openRecent(this, &KMyMoneyApp::slotFileOpenRecent, aC); KStandardAction::save(this, &KMyMoneyApp::slotFileSave, aC); KStandardAction::saveAs(this, &KMyMoneyApp::slotFileSaveAs, aC); KStandardAction::close(this, &KMyMoneyApp::slotFileClose, aC); KStandardAction::quit(this, &KMyMoneyApp::slotFileQuit, aC); KStandardAction::print(this, &KMyMoneyApp::slotPrintView, aC); KStandardAction::preferences(this, &KMyMoneyApp::slotSettings, aC); /* Look-up table for all custom actions. It's required for: 1) building QList with QActions to be added to ActionCollection 2) adding custom features to QActions like e.g. keyboard shortcut */ QHash lutActions; // ************* // Adding all actions // ************* { // struct for creating useless (unconnected) QAction struct actionInfo { Action action; QString name; QString text; Icon icon; }; const QVector actionInfos { // ************* // The File menu // ************* {Action::FileBackup, QStringLiteral("file_backup"), i18n("Backup..."), Icon::Empty}, {Action::FileImportStatement, QStringLiteral("file_import_statement"), i18n("Statement file..."), Icon::Empty}, {Action::FileImportTemplate, QStringLiteral("file_import_template"), i18n("Account Template..."), Icon::Empty}, {Action::FileExportTemplate, QStringLiteral("file_export_template"), i18n("Account Template..."), Icon::Empty}, {Action::FilePersonalData, QStringLiteral("view_personal_data"), i18n("Personal Data..."), Icon::UserProperties}, #ifdef KMM_DEBUG {Action::FileDump, QStringLiteral("file_dump"), i18n("Dump Memory"), Icon::Empty}, #endif {Action::FileInformation, QStringLiteral("view_file_info"), i18n("File-Information..."), Icon::DocumentProperties}, // ************* // The Edit menu // ************* {Action::EditFindTransaction, QStringLiteral("edit_find_transaction"), i18n("Find transaction..."), Icon::EditFindTransaction}, // ************* // The View menu // ************* {Action::ViewTransactionDetail, QStringLiteral("view_show_transaction_detail"), i18n("Show Transaction Detail"), Icon::ViewTransactionDetail}, {Action::ViewHideReconciled, QStringLiteral("view_hide_reconciled_transactions"), i18n("Hide reconciled transactions"), Icon::HideReconciled}, {Action::ViewHideCategories, QStringLiteral("view_hide_unused_categories"), i18n("Hide unused categories"), Icon::HideCategories}, {Action::ViewShowAll, QStringLiteral("view_show_all_accounts"), i18n("Show all accounts"), Icon::Empty}, // ********************* // The institutions menu // ********************* {Action::NewInstitution, QStringLiteral("institution_new"), i18n("New institution..."), Icon::InstitutionNew}, {Action::EditInstitution, QStringLiteral("institution_edit"), i18n("Edit institution..."), Icon::InstitutionEdit}, {Action::DeleteInstitution, QStringLiteral("institution_delete"), i18n("Delete institution..."), Icon::InstitutionDelete}, // ***************** // The accounts menu // ***************** {Action::NewAccount, QStringLiteral("account_new"), i18n("New account..."), Icon::AccountNew}, {Action::OpenAccount, QStringLiteral("account_open"), i18n("Open ledger"), Icon::ViewFinancialList}, {Action::StartReconciliation, QStringLiteral("account_reconcile"), i18n("Reconcile..."), Icon::Reconcile}, {Action::FinishReconciliation, QStringLiteral("account_reconcile_finish"), i18nc("Finish reconciliation", "Finish"), Icon::AccountFinishReconciliation}, {Action::PostponeReconciliation, QStringLiteral("account_reconcile_postpone"), i18n("Postpone reconciliation"), Icon::MediaPlaybackPause}, {Action::EditAccount, QStringLiteral("account_edit"), i18n("Edit account..."), Icon::AccountEdit}, {Action::DeleteAccount, QStringLiteral("account_delete"), i18n("Delete account..."), Icon::AccountDelete}, {Action::CloseAccount, QStringLiteral("account_close"), i18n("Close account"), Icon::AccountClose}, {Action::ReopenAccount, QStringLiteral("account_reopen"), i18n("Reopen account"), Icon::AccountReopen}, {Action::ReportAccountTransactions, QStringLiteral("account_transaction_report"), i18n("Transaction report"), Icon::ViewFinancialList}, {Action::ChartAccountBalance, QStringLiteral("account_chart"), i18n("Show balance chart..."), Icon::OfficeChartLine}, {Action::MapOnlineAccount, QStringLiteral("account_online_map"), i18n("Map account..."), Icon::NewsSubscribe}, {Action::UnmapOnlineAccount, QStringLiteral("account_online_unmap"), i18n("Unmap account..."), Icon::NewsUnsubscribe}, {Action::UpdateAccount, QStringLiteral("account_online_update"), i18n("Update account..."), Icon::AccountUpdate}, {Action::UpdateAllAccounts, QStringLiteral("account_online_update_all"), i18n("Update all accounts..."), Icon::AccountUpdateAll}, {Action::AccountCreditTransfer, QStringLiteral("account_online_new_credit_transfer"), i18n("New credit transfer"), Icon::AccountCreditTransfer}, // ******************* // The categories menu // ******************* {Action::NewCategory, QStringLiteral("category_new"), i18n("New category..."), Icon::CategoryNew}, {Action::EditCategory, QStringLiteral("category_edit"), i18n("Edit category..."), Icon::CategoryEdit}, {Action::DeleteCategory, QStringLiteral("category_delete"), i18n("Delete category..."), Icon::CategoryDelete}, // ************** // The tools menu // ************** {Action::ToolCurrencies, QStringLiteral("tools_currency_editor"), i18n("Currencies..."), Icon::ViewCurrencyList}, {Action::ToolPrices, QStringLiteral("tools_price_editor"), i18n("Prices..."), Icon::Empty}, {Action::ToolUpdatePrices, QStringLiteral("tools_update_prices"), i18n("Update Stock and Currency Prices..."), Icon::ToolUpdatePrices}, {Action::ToolConsistency, QStringLiteral("tools_consistency_check"), i18n("Consistency Check"), Icon::Empty}, {Action::ToolPerformance, QStringLiteral("tools_performancetest"), i18n("Performance-Test"), Icon::Fork}, {Action::ToolCalculator, QStringLiteral("tools_kcalc"), i18n("Calculator..."), Icon::AccessoriesCalculator}, // ***************** // The settings menu // ***************** {Action::SettingsAllMessages, QStringLiteral("settings_enable_messages"), i18n("Enable all messages"), Icon::Empty}, // ************* // The help menu // ************* {Action::HelpShow, QStringLiteral("help_show_tip"), i18n("&Show tip of the day"), Icon::Tip}, // *************************** // Actions w/o main menu entry // *************************** {Action::NewTransaction, QStringLiteral("transaction_new"), i18nc("New transaction button", "New"), Icon::TransactionNew}, {Action::EditTransaction, QStringLiteral("transaction_edit"), i18nc("Edit transaction button", "Edit"), Icon::TransactionEdit}, {Action::EnterTransaction, QStringLiteral("transaction_enter"), i18nc("Enter transaction", "Enter"), Icon::DialogOK}, {Action::EditSplits, QStringLiteral("transaction_editsplits"), i18nc("Edit split button", "Edit splits"), Icon::Split}, {Action::CancelTransaction, QStringLiteral("transaction_cancel"), i18nc("Cancel transaction edit", "Cancel"), Icon::DialogCancel}, {Action::DeleteTransaction, QStringLiteral("transaction_delete"), i18nc("Delete transaction", "Delete"), Icon::EditDelete}, {Action::DuplicateTransaction, QStringLiteral("transaction_duplicate"), i18nc("Duplicate transaction", "Duplicate"), Icon::EditCopy}, {Action::MatchTransaction, QStringLiteral("transaction_match"), i18nc("Button text for match transaction", "Match"),Icon::TransactionMatch}, {Action::AcceptTransaction, QStringLiteral("transaction_accept"), i18nc("Accept 'imported' and 'matched' transaction", "Accept"), Icon::TransactionAccept}, {Action::ToggleReconciliationFlag, QStringLiteral("transaction_mark_toggle"), i18nc("Toggle reconciliation flag", "Toggle"), Icon::Empty}, {Action::MarkCleared, QStringLiteral("transaction_mark_cleared"), i18nc("Mark transaction cleared", "Cleared"), Icon::Empty}, {Action::MarkReconciled, QStringLiteral("transaction_mark_reconciled"), i18nc("Mark transaction reconciled", "Reconciled"), Icon::Empty}, {Action::MarkNotReconciled, QStringLiteral("transaction_mark_notreconciled"), i18nc("Mark transaction not reconciled", "Not reconciled"), Icon::Empty}, {Action::SelectAllTransactions, QStringLiteral("transaction_select_all"), i18nc("Select all transactions", "Select all"), Icon::Empty}, {Action::GoToAccount, QStringLiteral("transaction_goto_account"), i18n("Go to account"), Icon::GoJump}, {Action::GoToPayee, QStringLiteral("transaction_goto_payee"), i18n("Go to payee"), Icon::GoJump}, {Action::NewScheduledTransaction, QStringLiteral("transaction_create_schedule"), i18n("Create scheduled transaction..."), Icon::AppointmentNew}, {Action::AssignTransactionsNumber, QStringLiteral("transaction_assign_number"), i18n("Assign next number"), Icon::Empty}, {Action::CombineTransactions, QStringLiteral("transaction_combine"), i18nc("Combine transactions", "Combine"), Icon::Empty}, {Action::CopySplits, QStringLiteral("transaction_copy_splits"), i18n("Copy splits"), Icon::Empty}, //Investment {Action::NewInvestment, QStringLiteral("investment_new"), i18n("New investment..."), Icon::InvestmentNew}, {Action::EditInvestment, QStringLiteral("investment_edit"), i18n("Edit investment..."), Icon::InvestmentEdit}, {Action::DeleteInvestment, QStringLiteral("investment_delete"), i18n("Delete investment..."), Icon::InvestmentDelete}, {Action::UpdatePriceOnline, QStringLiteral("investment_online_price_update"), i18n("Online price update..."), Icon::InvestmentOnlinePrice}, {Action::UpdatePriceManually, QStringLiteral("investment_manual_price_update"), i18n("Manual price update..."), Icon::Empty}, //Schedule {Action::NewSchedule, QStringLiteral("schedule_new"), i18n("New scheduled transaction"), Icon::AppointmentNew}, {Action::EditSchedule, QStringLiteral("schedule_edit"), i18n("Edit scheduled transaction"), Icon::DocumentEdit}, {Action::DeleteSchedule, QStringLiteral("schedule_delete"), i18n("Delete scheduled transaction"), Icon::EditDelete}, {Action::DuplicateSchedule, QStringLiteral("schedule_duplicate"), i18n("Duplicate scheduled transaction"), Icon::EditCopy}, {Action::EnterSchedule, QStringLiteral("schedule_enter"), i18n("Enter next transaction..."), Icon::KeyEnter}, {Action::SkipSchedule, QStringLiteral("schedule_skip"), i18n("Skip next transaction..."), Icon::MediaSeekForward}, //Payees {Action::NewPayee, QStringLiteral("payee_new"), i18n("New payee"), Icon::ListAddUser}, {Action::RenamePayee, QStringLiteral("payee_rename"), i18n("Rename payee"), Icon::PayeeRename}, {Action::DeletePayee, QStringLiteral("payee_delete"), i18n("Delete payee"), Icon::ListRemoveUser}, {Action::MergePayee, QStringLiteral("payee_merge"), i18n("Merge payees"), Icon::PayeeMerge}, //Tags {Action::NewTag, QStringLiteral("tag_new"), i18n("New tag"), Icon::ListAddTag}, {Action::RenameTag, QStringLiteral("tag_rename"), i18n("Rename tag"), Icon::TagRename}, {Action::DeleteTag, QStringLiteral("tag_delete"), i18n("Delete tag"), Icon::ListRemoveTag}, //debug actions #ifdef KMM_DEBUG {Action::WizardNewUser, QStringLiteral("new_user_wizard"), i18n("Test new feature"), Icon::Empty}, {Action::DebugTraces, QStringLiteral("debug_traces"), i18n("Debug Traces"), Icon::Empty}, #endif {Action::DebugTimers, QStringLiteral("debug_timers"), i18n("Debug Timers"), Icon::Empty}, // onlineJob actions {Action::DeleteOnlineJob, QStringLiteral("onlinejob_delete"), i18n("Remove credit transfer"), Icon::EditDelete}, {Action::EditOnlineJob, QStringLiteral("onlinejob_edit"), i18n("Edit credit transfer"), Icon::DocumentEdit}, {Action::LogOnlineJob, QStringLiteral("onlinejob_log"), i18n("Show log"), Icon::Empty}, }; for (const auto& info : actionInfos) { auto a = new QAction(this); // KActionCollection::addAction by name sets object name anyways, // so, as better alternative, set it here right from the start a->setObjectName(info.name); a->setText(info.text); if (info.icon != Icon::Empty) // no need to set empty icon a->setIcon(Icons::get(info.icon)); a->setEnabled(false); lutActions.insert(info.action, a); // store QAction's pointer for later processing } } { // List with slots that get connected here. Other slots get connected in e.g. appropriate views typedef void(KMyMoneyApp::*KMyMoneyAppFunc)(); const QHash actionConnections { // ************* // The File menu // ************* // {Action::FileOpenDatabase, &KMyMoneyApp::slotOpenDatabase}, // {Action::FileSaveAsDatabase, &KMyMoneyApp::slotSaveAsDatabase}, {Action::FileBackup, &KMyMoneyApp::slotBackupFile}, {Action::FileImportTemplate, &KMyMoneyApp::slotLoadAccountTemplates}, {Action::FileExportTemplate, &KMyMoneyApp::slotSaveAccountTemplates}, {Action::FilePersonalData, &KMyMoneyApp::slotFileViewPersonal}, #ifdef KMM_DEBUG {Action::FileDump, &KMyMoneyApp::slotFileFileInfo}, #endif {Action::FileInformation, &KMyMoneyApp::slotFileInfoDialog}, // ************* // The Edit menu // ************* {Action::EditFindTransaction, &KMyMoneyApp::slotFindTransaction}, // ************* // The View menu // ************* {Action::ViewTransactionDetail, &KMyMoneyApp::slotShowTransactionDetail}, {Action::ViewHideReconciled, &KMyMoneyApp::slotHideReconciledTransactions}, {Action::ViewHideCategories, &KMyMoneyApp::slotHideUnusedCategories}, {Action::ViewShowAll, &KMyMoneyApp::slotShowAllAccounts}, // ************** // The tools menu // ************** {Action::ToolCurrencies, &KMyMoneyApp::slotCurrencyDialog}, {Action::ToolPrices, &KMyMoneyApp::slotPriceDialog}, {Action::ToolUpdatePrices, &KMyMoneyApp::slotEquityPriceUpdate}, {Action::ToolConsistency, &KMyMoneyApp::slotFileConsistencyCheck}, {Action::ToolPerformance, &KMyMoneyApp::slotPerformanceTest}, // {Action::ToolSQL, &KMyMoneyApp::slotGenerateSql}, {Action::ToolCalculator, &KMyMoneyApp::slotToolsStartKCalc}, // ***************** // The settings menu // ***************** {Action::SettingsAllMessages, &KMyMoneyApp::slotEnableMessages}, // ************* // The help menu // ************* {Action::HelpShow, &KMyMoneyApp::slotShowTipOfTheDay}, // *************************** // Actions w/o main menu entry // *************************** //debug actions #ifdef KMM_DEBUG {Action::WizardNewUser, &KMyMoneyApp::slotNewFeature}, {Action::DebugTraces, &KMyMoneyApp::slotToggleTraces}, #endif {Action::DebugTimers, &KMyMoneyApp::slotToggleTimers}, }; for (auto connection = actionConnections.cbegin(); connection != actionConnections.cend(); ++connection) connect(lutActions[connection.key()], &QAction::triggered, this, connection.value()); } // ************* // Setting some of added actions checkable // ************* { // Some actions are checkable, // so set them here const QVector checkableActions { Action::ViewTransactionDetail, Action::ViewHideReconciled, Action::ViewHideCategories, #ifdef KMM_DEBUG Action::DebugTraces, Action::DebugTimers, #endif Action::ViewShowAll }; for (const auto& it : checkableActions) { lutActions[it]->setCheckable(true); lutActions[it]->setEnabled(true); } } // ************* // Setting actions that are always enabled // ************* { const QVector alwaysEnabled { Action::HelpShow, Action::SettingsAllMessages, Action::ToolPerformance, Action::ToolCalculator }; for (const auto& action : alwaysEnabled) { lutActions[action]->setEnabled(true); } } // ************* // Setting keyboard shortcuts for some of added actions // ************* { const QVector> actionShortcuts { {qMakePair(Action::EditFindTransaction, Qt::CTRL + Qt::Key_F)}, {qMakePair(Action::ViewTransactionDetail, Qt::CTRL + Qt::Key_T)}, {qMakePair(Action::ViewHideReconciled, Qt::CTRL + Qt::Key_R)}, {qMakePair(Action::ViewHideCategories, Qt::CTRL + Qt::Key_U)}, {qMakePair(Action::ViewShowAll, Qt::CTRL + Qt::SHIFT + Qt::Key_A)}, {qMakePair(Action::StartReconciliation, Qt::CTRL + Qt::SHIFT + Qt::Key_R)}, {qMakePair(Action::NewTransaction, Qt::CTRL + Qt::Key_Insert)}, {qMakePair(Action::ToggleReconciliationFlag, Qt::CTRL + Qt::Key_Space)}, {qMakePair(Action::MarkCleared, Qt::CTRL + Qt::ALT+ Qt::Key_Space)}, {qMakePair(Action::MarkReconciled, Qt::CTRL + Qt::SHIFT + Qt::Key_Space)}, {qMakePair(Action::SelectAllTransactions, Qt::CTRL + Qt::Key_A)}, #ifdef KMM_DEBUG {qMakePair(Action::WizardNewUser, Qt::CTRL + Qt::Key_G)}, #endif {qMakePair(Action::AssignTransactionsNumber, Qt::CTRL + Qt::SHIFT + Qt::Key_N)} }; for(const auto& it : actionShortcuts) aC->setDefaultShortcut(lutActions[it.first], it.second); } // ************* // Misc settings // ************* connect(onlineJobAdministration::instance(), &onlineJobAdministration::canSendCreditTransferChanged, lutActions.value(Action::AccountCreditTransfer), &QAction::setEnabled); // Setup transaction detail switch lutActions[Action::ViewTransactionDetail]->setChecked(KMyMoneySettings::showRegisterDetailed()); lutActions[Action::ViewHideReconciled]->setChecked(KMyMoneySettings::hideReconciledTransactions()); lutActions[Action::ViewHideCategories]->setChecked(KMyMoneySettings::hideUnusedCategory()); lutActions[Action::ViewShowAll]->setChecked(false); // ************* // Adding actions to ActionCollection // ************* actionCollection()->addActions(lutActions.values()); // ************************ // Currently unused actions // ************************ #if 0 new KToolBarPopupAction(i18n("View back"), "go-previous", 0, this, SLOT(slotShowPreviousView()), actionCollection(), "go_back"); new KToolBarPopupAction(i18n("View forward"), "go-next", 0, this, SLOT(slotShowNextView()), actionCollection(), "go_forward"); action("go_back")->setEnabled(false); action("go_forward")->setEnabled(false); #endif // use the absolute path to your kmymoneyui.rc file for testing purpose in createGUI(); setupGUI(); // reconnect about app entry to dialog with full credits information auto aboutApp = aC->action(QString::fromLatin1(KStandardAction::name(KStandardAction::AboutApp))); aboutApp->disconnect(); connect(aboutApp, &QAction::triggered, this, &KMyMoneyApp::slotShowCredits); QMenu *menuContainer; menuContainer = static_cast(factory()->container(QStringLiteral("import"), this)); menuContainer->setIcon(Icons::get(Icon::DocumentImport)); menuContainer = static_cast(factory()->container(QStringLiteral("export"), this)); menuContainer->setIcon(Icons::get(Icon::DocumentExport)); return lutActions; } #ifdef KMM_DEBUG void KMyMoneyApp::dumpActions() const { const QList list = actionCollection()->actions(); foreach (const auto it, list) std::cout << qPrintable(it->objectName()) << ": " << qPrintable(it->text()) << std::endl; } #endif bool KMyMoneyApp::isActionToggled(const Action _a) { return pActions[_a]->isChecked(); } void KMyMoneyApp::initStatusBar() { /////////////////////////////////////////////////////////////////// // STATUSBAR d->m_statusLabel = new QLabel; statusBar()->addWidget(d->m_statusLabel); ready(); // Initialization of progress bar taken from KDevelop ;-) d->m_progressBar = new QProgressBar; statusBar()->addWidget(d->m_progressBar); d->m_progressBar->setFixedHeight(d->m_progressBar->sizeHint().height() - 8); // hide the progress bar for now slotStatusProgressBar(-1, -1); } void KMyMoneyApp::saveOptions() { KConfigGroup grp = d->m_config->group("General Options"); grp.writeEntry("Geometry", size()); grp.writeEntry("Show Statusbar", actionCollection()->action(KStandardAction::name(KStandardAction::ShowStatusbar))->isChecked()); KConfigGroup toolbarGrp = d->m_config->group("mainToolBar"); toolBar("mainToolBar")->saveSettings(toolbarGrp); d->m_recentFiles->saveEntries(d->m_config->group("Recent Files")); } void KMyMoneyApp::readOptions() { KConfigGroup grp = d->m_config->group("General Options"); pActions[Action::ViewHideReconciled]->setChecked(KMyMoneySettings::hideReconciledTransactions()); pActions[Action::ViewHideCategories]->setChecked(KMyMoneySettings::hideUnusedCategory()); d->m_recentFiles->loadEntries(d->m_config->group("Recent Files")); // Startdialog is written in the settings dialog d->m_startDialog = grp.readEntry("StartDialog", true); } void KMyMoneyApp::resizeEvent(QResizeEvent* ev) { KMainWindow::resizeEvent(ev); updateCaption(true); } int KMyMoneyApp::askSaveOnClose() { int ans; if (KMyMoneySettings::autoSaveOnClose()) { ans = KMessageBox::Yes; } else { ans = KMessageBox::warningYesNoCancel(this, i18n("The file has been changed, save it?")); } return ans; } bool KMyMoneyApp::queryClose() { if (!isReady()) return false; if (d->dirty()) { int ans = askSaveOnClose(); if (ans == KMessageBox::Cancel) return false; else if (ans == KMessageBox::Yes) { bool saved = slotFileSave(); saveOptions(); return saved; } } // if (d->m_myMoneyView->isDatabase()) // slotFileClose(); // close off the database saveOptions(); return true; } ///////////////////////////////////////////////////////////////////// // SLOT IMPLEMENTATION ///////////////////////////////////////////////////////////////////// void KMyMoneyApp::slotFileInfoDialog() { QPointer dlg = new KMyMoneyFileInfoDlg(0); dlg->exec(); delete dlg; } void KMyMoneyApp::slotPerformanceTest() { // dump performance report to stderr int measurement[2]; QTime timer; MyMoneyAccount acc; qDebug("--- Starting performance tests ---"); // AccountList // MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; timer.start(); for (int i = 0; i < 1000; ++i) { QList list; MyMoneyFile::instance()->accountList(list); measurement[i != 0] = timer.elapsed(); } std::cerr << "accountList()" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Total time: " << (measurement[0] + measurement[1]) << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 1000 << " msec" << std::endl; // Balance of asset account(s) // MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; acc = MyMoneyFile::instance()->asset(); for (int i = 0; i < 1000; ++i) { timer.start(); MyMoneyMoney result = MyMoneyFile::instance()->balance(acc.id()); measurement[i != 0] += timer.elapsed(); } std::cerr << "balance(Asset)" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 1000 << " msec" << std::endl; // total balance of asset account // MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; acc = MyMoneyFile::instance()->asset(); for (int i = 0; i < 1000; ++i) { timer.start(); MyMoneyMoney result = MyMoneyFile::instance()->totalBalance(acc.id()); measurement[i != 0] += timer.elapsed(); } std::cerr << "totalBalance(Asset)" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 1000 << " msec" << std::endl; // Balance of expense account(s) // MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; acc = MyMoneyFile::instance()->expense(); for (int i = 0; i < 1000; ++i) { timer.start(); MyMoneyMoney result = MyMoneyFile::instance()->balance(acc.id()); measurement[i != 0] += timer.elapsed(); } std::cerr << "balance(Expense)" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 1000 << " msec" << std::endl; // total balance of expense account // MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; acc = MyMoneyFile::instance()->expense(); timer.start(); for (int i = 0; i < 1000; ++i) { MyMoneyMoney result = MyMoneyFile::instance()->totalBalance(acc.id()); measurement[i != 0] = timer.elapsed(); } std::cerr << "totalBalance(Expense)" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Total time: " << (measurement[0] + measurement[1]) << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 1000 << " msec" << std::endl; // transaction list // MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; if (MyMoneyFile::instance()->asset().accountCount()) { MyMoneyTransactionFilter filter(MyMoneyFile::instance()->asset().accountList()[0]); filter.setDateFilter(QDate(), QDate::currentDate()); QList list; timer.start(); for (int i = 0; i < 100; ++i) { list = MyMoneyFile::instance()->transactionList(filter); measurement[i != 0] = timer.elapsed(); } std::cerr << "transactionList()" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Total time: " << (measurement[0] + measurement[1]) << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 100 << " msec" << std::endl; } // transaction list // MyMoneyFile::instance()->preloadCache(); measurement[0] = measurement[1] = 0; if (MyMoneyFile::instance()->asset().accountCount()) { MyMoneyTransactionFilter filter(MyMoneyFile::instance()->asset().accountList()[0]); filter.setDateFilter(QDate(), QDate::currentDate()); QList list; timer.start(); for (int i = 0; i < 100; ++i) { MyMoneyFile::instance()->transactionList(list, filter); measurement[i != 0] = timer.elapsed(); } std::cerr << "transactionList(list)" << std::endl; std::cerr << "First time: " << measurement[0] << " msec" << std::endl; std::cerr << "Total time: " << (measurement[0] + measurement[1]) << " msec" << std::endl; std::cerr << "Average : " << (measurement[0] + measurement[1]) / 100 << " msec" << std::endl; } // MyMoneyFile::instance()->preloadCache(); } void KMyMoneyApp::slotFileNew() { KMSTATUS(i18n("Creating new document...")); slotFileClose(); if (!d->m_fileOpen) { // next line required until we move all file handling out of KMyMoneyView d->newFile(); d->m_fileName = QUrl(); updateCaption(); NewUserWizard::Wizard *wizard = new NewUserWizard::Wizard(); if (wizard->exec() == QDialog::Accepted) { MyMoneyFileTransaction ft; MyMoneyFile* file = MyMoneyFile::instance(); try { // store the user info file->setUser(wizard->user()); // create and setup base currency file->addCurrency(wizard->baseCurrency()); file->setBaseCurrency(wizard->baseCurrency()); // create a possible institution MyMoneyInstitution inst = wizard->institution(); if (inst.name().length()) { file->addInstitution(inst); } // create a possible checking account auto acc = wizard->account(); if (acc.name().length()) { acc.setInstitutionId(inst.id()); MyMoneyAccount asset = file->asset(); file->addAccount(acc, asset); // create possible opening balance transaction if (!wizard->openingBalance().isZero()) { file->createOpeningBalanceTransaction(acc, wizard->openingBalance()); } } // import the account templates QList templates = wizard->templates(); QList::iterator it_t; for (it_t = templates.begin(); it_t != templates.end(); ++it_t) { (*it_t).importTemplate(&progressCallback); } d->m_fileName = wizard->url(); ft.commit(); KMyMoneySettings::setFirstTimeRun(false); // FIXME This is a bit clumsy. We re-read the freshly // created file to be able to run through all the // fixup logic and then save it to keep the modified // flag off. slotFileSave(); if (d->openNondatabase(d->m_fileName)) { d->m_fileOpen = true; d->initializeStorage(); } slotFileSave(); // now keep the filename in the recent files used list //KRecentFilesAction *p = dynamic_cast(action(KStandardAction::name(KStandardAction::OpenRecent))); //if(p) d->m_recentFiles->addUrl(d->m_fileName); writeLastUsedFile(d->m_fileName.url()); } catch (const MyMoneyException &) { // next line required until we move all file handling out of KMyMoneyView d->closeFile(); } if (wizard->startSettingsAfterFinished()) slotSettings(); } else { // next line required until we move all file handling out of KMyMoneyView d->closeFile(); } delete wizard; updateCaption(); emit fileLoaded(d->m_fileName); } } bool KMyMoneyApp::isDatabase() { return (d->m_fileOpen && ((d->m_fileType == KmmDb))); } bool KMyMoneyApp::isNativeFile() { return (d->m_fileOpen && (d->m_fileType < MaxNativeFileType)); } bool KMyMoneyApp::fileOpen() const { return d->m_fileOpen; } // General open void KMyMoneyApp::slotFileOpen() { KMSTATUS(i18n("Open a file.")); QString prevDir = readLastUsedDir(); QString fileExtensions; fileExtensions.append(i18n("KMyMoney files (*.kmy *.xml)")); fileExtensions.append(QLatin1String(";;")); for (const auto& plugin : d->m_plugins.storage) { const auto fileExtension = plugin->fileExtension(); if (!fileExtension.isEmpty()) { fileExtensions.append(fileExtension); fileExtensions.append(QLatin1String(";;")); } } fileExtensions.append(i18n("All files (*)")); QPointer dialog = new QFileDialog(this, QString(), prevDir, fileExtensions); dialog->setFileMode(QFileDialog::ExistingFile); dialog->setAcceptMode(QFileDialog::AcceptOpen); if (dialog->exec() == QDialog::Accepted && dialog != nullptr) { slotFileOpenRecent(dialog->selectedUrls().first()); } delete dialog; } bool KMyMoneyApp::isImportableFile(const QUrl &url) { bool result = false; // Iterate through the plugins and see if there's a loaded plugin who can handle it QMap::const_iterator it_plugin = d->m_plugins.importer.constBegin(); while (it_plugin != d->m_plugins.importer.constEnd()) { if ((*it_plugin)->isMyFormat(url.path())) { result = true; break; } ++it_plugin; } // If we did not find a match, try importing it as a KMM statement file, // which is really just for testing. the statement file is not exposed // to users. if (it_plugin == d->m_plugins.importer.constEnd()) if (MyMoneyStatement::isStatementFile(url.path())) result = true; // Place code here to test for QIF and other locally-supported formats // (i.e. not a plugin). If you add them here, be sure to add it to // the webConnect function. return result; } bool KMyMoneyApp::isFileOpenedInAnotherInstance(const QUrl &url) { const auto instances = instanceList(); #ifdef KMM_DBUS // check if there are other instances which might have this file open for (const auto& instance : instances) { QDBusInterface remoteApp(instance, "/KMymoney", "org.kde.kmymoney"); QDBusReply reply = remoteApp.call("filename"); if (!reply.isValid()) qDebug("D-Bus error while calling app->filename()"); else if (reply.value() == url.url()) return true; } +#else + Q_UNUSED(url) #endif return false; } void KMyMoneyApp::slotFileOpenRecent(const QUrl &url) { KMSTATUS(i18n("Loading file...")); if (isFileOpenedInAnotherInstance(url)) { KMessageBox::sorry(this, i18n("

File %1 is already opened in another instance of KMyMoney

", url.toDisplayString(QUrl::PreferLocalFile)), i18n("Duplicate open")); return; } if (url.scheme() != QLatin1String("sql") && !KMyMoneyUtils::fileExists(url)) { KMessageBox::sorry(this, i18n("

%1 is either an invalid filename or the file does not exist. You can open another file or create a new one.

", url.toDisplayString(QUrl::PreferLocalFile)), i18n("File not found")); return; } if (d->m_fileOpen) slotFileClose(); if (d->m_fileOpen) return; try { auto isOpened = false; if (url.scheme() == QLatin1String("sql")) isOpened = d->openDatabase(url); else isOpened = d->openNondatabase(url); if (!isOpened) return; d->m_fileOpen = true; if (!d->initializeStorage()) { d->m_fileOpen = false; return; } if (isNativeFile()) { d->m_fileName = url; updateCaption(); writeLastUsedFile(url.toDisplayString(QUrl::PreferLocalFile)); /* Don't use url variable after KRecentFilesAction::addUrl * as it might delete it. * More in API reference to this method */ d->m_recentFiles->addUrl(url); } else { d->m_fileName = QUrl(); // imported files have no filename } } catch (const MyMoneyException &e) { KMessageBox::sorry(this, i18n("Cannot open file as requested. Error was: %1", e.what())); } updateCaption(); emit fileLoaded(d->m_fileName); } bool KMyMoneyApp::slotFileSave() { // if there's nothing changed, there's no need to save anything if (!d->dirty()) return true; bool rc = false; KMSTATUS(i18n("Saving file...")); if (d->m_fileName.isEmpty()) return slotFileSaveAs(); d->consistencyCheck(false); setEnabled(false); if (isDatabase()) { auto pluginFound = false; for (const auto& plugin : d->m_plugins.storage) { if (plugin->formatName().compare(QLatin1String("SQL")) == 0) { rc = plugin->save(d->m_fileName); pluginFound = true; break; } } if(!pluginFound) KMessageBox::error(this, i18n("Couldn't find suitable plugin to save your storage.")); } else { rc = d->saveFile(d->m_fileName, MyMoneyFile::instance()->value("kmm-encryption-key")); } setEnabled(true); d->m_autoSaveTimer->stop(); updateCaption(); return rc; } bool KMyMoneyApp::slotFileSaveAs() { bool rc = false; KMSTATUS(i18n("Saving file with a new filename...")); QString selectedKeyName; if (KGPGFile::GPGAvailable() && KMyMoneySettings::writeDataEncrypted()) { // fill the secret key list and combo box QStringList keyList; KGPGFile::secretKeyList(keyList); QPointer dlg = new KGpgKeySelectionDlg(this); dlg->setSecretKeys(keyList, KMyMoneySettings::gpgRecipient()); dlg->setAdditionalKeys(KMyMoneySettings::gpgRecipientList()); const int rc = dlg->exec(); if ((rc == QDialog::Accepted) && (dlg != 0)) { d->m_additionalGpgKeys = dlg->additionalKeys(); selectedKeyName = dlg->secretKey(); } delete dlg; if (rc != QDialog::Accepted) { return false; } } QString prevDir; // don't prompt file name if not a native file if (isNativeFile()) prevDir = readLastUsedDir(); QPointer dlg = new QFileDialog(this, i18n("Save As"), prevDir, QString(QLatin1String("%2 (%1);;")).arg(QStringLiteral("*.kmy")).arg(i18nc("KMyMoney (Filefilter)", "KMyMoney files")) + QString(QLatin1String("%2 (%1);;")).arg(QStringLiteral("*.xml")).arg(i18nc("XML (Filefilter)", "XML files")) + QString(QLatin1String("%2 (%1);;")).arg(QStringLiteral("*.anon.xml")).arg(i18nc("Anonymous (Filefilter)", "Anonymous files")) + QString(QLatin1String("%2 (%1);;")).arg(QStringLiteral("*")).arg(i18nc("All files (Filefilter)", "All files"))); dlg->setAcceptMode(QFileDialog::AcceptSave); if (dlg->exec() == QDialog::Accepted && dlg != 0) { QUrl newURL = dlg->selectedUrls().first(); if (!newURL.fileName().isEmpty()) { d->consistencyCheck(false); QString newName = newURL.toDisplayString(QUrl::PreferLocalFile); // append extension if not present if (!newName.endsWith(QLatin1String(".kmy"), Qt::CaseInsensitive) && !newName.endsWith(QLatin1String(".xml"), Qt::CaseInsensitive)) newName.append(QLatin1String(".kmy")); newURL = QUrl::fromUserInput(newName); d->m_recentFiles->addUrl(newURL); setEnabled(false); // If this is the anonymous file export, just save it, don't actually take the // name, or remember it! Don't even try to encrypt it if (newName.endsWith(QLatin1String(".anon.xml"), Qt::CaseInsensitive)) rc = d->saveFile(newURL); else { d->m_fileName = newURL; QString encryptionKeys; QRegExp keyExp(".* \\((.*)\\)"); if (keyExp.indexIn(selectedKeyName) != -1) { encryptionKeys = keyExp.cap(1); if (!d->m_additionalGpgKeys.isEmpty()) { if (!encryptionKeys.isEmpty()) encryptionKeys.append(QLatin1Char(',')); encryptionKeys.append(d->m_additionalGpgKeys.join(QLatin1Char(','))); } } rc = d->saveFile(d->m_fileName, encryptionKeys); //write the directory used for this file as the default one for next time. writeLastUsedDir(newURL.toDisplayString(QUrl::RemoveFilename | QUrl::PreferLocalFile | QUrl::StripTrailingSlash)); writeLastUsedFile(newName); } d->m_autoSaveTimer->stop(); setEnabled(true); } } delete dlg; updateCaption(); return rc; } void KMyMoneyApp::slotFileCloseWindow() { KMSTATUS(i18n("Closing window...")); if (d->dirty()) { int answer = askSaveOnClose(); if (answer == KMessageBox::Cancel) return; else if (answer == KMessageBox::Yes) slotFileSave(); } close(); } void KMyMoneyApp::slotFileClose() { // bool okToSelect = true; // check if transaction editor is open and ask user what he wants to do // slotTransactionsCancelOrEnter(okToSelect); // if (!okToSelect) // return; // no update status here, as we might delete the status too early. if (d->dirty()) { int answer = askSaveOnClose(); if (answer == KMessageBox::Cancel) return; else if (answer == KMessageBox::Yes) slotFileSave(); } d->closeFile(); } void KMyMoneyApp::slotFileQuit() { // don't modify the status message here as this will prevent quit from working!! // See the beginning of queryClose() and isReady() why. Thomas Baumgart 2005-10-17 bool quitApplication = true; QList memberList = KMainWindow::memberList(); if (!memberList.isEmpty()) { QList::const_iterator w_it = memberList.constBegin(); for (; w_it != memberList.constEnd(); ++w_it) { // only close the window if the closeEvent is accepted. If the user presses Cancel on the saveModified() dialog, // the window and the application stay open. if (!(*w_it)->close()) { quitApplication = false; break; } } } // We will only quit if all windows were processed and not cancelled if (quitApplication) { QCoreApplication::quit(); } } void KMyMoneyApp::slotShowTransactionDetail() { } void KMyMoneyApp::slotHideReconciledTransactions() { KMyMoneySettings::setHideReconciledTransactions(pActions[Action::ViewHideReconciled]->isChecked()); d->m_myMoneyView->slotRefreshViews(); } void KMyMoneyApp::slotHideUnusedCategories() { KMyMoneySettings::setHideUnusedCategory(pActions[Action::ViewHideCategories]->isChecked()); d->m_myMoneyView->slotRefreshViews(); } void KMyMoneyApp::slotShowAllAccounts() { KMyMoneySettings::setShowAllAccounts(pActions[Action::ViewShowAll]->isChecked()); d->m_myMoneyView->slotRefreshViews(); } #ifdef KMM_DEBUG void KMyMoneyApp::slotFileFileInfo() { if (!d->m_fileOpen) { KMessageBox::information(this, i18n("No KMyMoneyFile open")); return; } QFile g("kmymoney.dump"); g.open(QIODevice::WriteOnly); QDataStream st(&g); MyMoneyStorageDump dumper; dumper.writeStream(st, MyMoneyFile::instance()->storage()); g.close(); } void KMyMoneyApp::slotToggleTraces() { MyMoneyTracer::onOff(pActions[Action::DebugTraces]->isChecked() ? 1 : 0); } #endif void KMyMoneyApp::slotToggleTimers() { extern bool timersOn; // main.cpp timersOn = pActions[Action::DebugTimers]->isChecked(); } QString KMyMoneyApp::slotStatusMsg(const QString &text) { /////////////////////////////////////////////////////////////////// // change status message permanently QString previousMessage = d->m_statusLabel->text(); d->m_applicationIsReady = false; QString currentMessage = text; if (currentMessage.isEmpty() || currentMessage == i18nc("Application is ready to use", "Ready.")) { d->m_applicationIsReady = true; currentMessage = i18nc("Application is ready to use", "Ready."); } statusBar()->clearMessage(); d->m_statusLabel->setText(currentMessage); return previousMessage; } void KMyMoneyApp::ready() { slotStatusMsg(QString()); } bool KMyMoneyApp::isReady() { return d->m_applicationIsReady; } void KMyMoneyApp::slotStatusProgressBar(int current, int total) { if (total == -1 && current == -1) { // reset if (d->m_progressTimer) { d->m_progressTimer->start(500); // remove from screen in 500 msec d->m_progressBar->setValue(d->m_progressBar->maximum()); } } else if (total != 0) { // init d->m_progressTimer->stop(); d->m_progressBar->setMaximum(total); d->m_progressBar->setValue(0); d->m_progressBar->show(); } else { // update QTime currentTime = QTime::currentTime(); // only process painting if last update is at least 250 ms ago if (abs(d->m_lastUpdate.msecsTo(currentTime)) > 250) { d->m_progressBar->setValue(current); d->m_lastUpdate = currentTime; } } } void KMyMoneyApp::slotStatusProgressDone() { d->m_progressTimer->stop(); d->m_progressBar->reset(); d->m_progressBar->hide(); d->m_progressBar->setValue(0); } void KMyMoneyApp::progressCallback(int current, int total, const QString& msg) { if (!msg.isEmpty()) kmymoney->slotStatusMsg(msg); kmymoney->slotStatusProgressBar(current, total); } void KMyMoneyApp::slotFileViewPersonal() { if (!d->m_fileOpen) { KMessageBox::information(this, i18n("No KMyMoneyFile open")); return; } KMSTATUS(i18n("Viewing personal data...")); MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyPayee user = file->user(); QPointer editPersonalDataDlg = new EditPersonalDataDlg(user.name(), user.address(), user.city(), user.state(), user.postcode(), user.telephone(), user.email(), this, i18n("Edit Personal Data")); if (editPersonalDataDlg->exec() == QDialog::Accepted && editPersonalDataDlg != 0) { user.setName(editPersonalDataDlg->userName()); user.setAddress(editPersonalDataDlg->userStreet()); user.setCity(editPersonalDataDlg->userTown()); user.setState(editPersonalDataDlg->userCountry()); user.setPostcode(editPersonalDataDlg->userPostcode()); user.setTelephone(editPersonalDataDlg->userTelephone()); user.setEmail(editPersonalDataDlg->userEmail()); MyMoneyFileTransaction ft; try { file->setUser(user); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::information(this, i18n("Unable to store user information: %1", e.what())); } } delete editPersonalDataDlg; } void KMyMoneyApp::slotLoadAccountTemplates() { KMSTATUS(i18n("Importing account templates.")); int rc; QPointer dlg = new KLoadTemplateDlg(); if ((rc = dlg->exec()) == QDialog::Accepted && dlg != 0) { MyMoneyFileTransaction ft; try { // import the account templates QList templates = dlg->templates(); QList::iterator it_t; for (it_t = templates.begin(); it_t != templates.end(); ++it_t) { (*it_t).importTemplate(&progressCallback); } ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(0, i18n("Error"), i18n("Unable to import template(s): %1, thrown in %2:%3", e.what(), e.file(), e.line())); } } delete dlg; } void KMyMoneyApp::slotSaveAccountTemplates() { KMSTATUS(i18n("Exporting account templates.")); QString savePath = QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/templates/" + QLocale().name(); QDir d(savePath); if (!d.exists()) d.mkpath(savePath); QString newName = QFileDialog::getSaveFileName(this, i18n("Save as..."), savePath, i18n("KMyMoney template files (*.kmt);;All files (*)")); // // If there is no file extension, then append a .kmt at the end of the file name. // If there is a file extension, make sure it is .kmt, delete any others. // if (!newName.isEmpty()) { // find last . delimiter int nLoc = newName.lastIndexOf('.'); if (nLoc != -1) { QString strExt, strTemp; strTemp = newName.left(nLoc + 1); strExt = newName.right(newName.length() - (nLoc + 1)); if ((strExt.indexOf("kmt", 0, Qt::CaseInsensitive) == -1)) { strTemp.append("kmt"); //append to make complete file name newName = strTemp; } } else { newName.append(".kmt"); } if (okToWriteFile(QUrl::fromLocalFile(newName))) { QPointer dlg = new KTemplateExportDlg(this); if (dlg->exec() == QDialog::Accepted && dlg) { MyMoneyTemplate templ; templ.setTitle(dlg->title()); templ.setShortDescription(dlg->shortDescription()); templ.setLongDescription(dlg->longDescription()); templ.exportTemplate(&progressCallback); templ.saveTemplate(QUrl::fromLocalFile(newName)); } delete dlg; } } } bool KMyMoneyApp::okToWriteFile(const QUrl &url) { Q_UNUSED(url) // check if the file exists and warn the user bool reallySaveFile = true; if (KMyMoneyUtils::fileExists(url)) { if (KMessageBox::warningYesNo(this, QLatin1String("") + i18n("The file %1 already exists. Do you really want to overwrite it?", url.toDisplayString(QUrl::PreferLocalFile)) + QLatin1String(""), i18n("File already exists")) != KMessageBox::Yes) reallySaveFile = false; } return reallySaveFile; } void KMyMoneyApp::slotSettings() { // if we already have an instance of the settings dialog, then use it if (KConfigDialog::showDialog("KMyMoney-Settings")) return; // otherwise, we have to create it auto dlg = new KSettingsKMyMoney(this, "KMyMoney-Settings", KMyMoneySettings::self()); connect(dlg, &KSettingsKMyMoney::settingsChanged, this, &KMyMoneyApp::slotUpdateConfiguration); dlg->show(); } void KMyMoneyApp::slotShowCredits() { KAboutData aboutData = initializeCreditsData(); KAboutApplicationDialog dlg(aboutData, this); dlg.exec(); } void KMyMoneyApp::slotUpdateConfiguration(const QString &dialogName) { if(dialogName.compare(QLatin1String("Plugins")) == 0) { KMyMoneyPlugin::pluginHandling(KMyMoneyPlugin::Action::Reorganize, d->m_plugins, this, guiFactory()); onlineJobAdministration::instance()->updateActions(); return; } MyMoneyTransactionFilter::setFiscalYearStart(KMyMoneySettings::firstFiscalMonth(), KMyMoneySettings::firstFiscalDay()); #ifdef ENABLE_UNFINISHEDFEATURES LedgerSeparator::setFirstFiscalDate(KMyMoneySettings::firstFiscalMonth(), KMyMoneySettings::firstFiscalDay()); #endif d->m_myMoneyView->updateViewType(); // update the sql storage module settings // MyMoneyStorageSql::setStartDate(KMyMoneySettings::startDate().date()); // update the report module settings MyMoneyReport::setLineWidth(KMyMoneySettings::lineWidth()); // update the holiday region configuration setHolidayRegion(KMyMoneySettings::holidayRegion()); d->m_myMoneyView->slotRefreshViews(); // re-read autosave configuration d->m_autoSaveEnabled = KMyMoneySettings::autoSaveFile(); d->m_autoSavePeriod = KMyMoneySettings::autoSavePeriod(); // stop timer if turned off but running if (d->m_autoSaveTimer->isActive() && !d->m_autoSaveEnabled) { d->m_autoSaveTimer->stop(); } // start timer if turned on and needed but not running if (!d->m_autoSaveTimer->isActive() && d->m_autoSaveEnabled && d->dirty()) { d->m_autoSaveTimer->setSingleShot(true); d->m_autoSaveTimer->start(d->m_autoSavePeriod * 60 * 1000); } d->setThemedCSS(); // check if the recovery key is still valid or expires soon if (KMyMoneySettings::writeDataEncrypted() && KMyMoneySettings::encryptRecover()) { if (KGPGFile::GPGAvailable()) { KGPGFile file; QDateTime expirationDate = file.keyExpires(QLatin1String(recoveryKeyId2)); if (expirationDate.isValid() && QDateTime::currentDateTime().daysTo(expirationDate) <= RECOVER_KEY_EXPIRATION_WARNING) { bool skipMessage = false; //get global config object for our app. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); KConfigGroup grp; QDate lastWarned; if (kconfig) { grp = d->m_config->group("General Options"); lastWarned = grp.readEntry("LastRecoverKeyExpirationWarning", QDate()); if (QDate::currentDate() == lastWarned) { skipMessage = true; } } if (!skipMessage) { if (kconfig) { grp.writeEntry("LastRecoverKeyExpirationWarning", QDate::currentDate()); } KMessageBox::information(this, i18np("You have configured KMyMoney to use GPG to protect your data and to encrypt your data also with the KMyMoney recover key. This key is about to expire in %1 day. Please update the key from a keyserver using your GPG frontend (e.g. KGPG).", "You have configured KMyMoney to use GPG to protect your data and to encrypt your data also with the KMyMoney recover key. This key is about to expire in %1 days. Please update the key from a keyserver using your GPG frontend (e.g. KGPG).", QDateTime::currentDateTime().daysTo(expirationDate)), i18n("Recover key expires soon")); } } } } } void KMyMoneyApp::slotBackupFile() { // Save the file first so isLocalFile() works if (d->m_myMoneyView && d->dirty()) { if (KMessageBox::questionYesNo(this, i18n("The file must be saved first " "before it can be backed up. Do you want to continue?")) == KMessageBox::No) { return; } slotFileSave(); } if (d->m_fileName.isEmpty()) return; if (!d->m_fileName.isLocalFile()) { KMessageBox::sorry(this, i18n("The current implementation of the backup functionality only supports local files as source files. Your current source file is '%1'.", d->m_fileName.url()), i18n("Local files only")); return; } QPointer backupDlg = new KBackupDlg(this); int returncode = backupDlg->exec(); if (returncode == QDialog::Accepted && backupDlg != 0) { d->m_backupMount = backupDlg->mountCheckBoxChecked(); d->m_proc.clearProgram(); d->m_backupState = BACKUP_MOUNTING; d->m_mountpoint = backupDlg->mountPoint(); if (d->m_backupMount) { slotBackupMount(); } else { progressCallback(0, 300, ""); #ifdef Q_OS_WIN d->m_ignoreBackupExitCode = true; QTimer::singleShot(0, this, SLOT(slotBackupHandleEvents())); #else // If we don't have to mount a device, we just issue // a dummy command to start the copy operation d->m_proc.setProgram("true"); d->m_proc.start(); #endif } } delete backupDlg; } void KMyMoneyApp::slotBackupMount() { progressCallback(0, 300, i18n("Mounting %1", d->m_mountpoint)); d->m_proc.setProgram("mount"); d->m_proc << d->m_mountpoint; d->m_proc.start(); } bool KMyMoneyApp::slotBackupWriteFile() { QFileInfo fi(d->m_fileName.fileName()); QString today = QDate::currentDate().toString("-yyyy-MM-dd.") + fi.suffix(); QString backupfile = d->m_mountpoint + '/' + d->m_fileName.fileName(); KMyMoneyUtils::appendCorrectFileExt(backupfile, today); // check if file already exists and ask what to do QFile f(backupfile); if (f.exists()) { int answer = KMessageBox::warningContinueCancel(this, i18n("Backup file for today exists on that device. Replace?"), i18n("Backup"), KGuiItem(i18n("&Replace"))); if (answer == KMessageBox::Cancel) { return false; } } progressCallback(50, 0, i18n("Writing %1", backupfile)); d->m_proc.clearProgram(); #ifdef Q_OS_WIN d->m_proc << "cmd.exe" << "/c" << "copy" << "/b" << "/y"; d->m_proc << (QDir::toNativeSeparators(d->m_fileName.toLocalFile()) + "+ nul") << QDir::toNativeSeparators(backupfile); #else d->m_proc << "cp" << "-f"; d->m_proc << d->m_fileName.toLocalFile() << backupfile; #endif d->m_backupState = BACKUP_COPYING; d->m_proc.start(); return true; } void KMyMoneyApp::slotBackupUnmount() { progressCallback(250, 0, i18n("Unmounting %1", d->m_mountpoint)); d->m_proc.clearProgram(); d->m_proc.setProgram("umount"); d->m_proc << d->m_mountpoint; d->m_backupState = BACKUP_UNMOUNTING; d->m_proc.start(); } void KMyMoneyApp::slotBackupFinish() { d->m_backupState = BACKUP_IDLE; progressCallback(-1, -1, QString()); ready(); } void KMyMoneyApp::slotBackupHandleEvents() { switch (d->m_backupState) { case BACKUP_MOUNTING: if (d->m_ignoreBackupExitCode || (d->m_proc.exitStatus() == QProcess::NormalExit && d->m_proc.exitCode() == 0)) { d->m_ignoreBackupExitCode = false; d->m_backupResult = 0; if (!slotBackupWriteFile()) { d->m_backupResult = 1; if (d->m_backupMount) slotBackupUnmount(); else slotBackupFinish(); } } else { KMessageBox::information(this, i18n("Error mounting device"), i18n("Backup")); d->m_backupResult = 1; if (d->m_backupMount) slotBackupUnmount(); else slotBackupFinish(); } break; case BACKUP_COPYING: if (d->m_proc.exitStatus() == QProcess::NormalExit && d->m_proc.exitCode() == 0) { if (d->m_backupMount) { slotBackupUnmount(); } else { progressCallback(300, 0, i18nc("Backup done", "Done")); KMessageBox::information(this, i18n("File successfully backed up"), i18n("Backup")); slotBackupFinish(); } } else { qDebug("copy exit code is %d", d->m_proc.exitCode()); d->m_backupResult = 1; KMessageBox::information(this, i18n("Error copying file to device"), i18n("Backup")); if (d->m_backupMount) slotBackupUnmount(); else slotBackupFinish(); } break; case BACKUP_UNMOUNTING: if (d->m_proc.exitStatus() == QProcess::NormalExit && d->m_proc.exitCode() == 0) { progressCallback(300, 0, i18nc("Backup done", "Done")); if (d->m_backupResult == 0) KMessageBox::information(this, i18n("File successfully backed up"), i18n("Backup")); } else { KMessageBox::information(this, i18n("Error unmounting device"), i18n("Backup")); } slotBackupFinish(); break; default: qWarning("Unknown state for backup operation!"); progressCallback(-1, -1, QString()); ready(); break; } } void KMyMoneyApp::slotShowTipOfTheDay() { KTipDialog::showTip(d->m_myMoneyView, "", true); } void KMyMoneyApp::slotShowPreviousView() { } void KMyMoneyApp::slotShowNextView() { } void KMyMoneyApp::slotGenerateSql() { // QPointer editor = new KGenerateSqlDlg(this); // editor->setObjectName("Generate Database SQL"); // editor->exec(); // delete editor; } void KMyMoneyApp::slotToolsStartKCalc() { QString cmd = KMyMoneySettings::externalCalculator(); // if none is present, we fall back to the default if (cmd.isEmpty()) { #if defined(Q_OS_WIN32) cmd = QLatin1String("calc"); #elif defined(Q_OS_MAC) cmd = QLatin1String("open -a Calculator"); #else cmd = QLatin1String("kcalc"); #endif } KRun::runCommand(cmd, this); } void KMyMoneyApp::slotFindTransaction() { if (d->m_searchDlg == 0) { d->m_searchDlg = new KFindTransactionDlg(this); connect(d->m_searchDlg, SIGNAL(destroyed()), this, SLOT(slotCloseSearchDialog())); connect(d->m_searchDlg, SIGNAL(transactionSelected(QString,QString)), this, SIGNAL(transactionSelected(QString,QString))); } d->m_searchDlg->show(); d->m_searchDlg->raise(); d->m_searchDlg->activateWindow(); } void KMyMoneyApp::slotCloseSearchDialog() { if (d->m_searchDlg) d->m_searchDlg->deleteLater(); d->m_searchDlg = 0; } void KMyMoneyApp::createAccount(MyMoneyAccount& newAccount, MyMoneyAccount& parentAccount, MyMoneyAccount& brokerageAccount, MyMoneyMoney openingBal) { MyMoneyFile *file = MyMoneyFile::instance(); try { const MyMoneySecurity& sec = file->security(newAccount.currencyId()); // Check the opening balance if (openingBal.isPositive() && newAccount.accountGroup() == eMyMoney::Account::Type::Liability) { QString message = i18n("This account is a liability and if the " "opening balance represents money owed, then it should be negative. " "Negate the amount?\n\n" "Please click Yes to change the opening balance to %1,\n" "Please click No to leave the amount as %2,\n" "Please click Cancel to abort the account creation." , MyMoneyUtils::formatMoney(-openingBal, newAccount, sec) , MyMoneyUtils::formatMoney(openingBal, newAccount, sec)); int ans = KMessageBox::questionYesNoCancel(this, message); if (ans == KMessageBox::Yes) { openingBal = -openingBal; } else if (ans == KMessageBox::Cancel) return; } file->createAccount(newAccount, parentAccount, brokerageAccount, openingBal); } catch (const MyMoneyException &e) { KMessageBox::information(this, i18n("Unable to add account: %1", e.what())); } } void KMyMoneyApp::slotInvestmentNew(MyMoneyAccount& account, const MyMoneyAccount& parent) { KNewInvestmentWizard::newInvestment(account, parent); } void KMyMoneyApp::slotCategoryNew(MyMoneyAccount& account, const MyMoneyAccount& parent) { KNewAccountDlg::newCategory(account, parent); } void KMyMoneyApp::slotCategoryNew(MyMoneyAccount& account) { KNewAccountDlg::newCategory(account, MyMoneyAccount()); } void KMyMoneyApp::slotAccountNew(MyMoneyAccount& account) { NewAccountWizard::Wizard::newAccount(account); } void KMyMoneyApp::createSchedule(MyMoneySchedule newSchedule, MyMoneyAccount& newAccount) { MyMoneyFile* file = MyMoneyFile::instance(); // Add the schedule only if one exists // // Remember to modify the first split to reference the newly created account if (!newSchedule.name().isEmpty()) { try { // We assume at least 2 splits in the transaction MyMoneyTransaction t = newSchedule.transaction(); if (t.splitCount() < 2) { throw MYMONEYEXCEPTION("Transaction for schedule has less than 2 splits!"); } MyMoneyFileTransaction ft; try { file->addSchedule(newSchedule); // in case of a loan account, we keep a reference to this // schedule in the account if (newAccount.accountType() == eMyMoney::Account::Type::Loan || newAccount.accountType() == eMyMoney::Account::Type::AssetLoan) { newAccount.setValue("schedule", newSchedule.id()); file->modifyAccount(newAccount); } ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::information(this, i18n("Unable to add scheduled transaction: %1", e.what())); } } catch (const MyMoneyException &e) { KMessageBox::information(this, i18n("Unable to add scheduled transaction: %1", e.what())); } } } QList > KMyMoneyApp::Private::automaticReconciliation(const MyMoneyAccount &account, const QList > &transactions, const MyMoneyMoney &amount) { static const int NR_OF_STEPS_LIMIT = 300000; static const int PROGRESSBAR_STEPS = 1000; QList > result = transactions; KMSTATUS(i18n("Running automatic reconciliation")); int progressBarIndex = 0; kmymoney->slotStatusProgressBar(progressBarIndex, NR_OF_STEPS_LIMIT / PROGRESSBAR_STEPS); // optimize the most common case - all transactions should be cleared QListIterator > itTransactionSplitResult(result); MyMoneyMoney transactionsBalance; while (itTransactionSplitResult.hasNext()) { const QPair &transactionSplit = itTransactionSplitResult.next(); transactionsBalance += transactionSplit.second.shares(); } if (amount == transactionsBalance) { result = transactions; return result; } kmymoney->slotStatusProgressBar(progressBarIndex++, 0); // only one transaction is uncleared itTransactionSplitResult.toFront(); int index = 0; while (itTransactionSplitResult.hasNext()) { const QPair &transactionSplit = itTransactionSplitResult.next(); if (transactionsBalance - transactionSplit.second.shares() == amount) { result.removeAt(index); return result; } index++; } kmymoney->slotStatusProgressBar(progressBarIndex++, 0); // more than one transaction is uncleared - apply the algorithm result.clear(); const MyMoneySecurity &security = MyMoneyFile::instance()->security(account.currencyId()); double precision = 0.1 / account.fraction(security); QList sumList; sumList << MyMoneyMoney(); QMap > > sumToComponentsMap; // compute the possible matches QListIterator > itTransactionSplit(transactions); while (itTransactionSplit.hasNext()) { const QPair &transactionSplit = itTransactionSplit.next(); QListIterator itSum(sumList); QList tempList; while (itSum.hasNext()) { const MyMoneyMoney &sum = itSum.next(); QList > splitIds; splitIds << qMakePair(transactionSplit.first.id(), transactionSplit.second.id()); if (sumToComponentsMap.contains(sum)) { if (sumToComponentsMap.value(sum).contains(qMakePair(transactionSplit.first.id(), transactionSplit.second.id()))) { continue; } splitIds.append(sumToComponentsMap.value(sum)); } tempList << transactionSplit.second.shares() + sum; sumToComponentsMap[transactionSplit.second.shares() + sum] = splitIds; int size = sumToComponentsMap.size(); if (size % PROGRESSBAR_STEPS == 0) { kmymoney->slotStatusProgressBar(progressBarIndex++, 0); } if (size > NR_OF_STEPS_LIMIT) { return result; // it's taking too much resources abort the algorithm } } QList unionList; unionList.append(tempList); unionList.append(sumList); qSort(unionList); sumList.clear(); MyMoneyMoney smallestSumFromUnion = unionList.first(); sumList.append(smallestSumFromUnion); QListIterator itUnion(unionList); while (itUnion.hasNext()) { MyMoneyMoney sumFromUnion = itUnion.next(); if (smallestSumFromUnion < MyMoneyMoney(1 - precision / transactions.size())*sumFromUnion) { smallestSumFromUnion = sumFromUnion; sumList.append(sumFromUnion); } } } kmymoney->slotStatusProgressBar(NR_OF_STEPS_LIMIT / PROGRESSBAR_STEPS, 0); if (sumToComponentsMap.contains(amount)) { QListIterator > itTransactionSplit(transactions); while (itTransactionSplit.hasNext()) { const QPair &transactionSplit = itTransactionSplit.next(); const QList > &splitIds = sumToComponentsMap.value(amount); if (splitIds.contains(qMakePair(transactionSplit.first.id(), transactionSplit.second.id()))) { result.append(transactionSplit); } } } #ifdef KMM_DEBUG qDebug("For the amount %s a number of %d possible sums where computed from the set of %d transactions: ", qPrintable(MyMoneyUtils::formatMoney(amount, security)), sumToComponentsMap.size(), transactions.size()); #endif kmymoney->slotStatusProgressBar(-1, -1); return result; } void KMyMoneyApp::slotReparentAccount(const MyMoneyAccount& _src, const MyMoneyInstitution& _dst) { MyMoneyAccount src(_src); src.setInstitutionId(_dst.id()); MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->modifyAccount(src); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::sorry(this, i18n("

%1 cannot be moved to institution %2. Reason: %3

", src.name(), _dst.name(), e.what())); } } void KMyMoneyApp::slotReparentAccount(const MyMoneyAccount& _src, const MyMoneyAccount& _dst) { MyMoneyAccount src(_src); MyMoneyAccount dst(_dst); MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->reparentAccount(src, dst); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::sorry(this, i18n("

%1 cannot be moved to %2. Reason: %3

", src.name(), dst.name(), e.what())); } } void KMyMoneyApp::slotScheduleNew(const MyMoneyTransaction& _t, eMyMoney::Schedule::Occurrence occurrence) { KEditScheduleDlg::newSchedule(_t, occurrence); } void KMyMoneyApp::slotPayeeNew(const QString& newnameBase, QString& id) { KMyMoneyUtils::newPayee(newnameBase, id); } void KMyMoneyApp::slotNewFeature() { } // move a stock transaction from one investment account to another void KMyMoneyApp::Private::moveInvestmentTransaction(const QString& /*fromId*/, const QString& toId, const MyMoneyTransaction& tx) { MyMoneyAccount toInvAcc = MyMoneyFile::instance()->account(toId); MyMoneyTransaction t(tx); // first determine which stock we are dealing with. // fortunately, investment transactions have only one stock involved QString stockAccountId; QString stockSecurityId; MyMoneySplit s; foreach (const auto split, t.splits()) { stockAccountId = split.accountId(); stockSecurityId = MyMoneyFile::instance()->account(stockAccountId).currencyId(); if (!MyMoneyFile::instance()->security(stockSecurityId).isCurrency()) { s = split; break; } } // Now check the target investment account to see if it // contains a stock with this id QString newStockAccountId; foreach (const auto sAccount, toInvAcc.accountList()) { if (MyMoneyFile::instance()->account(sAccount).currencyId() == stockSecurityId) { newStockAccountId = sAccount; break; } } // if it doesn't exist, we need to add it as a copy of the old one // no 'copyAccount()' function?? if (newStockAccountId.isEmpty()) { MyMoneyAccount stockAccount = MyMoneyFile::instance()->account(stockAccountId); MyMoneyAccount newStock; newStock.setName(stockAccount.name()); newStock.setNumber(stockAccount.number()); newStock.setDescription(stockAccount.description()); newStock.setInstitutionId(stockAccount.institutionId()); newStock.setOpeningDate(stockAccount.openingDate()); newStock.setAccountType(stockAccount.accountType()); newStock.setCurrencyId(stockAccount.currencyId()); newStock.setClosed(stockAccount.isClosed()); MyMoneyFile::instance()->addAccount(newStock, toInvAcc); newStockAccountId = newStock.id(); } // now update the split and the transaction s.setAccountId(newStockAccountId); t.modifySplit(s); MyMoneyFile::instance()->modifyTransaction(t); } void KMyMoneyApp::showContextMenu(const QString& containerName) { QWidget* w = factory()->container(containerName, this); if (auto menu = dynamic_cast(w)) menu->exec(QCursor::pos()); else qDebug("menu '%s' not found: w = %p, menu = %p", qPrintable(containerName), w, menu); } void KMyMoneyApp::slotPrintView() { d->m_myMoneyView->slotPrintView(); } void KMyMoneyApp::updateCaption(bool skipActions) { QString caption; caption = d->m_fileName.fileName(); if (caption.isEmpty() && d->m_myMoneyView && d->m_fileOpen) caption = i18n("Untitled"); // MyMoneyFile::instance()->dirty() throws an exception, if // there's no storage object available. In this case, we // assume that the storage object is not changed. Actually, // this can only happen if we are newly created early on. bool modified; try { modified = MyMoneyFile::instance()->dirty(); } catch (const MyMoneyException &) { modified = false; skipActions = true; } #ifdef KMM_DEBUG caption += QString(" (%1 x %2)").arg(width()).arg(height()); #endif setCaption(caption, modified); if (!skipActions) { d->m_myMoneyView->enableViewsIfFileOpen(d->m_fileOpen); slotUpdateActions(); } } void KMyMoneyApp::slotUpdateActions() { const auto file = MyMoneyFile::instance(); const bool fileOpen = d->m_fileOpen; const bool modified = file->dirty(); // const bool importRunning = (d->m_smtReader != 0); auto aC = actionCollection(); // ************* // Disabling actions based on conditions // ************* { QString tooltip = i18n("Create a new transaction"); const QVector> actionStates { // {qMakePair(Action::FileOpenDatabase, true)}, // {qMakePair(Action::FileSaveAsDatabase, fileOpen)}, {qMakePair(Action::FilePersonalData, fileOpen)}, {qMakePair(Action::FileBackup, (fileOpen && !isDatabase()))}, {qMakePair(Action::FileInformation, fileOpen)}, {qMakePair(Action::FileImportTemplate, fileOpen/* && !importRunning*/)}, {qMakePair(Action::FileExportTemplate, fileOpen/* && !importRunning*/)}, #ifdef KMM_DEBUG {qMakePair(Action::FileDump, fileOpen)}, #endif {qMakePair(Action::EditFindTransaction, fileOpen)}, {qMakePair(Action::ToolCurrencies, fileOpen)}, {qMakePair(Action::ToolPrices, fileOpen)}, {qMakePair(Action::ToolUpdatePrices, fileOpen)}, {qMakePair(Action::ToolConsistency, fileOpen)}, {qMakePair(Action::NewAccount, fileOpen)}, {qMakePair(Action::AccountCreditTransfer, onlineJobAdministration::instance()->canSendCreditTransfer())}, {qMakePair(Action::NewInstitution, fileOpen)}, // {qMakePair(Action::TransactionNew, (fileOpen && d->m_myMoneyView->canCreateTransactions(KMyMoneyRegister::SelectedTransactions(), tooltip)))}, {qMakePair(Action::NewSchedule, fileOpen)}, // {qMakePair(Action::CurrencyNew, fileOpen)}, // {qMakePair(Action::PriceNew, fileOpen)}, }; for (const auto& a : actionStates) pActions[a.first]->setEnabled(a.second); } // ************* // Disabling standard actions based on conditions // ************* aC->action(QString::fromLatin1(KStandardAction::name(KStandardAction::Save)))->setEnabled(modified /*&& !d->m_myMoneyView->isDatabase()*/); aC->action(QString::fromLatin1(KStandardAction::name(KStandardAction::SaveAs)))->setEnabled(fileOpen); aC->action(QString::fromLatin1(KStandardAction::name(KStandardAction::Close)))->setEnabled(fileOpen); aC->action(QString::fromLatin1(KStandardAction::name(KStandardAction::Print)))->setEnabled(fileOpen && d->m_myMoneyView->canPrint()); } void KMyMoneyApp::slotResetSelections() { d->m_myMoneyView->slotObjectSelected(MyMoneyAccount()); d->m_myMoneyView->slotObjectSelected(MyMoneyInstitution()); d->m_myMoneyView->slotObjectSelected(MyMoneySchedule()); d->m_myMoneyView->slotObjectSelected(MyMoneyTag()); d->m_myMoneyView->slotSelectByVariant(QVariantList {QVariant::fromValue(KMyMoneyRegister::SelectedTransactions())}, eView::Intent::SelectRegisterTransactions); slotUpdateActions(); } void KMyMoneyApp::slotDataChanged() { // As this method is called every time the MyMoneyFile instance // notifies a modification, it's the perfect place to start the timer if needed if (d->m_autoSaveEnabled && !d->m_autoSaveTimer->isActive()) { d->m_autoSaveTimer->setSingleShot(true); d->m_autoSaveTimer->start(d->m_autoSavePeriod * 60 * 1000); //miliseconds } updateCaption(); } void KMyMoneyApp::slotCurrencyDialog() { QPointer dlg = new KCurrencyEditDlg(this); dlg->exec(); delete dlg; } void KMyMoneyApp::slotPriceDialog() { QPointer dlg = new KMyMoneyPriceDlg(this); dlg->exec(); delete dlg; } void KMyMoneyApp::slotFileConsistencyCheck() { d->consistencyCheck(true); updateCaption(); } void KMyMoneyApp::Private::consistencyCheck(bool alwaysDisplayResult) { KMSTATUS(i18n("Running consistency check...")); MyMoneyFileTransaction ft; try { m_consistencyCheckResult = MyMoneyFile::instance()->consistencyCheck(); ft.commit(); } catch (const MyMoneyException &e) { m_consistencyCheckResult.append(i18n("Consistency check failed: %1", e.what())); // always display the result if the check failed alwaysDisplayResult = true; } // in case the consistency check was OK, we get a single line as result // in all errneous cases, we get more than one line and force the // display of them. if (alwaysDisplayResult || m_consistencyCheckResult.size() > 1) { QString msg = i18n("The consistency check has found no issues in your data. Details are presented below."); if (m_consistencyCheckResult.size() > 1) msg = i18n("The consistency check has found some issues in your data. Details are presented below. Those issues that could not be corrected automatically need to be solved by the user."); // install a context menu for the list after the dialog is displayed QTimer::singleShot(500, q, SLOT(slotInstallConsistencyCheckContextMenu())); KMessageBox::informationList(0, msg, m_consistencyCheckResult, i18n("Consistency check result")); } // this data is no longer needed m_consistencyCheckResult.clear(); } void KMyMoneyApp::Private::copyConsistencyCheckResults() { QClipboard *clipboard = QApplication::clipboard(); clipboard->setText(m_consistencyCheckResult.join(QLatin1String("\n"))); } void KMyMoneyApp::Private::saveConsistencyCheckResults() { QUrl fileUrl = QFileDialog::getSaveFileUrl(q); if (!fileUrl.isEmpty()) { QFile file(fileUrl.toLocalFile()); if (file.open(QFile::WriteOnly | QFile::Append | QFile::Text)) { QTextStream out(&file); out << m_consistencyCheckResult.join(QLatin1String("\n")); file.close(); } } } void KMyMoneyApp::Private::setThemedCSS() { const QStringList CSSnames {QStringLiteral("kmymoney.css"), QStringLiteral("welcome.css")}; const QString rcDir("/html/"); const QStringList defaultCSSDirs = QStandardPaths::locateAll(QStandardPaths::AppDataLocation, rcDir, QStandardPaths::LocateDirectory); // scan the list of directories to find the ones that really // contains all files we look for QString defaultCSSDir; foreach (const auto dir, defaultCSSDirs) { defaultCSSDir = dir; foreach (const auto CSSname, CSSnames) { QFileInfo fileInfo(defaultCSSDir + CSSname); if (!fileInfo.exists()) { defaultCSSDir.clear(); break; } } if (!defaultCSSDir.isEmpty()) { break; } } // make sure we have the local directory where the themed version is stored const QString themedCSSDir = QStandardPaths::standardLocations(QStandardPaths::AppConfigLocation).first() + rcDir; QDir().mkpath(themedCSSDir); foreach (const auto CSSname, CSSnames) { const QString defaultCSSFilename = defaultCSSDir + CSSname; QFileInfo fileInfo(defaultCSSFilename); if (fileInfo.exists()) { const QString themedCSSFilename = themedCSSDir + CSSname; QFile::remove(themedCSSFilename); if (QFile::copy(defaultCSSFilename, themedCSSFilename)) { QFile cssFile (themedCSSFilename); if (cssFile.open(QIODevice::ReadWrite)) { QTextStream cssStream(&cssFile); auto cssText = cssStream.readAll(); cssText.replace(QLatin1String("./"), defaultCSSDir, Qt::CaseSensitive); cssText.replace(QLatin1String("WindowText"), KMyMoneySettings::schemeColor(SchemeColor::WindowText).name(), Qt::CaseSensitive); cssText.replace(QLatin1String("Window"), KMyMoneySettings::schemeColor(SchemeColor::WindowBackground).name(), Qt::CaseSensitive); cssText.replace(QLatin1String("HighlightText"), KMyMoneySettings::schemeColor(SchemeColor::ListHighlightText).name(), Qt::CaseSensitive); cssText.replace(QLatin1String("Highlight"), KMyMoneySettings::schemeColor(SchemeColor::ListHighlight).name(), Qt::CaseSensitive); cssText.replace(QLatin1String("black"), KMyMoneySettings::schemeColor(SchemeColor::ListGrid).name(), Qt::CaseSensitive); cssStream.seek(0); cssStream << cssText; cssFile.close(); } } } } } void KMyMoneyApp::slotCheckSchedules() { if (KMyMoneySettings::checkSchedule() == true) { KMSTATUS(i18n("Checking for overdue scheduled transactions...")); MyMoneyFile *file = MyMoneyFile::instance(); QDate checkDate = QDate::currentDate().addDays(KMyMoneySettings::checkSchedulePreview()); QList scheduleList = file->scheduleList(); QList::Iterator it; eDialogs::ScheduleResultCode rc = eDialogs::ScheduleResultCode::Enter; for (it = scheduleList.begin(); (it != scheduleList.end()) && (rc != eDialogs::ScheduleResultCode::Cancel); ++it) { // Get the copy in the file because it might be modified by commitTransaction MyMoneySchedule schedule = file->schedule((*it).id()); if (schedule.autoEnter()) { try { while (!schedule.isFinished() && (schedule.adjustedNextDueDate() <= checkDate) && rc != eDialogs::ScheduleResultCode::Ignore && rc != eDialogs::ScheduleResultCode::Cancel) { rc = d->m_myMoneyView->enterSchedule(schedule, true, true); schedule = file->schedule((*it).id()); // get a copy of the modified schedule } } catch (const MyMoneyException &) { } } if (rc == eDialogs::ScheduleResultCode::Ignore) { // if the current schedule was ignored then we must make sure that the user can still enter the next scheduled transaction rc = eDialogs::ScheduleResultCode::Enter; } } updateCaption(); } } void KMyMoneyApp::writeLastUsedDir(const QString& directory) { //get global config object for our app. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); if (kconfig) { KConfigGroup grp = kconfig->group("General Options"); //write path entry, no error handling since its void. grp.writeEntry("LastUsedDirectory", directory); } } void KMyMoneyApp::writeLastUsedFile(const QString& fileName) { //get global config object for our app. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); if (kconfig) { KConfigGroup grp = d->m_config->group("General Options"); // write path entry, no error handling since its void. // use a standard string, as fileName could contain a protocol // e.g. file:/home/thb/.... grp.writeEntry("LastUsedFile", fileName); } } QString KMyMoneyApp::readLastUsedDir() const { QString str; //get global config object for our app. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); if (kconfig) { KConfigGroup grp = d->m_config->group("General Options"); //read path entry. Second parameter is the default if the setting is not found, which will be the default document path. str = grp.readEntry("LastUsedDirectory", QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)); // if the path stored is empty, we use the default nevertheless if (str.isEmpty()) str = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); } return str; } QString KMyMoneyApp::readLastUsedFile() const { QString str; // get global config object for our app. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); if (kconfig) { KConfigGroup grp = d->m_config->group("General Options"); // read filename entry. str = grp.readEntry("LastUsedFile", ""); } return str; } QString KMyMoneyApp::filename() const { return d->m_fileName.url(); } QUrl KMyMoneyApp::filenameURL() const { return d->m_fileName; } void KMyMoneyApp::addToRecentFiles(const QUrl& url) { d->m_recentFiles->addUrl(url); } QTimer* KMyMoneyApp::autosaveTimer() { return d->m_autoSaveTimer; } WebConnect* KMyMoneyApp::webConnect() const { return d->m_webConnect; } QList KMyMoneyApp::instanceList() const { QList list; #ifdef KMM_DBUS QDBusReply reply = QDBusConnection::sessionBus().interface()->registeredServiceNames(); if (reply.isValid()) { QStringList apps = reply.value(); QStringList::ConstIterator it; // build a list of service names of all running kmymoney applications without this one for (it = apps.constBegin(); it != apps.constEnd(); ++it) { // please change this method of creating a list of 'all the other kmymoney instances that are running on the system' // since assuming that D-Bus creates service names with org.kde.kmymoney-PID is an observation I don't think that it's documented somwhere if ((*it).indexOf("org.kde.kmymoney-") == 0) { uint thisProcPid = platformTools::processId(); if ((*it).indexOf(QString("org.kde.kmymoney-%1").arg(thisProcPid)) != 0) list += (*it); } } } else { qDebug("D-Bus returned the following error while obtaining instances: %s", qPrintable(reply.error().message())); } #endif return list; } void KMyMoneyApp::slotEquityPriceUpdate() { QPointer dlg = new KEquityPriceUpdateDlg(this); if (dlg->exec() == QDialog::Accepted && dlg != 0) dlg->storePrices(); delete dlg; } void KMyMoneyApp::webConnect(const QString& sourceUrl, const QByteArray& asn_id) { // // Web connect attempts to go through the known importers and see if the file // can be importing using that method. If so, it will import it using that // plugin // Q_UNUSED(asn_id) d->m_importUrlsQueue.enqueue(sourceUrl); // only start processing if this is the only import so far if (d->m_importUrlsQueue.count() == 1) { while (!d->m_importUrlsQueue.isEmpty()) { // get the value of the next item from the queue // but leave it on the queue for now QString url = d->m_importUrlsQueue.head(); // Bring this window to the forefront. This method was suggested by // Lubos Lunak of the KDE core development team. // TODO: port KF5 (WebConnect) //KStartupInfo::setNewStartupId(this, asn_id); // Make sure we have an open file if (! d->m_fileOpen && KMessageBox::warningContinueCancel(kmymoney, i18n("You must first select a KMyMoney file before you can import a statement.")) == KMessageBox::Continue) kmymoney->slotFileOpen(); // only continue if the user really did open a file. if (d->m_fileOpen) { KMSTATUS(i18n("Importing a statement via Web Connect")); // remove the statement files d->unlinkStatementXML(); QMap::const_iterator it_plugin = d->m_plugins.importer.constBegin(); while (it_plugin != d->m_plugins.importer.constEnd()) { if ((*it_plugin)->isMyFormat(url)) { QList statements; if (!(*it_plugin)->import(url)) { KMessageBox::error(this, i18n("Unable to import %1 using %2 plugin. The plugin returned the following error: %3", url, (*it_plugin)->formatName(), (*it_plugin)->lastError()), i18n("Importing error")); } break; } ++it_plugin; } // If we did not find a match, try importing it as a KMM statement file, // which is really just for testing. the statement file is not exposed // to users. if (it_plugin == d->m_plugins.importer.constEnd()) if (MyMoneyStatement::isStatementFile(url)) MyMoneyStatementReader::importStatement(url, false, &progressCallback); } // remove the current processed item from the queue d->m_importUrlsQueue.dequeue(); } } } void KMyMoneyApp::slotEnableMessages() { KMessageBox::enableAllMessages(); KMessageBox::information(this, i18n("All messages have been enabled."), i18n("All messages")); } void KMyMoneyApp::createInterfaces() { // Sets up the plugin interface KMyMoneyPlugin::pluginInterfaces().appInterface = new KMyMoneyPlugin::KMMAppInterface(this, this); KMyMoneyPlugin::pluginInterfaces().importInterface = new KMyMoneyPlugin::KMMImportInterface(this); KMyMoneyPlugin::pluginInterfaces().statementInterface = new KMyMoneyPlugin::KMMStatementInterface(this); KMyMoneyPlugin::pluginInterfaces().viewInterface = new KMyMoneyPlugin::KMMViewInterface(d->m_myMoneyView, this); // setup the calendar interface for schedules MyMoneySchedule::setProcessingCalendar(this); } void KMyMoneyApp::slotAutoSave() { if (!d->m_inAutoSaving) { // store the focus widget so we can restore it after save QPointer focusWidget = qApp->focusWidget(); d->m_inAutoSaving = true; KMSTATUS(i18n("Auto saving...")); //calls slotFileSave if needed, and restart the timer //it the file is not saved, reinitializes the countdown. if (d->dirty() && d->m_autoSaveEnabled) { if (!slotFileSave() && d->m_autoSavePeriod > 0) { d->m_autoSaveTimer->setSingleShot(true); d->m_autoSaveTimer->start(d->m_autoSavePeriod * 60 * 1000); } } d->m_inAutoSaving = false; if (focusWidget && focusWidget != qApp->focusWidget()) { // we have a valid focus widget so restore it focusWidget->setFocus(); } } } void KMyMoneyApp::slotDateChanged() { QDateTime dt = QDateTime::currentDateTime(); QDateTime nextDay(QDate(dt.date().addDays(1)), QTime(0, 0, 0)); // +1 is to make sure that we're already in the next day when the // signal is sent (this way we also avoid setting the timer to 0) QTimer::singleShot((dt.secsTo(nextDay) + 1)*1000, this, SLOT(slotDateChanged())); d->m_myMoneyView->slotRefreshViews(); } void KMyMoneyApp::setHolidayRegion(const QString& holidayRegion) { #ifdef KF5Holidays_FOUND //since the cost of updating the cache is now not negligible //check whether the region has been modified if (!d->m_holidayRegion || d->m_holidayRegion->regionCode() != holidayRegion) { // Delete the previous holidayRegion before creating a new one. delete d->m_holidayRegion; // Create a new holidayRegion. d->m_holidayRegion = new KHolidays::HolidayRegion(holidayRegion); //clear and update the holiday cache preloadHolidays(); } #else Q_UNUSED(holidayRegion); #endif } bool KMyMoneyApp::isProcessingDate(const QDate& date) const { #ifdef KF5Holidays_FOUND if (!d->m_processingDays.testBit(date.dayOfWeek())) return false; if (!d->m_holidayRegion || !d->m_holidayRegion->isValid()) return true; //check first whether it's already in cache if (d->m_holidayMap.contains(date)) { return d->m_holidayMap.value(date, true); } else { bool processingDay = !d->m_holidayRegion->isHoliday(date); d->m_holidayMap.insert(date, processingDay); return processingDay; } #else Q_UNUSED(date); return true; #endif } void KMyMoneyApp::preloadHolidays() { #ifdef KF5Holidays_FOUND //clear the cache before loading d->m_holidayMap.clear(); //only do this if it is a valid region if (d->m_holidayRegion && d->m_holidayRegion->isValid()) { //load holidays for the forecast days plus 1 cycle, to be on the safe side int forecastDays = KMyMoneySettings::forecastDays() + KMyMoneySettings::forecastAccountCycle(); QDate endDate = QDate::currentDate().addDays(forecastDays); //look for holidays for the next 2 years as a minimum. That should give a good margin for the cache if (endDate < QDate::currentDate().addYears(2)) endDate = QDate::currentDate().addYears(2); KHolidays::Holiday::List holidayList = d->m_holidayRegion->holidays(QDate::currentDate(), endDate); KHolidays::Holiday::List::const_iterator holiday_it; for (holiday_it = holidayList.constBegin(); holiday_it != holidayList.constEnd(); ++holiday_it) { for (QDate holidayDate = (*holiday_it).observedStartDate(); holidayDate <= (*holiday_it).observedEndDate(); holidayDate = holidayDate.addDays(1)) d->m_holidayMap.insert(holidayDate, false); } for (QDate date = QDate::currentDate(); date <= endDate; date = date.addDays(1)) { //if it is not a processing day, set it to false if (!d->m_processingDays.testBit(date.dayOfWeek())) { d->m_holidayMap.insert(date, false); } else if (!d->m_holidayMap.contains(date)) { //if it is not a holiday nor a weekend, it is a processing day d->m_holidayMap.insert(date, true); } } } #endif } KMStatus::KMStatus(const QString &text) { m_prevText = kmymoney->slotStatusMsg(text); } KMStatus::~KMStatus() { kmymoney->slotStatusMsg(m_prevText); } void KMyMoneyApp::Private::unlinkStatementXML() { QDir d(KMyMoneySettings::logPath(), "kmm-statement*"); for (uint i = 0; i < d.count(); ++i) { qDebug("Remove %s", qPrintable(d[i])); d.remove(KMyMoneySettings::logPath() + QString("/%1").arg(d[i])); } m_statementXMLindex = 0; } void KMyMoneyApp::Private::closeFile() { m_myMoneyView->slotObjectSelected(MyMoneyAccount()); m_myMoneyView->slotObjectSelected(MyMoneyInstitution()); m_myMoneyView->slotObjectSelected(MyMoneySchedule()); m_myMoneyView->slotObjectSelected(MyMoneyTag()); m_myMoneyView->slotSelectByVariant(QVariantList {QVariant::fromValue(KMyMoneyRegister::SelectedTransactions())}, eView::Intent::SelectRegisterTransactions); m_myMoneyView->finishReconciliation(MyMoneyAccount()); m_myMoneyView->slotFileClosed(); disconnectStorageFromModels(); // notify the models that the file is going to be closed (we should have something like dataChanged that reaches the models first) Models::instance()->fileClosed(); emit q->kmmFilePlugin(KMyMoneyApp::preClose); if (q->isDatabase()) MyMoneyFile::instance()->storage()->close(); // to log off a database user newStorage(); emit q->kmmFilePlugin(postClose); m_fileOpen = false; m_fileName = QUrl(); q->updateCaption(); // just create a new balance warning object delete m_balanceWarning; m_balanceWarning = new KBalanceWarning(q); emit q->fileLoaded(m_fileName); } diff --git a/kmymoney/misc/platformtools_nognu.cpp b/kmymoney/misc/platformtools_nognu.cpp index 4f6a38050..148bd8c38 100644 --- a/kmymoney/misc/platformtools_nognu.cpp +++ b/kmymoney/misc/platformtools_nognu.cpp @@ -1,46 +1,47 @@ /* * This file is part of KMyMoney, A Personal Finance Manager by KDE * Copyright (C) 2017 Marc Hübner * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "platformtools.h" #include #include #include #include QString platformTools::osUsername() { QString name; DWORD size = UNLEN+1; wchar_t wcname[UNLEN+1]; if(GetUserNameW((LPWSTR) wcname, &size)) { name = QString::fromWCharArray(wcname); } return name; } uint platformTools::processId() { return _getpid(); } platformTools::currencySymbolPosition_t platformTools::currencySymbolPosition(bool negativeValues) { + Q_UNUSED(negativeValues) return platformTools::AfterQuantityMoneyWithSpace; } diff --git a/kmymoney/models/onlinejobmessagesmodel.cpp b/kmymoney/models/onlinejobmessagesmodel.cpp index 45bc4c578..a380f319c 100644 --- a/kmymoney/models/onlinejobmessagesmodel.cpp +++ b/kmymoney/models/onlinejobmessagesmodel.cpp @@ -1,127 +1,129 @@ /* * This file is part of KMyMoney, A Personal Finance Manager by KDE * Copyright (C) 2015 Christian Dávid * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "onlinejobmessagesmodel.h" #include #include #include #include "mymoneyenums.h" #include "icons/icons.h" using namespace Icons; onlineJobMessagesModel::onlineJobMessagesModel(QObject* parent) : QAbstractTableModel(parent), m_job() { } QVariant onlineJobMessagesModel::data(const QModelIndex& index, int role) const { const QList messages = m_job.jobMessageList(); if (index.row() >= messages.count()) return QVariant(); switch (index.column()) { case 0: switch (role) { // Status/Date column case Qt::DisplayRole: return messages[index.row()].timestamp(); case Qt::DecorationRole: switch (messages[index.row()].type()) { case eMyMoney::OnlineJob::MessageType::Debug: case eMyMoney::OnlineJob::MessageType::Log: case eMyMoney::OnlineJob::MessageType::Information: return Icons::get(Icon::DialogInformation); case eMyMoney::OnlineJob::MessageType::Warning: return Icons::get(Icon::DialogWarning); case eMyMoney::OnlineJob::MessageType::Error: return Icons::get(Icon::DialogError); break; } + break; case Qt::ToolTipRole: switch (messages[index.row()].type()) { case eMyMoney::OnlineJob::MessageType::Debug: return i18n("Information to find issues."); case eMyMoney::OnlineJob::MessageType::Log: return i18n("Information stored for provability."); case eMyMoney::OnlineJob::MessageType::Information: return i18n("Informative message without certain significance."); case eMyMoney::OnlineJob::MessageType::Warning: return i18n("Warning message."); case eMyMoney::OnlineJob::MessageType::Error: return i18n("Error"); break; } + break; default: return QVariant(); } case 1: switch (role) { // Origin column case Qt::DisplayRole: return messages[index.row()].sender(); default: return QVariant(); } case 2: switch (role) { // Message column case Qt::DisplayRole: return messages[index.row()].message(); default: return QVariant(); } } // Actually we should never get here. But let's make this model bullet proof. return QVariant(); } int onlineJobMessagesModel::columnCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; return 3; } int onlineJobMessagesModel::rowCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; return m_job.jobMessageList().count(); } QModelIndex onlineJobMessagesModel::parent(const QModelIndex&) const { return QModelIndex(); } QVariant onlineJobMessagesModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal) { switch (section) { case 0: switch (role) { case Qt::DisplayRole: return i18n("Date"); default: return QVariant(); } case 1: switch (role) { case Qt::DisplayRole: return i18n("Origin"); default: return QVariant(); } case 2: switch (role) { case Qt::DisplayRole: return i18n("Description"); default: return QVariant(); } } } return QVariant(); } void onlineJobMessagesModel::setOnlineJob(const onlineJob& job) { beginResetModel(); m_job = job; endResetModel(); } diff --git a/kmymoney/mymoney/mymoneyfile.cpp b/kmymoney/mymoney/mymoneyfile.cpp index 451db0718..09da4bf14 100644 --- a/kmymoney/mymoney/mymoneyfile.cpp +++ b/kmymoney/mymoney/mymoneyfile.cpp @@ -1,3465 +1,3470 @@ /*************************************************************************** mymoneyfile.cpp ------------------- copyright : (C) 2000 by Michael Edwardes (C) 2002, 2007-2011 by Thomas Baumgart (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 "mymoneyfile.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include +#include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneystoragemgr.h" #include "mymoneyinstitution.h" #include "mymoneyaccount.h" #include "mymoneyaccountloan.h" #include "mymoneysecurity.h" #include "mymoneyreport.h" #include "mymoneybalancecache.h" #include "mymoneybudget.h" #include "mymoneyprice.h" #include "mymoneypayee.h" #include "mymoneytag.h" #include "mymoneyschedule.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneycostcenter.h" #include "mymoneyexception.h" #include "onlinejob.h" #include "storageenums.h" #include "mymoneystoragenames.h" #include "mymoneyenums.h" // include the following line to get a 'cout' for debug purposes // #include using namespace eMyMoney; using namespace MyMoneyStandardAccounts; const QString MyMoneyFile::AccountSeparator = QChar(':'); MyMoneyFile MyMoneyFile::file; typedef QList > BalanceNotifyList; typedef QMap CacheNotifyList; /// @todo make this template based class MyMoneyNotification { public: MyMoneyNotification(File::Mode mode, const MyMoneyTransaction& t) : m_objType(File::Object::Transaction), m_notificationMode(mode), m_id(t.id()) { } MyMoneyNotification(File::Mode mode, const MyMoneyAccount& acc) : m_objType(File::Object::Account), m_notificationMode(mode), m_id(acc.id()) { } MyMoneyNotification(File::Mode mode, const MyMoneyInstitution& institution) : m_objType(File::Object::Institution), m_notificationMode(mode), m_id(institution.id()) { } MyMoneyNotification(File::Mode mode, const MyMoneyPayee& payee) : m_objType(File::Object::Payee), m_notificationMode(mode), m_id(payee.id()) { } MyMoneyNotification(File::Mode mode, const MyMoneyTag& tag) : m_objType(File::Object::Tag), m_notificationMode(mode), m_id(tag.id()) { } MyMoneyNotification(File::Mode mode, const MyMoneySchedule& schedule) : m_objType(File::Object::Schedule), m_notificationMode(mode), m_id(schedule.id()) { } MyMoneyNotification(File::Mode mode, const MyMoneySecurity& security) : m_objType(File::Object::Security), m_notificationMode(mode), m_id(security.id()) { } MyMoneyNotification(File::Mode mode, const onlineJob& job) : m_objType(File::Object::OnlineJob), m_notificationMode(mode), m_id(job.id()) { } File::Object objectType() const { return m_objType; } File::Mode notificationMode() const { return m_notificationMode; } const QString& id() const { return m_id; } protected: MyMoneyNotification(File::Object obj, File::Mode mode, const QString& id) : m_objType(obj), m_notificationMode(mode), m_id(id) {} private: File::Object m_objType; File::Mode m_notificationMode; QString m_id; }; class MyMoneyFile::Private { public: Private() : m_storage(0), m_inTransaction(false) {} ~Private() { delete m_storage; } /** * This method is used to add an id to the list of objects * to be removed from the cache. If id is empty, then nothing is added to the list. * * @param id id of object to be notified * @param reload reload the object (@c true) or not (@c false). The default is @c true * @see attach, detach */ void addCacheNotification(const QString& id, const QDate& date) { if (!id.isEmpty()) m_balanceNotifyList.append(std::make_pair(id, date)); } /** * This method is used to clear the notification list */ void clearCacheNotification() { // reset list to be empty m_balanceNotifyList.clear(); } /** * This method is used to clear all * objects mentioned in m_notificationList from the cache. */ void notify() { foreach (const BalanceNotifyList::value_type & i, m_balanceNotifyList) { m_balanceChangedSet += i.first; if (i.second.isValid()) { m_balanceCache.clear(i.first, i.second); } else { m_balanceCache.clear(i.first); } } clearCacheNotification(); } /** * This method checks if a storage object is attached and * throws and exception if not. */ inline void checkStorage() const { if (m_storage == 0) throw MYMONEYEXCEPTION("No storage object attached to MyMoneyFile"); } /** * This method checks that a transaction has been started with * startTransaction() and throws an exception otherwise. Calls * checkStorage() to make sure a storage object is present and attached. */ void checkTransaction(const char* txt) const { checkStorage(); if (!m_inTransaction) { throw MYMONEYEXCEPTION(QString("No transaction started for %1").arg(txt)); } } void priceChanged(const MyMoneyFile& file, const MyMoneyPrice price) { // get all affected accounts and add them to the m_valueChangedSet QList accList; file.accountList(accList); QList::const_iterator account_it; for (account_it = accList.constBegin(); account_it != accList.constEnd(); ++account_it) { QString currencyId = account_it->currencyId(); if (currencyId != file.baseCurrency().id() && (currencyId == price.from() || currencyId == price.to())) { // this account is not in the base currency and the price affects it's value m_valueChangedSet.insert(account_it->id()); } } } /** * This member points to the storage strategy */ MyMoneyStorageMgr *m_storage; bool m_inTransaction; MyMoneySecurity m_baseCurrency; /** * @brief Cache for MyMoneyObjects * * It is also used to emit the objectAdded() and objectModified() signals. * => If one of these signals is used, you must use this cache. */ MyMoneyPriceList m_priceCache; MyMoneyBalanceCache m_balanceCache; /** * This member keeps a list of account ids to notify * after a single operation is completed. The balance cache * is cleared for that account and all dates on or after * the one supplied. If the date is invalid, the entire * balance cache is cleared for that account. */ BalanceNotifyList m_balanceNotifyList; /** * This member keeps a list of account ids for which * a balanceChanged() signal needs to be emitted when * a set of operations has been committed. * * @sa MyMoneyFile::commitTransaction() */ QSet m_balanceChangedSet; /** * This member keeps a list of account ids for which * a valueChanged() signal needs to be emitted when * a set of operations has been committed. * * @sa MyMoneyFile::commitTransaction() */ QSet m_valueChangedSet; /** * This member keeps the list of changes in the engine * in historical order. The type can be 'added', 'modified' * or removed. */ QList m_changeSet; }; class MyMoneyNotifier { public: MyMoneyNotifier(MyMoneyFile::Private* file) { m_file = file; m_file->clearCacheNotification(); } ~MyMoneyNotifier() { m_file->notify(); } private: MyMoneyFile::Private* m_file; }; MyMoneyFile::MyMoneyFile() : d(new Private) { } MyMoneyFile::~MyMoneyFile() { delete d; } MyMoneyFile::MyMoneyFile(MyMoneyStorageMgr *storage) : d(new Private) { attachStorage(storage); } MyMoneyFile* MyMoneyFile::instance() { return &file; } void MyMoneyFile::attachStorage(MyMoneyStorageMgr* const storage) { if (d->m_storage != 0) throw MYMONEYEXCEPTION("Storage already attached"); if (storage == 0) throw MYMONEYEXCEPTION("Storage must not be 0"); d->m_storage = storage; // force reload of base currency d->m_baseCurrency = MyMoneySecurity(); // and the whole cache d->m_balanceCache.clear(); d->m_priceCache.clear(); // notify application about new data availability emit beginChangeNotification(); emit dataChanged(); emit endChangeNotification(); } void MyMoneyFile::detachStorage(MyMoneyStorageMgr* const /* storage */) { d->m_balanceCache.clear(); d->m_priceCache.clear(); d->m_storage = nullptr; } MyMoneyStorageMgr* MyMoneyFile::storage() const { return d->m_storage; } bool MyMoneyFile::storageAttached() const { return d->m_storage != 0; } void MyMoneyFile::startTransaction() { d->checkStorage(); if (d->m_inTransaction) { throw MYMONEYEXCEPTION("Already started a transaction!"); } d->m_storage->startTransaction(); d->m_inTransaction = true; d->m_changeSet.clear(); } bool MyMoneyFile::hasTransaction() const { return d->m_inTransaction; } void MyMoneyFile::commitTransaction() { d->checkTransaction(Q_FUNC_INFO); // commit the transaction in the storage const auto changed = d->m_storage->commitTransaction(); d->m_inTransaction = false; // inform the outside world about the beginning of notifications emit beginChangeNotification(); // Now it's time to send out some signals to the outside world // First we go through the d->m_changeSet and emit respective // signals about addition, modification and removal of engine objects const auto& changes = d->m_changeSet; for (const auto& change : changes) { switch (change.notificationMode()) { case File::Mode::Remove: emit objectRemoved(change.objectType(), change.id()); // if there is a balance change recorded for this account remove it since the account itself will be removed // this can happen when deleting categories that have transactions and the reassign category feature was used d->m_balanceChangedSet.remove(change.id()); break; case File::Mode::Add: emit objectAdded(change.objectType(), change.id()); break; case File::Mode::Modify: emit objectModified(change.objectType(), change.id()); break; } } // we're done with the change set, so we clear it d->m_changeSet.clear(); // now send out the balanceChanged signal for all those // accounts for which we have an indication about a possible // change. const auto& balanceChanges = d->m_balanceChangedSet; for (const auto& id : balanceChanges) { // if we notify about balance change we don't need to notify about value change // for the same account since a balance change implies a value change d->m_valueChangedSet.remove(id); emit balanceChanged(account(id)); } d->m_balanceChangedSet.clear(); // now notify about the remaining value changes const auto& m_valueChanges = d->m_valueChangedSet; for (const auto& id : m_valueChanges) emit valueChanged(account(id)); d->m_valueChangedSet.clear(); // as a last action, send out the global dataChanged signal if (changed) emit dataChanged(); // inform the outside world about the end of notifications emit endChangeNotification(); } void MyMoneyFile::rollbackTransaction() { d->checkTransaction(Q_FUNC_INFO); d->m_storage->rollbackTransaction(); d->m_inTransaction = false; d->m_balanceChangedSet.clear(); d->m_valueChangedSet.clear(); d->m_changeSet.clear(); } void MyMoneyFile::addInstitution(MyMoneyInstitution& institution) { // perform some checks to see that the institution stuff is OK. For // now we assume that the institution must have a name, the ID is not set // and it does not have a parent (MyMoneyFile). if (institution.name().length() == 0 || institution.id().length() != 0) throw MYMONEYEXCEPTION("Not a new institution"); d->checkTransaction(Q_FUNC_INFO); d->m_storage->addInstitution(institution); d->m_changeSet += MyMoneyNotification(File::Mode::Add, institution); } void MyMoneyFile::modifyInstitution(const MyMoneyInstitution& institution) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->modifyInstitution(institution); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, institution); } void MyMoneyFile::modifyTransaction(const MyMoneyTransaction& transaction) { d->checkTransaction(Q_FUNC_INFO); MyMoneyTransaction tCopy(transaction); // now check the splits bool loanAccountAffected = false; const auto splits1 = transaction.splits(); for (const auto& split : splits1) { // the following line will throw an exception if the // account does not exist auto acc = MyMoneyFile::account(split.accountId()); if (acc.id().isEmpty()) throw MYMONEYEXCEPTION("Cannot store split with no account assigned"); if (isStandardAccount(split.accountId())) throw MYMONEYEXCEPTION("Cannot store split referencing standard account"); if (acc.isLoan() && (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer))) loanAccountAffected = true; } // change transfer splits between asset/liability and loan accounts // into amortization splits if (loanAccountAffected) { const auto splits = transaction.splits(); for (const auto& split : splits) { if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer)) { auto acc = MyMoneyFile::account(split.accountId()); if (acc.isAssetLiability()) { MyMoneySplit s = split; s.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Amortization)); tCopy.modifySplit(s); } } } } // clear all changed objects from cache MyMoneyNotifier notifier(d); // get the current setting of this transaction MyMoneyTransaction tr = MyMoneyFile::transaction(transaction.id()); // scan the splits again to update notification list // and mark all accounts that are referenced const auto splits2 = tr.splits(); foreach (const auto& split, splits2) d->addCacheNotification(split.accountId(), tr.postDate()); // make sure the value is rounded to the accounts precision fixSplitPrecision(tCopy); // perform modification d->m_storage->modifyTransaction(tCopy); // and mark all accounts that are referenced const auto splits3 = tCopy.splits(); for (const auto& split : splits3) d->addCacheNotification(split.accountId(), tCopy.postDate()); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, transaction); } void MyMoneyFile::modifyAccount(const MyMoneyAccount& _account) { d->checkTransaction(Q_FUNC_INFO); MyMoneyAccount account(_account); auto acc = MyMoneyFile::account(account.id()); // check that for standard accounts only specific parameters are changed if (isStandardAccount(account.id())) { // make sure to use the stuff we found on file account = acc; // and only use the changes that are allowed account.setName(_account.name()); account.setCurrencyId(_account.currencyId()); // now check that it is the same if (!(account == _account)) throw MYMONEYEXCEPTION("Unable to modify the standard account groups"); } if (account.accountType() != acc.accountType() && !account.isLiquidAsset() && !acc.isLiquidAsset()) throw MYMONEYEXCEPTION("Unable to change account type"); // if the account was moved to another institution, we notify // the old one as well as the new one and the structure change if (acc.institutionId() != account.institutionId()) { MyMoneyInstitution inst; if (!acc.institutionId().isEmpty()) { inst = institution(acc.institutionId()); inst.removeAccountId(acc.id()); modifyInstitution(inst); // modifyInstitution updates d->m_changeSet already } if (!account.institutionId().isEmpty()) { inst = institution(account.institutionId()); inst.addAccountId(acc.id()); modifyInstitution(inst); // modifyInstitution updates d->m_changeSet already } } d->m_storage->modifyAccount(account); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, account); } void MyMoneyFile::reparentAccount(MyMoneyAccount &acc, MyMoneyAccount& parent) { d->checkTransaction(Q_FUNC_INFO); // check that it's not one of the standard account groups if (isStandardAccount(acc.id())) throw MYMONEYEXCEPTION("Unable to reparent the standard account groups"); if (acc.accountGroup() == parent.accountGroup() || (acc.accountType() == Account::Type::Income && parent.accountType() == Account::Type::Expense) || (acc.accountType() == Account::Type::Expense && parent.accountType() == Account::Type::Income)) { if (acc.isInvest() && parent.accountType() != Account::Type::Investment) throw MYMONEYEXCEPTION("Unable to reparent Stock to non-investment account"); if (parent.accountType() == Account::Type::Investment && !acc.isInvest()) throw MYMONEYEXCEPTION("Unable to reparent non-stock to investment account"); // keep a notification of the current parent MyMoneyAccount curParent = account(acc.parentAccountId()); d->m_storage->reparentAccount(acc, parent); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, curParent); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, parent); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, acc); } else throw MYMONEYEXCEPTION("Unable to reparent to different account type"); } MyMoneyInstitution MyMoneyFile::institution(const QString& id) const { return d->m_storage->institution(id); } MyMoneyAccount MyMoneyFile::account(const QString& id) const { if (Q_UNLIKELY(id.isEmpty())) // FIXME: Stop requesting accounts with empty id return MyMoneyAccount(); return d->m_storage->account(id); } MyMoneyAccount MyMoneyFile::subAccountByName(const MyMoneyAccount& acc, const QString& name) const { static MyMoneyAccount nullAccount; const auto accounts = acc.accountList(); for (const auto& acc : accounts) { const auto sacc = account(acc); if (sacc.name().compare(name) == 0) return sacc; } return nullAccount; } MyMoneyAccount MyMoneyFile::accountByName(const QString& name) const { try { return d->m_storage->accountByName(name); } catch(const MyMoneyException&) { } return MyMoneyAccount(); } void MyMoneyFile::removeTransaction(const MyMoneyTransaction& transaction) { d->checkTransaction(Q_FUNC_INFO); // clear all changed objects from cache MyMoneyNotifier notifier(d); // get the engine's idea about this transaction MyMoneyTransaction tr = MyMoneyFile::transaction(transaction.id()); // scan the splits again to update notification list const auto splits = tr.splits(); for (const auto& split : splits) { auto acc = account(split.accountId()); if (acc.isClosed()) throw MYMONEYEXCEPTION(i18n("Cannot remove transaction that references a closed account.")); d->addCacheNotification(split.accountId(), tr.postDate()); //FIXME-ALEX Do I need to add d->addCacheNotification(split.tagList()); ?? } d->m_storage->removeTransaction(transaction); // remove a possible notification of that same object from the changeSet QList::iterator it; for(it = d->m_changeSet.begin(); it != d->m_changeSet.end();) { if((*it).id() == transaction.id()) { it = d->m_changeSet.erase(it); } else { ++it; } } d->m_changeSet += MyMoneyNotification(File::Mode::Remove, transaction); } bool MyMoneyFile::hasActiveSplits(const QString& id) const { d->checkStorage(); return d->m_storage->hasActiveSplits(id); } bool MyMoneyFile::isStandardAccount(const QString& id) const { d->checkStorage(); return d->m_storage->isStandardAccount(id); } void MyMoneyFile::setAccountName(const QString& id, const QString& name) const { d->checkTransaction(Q_FUNC_INFO); auto acc = account(id); d->m_storage->setAccountName(id, name); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, acc); } void MyMoneyFile::removeAccount(const MyMoneyAccount& account) { d->checkTransaction(Q_FUNC_INFO); MyMoneyAccount parent; MyMoneyAccount acc; MyMoneyInstitution institution; // check that the account and its parent exist // this will throw an exception if the id is unknown acc = MyMoneyFile::account(account.id()); parent = MyMoneyFile::account(account.parentAccountId()); if (!acc.institutionId().isEmpty()) institution = MyMoneyFile::institution(acc.institutionId()); // check that it's not one of the standard account groups if (isStandardAccount(account.id())) throw MYMONEYEXCEPTION("Unable to remove the standard account groups"); if (hasActiveSplits(account.id())) { throw MYMONEYEXCEPTION("Unable to remove account with active splits"); } // collect all sub-ordinate accounts for notification const auto accounts = acc.accountList(); for (const auto& id : accounts) d->m_changeSet += MyMoneyNotification(File::Mode::Modify, MyMoneyFile::account(id)); // don't forget the parent and a possible institution if (!institution.id().isEmpty()) { institution.removeAccountId(account.id()); d->m_storage->modifyInstitution(institution); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, institution); } acc.setInstitutionId(QString()); d->m_storage->removeAccount(acc); d->m_balanceCache.clear(acc.id()); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, parent); d->m_changeSet += MyMoneyNotification(File::Mode::Remove, acc); } void MyMoneyFile::removeAccountList(const QStringList& account_list, unsigned int level) { if (level > 100) throw MYMONEYEXCEPTION("Too deep recursion in [MyMoneyFile::removeAccountList]!"); d->checkTransaction(Q_FUNC_INFO); // upon entry, we check that we could proceed with the operation if (!level) { if (!hasOnlyUnusedAccounts(account_list, 0)) { throw MYMONEYEXCEPTION("One or more accounts cannot be removed"); } } // process all accounts in the list and test if they have transactions assigned foreach (const auto sAccount, account_list) { auto a = d->m_storage->account(sAccount); //qDebug() << "Deleting account '"<< a.name() << "'"; // first remove all sub-accounts if (!a.accountList().isEmpty()) { removeAccountList(a.accountList(), level + 1); // then remove account itself, but we first have to get // rid of the account list that is still stored in // the MyMoneyAccount object. Easiest way is to get a fresh copy. a = d->m_storage->account(sAccount); } // make sure to remove the item from the cache removeAccount(a); } } bool MyMoneyFile::hasOnlyUnusedAccounts(const QStringList& account_list, unsigned int level) { if (level > 100) throw MYMONEYEXCEPTION("Too deep recursion in [MyMoneyFile::hasOnlyUnusedAccounts]!"); // process all accounts in the list and test if they have transactions assigned for (const auto& sAccount : account_list) { if (transactionCount(sAccount) != 0) return false; // the current account has a transaction assigned if (!hasOnlyUnusedAccounts(account(sAccount).accountList(), level + 1)) return false; // some sub-account has a transaction assigned } return true; // all subaccounts unused } void MyMoneyFile::removeInstitution(const MyMoneyInstitution& institution) { d->checkTransaction(Q_FUNC_INFO); MyMoneyInstitution inst = MyMoneyFile::institution(institution.id()); bool blocked = signalsBlocked(); blockSignals(true); const auto accounts = inst.accountList(); for (const auto& acc : accounts) { auto a = account(acc); a.setInstitutionId(QString()); modifyAccount(a); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, a); } blockSignals(blocked); d->m_storage->removeInstitution(institution); d->m_changeSet += MyMoneyNotification(File::Mode::Remove, institution); } void MyMoneyFile::createAccount(MyMoneyAccount& newAccount, MyMoneyAccount& parentAccount, MyMoneyAccount& brokerageAccount, MyMoneyMoney openingBal) { // make sure we have a currency. If none is assigned, we assume base currency if (newAccount.currencyId().isEmpty()) newAccount.setCurrencyId(baseCurrency().id()); MyMoneyFileTransaction ft; try { int pos; // check for ':' in the name and use it as separator for a hierarchy while ((pos = newAccount.name().indexOf(MyMoneyFile::AccountSeparator)) != -1) { QString part = newAccount.name().left(pos); QString remainder = newAccount.name().mid(pos + 1); const MyMoneyAccount& existingAccount = subAccountByName(parentAccount, part); if (existingAccount.id().isEmpty()) { newAccount.setName(part); addAccount(newAccount, parentAccount); parentAccount = newAccount; } else { parentAccount = existingAccount; } newAccount.setParentAccountId(QString()); // make sure, there's no parent newAccount.clearId(); // and no id set for adding newAccount.removeAccountIds(); // and no sub-account ids newAccount.setName(remainder); } addAccount(newAccount, parentAccount); // in case of a loan account, we add the initial payment if ((newAccount.accountType() == Account::Type::Loan || newAccount.accountType() == Account::Type::AssetLoan) && !newAccount.value("kmm-loan-payment-acc").isEmpty() && !newAccount.value("kmm-loan-payment-date").isEmpty()) { MyMoneyAccountLoan acc(newAccount); MyMoneyTransaction t; MyMoneySplit a, b; a.setAccountId(acc.id()); b.setAccountId(acc.value("kmm-loan-payment-acc").toLatin1()); a.setValue(acc.loanAmount()); if (acc.accountType() == Account::Type::Loan) a.setValue(-a.value()); a.setShares(a.value()); b.setValue(-a.value()); b.setShares(b.value()); a.setMemo(i18n("Loan payout")); b.setMemo(i18n("Loan payout")); t.setPostDate(QDate::fromString(acc.value("kmm-loan-payment-date"), Qt::ISODate)); newAccount.deletePair("kmm-loan-payment-acc"); newAccount.deletePair("kmm-loan-payment-date"); MyMoneyFile::instance()->modifyAccount(newAccount); t.addSplit(a); t.addSplit(b); addTransaction(t); createOpeningBalanceTransaction(newAccount, openingBal); // in case of an investment account we check if we should create // a brokerage account } else if (newAccount.accountType() == Account::Type::Investment && !brokerageAccount.name().isEmpty()) { addAccount(brokerageAccount, parentAccount); // set a link from the investment account to the brokerage account modifyAccount(newAccount); createOpeningBalanceTransaction(brokerageAccount, openingBal); } else createOpeningBalanceTransaction(newAccount, openingBal); ft.commit(); } catch (const MyMoneyException &e) { qWarning("Unable to create account: %s", qPrintable(e.what())); throw MYMONEYEXCEPTION(e.what()); } } void MyMoneyFile::addAccount(MyMoneyAccount& account, MyMoneyAccount& parent) { d->checkTransaction(Q_FUNC_INFO); MyMoneyInstitution institution; // perform some checks to see that the account stuff is OK. For // now we assume that the account must have a name, has no // transaction and sub-accounts and parent account // it's own ID is not set and it does not have a pointer to (MyMoneyFile) if (account.name().length() == 0) throw MYMONEYEXCEPTION("Account has no name"); if (account.id().length() != 0) throw MYMONEYEXCEPTION("New account must have no id"); if (account.accountList().count() != 0) throw MYMONEYEXCEPTION("New account must have no sub-accounts"); if (!account.parentAccountId().isEmpty()) throw MYMONEYEXCEPTION("New account must have no parent-id"); if (account.accountType() == Account::Type::Unknown) throw MYMONEYEXCEPTION("Account has invalid type"); // make sure, that the parent account exists // if not, an exception is thrown. If it exists, // get a copy of the current data auto acc = MyMoneyFile::account(parent.id()); #if 0 // TODO: remove the following code as we now can have multiple accounts // with the same name even in the same hierarchy position of the account tree // // check if the selected name is currently not among the child accounts // if we find one, then return it as the new account QStringList::const_iterator it_a; foreach (const auto accountID, acc.accountList()) { MyMoneyAccount a = MyMoneyFile::account(accountID); if (account.name() == a.name()) { account = a; return; } } #endif // FIXME: make sure, that the parent has the same type // I left it out here because I don't know, if there is // a tight coupling between e.g. checking accounts and the // class asset. It certainly does not make sense to create an // expense account under an income account. Maybe it does, I don't know. // We enforce, that a stock account can never be a parent and // that the parent for a stock account must be an investment. Also, // an investment cannot have another investment account as it's parent if (parent.isInvest()) throw MYMONEYEXCEPTION("Stock account cannot be parent account"); if (account.isInvest() && parent.accountType() != Account::Type::Investment) throw MYMONEYEXCEPTION("Stock account must have investment account as parent "); if (!account.isInvest() && parent.accountType() == Account::Type::Investment) throw MYMONEYEXCEPTION("Investment account can only have stock accounts as children"); // if an institution is set, verify that it exists if (account.institutionId().length() != 0) { // check the presence of the institution. if it // does not exist, an exception is thrown institution = MyMoneyFile::institution(account.institutionId()); } // if we don't have a valid opening date use today if (!account.openingDate().isValid()) { account.setOpeningDate(QDate::currentDate()); } // make sure to set the opening date for categories to a // fixed date (1900-1-1). See #313793 on b.k.o for details if (account.isIncomeExpense()) { account.setOpeningDate(QDate(1900, 1, 1)); } // if we don't have a currency assigned use the base currency if (account.currencyId().isEmpty()) { account.setCurrencyId(baseCurrency().id()); } // make sure the parent id is setup account.setParentAccountId(parent.id()); d->m_storage->addAccount(account); d->m_changeSet += MyMoneyNotification(File::Mode::Add, account); d->m_storage->addAccount(parent, account); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, parent); if (account.institutionId().length() != 0) { institution.addAccountId(account.id()); d->m_storage->modifyInstitution(institution); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, institution); } } MyMoneyTransaction MyMoneyFile::createOpeningBalanceTransaction(const MyMoneyAccount& acc, const MyMoneyMoney& balance) { MyMoneyTransaction t; // if the opening balance is not zero, we need // to create the respective transaction if (!balance.isZero()) { d->checkTransaction(Q_FUNC_INFO); MyMoneySecurity currency = security(acc.currencyId()); MyMoneyAccount openAcc = openingBalanceAccount(currency); if (openAcc.openingDate() > acc.openingDate()) { openAcc.setOpeningDate(acc.openingDate()); modifyAccount(openAcc); } MyMoneySplit s; t.setPostDate(acc.openingDate()); t.setCommodity(acc.currencyId()); s.setAccountId(acc.id()); s.setShares(balance); s.setValue(balance); t.addSplit(s); s.clearId(); s.setAccountId(openAcc.id()); s.setShares(-balance); s.setValue(-balance); t.addSplit(s); addTransaction(t); } return t; } QString MyMoneyFile::openingBalanceTransaction(const MyMoneyAccount& acc) const { QString result; MyMoneySecurity currency = security(acc.currencyId()); MyMoneyAccount openAcc; try { openAcc = openingBalanceAccount(currency); } catch (const MyMoneyException &) { return result; } // Iterate over all the opening balance transactions for this currency MyMoneyTransactionFilter filter; filter.addAccount(openAcc.id()); QList transactions = transactionList(filter); QList::const_iterator it_t = transactions.constBegin(); while (it_t != transactions.constEnd()) { try { // Test whether the transaction also includes a split into // this account (*it_t).splitByAccount(acc.id(), true /*match*/); // If so, we have a winner! result = (*it_t).id(); break; } catch (const MyMoneyException &) { // If not, keep searching ++it_t; } } return result; } MyMoneyAccount MyMoneyFile::openingBalanceAccount(const MyMoneySecurity& security) { if (!security.isCurrency()) throw MYMONEYEXCEPTION("Opening balance for non currencies not supported"); try { return openingBalanceAccount_internal(security); } catch (const MyMoneyException &) { MyMoneyFileTransaction ft; MyMoneyAccount acc; try { acc = createOpeningBalanceAccount(security); ft.commit(); } catch (const MyMoneyException &) { qDebug("Unable to create opening balance account for security %s", qPrintable(security.id())); } return acc; } } MyMoneyAccount MyMoneyFile::openingBalanceAccount(const MyMoneySecurity& security) const { return openingBalanceAccount_internal(security); } MyMoneyAccount MyMoneyFile::openingBalanceAccount_internal(const MyMoneySecurity& security) const { if (!security.isCurrency()) throw MYMONEYEXCEPTION("Opening balance for non currencies not supported"); MyMoneyAccount acc; QList accounts; QList::ConstIterator it; accountList(accounts, equity().accountList(), true); for (it = accounts.constBegin(); it != accounts.constEnd(); ++it) { if (it->value("OpeningBalanceAccount") == QLatin1String("Yes") && it->currencyId() == security.id()) { acc = *it; break; } } if (acc.id().isEmpty()) { for (it = accounts.constBegin(); it != accounts.constEnd(); ++it) { if (it->name().startsWith(MyMoneyFile::openingBalancesPrefix()) && it->currencyId() == security.id()) { acc = *it; break; } } } if (acc.id().isEmpty()) { throw MYMONEYEXCEPTION(QString("No opening balance account for %1").arg(security.tradingSymbol())); } return acc; } MyMoneyAccount MyMoneyFile::createOpeningBalanceAccount(const MyMoneySecurity& security) { d->checkTransaction(Q_FUNC_INFO); MyMoneyAccount acc; QList accounts; QList::ConstIterator it; accountList(accounts, equity().accountList(), true); // find present opening balance accounts without containing '(' QString name; QString parentAccountId; QRegExp exp(QString("\\([A-Z]{3}\\)")); for (it = accounts.constBegin(); it != accounts.constEnd(); ++it) { if (it->value("OpeningBalanceAccount") == QLatin1String("Yes") && exp.indexIn(it->name()) == -1) { name = it->name(); parentAccountId = it->parentAccountId(); break; } } if (name.isEmpty()) name = MyMoneyFile::openingBalancesPrefix(); if (security.id() != baseCurrency().id()) { name += QString(" (%1)").arg(security.id()); } acc.setName(name); acc.setAccountType(Account::Type::Equity); acc.setCurrencyId(security.id()); acc.setValue("OpeningBalanceAccount", "Yes"); MyMoneyAccount parent = !parentAccountId.isEmpty() ? account(parentAccountId) : equity(); this->addAccount(acc, parent); return acc; } void MyMoneyFile::addTransaction(MyMoneyTransaction& transaction) { d->checkTransaction(Q_FUNC_INFO); // clear all changed objects from cache MyMoneyNotifier notifier(d); // perform some checks to see that the transaction stuff is OK. For // now we assume that // * no ids are assigned // * the date valid (must not be empty) // * the referenced accounts in the splits exist // first perform all the checks if (!transaction.id().isEmpty()) throw MYMONEYEXCEPTION("Unable to add transaction with id set"); if (!transaction.postDate().isValid()) throw MYMONEYEXCEPTION("Unable to add transaction with invalid postdate"); // now check the splits auto loanAccountAffected = false; const auto splits1 = transaction.splits(); for (const auto& split : splits1) { // the following line will throw an exception if the // account does not exist or is one of the standard accounts auto acc = MyMoneyFile::account(split.accountId()); if (acc.id().isEmpty()) throw MYMONEYEXCEPTION("Cannot add split with no account assigned"); if (acc.isLoan()) loanAccountAffected = true; if (isStandardAccount(split.accountId())) throw MYMONEYEXCEPTION("Cannot add split referencing standard account"); } // change transfer splits between asset/liability and loan accounts // into amortization splits if (loanAccountAffected) { foreach (const auto split, transaction.splits()) { if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer)) { auto acc = MyMoneyFile::account(split.accountId()); if (acc.isAssetLiability()) { MyMoneySplit s = split; s.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Amortization)); transaction.modifySplit(s); } } } } // check that we have a commodity if (transaction.commodity().isEmpty()) { transaction.setCommodity(baseCurrency().id()); } // make sure the value is rounded to the accounts precision fixSplitPrecision(transaction); // then add the transaction to the file global pool d->m_storage->addTransaction(transaction); // scan the splits again to update notification list const auto splits2 = transaction.splits(); for (const auto& split : splits2) d->addCacheNotification(split.accountId(), transaction.postDate()); d->m_changeSet += MyMoneyNotification(File::Mode::Add, transaction); } MyMoneyTransaction MyMoneyFile::transaction(const QString& id) const { d->checkStorage(); return d->m_storage->transaction(id); } MyMoneyTransaction MyMoneyFile::transaction(const QString& account, const int idx) const { d->checkStorage(); return d->m_storage->transaction(account, idx); } void MyMoneyFile::addPayee(MyMoneyPayee& payee) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->addPayee(payee); d->m_changeSet += MyMoneyNotification(File::Mode::Add, payee); } MyMoneyPayee MyMoneyFile::payee(const QString& id) const { if (Q_UNLIKELY(id.isEmpty())) return MyMoneyPayee(); return d->m_storage->payee(id); } MyMoneyPayee MyMoneyFile::payeeByName(const QString& name) const { d->checkStorage(); return d->m_storage->payeeByName(name); } void MyMoneyFile::modifyPayee(const MyMoneyPayee& payee) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->modifyPayee(payee); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, payee); } void MyMoneyFile::removePayee(const MyMoneyPayee& payee) { d->checkTransaction(Q_FUNC_INFO); // FIXME we need to make sure, that the payee is not referenced anymore d->m_storage->removePayee(payee); d->m_changeSet += MyMoneyNotification(File::Mode::Remove, payee); } void MyMoneyFile::addTag(MyMoneyTag& tag) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->addTag(tag); d->m_changeSet += MyMoneyNotification(File::Mode::Add, tag); } MyMoneyTag MyMoneyFile::tag(const QString& id) const { return d->m_storage->tag(id); } MyMoneyTag MyMoneyFile::tagByName(const QString& name) const { d->checkStorage(); return d->m_storage->tagByName(name); } void MyMoneyFile::modifyTag(const MyMoneyTag& tag) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->modifyTag(tag); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, tag); } void MyMoneyFile::removeTag(const MyMoneyTag& tag) { d->checkTransaction(Q_FUNC_INFO); // FIXME we need to make sure, that the tag is not referenced anymore d->m_storage->removeTag(tag); d->m_changeSet += MyMoneyNotification(File::Mode::Remove, tag); } void MyMoneyFile::accountList(QList& list, const QStringList& idlist, const bool recursive) const { if (idlist.isEmpty()) { d->m_storage->accountList(list); #if 0 // TODO: I have no idea what this was good for, but it caused the networth report // to show double the numbers so I commented it out (ipwizard, 2008-05-24) if (d->m_storage && (list.isEmpty() || list.size() != d->m_storage->accountCount())) { d->m_storage->accountList(list); d->m_cache.preloadAccount(list); } #endif QList::Iterator it; for (it = list.begin(); it != list.end();) { if (isStandardAccount((*it).id())) { it = list.erase(it); } else { ++it; } } } else { QList::ConstIterator it; QList list_a; d->m_storage->accountList(list_a); for (it = list_a.constBegin(); it != list_a.constEnd(); ++it) { if (!isStandardAccount((*it).id())) { if (idlist.indexOf((*it).id()) != -1) { list.append(*it); if (recursive == true && !(*it).accountList().isEmpty()) { accountList(list, (*it).accountList(), true); } } } } } } QList MyMoneyFile::institutionList() const { return d->m_storage->institutionList(); } // general get functions MyMoneyPayee MyMoneyFile::user() const { d->checkStorage(); return d->m_storage->user(); } // general set functions void MyMoneyFile::setUser(const MyMoneyPayee& user) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->setUser(user); } bool MyMoneyFile::dirty() const { if (!d->m_storage) return false; return d->m_storage->dirty(); } void MyMoneyFile::setDirty() const { d->checkStorage(); d->m_storage->setDirty(); } unsigned int MyMoneyFile::accountCount() const { d->checkStorage(); return d->m_storage->accountCount(); } void MyMoneyFile::ensureDefaultCurrency(MyMoneyAccount& acc) const { if (acc.currencyId().isEmpty()) { if (!baseCurrency().id().isEmpty()) acc.setCurrencyId(baseCurrency().id()); } } MyMoneyAccount MyMoneyFile::liability() const { d->checkStorage(); return account(stdAccNames[stdAccLiability]); } MyMoneyAccount MyMoneyFile::asset() const { d->checkStorage(); return account(stdAccNames[stdAccAsset]); } MyMoneyAccount MyMoneyFile::expense() const { d->checkStorage(); return account(stdAccNames[stdAccExpense]); } MyMoneyAccount MyMoneyFile::income() const { d->checkStorage(); return account(stdAccNames[stdAccIncome]); } MyMoneyAccount MyMoneyFile::equity() const { d->checkStorage(); return account(stdAccNames[stdAccEquity]); } unsigned int MyMoneyFile::transactionCount(const QString& account) const { d->checkStorage(); return d->m_storage->transactionCount(account); } unsigned int MyMoneyFile::transactionCount() const { return transactionCount(QString()); } QMap MyMoneyFile::transactionCountMap() const { d->checkStorage(); return d->m_storage->transactionCountMap(); } unsigned int MyMoneyFile::institutionCount() const { d->checkStorage(); return d->m_storage->institutionCount(); } MyMoneyMoney MyMoneyFile::balance(const QString& id, const QDate& date) const { if (date.isValid()) { MyMoneyBalanceCacheItem bal = d->m_balanceCache.balance(id, date); if (bal.isValid()) return bal.balance(); } d->checkStorage(); MyMoneyMoney returnValue = d->m_storage->balance(id, date); if (date.isValid()) { d->m_balanceCache.insert(id, date, returnValue); } return returnValue; } MyMoneyMoney MyMoneyFile::balance(const QString& id) const { return balance(id, QDate()); } MyMoneyMoney MyMoneyFile::clearedBalance(const QString &id, const QDate& date) const { MyMoneyMoney cleared; QList list; cleared = balance(id, date); MyMoneyAccount account = this->account(id); MyMoneyMoney factor(1, 1); if (account.accountGroup() == Account::Type::Liability || account.accountGroup() == Account::Type::Equity) factor = -factor; MyMoneyTransactionFilter filter; filter.addAccount(id); filter.setDateFilter(QDate(), date); filter.setReportAllSplits(false); filter.addState((int)TransactionFilter::State::NotReconciled); transactionList(list, filter); for (QList::const_iterator it_t = list.constBegin(); it_t != list.constEnd(); ++it_t) { const QList& splits = (*it_t).splits(); for (QList::const_iterator it_s = splits.constBegin(); it_s != splits.constEnd(); ++it_s) { const MyMoneySplit &split = (*it_s); if (split.accountId() != id) continue; cleared -= split.shares(); } } return cleared * factor; } MyMoneyMoney MyMoneyFile::totalBalance(const QString& id, const QDate& date) const { d->checkStorage(); return d->m_storage->totalBalance(id, date); } MyMoneyMoney MyMoneyFile::totalBalance(const QString& id) const { return totalBalance(id, QDate()); } void MyMoneyFile::warningMissingRate(const QString& fromId, const QString& toId) const { MyMoneySecurity from, to; try { from = security(fromId); to = security(toId); qWarning("Missing price info for conversion from %s to %s", qPrintable(from.name()), qPrintable(to.name())); } catch (const MyMoneyException &e) { qWarning("Missing security caught in MyMoneyFile::warningMissingRate(): %s(%ld) %s", qPrintable(e.file()), e.line(), qPrintable(e.what())); } } void MyMoneyFile::transactionList(QList >& list, MyMoneyTransactionFilter& filter) const { d->checkStorage(); d->m_storage->transactionList(list, filter); } void MyMoneyFile::transactionList(QList& list, MyMoneyTransactionFilter& filter) const { d->checkStorage(); d->m_storage->transactionList(list, filter); } QList MyMoneyFile::transactionList(MyMoneyTransactionFilter& filter) const { d->checkStorage(); return d->m_storage->transactionList(filter); } QList MyMoneyFile::payeeList() const { return d->m_storage->payeeList(); } QList MyMoneyFile::tagList() const { return d->m_storage->tagList(); } QString MyMoneyFile::accountToCategory(const QString& accountId, bool includeStandardAccounts) const { MyMoneyAccount acc; QString rc; if (!accountId.isEmpty()) { acc = account(accountId); do { if (!rc.isEmpty()) rc = AccountSeparator + rc; rc = acc.name() + rc; acc = account(acc.parentAccountId()); } while (!acc.id().isEmpty() && (includeStandardAccounts || !isStandardAccount(acc.id()))); } return rc; } QString MyMoneyFile::categoryToAccount(const QString& category, Account::Type type) const { QString id; // search the category in the expense accounts and if it is not found, try // to locate it in the income accounts if (type == Account::Type::Unknown || type == Account::Type::Expense) { id = locateSubAccount(MyMoneyFile::instance()->expense(), category); } if ((id.isEmpty() && type == Account::Type::Unknown) || type == Account::Type::Income) { id = locateSubAccount(MyMoneyFile::instance()->income(), category); } return id; } QString MyMoneyFile::categoryToAccount(const QString& category) const { return categoryToAccount(category, Account::Type::Unknown); } QString MyMoneyFile::nameToAccount(const QString& name) const { QString id; // search the category in the asset accounts and if it is not found, try // to locate it in the liability accounts id = locateSubAccount(MyMoneyFile::instance()->asset(), name); if (id.isEmpty()) id = locateSubAccount(MyMoneyFile::instance()->liability(), name); return id; } QString MyMoneyFile::parentName(const QString& name) const { return name.section(AccountSeparator, 0, -2); } QString MyMoneyFile::locateSubAccount(const MyMoneyAccount& base, const QString& category) const { MyMoneyAccount nextBase; QString level, remainder; level = category.section(AccountSeparator, 0, 0); remainder = category.section(AccountSeparator, 1); foreach (const auto sAccount, base.accountList()) { nextBase = account(sAccount); if (nextBase.name() == level) { if (remainder.isEmpty()) { return nextBase.id(); } return locateSubAccount(nextBase, remainder); } } return QString(); } QString MyMoneyFile::value(const QString& key) const { d->checkStorage(); return d->m_storage->value(key); } void MyMoneyFile::setValue(const QString& key, const QString& val) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->setValue(key, val); } void MyMoneyFile::deletePair(const QString& key) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->deletePair(key); } void MyMoneyFile::addSchedule(MyMoneySchedule& sched) { d->checkTransaction(Q_FUNC_INFO); const auto splits = sched.transaction().splits(); for (const auto& split : splits) { // the following line will throw an exception if the // account does not exist or is one of the standard accounts const auto acc = account(split.accountId()); if (acc.id().isEmpty()) throw MYMONEYEXCEPTION("Cannot add split with no account assigned"); if (isStandardAccount(split.accountId())) throw MYMONEYEXCEPTION("Cannot add split referencing standard account"); } d->m_storage->addSchedule(sched); d->m_changeSet += MyMoneyNotification(File::Mode::Add, sched); } void MyMoneyFile::modifySchedule(const MyMoneySchedule& sched) { d->checkTransaction(Q_FUNC_INFO); foreach (const auto split, sched.transaction().splits()) { // the following line will throw an exception if the // account does not exist or is one of the standard accounts auto acc = MyMoneyFile::account(split.accountId()); if (acc.id().isEmpty()) throw MYMONEYEXCEPTION("Cannot store split with no account assigned"); if (isStandardAccount(split.accountId())) throw MYMONEYEXCEPTION("Cannot store split referencing standard account"); } d->m_storage->modifySchedule(sched); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, sched); } void MyMoneyFile::removeSchedule(const MyMoneySchedule& sched) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->removeSchedule(sched); d->m_changeSet += MyMoneyNotification(File::Mode::Remove, sched); } MyMoneySchedule MyMoneyFile::schedule(const QString& id) const { return d->m_storage->schedule(id); } QList MyMoneyFile::scheduleList( const QString& accountId, const Schedule::Type type, const Schedule::Occurrence occurrence, const Schedule::PaymentType paymentType, const QDate& startDate, const QDate& endDate, const bool overdue) const { d->checkStorage(); return d->m_storage->scheduleList(accountId, type, occurrence, paymentType, startDate, endDate, overdue); } QList MyMoneyFile::scheduleList( const QString& accountId) const { return scheduleList(accountId, Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, QDate(), QDate(), false); } QList MyMoneyFile::scheduleList() const { return scheduleList(QString(), Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, QDate(), QDate(), false); } QStringList MyMoneyFile::consistencyCheck() { QList list; QList::Iterator it_a; QList::Iterator it_sch; QList::Iterator it_p; QList::Iterator it_t; QList::Iterator it_r; QStringList accountRebuild; QMap interestAccounts; MyMoneyAccount parent; MyMoneyAccount child; MyMoneyAccount toplevel; QString parentId; QStringList rc; int problemCount = 0; int unfixedCount = 0; QString problemAccount; // check that we have a storage object d->checkTransaction(Q_FUNC_INFO); // get the current list of accounts accountList(list); // add the standard accounts list << MyMoneyFile::instance()->asset(); list << MyMoneyFile::instance()->liability(); list << MyMoneyFile::instance()->income(); list << MyMoneyFile::instance()->expense(); for (it_a = list.begin(); it_a != list.end(); ++it_a) { // no more checks for standard accounts if (isStandardAccount((*it_a).id())) { continue; } switch ((*it_a).accountGroup()) { case Account::Type::Asset: toplevel = asset(); break; case Account::Type::Liability: toplevel = liability(); break; case Account::Type::Expense: toplevel = expense(); break; case Account::Type::Income: toplevel = income(); break; case Account::Type::Equity: toplevel = equity(); break; default: qWarning("%s:%d This should never happen!", __FILE__ , __LINE__); break; } // check for loops in the hierarchy parentId = (*it_a).parentAccountId(); try { bool dropOut = false; while (!isStandardAccount(parentId) && !dropOut) { parent = account(parentId); if (parent.id() == (*it_a).id()) { // parent loops, so we need to re-parent to toplevel account // find parent account in our list problemCount++; QList::Iterator it_b; for (it_b = list.begin(); it_b != list.end(); ++it_b) { if ((*it_b).id() == parent.id()) { if (problemAccount != (*it_a).name()) { problemAccount = (*it_a).name(); rc << i18n("* Problem with account '%1'", problemAccount); rc << i18n(" * Loop detected between this account and account '%1'.", (*it_b).name()); rc << i18n(" Reparenting account '%2' to top level account '%1'.", toplevel.name(), (*it_a).name()); (*it_a).setParentAccountId(toplevel.id()); if (accountRebuild.contains(toplevel.id()) == 0) accountRebuild << toplevel.id(); if (accountRebuild.contains((*it_a).id()) == 0) accountRebuild << (*it_a).id(); dropOut = true; break; } } } } parentId = parent.parentAccountId(); } } catch (const MyMoneyException &) { // if we don't know about a parent, we catch it later } // check that the parent exists parentId = (*it_a).parentAccountId(); try { parent = account(parentId); if ((*it_a).accountGroup() != parent.accountGroup()) { problemCount++; if (problemAccount != (*it_a).name()) { problemAccount = (*it_a).name(); rc << i18n("* Problem with account '%1'", problemAccount); } // the parent belongs to a different group, so we reconnect to the // master group account (asset, liability, etc) to which this account // should belong and update it in the engine. rc << i18n(" * Parent account '%1' belongs to a different group.", parent.name()); rc << i18n(" New parent account is the top level account '%1'.", toplevel.name()); (*it_a).setParentAccountId(toplevel.id()); // make sure to rebuild the sub-accounts of the top account // and the one we removed this account from if (accountRebuild.contains(toplevel.id()) == 0) accountRebuild << toplevel.id(); if (accountRebuild.contains(parent.id()) == 0) accountRebuild << parent.id(); } else if (!parent.accountList().contains((*it_a).id())) { problemCount++; if (problemAccount != (*it_a).name()) { problemAccount = (*it_a).name(); rc << i18n("* Problem with account '%1'", problemAccount); } // parent exists, but does not have a reference to the account rc << i18n(" * Parent account '%1' does not contain '%2' as sub-account.", parent.name(), problemAccount); if (accountRebuild.contains(parent.id()) == 0) accountRebuild << parent.id(); } } catch (const MyMoneyException &) { // apparently, the parent does not exist anymore. we reconnect to the // master group account (asset, liability, etc) to which this account // should belong and update it in the engine. problemCount++; if (problemAccount != (*it_a).name()) { problemAccount = (*it_a).name(); rc << i18n("* Problem with account '%1'", problemAccount); } rc << i18n(" * The parent with id %1 does not exist anymore.", parentId); rc << i18n(" New parent account is the top level account '%1'.", toplevel.name()); (*it_a).setParentAccountId(toplevel.id()); // make sure to rebuild the sub-accounts of the top account if (accountRebuild.contains(toplevel.id()) == 0) accountRebuild << toplevel.id(); } // now check that all the children exist and have the correct type foreach (const auto accountID, (*it_a).accountList()) { // check that the child exists try { child = account(accountID); if (child.parentAccountId() != (*it_a).id()) { throw MYMONEYEXCEPTION("Child account has a different parent"); } } catch (const MyMoneyException &) { problemCount++; if (problemAccount != (*it_a).name()) { problemAccount = (*it_a).name(); rc << i18n("* Problem with account '%1'", problemAccount); } rc << i18n(" * Child account with id %1 does not exist anymore.", accountID); rc << i18n(" The child account list will be reconstructed."); if (accountRebuild.contains((*it_a).id()) == 0) accountRebuild << (*it_a).id(); } } // see if it is a loan account. if so, remember the assigned interest account if ((*it_a).isLoan()) { MyMoneyAccountLoan loan(*it_a); if (!loan.interestAccountId().isEmpty()) { interestAccounts[loan.interestAccountId()] = true; } try { payee(loan.payee()); } catch (const MyMoneyException &) { problemCount++; if (problemAccount != (*it_a).name()) { problemAccount = (*it_a).name(); rc << i18n("* Problem with account '%1'", problemAccount); } rc << i18n(" * The payee with id %1 referenced by the loan does not exist anymore.", loan.payee()); rc << i18n(" The payee will be removed."); // remove the payee - the account will be modified in the engine later (*it_a).deletePair("payee"); } } // check if it is a category and set the date to 1900-01-01 if different if ((*it_a).isIncomeExpense()) { if (((*it_a).openingDate().isValid() == false) || ((*it_a).openingDate() != QDate(1900, 1, 1))) { (*it_a).setOpeningDate(QDate(1900, 1, 1)); } } // check for clear text online password in the online settings if (!(*it_a).onlineBankingSettings().value("password").isEmpty()) { if (problemAccount != (*it_a).name()) { problemAccount = (*it_a).name(); rc << i18n("* Problem with account '%1'", problemAccount); } rc << i18n(" * Older versions of KMyMoney stored an OFX password for this account in cleartext."); rc << i18n(" Please open it in the account editor (Account/Edit account) once and press OK."); rc << i18n(" This will store the password in the KDE wallet and remove the cleartext version."); ++unfixedCount; } // if the account was modified, we need to update it in the engine if (!(d->m_storage->account((*it_a).id()) == (*it_a))) { try { d->m_storage->modifyAccount(*it_a, true); } catch (const MyMoneyException &) { rc << i18n(" * Unable to update account data in engine."); return rc; } } } if (accountRebuild.count() != 0) { rc << i18n("* Reconstructing the child lists for"); } // clear the affected lists for (it_a = list.begin(); it_a != list.end(); ++it_a) { if (accountRebuild.contains((*it_a).id())) { rc << QString(" %1").arg((*it_a).name()); // clear the account list (*it_a).removeAccountIds(); } } // reconstruct the lists for (it_a = list.begin(); it_a != list.end(); ++it_a) { QList::Iterator it; parentId = (*it_a).parentAccountId(); if (accountRebuild.contains(parentId)) { for (it = list.begin(); it != list.end(); ++it) { if ((*it).id() == parentId) { (*it).addAccountId((*it_a).id()); break; } } } } // update the engine objects for (it_a = list.begin(); it_a != list.end(); ++it_a) { if (accountRebuild.contains((*it_a).id())) { try { d->m_storage->modifyAccount(*it_a, true); } catch (const MyMoneyException &) { rc << i18n(" * Unable to update account data for account %1 in engine", (*it_a).name()); } } } // For some reason, files exist with invalid ids. This has been found in the payee id // so we fix them here QList pList = payeeList(); QMappayeeConversionMap; for (it_p = pList.begin(); it_p != pList.end(); ++it_p) { if ((*it_p).id().length() > 7) { // found one of those with an invalid ids // create a new one and store it in the map. MyMoneyPayee payee = (*it_p); payee.clearId(); d->m_storage->addPayee(payee); payeeConversionMap[(*it_p).id()] = payee.id(); rc << i18n(" * Payee %1 recreated with fixed id", payee.name()); ++problemCount; } } // Fix the transactions MyMoneyTransactionFilter filter; filter.setReportAllSplits(false); const auto tList = d->m_storage->transactionList(filter); // Generate the list of interest accounts for (const auto& transaction : tList) { const auto splits = transaction.splits(); for (const auto& split : splits) { if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)) interestAccounts[split.accountId()] = true; } } QSet supportedAccountTypes; supportedAccountTypes << Account::Type::Checkings << Account::Type::Savings << Account::Type::Cash << Account::Type::CreditCard << Account::Type::Asset << Account::Type::Liability; QSet reportedUnsupportedAccounts; for (const auto& transaction : tList) { MyMoneyTransaction t = transaction; bool tChanged = false; QDate accountOpeningDate; QStringList accountList; const auto splits = t.splits(); foreach (const auto split, splits) { bool sChanged = false; MyMoneySplit s = split; if (payeeConversionMap.find(split.payeeId()) != payeeConversionMap.end()) { s.setPayeeId(payeeConversionMap[s.payeeId()]); sChanged = true; rc << i18n(" * Payee id updated in split of transaction '%1'.", t.id()); ++problemCount; } try { const auto acc = this->account(s.accountId()); // compute the newest opening date of all accounts involved in the transaction // in case the newest opening date is newer than the transaction post date, do one // of the following: // // a) for category and stock accounts: update the opening date of the account // b) for account types where the user cannot modify the opening date through // the UI issue a warning (for each account only once) // c) others will be caught later if (!acc.isIncomeExpense() && !acc.isInvest()) { if (acc.openingDate() > t.postDate()) { if (!accountOpeningDate.isValid() || acc.openingDate() > accountOpeningDate) { accountOpeningDate = acc.openingDate(); } accountList << this->accountToCategory(acc.id()); if (!supportedAccountTypes.contains(acc.accountType()) && !reportedUnsupportedAccounts.contains(acc.id())) { rc << i18n(" * Opening date of Account '%1' cannot be changed to support transaction '%2' post date.", this->accountToCategory(acc.id()), t.id()); reportedUnsupportedAccounts << acc.id(); ++unfixedCount; } } } else { if (acc.openingDate() > t.postDate()) { rc << i18n(" * Transaction '%1' post date '%2' is older than opening date '%4' of account '%3'.", t.id(), t.postDate().toString(Qt::ISODate), this->accountToCategory(acc.id()), acc.openingDate().toString(Qt::ISODate)); rc << i18n(" Account opening date updated."); MyMoneyAccount newAcc = acc; newAcc.setOpeningDate(t.postDate()); this->modifyAccount(newAcc); ++problemCount; } } // make sure, that shares and value have the same number if they // represent the same currency. if (t.commodity() == acc.currencyId() && s.shares().reduce() != s.value().reduce()) { // use the value as master if the transaction is balanced if (t.splitSum().isZero()) { s.setShares(s.value()); rc << i18n(" * shares set to value in split of transaction '%1'.", t.id()); } else { s.setValue(s.shares()); rc << i18n(" * value set to shares in split of transaction '%1'.", t.id()); } sChanged = true; ++problemCount; } } catch (const MyMoneyException &) { rc << i18n(" * Split %2 in transaction '%1' contains a reference to invalid account %3. Please fix manually.", t.id(), split.id(), split.accountId()); ++unfixedCount; } // make sure the interest splits are marked correct as such if (interestAccounts.find(s.accountId()) != interestAccounts.end() && s.action() != MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)) { s.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Interest)); sChanged = true; rc << i18n(" * action marked as interest in split of transaction '%1'.", t.id()); ++problemCount; } if (sChanged) { tChanged = true; t.modifySplit(s); } } // make sure that the transaction's post date is valid if (!t.postDate().isValid()) { tChanged = true; t.setPostDate(t.entryDate().isValid() ? t.entryDate() : QDate::currentDate()); rc << i18n(" * Transaction '%1' has an invalid post date.", t.id()); rc << i18n(" The post date was updated to '%1'.", QLocale().toString(t.postDate(), QLocale::ShortFormat)); ++problemCount; } // check if the transaction's post date is after the opening date // of all accounts involved in the transaction. In case it is not, // issue a warning with the details about the transaction incl. // the account names and dates involved if (accountOpeningDate.isValid() && t.postDate() < accountOpeningDate) { QDate originalPostDate = t.postDate(); #if 0 // for now we do not activate the logic to move the post date to a later // point in time. This could cause some severe trouble if you have lots // of ancient data collected with older versions of KMyMoney that did not // enforce certain conditions like we do now. t.setPostDate(accountOpeningDate); tChanged = true; // copy the price information for investments to the new date QList::const_iterator it_t; foreach (const auto split, t.splits()) { if ((split.action() != "Buy") && (split.action() != "Reinvest")) { continue; } QString id = split.accountId(); auto acc = this->account(id); MyMoneySecurity sec = this->security(acc.currencyId()); MyMoneyPrice price(acc.currencyId(), sec.tradingCurrency(), t.postDate(), split.price(), "Transaction"); this->addPrice(price); break; } #endif rc << i18n(" * Transaction '%1' has a post date '%2' before one of the referenced account's opening date.", t.id(), QLocale().toString(originalPostDate, QLocale::ShortFormat)); rc << i18n(" Referenced accounts: %1", accountList.join(",")); rc << i18n(" The post date was not updated to '%1'.", QLocale().toString(accountOpeningDate, QLocale::ShortFormat)); ++unfixedCount; } if (tChanged) { d->m_storage->modifyTransaction(t); } } // Fix the schedules QList schList = scheduleList(); for (it_sch = schList.begin(); it_sch != schList.end(); ++it_sch) { MyMoneySchedule sch = (*it_sch); MyMoneyTransaction t = sch.transaction(); auto tChanged = false; foreach (const auto split, t.splits()) { MyMoneySplit s = split; bool sChanged = false; if (payeeConversionMap.find(split.payeeId()) != payeeConversionMap.end()) { s.setPayeeId(payeeConversionMap[s.payeeId()]); sChanged = true; rc << i18n(" * Payee id updated in split of schedule '%1'.", (*it_sch).name()); ++problemCount; } if (!split.value().isZero() && split.shares().isZero()) { s.setShares(s.value()); sChanged = true; rc << i18n(" * Split in scheduled transaction '%1' contained value != 0 and shares == 0.", (*it_sch).name()); rc << i18n(" Shares set to value."); ++problemCount; } // make sure, we don't have a bankid stored with a split in a schedule if (!split.bankID().isEmpty()) { s.setBankID(QString()); sChanged = true; rc << i18n(" * Removed bankid from split in scheduled transaction '%1'.", (*it_sch).name()); ++problemCount; } // make sure, that shares and value have the same number if they // represent the same currency. try { const auto acc = this->account(s.accountId()); if (t.commodity() == acc.currencyId() && s.shares().reduce() != s.value().reduce()) { // use the value as master if the transaction is balanced if (t.splitSum().isZero()) { s.setShares(s.value()); rc << i18n(" * shares set to value in split in schedule '%1'.", (*it_sch).name()); } else { s.setValue(s.shares()); rc << i18n(" * value set to shares in split in schedule '%1'.", (*it_sch).name()); } sChanged = true; ++problemCount; } } catch (const MyMoneyException &) { rc << i18n(" * Split %2 in schedule '%1' contains a reference to invalid account %3. Please fix manually.", (*it_sch).name(), split.id(), split.accountId()); ++unfixedCount; } if (sChanged) { t.modifySplit(s); tChanged = true; } } if (tChanged) { sch.setTransaction(t); d->m_storage->modifySchedule(sch); } } // Fix the reports QList rList = reportList(); for (it_r = rList.begin(); it_r != rList.end(); ++it_r) { MyMoneyReport r = *it_r; QStringList pList; QStringList::Iterator it_p; (*it_r).payees(pList); bool rChanged = false; for (it_p = pList.begin(); it_p != pList.end(); ++it_p) { if (payeeConversionMap.find(*it_p) != payeeConversionMap.end()) { rc << i18n(" * Payee id updated in report '%1'.", (*it_r).name()); ++problemCount; r.removeReference(*it_p); r.addPayee(payeeConversionMap[*it_p]); rChanged = true; } } if (rChanged) { d->m_storage->modifyReport(r); } } // erase old payee ids QMap::Iterator it_m; for (it_m = payeeConversionMap.begin(); it_m != payeeConversionMap.end(); ++it_m) { MyMoneyPayee payee = this->payee(it_m.key()); removePayee(payee); rc << i18n(" * Payee '%1' removed.", payee.id()); ++problemCount; } //look for accounts which have currencies other than the base currency but no price on the opening date //all accounts using base currency are excluded, since that's the base used for foreing currency calculation //thus it is considered as always present //accounts that represent Income/Expense categories are also excluded as price is irrelevant for their //fake opening date since a forex rate is required for all multi-currency transactions //get all currencies in use QStringList currencyList; QList accountForeignCurrency; QList accList; accountList(accList); QList::const_iterator account_it; for (account_it = accList.constBegin(); account_it != accList.constEnd(); ++account_it) { MyMoneyAccount account = *account_it; if (!account.isIncomeExpense() && !currencyList.contains(account.currencyId()) && account.currencyId() != baseCurrency().id() && !account.currencyId().isEmpty()) { //add the currency and the account-currency pair currencyList.append(account.currencyId()); accountForeignCurrency.append(account); } } MyMoneyPriceList pricesList = priceList(); QMap securityPriceDate; //get the first date of the price for each security MyMoneyPriceList::const_iterator prices_it; for (prices_it = pricesList.constBegin(); prices_it != pricesList.constEnd(); ++prices_it) { MyMoneyPrice firstPrice = (*((*prices_it).constBegin())); //only check the price if the currency is in use if (currencyList.contains(firstPrice.from()) || currencyList.contains(firstPrice.to())) { //check the security in the from field //if it is there, check if it is older QPair pricePair = qMakePair(firstPrice.from(), firstPrice.to()); securityPriceDate[pricePair] = firstPrice.date(); } } //compare the dates with the opening dates of the accounts using each currency QList::const_iterator accForeignList_it; bool firstInvProblem = true; for (accForeignList_it = accountForeignCurrency.constBegin(); accForeignList_it != accountForeignCurrency.constEnd(); ++accForeignList_it) { //setup the price pair correctly QPair pricePair; //setup the reverse, which can also be used for rate conversion QPair reversePricePair; if ((*accForeignList_it).isInvest()) { //if it is a stock, we have to search for a price from its stock to the currency of the account QString securityId = (*accForeignList_it).currencyId(); QString tradingCurrencyId = security(securityId).tradingCurrency(); pricePair = qMakePair(securityId, tradingCurrencyId); reversePricePair = qMakePair(tradingCurrencyId, securityId); } else { //if it is a regular account we search for a price from the currency of the account to the base currency QString currency = (*accForeignList_it).currencyId(); QString baseCurrencyId = baseCurrency().id(); pricePair = qMakePair(currency, baseCurrencyId); reversePricePair = qMakePair(baseCurrencyId, currency); } //compare the first price with the opening date of the account if ((!securityPriceDate.contains(pricePair) || securityPriceDate.value(pricePair) > (*accForeignList_it).openingDate()) && (!securityPriceDate.contains(reversePricePair) || securityPriceDate.value(reversePricePair) > (*accForeignList_it).openingDate())) { if (firstInvProblem) { firstInvProblem = false; rc << i18n("* Potential problem with investments/currencies"); } QDate openingDate = (*accForeignList_it).openingDate(); MyMoneySecurity secError = security((*accForeignList_it).currencyId()); if (!(*accForeignList_it).isInvest()) { rc << i18n(" * The account '%1' in currency '%2' has no price set for the opening date '%3'.", (*accForeignList_it).name(), secError.name(), openingDate.toString(Qt::ISODate)); rc << i18n(" Please enter a price for the currency on or before the opening date."); } else { rc << i18n(" * The investment '%1' has no price set for the opening date '%2'.", (*accForeignList_it).name(), openingDate.toString(Qt::ISODate)); rc << i18n(" Please enter a price for the investment on or before the opening date."); } ++unfixedCount; } } // Fix the budgets that somehow still reference invalid accounts QString problemBudget; QList bList = budgetList(); for (QList::const_iterator it_b = bList.constBegin(); it_b != bList.constEnd(); ++it_b) { MyMoneyBudget b = *it_b; QList baccounts = b.getaccounts(); bool bChanged = false; for (QList::const_iterator it_bacc = baccounts.constBegin(); it_bacc != baccounts.constEnd(); ++it_bacc) { try { account((*it_bacc).id()); } catch (const MyMoneyException &) { problemCount++; if (problemBudget != b.name()) { problemBudget = b.name(); rc << i18n("* Problem with budget '%1'", problemBudget); } rc << i18n(" * The account with id %1 referenced by the budget does not exist anymore.", (*it_bacc).id()); rc << i18n(" The account reference will be removed."); // remove the reference to the account b.removeReference((*it_bacc).id()); bChanged = true; } } if (bChanged) { d->m_storage->modifyBudget(b); } } // add more checks here if (problemCount == 0 && unfixedCount == 0) { rc << i18n("Finished: data is consistent."); } else { const QString problemsCorrected = i18np("%1 problem corrected.", "%1 problems corrected.", problemCount); const QString problemsRemaining = i18np("%1 problem still present.", "%1 problems still present.", unfixedCount); rc << QString(); rc << i18nc("%1 is a string, e.g. 7 problems corrected; %2 is a string, e.g. 3 problems still present", "Finished: %1 %2", problemsCorrected, problemsRemaining); } return rc; } QString MyMoneyFile::createCategory(const MyMoneyAccount& base, const QString& name) { d->checkTransaction(Q_FUNC_INFO); MyMoneyAccount parent = base; QString categoryText; if (base.id() != expense().id() && base.id() != income().id()) throw MYMONEYEXCEPTION("Invalid base category"); QStringList subAccounts = name.split(AccountSeparator); QStringList::Iterator it; for (it = subAccounts.begin(); it != subAccounts.end(); ++it) { MyMoneyAccount categoryAccount; categoryAccount.setName(*it); categoryAccount.setAccountType(base.accountType()); if (it == subAccounts.begin()) categoryText += *it; else categoryText += (AccountSeparator + *it); // Only create the account if it doesn't exist try { QString categoryId = categoryToAccount(categoryText); if (categoryId.isEmpty()) addAccount(categoryAccount, parent); else { categoryAccount = account(categoryId); } } catch (const MyMoneyException &e) { qDebug("Unable to add account %s, %s, %s: %s", qPrintable(categoryAccount.name()), qPrintable(parent.name()), qPrintable(categoryText), qPrintable(e.what())); } parent = categoryAccount; } return categoryToAccount(name); } QString MyMoneyFile::checkCategory(const QString& name, const MyMoneyMoney& value, const MyMoneyMoney& value2) { QString accountId; MyMoneyAccount newAccount; bool found = true; if (!name.isEmpty()) { // The category might be constructed with an arbitraty depth (number of // colon delimited fields). We try to find a parent account within this // hierarchy by searching the following sequence: // // aaaa:bbbb:cccc:ddddd // // 1. search aaaa:bbbb:cccc:dddd, create nothing // 2. search aaaa:bbbb:cccc , create dddd // 3. search aaaa:bbbb , create cccc:dddd // 4. search aaaa , create bbbb:cccc:dddd // 5. don't search , create aaaa:bbbb:cccc:dddd newAccount.setName(name); QString accName; // part to be created (right side in above list) QString parent(name); // a possible parent part (left side in above list) do { accountId = categoryToAccount(parent); if (accountId.isEmpty()) { found = false; // prepare next step if (!accName.isEmpty()) accName.prepend(':'); accName.prepend(parent.section(':', -1)); newAccount.setName(accName); parent = parent.section(':', 0, -2); } else if (!accName.isEmpty()) { newAccount.setParentAccountId(accountId); } } while (!parent.isEmpty() && accountId.isEmpty()); // if we did not find the category, we create it if (!found) { MyMoneyAccount parent; if (newAccount.parentAccountId().isEmpty()) { if (!value.isNegative() && value2.isNegative()) parent = income(); else parent = expense(); } else { parent = account(newAccount.parentAccountId()); } newAccount.setAccountType((!value.isNegative() && value2.isNegative()) ? Account::Type::Income : Account::Type::Expense); MyMoneyAccount brokerage; // clear out the parent id, because createAccount() does not like that newAccount.setParentAccountId(QString()); createAccount(newAccount, parent, brokerage, MyMoneyMoney()); accountId = newAccount.id(); } } return accountId; } void MyMoneyFile::addSecurity(MyMoneySecurity& security) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->addSecurity(security); d->m_changeSet += MyMoneyNotification(File::Mode::Add, security); } void MyMoneyFile::modifySecurity(const MyMoneySecurity& security) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->modifySecurity(security); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, security); } void MyMoneyFile::removeSecurity(const MyMoneySecurity& security) { d->checkTransaction(Q_FUNC_INFO); // FIXME check that security is not referenced by other object d->m_storage->removeSecurity(security); d->m_changeSet += MyMoneyNotification(File::Mode::Remove, security); } MyMoneySecurity MyMoneyFile::security(const QString& id) const { if (Q_UNLIKELY(id.isEmpty())) return baseCurrency(); return d->m_storage->security(id); } QList MyMoneyFile::securityList() const { d->checkStorage(); return d->m_storage->securityList(); } void MyMoneyFile::addCurrency(const MyMoneySecurity& currency) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->addCurrency(currency); d->m_changeSet += MyMoneyNotification(File::Mode::Add, currency); } void MyMoneyFile::modifyCurrency(const MyMoneySecurity& currency) { d->checkTransaction(Q_FUNC_INFO); // force reload of base currency object if (currency.id() == d->m_baseCurrency.id()) d->m_baseCurrency.clearId(); d->m_storage->modifyCurrency(currency); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, currency); } void MyMoneyFile::removeCurrency(const MyMoneySecurity& currency) { d->checkTransaction(Q_FUNC_INFO); if (currency.id() == d->m_baseCurrency.id()) throw MYMONEYEXCEPTION("Cannot delete base currency."); // FIXME check that security is not referenced by other object d->m_storage->removeCurrency(currency); d->m_changeSet += MyMoneyNotification(File::Mode::Remove, currency); } MyMoneySecurity MyMoneyFile::currency(const QString& id) const { if (id.isEmpty()) return baseCurrency(); try { const auto currency = d->m_storage->currency(id); if (currency.id().isEmpty()) throw MYMONEYEXCEPTION(QString::fromLatin1("Currency '%1' not found.").arg(id)); return currency; } catch(const MyMoneyException&) { const auto security = d->m_storage->security(id); if (security.id().isEmpty()) { throw MYMONEYEXCEPTION(QString::fromLatin1("Security '%1' not found.").arg(id)); } return security; } } QMap MyMoneyFile::ancientCurrencies() const { QMap ancientCurrencies; ancientCurrencies.insert(MyMoneySecurity("ATS", i18n("Austrian Schilling"), QString::fromUtf8("ÖS")), MyMoneyPrice("ATS", "EUR", QDate(1998, 12, 31), MyMoneyMoney(10000, 137603), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("DEM", i18n("German Mark"), "DM"), MyMoneyPrice("ATS", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100000, 195583), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("FRF", i18n("French Franc"), "FF"), MyMoneyPrice("FRF", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100000, 655957), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("ITL", i18n("Italian Lira"), QChar(0x20A4)), MyMoneyPrice("ITL", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100, 193627), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("ESP", i18n("Spanish Peseta"), QString()), MyMoneyPrice("ESP", "EUR", QDate(1998, 12, 31), MyMoneyMoney(1000, 166386), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("NLG", i18n("Dutch Guilder"), QString()), MyMoneyPrice("NLG", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100000, 220371), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("BEF", i18n("Belgian Franc"), "Fr"), MyMoneyPrice("BEF", "EUR", QDate(1998, 12, 31), MyMoneyMoney(10000, 403399), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("LUF", i18n("Luxembourg Franc"), "Fr"), MyMoneyPrice("LUF", "EUR", QDate(1998, 12, 31), MyMoneyMoney(10000, 403399), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("PTE", i18n("Portuguese Escudo"), QString()), MyMoneyPrice("PTE", "EUR", QDate(1998, 12, 31), MyMoneyMoney(1000, 200482), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("IEP", i18n("Irish Pound"), QChar(0x00A3)), MyMoneyPrice("IEP", "EUR", QDate(1998, 12, 31), MyMoneyMoney(1000000, 787564), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("FIM", i18n("Finnish Markka"), QString()), MyMoneyPrice("FIM", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100000, 594573), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("GRD", i18n("Greek Drachma"), QChar(0x20AF)), MyMoneyPrice("GRD", "EUR", QDate(1998, 12, 31), MyMoneyMoney(100, 34075), QLatin1Literal("KMyMoney"))); // http://en.wikipedia.org/wiki/Bulgarian_lev ancientCurrencies.insert(MyMoneySecurity("BGL", i18n("Bulgarian Lev"), "BGL"), MyMoneyPrice("BGL", "BGN", QDate(1999, 7, 5), MyMoneyMoney(1, 1000), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("ROL", i18n("Romanian Leu"), "ROL"), MyMoneyPrice("ROL", "RON", QDate(2005, 6, 30), MyMoneyMoney(1, 10000), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("RUR", i18n("Russian Ruble (old)"), "RUR"), MyMoneyPrice("RUR", "RUB", QDate(1998, 1, 1), MyMoneyMoney(1, 1000), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("SIT", i18n("Slovenian Tolar"), "SIT"), MyMoneyPrice("SIT", "EUR", QDate(2006, 12, 31), MyMoneyMoney(1, 23964), QLatin1Literal("KMyMoney"))); // Source: http://www.tf-portfoliosolutions.net/products/turkishlira.aspx ancientCurrencies.insert(MyMoneySecurity("TRL", i18n("Turkish Lira (old)"), "TL"), MyMoneyPrice("TRL", "TRY", QDate(2004, 12, 31), MyMoneyMoney(1, 1000000), QLatin1Literal("KMyMoney"))); // Source: http://www.focus.de/finanzen/news/malta-und-zypern_aid_66058.html ancientCurrencies.insert(MyMoneySecurity("MTL", i18n("Maltese Lira"), "MTL"), MyMoneyPrice("MTL", "EUR", QDate(2008, 1, 1), MyMoneyMoney(429300, 1000000), QLatin1Literal("KMyMoney"))); ancientCurrencies.insert(MyMoneySecurity("CYP", i18n("Cyprus Pound"), QString("C%1").arg(QChar(0x00A3))), MyMoneyPrice("CYP", "EUR", QDate(2008, 1, 1), MyMoneyMoney(585274, 1000000), QLatin1Literal("KMyMoney"))); // Source: http://www.focus.de/finanzen/news/waehrungszone-slowakei-ist-neuer-euro-staat_aid_359025.html ancientCurrencies.insert(MyMoneySecurity("SKK", i18n("Slovak Koruna"), "SKK"), MyMoneyPrice("SKK", "EUR", QDate(2008, 12, 31), MyMoneyMoney(1000, 30126), QLatin1Literal("KMyMoney"))); // Source: http://en.wikipedia.org/wiki/Mozambican_metical ancientCurrencies.insert(MyMoneySecurity("MZM", i18n("Mozambique Metical"), "MT"), MyMoneyPrice("MZM", "MZN", QDate(2006, 7, 1), MyMoneyMoney(1, 1000), QLatin1Literal("KMyMoney"))); // Source https://en.wikipedia.org/wiki/Azerbaijani_manat ancientCurrencies.insert(MyMoneySecurity("AZM", i18n("Azerbaijani Manat"), "m."), MyMoneyPrice("AZM", "AZN", QDate(2006, 1, 1), MyMoneyMoney(1, 5000), QLatin1Literal("KMyMoney"))); // Source: https://en.wikipedia.org/wiki/Litas ancientCurrencies.insert(MyMoneySecurity("LTL", i18n("Lithuanian Litas"), "Lt"), MyMoneyPrice("LTL", "EUR", QDate(2015, 1, 1), MyMoneyMoney(100000, 345280), QLatin1Literal("KMyMoney"))); // Source: https://en.wikipedia.org/wiki/Belarusian_ruble ancientCurrencies.insert(MyMoneySecurity("BYR", i18n("Belarusian Ruble (old)"), "BYR"), MyMoneyPrice("BYR", "BYN", QDate(2016, 7, 1), MyMoneyMoney(1, 10000), QLatin1Literal("KMyMoney"))); return ancientCurrencies; } QList MyMoneyFile::availableCurrencyList() const { QList currencyList; currencyList.append(MyMoneySecurity("AFA", i18n("Afghanistan Afghani"))); currencyList.append(MyMoneySecurity("ALL", i18n("Albanian Lek"))); currencyList.append(MyMoneySecurity("ANG", i18n("Netherland Antillian Guilder"))); currencyList.append(MyMoneySecurity("DZD", i18n("Algerian Dinar"))); currencyList.append(MyMoneySecurity("ADF", i18n("Andorran Franc"))); currencyList.append(MyMoneySecurity("ADP", i18n("Andorran Peseta"))); currencyList.append(MyMoneySecurity("AON", i18n("Angolan New Kwanza"))); currencyList.append(MyMoneySecurity("ARS", i18n("Argentine Peso"), "$")); currencyList.append(MyMoneySecurity("AWG", i18n("Aruban Florin"))); currencyList.append(MyMoneySecurity("AUD", i18n("Australian Dollar"), "$")); currencyList.append(MyMoneySecurity("AZN", i18n("Azerbaijani Manat"), "m.")); currencyList.append(MyMoneySecurity("BSD", i18n("Bahamian Dollar"), "$")); currencyList.append(MyMoneySecurity("BHD", i18n("Bahraini Dinar"), "BHD", 1000)); currencyList.append(MyMoneySecurity("BDT", i18n("Bangladeshi Taka"))); currencyList.append(MyMoneySecurity("BBD", i18n("Barbados Dollar"), "$")); currencyList.append(MyMoneySecurity("BTC", i18n("Bitcoin"), "BTC")); currencyList.append(MyMoneySecurity("BYN", i18n("Belarusian Ruble"), "Br")); currencyList.append(MyMoneySecurity("BZD", i18n("Belize Dollar"), "$")); currencyList.append(MyMoneySecurity("BMD", i18n("Bermudian Dollar"), "$")); currencyList.append(MyMoneySecurity("BTN", i18n("Bhutan Ngultrum"))); currencyList.append(MyMoneySecurity("BOB", i18n("Bolivian Boliviano"))); currencyList.append(MyMoneySecurity("BAM", i18n("Bosnian Convertible Mark"))); currencyList.append(MyMoneySecurity("BWP", i18n("Botswana Pula"))); currencyList.append(MyMoneySecurity("BRL", i18n("Brazilian Real"), "R$")); currencyList.append(MyMoneySecurity("GBP", i18n("British Pound"), QChar(0x00A3))); currencyList.append(MyMoneySecurity("BND", i18n("Brunei Dollar"), "$")); currencyList.append(MyMoneySecurity("BGN", i18n("Bulgarian Lev (new)"))); currencyList.append(MyMoneySecurity("BIF", i18n("Burundi Franc"))); currencyList.append(MyMoneySecurity("XAF", i18n("CFA Franc BEAC"))); currencyList.append(MyMoneySecurity("XOF", i18n("CFA Franc BCEAO"))); currencyList.append(MyMoneySecurity("XPF", i18n("CFP Franc Pacifique"), "F", 1, 100)); currencyList.append(MyMoneySecurity("KHR", i18n("Cambodia Riel"))); currencyList.append(MyMoneySecurity("CAD", i18n("Canadian Dollar"), "$")); currencyList.append(MyMoneySecurity("CVE", i18n("Cape Verde Escudo"))); currencyList.append(MyMoneySecurity("KYD", i18n("Cayman Islands Dollar"), "$")); currencyList.append(MyMoneySecurity("CLP", i18n("Chilean Peso"))); currencyList.append(MyMoneySecurity("CNY", i18n("Chinese Yuan Renminbi"))); currencyList.append(MyMoneySecurity("COP", i18n("Colombian Peso"))); currencyList.append(MyMoneySecurity("KMF", i18n("Comoros Franc"))); currencyList.append(MyMoneySecurity("CRC", i18n("Costa Rican Colon"), QChar(0x20A1))); currencyList.append(MyMoneySecurity("HRK", i18n("Croatian Kuna"))); currencyList.append(MyMoneySecurity("CUP", i18n("Cuban Peso"))); currencyList.append(MyMoneySecurity("CZK", i18n("Czech Koruna"))); currencyList.append(MyMoneySecurity("DKK", i18n("Danish Krone"), "kr")); currencyList.append(MyMoneySecurity("DJF", i18n("Djibouti Franc"))); currencyList.append(MyMoneySecurity("DOP", i18n("Dominican Peso"))); currencyList.append(MyMoneySecurity("XCD", i18n("East Caribbean Dollar"), "$")); currencyList.append(MyMoneySecurity("EGP", i18n("Egyptian Pound"), QChar(0x00A3))); currencyList.append(MyMoneySecurity("SVC", i18n("El Salvador Colon"))); currencyList.append(MyMoneySecurity("ERN", i18n("Eritrean Nakfa"))); currencyList.append(MyMoneySecurity("EEK", i18n("Estonian Kroon"))); currencyList.append(MyMoneySecurity("ETB", i18n("Ethiopian Birr"))); currencyList.append(MyMoneySecurity("EUR", i18n("Euro"), QChar(0x20ac))); currencyList.append(MyMoneySecurity("FKP", i18n("Falkland Islands Pound"), QChar(0x00A3))); currencyList.append(MyMoneySecurity("FJD", i18n("Fiji Dollar"), "$")); currencyList.append(MyMoneySecurity("GMD", i18n("Gambian Dalasi"))); currencyList.append(MyMoneySecurity("GEL", i18n("Georgian Lari"))); currencyList.append(MyMoneySecurity("GHC", i18n("Ghanaian Cedi"))); currencyList.append(MyMoneySecurity("GIP", i18n("Gibraltar Pound"), QChar(0x00A3))); currencyList.append(MyMoneySecurity("GTQ", i18n("Guatemalan Quetzal"))); currencyList.append(MyMoneySecurity("GWP", i18n("Guinea-Bissau Peso"))); currencyList.append(MyMoneySecurity("GYD", i18n("Guyanan Dollar"), "$")); currencyList.append(MyMoneySecurity("HTG", i18n("Haitian Gourde"))); currencyList.append(MyMoneySecurity("HNL", i18n("Honduran Lempira"))); currencyList.append(MyMoneySecurity("HKD", i18n("Hong Kong Dollar"), "$")); currencyList.append(MyMoneySecurity("HUF", i18n("Hungarian Forint"), "HUF", 1, 100)); currencyList.append(MyMoneySecurity("ISK", i18n("Iceland Krona"))); currencyList.append(MyMoneySecurity("INR", i18n("Indian Rupee"), QChar(0x20A8))); currencyList.append(MyMoneySecurity("IDR", i18n("Indonesian Rupiah"), "IDR", 1)); currencyList.append(MyMoneySecurity("IRR", i18n("Iranian Rial"), "IRR", 1)); currencyList.append(MyMoneySecurity("IQD", i18n("Iraqi Dinar"), "IQD", 1000)); currencyList.append(MyMoneySecurity("ILS", i18n("Israeli New Shekel"), QChar(0x20AA))); currencyList.append(MyMoneySecurity("JMD", i18n("Jamaican Dollar"), "$")); currencyList.append(MyMoneySecurity("JPY", i18n("Japanese Yen"), QChar(0x00A5), 1)); currencyList.append(MyMoneySecurity("JOD", i18n("Jordanian Dinar"), "JOD", 1000)); currencyList.append(MyMoneySecurity("KZT", i18n("Kazakhstan Tenge"))); currencyList.append(MyMoneySecurity("KES", i18n("Kenyan Shilling"))); currencyList.append(MyMoneySecurity("KWD", i18n("Kuwaiti Dinar"), "KWD", 1000)); currencyList.append(MyMoneySecurity("KGS", i18n("Kyrgyzstan Som"))); currencyList.append(MyMoneySecurity("LAK", i18n("Laos Kip"), QChar(0x20AD))); currencyList.append(MyMoneySecurity("LVL", i18n("Latvian Lats"))); currencyList.append(MyMoneySecurity("LBP", i18n("Lebanese Pound"), QChar(0x00A3))); currencyList.append(MyMoneySecurity("LSL", i18n("Lesotho Loti"))); currencyList.append(MyMoneySecurity("LRD", i18n("Liberian Dollar"), "$")); currencyList.append(MyMoneySecurity("LYD", i18n("Libyan Dinar"), "LYD", 1000)); currencyList.append(MyMoneySecurity("MOP", i18n("Macau Pataca"))); currencyList.append(MyMoneySecurity("MKD", i18n("Macedonian Denar"))); currencyList.append(MyMoneySecurity("MGF", i18n("Malagasy Franc"), "MGF", 500)); currencyList.append(MyMoneySecurity("MWK", i18n("Malawi Kwacha"))); currencyList.append(MyMoneySecurity("MYR", i18n("Malaysian Ringgit"))); currencyList.append(MyMoneySecurity("MVR", i18n("Maldive Rufiyaa"))); currencyList.append(MyMoneySecurity("MLF", i18n("Mali Republic Franc"))); currencyList.append(MyMoneySecurity("MRO", i18n("Mauritanian Ouguiya"), "MRO", 5)); currencyList.append(MyMoneySecurity("MUR", i18n("Mauritius Rupee"))); currencyList.append(MyMoneySecurity("MXN", i18n("Mexican Peso"), "$")); currencyList.append(MyMoneySecurity("MDL", i18n("Moldavian Leu"))); currencyList.append(MyMoneySecurity("MNT", i18n("Mongolian Tugrik"), QChar(0x20AE))); currencyList.append(MyMoneySecurity("MAD", i18n("Moroccan Dirham"))); currencyList.append(MyMoneySecurity("MZN", i18n("Mozambique Metical"), "MT")); currencyList.append(MyMoneySecurity("MMK", i18n("Myanmar Kyat"))); currencyList.append(MyMoneySecurity("NAD", i18n("Namibian Dollar"), "$")); currencyList.append(MyMoneySecurity("NPR", i18n("Nepalese Rupee"))); currencyList.append(MyMoneySecurity("NZD", i18n("New Zealand Dollar"), "$")); currencyList.append(MyMoneySecurity("NIC", i18n("Nicaraguan Cordoba Oro"))); currencyList.append(MyMoneySecurity("NGN", i18n("Nigerian Naira"), QChar(0x20A6))); currencyList.append(MyMoneySecurity("KPW", i18n("North Korean Won"), QChar(0x20A9))); currencyList.append(MyMoneySecurity("NOK", i18n("Norwegian Kroner"), "kr")); currencyList.append(MyMoneySecurity("OMR", i18n("Omani Rial"), "OMR", 1000)); currencyList.append(MyMoneySecurity("PKR", i18n("Pakistan Rupee"))); currencyList.append(MyMoneySecurity("PAB", i18n("Panamanian Balboa"))); currencyList.append(MyMoneySecurity("PGK", i18n("Papua New Guinea Kina"))); currencyList.append(MyMoneySecurity("PYG", i18n("Paraguay Guarani"))); currencyList.append(MyMoneySecurity("PEN", i18n("Peruvian Nuevo Sol"))); currencyList.append(MyMoneySecurity("PHP", i18n("Philippine Peso"), QChar(0x20B1))); currencyList.append(MyMoneySecurity("PLN", i18n("Polish Zloty"))); currencyList.append(MyMoneySecurity("QAR", i18n("Qatari Rial"))); currencyList.append(MyMoneySecurity("RON", i18n("Romanian Leu (new)"))); currencyList.append(MyMoneySecurity("RUB", i18n("Russian Ruble"))); currencyList.append(MyMoneySecurity("RWF", i18n("Rwanda Franc"))); currencyList.append(MyMoneySecurity("WST", i18n("Samoan Tala"))); currencyList.append(MyMoneySecurity("STD", i18n("Sao Tome and Principe Dobra"))); currencyList.append(MyMoneySecurity("SAR", i18n("Saudi Riyal"))); currencyList.append(MyMoneySecurity("RSD", i18n("Serbian Dinar"))); currencyList.append(MyMoneySecurity("SCR", i18n("Seychelles Rupee"))); currencyList.append(MyMoneySecurity("SLL", i18n("Sierra Leone Leone"))); currencyList.append(MyMoneySecurity("SGD", i18n("Singapore Dollar"), "$")); currencyList.append(MyMoneySecurity("SBD", i18n("Solomon Islands Dollar"), "$")); currencyList.append(MyMoneySecurity("SOS", i18n("Somali Shilling"))); currencyList.append(MyMoneySecurity("ZAR", i18n("South African Rand"))); currencyList.append(MyMoneySecurity("KRW", i18n("South Korean Won"), QChar(0x20A9))); currencyList.append(MyMoneySecurity("LKR", i18n("Sri Lanka Rupee"))); currencyList.append(MyMoneySecurity("SHP", i18n("St. Helena Pound"), QChar(0x00A3))); currencyList.append(MyMoneySecurity("SDD", i18n("Sudanese Dinar"))); currencyList.append(MyMoneySecurity("SRG", i18n("Suriname Guilder"))); currencyList.append(MyMoneySecurity("SZL", i18n("Swaziland Lilangeni"))); currencyList.append(MyMoneySecurity("SEK", i18n("Swedish Krona"))); currencyList.append(MyMoneySecurity("CHF", i18n("Swiss Franc"), "SFr")); currencyList.append(MyMoneySecurity("SYP", i18n("Syrian Pound"), QChar(0x00A3))); currencyList.append(MyMoneySecurity("TWD", i18n("Taiwan Dollar"), "$")); currencyList.append(MyMoneySecurity("TJS", i18n("Tajikistan Somoni"))); currencyList.append(MyMoneySecurity("TZS", i18n("Tanzanian Shilling"))); currencyList.append(MyMoneySecurity("THB", i18n("Thai Baht"), QChar(0x0E3F))); currencyList.append(MyMoneySecurity("TOP", i18n("Tongan Pa'anga"))); currencyList.append(MyMoneySecurity("TTD", i18n("Trinidad and Tobago Dollar"), "$")); currencyList.append(MyMoneySecurity("TND", i18n("Tunisian Dinar"), "TND", 1000)); currencyList.append(MyMoneySecurity("TRY", i18n("Turkish Lira"), QChar(0x20BA))); currencyList.append(MyMoneySecurity("TMM", i18n("Turkmenistan Manat"))); currencyList.append(MyMoneySecurity("USD", i18n("US Dollar"), "$")); currencyList.append(MyMoneySecurity("UGX", i18n("Uganda Shilling"))); currencyList.append(MyMoneySecurity("UAH", i18n("Ukraine Hryvnia"))); currencyList.append(MyMoneySecurity("CLF", i18n("Unidad de Fometo"))); currencyList.append(MyMoneySecurity("AED", i18n("United Arab Emirates Dirham"))); currencyList.append(MyMoneySecurity("UYU", i18n("Uruguayan Peso"))); currencyList.append(MyMoneySecurity("UZS", i18n("Uzbekistani Sum"))); currencyList.append(MyMoneySecurity("VUV", i18n("Vanuatu Vatu"))); currencyList.append(MyMoneySecurity("VEB", i18n("Venezuelan Bolivar"))); currencyList.append(MyMoneySecurity("VND", i18n("Vietnamese Dong"), QChar(0x20AB))); currencyList.append(MyMoneySecurity("ZMK", i18n("Zambian Kwacha"))); currencyList.append(MyMoneySecurity("ZWD", i18n("Zimbabwe Dollar"), "$")); currencyList.append(ancientCurrencies().keys()); // sort the currencies ... qSort(currencyList.begin(), currencyList.end(), [] (const MyMoneySecurity& c1, const MyMoneySecurity& c2) { return c1.name().compare(c2.name()) < 0; }); // ... and add a few precious metals at the ned currencyList.append(MyMoneySecurity("XAU", i18n("Gold"), "XAU", 1000000)); currencyList.append(MyMoneySecurity("XPD", i18n("Palladium"), "XPD", 1000000)); currencyList.append(MyMoneySecurity("XPT", i18n("Platinum"), "XPT", 1000000)); currencyList.append(MyMoneySecurity("XAG", i18n("Silver"), "XAG", 1000000)); return currencyList; } QList MyMoneyFile::currencyList() const { d->checkStorage(); return d->m_storage->currencyList(); } QString MyMoneyFile::foreignCurrency(const QString& first, const QString& second) const { if (baseCurrency().id() == second) return first; return second; } MyMoneySecurity MyMoneyFile::baseCurrency() const { if (d->m_baseCurrency.id().isEmpty()) { QString id = QString(value("kmm-baseCurrency")); if (!id.isEmpty()) d->m_baseCurrency = currency(id); } return d->m_baseCurrency; } void MyMoneyFile::setBaseCurrency(const MyMoneySecurity& curr) { // make sure the currency exists MyMoneySecurity c = currency(curr.id()); if (c.id() != d->m_baseCurrency.id()) { setValue("kmm-baseCurrency", curr.id()); // force reload of base currency cache d->m_baseCurrency = MyMoneySecurity(); } } void MyMoneyFile::addPrice(const MyMoneyPrice& price) { if (price.rate(QString()).isZero()) return; d->checkTransaction(Q_FUNC_INFO); // store the account's which are affected by this price regarding their value d->priceChanged(*this, price); d->m_storage->addPrice(price); } void MyMoneyFile::removePrice(const MyMoneyPrice& price) { d->checkTransaction(Q_FUNC_INFO); // store the account's which are affected by this price regarding their value d->priceChanged(*this, price); d->m_storage->removePrice(price); } MyMoneyPrice MyMoneyFile::price(const QString& fromId, const QString& toId, const QDate& date, const bool exactDate) const { d->checkStorage(); QString to(toId); if (to.isEmpty()) to = value("kmm-baseCurrency"); // if some id is missing, we can return an empty price object if (fromId.isEmpty() || to.isEmpty()) return MyMoneyPrice(); // we don't search our tables if someone asks stupid stuff if (fromId == toId) { return MyMoneyPrice(fromId, toId, date, MyMoneyMoney::ONE, "KMyMoney"); } // if not asking for exact date, try to find the exact date match first, // either the requested price or its reciprocal value. If unsuccessful, it will move // on and look for prices of previous dates MyMoneyPrice rc = d->m_storage->price(fromId, to, date, true); if (!rc.isValid()) { // not found, search 'to-from' rate and use reciprocal value rc = d->m_storage->price(to, fromId, date, true); // not found, search previous dates, if exact date is not needed if (!exactDate && !rc.isValid()) { // search 'from-to' and 'to-from', select the most recent one MyMoneyPrice fromPrice = d->m_storage->price(fromId, to, date, exactDate); MyMoneyPrice toPrice = d->m_storage->price(to, fromId, date, exactDate); // check first whether both prices are valid if (fromPrice.isValid() && toPrice.isValid()) { if (fromPrice.date() >= toPrice.date()) { // if 'from-to' is newer or the same date, prefer that one rc = fromPrice; } else { // otherwise, use the reciprocal price rc = toPrice; } } else if (fromPrice.isValid()) { // check if any of the prices is valid, return that one rc = fromPrice; } else if (toPrice.isValid()) { rc = toPrice; } } } return rc; } MyMoneyPrice MyMoneyFile::price(const QString& fromId, const QString& toId) const { return price(fromId, toId, QDate::currentDate(), false); } MyMoneyPrice MyMoneyFile::price(const QString& fromId) const { return price(fromId, QString(), QDate::currentDate(), false); } MyMoneyPriceList MyMoneyFile::priceList() const { d->checkStorage(); return d->m_storage->priceList(); } bool MyMoneyFile::hasAccount(const QString& id, const QString& name) const { const auto accounts = account(id).accountList(); for (const auto& acc : accounts) { if (account(acc).name().compare(name) == 0) return true; } return false; } QList MyMoneyFile::reportList() const { d->checkStorage(); return d->m_storage->reportList(); } void MyMoneyFile::addReport(MyMoneyReport& report) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->addReport(report); } void MyMoneyFile::modifyReport(const MyMoneyReport& report) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->modifyReport(report); } unsigned MyMoneyFile::countReports() const { d->checkStorage(); return d->m_storage->countReports(); } MyMoneyReport MyMoneyFile::report(const QString& id) const { d->checkStorage(); return d->m_storage->report(id); } void MyMoneyFile::removeReport(const MyMoneyReport& report) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->removeReport(report); } QList MyMoneyFile::budgetList() const { d->checkStorage(); return d->m_storage->budgetList(); } void MyMoneyFile::addBudget(MyMoneyBudget &budget) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->addBudget(budget); } MyMoneyBudget MyMoneyFile::budgetByName(const QString& name) const { d->checkStorage(); return d->m_storage->budgetByName(name); } void MyMoneyFile::modifyBudget(const MyMoneyBudget& budget) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->modifyBudget(budget); } unsigned MyMoneyFile::countBudgets() const { d->checkStorage(); return d->m_storage->countBudgets(); } MyMoneyBudget MyMoneyFile::budget(const QString& id) const { d->checkStorage(); return d->m_storage->budget(id); } void MyMoneyFile::removeBudget(const MyMoneyBudget& budget) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->removeBudget(budget); } void MyMoneyFile::addOnlineJob(onlineJob& job) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->addOnlineJob(job); d->m_changeSet += MyMoneyNotification(File::Mode::Add, job); } void MyMoneyFile::modifyOnlineJob(const onlineJob job) { d->checkTransaction(Q_FUNC_INFO); d->m_storage->modifyOnlineJob(job); d->m_changeSet += MyMoneyNotification(File::Mode::Modify, job); } onlineJob MyMoneyFile::getOnlineJob(const QString &jobId) const { d->checkStorage(); return d->m_storage->getOnlineJob(jobId); } QList MyMoneyFile::onlineJobList() const { d->checkStorage(); return d->m_storage->onlineJobList(); } /** @todo improve speed by passing count job to m_storage */ int MyMoneyFile::countOnlineJobs() const { return onlineJobList().count(); } /** * @brief Remove onlineJob * @param job onlineJob to remove */ void MyMoneyFile::removeOnlineJob(const onlineJob& job) { d->checkTransaction(Q_FUNC_INFO); // clear all changed objects from cache if (job.isLocked()) { return; } d->m_changeSet += MyMoneyNotification(File::Mode::Remove, job); d->m_storage->removeOnlineJob(job); } void MyMoneyFile::removeOnlineJob(const QStringList onlineJobIds) { foreach (QString jobId, onlineJobIds) { removeOnlineJob(getOnlineJob(jobId)); } } void MyMoneyFile::costCenterList(QList< MyMoneyCostCenter >& list) const { d->checkStorage(); list = d->m_storage->costCenterList(); } bool MyMoneyFile::addVATSplit(MyMoneyTransaction& transaction, const MyMoneyAccount& account, const MyMoneyAccount& category, const MyMoneyMoney& amount) { bool rc = false; try { MyMoneySplit cat; // category MyMoneySplit tax; // tax if (category.value("VatAccount").isEmpty()) return false; MyMoneyAccount vatAcc = this->account(category.value("VatAccount").toLatin1()); const MyMoneySecurity& asec = security(account.currencyId()); const MyMoneySecurity& csec = security(category.currencyId()); const MyMoneySecurity& vsec = security(vatAcc.currencyId()); if (asec.id() != csec.id() || asec.id() != vsec.id()) { qDebug("Auto VAT assignment only works if all three accounts use the same currency."); return false; } MyMoneyMoney vatRate(vatAcc.value("VatRate")); MyMoneyMoney gv, nv; // gross value, net value int fract = account.fraction(); if (!vatRate.isZero()) { tax.setAccountId(vatAcc.id()); // qDebug("vat amount is '%s'", category.value("VatAmount").toLatin1()); if (category.value("VatAmount").toLower() != QString("net")) { // split value is the gross value gv = amount; nv = (gv / (MyMoneyMoney::ONE + vatRate)).convert(fract); MyMoneySplit catSplit = transaction.splitByAccount(account.id(), false); catSplit.setShares(-nv); catSplit.setValue(catSplit.shares()); transaction.modifySplit(catSplit); } else { // split value is the net value nv = amount; gv = (nv * (MyMoneyMoney::ONE + vatRate)).convert(fract); MyMoneySplit accSplit = transaction.splitByAccount(account.id()); accSplit.setValue(gv.convert(fract)); accSplit.setShares(accSplit.value()); transaction.modifySplit(accSplit); } tax.setValue(-(gv - nv).convert(fract)); tax.setShares(tax.value()); transaction.addSplit(tax); rc = true; } } catch (const MyMoneyException &) { } return rc; } bool MyMoneyFile::isReferenced(const MyMoneyObject& obj, const QBitArray& skipChecks) const { d->checkStorage(); return d->m_storage->isReferenced(obj, skipChecks); } bool MyMoneyFile::isReferenced(const MyMoneyObject& obj) const { return isReferenced(obj, QBitArray((int)eStorage::Reference::Count)); } bool MyMoneyFile::checkNoUsed(const QString& accId, const QString& no) const { // by definition, an empty string or a non-numeric string is not used QRegExp exp(QString("(.*\\D)?(\\d+)(\\D.*)?")); if (no.isEmpty() || exp.indexIn(no) == -1) return false; MyMoneyTransactionFilter filter; filter.addAccount(accId); QList transactions = transactionList(filter); QList::ConstIterator it_t = transactions.constBegin(); while (it_t != transactions.constEnd()) { try { MyMoneySplit split; // Test whether the transaction also includes a split into // this account split = (*it_t).splitByAccount(accId, true /*match*/); if (!split.number().isEmpty() && split.number() == no) return true; } catch (const MyMoneyException &) { } ++it_t; } return false; } QString MyMoneyFile::highestCheckNo(const QString& accId) const { unsigned64 lno = 0; unsigned64 cno; QString no; MyMoneyTransactionFilter filter; filter.addAccount(accId); QList transactions = transactionList(filter); QList::ConstIterator it_t = transactions.constBegin(); while (it_t != transactions.constEnd()) { try { // Test whether the transaction also includes a split into // this account MyMoneySplit split = (*it_t).splitByAccount(accId, true /*match*/); if (!split.number().isEmpty()) { // non-numerical values stored in number will return 0 in the next line cno = split.number().toULongLong(); if (cno > lno) { lno = cno; no = split.number(); } } } catch (const MyMoneyException &) { } ++it_t; } return no; } bool MyMoneyFile::hasNewerTransaction(const QString& accId, const QDate& date) const { MyMoneyTransactionFilter filter; filter.addAccount(accId); filter.setDateFilter(date.addDays(+1), QDate()); return !transactionList(filter).isEmpty(); } void MyMoneyFile::clearCache() { d->checkStorage(); d->m_balanceCache.clear(); } void MyMoneyFile::forceDataChanged() { emit dataChanged(); } bool MyMoneyFile::isTransfer(const MyMoneyTransaction& t) const { auto rc = false; if (t.splitCount() == 2) { foreach (const auto split, t.splits()) { auto acc = account(split.accountId()); if (acc.isIncomeExpense()) { rc = true; break; } } } return rc; } bool MyMoneyFile::referencesClosedAccount(const MyMoneyTransaction& t) const { auto ret = false; foreach (const auto split, t.splits()) { if (referencesClosedAccount(split)) { ret = true; break; } } return ret; } bool MyMoneyFile::referencesClosedAccount(const MyMoneySplit& s) const { if (s.accountId().isEmpty()) return false; try { return account(s.accountId()).isClosed(); } catch (const MyMoneyException &) { } return false; } QString MyMoneyFile::storageId() { QString id = value("kmm-id"); if (id.isEmpty()) { MyMoneyFileTransaction ft; try { QUuid uid = QUuid::createUuid(); setValue("kmm-id", uid.toString()); ft.commit(); id = uid.toString(); } catch (const MyMoneyException &) { qDebug("Unable to setup UID for new storage object"); } } return id; } QString MyMoneyFile::openingBalancesPrefix() { return i18n("Opening Balances"); } bool MyMoneyFile::hasMatchingOnlineBalance(const MyMoneyAccount& _acc) const { // get current values auto acc = account(_acc.id()); // if there's no last transaction import data we are done if (acc.value("lastImportedTransactionDate").isEmpty() || acc.value("lastStatementBalance").isEmpty()) return false; // otherwise, we compare the balances MyMoneyMoney balance(acc.value("lastStatementBalance")); MyMoneyMoney accBalance = this->balance(acc.id(), QDate::fromString(acc.value("lastImportedTransactionDate"), Qt::ISODate)); return balance == accBalance; } int MyMoneyFile::countTransactionsWithSpecificReconciliationState(const QString& accId, TransactionFilter::State state) const { MyMoneyTransactionFilter filter; filter.addAccount(accId); filter.addState((int)state); return transactionList(filter).count(); } /** * Make sure that the splits value has the precision of the corresponding account */ void MyMoneyFile::fixSplitPrecision(MyMoneyTransaction& t) const { auto transactionSecurity = security(t.commodity()); auto transactionFraction = transactionSecurity.smallestAccountFraction(); for (auto& split : t.splits()) { auto acc = account(split.accountId()); auto fraction = acc.fraction(); if(fraction == -1) { auto sec = security(acc.currencyId()); fraction = acc.fraction(sec); } split.setShares(static_cast(split.shares().convertDenominator(fraction).canonicalize())); split.setValue(static_cast(split.value().convertDenominator(transactionFraction).canonicalize())); } } class MyMoneyFileTransactionPrivate { Q_DISABLE_COPY(MyMoneyFileTransactionPrivate) public: MyMoneyFileTransactionPrivate() : m_isNested(MyMoneyFile::instance()->hasTransaction()), m_needRollback(!m_isNested) { } public: bool m_isNested; bool m_needRollback; }; MyMoneyFileTransaction::MyMoneyFileTransaction() : d_ptr(new MyMoneyFileTransactionPrivate) { Q_D(MyMoneyFileTransaction); if (!d->m_isNested) MyMoneyFile::instance()->startTransaction(); } MyMoneyFileTransaction::~MyMoneyFileTransaction() { - rollback(); + try { + rollback(); + } catch (const MyMoneyException & e) { + qDebug() << e.what(); + } Q_D(MyMoneyFileTransaction); delete d; } void MyMoneyFileTransaction::restart() { rollback(); Q_D(MyMoneyFileTransaction); d->m_needRollback = !d->m_isNested; if (!d->m_isNested) MyMoneyFile::instance()->startTransaction(); } void MyMoneyFileTransaction::commit() { Q_D(MyMoneyFileTransaction); if (!d->m_isNested) MyMoneyFile::instance()->commitTransaction(); d->m_needRollback = false; } void MyMoneyFileTransaction::rollback() { Q_D(MyMoneyFileTransaction); if (d->m_needRollback) MyMoneyFile::instance()->rollbackTransaction(); d->m_needRollback = false; } diff --git a/kmymoney/mymoney/mymoneytransactionfilter.cpp b/kmymoney/mymoney/mymoneytransactionfilter.cpp index ebb5a9eb2..01293a0b0 100644 --- a/kmymoney/mymoney/mymoneytransactionfilter.cpp +++ b/kmymoney/mymoney/mymoneytransactionfilter.cpp @@ -1,986 +1,985 @@ /*************************************************************************** mymoneytransactionfilter.cpp - description ------------------- begin : Fri Aug 22 2003 copyright : (C) 2003 by Thomas Baumgart 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 "mymoneytransactionfilter.h" // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "mymoneymoney.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneypayee.h" #include "mymoneytag.h" #include "mymoneytransaction.h" #include "mymoneysplit.h" #include "mymoneyenums.h" class MyMoneyTransactionFilterPrivate { public: MyMoneyTransactionFilterPrivate() : m_reportAllSplits(false) , m_considerCategory(false) , m_matchOnly(false) , m_matchingSplitsCount(0) , m_invertText(false) { m_filterSet.allFilter = 0; - m_invertText = false; } MyMoneyTransactionFilter::FilterSet m_filterSet; bool m_reportAllSplits; bool m_considerCategory; bool m_matchOnly; uint m_matchingSplitsCount; QRegExp m_text; bool m_invertText; QHash m_accounts; QHash m_payees; QHash m_tags; QHash m_categories; QHash m_states; QHash m_types; QHash m_validity; QString m_fromNr, m_toNr; QDate m_fromDate, m_toDate; MyMoneyMoney m_fromAmount, m_toAmount; }; MyMoneyTransactionFilter::MyMoneyTransactionFilter() : d_ptr(new MyMoneyTransactionFilterPrivate) { Q_D(MyMoneyTransactionFilter); d->m_reportAllSplits = true; d->m_considerCategory = true; } MyMoneyTransactionFilter::MyMoneyTransactionFilter(const QString& id) : d_ptr(new MyMoneyTransactionFilterPrivate) { addAccount(id); } MyMoneyTransactionFilter::MyMoneyTransactionFilter(const MyMoneyTransactionFilter& other) : d_ptr(new MyMoneyTransactionFilterPrivate(*other.d_func())) { } MyMoneyTransactionFilter::~MyMoneyTransactionFilter() { Q_D(MyMoneyTransactionFilter); delete d; } void MyMoneyTransactionFilter::clear() { Q_D(MyMoneyTransactionFilter); d->m_filterSet.allFilter = 0; d->m_invertText = false; d->m_accounts.clear(); d->m_categories.clear(); d->m_payees.clear(); d->m_tags.clear(); d->m_types.clear(); d->m_states.clear(); d->m_validity.clear(); d->m_fromDate = QDate(); d->m_toDate = QDate(); } void MyMoneyTransactionFilter::clearAccountFilter() { Q_D(MyMoneyTransactionFilter); d->m_filterSet.singleFilter.accountFilter = 0; d->m_accounts.clear(); } void MyMoneyTransactionFilter::setTextFilter(const QRegExp& text, bool invert) { Q_D(MyMoneyTransactionFilter); d->m_filterSet.singleFilter.textFilter = 1; d->m_invertText = invert; d->m_text = text; } void MyMoneyTransactionFilter::addAccount(const QStringList& ids) { Q_D(MyMoneyTransactionFilter); d->m_filterSet.singleFilter.accountFilter = 1; for (const auto& id : ids) addAccount(id); } void MyMoneyTransactionFilter::addAccount(const QString& id) { Q_D(MyMoneyTransactionFilter); if (!d->m_accounts.isEmpty() && !id.isEmpty() && d->m_accounts.contains(id)) return; d->m_filterSet.singleFilter.accountFilter = 1; if (!id.isEmpty()) d->m_accounts.insert(id, QString()); } void MyMoneyTransactionFilter::addCategory(const QStringList& ids) { Q_D(MyMoneyTransactionFilter); d->m_filterSet.singleFilter.categoryFilter = 1; for (const auto& id : ids) addCategory(id); } void MyMoneyTransactionFilter::addCategory(const QString& id) { Q_D(MyMoneyTransactionFilter); if (!d->m_categories.isEmpty() && !id.isEmpty() && d->m_categories.contains(id)) return; d->m_filterSet.singleFilter.categoryFilter = 1; if (!id.isEmpty()) d->m_categories.insert(id, QString()); } void MyMoneyTransactionFilter::setDateFilter(const QDate& from, const QDate& to) { Q_D(MyMoneyTransactionFilter); d->m_filterSet.singleFilter.dateFilter = from.isValid() | to.isValid(); d->m_fromDate = from; d->m_toDate = to; } void MyMoneyTransactionFilter::setAmountFilter(const MyMoneyMoney& from, const MyMoneyMoney& to) { Q_D(MyMoneyTransactionFilter); d->m_filterSet.singleFilter.amountFilter = 1; d->m_fromAmount = from.abs(); d->m_toAmount = to.abs(); // make sure that the user does not try to fool us ;-) if (from > to) std::swap(d->m_fromAmount, d->m_toAmount); } void MyMoneyTransactionFilter::addPayee(const QString& id) { Q_D(MyMoneyTransactionFilter); if (!d->m_payees.isEmpty() && !id.isEmpty() && d->m_payees.contains(id)) return; d->m_filterSet.singleFilter.payeeFilter = 1; if (!id.isEmpty()) d->m_payees.insert(id, QString()); } void MyMoneyTransactionFilter::addTag(const QString& id) { Q_D(MyMoneyTransactionFilter); if (!d->m_tags.isEmpty() && !id.isEmpty() && d->m_tags.contains(id)) return; d->m_filterSet.singleFilter.tagFilter = 1; if (!id.isEmpty()) d->m_tags.insert(id, QString()); } void MyMoneyTransactionFilter::addType(const int type) { Q_D(MyMoneyTransactionFilter); if (!d->m_types.isEmpty() && d->m_types.contains(type)) return; d->m_filterSet.singleFilter.typeFilter = 1; d->m_types.insert(type, QString()); } void MyMoneyTransactionFilter::addState(const int state) { Q_D(MyMoneyTransactionFilter); if (!d->m_states.isEmpty() && d->m_states.contains(state)) return; d->m_filterSet.singleFilter.stateFilter = 1; d->m_states.insert(state, QString()); } void MyMoneyTransactionFilter::addValidity(const int type) { Q_D(MyMoneyTransactionFilter); if (!d->m_validity.isEmpty() && d->m_validity.contains(type)) return; d->m_filterSet.singleFilter.validityFilter = 1; d->m_validity.insert(type, QString()); } void MyMoneyTransactionFilter::setNumberFilter(const QString& from, const QString& to) { Q_D(MyMoneyTransactionFilter); d->m_filterSet.singleFilter.nrFilter = 1; d->m_fromNr = from; d->m_toNr = to; } void MyMoneyTransactionFilter::setReportAllSplits(const bool report) { Q_D(MyMoneyTransactionFilter); d->m_reportAllSplits = report; } void MyMoneyTransactionFilter::setConsiderCategory(const bool check) { Q_D(MyMoneyTransactionFilter); d->m_considerCategory = check; } uint MyMoneyTransactionFilter::matchingSplitsCount(const MyMoneyTransaction& transaction) { Q_D(MyMoneyTransactionFilter); d->m_matchOnly = true; matchingSplits(transaction); d->m_matchOnly = false; return d->m_matchingSplitsCount; } QVector MyMoneyTransactionFilter::matchingSplits(const MyMoneyTransaction& transaction) { Q_D(MyMoneyTransactionFilter); QVector matchingSplits; const auto file = MyMoneyFile::instance(); // qDebug("T: %s", transaction.id().data()); // if no filter is set, we can safely return a match // if we should report all splits, then we collect them if (!d->m_filterSet.allFilter && d->m_reportAllSplits) { d->m_matchingSplitsCount = transaction.splitCount(); if (!d->m_matchOnly) matchingSplits = QVector::fromList(transaction.splits()); return matchingSplits; } d->m_matchingSplitsCount = 0; const auto filter = d->m_filterSet.singleFilter; // perform checks on the MyMoneyTransaction object first // check the date range if (filter.dateFilter) { if ((d->m_fromDate != QDate() && transaction.postDate() < d->m_fromDate) || (d->m_toDate != QDate() && transaction.postDate() > d->m_toDate)) { return matchingSplits; } } auto categoryMatched = !filter.categoryFilter; auto accountMatched = !filter.accountFilter; auto isTransfer = true; // check the transaction's validity if (filter.validityFilter) { if (!d->m_validity.isEmpty() && !d->m_validity.contains((int)validTransaction(transaction))) return matchingSplits; } // if d->m_reportAllSplits == false.. // ...then we don't need splits... // ...but we need to know if there were any found auto isMatchingSplitsEmpty = true; auto extendedFilter = d->m_filterSet; extendedFilter.singleFilter.dateFilter = extendedFilter.singleFilter.accountFilter = extendedFilter.singleFilter.categoryFilter = 0; if (filter.accountFilter || filter.categoryFilter || extendedFilter.allFilter) { const auto& splits = transaction.splits(); for (const auto& s : splits) { if (filter.accountFilter || filter.categoryFilter) { auto removeSplit = true; if (d->m_considerCategory) { switch (file->account(s.accountId()).accountGroup()) { case eMyMoney::Account::Type::Income: case eMyMoney::Account::Type::Expense: isTransfer = false; // check if the split references one of the categories in the list if (filter.categoryFilter) { if (d->m_categories.isEmpty()) { // we're looking for transactions with 'no' categories d->m_matchingSplitsCount = 0; matchingSplits.clear(); return matchingSplits; } else if (d->m_categories.contains(s.accountId())) { categoryMatched = true; removeSplit = false; } } break; default: // check if the split references one of the accounts in the list if (!filter.accountFilter) { removeSplit = false; } else if (!d->m_accounts.isEmpty() && d->m_accounts.contains(s.accountId())) { accountMatched = true; removeSplit = false; } break; } } else { if (!filter.accountFilter) { removeSplit = false; } else if (!d->m_accounts.isEmpty() && d->m_accounts.contains(s.accountId())) { accountMatched = true; removeSplit = false; } } if (removeSplit) continue; } // check if less frequent filters are active if (extendedFilter.allFilter) { const auto acc = file->account(s.accountId()); if (!(matchAmount(s) && matchText(s, acc))) continue; // Determine if this account is a category or an account auto isCategory = false; switch (acc.accountGroup()) { case eMyMoney::Account::Type::Income: case eMyMoney::Account::Type::Expense: isCategory = true; default: break; } if (!isCategory) { // check the payee list if (filter.payeeFilter) { if (!d->m_payees.isEmpty()) { if (s.payeeId().isEmpty() || !d->m_payees.contains(s.payeeId())) continue; } else if (!s.payeeId().isEmpty()) continue; } // check the tag list if (filter.tagFilter) { const auto tags = s.tagIdList(); if (!d->m_tags.isEmpty()) { if (tags.isEmpty()) { continue; } else { auto found = false; for (const auto& tag : tags) { if (d->m_tags.contains(tag)) { found = true; break; } } if (!found) continue; } } else if (!tags.isEmpty()) continue; } // check the type list if (filter.typeFilter && !d->m_types.isEmpty() && !d->m_types.contains(splitType(transaction, s, acc))) continue; // check the state list if (filter.stateFilter && !d->m_states.isEmpty() && !d->m_states.contains(splitState(s))) continue; if (filter.nrFilter && ((!d->m_fromNr.isEmpty() && s.number() < d->m_fromNr) || (!d->m_toNr.isEmpty() && s.number() > d->m_toNr))) continue; } else if (filter.payeeFilter || filter.tagFilter || filter.typeFilter || filter.stateFilter || filter.nrFilter) { continue; } } if (d->m_reportAllSplits) matchingSplits.append(s); isMatchingSplitsEmpty = false; } } else if (d->m_reportAllSplits) { const auto& splits = transaction.splits(); for (const auto& s : splits) matchingSplits.append(s); d->m_matchingSplitsCount = matchingSplits.count(); return matchingSplits; } else if (transaction.splitCount() > 0) { isMatchingSplitsEmpty = false; } // check if we're looking for transactions without assigned category if (!categoryMatched && transaction.splitCount() == 1 && d->m_categories.isEmpty()) categoryMatched = true; // if there's no category filter and the category did not // match, then we still want to see this transaction if it's // a transfer if (!categoryMatched && !filter.categoryFilter) categoryMatched = isTransfer; if (isMatchingSplitsEmpty || !(accountMatched && categoryMatched)) { d->m_matchingSplitsCount = 0; return matchingSplits; } if (!d->m_reportAllSplits && !isMatchingSplitsEmpty) { d->m_matchingSplitsCount = 1; if (!d->m_matchOnly) matchingSplits.append(transaction.firstSplit()); } else { d->m_matchingSplitsCount = matchingSplits.count(); } // all filters passed, I guess we have a match // qDebug(" C: %d", m_matchingSplits.count()); return matchingSplits; } QDate MyMoneyTransactionFilter::fromDate() const { Q_D(const MyMoneyTransactionFilter); return d->m_fromDate; } QDate MyMoneyTransactionFilter::toDate() const { Q_D(const MyMoneyTransactionFilter); return d->m_toDate; } bool MyMoneyTransactionFilter::matchText(const MyMoneySplit& s, const MyMoneyAccount& acc) const { Q_D(const MyMoneyTransactionFilter); // check if the text is contained in one of the fields // memo, value, number, payee, tag, account, date if (d->m_filterSet.singleFilter.textFilter) { const auto file = MyMoneyFile::instance(); const auto sec = file->security(acc.currencyId()); if (s.memo().contains(d->m_text) || s.shares().formatMoney(acc.fraction(sec)).contains(d->m_text) || s.value().formatMoney(acc.fraction(sec)).contains(d->m_text) || s.number().contains(d->m_text) || (d->m_text.pattern().compare(s.transactionId())) == 0) return !d->m_invertText; if (acc.name().contains(d->m_text)) return !d->m_invertText; if (!s.payeeId().isEmpty() && file->payee(s.payeeId()).name().contains(d->m_text)) return !d->m_invertText; for (const auto& tag : s.tagIdList()) if (file->tag(tag).name().contains(d->m_text)) return !d->m_invertText; return d->m_invertText; } return true; } bool MyMoneyTransactionFilter::matchAmount(const MyMoneySplit& s) const { Q_D(const MyMoneyTransactionFilter); if (d->m_filterSet.singleFilter.amountFilter) { const auto value = s.value().abs(); const auto shares = s.shares().abs(); if ((value < d->m_fromAmount || value > d->m_toAmount) && (shares < d->m_fromAmount || shares > d->m_toAmount)) return false; } return true; } bool MyMoneyTransactionFilter::match(const MyMoneySplit& s) const { const auto& acc = MyMoneyFile::instance()->account(s.accountId()); return matchText(s, acc) && matchAmount(s); } bool MyMoneyTransactionFilter::match(const MyMoneyTransaction& transaction) { Q_D(MyMoneyTransactionFilter); d->m_matchOnly = true; matchingSplits(transaction); d->m_matchOnly = false; return d->m_matchingSplitsCount > 0; } int MyMoneyTransactionFilter::splitState(const MyMoneySplit& split) const { switch (split.reconcileFlag()) { default: case eMyMoney::Split::State::NotReconciled: return (int)eMyMoney::TransactionFilter::State::NotReconciled; case eMyMoney::Split::State::Cleared: return (int)eMyMoney::TransactionFilter::State::Cleared; case eMyMoney::Split::State::Reconciled: return (int)eMyMoney::TransactionFilter::State::Reconciled; case eMyMoney::Split::State::Frozen: return (int)eMyMoney::TransactionFilter::State::Frozen; } } int MyMoneyTransactionFilter::splitType(const MyMoneyTransaction& t, const MyMoneySplit& split, const MyMoneyAccount& acc) const { qDebug() << "SplitType"; if (acc.isIncomeExpense()) return (int)eMyMoney::TransactionFilter::Type::All; if (t.splitCount() == 2) { const auto& splits = t.splits(); const auto file = MyMoneyFile::instance(); const auto& a = splits.at(0).id().compare(split.id()) == 0 ? acc : file->account(splits.at(0).accountId()); const auto& b = splits.at(1).id().compare(split.id()) == 0 ? acc : file->account(splits.at(1).accountId()); qDebug() << "first split: " << splits.at(0).accountId() << "second split: " << splits.at(1).accountId(); if (!a.isIncomeExpense() && !b.isIncomeExpense()) return (int)eMyMoney::TransactionFilter::Type::Transfers; } if (split.value().isPositive()) return (int)eMyMoney::TransactionFilter::Type::Deposits; return (int)eMyMoney::TransactionFilter::Type::Payments; } eMyMoney::TransactionFilter::Validity MyMoneyTransactionFilter::validTransaction(const MyMoneyTransaction& t) const { MyMoneyMoney val; for (const auto& split : t.splits()) val += split.value(); return (val == MyMoneyMoney()) ? eMyMoney::TransactionFilter::Validity::Valid : eMyMoney::TransactionFilter::Validity::Invalid; } bool MyMoneyTransactionFilter::includesCategory(const QString& cat) const { Q_D(const MyMoneyTransactionFilter); return !d->m_filterSet.singleFilter.categoryFilter || d->m_categories.contains(cat); } bool MyMoneyTransactionFilter::includesAccount(const QString& acc) const { Q_D(const MyMoneyTransactionFilter); return !d->m_filterSet.singleFilter.accountFilter || d->m_accounts.contains(acc); } bool MyMoneyTransactionFilter::includesPayee(const QString& pye) const { Q_D(const MyMoneyTransactionFilter); return !d->m_filterSet.singleFilter.payeeFilter || d->m_payees.contains(pye); } bool MyMoneyTransactionFilter::includesTag(const QString& tag) const { Q_D(const MyMoneyTransactionFilter); return !d->m_filterSet.singleFilter.tagFilter || d->m_tags.contains(tag); } bool MyMoneyTransactionFilter::dateFilter(QDate& from, QDate& to) const { Q_D(const MyMoneyTransactionFilter); from = d->m_fromDate; to = d->m_toDate; return d->m_filterSet.singleFilter.dateFilter == 1; } bool MyMoneyTransactionFilter::amountFilter(MyMoneyMoney& from, MyMoneyMoney& to) const { Q_D(const MyMoneyTransactionFilter); from = d->m_fromAmount; to = d->m_toAmount; return d->m_filterSet.singleFilter.amountFilter == 1; } bool MyMoneyTransactionFilter::numberFilter(QString& from, QString& to) const { Q_D(const MyMoneyTransactionFilter); from = d->m_fromNr; to = d->m_toNr; return d->m_filterSet.singleFilter.nrFilter == 1; } bool MyMoneyTransactionFilter::payees(QStringList& list) const { Q_D(const MyMoneyTransactionFilter); auto result = d->m_filterSet.singleFilter.payeeFilter; if (result) { QHashIterator it_payee(d->m_payees); while (it_payee.hasNext()) { it_payee.next(); list += it_payee.key(); } } return result; } bool MyMoneyTransactionFilter::tags(QStringList& list) const { Q_D(const MyMoneyTransactionFilter); auto result = d->m_filterSet.singleFilter.tagFilter; if (result) { QHashIterator it_tag(d->m_tags); while (it_tag.hasNext()) { it_tag.next(); list += it_tag.key(); } } return result; } bool MyMoneyTransactionFilter::accounts(QStringList& list) const { Q_D(const MyMoneyTransactionFilter); auto result = d->m_filterSet.singleFilter.accountFilter; if (result) { QHashIterator it_account(d->m_accounts); while (it_account.hasNext()) { it_account.next(); QString account = it_account.key(); list += account; } } return result; } bool MyMoneyTransactionFilter::categories(QStringList& list) const { Q_D(const MyMoneyTransactionFilter); auto result = d->m_filterSet.singleFilter.categoryFilter; if (result) { QHashIterator it_category(d->m_categories); while (it_category.hasNext()) { it_category.next(); list += it_category.key(); } } return result; } bool MyMoneyTransactionFilter::types(QList& list) const { Q_D(const MyMoneyTransactionFilter); auto result = d->m_filterSet.singleFilter.typeFilter; if (result) { QHashIterator it_type(d->m_types); while (it_type.hasNext()) { it_type.next(); list += it_type.key(); } } return result; } bool MyMoneyTransactionFilter::states(QList& list) const { Q_D(const MyMoneyTransactionFilter); auto result = d->m_filterSet.singleFilter.stateFilter; if (result) { QHashIterator it_state(d->m_states); while (it_state.hasNext()) { it_state.next(); list += it_state.key(); } } return result; } bool MyMoneyTransactionFilter::firstType(int&i) const { Q_D(const MyMoneyTransactionFilter); auto result = d->m_filterSet.singleFilter.typeFilter; if (result) { QHashIterator it_type(d->m_types); if (it_type.hasNext()) { it_type.next(); i = it_type.key(); } } return result; } bool MyMoneyTransactionFilter::firstState(int&i) const { Q_D(const MyMoneyTransactionFilter); auto result = d->m_filterSet.singleFilter.stateFilter; if (result) { QHashIterator it_state(d->m_states); if (it_state.hasNext()) { it_state.next(); i = it_state.key(); } } return result; } bool MyMoneyTransactionFilter::textFilter(QRegExp& exp) const { Q_D(const MyMoneyTransactionFilter); exp = d->m_text; return d->m_filterSet.singleFilter.textFilter == 1; } bool MyMoneyTransactionFilter::isInvertingText() const { Q_D(const MyMoneyTransactionFilter); return d->m_invertText; } void MyMoneyTransactionFilter::setDateFilter(eMyMoney::TransactionFilter::Date range) { QDate from, to; if (translateDateRange(range, from, to)) setDateFilter(from, to); } static int fiscalYearStartMonth = 1; static int fiscalYearStartDay = 1; void MyMoneyTransactionFilter::setFiscalYearStart(int firstMonth, int firstDay) { fiscalYearStartMonth = firstMonth; fiscalYearStartDay = firstDay; } bool MyMoneyTransactionFilter::translateDateRange(eMyMoney::TransactionFilter::Date id, QDate& start, QDate& end) { bool rc = true; int yr = QDate::currentDate().year(); int mon = QDate::currentDate().month(); switch (id) { case eMyMoney::TransactionFilter::Date::All: start = QDate(); end = QDate(); break; case eMyMoney::TransactionFilter::Date::AsOfToday: start = QDate(); end = QDate::currentDate(); break; case eMyMoney::TransactionFilter::Date::CurrentMonth: start = QDate(yr, mon, 1); end = QDate(yr, mon, 1).addMonths(1).addDays(-1); break; case eMyMoney::TransactionFilter::Date::CurrentYear: start = QDate(yr, 1, 1); end = QDate(yr, 12, 31); break; case eMyMoney::TransactionFilter::Date::MonthToDate: start = QDate(yr, mon, 1); end = QDate::currentDate(); break; case eMyMoney::TransactionFilter::Date::YearToDate: start = QDate(yr, 1, 1); end = QDate::currentDate(); break; case eMyMoney::TransactionFilter::Date::YearToMonth: start = QDate(yr, 1, 1); end = QDate(yr, mon, 1).addDays(-1); break; case eMyMoney::TransactionFilter::Date::LastMonth: start = QDate(yr, mon, 1).addMonths(-1); end = QDate(yr, mon, 1).addDays(-1); break; case eMyMoney::TransactionFilter::Date::LastYear: start = QDate(yr, 1, 1).addYears(-1); end = QDate(yr, 12, 31).addYears(-1); break; case eMyMoney::TransactionFilter::Date::Last7Days: start = QDate::currentDate().addDays(-7); end = QDate::currentDate(); break; case eMyMoney::TransactionFilter::Date::Last30Days: start = QDate::currentDate().addDays(-30); end = QDate::currentDate(); break; case eMyMoney::TransactionFilter::Date::Last3Months: start = QDate::currentDate().addMonths(-3); end = QDate::currentDate(); break; case eMyMoney::TransactionFilter::Date::Last6Months: start = QDate::currentDate().addMonths(-6); end = QDate::currentDate(); break; case eMyMoney::TransactionFilter::Date::Last11Months: start = QDate(yr, mon, 1).addMonths(-12); end = QDate(yr, mon, 1).addDays(-1); break; case eMyMoney::TransactionFilter::Date::Last12Months: start = QDate::currentDate().addMonths(-12); end = QDate::currentDate(); break; case eMyMoney::TransactionFilter::Date::Next7Days: start = QDate::currentDate(); end = QDate::currentDate().addDays(7); break; case eMyMoney::TransactionFilter::Date::Next30Days: start = QDate::currentDate(); end = QDate::currentDate().addDays(30); break; case eMyMoney::TransactionFilter::Date::Next3Months: start = QDate::currentDate(); end = QDate::currentDate().addMonths(3); break; case eMyMoney::TransactionFilter::Date::Next6Months: start = QDate::currentDate(); end = QDate::currentDate().addMonths(6); break; case eMyMoney::TransactionFilter::Date::Next12Months: start = QDate::currentDate(); end = QDate::currentDate().addMonths(12); break; case eMyMoney::TransactionFilter::Date::Next18Months: start = QDate::currentDate(); end = QDate::currentDate().addMonths(18); break; case eMyMoney::TransactionFilter::Date::UserDefined: start = QDate(); end = QDate(); break; case eMyMoney::TransactionFilter::Date::Last3ToNext3Months: start = QDate::currentDate().addMonths(-3); end = QDate::currentDate().addMonths(3); break; case eMyMoney::TransactionFilter::Date::CurrentQuarter: start = QDate(yr, mon - ((mon - 1) % 3), 1); end = start.addMonths(3).addDays(-1); break; case eMyMoney::TransactionFilter::Date::LastQuarter: start = QDate(yr, mon - ((mon - 1) % 3), 1).addMonths(-3); end = start.addMonths(3).addDays(-1); break; case eMyMoney::TransactionFilter::Date::NextQuarter: start = QDate(yr, mon - ((mon - 1) % 3), 1).addMonths(3); end = start.addMonths(3).addDays(-1); break; case eMyMoney::TransactionFilter::Date::CurrentFiscalYear: start = QDate(QDate::currentDate().year(), fiscalYearStartMonth, fiscalYearStartDay); if (QDate::currentDate() < start) start = start.addYears(-1); end = start.addYears(1).addDays(-1); break; case eMyMoney::TransactionFilter::Date::LastFiscalYear: start = QDate(QDate::currentDate().year(), fiscalYearStartMonth, fiscalYearStartDay); if (QDate::currentDate() < start) start = start.addYears(-1); start = start.addYears(-1); end = start.addYears(1).addDays(-1); break; case eMyMoney::TransactionFilter::Date::Today: start = QDate::currentDate(); end = QDate::currentDate(); break; default: qWarning("Unknown date identifier %d in MyMoneyTransactionFilter::translateDateRange()", (int)id); rc = false; break; } return rc; } MyMoneyTransactionFilter::FilterSet MyMoneyTransactionFilter::filterSet() const { Q_D(const MyMoneyTransactionFilter); return d->m_filterSet; } void MyMoneyTransactionFilter::removeReference(const QString& id) { Q_D(MyMoneyTransactionFilter); if (d->m_accounts.end() != d->m_accounts.find(id)) { qDebug("%s", qPrintable(QString("Remove account '%1' from report").arg(id))); d->m_accounts.take(id); } else if (d->m_categories.end() != d->m_categories.find(id)) { qDebug("%s", qPrintable(QString("Remove category '%1' from report").arg(id))); d->m_categories.remove(id); } else if (d->m_payees.end() != d->m_payees.find(id)) { qDebug("%s", qPrintable(QString("Remove payee '%1' from report").arg(id))); d->m_payees.remove(id); } else if (d->m_tags.end() != d->m_tags.find(id)) { qDebug("%s", qPrintable(QString("Remove tag '%1' from report").arg(id))); d->m_tags.remove(id); } } diff --git a/kmymoney/mymoney/storage/mymoneymap.h b/kmymoney/mymoney/storage/mymoneymap.h index 00343c03b..b4f2b331a 100644 --- a/kmymoney/mymoney/storage/mymoneymap.h +++ b/kmymoney/mymoney/storage/mymoneymap.h @@ -1,368 +1,366 @@ /*************************************************************************** mymoneymap.h ------------------- copyright : (C) 2007-2008 by Thomas Baumgart email : + (C) 2018 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 #include #include #include #ifndef MYMONEYMAP_H #define MYMONEYMAP_H #define MY_OWN_DEBUG 0 /** * @author Thomas Baumgart * * This template class adds transaction security to the QMap<> class. * The interface is very simple. Before you perform any changes, * you have to call the startTransaction() method. Then you can use * the insert(), modify() and remove() methods to modify the map. * Changes are recorded and if you are finished, use the * commitTransaction() to finish the transaction. If you want to go * back before you have committed the transaction, use * rollbackTransaction() to set the container to the state it was * in before you called startTransaction(). * * The implementation is based on the command pattern, in case * someone is interested. */ template class MyMoneyMap : protected QMap { private: // check if a key required (not already contained in the stack) or not bool required(const Key& key) const { if (m_stack.count() > 1) { - for (int i = 0; i < m_stack.count(); ++i) { + for (auto i = 0; i < m_stack.count(); ++i) { if (m_stack[i]->key() == key) { return false; } } } return true; } public: MyMoneyMap() : QMap() {} ~MyMoneyMap() {} void startTransaction(unsigned long* id = 0) { m_stack.push(new MyMoneyMapStart(this, id)); } void rollbackTransaction(void) { - if (m_stack.count() == 0) + if (m_stack.isEmpty()) throw MYMONEYEXCEPTION("No transaction started to rollback changes"); // undo all actions MyMoneyMapAction* action; - while (m_stack.count()) { + while (!m_stack.isEmpty()) { action = m_stack.pop(); action->undo(); delete action; } } bool commitTransaction(void) { - if (m_stack.count() == 0) + if (m_stack.isEmpty()) throw MYMONEYEXCEPTION("No transaction started to commit changes"); bool rc = m_stack.count() > 1; // remove all actions from the stack MyMoneyMapAction* action; - while (m_stack.count()) { + while (!m_stack.isEmpty()) { action = m_stack.pop(); delete action; } return rc; } void insert(const Key& key, const T& obj) { - if (m_stack.count() == 0) + if (m_stack.isEmpty()) throw MYMONEYEXCEPTION("No transaction started to insert new element into container"); // check if information about the object identified by 'key' // is already present in the stack if (!required(key)) { - QMap *container = this; - (*container)[key] = obj; + this->QMap::insert(key, obj); return; } // store object in m_stack.push(new MyMoneyMapInsert(this, key, obj)); } void modify(const Key& key, const T& obj) { - if (m_stack.count() == 0) + if (m_stack.isEmpty()) throw MYMONEYEXCEPTION("No transaction started to modify element in container"); #if 0 // had to take this out, because we use QPair in one instance as key if (key.isEmpty()) throw MYMONEYEXCEPTION("No key to update object"); #endif // check if information about the object identified by 'key' // is already present in the stack if (!required(key)) { - QMap *container = this; - (*container)[key] = obj; + this->QMap::insert(key, obj); return; } m_stack.push(new MyMoneyMapModify(this, key, obj)); } void remove(const Key& key) { - if (m_stack.count() == 0) + if (m_stack.isEmpty()) throw MYMONEYEXCEPTION("No transaction started to remove element from container"); #if 0 // had to take this out, because we use QPair in one instance as key if (key.isEmpty()) throw MYMONEYEXCEPTION("No key to remove object"); #endif // check if information about the object identified by 'key' // is already present in the stack if (!required(key)) { - QMap *container = this; - container->remove(key); + this->QMap::remove(key); return; } m_stack.push(new MyMoneyMapRemove(this, key)); } MyMoneyMap& operator= (const QMap& m) { - if (m_stack.count() != 0) { + if (!m_stack.isEmpty()) { throw MYMONEYEXCEPTION("Cannot assign whole container during transaction"); } QMap::operator=(m); return *this; } inline QList values(void) const { return QMap::values(); } inline QList keys(void) const { return QMap::keys(); } const T& operator[](const Key& k) const { return find(k).value(); #if 0 /*QT_CHECK_INVALID_MAP_ELEMENT;*/ /*PORT ME KDE4*/ return QMap::operator[](k); #endif } inline typename QMap::const_iterator find(const Key& k) const { return QMap::find(k); } inline typename QMap::const_iterator begin(void) const { return QMap::constBegin(); } inline typename QMap::const_iterator end(void) const { return QMap::constEnd(); } typedef typename QMap::const_iterator const_iterator; inline bool contains(const Key& k) const { return find(k) != end(); } inline void map(QMap& that) const { //QMap* ptr = dynamic_cast* >(this); //that = *ptr; that = *(static_cast* >(const_cast* >(this))); } inline int count(void) const { return QMap::count(); } #if MY_OWN_DEBUG void dump(void) const { printf("Container dump\n"); printf(" items in container = %d\n", count()); printf(" items on stack = %d\n", m_stack.count()); const_iterator it; for (it = begin(); it != end(); ++it) { printf(" %s \n", it.key().data()); } } #endif private: class MyMoneyMapAction { public: - MyMoneyMapAction(QMap* container) : + MyMoneyMapAction(MyMoneyMap* container) : m_container(container) {} - MyMoneyMapAction(QMap* container, const Key& key, const T& obj) : + MyMoneyMapAction(MyMoneyMap* container, const Key& key, const T& obj) : m_container(container), m_obj(obj), m_key(key) {} virtual ~MyMoneyMapAction() {} virtual void undo(void) = 0; const Key& key(void) const { return m_key; } protected: - QMap* m_container; + MyMoneyMap* m_container; T m_obj; Key m_key; }; class MyMoneyMapStart : public MyMoneyMapAction { public: - MyMoneyMapStart(QMap* container, unsigned long* id) : + MyMoneyMapStart(MyMoneyMap* container, unsigned long* id) : MyMoneyMapAction(container), m_idPtr(id), m_id(0) { if (id != 0) m_id = *id; } - virtual ~MyMoneyMapStart() {} - void undo(void) { + ~MyMoneyMapStart() final {} + void undo(void) final { if (m_idPtr != 0) *m_idPtr = m_id; } private: unsigned long* m_idPtr; unsigned long m_id; }; class MyMoneyMapInsert : public MyMoneyMapAction { public: - MyMoneyMapInsert(QMap* container, const Key& key, const T& obj) : + MyMoneyMapInsert(MyMoneyMap* container, const Key& key, const T& obj) : MyMoneyMapAction(container, key, obj) { - (*container)[key] = obj; + container->QMap::insert(key, obj); } - virtual ~MyMoneyMapInsert() {} - void undo(void) { + ~MyMoneyMapInsert() final {} + void undo(void) final { // m_container->remove(m_key) does not work on GCC 4.0.2 // using this-> to access those member does the trick - this->m_container->remove(this->m_key); + this->m_container->QMap::remove(this->m_key); } }; class MyMoneyMapRemove : public MyMoneyMapAction { public: - MyMoneyMapRemove(QMap* container, const Key& key) : + MyMoneyMapRemove(MyMoneyMap* container, const Key& key) : MyMoneyMapAction(container, key, (*container)[key]) { - container->remove(key); + container->QMap::remove(key); } - virtual ~MyMoneyMapRemove() {} - void undo(void) { - (*(this->m_container))[this->m_key] = this->m_obj; + ~MyMoneyMapRemove() final {} + void undo(void) final { + this->m_container->insert(this->m_key, this->m_obj); } }; class MyMoneyMapModify : public MyMoneyMapAction { public: - MyMoneyMapModify(QMap* container, const Key& key, const T& obj) : + MyMoneyMapModify(MyMoneyMap* container, const Key& key, const T& obj) : MyMoneyMapAction(container, key, (*container)[key]) { - (*container)[key] = obj; + container->QMap::insert(key, obj); } - virtual ~MyMoneyMapModify() {} - void undo(void) { - (*(this->m_container))[this->m_key] = this->m_obj; + ~MyMoneyMapModify() final {} + void undo(void) final { + this->m_container->QMap::insert(this->m_key, this->m_obj); } }; protected: QStack m_stack; }; #if MY_OWN_DEBUG #include #include main() { MyMoneyMap container; MyMoneyMap ct; MyMoneyAccount acc; acc.setName("Test"); // this should not be possible // container["a"] = acc; QList list; list = container.values(); MyMoneyAccount b; b.setName("Thomas"); try { container.startTransaction(); container.insert("001", acc); container.dump(); container.commitTransaction(); acc.setName("123"); container.startTransaction(); container.modify("001", acc); container.dump(); container.rollbackTransaction(); container.dump(); container.startTransaction(); container.remove(QString("001")); container.dump(); container.rollbackTransaction(); container.dump(); b = container["001"]; printf("b.name() = %s\n", b.name().data()); QMap::ConstIterator it; it = container.find("001"); it = container.begin(); } catch (const MyMoneyException &e) { printf("Caught exception: %s\n", e.what().data()); } QMap map; map["005"] = b; container = map; printf("b.name() = %s\n", container["001"].name().data()); printf("b.name() = %s\n", container["005"].name().data()); } #endif #endif diff --git a/kmymoney/mymoney/tests/mymoneyforecast-test.cpp b/kmymoney/mymoney/tests/mymoneyforecast-test.cpp index 972ae7e60..3e950c73d 100644 --- a/kmymoney/mymoney/tests/mymoneyforecast-test.cpp +++ b/kmymoney/mymoney/tests/mymoneyforecast-test.cpp @@ -1,986 +1,990 @@ /*************************************************************************** mymoneyforecasttest.cpp ------------------- copyright : (C) 2007 by Alvaro Soliverez email : asoliverez@gmail.com ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "mymoneyforecast-test.h" #include #include #include #include "mymoneybudget.h" #include "mymoneyexception.h" #include "mymoneystoragedump.h" #include "mymoneystoragexml.h" #include "reportstestcommon.h" #include "mymoneyinstitution.h" #include "mymoneysecurity.h" #include "mymoneymoney.h" #include "mymoneysplit.h" #include "mymoneyschedule.h" #include "mymoneypayee.h" #include "mymoneyenums.h" using namespace eMyMoney; using namespace test; QTEST_GUILESS_MAIN(MyMoneyForecastTest) MyMoneyForecastTest::MyMoneyForecastTest() : m(nullptr), storage(nullptr), file(nullptr) { - this->moT1 = MyMoneyMoney(57, 1); - this->moT2 = MyMoneyMoney(63, 1); - this->moT3 = MyMoneyMoney(84, 1); - this->moT4 = MyMoneyMoney(62, 1); - this->moT5 = MyMoneyMoney(104, 1); + try { + this->moT1 = MyMoneyMoney(57, 1); + this->moT2 = MyMoneyMoney(63, 1); + this->moT3 = MyMoneyMoney(84, 1); + this->moT4 = MyMoneyMoney(62, 1); + this->moT5 = MyMoneyMoney(104, 1); + } catch (const MyMoneyException & e) { + qDebug() << e.what(); + } } void MyMoneyForecastTest::init() { //all this has been taken from pivottabletest.cpp, by Thomas Baumgart and Ace Jones storage = new MyMoneyStorageMgr; file = MyMoneyFile::instance(); file->attachStorage(storage); MyMoneyFileTransaction ft; file->addCurrency(MyMoneySecurity("CAD", "Canadian Dollar", "C$")); file->addCurrency(MyMoneySecurity("USD", "US Dollar", "$")); file->addCurrency(MyMoneySecurity("JPY", "Japanese Yen", QChar(0x00A5), 1)); file->addCurrency(MyMoneySecurity("GBP", "British Pound", "#")); file->setBaseCurrency(file->currency("USD")); MyMoneyPayee payeeTest("Test Payee"); file->addPayee(payeeTest); MyMoneyPayee payeeTest2("Alvaro Soliverez"); file->addPayee(payeeTest2); acAsset = (MyMoneyFile::instance()->asset().id()); acLiability = (MyMoneyFile::instance()->liability().id()); acExpense = (MyMoneyFile::instance()->expense().id()); acIncome = (MyMoneyFile::instance()->income().id()); acChecking = makeAccount(QString("Checking Account"), Account::Type::Checkings, moCheckingOpen, QDate(2004, 5, 15), acAsset, "USD"); acCredit = makeAccount(QString("Credit Card"), Account::Type::CreditCard, moCreditOpen, QDate(2004, 7, 15), acLiability, "USD"); acSolo = makeAccount(QString("Solo"), Account::Type::Expense, MyMoneyMoney(), QDate(2004, 1, 11), acExpense, "USD"); acParent = makeAccount(QString("Parent"), Account::Type::Expense, MyMoneyMoney(), QDate(2004, 1, 11), acExpense, "USD"); acChild = makeAccount(QString("Child"), Account::Type::Expense, MyMoneyMoney(), QDate(2004, 2, 11), acParent, "USD"); acForeign = makeAccount(QString("Foreign"), Account::Type::Expense, MyMoneyMoney(), QDate(2004, 1, 11), acExpense, "USD"); acInvestment = makeAccount("Investment", Account::Type::Investment, moZero, QDate(2004, 1, 1), acAsset, "USD"); acSecondChild = makeAccount(QString("Second Child"), Account::Type::Expense, MyMoneyMoney(), QDate(2004, 2, 11), acParent, "USD"); acGrandChild1 = makeAccount(QString("Grand Child 1"), Account::Type::Expense, MyMoneyMoney(), QDate(2004, 2, 11), acChild, "USD"); acGrandChild2 = makeAccount(QString("Grand Child 2"), Account::Type::Expense, MyMoneyMoney(), QDate(2004, 2, 11), acChild, "USD"); //this account added to have an account to test opening date calculations acCash = makeAccount(QString("Cash"), Account::Type::Cash, moCreditOpen, QDate::currentDate().addDays(-2), acAsset, "USD"); MyMoneyInstitution i("Bank of the World", "", "", "", "", "", ""); file->addInstitution(i); inBank = i.id(); ft.commit(); } void MyMoneyForecastTest::cleanup() { file->detachStorage(storage); delete storage; } void MyMoneyForecastTest::testEmptyConstructor() { MyMoneyForecast a; MyMoneyAccount b; QVERIFY(a.forecastBalance(b, QDate::currentDate()).isZero()); QVERIFY(!a.isForecastAccount(b)); QVERIFY(a.forecastBalance(b, QDate::currentDate()) == MyMoneyMoney()); QVERIFY(a.daysToMinimumBalance(b) == -1); QVERIFY(a.daysToZeroBalance(b) == -2); QVERIFY(a.forecastDays() == 90); QVERIFY(a.accountsCycle() == 30); QVERIFY(a.forecastCycles() == 3); QVERIFY(a.historyStartDate() == QDate::currentDate().addDays(-3*30)); QVERIFY(a.historyEndDate() == QDate::currentDate().addDays(-1)); QVERIFY(a.historyDays() == 30 * 3); } void MyMoneyForecastTest::testDoForecastInit() { MyMoneyForecast a; a.doForecast(); /* //check the illegal argument validation try { KMyMoneySettings::setForecastDays(-10); a.doForecast(); } catch (const MyMoneyException &e) { QFAIL("Unexpected exception"); } try { KMyMoneySettings::setForecastAccountCycle(-20); a.doForecast(); } catch (const MyMoneyException &e) { QFAIL("Unexpected exception"); } try { KMyMoneySettings::setForecastCycles(-10); a.doForecast(); } catch (const MyMoneyException &e) { QFAIL("Unexpected exception"); } try { KMyMoneySettings::setForecastAccountCycle(0); a.doForecast(); } catch (const MyMoneyException &e) { QFAIL("Unexpected exception"); } try { KMyMoneySettings::setForecastDays(0); KMyMoneySettings::setForecastCycles(0); KMyMoneySettings::setForecastAccountCycle(0); a.doForecast(); } catch (const MyMoneyException &e) { QVERIFY("Unexpected exception"); }*/ } //test that it forecasts correctly with transactions in the period of forecast void MyMoneyForecastTest::testDoForecast() { //set up environment MyMoneyForecast a; MyMoneyAccount a_checking = file->account(acChecking); MyMoneyAccount a_credit = file->account(acCredit); //test empty forecast a.doForecast(); //this is just to check nothing goes wrong if forecast is run agains an empty template //setup some transactions TransactionHelper t1(QDate::currentDate().addDays(-1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT1, acChecking, acSolo); TransactionHelper t2(QDate::currentDate().addDays(-1), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), -(this->moT2), acCredit, acParent); TransactionHelper t3(QDate::currentDate().addDays(-1), MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer), this->moT1, acCredit, acChecking); a.setForecastMethod(1); a.setForecastDays(3); a.setAccountsCycle(1); a.setForecastCycles(1); a.setBeginForecastDay(0); a.setHistoryMethod(0); //moving average a.doForecast(); //checking didn't have balance variations, so the forecast should be equal to the current balance MyMoneyMoney b_checking = file->balance(a_checking.id(), QDate::currentDate()); QVERIFY(a.forecastBalance(a_checking, QDate::currentDate().addDays(1)) == b_checking); QVERIFY(a.forecastBalance(a_checking, QDate::currentDate().addDays(2)) == b_checking); QVERIFY(a.forecastBalance(a_checking, QDate::currentDate().addDays(3)) == b_checking); QVERIFY(a.forecastBalance(a_checking, QDate::currentDate()) == b_checking); //credit had a variation so the forecast should be different for each day MyMoneyMoney b_credit = file->balance(a_credit.id(), QDate::currentDate()); QVERIFY(a.forecastBalance(a_credit, 0) == b_credit); QVERIFY(a.forecastBalance(a_credit, QDate::currentDate().addDays(1)) == (b_credit + (moT2 - moT1))); QVERIFY(a.forecastBalance(a_credit, QDate::currentDate().addDays(2)) == (b_credit + ((moT2 - moT1)*2))); QVERIFY(a.forecastBalance(a_credit, QDate::currentDate().addDays(3)) == b_credit + ((moT2 - moT1)*3)); a.setHistoryMethod(1); //weighted moving average a.doForecast(); QVERIFY(a.forecastBalance(a_credit, 0) == b_credit); QVERIFY(a.forecastBalance(a_credit, QDate::currentDate().addDays(1)) == (b_credit + (moT2 - moT1))); QVERIFY(a.forecastBalance(a_credit, QDate::currentDate().addDays(2)) == (b_credit + ((moT2 - moT1)*2))); QVERIFY(a.forecastBalance(a_credit, QDate::currentDate().addDays(3)) == b_credit + ((moT2 - moT1)*3)); //insert transactions outside the forecast period. The calculation should be the same. TransactionHelper t4(QDate::currentDate().addDays(-2), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), -moT2, acCredit, acParent); TransactionHelper t5(QDate::currentDate().addDays(-10), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), -moT2, acCredit, acParent); TransactionHelper t6(QDate::currentDate().addDays(-3), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), -moT2, acCredit, acParent); a.setForecastMethod(1); a.setForecastDays(3); a.setAccountsCycle(1); a.setForecastCycles(1); a.setBeginForecastDay(0); a.setHistoryMethod(0); //moving average a.doForecast(); //check forecast b_credit = file->balance(a_credit.id(), QDate::currentDate()); MyMoneyMoney b_credit_1_exp = (b_credit + ((moT2 - moT1))); MyMoneyMoney b_credit_2 = a.forecastBalance(a_credit, QDate::currentDate().addDays(2)); MyMoneyMoney b_credit_2_exp = (b_credit + ((moT2 - moT1) * 2)); QVERIFY(a.forecastBalance(a_credit, QDate::currentDate()) == file->balance(a_credit.id(), QDate::currentDate())); QVERIFY(a.forecastBalance(a_credit, QDate::currentDate().addDays(1)) == b_credit + (moT2 - moT1)); QVERIFY(a.forecastBalance(a_credit, QDate::currentDate().addDays(2)) == b_credit + ((moT2 - moT1)*2)); QVERIFY(a.forecastBalance(a_credit, QDate::currentDate().addDays(3)) == b_credit + ((moT2 - moT1)*3)); //test weighted moving average a.setForecastMethod(1); a.setForecastDays(3); a.setAccountsCycle(1); a.setForecastCycles(3); a.setBeginForecastDay(0); a.setHistoryMethod(1); a.doForecast(); QVERIFY(a.forecastBalance(a_credit, 0) == b_credit); QVERIFY(a.forecastBalance(a_credit, QDate::currentDate().addDays(1)) == (b_credit + (((moT2 - moT1)*3 + moT2*2 + moT2) / MyMoneyMoney(6, 1)))); } void MyMoneyForecastTest::testGetForecastAccountList() { MyMoneyForecast a; MyMoneyAccount a_checking = file->account(acChecking); MyMoneyAccount a_parent = file->account(acParent); QList b; b = a.forecastAccountList(); //check that it contains asset account, but not expense accounts QVERIFY(b.contains(a_checking)); QVERIFY(!b.contains(a_parent)); } void MyMoneyForecastTest::testCalculateAccountTrend() { //set up environment TransactionHelper t1(QDate::currentDate().addDays(-3), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), -moT2, acChecking, acSolo); MyMoneyAccount a_checking = file->account(acChecking); //test invalid arguments try { MyMoneyForecast::calculateAccountTrend(a_checking, 0); } catch (const MyMoneyException &e) { QVERIFY(e.what().compare("Illegal arguments when calling calculateAccountTrend. trendDays must be higher than 0") == 0); } try { MyMoneyForecast::calculateAccountTrend(a_checking, -10); } catch (const MyMoneyException &e) { QVERIFY(e.what().compare("Illegal arguments when calling calculateAccountTrend. trendDays must be higher than 0") == 0); } //test that it calculates correctly QVERIFY(MyMoneyForecast::calculateAccountTrend(a_checking , 3) == moT2 / MyMoneyMoney(3, 1)); //test that it works for all kind of accounts MyMoneyAccount a_solo = file->account(acSolo); MyMoneyMoney soloTrend = MyMoneyForecast::calculateAccountTrend(a_solo, 3); MyMoneyMoney soloTrendExp = -moT2 / MyMoneyMoney(3, 1); QVERIFY(MyMoneyForecast::calculateAccountTrend(a_solo, 3) == -moT2 / MyMoneyMoney(3, 1)); //test that it does not take into account the transactions of the opening date of the account MyMoneyAccount a_cash = file->account(acCash); TransactionHelper t2(QDate::currentDate().addDays(-2), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), moT2, acCash, acParent); TransactionHelper t3(QDate::currentDate().addDays(-1), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), moT1, acCash, acParent); QVERIFY(MyMoneyForecast::calculateAccountTrend(a_cash, 3) == -moT1); } void MyMoneyForecastTest::testGetForecastBalance() { //set up environment MyMoneyForecast a; TransactionHelper t1(QDate::currentDate().addDays(-1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT1, acChecking, acSolo); TransactionHelper t2(QDate::currentDate().addDays(-1), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), -(this->moT2), acCredit, acParent); TransactionHelper t3(QDate::currentDate().addDays(-1), MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer), this->moT1, acCredit, acChecking); a.setForecastMethod(1); a.setForecastDays(3); a.setAccountsCycle(1); a.setForecastCycles(1); a.setHistoryMethod(0); a.doForecast(); MyMoneyAccount a_checking = file->account(acChecking); MyMoneyAccount a_credit = file->account(acCredit); //test invalid arguments QVERIFY(a.forecastBalance(a_checking, QDate::currentDate().addDays(-1)) == MyMoneyMoney()); QVERIFY(a.forecastBalance(a_checking, QDate::currentDate().addDays(-10)) == MyMoneyMoney()); QVERIFY(a.forecastBalance(a_checking, -1) == MyMoneyMoney()); QVERIFY(a.forecastBalance(a_checking, -100) == MyMoneyMoney()); //test a date outside the forecast days QVERIFY(a.forecastBalance(a_checking, QDate::currentDate().addDays(4)) == MyMoneyMoney()); QVERIFY(a.forecastBalance(a_checking, 4) == MyMoneyMoney()); QVERIFY(a.forecastBalance(a_checking, QDate::currentDate().addDays(10)) == MyMoneyMoney()); QVERIFY(a.forecastBalance(a_checking, 10) == MyMoneyMoney()); //test it returns valid results MyMoneyMoney b_credit = file->balance(a_credit.id(), QDate::currentDate()); QVERIFY(a.forecastBalance(a_credit, QDate::currentDate()) == file->balance(a_credit.id(), QDate::currentDate())); QVERIFY(a.forecastBalance(a_credit, QDate::currentDate().addDays(1)) == b_credit + (moT2 - moT1)); QVERIFY(a.forecastBalance(a_credit, QDate::currentDate().addDays(2)) == b_credit + ((moT2 - moT1)*2)); QVERIFY(a.forecastBalance(a_credit, QDate::currentDate().addDays(3)) == b_credit + ((moT2 - moT1)*3)); } void MyMoneyForecastTest::testIsForecastAccount() { MyMoneyForecast a; MyMoneyAccount a_checking = file->account(acChecking); MyMoneyAccount a_solo = file->account(acSolo); MyMoneyAccount a_investment = file->account(acInvestment); //test an invalid account QVERIFY(a.isForecastAccount(a_solo) == false); QVERIFY(a.isForecastAccount(a_investment) == true); //test a valid account QVERIFY(a.isForecastAccount(a_checking) == true); } void MyMoneyForecastTest::testDoFutureScheduledForecast() { //set up future transactions MyMoneyForecast a; MyMoneyAccount a_cash = file->account(acCash); TransactionHelper t1(QDate::currentDate().addDays(1), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), -moT1, acCash, acParent); TransactionHelper t2(QDate::currentDate().addDays(2), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), -moT2, acCash, acParent); TransactionHelper t3(QDate::currentDate().addDays(3), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), -moT3, acCash, acParent); TransactionHelper t4(QDate::currentDate().addDays(10), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), -moT4, acCash, acParent); a.setForecastMethod(0); a.setForecastDays(3); a.setAccountsCycle(1); a.setForecastCycles(1); a.doForecast(); MyMoneyMoney b_cash = file->balance(a_cash.id(), QDate::currentDate()); //test valid results QVERIFY(a.forecastBalance(a_cash, QDate::currentDate()) == b_cash); QVERIFY(a.forecastBalance(a_cash, QDate::currentDate().addDays(1)) == b_cash + moT1); QVERIFY(a.forecastBalance(a_cash, QDate::currentDate().addDays(2)) == b_cash + moT1 + moT2); QVERIFY(a.forecastBalance(a_cash, QDate::currentDate().addDays(3)) == b_cash + moT1 + moT2 + moT3); } void MyMoneyForecastTest::testScheduleForecast() { //set up schedule environment for testing MyMoneyAccount a_cash = file->account(acCash); MyMoneyAccount a_parent = file->account(acParent); MyMoneyFileTransaction ft; MyMoneySchedule sch("A Name", Schedule::Type::Bill, Schedule::Occurrence::Weekly, 1, Schedule::PaymentType::DirectDebit, QDate::currentDate().addDays(1), QDate(), true, true); MyMoneyTransaction t; t.setPostDate(QDate::currentDate().addDays(1)); t.setEntryDate(QDate::currentDate().addDays(1)); //t.setId("T000000000000000001"); t.setBankID("BID"); t.setMemo("Wohnung:Miete"); t.setCommodity("USD"); t.setValue("key", "value"); MyMoneySplit s; s.setPayeeId("P000001"); s.setShares(moT2); s.setValue(moT2); s.setAccountId(a_parent.id()); s.setBankID("SPID1"); s.setReconcileFlag(eMyMoney::Split::State::Reconciled); t.addSplit(s); s.setPayeeId("P000001"); s.setShares(-moT2); s.setValue(-moT2); s.setAccountId(a_cash.id()); s.setBankID("SPID2"); s.setReconcileFlag(eMyMoney::Split::State::Cleared); s.clearId(); t.addSplit(s); sch.setTransaction(t); file->addSchedule(sch); ft.commit(); MyMoneyFileTransaction ft3; MyMoneySchedule sch3("A Name1", Schedule::Type::Bill, Schedule::Occurrence::Weekly, 1, Schedule::PaymentType::DirectDebit, QDate::currentDate().addDays(5), QDate(), true, true); //sch.setLastPayment(QDate::currentDate()); //sch.recordPayment(QDate::currentDate().addDays(1)); //sch.setId("SCH0001"); MyMoneyTransaction t3; t3.setPostDate(QDate::currentDate().addDays(5)); t3.setEntryDate(QDate::currentDate().addDays(5)); //t.setId("T000000000000000001"); t3.setBankID("BID"); t3.setMemo("Wohnung:Miete"); t3.setCommodity("USD"); t3.setValue("key", "value"); MyMoneySplit s3; s3.setPayeeId("P000001"); s3.setShares(moT2); s3.setValue(moT2); s3.setAccountId(a_parent.id()); s3.setBankID("SPID1"); s3.setReconcileFlag(eMyMoney::Split::State::Reconciled); t3.addSplit(s3); s3.setPayeeId("P000001"); s3.setShares(-moT2); s3.setValue(-moT2); s3.setAccountId(a_cash.id()); s3.setBankID("SPID2"); s3.setReconcileFlag(eMyMoney::Split::State::Cleared); s3.clearId(); t3.addSplit(s3); sch3.setTransaction(t3); file->addSchedule(sch3); ft3.commit(); MyMoneyFileTransaction ft2; MyMoneySchedule sch2("A Name2", Schedule::Type::Bill, Schedule::Occurrence::Weekly, 1, Schedule::PaymentType::DirectDebit, QDate::currentDate().addDays(2), QDate(), true, true); //sch.setLastPayment(QDate::currentDate()); //sch.recordPayment(QDate::currentDate().addDays(1)); //sch.setId("SCH0001"); MyMoneyTransaction t2; t2.setPostDate(QDate::currentDate().addDays(2)); t2.setEntryDate(QDate::currentDate().addDays(2)); //t.setId("T000000000000000001"); t2.setBankID("BID"); t2.setMemo("Wohnung:Miete"); t2.setCommodity("USD"); t2.setValue("key", "value"); MyMoneySplit s2; s2.setPayeeId("P000001"); s2.setShares(moT1); s2.setValue(moT1); s2.setAccountId(a_parent.id()); s2.setBankID("SPID1"); s2.setReconcileFlag(eMyMoney::Split::State::Reconciled); t2.addSplit(s2); s2.setPayeeId("P000001"); s2.setShares(-moT1); s2.setValue(-moT1); s2.setAccountId(a_cash.id()); s2.setBankID("SPID2"); s2.setReconcileFlag(eMyMoney::Split::State::Cleared); s2.clearId(); t2.addSplit(s2); sch2.setTransaction(t2); file->addSchedule(sch2); ft2.commit(); //run forecast MyMoneyForecast a; a.setForecastMethod(0); a.setForecastDays(3); a.setAccountsCycle(1); a.setForecastCycles(1); a.doForecast(); //check result for single schedule MyMoneyMoney b_cash = file->balance(a_cash.id(), QDate::currentDate()); MyMoneyMoney b_cash1 = a.forecastBalance(a_cash, QDate::currentDate().addDays(1)); //test valid results QVERIFY(a.forecastBalance(a_cash, QDate::currentDate()) == b_cash); QVERIFY(a.forecastBalance(a_cash, QDate::currentDate().addDays(1)) == b_cash - moT2); QVERIFY(a.forecastBalance(a_cash, QDate::currentDate().addDays(2)) == b_cash - moT2 - moT1); } void MyMoneyForecastTest::testDaysToMinimumBalance() { //setup environment MyMoneyForecast a; MyMoneyAccount a_cash = file->account(acCash); MyMoneyAccount a_credit = file->account(acCredit); MyMoneyAccount a_parent = file->account(acParent); a_cash.setValue("minBalanceAbsolute", "50"); a_credit.setValue("minBalanceAbsolute", "50"); TransactionHelper t1(QDate::currentDate().addDays(-1), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), -moT1, acCash, acParent); TransactionHelper t2(QDate::currentDate().addDays(2), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), moT2, acCash, acParent); TransactionHelper t3(QDate::currentDate().addDays(-1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), -moT1, acCredit, acParent); TransactionHelper t4(QDate::currentDate().addDays(4), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), moT5, acCredit, acParent); a.setForecastMethod(0); a.setForecastDays(3); a.setAccountsCycle(1); a.setForecastCycles(1); a.setBeginForecastDay(0); a.doForecast(); //test invalid arguments MyMoneyAccount nullAcc; QVERIFY(a.daysToMinimumBalance(nullAcc) == -1); //test when not a forecast account QVERIFY(a.daysToMinimumBalance(a_parent) == -1); //test it warns when inside the forecast period QVERIFY(a.daysToMinimumBalance(a_cash) == 2); //test it does not warn when it will be outside of the forecast period QVERIFY(a.daysToMinimumBalance(a_credit) == -1); } void MyMoneyForecastTest::testDaysToZeroBalance() { //set up environment MyMoneyAccount a_Solo = file->account(acSolo); MyMoneyAccount a_Cash = file->account(acCash); MyMoneyAccount a_Credit = file->account(acCredit); //MyMoneyFileTransaction ft; TransactionHelper t1(QDate::currentDate().addDays(2), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), -moT1, acChecking, acSolo); TransactionHelper t2(QDate::currentDate().addDays(2), MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer), (moT5), acCash, acCredit); TransactionHelper t3(QDate::currentDate().addDays(2), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), (moT5*100), acCredit, acParent); //ft.commit(); MyMoneyForecast a; a.setForecastMethod(0); a.setForecastDays(30); a.setAccountsCycle(1); a.setForecastCycles(3); a.doForecast(); //test invalid arguments MyMoneyAccount nullAcc; try { auto days = a.daysToZeroBalance(nullAcc); Q_UNUSED(days) } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } //test when not a forecast account MyMoneyAccount a_solo = file->account(acSolo); int iSolo = a.daysToZeroBalance(a_Solo); QVERIFY(iSolo == -2); //test it warns when inside the forecast period MyMoneyMoney fCash = a.forecastBalance(a_Cash, QDate::currentDate().addDays(2)); QVERIFY(a.daysToZeroBalance(a_Cash) == 2); //test it does not warn when it will be outside of the forecast period } void MyMoneyForecastTest::testSkipOpeningDate() { //set up environment MyMoneyForecast a; TransactionHelper t1(QDate::currentDate().addDays(-2), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT1, acCash, acSolo); TransactionHelper t2(QDate::currentDate().addDays(-1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT2, acCash, acSolo); a.setForecastMethod(1); a.setForecastDays(3); a.setAccountsCycle(2); a.setForecastCycles(1); a.setHistoryMethod(0); a.doForecast(); MyMoneyAccount a_cash = file->account(acCash); //test it has no variation because it skipped the variation of the opening date MyMoneyMoney b_cash = file->balance(a_cash.id(), QDate::currentDate()); QVERIFY(a.skipOpeningDate() == true); QVERIFY(a.forecastBalance(a_cash, QDate::currentDate()) == b_cash); QVERIFY(a.forecastBalance(a_cash, QDate::currentDate().addDays(1)) == b_cash); QVERIFY(a.forecastBalance(a_cash, QDate::currentDate().addDays(2)) == b_cash - moT2); QVERIFY(a.forecastBalance(a_cash, QDate::currentDate().addDays(3)) == b_cash - moT2); } void MyMoneyForecastTest::testAccountMinimumBalanceDateList() { //set up environment MyMoneyForecast a; TransactionHelper t1(QDate::currentDate().addDays(-2), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT1, acCash, acSolo); TransactionHelper t2(QDate::currentDate().addDays(-1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT2, acCash, acSolo); a.setForecastMethod(1); a.setForecastDays(6); a.setAccountsCycle(2); a.setForecastCycles(3); a.setHistoryMethod(0); a.setBeginForecastDay(QDate::currentDate().addDays(1).day()); a.doForecast(); MyMoneyAccount a_cash = file->account(acCash); //test QList dateList; dateList = a.accountMinimumBalanceDateList(a_cash); QList::iterator it = dateList.begin(); QDate minDate = *it; QVERIFY(minDate == QDate::currentDate().addDays(2)); it++; minDate = *it; QVERIFY(minDate == QDate::currentDate().addDays(4)); it++; minDate = *it; QVERIFY(minDate == QDate::currentDate().addDays(6)); } void MyMoneyForecastTest::testAccountMaximumBalanceDateList() { //set up environment MyMoneyForecast a; TransactionHelper t1(QDate::currentDate().addDays(-2), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT1, acCash, acSolo); TransactionHelper t2(QDate::currentDate().addDays(-1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT2, acCash, acSolo); a.setForecastMethod(1); a.setForecastDays(6); a.setAccountsCycle(2); a.setForecastCycles(3); a.setHistoryMethod(0); a.setBeginForecastDay(QDate::currentDate().addDays(1).day()); a.doForecast(); MyMoneyAccount a_cash = file->account(acCash); //test QList dateList; dateList = a.accountMaximumBalanceDateList(a_cash); QList::iterator it = dateList.begin(); QDate maxDate = *it; QVERIFY(maxDate == QDate::currentDate().addDays(1)); it++; maxDate = *it; QVERIFY(maxDate == QDate::currentDate().addDays(3)); it++; maxDate = *it; QVERIFY(maxDate == QDate::currentDate().addDays(5)); } void MyMoneyForecastTest::testAccountAverageBalance() { //set up environment MyMoneyForecast a; TransactionHelper t1(QDate::currentDate().addDays(-2), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT1, acCash, acSolo); TransactionHelper t2(QDate::currentDate().addDays(-1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT2, acCash, acSolo); a.setForecastMethod(1); a.setForecastDays(3); a.setAccountsCycle(2); a.setForecastCycles(1); a.setBeginForecastDay(0); a.doForecast(); MyMoneyAccount a_cash = file->account(acCash); //test MyMoneyMoney b_cash1 = a.forecastBalance(a_cash, QDate::currentDate().addDays(1)); MyMoneyMoney b_cash2 = a.forecastBalance(a_cash, QDate::currentDate().addDays(2)); MyMoneyMoney b_cash3 = a.forecastBalance(a_cash, QDate::currentDate().addDays(3)); MyMoneyMoney average = (b_cash1 + b_cash2 + b_cash3) / MyMoneyMoney(3, 1); QVERIFY(a.accountAverageBalance(a_cash) == average); } void MyMoneyForecastTest::testBeginForecastDate() { //set up environment MyMoneyForecast a; QDate beginDate; int beginDay; a.setForecastMethod(1); a.setForecastDays(90); a.setAccountsCycle(14); a.setForecastCycles(3); a.setBeginForecastDay(0); a.doForecast(); //test when using old method without begin day QVERIFY(QDate::currentDate() == a.beginForecastDate()); //setup begin to last day of month a.setBeginForecastDay(31); beginDay = a.beginForecastDay(); a.doForecast(); //test if (QDate::currentDate().day() < beginDay) { if (QDate::currentDate().daysInMonth() < beginDay) beginDay = QDate::currentDate().daysInMonth(); beginDate = QDate(QDate::currentDate().year(), QDate::currentDate().month(), beginDay); QVERIFY(beginDate == a.beginForecastDate()); } //setup begin day to same date a.setBeginForecastDay(QDate::currentDate().day()); beginDay = a.beginForecastDay(); a.doForecast(); QVERIFY(QDate::currentDate() == a.beginForecastDate()); //setup to first day of month with small interval a.setBeginForecastDay(1); a.setAccountsCycle(1); beginDay = a.beginForecastDay(); a.doForecast(); //test if (QDate::currentDate() == a.beginForecastDate()) { QVERIFY(QDate::currentDate() == a.beginForecastDate()); } else { beginDay = ((((QDate::currentDate().day() - beginDay) / a.accountsCycle()) + 1) * a.accountsCycle()) + beginDay; if (beginDay > QDate::currentDate().daysInMonth()) beginDay = QDate::currentDate().daysInMonth(); beginDate = QDate(QDate::currentDate().year(), QDate::currentDate().month(), beginDay); if (QDate::currentDate().day() == QDate::currentDate().daysInMonth()) { std::cout << std::endl << "testBeginForecastDate(): test of first day of month with small interval skipped because it is the last day of month" << std::endl; } else { QVERIFY(beginDate == a.beginForecastDate()); } } //setup to test when current date plus cycle equals begin day a.setAccountsCycle(14); beginDay = QDate::currentDate().addDays(14).day(); a.setBeginForecastDay(beginDay); beginDate = QDate::currentDate().addDays(14); a.doForecast(); //test QVERIFY(beginDate == a.beginForecastDate()); //setup to test when the begin day will be next month a.setBeginForecastDay(1); a.setAccountsCycle(40); a.doForecast(); beginDate = QDate(QDate::currentDate().addMonths(1).year(), QDate::currentDate().addMonths(1).month(), 1); //test if (QDate::currentDate().day() > 1) { QVERIFY(beginDate == a.beginForecastDate()); } else { //test is not valid if today is 1st of month std::cout << std::endl << "testBeginForecastDate(): test of first day of month skipped because current day is 1st of month" << std::endl; } } void MyMoneyForecastTest::testHistoryDays() { MyMoneyForecast a; QVERIFY(a.historyStartDate() == QDate::currentDate().addDays(-a.forecastCycles()*a.accountsCycle())); QVERIFY(a.historyEndDate() == QDate::currentDate().addDays(-1)); QVERIFY(a.historyDays() == a.forecastCycles()*a.accountsCycle()); a.setForecastMethod(1); a.setForecastDays(90); a.setAccountsCycle(14); a.setForecastCycles(3); a.setBeginForecastDay(0); a.doForecast(); QVERIFY(a.historyStartDate() == QDate::currentDate().addDays(-14*3)); QVERIFY(a.historyDays() == (14*3)); QVERIFY(a.historyEndDate() == (QDate::currentDate().addDays(-1))); } void MyMoneyForecastTest::testCreateBudget() { //set up environment MyMoneyForecast a; MyMoneyForecast b; MyMoneyBudget budget; TransactionHelper t1(QDate(2005, 1, 3), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT1, acCash, acSolo); TransactionHelper t2(QDate(2005, 1, 15), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT2, acCash, acParent); TransactionHelper t3(QDate(2005, 1, 30), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT3, acCash, acSolo); TransactionHelper t4(QDate(2006, 1, 25), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT4, acCash, acParent); TransactionHelper t5(QDate(2005, 4, 3), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT1, acCash, acSolo); TransactionHelper t6(QDate(2006, 5, 15), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT2, acCash, acParent); TransactionHelper t7(QDate(2005, 8, 3), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT3, acCash, acSolo); TransactionHelper t8(QDate(2006, 9, 15), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT4, acCash, acParent); a.setHistoryMethod(0); a.setForecastMethod(1); a.createBudget(budget, QDate(2005, 1, 1), QDate(2006, 12, 31), QDate(2007, 1, 1), QDate(2007, 12, 31), true); //test MyMoneyAccount a_solo = file->account(acSolo); MyMoneyAccount a_parent = file->account(acParent); //test it has no variation because it skipped the variation of the opening date QVERIFY(a.forecastBalance(a_solo, QDate(2007, 1, 1)) == ((moT1 + moT3) / MyMoneyMoney(2, 1))); QVERIFY(a.forecastBalance(a_parent, QDate(2007, 1, 1)) == ((moT2 + moT4) / MyMoneyMoney(2, 1))); QVERIFY(a.forecastBalance(a_solo, QDate(2007, 4, 1)) == ((moT1) / MyMoneyMoney(2, 1))); QVERIFY(a.forecastBalance(a_parent, QDate(2007, 5, 1)) == ((moT2) / MyMoneyMoney(2, 1))); QVERIFY(a.forecastBalance(a_solo, QDate(2007, 8, 1)) == ((moT3) / MyMoneyMoney(2, 1))); QVERIFY(a.forecastBalance(a_parent, QDate(2007, 9, 1)) == ((moT4) / MyMoneyMoney(2, 1))); //test the budget object returned by the method QVERIFY(budget.account(a_parent.id()).period(QDate(2007, 9, 1)).amount() == ((moT4) / MyMoneyMoney(2, 1))); //setup test for a length lower than a year b.setForecastMethod(1); b.setHistoryMethod(0); b.createBudget(budget, QDate(2005, 1, 1), QDate(2005, 6, 30), QDate(2007, 1, 1), QDate(2007, 6, 30), true); //test QVERIFY(b.forecastBalance(a_solo, QDate(2007, 1, 1)) == (moT1 + moT3)); QVERIFY(b.forecastBalance(a_parent, QDate(2007, 1, 1)) == (moT2)); QVERIFY(b.forecastBalance(a_solo, QDate(2007, 4, 1)) == (moT1)); QVERIFY(b.forecastBalance(a_parent, QDate(2007, 5, 1)) == (MyMoneyMoney())); //set up schedule environment for testing MyMoneyAccount a_cash = file->account(acCash); MyMoneyFileTransaction ft; MyMoneySchedule sch("A Name", Schedule::Type::Bill, Schedule::Occurrence::Monthly, 1, Schedule::PaymentType::DirectDebit, QDate::currentDate(), QDate(), true, true); MyMoneyTransaction t10; t10.setPostDate(QDate::currentDate().addMonths(1)); t10.setEntryDate(QDate::currentDate().addMonths(1)); //t.setId("T000000000000000001"); t10.setBankID("BID"); t10.setMemo("Wohnung:Miete"); t10.setCommodity("USD"); t10.setValue("key", "value"); MyMoneySplit s; s.setPayeeId("P000001"); s.setShares(moT2); s.setValue(moT2); s.setAccountId(a_parent.id()); s.setBankID("SPID1"); s.setReconcileFlag(eMyMoney::Split::State::Reconciled); t10.addSplit(s); s.setPayeeId("P000001"); s.setShares(-moT2); s.setValue(-moT2); s.setAccountId(a_cash.id()); s.setBankID("SPID2"); s.setReconcileFlag(eMyMoney::Split::State::Cleared); s.clearId(); t10.addSplit(s); sch.setTransaction(t10); file->addSchedule(sch); ft.commit(); //run forecast MyMoneyForecast c; c.setForecastMethod(0); c.setForecastCycles(1); c.createBudget(budget, QDate::currentDate().addYears(-2), QDate::currentDate().addYears(-1), QDate::currentDate().addMonths(-2), QDate::currentDate().addMonths(6), true); MyMoneyMoney c_parent = c.forecastBalance(a_parent, QDate(QDate::currentDate().addMonths(1).year(), QDate::currentDate().addMonths(1).month(), 1)); //test valid results QVERIFY(c.forecastBalance(a_parent, QDate(QDate::currentDate().addMonths(1).year(), QDate::currentDate().addMonths(1).month(), 1)) == (moT2)); } void MyMoneyForecastTest::testLinearRegression() { //set up environment MyMoneyForecast a; MyMoneyAccount a_checking = file->account(acChecking); MyMoneyAccount a_credit = file->account(acCredit); //setup some transactions TransactionHelper t1(QDate::currentDate().addDays(-1), MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal), this->moT1, acChecking, acSolo); TransactionHelper t2(QDate::currentDate().addDays(-1), MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit), -(this->moT2), acCredit, acParent); TransactionHelper t3(QDate::currentDate().addDays(-1), MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer), this->moT1, acCredit, acChecking); //TODO Add tests specific for linear regression } diff --git a/kmymoney/mymoney/tests/mymoneyinstitution-test.cpp b/kmymoney/mymoney/tests/mymoneyinstitution-test.cpp index 56596c5a7..0f90a5e5d 100644 --- a/kmymoney/mymoney/tests/mymoneyinstitution-test.cpp +++ b/kmymoney/mymoney/tests/mymoneyinstitution-test.cpp @@ -1,369 +1,357 @@ /*************************************************************************** mymoneyinstitutiontest.cpp ------------------- copyright : (C) 2002 by Thomas Baumgart email : ipwizard@users.sourceforge.net ***************************************************************************/ /*************************************************************************** * * * 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 "mymoneyinstitution-test.h" #include #include #include #define KMM_MYMONEY_UNIT_TESTABLE friend class MyMoneyInstitutionTest; #include "mymoneyexception.h" #include "mymoneyinstitution.h" #include "mymoneyinstitution_p.h" QTEST_GUILESS_MAIN(MyMoneyInstitutionTest) void MyMoneyInstitutionTest::init() { m = new MyMoneyInstitution(); n = new MyMoneyInstitution("name", "town", "street", "postcode", "telephone", "manager", "sortcode"); } void MyMoneyInstitutionTest::cleanup() { delete m; delete n; } void MyMoneyInstitutionTest::testEmptyConstructor() { QVERIFY(m->id().isEmpty()); QVERIFY(m->street().isEmpty()); QVERIFY(m->town().isEmpty()); QVERIFY(m->postcode().isEmpty()); QVERIFY(m->telephone().isEmpty()); QVERIFY(m->manager().isEmpty()); QVERIFY(m->accountCount() == 0); } void MyMoneyInstitutionTest::testSetFunctions() { m->setStreet("street"); m->setTown("town"); m->setPostcode("postcode"); m->setTelephone("telephone"); m->setManager("manager"); m->setName("name"); QVERIFY(m->id().isEmpty()); QVERIFY(m->street() == "street"); QVERIFY(m->town() == "town"); QVERIFY(m->postcode() == "postcode"); QVERIFY(m->telephone() == "telephone"); QVERIFY(m->manager() == "manager"); QVERIFY(m->name() == "name"); } void MyMoneyInstitutionTest::testNonemptyConstructor() { QVERIFY(n->id().isEmpty()); QVERIFY(n->street() == "street"); QVERIFY(n->town() == "town"); QVERIFY(n->postcode() == "postcode"); QVERIFY(n->telephone() == "telephone"); QVERIFY(n->manager() == "manager"); QVERIFY(n->name() == "name"); QVERIFY(n->sortcode() == "sortcode"); } void MyMoneyInstitutionTest::testCopyConstructor() { - MyMoneyInstitution* n1 = new MyMoneyInstitution("GUID1", *n); + QScopedPointer n1 (new MyMoneyInstitution("GUID1", *n)); MyMoneyInstitution n2(*n1); QVERIFY(*n1 == n2); - - delete n1; } void MyMoneyInstitutionTest::testMyMoneyFileConstructor() { - MyMoneyInstitution *t = new MyMoneyInstitution("GUID", *n); + QScopedPointer t (new MyMoneyInstitution("GUID", *n)); QVERIFY(t->id() == "GUID"); QVERIFY(t->street() == "street"); QVERIFY(t->town() == "town"); QVERIFY(t->postcode() == "postcode"); QVERIFY(t->telephone() == "telephone"); QVERIFY(t->manager() == "manager"); QVERIFY(t->name() == "name"); QVERIFY(t->sortcode() == "sortcode"); - - delete t; } void MyMoneyInstitutionTest::testEquality() { MyMoneyInstitution t("name", "town", "street", "postcode", "telephone", "manager", "sortcode"); QVERIFY(t == *n); t.setStreet("x"); QVERIFY(!(t == *n)); t.setStreet("street"); QVERIFY(t == *n); t.setName("x"); QVERIFY(!(t == *n)); t.setName("name"); QVERIFY(t == *n); t.setTown("x"); QVERIFY(!(t == *n)); t.setTown("town"); QVERIFY(t == *n); t.setPostcode("x"); QVERIFY(!(t == *n)); t.setPostcode("postcode"); QVERIFY(t == *n); t.setTelephone("x"); QVERIFY(!(t == *n)); t.setTelephone("telephone"); QVERIFY(t == *n); t.setManager("x"); QVERIFY(!(t == *n)); t.setManager("manager"); QVERIFY(t == *n); - MyMoneyInstitution* n1 = new MyMoneyInstitution("GUID1", *n); - MyMoneyInstitution* n2 = new MyMoneyInstitution("GUID1", *n); + QScopedPointer n1 (new MyMoneyInstitution("GUID1", *n)); + QScopedPointer n2 (new MyMoneyInstitution("GUID1", *n)); n1->addAccountId("A000001"); n2->addAccountId("A000001"); QVERIFY(*n1 == *n2); - - delete n1; - delete n2; } void MyMoneyInstitutionTest::testInequality() { - MyMoneyInstitution* n1 = new MyMoneyInstitution("GUID0", *n); - MyMoneyInstitution* n2 = new MyMoneyInstitution("GUID1", *n); - MyMoneyInstitution* n3 = new MyMoneyInstitution("GUID2", *n); - MyMoneyInstitution* n4 = new MyMoneyInstitution("GUID2", *n); + QScopedPointer n1 (new MyMoneyInstitution("GUID0", *n)); + QScopedPointer n2 (new MyMoneyInstitution("GUID1", *n)); + QScopedPointer n3 (new MyMoneyInstitution("GUID2", *n)); + QScopedPointer n4 (new MyMoneyInstitution("GUID2", *n)); QVERIFY(!(*n1 == *n2)); QVERIFY(!(*n1 == *n3)); QVERIFY(*n3 == *n4); n3->addAccountId("A000001"); n4->addAccountId("A000002"); QVERIFY(!(*n3 == *n4)); - - delete n1; - delete n2; - delete n3; - delete n4; } void MyMoneyInstitutionTest::testAccountIDList() { MyMoneyInstitution institution; QStringList list; QString id; // list must be empty list = institution.accountList(); QVERIFY(list.count() == 0); // add one account institution.addAccountId("A000002"); list = institution.accountList(); QVERIFY(list.count() == 1); QVERIFY(list.contains("A000002") == 1); // adding same account shouldn't make a difference institution.addAccountId("A000002"); list = institution.accountList(); QVERIFY(list.count() == 1); QVERIFY(list.contains("A000002") == 1); // now add another account institution.addAccountId("A000001"); list = institution.accountList(); QVERIFY(list.count() == 2); QVERIFY(list.contains("A000002") == 1); QVERIFY(list.contains("A000001") == 1); id = institution.removeAccountId("A000001"); QVERIFY(id == "A000001"); list = institution.accountList(); QVERIFY(list.count() == 1); QVERIFY(list.contains("A000002") == 1); } void MyMoneyInstitutionTest::testWriteXML() { MyMoneyKeyValueContainer kvp; n->addAccountId("A000001"); n->addAccountId("A000003"); n->setValue(QString("key"), "value"); QDomDocument doc("TEST"); QDomElement el = doc.createElement("INSTITUTION-CONTAINER"); doc.appendChild(el); MyMoneyInstitution i("I00001", *n); i.writeXML(doc, el); QCOMPARE(doc.doctype().name(), QLatin1String("TEST")); QDomElement institutionContainer = doc.documentElement(); QVERIFY(institutionContainer.isElement()); QCOMPARE(institutionContainer.tagName(), QLatin1String("INSTITUTION-CONTAINER")); QVERIFY(institutionContainer.childNodes().size() == 1); QVERIFY(institutionContainer.elementsByTagName("INSTITUTION").at(0).isElement()); QDomElement institution = institutionContainer.elementsByTagName("INSTITUTION").at(0).toElement(); QCOMPARE(institution.tagName(), QLatin1String("INSTITUTION")); QCOMPARE(institution.attribute("id"), QLatin1String("I00001")); QCOMPARE(institution.attribute("manager"), QLatin1String("manager")); QCOMPARE(institution.attribute("name"), QLatin1String("name")); QCOMPARE(institution.attribute("sortcode"), QLatin1String("sortcode")); QCOMPARE(institution.childNodes().size(), 3); QVERIFY(institution.childNodes().at(0).isElement()); QDomElement address = institution.childNodes().at(0).toElement(); QCOMPARE(address.tagName(), QLatin1String("ADDRESS")); QCOMPARE(address.attribute("street"), QLatin1String("street")); QCOMPARE(address.attribute("telephone"), QLatin1String("telephone")); QCOMPARE(address.attribute("zip"), QLatin1String("postcode")); QCOMPARE(address.attribute("city"), QLatin1String("town")); QCOMPARE(address.childNodes().size(), 0); QVERIFY(institution.childNodes().at(1).isElement()); QDomElement accountIds = institution.childNodes().at(1).toElement(); QCOMPARE(accountIds.tagName(), QLatin1String("ACCOUNTIDS")); QCOMPARE(accountIds.childNodes().size(), 2); QVERIFY(accountIds.childNodes().at(0).isElement()); QDomElement account1 = accountIds.childNodes().at(0).toElement(); QCOMPARE(account1.tagName(), QLatin1String("ACCOUNTID")); QCOMPARE(account1.attribute("id"), QLatin1String("A000001")); QCOMPARE(account1.childNodes().size(), 0); QVERIFY(accountIds.childNodes().at(1).isElement()); QDomElement account2 = accountIds.childNodes().at(1).toElement(); QCOMPARE(account2.tagName(), QLatin1String("ACCOUNTID")); QCOMPARE(account2.attribute("id"), QLatin1String("A000003")); QCOMPARE(account2.childNodes().size(), 0); QVERIFY(institution.childNodes().at(2).isElement()); QDomElement keyValuePairs = institution.childNodes().at(2).toElement(); QCOMPARE(keyValuePairs.tagName(), QLatin1String("KEYVALUEPAIRS")); QCOMPARE(keyValuePairs.childNodes().size(), 1); QVERIFY(keyValuePairs.childNodes().at(0).isElement()); QDomElement keyValuePair1 = keyValuePairs.childNodes().at(0).toElement(); QCOMPARE(keyValuePair1.tagName(), QLatin1String("PAIR")); QCOMPARE(keyValuePair1.attribute("key"), QLatin1String("key")); QCOMPARE(keyValuePair1.attribute("value"), QLatin1String("value")); QCOMPARE(keyValuePair1.childNodes().size(), 0); } void MyMoneyInstitutionTest::testReadXML() { MyMoneyInstitution i; QString ref_ok = QString( "\n" "\n" " \n" "
\n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" "\n"); QString ref_false = QString( "\n" "\n" " \n" "
\n" " \n" " \n" " \n" " \n" " \n" "\n"); QDomDocument doc; QDomElement node; doc.setContent(ref_false); node = doc.documentElement().firstChild().toElement(); try { i = MyMoneyInstitution(node); QFAIL("Missing expected exception"); } catch (const MyMoneyException &) { } i.addAccountId("TEST"); doc.setContent(ref_ok); node = doc.documentElement().firstChild().toElement(); try { QStringList alist; alist << "A000001" << "A000003"; i = MyMoneyInstitution(node); QVERIFY(i.sortcode() == "sortcode"); QVERIFY(i.id() == "I00001"); QVERIFY(i.manager() == "manager"); QVERIFY(i.name() == "name"); QVERIFY(i.street() == "street"); QVERIFY(i.postcode() == "postcode"); QVERIFY(i.city() == "town"); QVERIFY(i.telephone() == "telephone"); QVERIFY(i.accountList() == alist); QVERIFY(i.value(QString("key")) == "value"); } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } } void MyMoneyInstitutionTest::testElementNames() -{ +{ for (auto i = (int)Institution::Element::AccountID; i <= (int)Institution::Element::Address; ++i) { auto isEmpty = MyMoneyInstitutionPrivate::getElName(static_cast(i)).isEmpty(); if (isEmpty) qWarning() << "Empty element's name " << i; QVERIFY(!isEmpty); } } void MyMoneyInstitutionTest::testAttributeNames() { for (auto i = (int)Institution::Attribute::ID; i < (int)Institution::Attribute::LastAttribute; ++i) { auto isEmpty = MyMoneyInstitutionPrivate::getAttrName(static_cast(i)).isEmpty(); if (isEmpty) qWarning() << "Empty attribute's name " << i; QVERIFY(!isEmpty); } } diff --git a/kmymoney/payeeidentifier/ibanandbic/bicmodel.cpp b/kmymoney/payeeidentifier/ibanandbic/bicmodel.cpp index 3385cf66f..b22012ed6 100644 --- a/kmymoney/payeeidentifier/ibanandbic/bicmodel.cpp +++ b/kmymoney/payeeidentifier/ibanandbic/bicmodel.cpp @@ -1,112 +1,111 @@ /* * This file is part of KMyMoney, A Personal Finance Manager by KDE * Copyright (C) 2014 Christian Dávid * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "bicmodel.h" #include #include #include #include #include #include #include /** * @warning At the moment the completion may fail if bicModel was created in more than one thread * (it uses a QSqlDatabase object over all instances of bicModel, so the first created bicModel defines * the thread) * * @todo Make thread safe. */ bicModel::bicModel(QObject* parent) : QSqlQueryModel(parent) { QSqlDatabase db = QSqlDatabase::database("bicModel", true); // Save if the database was opened before bool attachDatabases = false; if (!db.isValid()) { db = QSqlDatabase::addDatabase("QSQLITE", "bicModel"); db.setDatabaseName(":memory:"); db.setConnectOptions("QSQLITE_OPEN_READONLY=1;QSQLITE_ENABLE_SHARED_CACHE=1;"); - db.open(); - // Database was not opened before - attachDatabases = true; + if (db.open()) + attachDatabases = true; // Database was not opened before } if (!db.isOpen()) { qWarning() << QString("Could not open in-memory database for bic data."); } QSqlQuery query(db); // Get services which support iban2bic and have a database entry KService::List services = KServiceTypeTrader::self()->query("KMyMoney/IbanBicData", QString("exist [X-KMyMoney-Bankdata-Database]") ); if (services.isEmpty()) { // Set a valid query if (query.exec("SELECT null;")) setQuery(query); return; } QStringList databases; QStringList dbNames; unsigned int databaseCount = 0; foreach (KService::Ptr service, services) { QString database = service->property(QLatin1String("X-KMyMoney-Bankdata-Database")).toString(); // Locate database QString path = QStandardPaths::locate(QStandardPaths::DataLocation, QLatin1String("kmymoney/ibanbicdata/") + database); if (path.isEmpty()) { qWarning() << QString("Could not locate database file \"%1\" to receive BIC data.").arg(database); } else { databases << path; dbNames << QString("db%1").arg(++databaseCount); } } if (attachDatabases) { query.prepare("ATTACH DATABASE ? AS ?"); query.addBindValue(databases); query.addBindValue(dbNames); if (!query.execBatch()) { qWarning() << "Could not init bic for bicModel, last error:" << query.lastError().text(); dbNames = QStringList(); // clear so no query will be set } } QStringList queries; foreach (QString dbName, dbNames) { queries.append(QString("SELECT bic, name FROM %1.institutions").arg(dbName)); } - query.exec(queries.join(QLatin1String(" UNION "))); - setQuery(query); + if (query.exec(queries.join(QLatin1String(" UNION ")))) + setQuery(query); } QVariant bicModel::data(const QModelIndex& item, int role) const { if (role == InstitutionNameRole) return QSqlQueryModel::data(createIndex(item.row(), 1), Qt::DisplayRole); return QSqlQueryModel::data(item, role); } diff --git a/kmymoney/plugins/csv/import/core/csvimportercore.cpp b/kmymoney/plugins/csv/import/core/csvimportercore.cpp index 604bf946c..671bc644d 100644 --- a/kmymoney/plugins/csv/import/core/csvimportercore.cpp +++ b/kmymoney/plugins/csv/import/core/csvimportercore.cpp @@ -1,1764 +1,1767 @@ /*************************************************************************** csvimportercore.cpp ------------------- begin : Sun May 21 2017 copyright : (C) 2010 by Allan Anderson email : agander93@gmail.com copyright : (C) 2016-2017 by Łukasz Wojniłowicz email : lukasz.wojnilowicz@gmail.com ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "csvimportercore.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneytransaction.h" #include "csvutil.h" #include "convdate.h" #include "mymoneyenums.h" const QHash CSVImporterCore::m_profileConfPrefix { {Profile::Banking, QStringLiteral("Bank")}, {Profile::Investment, QStringLiteral("Invest")}, {Profile::CurrencyPrices, QStringLiteral("CPrices")}, {Profile::StockPrices, QStringLiteral("SPrices")} }; const QHash CSVImporterCore::m_colTypeConfName { {Column::Date, QStringLiteral("DateCol")}, {Column::Memo, QStringLiteral("MemoCol")}, {Column::Number, QStringLiteral("NumberCol")}, {Column::Payee, QStringLiteral("PayeeCol")}, {Column::Amount, QStringLiteral("AmountCol")}, {Column::Credit, QStringLiteral("CreditCol")}, {Column::Debit, QStringLiteral("DebitCol")}, {Column::Category, QStringLiteral("CategoryCol")}, {Column::Type, QStringLiteral("TypeCol")}, {Column::Price, QStringLiteral("PriceCol")}, {Column::Quantity, QStringLiteral("QuantityCol")}, {Column::Fee, QStringLiteral("FeeCol")}, {Column::Symbol, QStringLiteral("SymbolCol")}, {Column::Name, QStringLiteral("NameCol")}, }; const QHash CSVImporterCore::m_miscSettingsConfName { {ConfDirectory, QStringLiteral("Directory")}, {ConfEncoding, QStringLiteral("Encoding")}, {ConfDateFormat, QStringLiteral("DateFormat")}, {ConfFieldDelimiter, QStringLiteral("FieldDelimiter")}, {ConfTextDelimiter, QStringLiteral("TextDelimiter")}, {ConfDecimalSymbol, QStringLiteral("DecimalSymbol")}, {ConfStartLine, QStringLiteral("StartLine")}, {ConfTrailerLines, QStringLiteral("TrailerLines")}, {ConfOppositeSigns, QStringLiteral("OppositeSigns")}, {ConfFeeIsPercentage, QStringLiteral("FeeIsPercentage")}, {ConfFeeRate, QStringLiteral("FeeRate")}, {ConfMinFee, QStringLiteral("MinFee")}, {ConfSecurityName, QStringLiteral("SecurityName")}, {ConfSecuritySymbol, QStringLiteral("SecuritySymbol")}, {ConfCurrencySymbol, QStringLiteral("CurrencySymbol")}, {ConfPriceFraction, QStringLiteral("PriceFraction")}, {ConfDontAsk, QStringLiteral("DontAsk")}, {ConfHeight, QStringLiteral("Height")}, {ConfWidth, QStringLiteral("Width")} }; const QHash CSVImporterCore::m_transactionConfName { {eMyMoney::Transaction::Action::Buy, QStringLiteral("BuyParam")}, {eMyMoney::Transaction::Action::Sell, QStringLiteral("SellParam")}, {eMyMoney::Transaction::Action::ReinvestDividend, QStringLiteral("ReinvdivParam")}, {eMyMoney::Transaction::Action::CashDividend, QStringLiteral("DivXParam")}, {eMyMoney::Transaction::Action::Interest, QStringLiteral("IntIncParam")}, {eMyMoney::Transaction::Action::Shrsin, QStringLiteral("ShrsinParam")}, {eMyMoney::Transaction::Action::Shrsout, QStringLiteral("ShrsoutParam")} }; const QString CSVImporterCore::m_confProfileNames = QStringLiteral("ProfileNames"); const QString CSVImporterCore::m_confPriorName = QStringLiteral("Prior"); const QString CSVImporterCore::m_confMiscName = QStringLiteral("Misc"); CSVImporterCore::CSVImporterCore() : m_profile(0), m_isActionTypeValidated(false) { m_convertDate = new ConvertDate; m_file = new CSVFile; m_priceFractions << MyMoneyMoney(0.01) << MyMoneyMoney(0.1) << MyMoneyMoney::ONE << MyMoneyMoney(10) << MyMoneyMoney(100); validateConfigFile(); readMiscSettings(); } CSVImporterCore::~CSVImporterCore() { delete m_convertDate; delete m_file; } MyMoneyStatement CSVImporterCore::unattendedImport(const QString &filename, CSVProfile *profile) { MyMoneyStatement st; m_profile = profile; m_convertDate->setDateFormatIndex(m_profile->m_dateFormat); if (m_file->getInFileName(filename)) { m_file->readFile(m_profile); m_file->setupParser(m_profile); if (profile->m_decimalSymbol == DecimalSymbol::Auto) { auto columns = getNumericalColumns(); if (detectDecimalSymbols(columns) != -2) return st; } if (!createStatement(st)) st = MyMoneyStatement(); } return st; } KSharedConfigPtr CSVImporterCore::configFile() { return KSharedConfig::openConfig(QStringLiteral("kmymoney/csvimporterrc")); } void CSVImporterCore::profileFactory(const Profile type, const QString &name) { // delete current profile if (m_profile) { delete m_profile; m_profile = nullptr; } switch (type) { default: case Profile::Investment: m_profile = new InvestmentProfile; break; case Profile::Banking: m_profile = new BankingProfile; break; case Profile::CurrencyPrices: case Profile::StockPrices: m_profile = new PricesProfile(type); break; } m_profile->m_profileName = name; } void CSVImporterCore::readMiscSettings() { KConfigGroup miscGroup(configFile(), m_confMiscName); m_autodetect.clear(); m_autodetect.insert(AutoFieldDelimiter, miscGroup.readEntry(QStringLiteral("AutoFieldDelimiter"), true)); m_autodetect.insert(AutoDecimalSymbol, miscGroup.readEntry(QStringLiteral("AutoDecimalSymbol"), true)); m_autodetect.insert(AutoDateFormat, miscGroup.readEntry(QStringLiteral("AutoDateFormat"), true)); m_autodetect.insert(AutoAccountInvest, miscGroup.readEntry(QStringLiteral("AutoAccountInvest"), true)); m_autodetect.insert(AutoAccountBank, miscGroup.readEntry(QStringLiteral("AutoAccountBank"), true)); } void CSVImporterCore::validateConfigFile() { const KSharedConfigPtr config = configFile(); KConfigGroup profileNamesGroup(config, m_confProfileNames); if (!profileNamesGroup.exists()) { profileNamesGroup.writeEntry(m_profileConfPrefix.value(Profile::Banking), QStringList()); profileNamesGroup.writeEntry(m_profileConfPrefix.value(Profile::Investment), QStringList()); profileNamesGroup.writeEntry(m_profileConfPrefix.value(Profile::CurrencyPrices), QStringList()); profileNamesGroup.writeEntry(m_profileConfPrefix.value(Profile::StockPrices), QStringList()); profileNamesGroup.writeEntry(m_confPriorName + m_profileConfPrefix.value(Profile::Banking), int()); profileNamesGroup.writeEntry(m_confPriorName + m_profileConfPrefix.value(Profile::Investment), int()); profileNamesGroup.writeEntry(m_confPriorName + m_profileConfPrefix.value(Profile::CurrencyPrices), int()); profileNamesGroup.writeEntry(m_confPriorName + m_profileConfPrefix.value(Profile::StockPrices), int()); profileNamesGroup.sync(); } KConfigGroup miscGroup(config, m_confMiscName); if (!miscGroup.exists()) { miscGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfHeight), "400"); miscGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfWidth), "800"); miscGroup.sync(); } QList confVer = miscGroup.readEntry("KMMVer", QList {0, 0, 0}); if (updateConfigFile(confVer)) // write kmmVer only if there were no errors miscGroup.writeEntry("KMMVer", confVer); } bool CSVImporterCore::updateConfigFile(QList &confVer) { bool ret = true; QList kmmVer = QList {5, 0, 0}; int kmmVersion = kmmVer.at(0) * 100 + kmmVer.at(1) * 10 + kmmVer.at(2); int confVersion = confVer.at(0) * 100 + confVer.at(1) * 10 + confVer.at(2); if (confVersion > kmmVersion) { KMessageBox::information(0, i18n("Version of your CSV config file is %1.%2.%3 and is newer than supported version %4.%5.%6. Expect troubles.", confVer.at(0), confVer.at(1), confVer.at(2), kmmVer.at(0), kmmVer.at(1), kmmVer.at(2))); ret = false; return ret; } else if (confVersion == kmmVersion) return true; confVer = kmmVer; const KSharedConfigPtr config = configFile(); QString configFilePath = config.constData()->name(); QFile::copy(configFilePath, configFilePath + QLatin1String(".bak")); KConfigGroup profileNamesGroup(config, m_confProfileNames); QStringList bankProfiles = profileNamesGroup.readEntry(m_profileConfPrefix.value(Profile::Banking), QStringList()); QStringList investProfiles = profileNamesGroup.readEntry(m_profileConfPrefix.value(Profile::Investment), QStringList()); QStringList invalidBankProfiles = profileNamesGroup.readEntry(QLatin1String("Invalid") + m_profileConfPrefix.value(Profile::Banking), QStringList()); // get profiles that was marked invalid during last update QStringList invalidInvestProfiles = profileNamesGroup.readEntry(QLatin1String("Invalid") + m_profileConfPrefix.value(Profile::Investment), QStringList()); QString bankPrefix = m_profileConfPrefix.value(Profile::Banking) + QLatin1Char('-'); QString investPrefix = m_profileConfPrefix.value(Profile::Investment) + QLatin1Char('-'); // for kmm < 5.0.0 change 'BankNames' to 'ProfileNames' and remove 'MainWindow' group if (confVersion < 500 && bankProfiles.isEmpty()) { KConfigGroup oldProfileNamesGroup(config, "BankProfiles"); bankProfiles = oldProfileNamesGroup.readEntry("BankNames", QStringList()); // profile names are under 'BankNames' entry for kmm < 5.0.0 bankPrefix = QLatin1String("Profiles-"); // needed to remove non-existent profiles in first run oldProfileNamesGroup.deleteGroup(); KConfigGroup oldMainWindowGroup(config, "MainWindow"); oldMainWindowGroup.deleteGroup(); KConfigGroup oldSecuritiesGroup(config, "Securities"); oldSecuritiesGroup.deleteGroup(); } bool firstTry = false; if (invalidBankProfiles.isEmpty() && invalidInvestProfiles.isEmpty()) // if there is no invalid profiles then this might be first update try firstTry = true; int invalidProfileResponse = QDialogButtonBox::No; for (auto profileName = bankProfiles.begin(); profileName != bankProfiles.end();) { KConfigGroup bankProfile(config, bankPrefix + *profileName); if (!bankProfile.exists() && !invalidBankProfiles.contains(*profileName)) { // if there is reference to profile but no profile then remove this reference profileName = bankProfiles.erase(profileName); continue; } // for kmm < 5.0.0 remove 'FileType' and 'ProfileName' and assign them to either "Bank=" or "Invest=" if (confVersion < 500) { QString lastUsedDirectory; KConfigGroup oldBankProfile(config, QLatin1String("Profiles-") + *profileName); // if half of configuration is updated and the other one untouched this is needed QString oldProfileType = oldBankProfile.readEntry("FileType", QString()); KConfigGroup newProfile; if (oldProfileType == QLatin1String("Invest")) { oldBankProfile.deleteEntry("BrokerageParam"); oldBankProfile.writeEntry(m_colTypeConfName.value(Column::Type), oldBankProfile.readEntry("PayeeCol")); oldBankProfile.deleteEntry("PayeeCol"); oldBankProfile.deleteEntry("Filter"); oldBankProfile.deleteEntry("SecurityName"); lastUsedDirectory = oldBankProfile.readEntry("InvDirectory"); newProfile = KConfigGroup(config, m_profileConfPrefix.value(Profile::Investment) + QLatin1Char('-') + *profileName); investProfiles.append(*profileName); profileName = bankProfiles.erase(profileName); } else if (oldProfileType == QLatin1String("Banking")) { lastUsedDirectory = oldBankProfile.readEntry("CsvDirectory"); newProfile = KConfigGroup(config, m_profileConfPrefix.value(Profile::Banking) + QLatin1Char('-') + *profileName); ++profileName; } else { if (invalidProfileResponse != QDialogButtonBox::YesToAll && invalidProfileResponse != QDialogButtonBox::NoToAll) { if (!firstTry && !invalidBankProfiles.contains(*profileName)) { // if it isn't first update run and profile isn't on the list of invalid ones then don't bother ++profileName; continue; } invalidProfileResponse = KMessageBox::createKMessageBox(nullptr, new QDialogButtonBox(QDialogButtonBox::Yes | QDialogButtonBox::YesToAll | QDialogButtonBox::No | QDialogButtonBox::NoToAll), QMessageBox::Warning, i18n("
During update of %1
" "the profile type for %2 could not be recognized.
" "The profile cannot be used because of that.
" "Do you want to delete it?
", configFilePath, *profileName), QStringList(), QString(), nullptr, KMessageBox::Dangerous); } switch (invalidProfileResponse) { case QDialogButtonBox::YesToAll: case QDialogButtonBox::Yes: oldBankProfile.deleteGroup(); invalidBankProfiles.removeOne(*profileName); profileName = bankProfiles.erase(profileName); break; case QDialogButtonBox::NoToAll: case QDialogButtonBox::No: if (!invalidBankProfiles.contains(*profileName)) // on user request: don't delete profile but keep eye on it invalidBankProfiles.append(*profileName); ret = false; ++profileName; break; } continue; } oldBankProfile.deleteEntry("FileType"); oldBankProfile.deleteEntry("ProfileName"); oldBankProfile.deleteEntry("DebitFlag"); oldBankProfile.deleteEntry("InvDirectory"); oldBankProfile.deleteEntry("CsvDirectory"); oldBankProfile.sync(); oldBankProfile.copyTo(&newProfile); oldBankProfile.deleteGroup(); newProfile.writeEntry(m_miscSettingsConfName.value(ConfDirectory), lastUsedDirectory); newProfile.writeEntry(m_miscSettingsConfName.value(ConfEncoding), "106" /*UTF-8*/ ); // in 4.8 encoding wasn't supported well so set it to utf8 by default newProfile.sync(); } } for (auto profileName = investProfiles.begin(); profileName != investProfiles.end();) { KConfigGroup investProfile(config, investPrefix + *profileName); if (!investProfile.exists() && !invalidInvestProfiles.contains(*profileName)) { // if there is reference to profile but no profile then remove this reference profileName = investProfiles.erase(profileName); continue; } ++profileName; } profileNamesGroup.writeEntry(m_profileConfPrefix.value(Profile::Banking), bankProfiles); // update profile names as some of them might have been changed profileNamesGroup.writeEntry(m_profileConfPrefix.value(Profile::Investment), investProfiles); if (invalidBankProfiles.isEmpty()) // if no invalid profiles then we don't need this variable anymore profileNamesGroup.deleteEntry("InvalidBank"); else profileNamesGroup.writeEntry("InvalidBank", invalidBankProfiles); if (invalidInvestProfiles.isEmpty()) profileNamesGroup.deleteEntry("InvalidInvest"); else profileNamesGroup.writeEntry("InvalidInvest", invalidInvestProfiles); if (ret) QFile::remove(configFilePath + ".bak"); // remove backup if all is ok return ret; } bool CSVImporterCore::profilesAction(const Profile type, const ProfileAction action, const QString &name, const QString &newname) { bool ret = false; const KSharedConfigPtr config = configFile(); KConfigGroup profileNamesGroup(config, m_confProfileNames); QString profileTypeStr = m_profileConfPrefix.value(type); QStringList profiles = profileNamesGroup.readEntry(profileTypeStr, QStringList()); KConfigGroup profileName(config, profileTypeStr + QLatin1Char('-') + name); switch (action) { case ProfileAction::UpdateLastUsed: profileNamesGroup.writeEntry(m_confPriorName + profileTypeStr, profiles.indexOf(name)); break; case ProfileAction::Add: if (!profiles.contains(newname)) { profiles.append(newname); ret = true; } break; case ProfileAction::Remove: { profiles.removeOne(name); profileName.deleteGroup(); profileName.sync(); ret = true; break; } case ProfileAction::Rename: { if (!newname.isEmpty() && name != newname) { int idx = profiles.indexOf(name); if (idx != -1) { profiles[idx] = newname; KConfigGroup newProfileName(config, profileTypeStr + QLatin1Char('-') + newname); if (profileName.exists() && !newProfileName.exists()) { profileName.copyTo(&newProfileName); profileName.deleteGroup(); profileName.sync(); newProfileName.sync(); ret = true; } } } break; } } profileNamesGroup.writeEntry(profileTypeStr, profiles); profileNamesGroup.sync(); return ret; } bool CSVImporterCore::validateDateFormat(const int col) { bool isOK = true; for (int row = m_profile->m_startLine; row <= m_profile->m_endLine; ++row) { QStandardItem* item = m_file->m_model->item(row, col); QDate dat = m_convertDate->convertDate(item->text()); if (dat == QDate()) { isOK = false; break; } } return isOK; } bool CSVImporterCore::validateDecimalSymbols(const QList &columns) { bool isOK = true; foreach (const auto column, columns) { m_file->m_parse->setDecimalSymbol(m_decimalSymbolIndexMap.value(column)); for (int row = m_profile->m_startLine; row <= m_profile->m_endLine; ++row) { QStandardItem *item = m_file->m_model->item(row, column); QString rawNumber = item->text(); m_file->m_parse->possiblyReplaceSymbol(rawNumber); if (m_file->m_parse->invalidConversion() && !rawNumber.isEmpty()) { // empty strings are welcome isOK = false; break; } } } return isOK; } bool CSVImporterCore::validateCurrencies(const PricesProfile *profile) { if (profile->m_securitySymbol.isEmpty() || profile->m_currencySymbol.isEmpty()) return false; return true; } bool CSVImporterCore::validateSecurity(const PricesProfile *profile) { if (profile->m_securitySymbol.isEmpty() || profile->m_securityName.isEmpty()) return false; return true; } bool CSVImporterCore::validateSecurity(const InvestmentProfile *profile) { if (profile->m_securitySymbol.isEmpty() || profile->m_securityName.isEmpty()) return false; return true; } bool CSVImporterCore::validateSecurities() { QSet onlySymbols; QSet onlyNames; sortSecurities(onlySymbols, onlyNames, m_mapSymbolName); if (!onlySymbols.isEmpty() || !onlyNames.isEmpty()) return false; return true; } eMyMoney::Transaction::Action CSVImporterCore::processActionTypeField(const InvestmentProfile *profile, const int row, const int col) { if (col == -1) return eMyMoney::Transaction::Action::None; QString type = m_file->m_model->item(row, col)->text(); QList actions; actions << eMyMoney::Transaction::Action::Buy << eMyMoney::Transaction::Action::Sell << // first and second most frequent action eMyMoney::Transaction::Action::ReinvestDividend << eMyMoney::Transaction::Action::CashDividend << // we don't want "reinv-dividend" to be accidentaly caught by "dividend" eMyMoney::Transaction::Action::Interest << eMyMoney::Transaction::Action::Shrsin << eMyMoney::Transaction::Action::Shrsout; foreach (const auto action, actions) { if (profile->m_transactionNames.value(action).contains(type, Qt::CaseInsensitive)) return action; } return eMyMoney::Transaction::Action::None; } validationResultE CSVImporterCore::validateActionType(MyMoneyStatement::Transaction &tr) { validationResultE ret = ValidActionType; QList validActionTypes = createValidActionTypes(tr); if (validActionTypes.isEmpty()) ret = InvalidActionValues; else if (!validActionTypes.contains(tr.m_eAction)) ret = NoActionType; return ret; } bool CSVImporterCore::calculateFee() { auto profile = dynamic_cast(m_profile); if (!profile) return false; if ((profile->m_feeRate.isEmpty() || // check whether feeRate... profile->m_colTypeNum.value(Column::Amount) == -1)) // ...and amount is in place return false; QString decimalSymbol; if (profile->m_decimalSymbol == DecimalSymbol::Auto) { DecimalSymbol detectedSymbol = detectDecimalSymbol(profile->m_colTypeNum.value(Column::Amount), QString()); if (detectedSymbol == DecimalSymbol::Auto) return false; m_file->m_parse->setDecimalSymbol(detectedSymbol); decimalSymbol = m_file->m_parse->decimalSymbol(detectedSymbol); } else decimalSymbol = m_file->m_parse->decimalSymbol(profile->m_decimalSymbol); MyMoneyMoney feePercent(m_file->m_parse->possiblyReplaceSymbol(profile->m_feeRate)); // convert 0.67% ... feePercent /= MyMoneyMoney(100); // ... to 0.0067 if (profile->m_minFee.isEmpty()) profile->m_minFee = QString::number(0.00, 'f', 2); MyMoneyMoney minFee(m_file->m_parse->possiblyReplaceSymbol(profile->m_minFee)); QList items; for (int row = 0; row < profile->m_startLine; ++row) // fill rows above with whitespace for nice effect with markUnwantedRows items.append(new QStandardItem(QString())); for (int row = profile->m_startLine; row <= profile->m_endLine; ++row) { QString txt, numbers; bool ok = false; numbers = txt = m_file->m_model->item(row, profile->m_colTypeNum.value(Column::Amount))->text(); numbers.remove(QRegularExpression(QStringLiteral("[,. ]"))).toInt(&ok); if (!ok) { // check if it's numerical string... items.append(new QStandardItem(QString())); continue; // ...and skip if not (TODO: allow currency symbols and IDs) } if (txt.startsWith(QLatin1Char('('))) { txt.remove(QRegularExpression(QStringLiteral("[()]"))); txt.prepend(QLatin1Char('-')); } txt = m_file->m_parse->possiblyReplaceSymbol(txt); MyMoneyMoney fee(txt); fee *= feePercent; if (fee < minFee) fee = minFee; txt.setNum(fee.toDouble(), 'f', 4); txt.replace(QLatin1Char('.'), decimalSymbol); //make sure decimal symbol is uniform in whole line items.append(new QStandardItem(txt)); } for (int row = profile->m_endLine + 1; row < m_file->m_rowCount; ++row) // fill rows below with whitespace for nice effect with markUnwantedRows items.append(new QStandardItem(QString())); int col = profile->m_colTypeNum.value(Column::Fee, -1); if (col == -1) { // fee column isn't present m_file->m_model->appendColumn(items); ++m_file->m_columnCount; } else if (col >= m_file->m_columnCount) { // column number must have been stored in profile m_file->m_model->appendColumn(items); ++m_file->m_columnCount; } else { // fee column is present and has been recalculated m_file->m_model->removeColumn(m_file->m_columnCount - 1); m_file->m_model->appendColumn(items); } profile->m_colTypeNum[Column::Fee] = m_file->m_columnCount - 1; return true; } DecimalSymbol CSVImporterCore::detectDecimalSymbol(const int col, const QString &exclude) { DecimalSymbol detectedSymbol = DecimalSymbol::Auto; QString pattern; QRegularExpression re("^[\\(+-]?\\d+[\\)]?$"); // matches '0' ; '+12' ; '-345' ; '(6789)' bool dotIsDecimalSeparator = false; bool commaIsDecimalSeparator = false; for (int row = m_profile->m_startLine; row <= m_profile->m_endLine; ++row) { QString txt = m_file->m_model->item(row, col)->text(); if (txt.isEmpty()) // nothing to process, so go to next row continue; int dotPos = txt.lastIndexOf(QLatin1Char('.')); // get last positions of decimal/thousand separator... int commaPos = txt.lastIndexOf(QLatin1Char(',')); // ...to be able to determine which one is the last if (dotPos != -1 && commaPos != -1) { if (dotPos > commaPos && commaIsDecimalSeparator == false) // following case 1,234.56 dotIsDecimalSeparator = true; else if (dotPos < commaPos && dotIsDecimalSeparator == false) // following case 1.234,56 commaIsDecimalSeparator = true; else // following case 1.234,56 and somwhere earlier there was 1,234.56 so unresolvable conflict return detectedSymbol; } else if (dotPos != -1) { // following case 1.23 if (dotIsDecimalSeparator) // it's already know that dotIsDecimalSeparator continue; if (!commaIsDecimalSeparator) // if there is no conflict with comma as decimal separator dotIsDecimalSeparator = true; else { if (txt.count(QLatin1Char('.')) > 1) // following case 1.234.567 so OK continue; else if (txt.length() - 4 == dotPos) // following case 1.234 and somwhere earlier there was 1.234,56 so OK continue; else // following case 1.23 and somwhere earlier there was 1,23 so unresolvable conflict return detectedSymbol; } } else if (commaPos != -1) { // following case 1,23 if (commaIsDecimalSeparator) // it's already know that commaIsDecimalSeparator continue; else if (!dotIsDecimalSeparator) // if there is no conflict with dot as decimal separator commaIsDecimalSeparator = true; else { if (txt.count(QLatin1Char(',')) > 1) // following case 1,234,567 so OK continue; else if (txt.length() - 4 == commaPos) // following case 1,234 and somwhere earlier there was 1,234.56 so OK continue; else // following case 1,23 and somwhere earlier there was 1.23 so unresolvable conflict return detectedSymbol; } } else { // following case 123 if (pattern.isEmpty()) { } txt.remove(QRegularExpression(QLatin1String("[ ") + QRegularExpression::escape(exclude) + QLatin1String("]"))); QRegularExpressionMatch match = re.match(txt); if (match.hasMatch()) // if string is pure numerical then go forward... continue; else // ...if not then it's non-numerical garbage return detectedSymbol; } } if (dotIsDecimalSeparator) detectedSymbol = DecimalSymbol::Dot; else if (commaIsDecimalSeparator) detectedSymbol = DecimalSymbol::Comma; else { // whole column was empty, but we don't want to fail so take OS's decimal symbol if (QLocale().decimalPoint() == QLatin1Char('.')) detectedSymbol = DecimalSymbol::Dot; else detectedSymbol = DecimalSymbol::Comma; } return detectedSymbol; } int CSVImporterCore::detectDecimalSymbols(const QList &columns) { int ret = -2; // get list of used currencies to remove them from col QList accounts; MyMoneyFile *file = MyMoneyFile::instance(); file->accountList(accounts); QList accountTypes; accountTypes << eMyMoney::Account::Type::Checkings << eMyMoney::Account::Type::Savings << eMyMoney::Account::Type::Liability << eMyMoney::Account::Type::Checkings << eMyMoney::Account::Type::Savings << eMyMoney::Account::Type::Cash << eMyMoney::Account::Type::CreditCard << eMyMoney::Account::Type::Loan << eMyMoney::Account::Type::Asset << eMyMoney::Account::Type::Liability; QSet currencySymbols; foreach (const auto account, accounts) { if (accountTypes.contains(account.accountType())) { // account must actually have currency property currencySymbols.insert(account.currencyId()); // add currency id currencySymbols.insert(file->currency(account.currencyId()).tradingSymbol()); // add currency symbol } } QString filteredCurrencies = QStringList(currencySymbols.values()).join(""); QString pattern = QString::fromLatin1("%1%2").arg(QLocale().currencySymbol()).arg(filteredCurrencies); foreach (const auto column, columns) { DecimalSymbol detectedSymbol = detectDecimalSymbol(column, pattern); if (detectedSymbol == DecimalSymbol::Auto) { ret = column; return ret; } m_decimalSymbolIndexMap.insert(column, detectedSymbol); } return ret; } QList CSVImporterCore::findAccounts(const QList &accountTypes, const QString &statementHeader) { MyMoneyFile* file = MyMoneyFile::instance(); QList accountList; file->accountList(accountList); QList filteredTypes; QList filteredAccounts; QList::iterator account; QRegularExpression filterOutChars(QStringLiteral("[-., ]")); foreach (const auto account, accountList) { if (accountTypes.contains(account.accountType()) && !(account).isClosed()) filteredTypes.append(account); } // filter out accounts whose names aren't in statements header foreach (const auto account, filteredTypes) { QString txt = account.name(); txt.remove(filterOutChars); if (txt.isEmpty() || txt.length() < 3) continue; if (statementHeader.contains(txt, Qt::CaseInsensitive)) filteredAccounts.append(account); } // if filtering returned more results, filter out accounts whose numbers aren't in statements header if (filteredAccounts.count() > 1) { for (account = filteredAccounts.begin(); account != filteredAccounts.end();) { QString txt = (*account).number(); txt.remove(filterOutChars); if (txt.isEmpty() || txt.length() < 3) { ++account; continue; } if (statementHeader.contains(txt, Qt::CaseInsensitive)) ++account; else account = filteredAccounts.erase(account); } } // if filtering returned more results, filter out accounts whose numbers are the shortest if (filteredAccounts.count() > 1) { for (auto i = 1; i < filteredAccounts.count();) { auto firstAccNumber = filteredAccounts.at(0).number(); auto secondAccNumber = filteredAccounts.at(i).number(); if (firstAccNumber.length() > secondAccNumber.length()) { filteredAccounts.removeAt(i); } else if (firstAccNumber.length() < secondAccNumber.length()) { filteredAccounts.removeAt(0); --i; } else { ++i; } } } // if filtering returned more results, filter out accounts whose names are the shortest if (filteredAccounts.count() > 1) { for (auto i = 1; i < filteredAccounts.count();) { auto firstAccName = filteredAccounts.at(0).name(); auto secondAccName = filteredAccounts.at(i).name(); if (firstAccName.length() > secondAccName.length()) { filteredAccounts.removeAt(i); } else if (firstAccName.length() < secondAccName.length()) { filteredAccounts.removeAt(0); --i; } else { ++i; } } } // if filtering by name and number didn't return nothing, then try filtering by number only if (filteredAccounts.isEmpty()) { foreach (const auto account, filteredTypes) { QString txt = account.number(); txt.remove(filterOutChars); if (txt.isEmpty() || txt.length() < 3) continue; if (statementHeader.contains(txt, Qt::CaseInsensitive)) filteredAccounts.append(account); } } return filteredAccounts; } bool CSVImporterCore::detectAccount(MyMoneyStatement &st) { QString statementHeader; for (int row = 0; row < m_profile->m_startLine; ++row) // concatenate header for better search for (int col = 0; col < m_file->m_columnCount; ++col) statementHeader.append(m_file->m_model->item(row, col)->text()); statementHeader.remove(QRegularExpression(QStringLiteral("[-., ]"))); QList accounts; QList accountTypes; switch(m_profile->type()) { default: case Profile::Banking: accountTypes << eMyMoney::Account::Type::Checkings << eMyMoney::Account::Type::Savings << eMyMoney::Account::Type::Liability << eMyMoney::Account::Type::Checkings << eMyMoney::Account::Type::Savings << eMyMoney::Account::Type::Cash << eMyMoney::Account::Type::CreditCard << eMyMoney::Account::Type::Loan << eMyMoney::Account::Type::Asset << eMyMoney::Account::Type::Liability; accounts = findAccounts(accountTypes, statementHeader); break; case Profile::Investment: accountTypes << eMyMoney::Account::Type::Investment; // take investment accounts... accounts = findAccounts(accountTypes, statementHeader); //...and search them in statement header break; } if (accounts.count() == 1) { // set account in statement, if it was the only one match st.m_strAccountName = accounts.first().name(); st.m_strAccountNumber = accounts.first().number(); st.m_accountId = accounts.first().id(); switch (accounts.first().accountType()) { case eMyMoney::Account::Type::Checkings: st.m_eType = eMyMoney::Statement::Type::Checkings; break; case eMyMoney::Account::Type::Savings: st.m_eType = eMyMoney::Statement::Type::Savings; break; case eMyMoney::Account::Type::Investment: st.m_eType = eMyMoney::Statement::Type::Investment; break; case eMyMoney::Account::Type::CreditCard: st.m_eType = eMyMoney::Statement::Type::CreditCard; break; default: st.m_eType = eMyMoney::Statement::Type::None; } return true; } return false; } bool CSVImporterCore::processBankRow(MyMoneyStatement &st, const BankingProfile *profile, const int row) { MyMoneyStatement::Transaction tr; QString memo; QString txt; // process number field if (profile->m_colTypeNum.value(Column::Number, -1) != -1) tr.m_strNumber = txt; // process date field int col = profile->m_colTypeNum.value(Column::Date, -1); tr.m_datePosted = processDateField(row, col); if (tr.m_datePosted == QDate()) return false; // process payee field col = profile->m_colTypeNum.value(Column::Payee, -1); if (col != -1) tr.m_strPayee = m_file->m_model->item(row, col)->text(); // process memo field col = profile->m_colTypeNum.value(Column::Memo, -1); if (col != -1) memo.append(m_file->m_model->item(row, col)->text()); for (int i = 0; i < profile->m_memoColList.count(); ++i) { if (profile->m_memoColList.at(i) != col) { if (!memo.isEmpty()) memo.append(QLatin1Char('\n')); if (profile->m_memoColList.at(i) < m_file->m_columnCount) memo.append(m_file->m_model->item(row, profile->m_memoColList.at(i))->text()); } } tr.m_strMemo = memo; // process amount field col = profile->m_colTypeNum.value(Column::Amount, -1); tr.m_amount = processAmountField(profile, row, col); if (col != -1 && profile->m_oppositeSigns) // change signs to opposite if requested by user tr.m_amount *= MyMoneyMoney(-1); // process credit/debit field if (profile->m_colTypeNum.value(Column::Credit, -1) != -1 && profile->m_colTypeNum.value(Column::Debit, -1) != -1) { QString credit = m_file->m_model->item(row, profile->m_colTypeNum.value(Column::Credit))->text(); QString debit = m_file->m_model->item(row, profile->m_colTypeNum.value(Column::Debit))->text(); tr.m_amount = processCreditDebit(credit, debit); if (!credit.isEmpty() && !debit.isEmpty()) return false; } MyMoneyStatement::Split s1; s1.m_amount = tr.m_amount; s1.m_strMemo = tr.m_strMemo; MyMoneyStatement::Split s2 = s1; s2.m_reconcile = tr.m_reconcile; s2.m_amount = -s1.m_amount; // process category field col = profile->m_colTypeNum.value(Column::Category, -1); if (col != -1) { txt = m_file->m_model->item(row, col)->text(); QString accountId = MyMoneyFile::instance()->checkCategory(txt, s1.m_amount, s2.m_amount); if (!accountId.isEmpty()) { s2.m_accountId = accountId; s2.m_strCategoryName = txt; tr.m_listSplits.append(s2); } } // calculate hash txt.clear(); for (int i = 0; i < m_file->m_columnCount; ++i) txt.append(m_file->m_model->item(row, i)->text()); QString hashBase = QString::fromLatin1("%1-%2") .arg(tr.m_datePosted.toString(Qt::ISODate)) .arg(MyMoneyTransaction::hash(txt)); QString hash; for (uchar idx = 0; idx < 0xFF; ++idx) { // assuming threre will be no more than 256 transactions with the same hashBase hash = QString::fromLatin1("%1-%2").arg(hashBase).arg(idx); QSet::const_iterator it = m_hashSet.constFind(hash); if (it == m_hashSet.constEnd()) break; } m_hashSet.insert(hash); tr.m_strBankID = hash; st.m_listTransactions.append(tr); // Add the MyMoneyStatement::Transaction to the statement return true; } bool CSVImporterCore::processInvestRow(MyMoneyStatement &st, const InvestmentProfile *profile, const int row) { MyMoneyStatement::Transaction tr; QString memo; QString txt; // process date field int col = profile->m_colTypeNum.value(Column::Date, -1); tr.m_datePosted = processDateField(row, col); if (tr.m_datePosted == QDate()) return false; // process quantity field col = profile->m_colTypeNum.value(Column::Quantity, -1); tr.m_shares = processQuantityField(profile, row, col); // process price field col = profile->m_colTypeNum.value(Column::Price, -1); tr.m_price = processPriceField(profile, row, col); // process amount field col = profile->m_colTypeNum.value(Column::Amount, -1); tr.m_amount = processAmountField(profile, row, col); // process type field col = profile->m_colTypeNum.value(Column::Type, -1); tr.m_eAction = processActionTypeField(profile, row, col); if (!m_isActionTypeValidated && col != -1 && // if action type wasn't validated in wizard then... validateActionType(tr) != ValidActionType) // ...check if price, amount, quantity is appropriate return false; // process fee field col = profile->m_colTypeNum.value(Column::Fee, -1); if (col != -1) { if (profile->m_decimalSymbol == DecimalSymbol::Auto) { DecimalSymbol decimalSymbol = m_decimalSymbolIndexMap.value(col); m_file->m_parse->setDecimalSymbol(decimalSymbol); } txt = m_file->m_model->item(row, col)->text(); if (txt.startsWith(QLatin1Char('('))) // check if brackets notation is used for negative numbers txt.remove(QRegularExpression(QStringLiteral("[()]"))); if (txt.isEmpty()) tr.m_fees = MyMoneyMoney(); else { MyMoneyMoney fee(m_file->m_parse->possiblyReplaceSymbol(txt)); if (profile->m_feeIsPercentage && profile->m_feeRate.isEmpty()) // fee is percent fee *= tr.m_amount / MyMoneyMoney(100); // as percentage fee.abs(); tr.m_fees = fee; } } // process symbol and name field col = profile->m_colTypeNum.value(Column::Symbol, -1); if (col != -1) tr.m_strSymbol = m_file->m_model->item(row, col)->text(); col = profile->m_colTypeNum.value(Column::Name, -1); if (col != -1 && tr.m_strSymbol.isEmpty()) { // case in which symbol field is empty txt = m_file->m_model->item(row, col)->text(); tr.m_strSymbol = m_mapSymbolName.key(txt); // it's all about getting the right symbol } else if (!profile->m_securitySymbol.isEmpty()) tr.m_strSymbol = profile->m_securitySymbol; else if (tr.m_strSymbol.isEmpty()) return false; tr.m_strSecurity = m_mapSymbolName.value(tr.m_strSymbol); // take name from prepared names to avoid potential name mismatch // process memo field col = profile->m_colTypeNum.value(Column::Memo, -1); if (col != -1) memo.append(m_file->m_model->item(row, col)->text()); for (int i = 0; i < profile->m_memoColList.count(); ++i) { if (profile->m_memoColList.at(i) != col) { if (!memo.isEmpty()) memo.append(QLatin1Char('\n')); if (profile->m_memoColList.at(i) < m_file->m_columnCount) memo.append(m_file->m_model->item(row, profile->m_memoColList.at(i))->text()); } } tr.m_strMemo = memo; tr.m_strInterestCategory.clear(); // no special category tr.m_strBrokerageAccount.clear(); // no brokerage account auto-detection MyMoneyStatement::Split s1; s1.m_amount = tr.m_amount; s1.m_strMemo = tr.m_strMemo; MyMoneyStatement::Split s2 = s1; s2.m_amount = -s1.m_amount; s2.m_accountId = MyMoneyFile::instance()->checkCategory(tr.m_strInterestCategory, s1.m_amount, s2.m_amount); // deduct fees from amount if (tr.m_eAction == eMyMoney::Transaction::Action::CashDividend || tr.m_eAction == eMyMoney::Transaction::Action::Sell || tr.m_eAction == eMyMoney::Transaction::Action::Interest) tr.m_amount -= tr.m_fees; else if (tr.m_eAction == eMyMoney::Transaction::Action::Buy) { if (tr.m_amount.isPositive()) tr.m_amount = -tr.m_amount; //if broker doesn't use minus sings for buy transactions, set it manually here tr.m_amount -= tr.m_fees; } else if (tr.m_eAction == eMyMoney::Transaction::Action::None) tr.m_listSplits.append(s2); st.m_listTransactions.append(tr); // Add the MyMoneyStatement::Transaction to the statement return true; } bool CSVImporterCore::processPriceRow(MyMoneyStatement &st, const PricesProfile *profile, const int row) { MyMoneyStatement::Price pr; // process date field int col = profile->m_colTypeNum.value(Column::Date, -1); pr.m_date = processDateField(row, col); if (pr.m_date == QDate()) return false; // process price field col = profile->m_colTypeNum.value(Column::Price, -1); pr.m_amount = processPriceField(profile, row, col); switch (profile->type()) { case Profile::CurrencyPrices: if (profile->m_securitySymbol.isEmpty() || profile->m_currencySymbol.isEmpty()) return false; pr.m_strSecurity = profile->m_securitySymbol; pr.m_strCurrency = profile->m_currencySymbol; break; case Profile::StockPrices: if (profile->m_securityName.isEmpty()) return false; pr.m_strSecurity = profile->m_securityName; break; default: return false; } pr.m_sourceName = profile->m_profileName; st.m_listPrices.append(pr); // Add price to the statement return true; } QDate CSVImporterCore::processDateField(const int row, const int col) { QDate date; if (col != -1) { QString txt = m_file->m_model->item(row, col)->text(); date = m_convertDate->convertDate(txt); // Date column } return date; } MyMoneyMoney CSVImporterCore::processCreditDebit(QString &credit, QString &debit) { MyMoneyMoney amount; if (m_profile->m_decimalSymbol == DecimalSymbol::Auto) setupFieldDecimalSymbol(m_profile->m_colTypeNum.value(Column::Credit)); if (credit.startsWith(QLatin1Char('('))) { // check if brackets notation is used for negative numbers credit.remove(QRegularExpression(QStringLiteral("[()]"))); credit.prepend(QLatin1Char('-')); } if (debit.startsWith(QLatin1Char('('))) { // check if brackets notation is used for negative numbers debit.remove(QRegularExpression(QStringLiteral("[()]"))); debit.prepend(QLatin1Char('-')); } if (!credit.isEmpty() && !debit.isEmpty()) { // we do not expect both fields to be non-zero if (MyMoneyMoney(credit).isZero()) credit = QString(); if (MyMoneyMoney(debit).isZero()) debit = QString(); } if (!debit.startsWith(QLatin1Char('-')) && !debit.isEmpty()) // ensure debit field is negative debit.prepend(QLatin1Char('-')); if (!credit.isEmpty() && debit.isEmpty()) amount = MyMoneyMoney(m_file->m_parse->possiblyReplaceSymbol(credit)); else if (credit.isEmpty() && !debit.isEmpty()) amount = MyMoneyMoney(m_file->m_parse->possiblyReplaceSymbol(debit)); else if (!credit.isEmpty() && !debit.isEmpty()) { // both fields are non-empty and non-zero so let user decide return amount; } else amount = MyMoneyMoney(); // both fields are empty and zero so set amount to zero return amount; } MyMoneyMoney CSVImporterCore::processQuantityField(const CSVProfile *profile, const int row, const int col) { MyMoneyMoney shares; if (col != -1) { if (profile->m_decimalSymbol == DecimalSymbol::Auto) setupFieldDecimalSymbol(col); QString txt = m_file->m_model->item(row, col)->text(); txt.remove(QRegularExpression(QStringLiteral("-+"))); // remove unwanted sings in quantity if (!txt.isEmpty()) shares = MyMoneyMoney(m_file->m_parse->possiblyReplaceSymbol(txt)); } return shares; } MyMoneyMoney CSVImporterCore::processAmountField(const CSVProfile *profile, const int row, const int col) { MyMoneyMoney amount; if (col != -1) { if (profile->m_decimalSymbol == DecimalSymbol::Auto) setupFieldDecimalSymbol(col); QString txt = m_file->m_model->item(row, col)->text(); if (txt.startsWith(QLatin1Char('('))) { // check if brackets notation is used for negative numbers txt.remove(QRegularExpression(QStringLiteral("[()]"))); txt.prepend(QLatin1Char('-')); } if (!txt.isEmpty()) amount = MyMoneyMoney(m_file->m_parse->possiblyReplaceSymbol(txt)); } return amount; } MyMoneyMoney CSVImporterCore::processPriceField(const InvestmentProfile *profile, const int row, const int col) { MyMoneyMoney price; if (col != -1) { if (profile->m_decimalSymbol == DecimalSymbol::Auto) setupFieldDecimalSymbol(col); QString txt = m_file->m_model->item(row, col)->text(); if (!txt.isEmpty()) { price = MyMoneyMoney(m_file->m_parse->possiblyReplaceSymbol(txt)); price *= m_priceFractions.at(profile->m_priceFraction); } } return price; } MyMoneyMoney CSVImporterCore::processPriceField(const PricesProfile *profile, const int row, const int col) { MyMoneyMoney price; if (col != -1) { if (profile->m_decimalSymbol == DecimalSymbol::Auto) setupFieldDecimalSymbol(col); QString txt = m_file->m_model->item(row, col)->text(); if (!txt.isEmpty()) { price = MyMoneyMoney(m_file->m_parse->possiblyReplaceSymbol(txt)); price *= m_priceFractions.at(profile->m_priceFraction); } } return price; } QList CSVImporterCore::createValidActionTypes(MyMoneyStatement::Transaction &tr) { QList validActionTypes; if (tr.m_shares.isPositive() && tr.m_price.isPositive() && !tr.m_amount.isZero()) validActionTypes << eMyMoney::Transaction::Action::ReinvestDividend << eMyMoney::Transaction::Action::Buy << eMyMoney::Transaction::Action::Sell; else if (tr.m_shares.isZero() && tr.m_price.isZero() && !tr.m_amount.isZero()) validActionTypes << eMyMoney::Transaction::Action::CashDividend << eMyMoney::Transaction::Action::Interest; else if (tr.m_shares.isPositive() && tr.m_price.isZero() && tr.m_amount.isZero()) validActionTypes << eMyMoney::Transaction::Action::Shrsin << eMyMoney::Transaction::Action::Shrsout; return validActionTypes; } bool CSVImporterCore::sortSecurities(QSet& onlySymbols, QSet& onlyNames, QMap& mapSymbolName) { QList securityList = MyMoneyFile::instance()->securityList(); int symbolCol = m_profile->m_colTypeNum.value(Column::Symbol, -1); int nameCol = m_profile->m_colTypeNum.value(Column::Name, -1); // sort by availability of symbol and name for (int row = m_profile->m_startLine; row <= m_profile->m_endLine; ++row) { QString symbol; QString name; if (symbolCol != -1) symbol = m_file->m_model->item(row, symbolCol)->text().trimmed(); if (nameCol != -1) name = m_file->m_model->item(row, nameCol)->text().trimmed(); if (!symbol.isEmpty() && !name.isEmpty()) mapSymbolName.insert(symbol, name); else if (!symbol.isEmpty()) onlySymbols.insert(symbol); else if (!name.isEmpty()) onlyNames.insert(name); else return false; } // try to find names for symbols for (QSet::iterator symbol = onlySymbols.begin(); symbol != onlySymbols.end();) { QList filteredSecurities; foreach (const auto sec, securityList) { if ((*symbol).compare(sec.tradingSymbol(), Qt::CaseInsensitive) == 0) filteredSecurities.append(sec); // gather all securities that by matched by symbol } if (filteredSecurities.count() == 1) { // single security matched by the symbol so... mapSymbolName.insert(*symbol, filteredSecurities.first().name()); symbol = onlySymbols.erase(symbol); // ...it's no longer unknown } else if (!filteredSecurities.isEmpty()) { // multiple securities matched by the symbol // TODO: Ask user which security should we match to mapSymbolName.insert(*symbol, filteredSecurities.first().name()); symbol = onlySymbols.erase(symbol); } else // no security matched, so leave it as unknown ++symbol; } // try to find symbols for names for (QSet::iterator name = onlyNames.begin(); name != onlyNames.end();) { QList filteredSecurities; foreach (const auto sec, securityList) { if ((*name).compare(sec.name(), Qt::CaseInsensitive) == 0) filteredSecurities.append(sec); // gather all securities that by matched by name } if (filteredSecurities.count() == 1) { // single security matched by the name so... mapSymbolName.insert(filteredSecurities.first().tradingSymbol(), *name); name = onlyNames.erase(name); // ...it's no longer unknown } else if (!filteredSecurities.isEmpty()) { // multiple securities matched by the name // TODO: Ask user which security should we match to mapSymbolName.insert(filteredSecurities.first().tradingSymbol(), *name); name = onlySymbols.erase(name); } else // no security matched, so leave it as unknown ++name; } return true; } void CSVImporterCore::setupFieldDecimalSymbol(int col) { m_file->m_parse->setDecimalSymbol(m_decimalSymbolIndexMap.value(col)); } QList CSVImporterCore::getNumericalColumns() { QList columns; switch(m_profile->type()) { case Profile::Banking: if (m_profile->m_colTypeNum.value(Column::Amount, -1) != -1) { columns << m_profile->m_colTypeNum.value(Column::Amount); } else { columns << m_profile->m_colTypeNum.value(Column::Debit); columns << m_profile->m_colTypeNum.value(Column::Credit); } break; case Profile::Investment: columns << m_profile->m_colTypeNum.value(Column::Amount); columns << m_profile->m_colTypeNum.value(Column::Price); columns << m_profile->m_colTypeNum.value(Column::Quantity); if (m_profile->m_colTypeNum.value(Column::Fee, -1) != -1) columns << m_profile->m_colTypeNum.value(Column::Fee); break; case Profile::CurrencyPrices: case Profile::StockPrices: columns << m_profile->m_colTypeNum.value(Column::Price); break; default: break; } return columns; } bool CSVImporterCore::createStatement(MyMoneyStatement &st) { switch (m_profile->type()) { case Profile::Banking: { if (!st.m_listTransactions.isEmpty()) // don't create statement if there is one return true; st.m_eType = eMyMoney::Statement::Type::None; if (m_autodetect.value(AutoAccountBank)) detectAccount(st); m_hashSet.clear(); BankingProfile *profile = dynamic_cast(m_profile); for (int row = m_profile->m_startLine; row <= m_profile->m_endLine; ++row) if (!processBankRow(st, profile, row)) { // parse fields st = MyMoneyStatement(); return false; } return true; break; } case Profile::Investment: { if (!st.m_listTransactions.isEmpty()) // don't create statement if there is one return true; st.m_eType = eMyMoney::Statement::Type::Investment; if (m_autodetect.value(AutoAccountInvest)) detectAccount(st); auto profile = dynamic_cast(m_profile); if ((m_profile->m_colTypeNum.value(Column::Fee, -1) == -1 || m_profile->m_colTypeNum.value(Column::Fee, -1) >= m_file->m_columnCount) && profile && !profile->m_feeRate.isEmpty()) // fee column has not been calculated so do it now calculateFee(); - for (int row = m_profile->m_startLine; row <= m_profile->m_endLine; ++row) - if (!processInvestRow(st, profile, row)) { // parse fields - st = MyMoneyStatement(); - return false; - } + if (profile) { + for (int row = m_profile->m_startLine; row <= m_profile->m_endLine; ++row) + if (!processInvestRow(st, profile, row)) { // parse fields + st = MyMoneyStatement(); + return false; + } + } for (QMap::const_iterator it = m_mapSymbolName.cbegin(); it != m_mapSymbolName.cend(); ++it) { MyMoneyStatement::Security security; security.m_strSymbol = it.key(); security.m_strName = it.value(); st.m_listSecurities.append(security); } return true; break; } default: case Profile::CurrencyPrices: case Profile::StockPrices: { if (!st.m_listPrices.isEmpty()) // don't create statement if there is one return true; st.m_eType = eMyMoney::Statement::Type::None; - auto*profile = dynamic_cast(m_profile); - for (int row = m_profile->m_startLine; row <= m_profile->m_endLine; ++row) - if (profile && !processPriceRow(st, profile, row)) { // parse fields - st = MyMoneyStatement(); - return false; - } + if (auto profile = dynamic_cast(m_profile)) { + for (int row = m_profile->m_startLine; row <= m_profile->m_endLine; ++row) + if (!processPriceRow(st, profile, row)) { // parse fields + st = MyMoneyStatement(); + return false; + } + } for (QMap::const_iterator it = m_mapSymbolName.cbegin(); it != m_mapSymbolName.cend(); ++it) { MyMoneyStatement::Security security; security.m_strSymbol = it.key(); security.m_strName = it.value(); st.m_listSecurities.append(security); } return true; } } return true; } void CSVProfile::readSettings(const KConfigGroup &profilesGroup) { m_lastUsedDirectory = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfDirectory), QString()); m_startLine = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfStartLine), 0); m_trailerLines = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfTrailerLines), 0); m_encodingMIBEnum = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfEncoding), 106 /* UTF-8 */); m_dateFormat = static_cast(profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfDateFormat), (int)DateFormat::YearMonthDay)); m_textDelimiter = static_cast(profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfTextDelimiter), (int)TextDelimiter::DoubleQuote)); m_fieldDelimiter = static_cast(profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfFieldDelimiter), (int)FieldDelimiter::Auto)); m_decimalSymbol = static_cast(profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfDecimalSymbol), (int)DecimalSymbol::Auto)); initColNumType(); } void CSVProfile::writeSettings(KConfigGroup &profilesGroup) { QFileInfo fileInfo (m_lastUsedDirectory); if (fileInfo.isFile()) m_lastUsedDirectory = fileInfo.absolutePath(); if (m_lastUsedDirectory.startsWith(QDir::homePath())) // replace /home/user with ~/ for brevity m_lastUsedDirectory.replace(0, QDir::homePath().length(), QLatin1Char('~')); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfDirectory), m_lastUsedDirectory); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfEncoding), m_encodingMIBEnum); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfDateFormat), (int)m_dateFormat); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfFieldDelimiter), (int)m_fieldDelimiter); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfTextDelimiter), (int)m_textDelimiter); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfDecimalSymbol), (int)m_decimalSymbol); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfStartLine), m_startLine); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfTrailerLines), m_trailerLines); } bool BankingProfile::readSettings(const KSharedConfigPtr &config) { bool exists = true; KConfigGroup profilesGroup(config, CSVImporterCore::m_profileConfPrefix.value(type()) + QLatin1Char('-') + m_profileName); if (!profilesGroup.exists()) exists = false; m_colTypeNum[Column::Payee] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Payee), -1); m_colTypeNum[Column::Number] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Number), -1); m_colTypeNum[Column::Amount] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Amount), -1); m_colTypeNum[Column::Debit] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Debit), -1); m_colTypeNum[Column::Credit] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Credit), -1); m_colTypeNum[Column::Date] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Date), -1); m_colTypeNum[Column::Category] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Category), -1); m_colTypeNum[Column::Memo] = -1; // initialize, otherwise random data may go here m_oppositeSigns = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfOppositeSigns), 0); m_memoColList = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Memo), QList()); CSVProfile::readSettings(profilesGroup); return exists; } void BankingProfile::writeSettings(const KSharedConfigPtr &config) { KConfigGroup profilesGroup(config, CSVImporterCore::m_profileConfPrefix.value(type()) + QLatin1Char('-') + m_profileName); CSVProfile::writeSettings(profilesGroup); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfOppositeSigns), m_oppositeSigns); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Payee), m_colTypeNum.value(Column::Payee)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Number), m_colTypeNum.value(Column::Number)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Amount), m_colTypeNum.value(Column::Amount)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Debit), m_colTypeNum.value(Column::Debit)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Credit), m_colTypeNum.value(Column::Credit)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Date), m_colTypeNum.value(Column::Date)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Category), m_colTypeNum.value(Column::Category)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Memo), m_memoColList); profilesGroup.config()->sync(); } bool InvestmentProfile::readSettings(const KSharedConfigPtr &config) { bool exists = true; KConfigGroup profilesGroup(config, CSVImporterCore::m_profileConfPrefix.value(type()) + QLatin1Char('-') + m_profileName); if (!profilesGroup.exists()) exists = false; m_transactionNames[eMyMoney::Transaction::Action::Buy] = profilesGroup.readEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::Buy), QString(i18nc("Type of operation as in financial statement", "buy")).split(',', QString::SkipEmptyParts)); m_transactionNames[eMyMoney::Transaction::Action::Sell] = profilesGroup.readEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::Sell), QString(i18nc("Type of operation as in financial statement", "sell,repurchase")).split(',', QString::SkipEmptyParts)); m_transactionNames[eMyMoney::Transaction::Action::ReinvestDividend] = profilesGroup.readEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::ReinvestDividend), QString(i18nc("Type of operation as in financial statement", "reinvest,reinv,re-inv")).split(',', QString::SkipEmptyParts)); m_transactionNames[eMyMoney::Transaction::Action::CashDividend] = profilesGroup.readEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::CashDividend), QString(i18nc("Type of operation as in financial statement", "dividend")).split(',', QString::SkipEmptyParts)); m_transactionNames[eMyMoney::Transaction::Action::Interest] = profilesGroup.readEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::Interest), QString(i18nc("Type of operation as in financial statement", "interest,income")).split(',', QString::SkipEmptyParts)); m_transactionNames[eMyMoney::Transaction::Action::Shrsin] = profilesGroup.readEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::Shrsin), QString(i18nc("Type of operation as in financial statement", "add,stock dividend,divd reinv,transfer in,re-registration in,journal entry")).split(',', QString::SkipEmptyParts)); m_transactionNames[eMyMoney::Transaction::Action::Shrsout] = profilesGroup.readEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::Shrsout), QString(i18nc("Type of operation as in financial statement", "remove")).split(',', QString::SkipEmptyParts)); m_colTypeNum[Column::Date] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Date), -1); m_colTypeNum[Column::Type] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Type), -1); //use for type col. m_colTypeNum[Column::Price] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Price), -1); m_colTypeNum[Column::Quantity] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Quantity), -1); m_colTypeNum[Column::Amount] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Amount), -1); m_colTypeNum[Column::Name] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Name), -1); m_colTypeNum[Column::Fee] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Fee), -1); m_colTypeNum[Column::Symbol] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Symbol), -1); m_colTypeNum[Column::Memo] = -1; // initialize, otherwise random data may go here m_feeIsPercentage = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfFeeIsPercentage), false); m_feeRate = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfFeeRate), QString()); m_minFee = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfMinFee), QString()); m_memoColList = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Memo), QList()); m_securityName = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfSecurityName), QString()); m_securitySymbol = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfSecuritySymbol), QString()); m_dontAsk = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfDontAsk), 0); m_priceFraction = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfPriceFraction), 2); CSVProfile::readSettings(profilesGroup); return exists; } void InvestmentProfile::writeSettings(const KSharedConfigPtr &config) { KConfigGroup profilesGroup(config, CSVImporterCore::m_profileConfPrefix.value(type()) + QLatin1Char('-') + m_profileName); CSVProfile::writeSettings(profilesGroup); profilesGroup.writeEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::Buy), m_transactionNames.value(eMyMoney::Transaction::Action::Buy)); profilesGroup.writeEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::Sell), m_transactionNames.value(eMyMoney::Transaction::Action::Sell)); profilesGroup.writeEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::ReinvestDividend), m_transactionNames.value(eMyMoney::Transaction::Action::ReinvestDividend)); profilesGroup.writeEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::CashDividend), m_transactionNames.value(eMyMoney::Transaction::Action::CashDividend)); profilesGroup.writeEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::Interest), m_transactionNames.value(eMyMoney::Transaction::Action::Interest)); profilesGroup.writeEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::Shrsin), m_transactionNames.value(eMyMoney::Transaction::Action::Shrsin)); profilesGroup.writeEntry(CSVImporterCore::m_transactionConfName.value(eMyMoney::Transaction::Action::Shrsout), m_transactionNames.value(eMyMoney::Transaction::Action::Shrsout)); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfPriceFraction), m_priceFraction); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfFeeIsPercentage), m_feeIsPercentage); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfFeeRate), m_feeRate); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfMinFee), m_minFee); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfSecurityName), m_securityName); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfSecuritySymbol), m_securitySymbol); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfDontAsk), m_dontAsk); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Date), m_colTypeNum.value(Column::Date)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Type), m_colTypeNum.value(Column::Type)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Quantity), m_colTypeNum.value(Column::Quantity)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Amount), m_colTypeNum.value(Column::Amount)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Price), m_colTypeNum.value(Column::Price)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Symbol), m_colTypeNum.value(Column::Symbol)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Name), m_colTypeNum.value(Column::Name)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Fee), m_colTypeNum.value(Column::Fee)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Memo), m_memoColList); profilesGroup.config()->sync(); } bool PricesProfile::readSettings(const KSharedConfigPtr &config) { bool exists = true; KConfigGroup profilesGroup(config, CSVImporterCore::m_profileConfPrefix.value(type()) + QLatin1Char('-') + m_profileName); if (!profilesGroup.exists()) exists = false; m_colTypeNum[Column::Date] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Date), -1); m_colTypeNum[Column::Price] = profilesGroup.readEntry(CSVImporterCore::m_colTypeConfName.value(Column::Price), -1); m_priceFraction = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfPriceFraction), 2); m_securityName = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfSecurityName), QString()); m_securitySymbol = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfSecuritySymbol), QString()); m_currencySymbol = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfCurrencySymbol), QString()); m_dontAsk = profilesGroup.readEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfDontAsk), 0); CSVProfile::readSettings(profilesGroup); return exists; } void PricesProfile::writeSettings(const KSharedConfigPtr &config) { KConfigGroup profilesGroup(config, CSVImporterCore::m_profileConfPrefix.value(type()) + QLatin1Char('-') + m_profileName); CSVProfile::writeSettings(profilesGroup); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Date), m_colTypeNum.value(Column::Date)); profilesGroup.writeEntry(CSVImporterCore::m_colTypeConfName.value(Column::Price), m_colTypeNum.value(Column::Price)); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfPriceFraction), m_priceFraction); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfSecurityName), m_securityName); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfSecuritySymbol), m_securitySymbol); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfCurrencySymbol), m_currencySymbol); profilesGroup.writeEntry(CSVImporterCore::m_miscSettingsConfName.value(ConfDontAsk), m_dontAsk); profilesGroup.config()->sync(); } CSVFile::CSVFile() : m_columnCount(0), m_rowCount(0) { m_parse = new Parse; m_model = new QStandardItemModel; } CSVFile::~CSVFile() { delete m_parse; delete m_model; } void CSVFile::getStartEndRow(CSVProfile *profile) { profile->m_endLine = m_rowCount - 1; if (profile->m_endLine > profile->m_trailerLines) profile->m_endLine -= profile->m_trailerLines; if (profile->m_startLine > profile->m_endLine) // Don't allow m_startLine > m_endLine profile->m_startLine = profile->m_endLine; } void CSVFile::getColumnCount(CSVProfile *profile, const QStringList &rows) { if (rows.isEmpty()) return; QVector delimiterIndexes; if (profile->m_fieldDelimiter == FieldDelimiter::Auto) delimiterIndexes = QVector{FieldDelimiter::Comma, FieldDelimiter::Semicolon, FieldDelimiter::Colon, FieldDelimiter::Tab}; // include all delimiters to test or ... else delimiterIndexes = QVector{profile->m_fieldDelimiter}; // ... only the one specified QList totalDelimiterCount({0, 0, 0, 0}); // Total in file for each delimiter QList thisDelimiterCount({0, 0, 0, 0}); // Total in this line for each delimiter int colCount = 0; // Total delimiters in this line FieldDelimiter possibleDelimiter = FieldDelimiter::Comma; m_columnCount = 0; foreach (const auto row, rows) { foreach(const auto delimiterIndex, delimiterIndexes) { m_parse->setFieldDelimiter(delimiterIndex); colCount = m_parse->parseLine(row).count(); // parse each line using each delimiter if (colCount > thisDelimiterCount.at((int)delimiterIndex)) thisDelimiterCount[(int)delimiterIndex] = colCount; if (thisDelimiterCount[(int)delimiterIndex] > m_columnCount) m_columnCount = thisDelimiterCount.at((int)delimiterIndex); totalDelimiterCount[(int)delimiterIndex] += colCount; if (totalDelimiterCount.at((int)delimiterIndex) > totalDelimiterCount.at((int)possibleDelimiter)) possibleDelimiter = delimiterIndex; } } if (delimiterIndexes.count() != 1) // if purpose was to autodetect... profile->m_fieldDelimiter = possibleDelimiter; // ... then change field delimiter m_parse->setFieldDelimiter(profile->m_fieldDelimiter); // restore original field delimiter } bool CSVFile::getInFileName(QString inFileName) { QFileInfo fileInfo; if (!inFileName.isEmpty()) { if (inFileName.startsWith(QLatin1Char('~'))) inFileName.replace(0, 1, QDir::homePath()); fileInfo = QFileInfo(inFileName); if (fileInfo.isFile()) { // if it is file... if (fileInfo.exists()) { // ...and exists... m_inFileName = inFileName; // ...then set as valid filename return true; // ...and return success... } else { // ...but if not... fileInfo.setFile(fileInfo.absolutePath()); //...then set start directory to directory of that file... if (!fileInfo.exists()) //...and if it doesn't exist too... fileInfo.setFile(QDir::homePath()); //...then set start directory to home path } } else if (fileInfo.isDir()) { if (fileInfo.exists()) fileInfo = QFileInfo(inFileName); else fileInfo.setFile(QDir::homePath()); } } else fileInfo = QFileInfo(QDir::homePath()); QPointer dialog = new QFileDialog(nullptr, QString(), fileInfo.absoluteFilePath(), i18n("CSV Files (*.csv)")); dialog->setFileMode(QFileDialog::ExistingFile); QUrl url; if (dialog->exec() == QDialog::Accepted) url = dialog->selectedUrls().first(); delete dialog; if (url.isEmpty()) { m_inFileName.clear(); return false; } else m_inFileName = url.toDisplayString(QUrl::PreferLocalFile); return true; } void CSVFile::setupParser(CSVProfile *profile) { if (profile->m_decimalSymbol != DecimalSymbol::Auto) m_parse->setDecimalSymbol(profile->m_decimalSymbol); m_parse->setFieldDelimiter(profile->m_fieldDelimiter); m_parse->setTextDelimiter(profile->m_textDelimiter); } void CSVFile::readFile(CSVProfile *profile) { QFile inFile(m_inFileName); if (!inFile.exists()) return; inFile.open(QIODevice::ReadOnly); QTextStream inStream(&inFile); QTextCodec* codec = QTextCodec::codecForMib(profile->m_encodingMIBEnum); inStream.setCodec(codec); QString buf = inStream.readAll(); inFile.close(); m_parse->setTextDelimiter(profile->m_textDelimiter); QStringList rows = m_parse->parseFile(buf); // parse the buffer m_rowCount = m_parse->lastLine(); // won't work without above line getColumnCount(profile, rows); getStartEndRow(profile); // prepare model from rows having rowCount and columnCount m_model->clear(); for (int i = 0; i < m_rowCount; ++i) { QList itemList; QStringList columns = m_parse->parseLine(rows.takeFirst()); // take instead of read from rows to preserve memory for (int j = 0; j < m_columnCount; ++j) itemList.append(new QStandardItem(columns.value(j, QString()))); m_model->appendRow(itemList); } } diff --git a/kmymoney/plugins/csv/import/core/csvimportercore.h b/kmymoney/plugins/csv/import/core/csvimportercore.h index e8035eca9..8a746ac86 100644 --- a/kmymoney/plugins/csv/import/core/csvimportercore.h +++ b/kmymoney/plugins/csv/import/core/csvimportercore.h @@ -1,396 +1,403 @@ /*************************************************************************** csvimportercore.h ------------------- begin : Sun May 21 2017 copyright : (C) 2015 by Allan Anderson email : agander93@gmail.com copyright : (C) 2017 by Łukasz Wojniłowicz email : lukasz.wojnilowicz@gmail.com ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #ifndef CSVIMPORTERCORE_H #define CSVIMPORTERCORE_H // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // QT Includes #include // Project Includes #include "mymoneystatement.h" #include "csvenums.h" #include "csv/import/core/kmm_csvimportercore_export.h" class MyMoneyAccount; class KConfigGroup; class QStandardItemModel; class Parse; class ConvertDate; namespace eMyMoney { namespace Account { enum class Type; } } enum autodetectTypeE { AutoFieldDelimiter, AutoDecimalSymbol, AutoDateFormat, AutoAccountInvest, AutoAccountBank }; enum miscSettingsE { ConfDirectory, ConfEncoding, ConfDateFormat, ConfFieldDelimiter, ConfTextDelimiter, ConfDecimalSymbol, ConfStartLine, ConfTrailerLines, ConfOppositeSigns, ConfFeeIsPercentage, ConfFeeRate, ConfMinFee, ConfSecurityName, ConfSecuritySymbol, ConfCurrencySymbol, ConfPriceFraction, ConfDontAsk, ConfHeight, ConfWidth }; enum validationResultE { ValidActionType, InvalidActionValues, NoActionType }; class KMM_CSVIMPORTERCORE_NO_EXPORT CSVProfile { protected: CSVProfile() : m_encodingMIBEnum(0), m_startLine(0), m_endLine(0), m_trailerLines(0), m_dateFormat(DateFormat::DayMonthYear), m_fieldDelimiter(FieldDelimiter::Auto), m_textDelimiter(TextDelimiter::DoubleQuote), m_decimalSymbol(DecimalSymbol::Auto) { } CSVProfile(const QString &profileName, int encodingMIBEnum, int startLine, int trailerLines, DateFormat dateFormat, FieldDelimiter fieldDelimiter, TextDelimiter textDelimiter, DecimalSymbol decimalSymbol, QMap &colTypeNum) : m_profileName(profileName), m_encodingMIBEnum(encodingMIBEnum), m_startLine(startLine), m_endLine(startLine), m_trailerLines(trailerLines), m_dateFormat(dateFormat), m_fieldDelimiter(fieldDelimiter), m_textDelimiter(textDelimiter), m_decimalSymbol(decimalSymbol), m_colTypeNum(colTypeNum) { initColNumType(); } void readSettings(const KConfigGroup &profilesGroup); void writeSettings(KConfigGroup &profilesGroup); void initColNumType() { for (auto it = m_colTypeNum.constBegin(); it != m_colTypeNum.constEnd(); ++it) m_colNumType.insert(it.value(), it.key()); } public: virtual ~CSVProfile() {} virtual Profile type() const = 0; virtual bool readSettings(const KSharedConfigPtr &config) = 0; virtual void writeSettings(const KSharedConfigPtr &config) = 0; QString m_profileName; QString m_lastUsedDirectory; int m_encodingMIBEnum; int m_startLine; int m_endLine; int m_trailerLines; DateFormat m_dateFormat; FieldDelimiter m_fieldDelimiter; TextDelimiter m_textDelimiter; DecimalSymbol m_decimalSymbol; QMap m_colTypeNum; QMap m_colNumType; }; class KMM_CSVIMPORTERCORE_EXPORT BankingProfile : public CSVProfile { public: explicit BankingProfile() : CSVProfile(), m_oppositeSigns(false) {} BankingProfile(QString profileName, int encodingMIBEnum, int startLine, int trailerLines, DateFormat dateFormat, FieldDelimiter fieldDelimiter, TextDelimiter textDelimiter, DecimalSymbol decimalSymbol, QMap colTypeNum, bool oppositeSigns) : CSVProfile(profileName, encodingMIBEnum, startLine, trailerLines, dateFormat, fieldDelimiter, textDelimiter, decimalSymbol, colTypeNum), m_oppositeSigns(oppositeSigns) {} Profile type() const { return Profile::Banking; } bool readSettings(const KSharedConfigPtr &config); void writeSettings(const KSharedConfigPtr &config); QList m_memoColList; bool m_oppositeSigns; }; class KMM_CSVIMPORTERCORE_EXPORT InvestmentProfile : public CSVProfile { public: explicit InvestmentProfile() : CSVProfile(), m_priceFraction(2), m_dontAsk(0), m_feeIsPercentage(false) { } InvestmentProfile(QString profileName, int encodingMIBEnum, int startLine, int trailerLines, DateFormat dateFormat, FieldDelimiter fieldDelimiter, TextDelimiter textDelimiter, DecimalSymbol decimalSymbol, QMap colTypeNum, int priceFraction, QMap transactionNames) : CSVProfile(profileName, encodingMIBEnum, startLine, trailerLines, dateFormat, fieldDelimiter, textDelimiter, decimalSymbol, colTypeNum), m_transactionNames(transactionNames), m_priceFraction(priceFraction), m_dontAsk(0), m_feeIsPercentage(false) { } Profile type() const { return Profile::Investment; } bool readSettings(const KSharedConfigPtr &config); void writeSettings(const KSharedConfigPtr &config); QMap m_transactionNames; QString m_feeRate; QString m_minFee; QString m_securityName; QString m_securitySymbol; QList m_memoColList; int m_priceFraction; int m_dontAsk; bool m_feeIsPercentage; }; class KMM_CSVIMPORTERCORE_EXPORT PricesProfile : public CSVProfile { public: - explicit PricesProfile() : CSVProfile() {} + explicit PricesProfile() : + CSVProfile(), + m_dontAsk(0), + m_priceFraction(2), + m_profileType(Profile::CurrencyPrices) + { + } + explicit PricesProfile(const Profile profileType) : CSVProfile(), m_dontAsk(0), m_priceFraction(2), m_profileType(profileType) { } PricesProfile(QString profileName, int encodingMIBEnum, int startLine, int trailerLines, DateFormat dateFormat, FieldDelimiter fieldDelimiter, TextDelimiter textDelimiter, DecimalSymbol decimalSymbol, QMap colTypeNum, int priceFraction, Profile profileType) : CSVProfile(profileName, encodingMIBEnum, startLine, trailerLines, dateFormat, fieldDelimiter, textDelimiter, decimalSymbol, colTypeNum), m_dontAsk(0), m_priceFraction(priceFraction), m_profileType(profileType) { } Profile type() const { return m_profileType; } bool readSettings(const KSharedConfigPtr &config); void writeSettings(const KSharedConfigPtr &config); QString m_securityName; QString m_securitySymbol; QString m_currencySymbol; int m_dontAsk; int m_priceFraction; Profile m_profileType; }; class KMM_CSVIMPORTERCORE_EXPORT CSVFile { public: explicit CSVFile(); ~CSVFile(); void getStartEndRow(CSVProfile *profile); /** * If delimiter = -1 this method tries different field * delimiters to get the one with which file has the most columns. * Otherwise it gets only column count for specified delimiter. */ void getColumnCount(CSVProfile *profile, const QStringList &rows); /** * This method gets the filename of * the financial statement. */ bool getInFileName(QString startDir = QString()); void setupParser(CSVProfile *profile); /** * This method gets file into buffer * It will laso store file's end column and row. */ void readFile(CSVProfile *profile); Parse *m_parse; QStandardItemModel *m_model; QString m_inFileName; int m_columnCount; int m_rowCount; }; class KMM_CSVIMPORTERCORE_EXPORT CSVImporterCore { public: explicit CSVImporterCore(); ~CSVImporterCore(); /** * This method will silently import csv file. Main purpose of this method are online quotes. */ MyMoneyStatement unattendedImport(const QString &filename, CSVProfile *profile); static KSharedConfigPtr configFile(); void profileFactory(const Profile type, const QString &name); void readMiscSettings(); /** * This method ensures that configuration file contains all necessary fields * and that it is up to date. */ void validateConfigFile(); /** * This method contains routines to update configuration file * from kmmVer to latest. */ bool updateConfigFile(QList &confVer); /** * This method will update [ProfileNames] in csvimporterrrc */ static bool profilesAction(const Profile type, const ProfileAction action, const QString &name, const QString &newname); /** * This methods will ensure that fields of input rows are correct. */ bool validateDateFormat(const int col); bool validateDecimalSymbols(const QList &columns); bool validateCurrencies(const PricesProfile *profile); bool validateSecurity(const PricesProfile *profile); bool validateSecurity(const InvestmentProfile *profile); bool validateSecurities(); validationResultE validateActionType(MyMoneyStatement::Transaction &tr); /** * This method will try to detect decimal symbol in input column. */ int detectDecimalSymbols(const QList &columns); DecimalSymbol detectDecimalSymbol(const int col, const QString &exclude); /** * This method will try to detect account from csv header. */ QList findAccounts(const QList &accountTypes, const QString &statementHeader); bool detectAccount(MyMoneyStatement &st); /** * This methods will evaluate input row and append it to a statement. */ bool processBankRow(MyMoneyStatement &st, const BankingProfile *profile, const int row); bool processInvestRow(MyMoneyStatement &st, const InvestmentProfile *profile, const int row); bool processPriceRow(MyMoneyStatement &st, const PricesProfile *profile, const int row); /** * This methods will evaluate fields of input row and return statement's useful value. */ QDate processDateField(const int row, const int col); MyMoneyMoney processCreditDebit(QString &credit, QString &debit ); MyMoneyMoney processPriceField(const InvestmentProfile *profile, const int row, const int col); MyMoneyMoney processPriceField(const PricesProfile *profile, const int row, const int col); MyMoneyMoney processAmountField(const CSVProfile *profile, const int row, const int col); MyMoneyMoney processQuantityField(const CSVProfile *profile, const int row, const int col); eMyMoney::Transaction::Action processActionTypeField(const InvestmentProfile *profile, const int row, const int col); /** * This method creates valid set of possible transactions * according to quantity, amount and price */ QList createValidActionTypes(MyMoneyStatement::Transaction &tr); /** * This method will add fee column to model based on amount and fee rate. */ bool calculateFee(); /** * This method gets securities from investment statement and * tries to get pairs of symbol and name either * from KMM or from statement data. * In case it's not successful onlySymbols and onlyNames won't be empty. */ bool sortSecurities(QSet& onlySymbols, QSet& onlyNames, QMap& mapSymbolName); /** * Helper method to set decimal symbol in case it was set to autodetect. */ void setupFieldDecimalSymbol(int col); /** * Helper method to get all column numbers that were pointed as nummeric */ QList getNumericalColumns(); bool createStatement(MyMoneyStatement &st); ConvertDate *m_convertDate; CSVFile *m_file; CSVProfile *m_profile; KSharedConfigPtr m_config; bool m_isActionTypeValidated; QList m_priceFractions; QSet m_hashSet; QMap m_decimalSymbolIndexMap; QMap m_mapSymbolName; QMap m_autodetect; static const QHash m_colTypeConfName; static const QHash m_profileConfPrefix; static const QHash m_transactionConfName; static const QHash m_miscSettingsConfName; static const QString m_confProfileNames; static const QString m_confPriorName; static const QString m_confMiscName; }; #endif diff --git a/kmymoney/plugins/forecast/forecastview.cpp b/kmymoney/plugins/forecast/forecastview.cpp index 32b3c0006..900a13c7a 100644 --- a/kmymoney/plugins/forecast/forecastview.cpp +++ b/kmymoney/plugins/forecast/forecastview.cpp @@ -1,62 +1,63 @@ /*************************************************************************** forecastview.cpp ------------------- copyright : (C) 2018 by Łukasz Wojniłowicz email : lukasz.wojnilowicz@gmail.com ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "forecastview.h" // ---------------------------------------------------------------------------- // QT Includes // ---------------------------------------------------------------------------- // KDE Includes #include #include // ---------------------------------------------------------------------------- // Project Includes #include "viewinterface.h" #include "kforecastview.h" ForecastView::ForecastView(QObject *parent, const QVariantList &args) : - KMyMoneyPlugin::Plugin(parent, "forecastview"/*must be the same as X-KDE-PluginInfo-Name*/) + KMyMoneyPlugin::Plugin(parent, "forecastview"/*must be the same as X-KDE-PluginInfo-Name*/), + m_view(nullptr) { Q_UNUSED(args) setComponentName("forecastview", i18n("Forecast view")); // For information, announce that we have been loaded. qDebug("Plugins: forecastview loaded"); } ForecastView::~ForecastView() { qDebug("Plugins: forecastview unloaded"); } void ForecastView::plug() { m_view = new KForecastView; viewInterface()->addView(m_view, i18n("Forecast"), View::Forecast); } void ForecastView::unplug() { viewInterface()->removeView(View::Forecast); } K_PLUGIN_FACTORY_WITH_JSON(ForecastViewFactory, "forecastview.json", registerPlugin();) #include "forecastview.moc" diff --git a/kmymoney/plugins/gnc/import/mymoneygncreader.cpp b/kmymoney/plugins/gnc/import/mymoneygncreader.cpp index aae0afaf4..1ef37cbdd 100644 --- a/kmymoney/plugins/gnc/import/mymoneygncreader.cpp +++ b/kmymoney/plugins/gnc/import/mymoneygncreader.cpp @@ -1,2676 +1,2698 @@ /*************************************************************************** mymoneygncreader - description ------------------- begin : Wed Mar 3 2004 copyright : (C) 2000-2004 by Michael Edwardes email : mte@users.sourceforge.net Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio + (C) 2018 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 "mymoneygncreader.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include #include #include #include +#if QT_VERSION >= 0x051000 + #include +#endif // ---------------------------------------------------------------------------- // KDE Includes #ifndef _GNCFILEANON #include #include #endif #include // ---------------------------------------------------------------------------- // Third party Includes // ------------------------------------------------------------Box21---------------- // Project Includes #include #include "mymoneystoragemgr.h" #ifndef _GNCFILEANON #include "kmymoneyutils.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneyschedule.h" #include "mymoneyprice.h" #include "mymoneypayee.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneyexception.h" #include "kgncimportoptionsdlg.h" #include "kgncpricesourcedlg.h" #include "keditscheduledlg.h" #include "kmymoneyedit.h" #include "kmymoneymoneyvalidator.h" #define TRY try #define CATCH catch (const MyMoneyException &) #define PASS catch (const MyMoneyException &) { throw; } #else #include "mymoneymoney.h" #include // #define i18n QObject::tr #define TRY #define CATCH #define PASS #define MYMONEYEXCEPTION QString #define MyMoneyException QString #define PACKAGE "KMyMoney" #endif // _GNCFILEANON #include "mymoneyenums.h" using namespace eMyMoney; // init static variables double MyMoneyGncReader::m_fileHideFactor = 0.0; double GncObject::m_moneyHideFactor; // user options void MyMoneyGncReader::setOptions() { #ifndef _GNCFILEANON KGncImportOptionsDlg dlg; // display the dialog to allow the user to set own options if (dlg.exec()) { // set users input options m_dropSuspectSchedules = dlg.scheduleOption(); m_investmentOption = dlg.investmentOption(); m_useFinanceQuote = dlg.quoteOption(); m_useTxNotes = dlg.txNotesOption(); m_decoder = dlg.decodeOption(); gncdebug = dlg.generalDebugOption(); xmldebug = dlg.xmlDebugOption(); bAnonymize = dlg.anonymizeOption(); } else { // user declined, so set some sensible defaults m_dropSuspectSchedules = false; // investment option - 0, create investment a/c per stock a/c, 1 = single new investment account, 2 = prompt for each stock // option 2 doesn't really work too well at present m_investmentOption = 0; m_useFinanceQuote = false; m_useTxNotes = false; m_decoder = 0; gncdebug = false; // general debug messages xmldebug = false; // xml trace bAnonymize = false; // anonymize input } // no dialog option for the following; it will set base currency, and print actual XML data developerDebug = false; // set your fave currency here to save getting that enormous dialog each time you run a test // especially if you have to scroll down to USD... if (developerDebug) m_storage->setValue("kmm-baseCurrency", "GBP"); #endif // _GNCFILEANON } GncObject::GncObject() : pMain(0), m_subElementList(0), m_subElementListCount(0), m_dataElementList(0), m_dataElementListCount(0), m_dataPtr(0), m_state(0), m_anonClassList(0), m_anonClass(0) { } // Check that the current element is of a version we are coded for void GncObject::checkVersion(const QString& elName, const QXmlAttributes& elAttrs, const map_elementVersions& map) { TRY { if (map.contains(elName)) { // if it's not in the map, there's nothing to check if (!map[elName].contains(elAttrs.value("version"))) { QString em = Q_FUNC_INFO + i18n(": Sorry. This importer cannot handle version %1 of element %2" , elAttrs.value("version"), elName); throw MYMONEYEXCEPTION(em); } } return ; } PASS } // Check if this element is in the current object's sub element list GncObject *GncObject::isSubElement(const QString& elName, const QXmlAttributes& elAttrs) { TRY { uint i; GncObject *next = 0; for (i = 0; i < m_subElementListCount; i++) { if (elName == m_subElementList[i]) { m_state = i; next = startSubEl(); // go create the sub object if (next != 0) { next->initiate(elName, elAttrs); // initialize it next->m_elementName = elName; // save it's name so we can identify the end } break; } } return (next); } PASS } // Check if this element is in the current object's data element list bool GncObject::isDataElement(const QString &elName, const QXmlAttributes& elAttrs) { TRY { uint i; for (i = 0; i < m_dataElementListCount; i++) { if (elName == m_dataElementList[i]) { m_state = i; dataEl(elAttrs); // go set the pointer so the data can be stored return (true); } } m_dataPtr = 0; // we don't need this, so make sure we don't store extraneous data return (false); } PASS } // return the variable string, decoded if required QString GncObject::var(int i) const { /* This code was needed because the Qt3 XML reader apparently did not process the encoding parameter in the m_decoder == 0 ? m_v[i] : pMain->m_decoder->toUnicode(m_v[i].toUtf8())); } const QString GncObject::getKvpValue(const QString& key, const QString& type) const { QList::const_iterator it; // first check for exact match for (it = m_kvpList.begin(); it != m_kvpList.end(); ++it) { if (((*it).key() == key) && ((type.isEmpty()) || ((*it).type() == type))) return (*it).value(); } // then for partial match for (it = m_kvpList.begin(); it != m_kvpList.end(); ++it) { if (((*it).key().contains(key)) && ((type.isEmpty()) || ((*it).type() == type))) return (*it).value(); } return (QString()); } void GncObject::adjustHideFactor() { +#if QT_VERSION >= 0x051000 + m_moneyHideFactor = pMain->m_fileHideFactor * (1.0 + (int)(200.0 * QRandomGenerator::system()->generate() / (RAND_MAX + 1.0))) / 100.0; +#else m_moneyHideFactor = pMain->m_fileHideFactor * (1.0 + (int)(200.0 * rand() / (RAND_MAX + 1.0))) / 100.0; +#endif } // data anonymizer QString GncObject::hide(QString data, unsigned int anonClass) { TRY { if (!pMain->bAnonymize) return (data); // no anonymizing required // counters used to generate names for anonymizer static int nextAccount; static int nextEquity; static int nextPayee; static int nextSched; static QMap anonPayees; // to check for duplicate payee names static QMap anonStocks; // for reference to equities QString result(data); QMap::const_iterator it; MyMoneyMoney in, mresult; switch (anonClass) { case ASIS: // this is not personal data break; case SUPPRESS: // this is personal and is not essential result = ""; break; case NXTACC: // generate account name result = ki18n("Account%1").subs(++nextAccount, -6).toString(); break; case NXTEQU: // generate/return an equity name it = anonStocks.constFind(data); if (it == anonStocks.constEnd()) { result = ki18n("Stock%1").subs(++nextEquity, -6).toString(); anonStocks.insert(data, result); } else { result = (*it); } break; case NXTPAY: // generate/return a payee name it = anonPayees.constFind(data); if (it == anonPayees.constEnd()) { result = ki18n("Payee%1").subs(++nextPayee, -6).toString(); anonPayees.insert(data, result); } else { result = (*it); } break; case NXTSCHD: // generate a schedule name result = ki18n("Schedule%1").subs(++nextSched, -6).toString(); break; case MONEY1: in = MyMoneyMoney(data); if (data == "-1/0") in = MyMoneyMoney(); // spurious gnucash data - causes a crash sometimes mresult = MyMoneyMoney(m_moneyHideFactor) * in; mresult.convert(10000); result = mresult.toString(); break; case MONEY2: in = MyMoneyMoney(data); if (data == "-1/0") in = MyMoneyMoney(); mresult = MyMoneyMoney(m_moneyHideFactor) * in; mresult.convert(10000); mresult.setThousandSeparator(' '); result = mresult.formatMoney("", 2); break; } return (result); } PASS } // dump current object data values // only called if gncdebug set void GncObject::debugDump() { uint i; qDebug() << "Object" << m_elementName; for (i = 0; i < m_dataElementListCount; i++) { qDebug() << m_dataElementList[i] << "=" << m_v[i]; } } //***************************************************************** GncFile::GncFile() { static const QString subEls[] = {"gnc:book", "gnc:count-data", "gnc:commodity", "price", "gnc:account", "gnc:transaction", "gnc:template-transactions", "gnc:schedxaction" }; m_subElementList = subEls; m_subElementListCount = END_FILE_SELS; m_dataElementListCount = 0; m_processingTemplates = false; m_bookFound = false; } GncFile::~GncFile() {} GncObject *GncFile::startSubEl() { TRY { if (pMain->xmldebug) qDebug("File start subel m_state %d", m_state); GncObject *next = 0; switch (m_state) { case BOOK: if (m_bookFound) throw MYMONEYEXCEPTION(i18n("This version of the importer cannot handle multi-book files.")); m_bookFound = true; break; case COUNT: next = new GncCountData; break; case CMDTY: next = new GncCommodity; break; case PRICE: next = new GncPrice; break; case ACCT: // accounts within the template section are ignored if (!m_processingTemplates) next = new GncAccount; break; case TX: next = new GncTransaction(m_processingTemplates); break; case TEMPLATES: m_processingTemplates = true; break; case SCHEDULES: m_processingTemplates = false; next = new GncSchedule; break; default: throw MYMONEYEXCEPTION("GncFile rcvd invalid state"); } return (next); } PASS } void GncFile::endSubEl(GncObject *subObj) { if (pMain->xmldebug) qDebug("File end subel"); if (!m_processingTemplates) delete subObj; // template txs must be saved awaiting schedules m_dataPtr = 0; return ; } //****************************************** GncDate ********************************************* GncDate::GncDate() { m_subElementListCount = 0; static const QString dEls[] = {"ts:date", "gdate"}; m_dataElementList = dEls; m_dataElementListCount = END_Date_DELS; static const unsigned int anonClasses[] = {ASIS, ASIS}; m_anonClassList = anonClasses; for (uint i = 0; i < m_dataElementListCount; i++) m_v.append(QString()); } GncDate::~GncDate() {} //*************************************GncCmdtySpec*************************************** GncCmdtySpec::GncCmdtySpec() { m_subElementListCount = 0; static const QString dEls[] = {"cmdty:space", "cmdty:id"}; m_dataElementList = dEls; m_dataElementListCount = END_CmdtySpec_DELS; static const unsigned int anonClasses[] = {ASIS, ASIS}; m_anonClassList = anonClasses; for (uint i = 0; i < m_dataElementListCount; i++) m_v.append(QString()); } GncCmdtySpec::~GncCmdtySpec() {} QString GncCmdtySpec::hide(QString data, unsigned int) { // hide equity names, but not currency names unsigned int newClass = ASIS; switch (m_state) { case CMDTYID: if (!isCurrency()) newClass = NXTEQU; } return (GncObject::hide(data, newClass)); } //************* GncKvp******************************************** GncKvp::GncKvp() { m_subElementListCount = END_Kvp_SELS; static const QString subEls[] = {"slot"}; // kvp's may be nested m_subElementList = subEls; m_dataElementListCount = END_Kvp_DELS; static const QString dataEls[] = {"slot:key", "slot:value"}; m_dataElementList = dataEls; static const unsigned int anonClasses[] = {ASIS, ASIS}; m_anonClassList = anonClasses; for (uint i = 0; i < m_dataElementListCount; i++) m_v.append(QString()); } GncKvp::~GncKvp() {} void GncKvp::dataEl(const QXmlAttributes& elAttrs) { switch (m_state) { case VALUE: m_kvpType = elAttrs.value("type"); } m_dataPtr = &(m_v[m_state]); if (key().contains("formula")) { m_anonClass = MONEY2; } else { m_anonClass = ASIS; } return ; } GncObject *GncKvp::startSubEl() { if (pMain->xmldebug) qDebug("Kvp start subel m_state %d", m_state); TRY { GncObject *next = 0; switch (m_state) { case KVP: next = new GncKvp; break; default: throw MYMONEYEXCEPTION("GncKvp rcvd invalid m_state "); } return (next); } PASS } void GncKvp::endSubEl(GncObject *subObj) { if (pMain->xmldebug) qDebug("Kvp end subel"); m_kvpList.append(*(static_cast (subObj))); m_dataPtr = 0; return ; } //*********************************GncLot********************************************* GncLot::GncLot() { m_subElementListCount = 0; m_dataElementListCount = 0; } GncLot::~GncLot() {} //*********************************GncCountData*************************************** GncCountData::GncCountData() { m_subElementListCount = 0; m_dataElementListCount = 0; m_v.append(QString()); // only 1 data item } GncCountData::~GncCountData() {} void GncCountData::initiate(const QString&, const QXmlAttributes& elAttrs) { m_countType = elAttrs.value("cd:type"); m_dataPtr = &(m_v[0]); return ; } void GncCountData::terminate() { int i = m_v[0].toInt(); if (m_countType == "commodity") { pMain->setGncCommodityCount(i); return ; } if (m_countType == "account") { pMain->setGncAccountCount(i); return ; } if (m_countType == "transaction") { pMain->setGncTransactionCount(i); return ; } if (m_countType == "schedxaction") { pMain->setGncScheduleCount(i); return ; } if (i != 0) { if (m_countType == "budget") pMain->setBudgetsFound(true); else if (m_countType.left(7) == "gnc:Gnc") pMain->setSmallBusinessFound(true); else if (pMain->xmldebug) qDebug() << "Unknown count type" << m_countType; } return ; } //*********************************GncCommodity*************************************** GncCommodity::GncCommodity() { m_subElementListCount = 0; static const QString dEls[] = {"cmdty:space", "cmdty:id", "cmdty:name", "cmdty:fraction"}; m_dataElementList = dEls; m_dataElementListCount = END_Commodity_DELS; static const unsigned int anonClasses[] = {ASIS, NXTEQU, SUPPRESS, ASIS}; m_anonClassList = anonClasses; for (uint i = 0; i < m_dataElementListCount; i++) m_v.append(QString()); } GncCommodity::~GncCommodity() {} void GncCommodity::terminate() { TRY { pMain->convertCommodity(this); return ; } PASS } //************* GncPrice******************************************** GncPrice::GncPrice() { static const QString subEls[] = {"price:commodity", "price:currency", "price:time"}; m_subElementList = subEls; m_subElementListCount = END_Price_SELS; m_dataElementListCount = END_Price_DELS; static const QString dataEls[] = {"price:value"}; m_dataElementList = dataEls; static const unsigned int anonClasses[] = {ASIS}; m_anonClassList = anonClasses; for (uint i = 0; i < m_dataElementListCount; i++) m_v.append(QString()); m_vpCommodity = 0; m_vpCurrency = 0; m_vpPriceDate = 0; } GncPrice::~GncPrice() { delete m_vpCommodity; delete m_vpCurrency; delete m_vpPriceDate; } GncObject *GncPrice::startSubEl() { TRY { GncObject *next = 0; switch (m_state) { case CMDTY: next = new GncCmdtySpec; break; case CURR: next = new GncCmdtySpec; break; case PRICEDATE: next = new GncDate; break; default: throw MYMONEYEXCEPTION("GncPrice rcvd invalid m_state"); } return (next); } PASS } void GncPrice::endSubEl(GncObject *subObj) { TRY { switch (m_state) { case CMDTY: m_vpCommodity = static_cast(subObj); break; case CURR: m_vpCurrency = static_cast(subObj); break; case PRICEDATE: m_vpPriceDate = static_cast(subObj); break; default: throw MYMONEYEXCEPTION("GncPrice rcvd invalid m_state"); } return; } PASS } void GncPrice::terminate() { TRY { pMain->convertPrice(this); return ; } PASS } //************* GncAccount******************************************** GncAccount::GncAccount() { m_subElementListCount = END_Account_SELS; static const QString subEls[] = {"act:commodity", "slot", "act:lots"}; m_subElementList = subEls; m_dataElementListCount = END_Account_DELS; static const QString dataEls[] = {"act:id", "act:name", "act:description", "act:type", "act:parent" }; m_dataElementList = dataEls; static const unsigned int anonClasses[] = {ASIS, NXTACC, SUPPRESS, ASIS, ASIS}; m_anonClassList = anonClasses; for (uint i = 0; i < m_dataElementListCount; i++) m_v.append(QString()); m_vpCommodity = 0; } GncAccount::~GncAccount() { delete m_vpCommodity; } GncObject *GncAccount::startSubEl() { TRY { if (pMain->xmldebug) qDebug("Account start subel m_state %d", m_state); GncObject *next = 0; switch (m_state) { case CMDTY: next = new GncCmdtySpec; break; case KVP: next = new GncKvp; break; case LOTS: next = new GncLot(); pMain->setLotsFound(true); // we don't handle lots; just set flag to report break; default: throw MYMONEYEXCEPTION("GncAccount rcvd invalid m_state"); } return (next); } PASS } void GncAccount::endSubEl(GncObject *subObj) { if (pMain->xmldebug) qDebug("Account end subel"); switch (m_state) { case CMDTY: m_vpCommodity = static_cast(subObj); break; case KVP: m_kvpList.append(*(static_cast (subObj))); } return ; } void GncAccount::terminate() { TRY { pMain->convertAccount(this); return ; } PASS } //************* GncTransaction******************************************** GncTransaction::GncTransaction(bool processingTemplates) { m_subElementListCount = END_Transaction_SELS; static const QString subEls[] = {"trn:currency", "trn:date-posted", "trn:date-entered", "trn:split", "slot" }; m_subElementList = subEls; m_dataElementListCount = END_Transaction_DELS; static const QString dataEls[] = {"trn:id", "trn:num", "trn:description"}; m_dataElementList = dataEls; static const unsigned int anonClasses[] = {ASIS, SUPPRESS, NXTPAY}; m_anonClassList = anonClasses; adjustHideFactor(); m_template = processingTemplates; for (uint i = 0; i < m_dataElementListCount; i++) m_v.append(QString()); m_vpCurrency = 0; m_vpDateEntered = m_vpDatePosted = 0; } GncTransaction::~GncTransaction() { delete m_vpCurrency; delete m_vpDatePosted; delete m_vpDateEntered; } GncObject *GncTransaction::startSubEl() { TRY { if (pMain->xmldebug) qDebug("Transaction start subel m_state %d", m_state); GncObject *next = 0; switch (m_state) { case CURRCY: next = new GncCmdtySpec; break; case POSTED: case ENTERED: next = new GncDate; break; case SPLIT: if (isTemplate()) { next = new GncTemplateSplit; } else { next = new GncSplit; } break; case KVP: next = new GncKvp; break; default: throw MYMONEYEXCEPTION("GncTransaction rcvd invalid m_state"); } return (next); } PASS } void GncTransaction::endSubEl(GncObject *subObj) { if (pMain->xmldebug) qDebug("Transaction end subel"); switch (m_state) { case CURRCY: m_vpCurrency = static_cast(subObj); break; case POSTED: m_vpDatePosted = static_cast(subObj); break; case ENTERED: m_vpDateEntered = static_cast(subObj); break; case SPLIT: m_splitList.append(subObj); break; case KVP: m_kvpList.append(*(static_cast (subObj))); } return ; } void GncTransaction::terminate() { TRY { if (isTemplate()) { pMain->saveTemplateTransaction(this); } else { pMain->convertTransaction(this); } return ; } PASS } //************* GncSplit******************************************** GncSplit::GncSplit() { m_subElementListCount = END_Split_SELS; static const QString subEls[] = {"split:reconcile-date"}; m_subElementList = subEls; m_dataElementListCount = END_Split_DELS; static const QString dataEls[] = {"split:id", "split:memo", "split:reconciled-state", "split:value", "split:quantity", "split:account" }; m_dataElementList = dataEls; static const unsigned int anonClasses[] = {ASIS, SUPPRESS, ASIS, MONEY1, MONEY1, ASIS}; m_anonClassList = anonClasses; for (uint i = 0; i < m_dataElementListCount; i++) m_v.append(QString()); m_vpDateReconciled = 0; } GncSplit::~GncSplit() { delete m_vpDateReconciled; } GncObject *GncSplit::startSubEl() { TRY { GncObject *next = 0; switch (m_state) { case RECDATE: next = new GncDate; break; default: throw MYMONEYEXCEPTION("GncTemplateSplit rcvd invalid m_state "); } return (next); } PASS } void GncSplit::endSubEl(GncObject *subObj) { if (pMain->xmldebug) qDebug("Split end subel"); switch (m_state) { case RECDATE: m_vpDateReconciled = static_cast(subObj); break; } return ; } //************* GncTemplateSplit******************************************** GncTemplateSplit::GncTemplateSplit() { m_subElementListCount = END_TemplateSplit_SELS; static const QString subEls[] = {"slot"}; m_subElementList = subEls; m_dataElementListCount = END_TemplateSplit_DELS; static const QString dataEls[] = {"split:id", "split:memo", "split:reconciled-state", "split:value", "split:quantity", "split:account" }; m_dataElementList = dataEls; static const unsigned int anonClasses[] = {ASIS, SUPPRESS, ASIS, MONEY1, MONEY1, ASIS}; m_anonClassList = anonClasses; for (uint i = 0; i < m_dataElementListCount; i++) m_v.append(QString()); } GncTemplateSplit::~GncTemplateSplit() {} GncObject *GncTemplateSplit::startSubEl() { if (pMain->xmldebug) qDebug("TemplateSplit start subel m_state %d", m_state); TRY { GncObject *next = 0; switch (m_state) { case KVP: next = new GncKvp; break; default: throw MYMONEYEXCEPTION("GncTemplateSplit rcvd invalid m_state"); } return (next); } PASS } void GncTemplateSplit::endSubEl(GncObject *subObj) { if (pMain->xmldebug) qDebug("TemplateSplit end subel"); m_kvpList.append(*(static_cast (subObj))); m_dataPtr = 0; return ; } //************* GncSchedule******************************************** GncSchedule::GncSchedule() { m_subElementListCount = END_Schedule_SELS; static const QString subEls[] = {"sx:start", "sx:last", "sx:end", "gnc:freqspec", "gnc:recurrence", "sx:deferredInstance"}; m_subElementList = subEls; m_dataElementListCount = END_Schedule_DELS; static const QString dataEls[] = {"sx:name", "sx:enabled", "sx:autoCreate", "sx:autoCreateNotify", "sx:autoCreateDays", "sx:advanceCreateDays", "sx:advanceRemindDays", "sx:instanceCount", "sx:num-occur", "sx:rem-occur", "sx:templ-acct" }; m_dataElementList = dataEls; static const unsigned int anonClasses[] = {NXTSCHD, ASIS, ASIS, ASIS, ASIS, ASIS, ASIS, ASIS, ASIS, ASIS, ASIS}; m_anonClassList = anonClasses; for (uint i = 0; i < m_dataElementListCount; i++) m_v.append(QString()); m_vpStartDate = m_vpLastDate = m_vpEndDate = 0; m_vpFreqSpec = 0; m_vpRecurrence.clear(); m_vpSchedDef = 0; } GncSchedule::~GncSchedule() { delete m_vpStartDate; delete m_vpLastDate; delete m_vpEndDate; delete m_vpFreqSpec; delete m_vpSchedDef; } GncObject *GncSchedule::startSubEl() { if (pMain->xmldebug) qDebug("Schedule start subel m_state %d", m_state); TRY { GncObject *next = 0; switch (m_state) { case STARTDATE: case LASTDATE: case ENDDATE: next = new GncDate; break; case FREQ: next = new GncFreqSpec; break; case RECURRENCE: next = new GncRecurrence; break; case DEFINST: next = new GncSchedDef; break; default: throw MYMONEYEXCEPTION("GncSchedule rcvd invalid m_state"); } return (next); } PASS } void GncSchedule::endSubEl(GncObject *subObj) { if (pMain->xmldebug) qDebug("Schedule end subel"); switch (m_state) { case STARTDATE: m_vpStartDate = static_cast(subObj); break; case LASTDATE: m_vpLastDate = static_cast(subObj); break; case ENDDATE: m_vpEndDate = static_cast(subObj); break; case FREQ: m_vpFreqSpec = static_cast(subObj); break; case RECURRENCE: m_vpRecurrence.append(static_cast(subObj)); break; case DEFINST: m_vpSchedDef = static_cast(subObj); break; } return ; } void GncSchedule::terminate() { TRY { pMain->convertSchedule(this); return ; } PASS } //************* GncFreqSpec******************************************** GncFreqSpec::GncFreqSpec() { m_subElementListCount = END_FreqSpec_SELS; static const QString subEls[] = {"gnc:freqspec"}; m_subElementList = subEls; m_dataElementListCount = END_FreqSpec_DELS; static const QString dataEls[] = {"fs:ui_type", "fs:monthly", "fs:daily", "fs:weekly", "fs:interval", "fs:offset", "fs:day" }; m_dataElementList = dataEls; static const unsigned int anonClasses[] = {ASIS, ASIS, ASIS, ASIS, ASIS, ASIS, ASIS }; m_anonClassList = anonClasses; for (uint i = 0; i < m_dataElementListCount; i++) m_v.append(QString()); } GncFreqSpec::~GncFreqSpec() {} GncObject *GncFreqSpec::startSubEl() { TRY { if (pMain->xmldebug) qDebug("FreqSpec start subel m_state %d", m_state); GncObject *next = 0; switch (m_state) { case COMPO: next = new GncFreqSpec; break; default: throw MYMONEYEXCEPTION("GncFreqSpec rcvd invalid m_state"); } return (next); } PASS } void GncFreqSpec::endSubEl(GncObject *subObj) { if (pMain->xmldebug) qDebug("FreqSpec end subel"); switch (m_state) { case COMPO: m_fsList.append(subObj); break; } m_dataPtr = 0; return ; } void GncFreqSpec::terminate() { pMain->convertFreqSpec(this); return ; } //************* GncRecurrence******************************************** GncRecurrence::GncRecurrence() : m_vpStartDate(0) { m_subElementListCount = END_Recurrence_SELS; static const QString subEls[] = {"recurrence:start"}; m_subElementList = subEls; m_dataElementListCount = END_Recurrence_DELS; static const QString dataEls[] = {"recurrence:mult", "recurrence:period_type"}; m_dataElementList = dataEls; static const unsigned int anonClasses[] = {ASIS, ASIS}; m_anonClassList = anonClasses; for (uint i = 0; i < m_dataElementListCount; i++) m_v.append(QString()); } GncRecurrence::~GncRecurrence() { delete m_vpStartDate; } GncObject *GncRecurrence::startSubEl() { TRY { if (pMain->xmldebug) qDebug("Recurrence start subel m_state %d", m_state); GncObject *next = 0; switch (m_state) { case STARTDATE: next = new GncDate; break; default: throw MYMONEYEXCEPTION("GncRecurrence rcvd invalid m_state"); } return (next); } PASS } void GncRecurrence::endSubEl(GncObject *subObj) { if (pMain->xmldebug) qDebug("Recurrence end subel"); switch (m_state) { case STARTDATE: m_vpStartDate = static_cast(subObj); break; } m_dataPtr = 0; return ; } void GncRecurrence::terminate() { pMain->convertRecurrence(this); return ; } QString GncRecurrence::getFrequency() const { // This function converts a gnucash 2.2 recurrence specification into it's previous equivalent // This will all need re-writing when MTE finishes the schedule re-write if (periodType() == "once") return("once"); if ((periodType() == "day") && (mult() == "1")) return("daily"); if (periodType() == "week") { if (mult() == "1") return ("weekly"); if (mult() == "2") return ("bi_weekly"); if (mult() == "4") return ("four-weekly"); } if (periodType() == "month") { if (mult() == "1") return ("monthly"); if (mult() == "2") return ("two-monthly"); if (mult() == "3") return ("quarterly"); if (mult() == "4") return ("tri_annually"); if (mult() == "6") return ("semi_yearly"); if (mult() == "12") return ("yearly"); if (mult() == "24") return ("two-yearly"); } return ("unknown"); } //************* GncSchedDef******************************************** GncSchedDef::GncSchedDef() { // process ing for this sub-object is undefined at the present time m_subElementListCount = 0; m_dataElementListCount = 0; } GncSchedDef::~GncSchedDef() {} /************************************************************************************************ XML Reader ************************************************************************************************/ XmlReader::XmlReader(MyMoneyGncReader *pM) : m_source(0), m_reader(0), m_co(0), pMain(pM), m_headerFound(false) { } void XmlReader::processFile(QIODevice* pDevice) { m_source = new QXmlInputSource(pDevice); // set up the Qt XML reader m_reader = new QXmlSimpleReader; m_reader->setContentHandler(this); // go read the file if (!m_reader->parse(m_source)) { throw MYMONEYEXCEPTION(i18n("Input file cannot be parsed; may be corrupt\n%1", errorString())); } delete m_reader; delete m_source; return ; } // XML handling routines bool XmlReader::startDocument() { m_co = new GncFile; // create initial object, push to stack , pass it the 'main' pointer m_os.push(m_co); m_co->setPm(pMain); m_headerFound = false; #ifdef _GNCFILEANON pMain->oStream << ""; lastType = -1; indentCount = 0; #endif // _GNCFILEANON return (true); } bool XmlReader::startElement(const QString&, const QString&, const QString& elName , const QXmlAttributes& elAttrs) { try { if (pMain->gncdebug) qDebug() << "XML start -" << elName; #ifdef _GNCFILEANON int i; QString spaces; // anonymizer - write data if (elName == "gnc:book" || elName == "gnc:count-data" || elName == "book:id") lastType = -1; pMain->oStream << endl; switch (lastType) { case 0: indentCount += 2; // tricky fall through here case 2: spaces.fill(' ', indentCount); pMain->oStream << spaces.toLatin1(); break; } pMain->oStream << '<' << elName; for (i = 0; i < elAttrs.count(); ++i) { pMain->oStream << ' ' << elAttrs.qName(i) << '=' << '"' << elAttrs.value(i) << '"'; } pMain->oStream << '>'; lastType = 0; #else if ((!m_headerFound) && (elName != "gnc-v2")) throw MYMONEYEXCEPTION(i18n("Invalid header for file. Should be 'gnc-v2'")); m_headerFound = true; #endif // _GNCFILEANON m_co->checkVersion(elName, elAttrs, pMain->m_versionList); // check if this is a sub object element; if so, push stack and initialize GncObject *temp = m_co->isSubElement(elName, elAttrs); if (temp != 0) { m_os.push(temp); m_co = m_os.top(); m_co->setVersion(elAttrs.value("version")); m_co->setPm(pMain); // pass the 'main' pointer to the sub object // return true; // removed, as we hit a return true anyway } #if 0 // check for a data element if (m_co->isDataElement(elName, elAttrs)) return (true); #endif else { // reduced the above to m_co->isDataElement(elName, elAttrs); } } catch (const MyMoneyException &e) { #ifndef _GNCFILEANON // we can't pass on exceptions here coz the XML reader won't catch them and we just abort KMessageBox::error(0, i18n("Import failed:\n\n%1", e.what()), PACKAGE); qWarning("%s", qPrintable(e.what())); #else qWarning("%s", e->toLatin1()); #endif // _GNCFILEANON } return true; // to keep compiler happy } bool XmlReader::endElement(const QString&, const QString&, const QString&elName) { try { if (pMain->xmldebug) qDebug() << "XML end -" << elName; #ifdef _GNCFILEANON QString spaces; switch (lastType) { case 2: indentCount -= 2; spaces.fill(' ', indentCount); pMain->oStream << endl << spaces.toLatin1(); break; } pMain->oStream << "' ; lastType = 2; #endif // _GNCFILEANON m_co->resetDataPtr(); // so we don't get extraneous data loaded into the variables if (elName == m_co->getElName()) { // check if this is the end of the current object if (pMain->gncdebug) m_co->debugDump(); // dump the object data (temp) // call the terminate routine, pop the stack, and advise the parent that it's done m_co->terminate(); GncObject *temp = m_co; m_os.pop(); m_co = m_os.top(); m_co->endSubEl(temp); } return (true); } catch (const MyMoneyException &e) { #ifndef _GNCFILEANON // we can't pass on exceptions here coz the XML reader won't catch them and we just abort KMessageBox::error(0, i18n("Import failed:\n\n%1", e.what()), PACKAGE); qWarning("%s", qPrintable(e.what())); #else qWarning("%s", e->toLatin1()); #endif // _GNCFILEANON } return (true); // to keep compiler happy } bool XmlReader::characters(const QString &data) { if (pMain->xmldebug) qDebug("XML Data received - %d bytes", data.length()); QString pData = data.trimmed(); // data may contain line feeds and indentation spaces if (!pData.isEmpty()) { if (pMain->developerDebug) qDebug() << "XML Data -" << pData; m_co->storeData(pData); //go store it #ifdef _GNCFILEANON QString anonData = m_co->getData(); if (anonData.isEmpty()) anonData = pData; // there must be a Qt standard way of doing the following but I can't ... find it anonData.replace('<', "<"); anonData.replace('>', ">"); anonData.replace('&', "&"); pMain->oStream << anonData; // write original data lastType = 1; #endif // _GNCFILEANON } return (true); } bool XmlReader::endDocument() { #ifdef _GNCFILEANON pMain->oStream << endl << endl; pMain->oStream << "" << endl; pMain->oStream << "" << endl; pMain->oStream << "" << endl; #endif // _GNCFILEANON return (true); } /******************************************************************************************* Main class for this module Controls overall operation of the importer ********************************************************************************************/ //***************** Constructor *********************** MyMoneyGncReader::MyMoneyGncReader() : m_dropSuspectSchedules(0), m_investmentOption(0), m_useFinanceQuote(0), m_useTxNotes(0), gncdebug(0), xmldebug(0), bAnonymize(0), developerDebug(0), m_xr(0), m_progressCallback(0), m_ccCount(0), m_orCount(0), m_scCount(0), m_potentialTransfer(0), m_suspectSchedule(false) { #ifndef _GNCFILEANON m_storage = 0; #endif // _GNCFILEANON // to hold gnucash count data (only used for progress bar) m_gncCommodityCount = m_gncAccountCount = m_gncTransactionCount = m_gncScheduleCount = 0; m_smallBusinessFound = m_budgetsFound = m_lotsFound = false; m_commodityCount = m_priceCount = m_accountCount = m_transactionCount = m_templateCount = m_scheduleCount = 0; m_decoder = 0; // build a list of valid versions static const QString versionList[] = {"gnc:book 2.0.0", "gnc:commodity 2.0.0", "gnc:pricedb 1", "gnc:account 2.0.0", "gnc:transaction 2.0.0", "gnc:schedxaction 1.0.0", "gnc:schedxaction 2.0.0", // for gnucash 2.2 onward "gnc:freqspec 1.0.0", "zzz" // zzz = stopper }; unsigned int i; for (i = 0; versionList[i] != "zzz"; ++i) m_versionList[versionList[i].section(' ', 0, 0)].append(versionList[i].section(' ', 1, 1)); } //***************** Destructor ************************* MyMoneyGncReader::~MyMoneyGncReader() {} //**************************** Main Entry Point ************************************ #ifndef _GNCFILEANON void MyMoneyGncReader::readFile(QIODevice* pDevice, MyMoneyStorageMgr* storage) { Q_CHECK_PTR(pDevice); Q_CHECK_PTR(storage); m_storage = storage; qDebug("Entering gnucash importer"); setOptions(); // get a file anonymization factor from the user if (bAnonymize) setFileHideFactor(); //m_defaultPayee = createPayee (i18n("Unknown payee")); MyMoneyFile::instance()->attachStorage(m_storage); loadAllCurrencies(); MyMoneyFileTransaction ft; m_xr = new XmlReader(this); bool blocked = MyMoneyFile::instance()->signalsBlocked(); MyMoneyFile::instance()->blockSignals(true); try { m_xr->processFile(pDevice); terminate(); // do all the wind-up things ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::error(0, i18n("Import failed:\n\n%1", e.what()), PACKAGE); qWarning("%s", qPrintable(e.what())); } // end catch MyMoneyFile::instance()->blockSignals(blocked); MyMoneyFile::instance()->detachStorage(m_storage); signalProgress(0, 1, i18n("Import complete")); // switch off progress bar delete m_xr; signalProgress(0, 1, i18nc("Application is ready to use", "Ready.")); // application is ready for input qDebug("Exiting gnucash importer"); } #else // Control code for the file anonymizer void MyMoneyGncReader::readFile(QString in, QString out) { QFile pDevice(in); if (!pDevice.open(QIODevice::ReadOnly)) qWarning("Can't open input file"); QFile outFile(out); if (!outFile.open(QIODevice::WriteOnly)) qWarning("Can't open output file"); oStream.setDevice(&outFile); bAnonymize = true; // get a file anonymization factor from the user setFileHideFactor(); m_xr = new XmlReader(this); try { m_xr->processFile(&pDevice); } catch (const MyMoneyException &e) { qWarning("%s", e->toLatin1()); } // end catch delete m_xr; pDevice.close(); outFile.close(); return ; } #include int main(int argc, char ** argv) { QApplication a(argc, argv); MyMoneyGncReader m; QString inFile, outFile; if (argc > 0) inFile = a.argv()[1]; if (argc > 1) outFile = a.argv()[2]; if (inFile.isEmpty()) { inFile = KFileDialog::getOpenFileName("", "Gnucash files(*.nc *)", 0); } if (inFile.isEmpty()) qWarning("Input file required"); if (outFile.isEmpty()) outFile = inFile + ".anon"; m.readFile(inFile, outFile); exit(0); } #endif // _GNCFILEANON void MyMoneyGncReader::setFileHideFactor() { #define MINFILEHIDEF 0.01 #define MAXFILEHIDEF 99.99 +#if QT_VERSION >= 0x051000 + m_fileHideFactor = 0.0; + while (m_fileHideFactor == 0.0) { + m_fileHideFactor = QInputDialog::getDouble(0, + i18n("Disguise your wealth"), + i18n("Each monetary value on your file will be multiplied by a random number between 0.01 and 1.99\n" + "with a different value used for each transaction. In addition, to further disguise the true\n" + "values, you may enter a number between %1 and %2 which will be applied to all values.\n" + "These numbers will not be stored in the file.", MINFILEHIDEF, MAXFILEHIDEF), + (1.0 + (int)(1000.0 * QRandomGenerator::system()->generate() / (RAND_MAX + 1.0))) / 100.0, + MINFILEHIDEF, MAXFILEHIDEF, 2); + } +#else srand(QTime::currentTime().second()); // seed randomizer for anonymize m_fileHideFactor = 0.0; while (m_fileHideFactor == 0.0) { m_fileHideFactor = QInputDialog::getDouble(0, i18n("Disguise your wealth"), i18n("Each monetary value on your file will be multiplied by a random number between 0.01 and 1.99\n" "with a different value used for each transaction. In addition, to further disguise the true\n" "values, you may enter a number between %1 and %2 which will be applied to all values.\n" "These numbers will not be stored in the file.", MINFILEHIDEF, MAXFILEHIDEF), (1.0 + (int)(1000.0 * rand() / (RAND_MAX + 1.0))) / 100.0, MINFILEHIDEF, MAXFILEHIDEF, 2); } +#endif } #ifndef _GNCFILEANON //********************************* convertCommodity ******************************************* void MyMoneyGncReader::convertCommodity(const GncCommodity *gcm) { Q_CHECK_PTR(gcm); MyMoneySecurity equ; if (m_commodityCount == 0) signalProgress(0, m_gncCommodityCount, i18n("Loading commodities...")); if (!gcm->isCurrency()) { // currencies should not be present here but... equ.setName(gcm->name()); equ.setTradingSymbol(gcm->id()); equ.setTradingMarket(gcm->space()); // the 'space' may be market or quote source, dep on what the user did // don't set the source here since he may not want quotes //equ.setValue ("kmm-online-source", gcm->space()); // we don't know, so use it as both equ.setTradingCurrency(""); // not available here, will set from pricedb or transaction equ.setSecurityType(Security::Type::Stock); // default to it being a stock //tell the storage objects we have a new equity object. equ.setSmallestAccountFraction(gcm->fraction().toInt()); m_storage->addSecurity(equ); //assign the gnucash id as the key into the map to find our id if (gncdebug) qDebug() << "mapping, key =" << gcm->id() << "id =" << equ.id(); m_mapEquities[gcm->id().toUtf8()] = equ.id(); } signalProgress(++m_commodityCount, 0); return ; } //******************************* convertPrice ************************************************ void MyMoneyGncReader::convertPrice(const GncPrice *gpr) { Q_CHECK_PTR(gpr); // add this to our price history if (m_priceCount == 0) signalProgress(0, 1, i18n("Loading prices...")); MyMoneyMoney rate(convBadValue(gpr->value())); if (gpr->commodity()->isCurrency()) { MyMoneyPrice exchangeRate(gpr->commodity()->id().toUtf8(), gpr->currency()->id().toUtf8(), gpr->priceDate(), rate, i18n("Imported History")); if (!exchangeRate.rate(QString()).isZero()) m_storage->addPrice(exchangeRate); } else { MyMoneySecurity e = m_storage->security(m_mapEquities[gpr->commodity()->id().toUtf8()]); if (gncdebug) qDebug() << "Searching map, key = " << gpr->commodity()->id() << ", found id =" << e.id().data(); e.setTradingCurrency(gpr->currency()->id().toUtf8()); MyMoneyPrice stockPrice(e.id(), gpr->currency()->id().toUtf8(), gpr->priceDate(), rate, i18n("Imported History")); if (!stockPrice.rate(QString()).isZero()) m_storage->addPrice(stockPrice); m_storage->modifySecurity(e); } signalProgress(++m_priceCount, 0); return ; } //*********************************convertAccount **************************************** void MyMoneyGncReader::convertAccount(const GncAccount* gac) { Q_CHECK_PTR(gac); TRY { // we don't care about the GNC root account if ("ROOT" == gac->type()) { m_rootId = gac->id().toUtf8(); return; } MyMoneyAccount acc; if (m_accountCount == 0) signalProgress(0, m_gncAccountCount, i18n("Loading accounts...")); acc.setName(gac->name()); acc.setDescription(gac->desc()); QDate currentDate = QDate::currentDate(); acc.setOpeningDate(currentDate); acc.setLastModified(currentDate); acc.setLastReconciliationDate(currentDate); if (gac->commodity()->isCurrency()) { acc.setCurrencyId(gac->commodity()->id().toUtf8()); m_currencyCount[gac->commodity()->id()]++; } acc.setParentAccountId(gac->parent().toUtf8()); // now determine the account type and its parent id /* This list taken from # Feb 2006: A RELAX NG Compact schema for gnucash "v2" XML files. # Copyright (C) 2006 Joshua Sled "NO_TYPE" "BANK" "CASH" "CREDIT" "ASSET" "LIABILITY" "STOCK" "MUTUAL" "CURRENCY" "INCOME" "EXPENSE" "EQUITY" "RECEIVABLE" "PAYABLE" "CHECKING" "SAVINGS" "MONEYMRKT" "CREDITLINE" Some don't seem to be used in practice. Not sure what CREDITLINE s/be converted as. */ if ("BANK" == gac->type() || "CHECKING" == gac->type()) { acc.setAccountType(Account::Type::Checkings); } else if ("SAVINGS" == gac->type()) { acc.setAccountType(Account::Type::Savings); } else if ("ASSET" == gac->type()) { acc.setAccountType(Account::Type::Asset); } else if ("CASH" == gac->type()) { acc.setAccountType(Account::Type::Cash); } else if ("CURRENCY" == gac->type()) { acc.setAccountType(Account::Type::Cash); } else if ("STOCK" == gac->type() || "MUTUAL" == gac->type()) { // gnucash allows a 'broker' account to be denominated as type STOCK, but with // a currency balance. We do not need to create a stock account for this // actually, the latest version of gnc (1.8.8) doesn't seem to allow you to do // this any more, though I do have one in my own account... if (gac->commodity()->isCurrency()) { acc.setAccountType(Account::Type::Investment); } else { acc.setAccountType(Account::Type::Stock); } } else if ("EQUITY" == gac->type()) { acc.setAccountType(Account::Type::Equity); } else if ("LIABILITY" == gac->type()) { acc.setAccountType(Account::Type::Liability); } else if ("CREDIT" == gac->type()) { acc.setAccountType(Account::Type::CreditCard); } else if ("INCOME" == gac->type()) { acc.setAccountType(Account::Type::Income); } else if ("EXPENSE" == gac->type()) { acc.setAccountType(Account::Type::Expense); } else if ("RECEIVABLE" == gac->type()) { acc.setAccountType(Account::Type::Asset); } else if ("PAYABLE" == gac->type()) { acc.setAccountType(Account::Type::Liability); } else if ("MONEYMRKT" == gac->type()) { acc.setAccountType(Account::Type::MoneyMarket); } else { // we have here an account type we can't currently handle QString em = i18n("Current importer does not recognize GnuCash account type %1", gac->type()); throw MYMONEYEXCEPTION(em); } // if no parent account is present, assign to one of our standard accounts if ((acc.parentAccountId().isEmpty()) || (acc.parentAccountId() == m_rootId)) { switch (acc.accountGroup()) { case Account::Type::Asset: acc.setParentAccountId(m_storage->asset().id()); break; case Account::Type::Liability: acc.setParentAccountId(m_storage->liability().id()); break; case Account::Type::Income: acc.setParentAccountId(m_storage->income().id()); break; case Account::Type::Expense: acc.setParentAccountId(m_storage->expense().id()); break; case Account::Type::Equity: acc.setParentAccountId(m_storage->equity().id()); break; default: break; // not necessary but avoids compiler warnings } } // extra processing for a stock account if (acc.accountType() == Account::Type::Stock) { // save the id for later linking to investment account m_stockList.append(gac->id()); // set the equity type MyMoneySecurity e = m_storage->security(m_mapEquities[gac->commodity()->id().toUtf8()]); if (gncdebug) qDebug() << "Acct equity search, key =" << gac->commodity()->id() << "found id =" << e.id(); acc.setCurrencyId(e.id()); // actually, the security id if ("MUTUAL" == gac->type()) { e.setSecurityType(Security::Type::MutualFund); if (gncdebug) qDebug() << "Setting" << e.name() << "to mutual"; m_storage->modifySecurity(e); } QString priceSource = gac->getKvpValue("price-source", "string"); if (!priceSource.isEmpty()) getPriceSource(e, priceSource); } if (gac->getKvpValue("tax-related", "integer") == QChar('1')) acc.setValue("Tax", "Yes"); // all the details from the file about the account should be known by now. // calling addAccount will automatically fill in the account ID. m_storage->addAccount(acc); m_mapIds[gac->id().toUtf8()] = acc.id(); // to link gnucash id to ours for tx posting if (gncdebug) qDebug() << "Gnucash account" << gac->id() << "has id of" << acc.id() << ", type of" << MyMoneyAccount::accountTypeToString(acc.accountType()) << "parent is" << acc.parentAccountId(); signalProgress(++m_accountCount, 0); return ; } PASS } //********************************************** convertTransaction ***************************** void MyMoneyGncReader::convertTransaction(const GncTransaction *gtx) { Q_CHECK_PTR(gtx); MyMoneyTransaction tx; MyMoneySplit split; unsigned int i; if (m_transactionCount == 0) signalProgress(0, m_gncTransactionCount, i18n("Loading transactions...")); // initialize class variables related to transactions m_txCommodity = ""; m_txPayeeId = ""; m_potentialTransfer = true; m_splitList.clear(); m_liabilitySplitList.clear(); m_otherSplitList.clear(); // payee, dates, commodity if (!gtx->desc().isEmpty()) m_txPayeeId = createPayee(gtx->desc()); tx.setEntryDate(gtx->dateEntered()); tx.setPostDate(gtx->datePosted()); m_txDatePosted = tx.postDate(); // save for use in splits m_txChequeNo = gtx->no(); // ditto tx.setCommodity(gtx->currency().toUtf8()); m_txCommodity = tx.commodity(); // save in storage, maybe needed for Orphan accounts // process splits for (i = 0; i < gtx->splitCount(); i++) { convertSplit(static_cast(gtx->getSplit(i))); } // handle the odd case of just one split, which gnc allows, // by just duplicating the split // of course, we should change the sign but this case has only ever been seen // when the balance is zero, and can cause kmm to crash, so... if (gtx->splitCount() == 1) { convertSplit(static_cast(gtx->getSplit(0))); } m_splitList += m_liabilitySplitList += m_otherSplitList; // the splits are in order in splitList. Link them to the tx. also, determine the // action type, and fill in some fields which gnc holds at transaction level // first off, is it a transfer (can only have 2 splits?) // also, a tx with just 2 splits is shown by GnuCash as non-split bool nonSplitTx = true; if (m_splitList.count() != 2) { m_potentialTransfer = false; nonSplitTx = false; } QString slotMemo = gtx->getKvpValue(QString("notes")); if (!slotMemo.isEmpty()) tx.setMemo(slotMemo); QList::iterator it = m_splitList.begin(); while (!m_splitList.isEmpty()) { split = *it; // at this point, if m_potentialTransfer is still true, it is actually one! if (m_potentialTransfer) split.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer)); if ((m_useTxNotes) // if use txnotes option is set && (nonSplitTx) // and it's a (GnuCash) non-split transaction && (!tx.memo().isEmpty())) // and tx notes are present split.setMemo(tx.memo()); // use the tx notes as memo tx.addSplit(split); it = m_splitList.erase(it); } m_storage->addTransaction(tx, true); // all done, add the transaction to storage signalProgress(++m_transactionCount, 0); return ; } //******************************************convertSplit******************************** void MyMoneyGncReader::convertSplit(const GncSplit *gsp) { Q_CHECK_PTR(gsp); MyMoneySplit split; MyMoneyAccount splitAccount; // find the kmm account id corresponding to the gnc id QString kmmAccountId; map_accountIds::const_iterator id = m_mapIds.constFind(gsp->acct().toUtf8()); if (id != m_mapIds.constEnd()) { kmmAccountId = id.value(); } else { // for the case where the acs not found (which shouldn't happen?), create an account with gnc name kmmAccountId = createOrphanAccount(gsp->acct()); } // find the account pointer and save for later splitAccount = m_storage->account(kmmAccountId); // print some data so we can maybe identify this split later // TODO : prints personal data //if (gncdebug) qDebug ("Split data - gncid %s, kmmid %s, memo %s, value %s, recon state %s", // gsp->acct().toLatin1(), kmmAccountId.data(), gsp->memo().toLatin1(), gsp->value().toLatin1(), // gsp->recon().toLatin1()); // payee id split.setPayeeId(m_txPayeeId.toUtf8()); // reconciled state and date switch (gsp->recon().at(0).toLatin1()) { case 'n': split.setReconcileFlag(eMyMoney::Split::State::NotReconciled); break; case 'c': split.setReconcileFlag(eMyMoney::Split::State::Cleared); break; case 'y': split.setReconcileFlag(eMyMoney::Split::State::Reconciled); break; } split.setReconcileDate(gsp->reconDate()); // memo split.setMemo(gsp->memo()); // accountId split.setAccountId(kmmAccountId); // cheque no split.setNumber(m_txChequeNo); // value and quantity MyMoneyMoney splitValue(convBadValue(gsp->value())); if (gsp->value() == "-1/0") { // treat gnc invalid value as zero // it's not quite a consistency check, but easier to treat it as such m_messageList["CC"].append (i18n("Account or Category %1, transaction date %2; split contains invalid value; please check", splitAccount.name(), m_txDatePosted.toString(Qt::ISODate))); } MyMoneyMoney splitQuantity(convBadValue(gsp->qty())); split.setValue(splitValue); // if split currency = tx currency, set shares = value (14/10/05) if (splitAccount.currencyId() == m_txCommodity) { split.setShares(splitValue); } else { split.setShares(splitQuantity); } // in kmm, the first split is important. in this routine we will // save the splits in our split list with the priority: // 1. assets // 2. liabilities // 3. others (categories) // but keeping each in same order as gnucash switch (splitAccount.accountGroup()) { case Account::Type::Asset: if (splitAccount.accountType() == Account::Type::Stock) { split.value().isZero() ? split.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::AddShares)) : // free shares? split.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares)); m_potentialTransfer = false; // ? // add a price history entry MyMoneySecurity e = m_storage->security(splitAccount.currencyId()); MyMoneyMoney price; if (!split.shares().isZero()) { static const signed64 NEW_DENOM = 10000; price = split.value() / split.shares(); price = MyMoneyMoney(price.toDouble(), NEW_DENOM); } if (!price.isZero()) { TRY { // we can't use m_storage->security coz security list is not built yet m_storage->currency(m_txCommodity); // will throw exception if not currency e.setTradingCurrency(m_txCommodity); if (gncdebug) qDebug() << "added price for" << e.name() << price.toString() << "date" << m_txDatePosted.toString(Qt::ISODate); m_storage->modifySecurity(e); MyMoneyPrice dealPrice(e.id(), m_txCommodity, m_txDatePosted, price, i18n("Imported Transaction")); m_storage->addPrice(dealPrice); } CATCH { // stock transfer; treat like free shares? split.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::AddShares)); } } } else { // not stock if (split.value().isNegative()) { bool isNumeric = false; if (!split.number().isEmpty()) { split.number().toLong(&isNumeric); // No QString.isNumeric()?? } if (isNumeric) { split.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Check)); } else { split.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal)); } } else { split.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit)); } } m_splitList.append(split); break; case Account::Type::Liability: split.value().isNegative() ? split.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal)) : split.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit)); m_liabilitySplitList.append(split); break; default: m_potentialTransfer = false; m_otherSplitList.append(split); } // backdate the account opening date if necessary if (m_txDatePosted < splitAccount.openingDate()) { splitAccount.setOpeningDate(m_txDatePosted); m_storage->modifyAccount(splitAccount); } return ; } //********************************* convertTemplateTransaction ********************************************** MyMoneyTransaction MyMoneyGncReader::convertTemplateTransaction(const QString& schedName, const GncTransaction *gtx) { Q_CHECK_PTR(gtx); MyMoneyTransaction tx; MyMoneySplit split; unsigned int i; if (m_templateCount == 0) signalProgress(0, 1, i18n("Loading templates...")); // initialize class variables related to transactions m_txCommodity = ""; m_txPayeeId = ""; m_potentialTransfer = true; m_splitList.clear(); m_liabilitySplitList.clear(); m_otherSplitList.clear(); // payee, dates, commodity if (!gtx->desc().isEmpty()) { m_txPayeeId = createPayee(gtx->desc()); } else { m_txPayeeId = createPayee(i18n("Unknown payee")); // schedules require a payee tho normal tx's don't. not sure why... } tx.setEntryDate(gtx->dateEntered()); tx.setPostDate(gtx->datePosted()); m_txDatePosted = tx.postDate(); tx.setCommodity(gtx->currency().toUtf8()); m_txCommodity = tx.commodity(); // save for possible use in orphan account // process splits for (i = 0; i < gtx->splitCount(); i++) { convertTemplateSplit(schedName, static_cast(gtx->getSplit(i))); } // determine the action type for the splits and link them to the template tx if (!m_otherSplitList.isEmpty()) m_potentialTransfer = false; // tfrs can occur only between assets and asset/liabilities m_splitList += m_liabilitySplitList += m_otherSplitList; // the splits are in order in splitList. Transfer them to the tx // also, determine the action type. first off, is it a transfer (can only have 2 splits?) if (m_splitList.count() != 2) m_potentialTransfer = false; // at this point, if m_potentialTransfer is still true, it is actually one! QString txMemo = ""; QList::iterator it = m_splitList.begin(); while (!m_splitList.isEmpty()) { split = *it; if (m_potentialTransfer) { split.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer)); } else { if (split.value().isNegative()) { //split.setAction (negativeActionType); split.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Withdrawal)); } else { //split.setAction (positiveActionType); split.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit)); } } split.setNumber(gtx->no()); // set cheque no (or equivalent description) // Arbitrarily, save the first non-null split memo as the memo for the whole tx // I think this is necessary because txs with just 2 splits (the majority) // are not viewable as split transactions in kmm so the split memo is not seen if ((txMemo.isEmpty()) && (!split.memo().isEmpty())) txMemo = split.memo(); tx.addSplit(split); it = m_splitList.erase(it); } // memo - set from split tx.setMemo(txMemo); signalProgress(++m_templateCount, 0); return (tx); } //********************************* convertTemplateSplit **************************************************** void MyMoneyGncReader::convertTemplateSplit(const QString& schedName, const GncTemplateSplit *gsp) { Q_CHECK_PTR(gsp); // convertTemplateSplit MyMoneySplit split; MyMoneyAccount splitAccount; unsigned int i, j; bool nonNumericFormula = false; // action, value and account will be set from slots // reconcile state, always Not since it hasn't even been posted yet (?) split.setReconcileFlag(eMyMoney::Split::State::NotReconciled); // memo split.setMemo(gsp->memo()); // payee id split.setPayeeId(m_txPayeeId.toUtf8()); // read split slots (KVPs) int xactionCount = 0; int validSlotCount = 0; QString gncAccountId; for (i = 0; i < gsp->kvpCount(); i++) { const GncKvp& slot = gsp->getKvp(i); if ((slot.key() == "sched-xaction") && (slot.type() == "frame")) { bool bFoundStringCreditFormula = false; bool bFoundStringDebitFormula = false; bool bFoundGuidAccountId = false; QString gncCreditFormula, gncDebitFormula; for (j = 0; j < slot.kvpCount(); j++) { const GncKvp& subSlot = slot.getKvp(j); // again, see comments above. when we have a full specification // of all the options available to us, we can no doubt improve on this if ((subSlot.key() == "credit-formula") && (subSlot.type() == "string")) { gncCreditFormula = subSlot.value(); bFoundStringCreditFormula = true; } if ((subSlot.key() == "debit-formula") && (subSlot.type() == "string")) { gncDebitFormula = subSlot.value(); bFoundStringDebitFormula = true; } if ((subSlot.key() == "account") && (subSlot.type() == "guid")) { gncAccountId = subSlot.value(); bFoundGuidAccountId = true; } } // all data read, now check we have everything if ((bFoundStringCreditFormula) && (bFoundStringDebitFormula) && (bFoundGuidAccountId)) { if (gncdebug) qDebug() << "Found valid slot; credit" << gncCreditFormula << "debit" << gncDebitFormula << "acct" << gncAccountId; validSlotCount++; } // validate numeric, work out sign MyMoneyMoney exFormula; exFormula.setNegativeMonetarySignPosition(eMyMoney::Money::BeforeQuantityMoney); QString numericTest; char crdr = 0 ; if (!gncCreditFormula.isEmpty()) { crdr = 'C'; numericTest = gncCreditFormula; } else if (!gncDebitFormula.isEmpty()) { crdr = 'D'; numericTest = gncDebitFormula; } KMyMoneyMoneyValidator v(0); int pos; // useless, but required for validator if (v.validate(numericTest, pos) == QValidator::Acceptable) { switch (crdr) { case 'C': exFormula = QString("-" + numericTest); break; case 'D': exFormula = numericTest; } } else { if (gncdebug) qDebug() << numericTest << "is not numeric"; nonNumericFormula = true; } split.setValue(exFormula); xactionCount++; } else { m_messageList["SC"].append( i18n("Schedule %1 contains unknown action (key = %2, type = %3)", schedName, slot.key(), slot.type())); m_suspectSchedule = true; } } // report this as untranslatable tx if (xactionCount > 1) { m_messageList["SC"].append( i18n("Schedule %1 contains multiple actions; only one has been imported", schedName)); m_suspectSchedule = true; } if (validSlotCount == 0) { m_messageList["SC"].append( i18n("Schedule %1 contains no valid splits", schedName)); m_suspectSchedule = true; } if (nonNumericFormula) { m_messageList["SC"].append( i18n("Schedule %1 appears to contain a formula. GnuCash formulae are not convertible", schedName)); m_suspectSchedule = true; } // find the kmm account id corresponding to the gnc id QString kmmAccountId; map_accountIds::const_iterator id = m_mapIds.constFind(gncAccountId.toUtf8()); if (id != m_mapIds.constEnd()) { kmmAccountId = id.value(); } else { // for the case where the acs not found (which shouldn't happen?), create an account with gnc name kmmAccountId = createOrphanAccount(gncAccountId); } splitAccount = m_storage->account(kmmAccountId); split.setAccountId(kmmAccountId); // if split currency = tx currency, set shares = value (14/10/05) if (splitAccount.currencyId() == m_txCommodity) { split.setShares(split.value()); } /* else { //FIXME: scheduled currency or investment tx needs to be investigated split.setShares (splitQuantity); } */ // add the split to one of the lists switch (splitAccount.accountGroup()) { case Account::Type::Asset: m_splitList.append(split); break; case Account::Type::Liability: m_liabilitySplitList.append(split); break; default: m_otherSplitList.append(split); } // backdate the account opening date if necessary if (m_txDatePosted < splitAccount.openingDate()) { splitAccount.setOpeningDate(m_txDatePosted); m_storage->modifyAccount(splitAccount); } return ; } //********************************* convertSchedule ******************************************************** void MyMoneyGncReader::convertSchedule(const GncSchedule *gsc) { TRY { Q_CHECK_PTR(gsc); MyMoneySchedule sc; MyMoneyTransaction tx; m_suspectSchedule = false; QDate startDate, nextDate, lastDate, endDate; // for date calculations QDate today = QDate::currentDate(); int numOccurs, remOccurs; if (m_scheduleCount == 0) signalProgress(0, m_gncScheduleCount, i18n("Loading schedules...")); // schedule name sc.setName(gsc->name()); // find the transaction template as stored earlier QList::const_iterator itt; for (itt = m_templateList.constBegin(); itt != m_templateList.constEnd(); ++itt) { // the id to match against is the split:account value in the splits if (static_cast((*itt)->getSplit(0))->acct() == gsc->templId()) break; } if (itt == m_templateList.constEnd()) { throw MYMONEYEXCEPTION(i18n("Cannot find template transaction for schedule %1", sc.name())); } else { tx = convertTemplateTransaction(sc.name(), *itt); } tx.clearId(); // define the conversion table for intervals struct convIntvl { QString gncType; // the gnucash name unsigned char interval; // for date calculation unsigned int intervalCount; Schedule::Occurrence occ; // equivalent occurrence code Schedule::WeekendOption wo; }; /* other intervals supported by gnc according to Josh Sled's schema (see above) "none" "semi_monthly" */ /* some of these type names do not appear in gnucash and are difficult to generate for pre 2.2 files.They can be generated for 2.2 however, by GncRecurrence::getFrequency() */ static convIntvl vi [] = { {"once", 'o', 1, Schedule::Occurrence::Once, Schedule::WeekendOption::MoveNothing }, {"daily" , 'd', 1, Schedule::Occurrence::Daily, Schedule::WeekendOption::MoveNothing }, //{"daily_mf", 'd', 1, Schedule::Occurrence::Daily, Schedule::WeekendOption::MoveAfter }, doesn't work, need new freq in kmm {"30-days" , 'd', 30, Schedule::Occurrence::EveryThirtyDays, Schedule::WeekendOption::MoveNothing }, {"weekly", 'w', 1, Schedule::Occurrence::Weekly, Schedule::WeekendOption::MoveNothing }, {"bi_weekly", 'w', 2, Schedule::Occurrence::EveryOtherWeek, Schedule::WeekendOption::MoveNothing }, {"three-weekly", 'w', 3, Schedule::Occurrence::EveryThreeWeeks, Schedule::WeekendOption::MoveNothing }, {"four-weekly", 'w', 4, Schedule::Occurrence::EveryFourWeeks, Schedule::WeekendOption::MoveNothing }, {"eight-weekly", 'w', 8, Schedule::Occurrence::EveryEightWeeks, Schedule::WeekendOption::MoveNothing }, {"monthly", 'm', 1, Schedule::Occurrence::Monthly, Schedule::WeekendOption::MoveNothing }, {"two-monthly", 'm', 2, Schedule::Occurrence::EveryOtherMonth, Schedule::WeekendOption::MoveNothing }, {"quarterly", 'm', 3, Schedule::Occurrence::Quarterly, Schedule::WeekendOption::MoveNothing }, {"tri_annually", 'm', 4, Schedule::Occurrence::EveryFourMonths, Schedule::WeekendOption::MoveNothing }, {"semi_yearly", 'm', 6, Schedule::Occurrence::TwiceYearly, Schedule::WeekendOption::MoveNothing }, {"yearly", 'y', 1, Schedule::Occurrence::Yearly, Schedule::WeekendOption::MoveNothing }, {"two-yearly", 'y', 2, Schedule::Occurrence::EveryOtherYear, Schedule::WeekendOption::MoveNothing }, {"zzz", 'y', 1, Schedule::Occurrence::Yearly, Schedule::WeekendOption::MoveNothing} // zzz = stopper, may cause problems. what else can we do? }; QString frequency = "unknown"; // set default to unknown frequency bool unknownOccurs = false; // may have zero, or more than one frequency/recurrence spec QString schedEnabled; if (gsc->version() == "2.0.0") { if (gsc->m_vpRecurrence.count() != 1) { unknownOccurs = true; } else { const GncRecurrence *gre = gsc->m_vpRecurrence.first(); //qDebug (QString("Sched %1, pt %2, mu %3, sd %4").arg(gsc->name()).arg(gre->periodType()) // .arg(gre->mult()).arg(gre->startDate().toString(Qt::ISODate))); frequency = gre->getFrequency(); schedEnabled = gsc->enabled(); } sc.setOccurrence(Schedule::Occurrence::Once); // FIXME - how to convert } else { // find this interval const GncFreqSpec *fs = gsc->getFreqSpec(); if (fs == 0) { unknownOccurs = true; } else { frequency = fs->intervalType(); if (!fs->m_fsList.isEmpty()) unknownOccurs = true; // nested freqspec } schedEnabled = 'y'; // earlier versions did not have an enable flag } int i; for (i = 0; vi[i].gncType != "zzz"; i++) { if (frequency == vi[i].gncType) break; } if (vi[i].gncType == "zzz") { m_messageList["SC"].append( i18n("Schedule %1 has interval of %2 which is not currently available", sc.name(), frequency)); i = 0; // treat as single occurrence m_suspectSchedule = true; } if (unknownOccurs) { m_messageList["SC"].append( i18n("Schedule %1 contains unknown interval specification; please check for correct operation", sc.name())); m_suspectSchedule = true; } // set the occurrence interval, weekend option, start date sc.setOccurrence(vi[i].occ); sc.setWeekendOption(vi[i].wo); sc.setStartDate(gsc->startDate()); // if a last date was specified, use it, otherwise try to work out the last date sc.setLastPayment(gsc->lastDate()); numOccurs = gsc->numOccurs().toInt(); if (sc.lastPayment() == QDate()) { nextDate = lastDate = gsc->startDate(); while ((nextDate < today) && (numOccurs-- != 0)) { lastDate = nextDate; nextDate = incrDate(lastDate, vi[i].interval, vi[i].intervalCount); } sc.setLastPayment(lastDate); } // under Tom's new regime, the tx dates are the next due date (I think) tx.setPostDate(incrDate(sc.lastPayment(), vi[i].interval, vi[i].intervalCount)); tx.setEntryDate(incrDate(sc.lastPayment(), vi[i].interval, vi[i].intervalCount)); // if an end date was specified, use it, otherwise if the input file had a number // of occurs remaining, work out the end date sc.setEndDate(gsc->endDate()); numOccurs = gsc->numOccurs().toInt(); remOccurs = gsc->remOccurs().toInt(); if ((sc.endDate() == QDate()) && (remOccurs > 0)) { endDate = sc.lastPayment(); while (remOccurs-- > 0) { endDate = incrDate(endDate, vi[i].interval, vi[i].intervalCount); } sc.setEndDate(endDate); } // Check for sched deferred interval. Don't know how/if we can handle it, or even what it means... if (gsc->getSchedDef() != 0) { m_messageList["SC"].append( i18n("Schedule %1 contains a deferred interval specification; please check for correct operation", sc.name())); m_suspectSchedule = true; } // payment type, options sc.setPaymentType((Schedule::PaymentType)Schedule::PaymentType::Other); sc.setFixed(!m_suspectSchedule); // if any probs were found, set it as variable so user will always be prompted // we don't currently have a 'disable' option, but just make sure auto-enter is off if not enabled //qDebug(QString("%1 and %2").arg(gsc->autoCreate()).arg(schedEnabled)); sc.setAutoEnter((gsc->autoCreate() == QChar('y')) && (schedEnabled == QChar('y'))); //qDebug(QString("autoEnter set to %1").arg(sc.autoEnter())); // type QString actionType = tx.splits().first().action(); if (actionType == MyMoneySplit::actionName(eMyMoney::Split::Action::Deposit)) { sc.setType((Schedule::Type)Schedule::Type::Deposit); } else if (actionType == MyMoneySplit::actionName(eMyMoney::Split::Action::Transfer)) { sc.setType((Schedule::Type)Schedule::Type::Transfer); } else { sc.setType((Schedule::Type)Schedule::Type::Bill); } // finally, set the transaction pointer sc.setTransaction(tx); //tell the storage objects we have a new schedule object. if (m_suspectSchedule && m_dropSuspectSchedules) { m_messageList["SC"].append( i18n("Schedule %1 dropped at user request", sc.name())); } else { m_storage->addSchedule(sc); if (m_suspectSchedule) m_suspectList.append(sc.id()); } signalProgress(++m_scheduleCount, 0); return ; } PASS } //********************************* convertFreqSpec ******************************************************** void MyMoneyGncReader::convertFreqSpec(const GncFreqSpec *) { // Nowt to do here at the moment, convertSched only retrieves the interval type // but we will probably need to look into the nested freqspec when we properly implement semi-monthly and stuff return ; } //********************************* convertRecurrence ******************************************************** void MyMoneyGncReader::convertRecurrence(const GncRecurrence *) { return ; } //********************************************************************************************************** //************************************* terminate ********************************************************** void MyMoneyGncReader::terminate() { TRY { // All data has been converted and added to storage // this code is just temporary to show us what is in the file. if (gncdebug) qDebug("%d accounts found in the GnuCash file", (unsigned int)m_mapIds.count()); for (map_accountIds::const_iterator it = m_mapIds.constBegin(); it != m_mapIds.constEnd(); ++it) { if (gncdebug) qDebug() << "key =" << it.key() << "value =" << it.value(); } // first step is to implement the users investment option, now we // have all the accounts available QList::iterator stocks; for (stocks = m_stockList.begin(); stocks != m_stockList.end(); ++stocks) { checkInvestmentOption(*stocks); } // Next step is to walk the list and assign the parent/child relationship between the objects. unsigned int i = 0; signalProgress(0, m_accountCount, i18n("Reorganizing accounts...")); QList list; QList::iterator acc; m_storage->accountList(list); for (acc = list.begin(); acc != list.end(); ++acc) { if ((*acc).parentAccountId() == m_storage->asset().id()) { MyMoneyAccount assets = m_storage->asset(); m_storage->addAccount(assets, (*acc)); if (gncdebug) qDebug() << "Account id" << (*acc).id() << "is a child of the main asset account"; } else if ((*acc).parentAccountId() == m_storage->liability().id()) { MyMoneyAccount liabilities = m_storage->liability(); m_storage->addAccount(liabilities, (*acc)); if (gncdebug) qDebug() << "Account id" << (*acc).id() << "is a child of the main liability account"; } else if ((*acc).parentAccountId() == m_storage->income().id()) { MyMoneyAccount incomes = m_storage->income(); m_storage->addAccount(incomes, (*acc)); if (gncdebug) qDebug() << "Account id" << (*acc).id() << "is a child of the main income account"; } else if ((*acc).parentAccountId() == m_storage->expense().id()) { MyMoneyAccount expenses = m_storage->expense(); m_storage->addAccount(expenses, (*acc)); if (gncdebug) qDebug() << "Account id" << (*acc).id() << "is a child of the main expense account"; } else if ((*acc).parentAccountId() == m_storage->equity().id()) { MyMoneyAccount equity = m_storage->equity(); m_storage->addAccount(equity, (*acc)); if (gncdebug) qDebug() << "Account id" << (*acc).id() << "is a child of the main equity account"; } else if ((*acc).parentAccountId() == m_rootId) { if (gncdebug) qDebug() << "Account id" << (*acc).id() << "is a child of the main root account"; } else { // it is not under one of the main accounts, so find gnucash parent QString parentKey = (*acc).parentAccountId(); if (gncdebug) qDebug() << "Account id" << (*acc).id() << "is a child of " << (*acc).parentAccountId(); map_accountIds::const_iterator id = m_mapIds.constFind(parentKey); if (id != m_mapIds.constEnd()) { if (gncdebug) qDebug() << "Setting account id" << (*acc).id() << "parent account id to" << id.value(); MyMoneyAccount parent = m_storage->account(id.value()); parent = checkConsistency(parent, (*acc)); m_storage->addAccount(parent, (*acc)); } else { throw MYMONEYEXCEPTION("terminate() could not find account id"); } } signalProgress(++i, 0); } // end for account signalProgress(0, 1, (".")); // debug - get rid of reorg message // offer the most common account currency as a default QString mainCurrency = ""; unsigned int maxCount = 0; QMap::ConstIterator it; for (it = m_currencyCount.constBegin(); it != m_currencyCount.constEnd(); ++it) { if (it.value() > maxCount) { maxCount = it.value(); mainCurrency = it.key(); } } if (mainCurrency != "") { QString question = i18n("Your main currency seems to be %1 (%2); do you want to set this as your base currency?", mainCurrency, m_storage->currency(mainCurrency.toUtf8()).name()); if (KMessageBox::questionYesNo(0, question, PACKAGE) == KMessageBox::Yes) { m_storage->setValue("kmm-baseCurrency", mainCurrency); } } // now produce the end of job reports - first, work out which ones are required QList sectionsToReport; // list of sections needing report sectionsToReport.append("MN"); // always build the main section if ((m_ccCount = m_messageList["CC"].count()) > 0) sectionsToReport.append("CC"); if ((m_orCount = m_messageList["OR"].count()) > 0) sectionsToReport.append("OR"); if ((m_scCount = m_messageList["SC"].count()) > 0) sectionsToReport.append("SC"); // produce the sections in separate message boxes bool exit = false; int si; for (si = 0; (si < sectionsToReport.count()) && !exit; ++si) { QString button0Text = i18nc("Button to show more detailed data", "More"); if (si + 1 == sectionsToReport.count()) button0Text = i18nc("Button to close the current dialog", "Done"); // last section KGuiItem yesItem(button0Text, QIcon(), "", ""); KGuiItem noItem(i18n("Save Report"), QIcon(), "", ""); switch (KMessageBox::questionYesNoCancel(0, buildReportSection(sectionsToReport[si]), PACKAGE, yesItem, noItem)) { case KMessageBox::Yes: break; case KMessageBox::No: exit = writeReportToFile(sectionsToReport); break; default: exit = true; break; } } for (si = 0; si < m_suspectList.count(); ++si) { auto sc = m_storage->schedule(m_suspectList[si]); KMessageBox::information(0, i18n("Problems were encountered in converting schedule '%1'.", sc.name()), PACKAGE); // TODO: return this feature // switch (KMessageBox::warningYesNo(0, i18n("Problems were encountered in converting schedule '%1'.\nDo you want to review or edit it now?", sc.name()), PACKAGE)) { // case KMessageBox::Yes: // auto s = new KEditScheduleDlg(sc); // if (s->exec()) // m_storage->modifySchedule(s->schedule()); // delete s; // break; // default: // break; // } } } PASS } //************************************ buildReportSection************************************ QString MyMoneyGncReader::buildReportSection(const QString& source) { TRY { QString s = ""; bool more = false; if (source == "MN") { s.append(i18n("Found:\n\n")); s.append(i18np("%1 commodity (equity)\n", "%1 commodities (equities)\n", m_commodityCount)); s.append(i18np("%1 price\n", "%1 prices\n", m_priceCount)); s.append(i18np("%1 account\n", "%1 accounts\n", m_accountCount)); s.append(i18np("%1 transaction\n", "%1 transactions\n", m_transactionCount)); s.append(i18np("%1 schedule\n", "%1 schedules\n", m_scheduleCount)); s.append("\n\n"); if (m_ccCount == 0) { s.append(i18n("No inconsistencies were detected\n")); } else { s.append(i18np("%1 inconsistency was detected and corrected\n", "%1 inconsistencies were detected and corrected\n", m_ccCount)); more = true; } if (m_orCount > 0) { s.append("\n\n"); s.append(i18np("%1 orphan account was created\n", "%1 orphan accounts were created\n", m_orCount)); more = true; } if (m_scCount > 0) { s.append("\n\n"); s.append(i18np("%1 possible schedule problem was noted\n", "%1 possible schedule problems were noted\n", m_scCount)); more = true; } QString unsupported(""); QString lineSep("\n - "); if (m_smallBusinessFound) unsupported.append(lineSep + i18n("Small Business Features (Customers, Invoices, etc.)")); if (m_budgetsFound) unsupported.append(lineSep + i18n("Budgets")); if (m_lotsFound) unsupported.append(lineSep + i18n("Lots")); if (!unsupported.isEmpty()) { unsupported.prepend(i18n("The following features found in your file are not currently supported:")); s.append(unsupported); } if (more) s.append(i18n("\n\nPress More for further information")); } else { s = m_messageList[source].join(QChar('\n')); } if (gncdebug) qDebug() << s; return (static_cast(s)); } PASS } //************************ writeReportToFile********************************* bool MyMoneyGncReader::writeReportToFile(const QList& sectionsToReport) { TRY { int i; QString fd = QFileDialog::getSaveFileName(0, QString(), QString(), i18n("Save report as")); if (fd.isEmpty()) return (false); QFile reportFile(fd); if (!reportFile.open(QIODevice::WriteOnly)) { return (false); } QTextStream stream(&reportFile); for (i = 0; i < sectionsToReport.count(); i++) stream << buildReportSection(sectionsToReport[i]) << endl; reportFile.close(); return (true); } PASS } /**************************************************************************** Utility routines *****************************************************************************/ //************************ createPayee *************************** QString MyMoneyGncReader::createPayee(const QString& gncDescription) { MyMoneyPayee payee; TRY { payee = m_storage->payeeByName(gncDescription); } CATCH { // payee not found, create one payee.setName(gncDescription); m_storage->addPayee(payee); } return (payee.id()); } //************************************** createOrphanAccount ******************************* QString MyMoneyGncReader::createOrphanAccount(const QString& gncName) { MyMoneyAccount acc; acc.setName("orphan_" + gncName); acc.setDescription(i18n("Orphan created from unknown GnuCash account")); QDate today = QDate::currentDate(); acc.setOpeningDate(today); acc.setLastModified(today); acc.setLastReconciliationDate(today); acc.setCurrencyId(m_txCommodity); acc.setAccountType(Account::Type::Asset); acc.setParentAccountId(m_storage->asset().id()); m_storage->addAccount(acc); // assign the gnucash id as the key into the map to find our id m_mapIds[gncName.toUtf8()] = acc.id(); m_messageList["OR"].append( i18n("One or more transactions contain a reference to an otherwise unknown account\n" "An asset account with the name %1 has been created to hold the data", acc.name())); return (acc.id()); } //****************************** incrDate ********************************************* QDate MyMoneyGncReader::incrDate(QDate lastDate, unsigned char interval, unsigned int intervalCount) { TRY { switch (interval) { case 'd': return (lastDate.addDays(intervalCount)); case 'w': return (lastDate.addDays(intervalCount * 7)); case 'm': return (lastDate.addMonths(intervalCount)); case 'y': return (lastDate.addYears(intervalCount)); case 'o': // once-only return (lastDate); } throw MYMONEYEXCEPTION(i18n("Internal error - invalid interval char in incrDate")); // QDate r = QDate(); return (r); // to keep compiler happy } PASS } //********************************* checkConsistency ********************************** MyMoneyAccount MyMoneyGncReader::checkConsistency(MyMoneyAccount& parent, MyMoneyAccount& child) { TRY { // gnucash is flexible/weird enough to allow various inconsistencies // these are a couple I found in my file, no doubt more will be discovered if ((child.accountType() == Account::Type::Investment) && (parent.accountType() != Account::Type::Asset)) { m_messageList["CC"].append( i18n("An Investment account must be a child of an Asset account\n" "Account %1 will be stored under the main Asset account", child.name())); return m_storage->asset(); } if ((child.accountType() == Account::Type::Income) && (parent.accountType() != Account::Type::Income)) { m_messageList["CC"].append( i18n("An Income account must be a child of an Income account\n" "Account %1 will be stored under the main Income account", child.name())); return m_storage->income(); } if ((child.accountType() == Account::Type::Expense) && (parent.accountType() != Account::Type::Expense)) { m_messageList["CC"].append( i18n("An Expense account must be a child of an Expense account\n" "Account %1 will be stored under the main Expense account", child.name())); return m_storage->expense(); } return (parent); } PASS } //*********************************** checkInvestmentOption ************************* void MyMoneyGncReader::checkInvestmentOption(QString stockId) { // implement the investment option for stock accounts // first check whether the parent account (gnucash id) is actually an // investment account. if it is, no further action is needed MyMoneyAccount stockAcc = m_storage->account(m_mapIds[stockId.toUtf8()]); MyMoneyAccount parent; QString parentKey = stockAcc.parentAccountId(); map_accountIds::const_iterator id = m_mapIds.constFind(parentKey); if (id != m_mapIds.constEnd()) { parent = m_storage->account(id.value()); if (parent.accountType() == Account::Type::Investment) return ; } // so now, check the investment option requested by the user // option 0 creates a separate investment account for each stock account if (m_investmentOption == 0) { MyMoneyAccount invAcc(stockAcc); invAcc.setAccountType(Account::Type::Investment); invAcc.setCurrencyId(QString("")); // we don't know what currency it is!! invAcc.setParentAccountId(parentKey); // intersperse it between old parent and child stock acct m_storage->addAccount(invAcc); m_mapIds [invAcc.id()] = invAcc.id(); // so stock account gets parented (again) to investment account later if (gncdebug) qDebug() << "Created investment account" << invAcc.name() << "as id" << invAcc.id() << "parent" << invAcc.parentAccountId(); if (gncdebug) qDebug() << "Setting stock" << stockAcc.name() << "id" << stockAcc.id() << "as child of" << invAcc.id(); stockAcc.setParentAccountId(invAcc.id()); m_storage->addAccount(invAcc, stockAcc); // investment option 1 creates a single investment account for all stocks } else if (m_investmentOption == 1) { static QString singleInvAccId = ""; MyMoneyAccount singleInvAcc; bool ok = false; if (singleInvAccId.isEmpty()) { // if the account has not yet been created QString invAccName; while (!ok) { invAccName = QInputDialog::getText(0, QStringLiteral(PACKAGE), i18n("Enter the investment account name "), QLineEdit::Normal, i18n("My Investments"), &ok); } singleInvAcc.setName(invAccName); singleInvAcc.setAccountType(Account::Type::Investment); singleInvAcc.setCurrencyId(QString("")); singleInvAcc.setParentAccountId(m_storage->asset().id()); m_storage->addAccount(singleInvAcc); m_mapIds [singleInvAcc.id()] = singleInvAcc.id(); // so stock account gets parented (again) to investment account later if (gncdebug) qDebug() << "Created investment account" << singleInvAcc.name() << "as id" << singleInvAcc.id() << "parent" << singleInvAcc.parentAccountId() << "reparenting stock"; singleInvAccId = singleInvAcc.id(); } else { // the account has already been created singleInvAcc = m_storage->account(singleInvAccId); } m_storage->addAccount(singleInvAcc, stockAcc); // add stock as child // the original intention of option 2 was to allow any asset account to be converted to an investment (broker) account // however, since we have already stored the accounts as asset, we have no way at present of changing their type // the only alternative would be to hold all the gnucash data in memory, then implement this option, then convert all the data // that would mean a major overhaul of the code. Perhaps I'll think of another way... } else if (m_investmentOption == 2) { static int lastSelected = 0; MyMoneyAccount invAcc(stockAcc); QStringList accList; QList list; QList::iterator acc; m_storage->accountList(list); // build a list of candidates for the input box for (acc = list.begin(); acc != list.end(); ++acc) { // if (((*acc).accountGroup() == Account::Type::Asset) && ((*acc).accountType() != Account::Type::Stock)) accList.append ((*acc).name()); if ((*acc).accountType() == Account::Type::Investment) accList.append((*acc).name()); } //if (accList.isEmpty()) qWarning ("No available accounts"); bool ok = false; while (!ok) { // keep going till we have a valid investment parent QString invAccName = QInputDialog::getItem(0, PACKAGE, i18n("Select parent investment account or enter new name. Stock %1", stockAcc.name()), accList, lastSelected, true, &ok); if (ok) { lastSelected = accList.indexOf(invAccName); // preserve selection for next time for (acc = list.begin(); acc != list.end(); ++acc) { if ((*acc).name() == invAccName) break; } if (acc != list.end()) { // an account was selected invAcc = *acc; } else { // a new account name was entered invAcc.setAccountType(Account::Type::Investment); invAcc.setName(invAccName); invAcc.setCurrencyId(QString("")); invAcc.setParentAccountId(m_storage->asset().id()); m_storage->addAccount(invAcc); ok = true; } if (invAcc.accountType() == Account::Type::Investment) { ok = true; } else { // this code is probably not going to be implemented coz we can't change account types (??) #if 0 QMessageBox mb(PACKAGE, i18n("%1 is not an Investment Account. Do you wish to make it one?", invAcc.name()), QMessageBox::Question, QMessageBox::Yes | QMessageBox::Default, QMessageBox::No | QMessageBox::Escape, Qt::NoButton); switch (mb.exec()) { case QMessageBox::No : ok = false; break; default: // convert it - but what if it has splits??? qWarning("Not yet implemented"); ok = true; break; } #endif switch (KMessageBox::questionYesNo(0, i18n("%1 is not an Investment Account. Do you wish to make it one?", invAcc.name()), PACKAGE)) { case KMessageBox::Yes: // convert it - but what if it has splits??? qWarning("Not yet implemented"); ok = true; break; default: ok = false; break; } } } // end if ok - user pressed Cancel } // end while !ok m_mapIds [invAcc.id()] = invAcc.id(); // so stock account gets parented (again) to investment account later m_storage->addAccount(invAcc, stockAcc); } else { // investment option != 0, 1, 2 qWarning("Invalid investment option %d", m_investmentOption); } } // get the price source for a stock (gnc account) where online quotes are requested void MyMoneyGncReader::getPriceSource(MyMoneySecurity stock, QString gncSource) { // if he wants to use Finance::Quote, no conversion of source name is needed if (m_useFinanceQuote) { stock.setValue("kmm-online-quote-system", "Finance::Quote"); stock.setValue("kmm-online-source", gncSource.toLower()); m_storage->modifySecurity(stock); return; } // first check if we have already asked about this source // (mapSources is initialy empty. We may be able to pre-fill it with some equivalent // sources, if such things do exist. User feedback may help here.) QMap::const_iterator it; for (it = m_mapSources.constBegin(); it != m_mapSources.constEnd(); ++it) { if (it.key() == gncSource) { stock.setValue("kmm-online-source", it.value()); m_storage->modifySecurity(stock); return; } } // not found in map, so ask the user QPointer dlg = new KGncPriceSourceDlg(stock.name(), gncSource); dlg->exec(); QString s = dlg->selectedSource(); if (!s.isEmpty()) { stock.setValue("kmm-online-source", s); m_storage->modifySecurity(stock); } if (dlg->alwaysUse()) m_mapSources[gncSource] = s; delete dlg; return; } void MyMoneyGncReader::loadAllCurrencies() { auto file = MyMoneyFile::instance(); MyMoneyFileTransaction ft; if (!file->currencyList().isEmpty()) return; auto ancientCurrencies = file->ancientCurrencies(); try { foreach (auto currency, file->availableCurrencyList()) { file->addCurrency(currency); MyMoneyPrice price = ancientCurrencies.value(currency, MyMoneyPrice()); if (price != MyMoneyPrice()) file->addPrice(price); } ft.commit(); } catch (const MyMoneyException &e) { qDebug("Error %s loading currency", qPrintable(e.what())); } } // functions to control the progress bar //*********************** setProgressCallback ***************************** void MyMoneyGncReader::setProgressCallback(void(*callback)(int, int, const QString&)) { m_progressCallback = callback; return ; } //************************** signalProgress ******************************* void MyMoneyGncReader::signalProgress(int current, int total, const QString& msg) { if (m_progressCallback != 0) (*m_progressCallback)(current, total, msg); return ; } #endif // _GNCFILEANON diff --git a/kmymoney/plugins/gnc/import/mymoneygncreader.h b/kmymoney/plugins/gnc/import/mymoneygncreader.h index 9c99dfc0d..4bc0e118d 100644 --- a/kmymoney/plugins/gnc/import/mymoneygncreader.h +++ b/kmymoney/plugins/gnc/import/mymoneygncreader.h @@ -1,1091 +1,1092 @@ /*************************************************************************** mymoneygncreader - description ------------------- begin : Wed Mar 3 2004 copyright : (C) 2000-2004 by Michael Edwardes email : mte@users.sourceforge.net Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio + (C) 2018 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. * * * ***************************************************************************/ /* The main class of this module, MyMoneyGncReader, contains only a readFile() function, which controls the import of data from an XML file created by the current GnuCash version (1.8.8). The XML is processed in class XmlReader, which is an implementation of the Qt SAX2 reader class. Data in the input file is processed as a set of objects which fortunately, though perhaps not surprisingly, have almost a one-for-one correspondence with KMyMoney objects. These objects are bounded by start and end XML elements, and may contain both nested objects (described as sub objects in the code), and data items, also delimited by start and end elements. For example: * start of sub object within file Account Name * data string with start and end elements ... * end of sub objects A GnuCash file may consist of more than one 'book', or set of data. It is not clear how we could currently implement this, so only the first book in a file is processed. This should satisfy most user situations. GnuCash is somewhat inconsistent in its division of the major sections of the file. For example, multiple price history entries are delimited by elements, while each account starts with its own top-level element. In general, the 'container' elements are ignored. XmlReader This is an implementation of the Qt QXmlDefaultHandler class, which provides three main function calls in addition to start and end of document. The startElement() and endElement() calls are self-explanatory, the characters() function provides data strings. Thus in the above example, the sequence of calls would be startElement() for gnc:account startElement() for act:name characters() for 'Account Name' endElement() for act:name ... endElement() for gnc:account Objects Since the processing requirements of XML for most elements are very similar, the common code is implemented in a GncObject class, from which the others are derived, with virtual function calls to cater for any differences. The 'grandfather' object, GncFile representing the file (or more correctly, 'book') as a whole, is created in the startDocument() function call. The constructor function of each object is responsible for providing two lists for the XmlReader to scan, a list of element names which represent sub objects (called sub elements in the code), and a similar list of names representing data elements. In addition, an array of variables (m_v) is provided and initialized, to contain the actual data strings. Implementation Since objects may be nested, a stack is used, with the top element pointing to the 'current object'. The startDocument() call creates the first, GncFile, object at the top of the stack. As each startElement() call occurs, the two#include "mymoneygncreader.h" element lists created by the current object are scanned. If this element represents the start of a sub object, the current object's subEl() function is called to create an instance of the appropriate type. This is then pushed to the top of the stack, and the new object's initiate() function is called. This is used to process any XML attributes attached to the element; GnuCash makes little use of these. If this represents the start of a data element, a pointer (m_dataPointer) is set to point to an entry in the array (m_v) in which a subsequent characters() call can store the actual data. When an endElement() call occurs, a check is made to see if it matches the element name which started the current object. If so, the object's terminate() function is called. If the object represents a similar KMM object, this will normally result in a call to a conversion routine in the main (MyMoneyGncReader) class to convert the data to native format and place it in storage. The stack is then popped, and the parent (now current) object notified by a call to its endSubEl() function. Again depending on the type of object, this will either delete the instance, or save it in its own storage for later processing. For example, a GncSplit object makes little sense outside the context of its transaction, so will be saved by the transaction. A GncTransaction object on the other hand will be converted, along with its attendant splits, and then deleted by its parent. Since at any one time an object will only be processing either a subobject or a data element, a single object variable, m_state, is used to determine the actual type. In effect, it acts as the current index into either the subElement or dataElement list. As an object variable, it will be saved on the stack across subobject processing. Exceptions and Problems Fatal exceptions are processed via the standard MyMoneyException method. Due to differences in implementation between GnuCash and KMM, it is not always possible to provide an absolutely correct conversion. When such a problem situation is recognized, a message, along with any relevant variable data, is passed to the main class, and used to produce a report when processing terminates. Anonymizer When debugging problems, it is often useful to have a trace of what is happening within the module. However, in view of the sensitive nature of personal finance data, most users will be reluctant to provide this. Accordingly, an anonymize (hide()) function is provided to handle data strings. These may either be passed through asis (non-personal data), blanked out (non-critical but possibly personal data), replaced with a generated version (required, but possibly personal), or randomized (monetary amounts). The action for each data item is determined in the object's constructor function along with the creation of the data element list. This module will later be used as the basis of a file anonymizer, which will enable users to safely provide us with a copy of their GnuCash files, and will allow us to test the structure, if not the data content, of the file. */ #ifndef MYMONEYGNCREADER_H #define MYMONEYGNCREADER_H // system includes #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #ifndef _GNCFILEANON #include "storage/imymoneystorageformat.h" #endif // _GNCFILEANON // not sure what these are for, but leave them in #define VERSION_0_60_XML 0x10000010 // Version 0.5 file version info #define VERSION_0_61_XML 0x10000011 // use 8 bytes for MyMoneyMoney objects #define GNUCASH_ID_KEY "GNUCASH_ID" class MyMoneyAccount; class MyMoneySecurity; class MyMoneyTransaction; class MyMoneySplit; typedef QMap map_accountIds; typedef map_accountIds::iterator map_accountIds_iter; typedef map_accountIds::const_iterator map_accountIds_citer; typedef QMap map_elementVersions; class MyMoneyGncReader; class QIODevice; class QDate; class QTextCodec; class MyMoneyStorageMgr; class QXmlAttributes; class QXmlInputSource; class QXmlSimpleReader; /** GncObject is the base class for the various objects in the gnucash file Beyond the first level XML objects, elements will be of one of three types: 1. Sub object elements, which require creation of another object to process 2. Data object elements, which are only followed by data to be stored in a variable (m_v array) 3. Ignored objects, data not needed and not included herein */ class GncKvp; class GncObject { public: GncObject(); virtual ~GncObject() {} // make sure to have impl of all virtual rtns to avoid vtable errors? protected: friend class XmlReader; friend class MyMoneyGncReader; // check for sub object element; if it is, create the object GncObject *isSubElement(const QString &elName, const QXmlAttributes& elAttrs); // check for data element; if so, set data pointer bool isDataElement(const QString &elName, const QXmlAttributes& elAttrs); // process start element for 'this'; normally for attribute checking; other initialization done in constructor virtual void initiate(const QString&, const QXmlAttributes&) { return ; }; // a sub object has completed; process the data it gathered virtual void endSubEl(GncObject *) { m_dataPtr = 0; return ; }; // store data for data element void storeData(const QString& pData) { // NB - data MAY come in chunks, and may need to be anonymized if (m_dataPtr != 0) m_dataPtr->append(hide(pData, m_anonClass)); } // following is provided only for a future file anonymizer QString getData() const { return ((m_dataPtr != 0) ? *m_dataPtr : ""); }; void resetDataPtr() { m_dataPtr = 0; }; // process end element for 'this'; usually to convert to KMM format virtual void terminate() { return ; }; void setVersion(const QString& v) { m_version = v; return; }; QString version() const { return (m_version); }; // some gnucash elements have version attribute; check it void checkVersion(const QString&, const QXmlAttributes&, const map_elementVersions&); // get name of element processed by 'this' QString getElName() const { return (m_elementName); }; // pass 'main' pointer to object void setPm(MyMoneyGncReader *pM) { pMain = pM; }; const QString getKvpValue(const QString& key, const QString& type = QString()) const; // debug only void debugDump(); // called by isSubElement to create appropriate sub object virtual GncObject *startSubEl() { return (0); }; // called by isDataElement to set variable pointer virtual void dataEl(const QXmlAttributes&) { m_dataPtr = &(m_v[m_state]); m_anonClass = m_anonClassList[m_state]; }; // return gnucash data string variable pointer virtual QString var(int i) const; // anonymize data virtual QString hide(QString, unsigned int); unsigned int kvpCount() const { return (m_kvpList.count()); }; //! MyMoneyGncReader *pMain; // pointer to 'main' class // used at start of each transaction so same money hide factor is applied to all splits void adjustHideFactor(); QString m_elementName; // save 'this' element's name QString m_version; // and it's gnucash version const QString *m_subElementList; // list of sub object element names for 'this' unsigned int m_subElementListCount; // count of above const QString *m_dataElementList; // ditto for data elements unsigned int m_dataElementListCount; QString *m_dataPtr; // pointer to m_v variable for current data item mutable QList m_v; // storage for variable pointers unsigned int m_state; // effectively, the index to subElementList or dataElementList, whichever is currently in use const unsigned int *m_anonClassList; enum anonActions {ASIS, SUPPRESS, NXTACC, NXTEQU, NXTPAY, NXTSCHD, MAYBEQ, MONEY1, MONEY2}; // anonymize actions - see hide() unsigned int m_anonClass; // class of current data item for anonymizer static double m_moneyHideFactor; // a per-transaction factor QList m_kvpList; //! }; // ***************************************************************************** // This is the 'grandfather' object representing the gnucash file as a whole class GncFile : public GncObject { public: GncFile(); ~GncFile(); private: enum iSubEls {BOOK, COUNT, CMDTY, PRICE, ACCT, TX, TEMPLATES, SCHEDULES, END_FILE_SELS }; virtual GncObject *startSubEl(); virtual void endSubEl(GncObject *); bool m_processingTemplates; // gnc uses same transaction element for ordinary and template tx's; this will distinguish bool m_bookFound; // to detect multi-book files }; // The following are 'utility' objects, which occur within several other object types // ************* GncKvp******************************************** // Key/value pairs, which are introduced by the 'slot' element // Consist of slot:key (the 'name' of the kvp), and slot:value (the data value) // the slot value also contains a slot type (string, integer, etc) implemented as an XML attribute // kvp's may be nested class GncKvp : public GncObject { public: GncKvp(); ~GncKvp(); //protected: friend class MyMoneyGncReader; QString key() const { return (var(KEY)); }; QString value() const { return (var(VALUE)); }; QString type() const { return (m_kvpType); }; const GncKvp getKvp(unsigned int i) const { return (m_kvpList[i]); }; private: // subsidiary objects/elements enum KvpSubEls {KVP, END_Kvp_SELS }; virtual GncObject *startSubEl(); virtual void endSubEl(GncObject *); // data elements enum KvpDataEls {KEY, VALUE, END_Kvp_DELS }; virtual void dataEl(const QXmlAttributes&); QString m_kvpType; // type is an XML attribute }; // ************* GncLot******************************************** // KMM doesn't have support for lots as yet class GncLot : public GncObject { public: GncLot(); ~GncLot(); protected: friend class MyMoneyGncReader; private: }; // **************************************************************************** // commodity specification. consists of // cmdty:space - either ISO4217 if this cmdty is a currency, or, usually, the name of a stock exchange // cmdty:id - ISO4217 currency symbol, or 'ticker symbol' class GncCmdtySpec : public GncObject { public: GncCmdtySpec(); ~GncCmdtySpec(); protected: friend class MyMoneyGncReader; friend class GncTransaction; bool isCurrency() const { return (m_v[CMDTYSPC] == QString("ISO4217")); }; QString id() const { return (m_v[CMDTYID]); }; QString space() const { return (m_v[CMDTYSPC]); }; private: // data elements enum CmdtySpecDataEls {CMDTYSPC, CMDTYID, END_CmdtySpec_DELS}; virtual QString hide(QString, unsigned int); }; // ********************************************************************* // date; maybe one of two types, ts:date which is date/time, gdate which is date only // we do not preserve time data (at present) class GncDate : public GncObject { public: GncDate(); ~GncDate(); protected: friend class MyMoneyGncReader; friend class GncPrice; friend class GncTransaction; friend class GncSplit; friend class GncSchedule; friend class GncRecurrence; const QDate date() const { return (QDate::fromString(m_v[TSDATE].section(' ', 0, 0), Qt::ISODate)); }; private: // data elements enum DateDataEls {TSDATE, GDATE, END_Date_DELS}; virtual void dataEl(const QXmlAttributes&) { m_dataPtr = &(m_v[TSDATE]); m_anonClass = GncObject::ASIS; } ; // treat both date types the same }; /** Following are the main objects within the gnucash file, which correspond largely one-for-one with similar objects in the kmymoney structure, apart from schedules which gnc splits between template (transaction data) and schedule (date data) */ //******************************************************************** class GncCountData : public GncObject { public: GncCountData(); ~GncCountData(); private: virtual void initiate(const QString&, const QXmlAttributes&); virtual void terminate(); QString m_countType; // type of element being counted }; //******************************************************************** class GncCommodity : public GncObject { public: GncCommodity(); ~GncCommodity(); protected: friend class MyMoneyGncReader; // access data values bool isCurrency() const { return (var(SPACE) == QString("ISO4217")); }; QString space() const { return (var(SPACE)); }; QString id() const { return (var(ID)); }; QString name() const { return (var(NAME)); }; QString fraction() const { return (var(FRACTION)); }; private: virtual void terminate(); // data elements enum {SPACE, ID, NAME, FRACTION, END_Commodity_DELS}; }; // ************* GncPrice******************************************** class GncPrice : public GncObject { public: GncPrice(); ~GncPrice(); protected: friend class MyMoneyGncReader; // access data values const GncCmdtySpec *commodity() const { return (m_vpCommodity); }; const GncCmdtySpec *currency() const { return (m_vpCurrency); }; QString value() const { return (var(VALUE)); }; QDate priceDate() const { return (m_vpPriceDate->date()); }; private: virtual void terminate(); // sub object elements enum PriceSubEls {CMDTY, CURR, PRICEDATE, END_Price_SELS }; virtual GncObject *startSubEl(); virtual void endSubEl(GncObject *); // data elements enum PriceDataEls {VALUE, END_Price_DELS }; GncCmdtySpec *m_vpCommodity, *m_vpCurrency; GncDate *m_vpPriceDate; }; // ************* GncAccount******************************************** class GncAccount : public GncObject { public: GncAccount(); ~GncAccount(); protected: friend class MyMoneyGncReader; // access data values GncCmdtySpec *commodity() const { return (m_vpCommodity); }; QString id() const { return (var(ID)); }; QString name() const { return (var(NAME)); }; QString desc() const { return (var(DESC)); }; QString type() const { return (var(TYPE)); }; QString parent() const { return (var(PARENT)); }; private: // subsidiary objects/elements enum AccountSubEls {CMDTY, KVP, LOTS, END_Account_SELS }; virtual GncObject *startSubEl(); virtual void endSubEl(GncObject *); virtual void terminate(); // data elements enum AccountDataEls {ID, NAME, DESC, TYPE, PARENT, END_Account_DELS }; GncCmdtySpec *m_vpCommodity; }; // ************* GncSplit******************************************** class GncSplit : public GncObject { public: GncSplit(); ~GncSplit(); protected: friend class MyMoneyGncReader; // access data values QString id() const { return (var(ID)); }; QString memo() const { return (var(MEMO)); }; QString recon() const { return (var(RECON)); }; QString value() const { return (var(VALUE)); }; QString qty() const { return (var(QTY)); }; QString acct() const { return (var(ACCT)); }; const QDate reconDate() const { QDate x = QDate(); return (m_vpDateReconciled == NULL ? x : m_vpDateReconciled->date()); }; private: // subsidiary objects/elements enum TransactionSubEls {RECDATE, END_Split_SELS }; virtual GncObject *startSubEl(); virtual void endSubEl(GncObject *); // data elements enum SplitDataEls {ID, MEMO, RECON, VALUE, QTY, ACCT, END_Split_DELS }; GncDate *m_vpDateReconciled; }; // ************* GncTransaction******************************************** class GncTransaction : public GncObject { public: GncTransaction(bool processingTemplates); ~GncTransaction(); protected: friend class MyMoneyGncReader; // access data values QString id() const { return (var(ID)); }; QString no() const { return (var(NO)); }; QString desc() const { return (var(DESC)); }; QString currency() const { return (m_vpCurrency == NULL ? QString() : m_vpCurrency->id()); }; QDate dateEntered() const { return (m_vpDateEntered->date()); }; QDate datePosted() const { return (m_vpDatePosted->date()); }; bool isTemplate() const { return (m_template); }; unsigned int splitCount() const { return (m_splitList.count()); }; const GncObject *getSplit(unsigned int i) const { return (m_splitList.at(i)); }; private: // subsidiary objects/elements enum TransactionSubEls {CURRCY, POSTED, ENTERED, SPLIT, KVP, END_Transaction_SELS }; virtual GncObject *startSubEl(); virtual void endSubEl(GncObject *); virtual void terminate(); const GncKvp getKvp(unsigned int i) const { return (m_kvpList.at(i)); }; // data elements enum TransactionDataEls {ID, NO, DESC, END_Transaction_DELS }; GncCmdtySpec *m_vpCurrency; GncDate *m_vpDateEntered, *m_vpDatePosted; mutable QList m_splitList; bool m_template; // true if this is a template for scheduled transaction }; // ************* GncTemplateSplit******************************************** class GncTemplateSplit : public GncObject { public: GncTemplateSplit(); ~GncTemplateSplit(); protected: friend class MyMoneyGncReader; // access data values QString id() const { return (var(ID)); }; QString memo() const { return (var(MEMO)); }; QString recon() const { return (var(RECON)); }; QString value() const { return (var(VALUE)); }; QString qty() const { return (var(QTY)); }; QString acct() const { return (var(ACCT)); }; private: const GncKvp getKvp(unsigned int i) const { return (m_kvpList[i]); }; // subsidiary objects/elements enum TemplateSplitSubEls {KVP, END_TemplateSplit_SELS }; virtual GncObject *startSubEl(); virtual void endSubEl(GncObject *); // data elements enum TemplateSplitDataEls {ID, MEMO, RECON, VALUE, QTY, ACCT, END_TemplateSplit_DELS }; }; // ************* GncSchedule******************************************** class GncFreqSpec; class GncRecurrence; class GncSchedDef; class GncSchedule : public GncObject { public: GncSchedule(); ~GncSchedule(); protected: friend class MyMoneyGncReader; // access data values QString name() const { return (var(NAME)); }; QString enabled() const { return var(ENABLED); }; QString autoCreate() const { return (var(AUTOC)); }; QString autoCrNotify() const { return (var(AUTOCN)); }; QString autoCrDays() const { return (var(AUTOCD)); }; QString advCrDays() const { return (var(ADVCD)); }; QString advCrRemindDays() const { return (var(ADVRD)); }; QString instanceCount() const { return (var(INSTC)); }; QString numOccurs() const { return (var(NUMOCC)); }; QString remOccurs() const { return (var(REMOCC)); }; QString templId() const { return (var(TEMPLID)); }; QDate startDate() const { QDate x = QDate(); return (m_vpStartDate == NULL ? x : m_vpStartDate->date()); }; QDate lastDate() const { QDate x = QDate(); return (m_vpLastDate == NULL ? x : m_vpLastDate->date()); }; QDate endDate() const { QDate x = QDate(); return (m_vpEndDate == NULL ? x : m_vpEndDate->date()); }; const GncFreqSpec *getFreqSpec() const { return (m_vpFreqSpec); }; const GncSchedDef *getSchedDef() const { return (m_vpSchedDef); }; private: // subsidiary objects/elements enum ScheduleSubEls {STARTDATE, LASTDATE, ENDDATE, FREQ, RECURRENCE, DEFINST, END_Schedule_SELS }; virtual GncObject *startSubEl(); virtual void endSubEl(GncObject *); virtual void terminate(); // data elements enum ScheduleDataEls {NAME, ENABLED, AUTOC, AUTOCN, AUTOCD, ADVCD, ADVRD, INSTC, NUMOCC, REMOCC, TEMPLID, END_Schedule_DELS }; GncDate *m_vpStartDate, *m_vpLastDate, *m_vpEndDate; GncFreqSpec *m_vpFreqSpec; mutable QList m_vpRecurrence; // gnc handles multiple occurrences GncSchedDef *m_vpSchedDef; }; // ************* GncFreqSpec******************************************** class GncFreqSpec : public GncObject { public: GncFreqSpec(); ~GncFreqSpec(); protected: friend class MyMoneyGncReader; // access data values (only interval type used at present) QString intervalType() const { return (var(INTVT)); }; private: // subsidiary objects/elements enum FreqSpecSubEls {COMPO, END_FreqSpec_SELS }; virtual GncObject *startSubEl(); virtual void endSubEl(GncObject *); // data elements enum FreqSpecDataEls {INTVT, MONTHLY, DAILY, WEEKLY, INTVI, INTVO, INTVD, END_FreqSpec_DELS}; virtual void terminate(); mutable QList m_fsList; }; // ************* GncRecurrence******************************************** // this object replaces GncFreqSpec from Gnucash 2.2 onwards class GncRecurrence : public GncObject { public: GncRecurrence(); ~GncRecurrence(); protected: friend class MyMoneyGncReader; // access data values QDate startDate() const { QDate x = QDate(); return (m_vpStartDate == NULL ? x : m_vpStartDate->date()); }; QString mult() const { return (var(MULT)); }; QString periodType() const { return (var(PERIODTYPE)); }; QString getFrequency() const; private: // subsidiary objects/elements enum RecurrenceSubEls {STARTDATE, END_Recurrence_SELS }; virtual GncObject *startSubEl(); virtual void endSubEl(GncObject *); // data elements enum RecurrenceDataEls {MULT, PERIODTYPE, END_Recurrence_DELS}; virtual void terminate(); GncDate *m_vpStartDate; }; // ************* GncSchedDef******************************************** // This is a sub-object of GncSchedule, (sx:deferredInstance) function currently unknown class GncSchedDef : public GncObject { public: GncSchedDef(); ~GncSchedDef(); protected: friend class MyMoneyGncReader; private: // subsidiary objects/elements }; // **************************************************************************************** /** XML Reader The XML reader is an implementation of the Qt SAX2 XML parser. It determines the type of object represented by the XMl, and calls the appropriate object functions */ // ***************************************************************************************** class XmlReader : public QXmlDefaultHandler { protected: friend class MyMoneyGncReader; XmlReader(MyMoneyGncReader *pM); // keep pointer to 'main' void processFile(QIODevice*); // main entry point of reader // define xml content handler functions bool startDocument(); bool startElement(const QString&, const QString&, const QString&, const QXmlAttributes&); bool endElement(const QString&, const QString&, const QString&); bool characters(const QString &); bool endDocument(); private: QXmlInputSource *m_source; QXmlSimpleReader *m_reader; QStack m_os; // stack of sub objects GncObject *m_co; // current object, for ease of coding (=== m_os.top) MyMoneyGncReader *pMain; // the 'main' pointer, to pass on to objects bool m_headerFound; // check for gnc-v2 header #ifdef _GNCFILEANON int lastType; // 0 = start element, 1 = data, 2 = end element int indentCount; #endif // _GNCFILEANON }; /** MyMoneyGncReader - Main class for this module Controls overall operation of the importer */ #ifndef _GNCFILEANON class MyMoneyGncReader : public IMyMoneyOperationsFormat { #else class MyMoneyGncReader { #endif // _GNCFILEANON public: MyMoneyGncReader(); virtual ~MyMoneyGncReader(); /** * Import a GnuCash XML file * * @param pDevice : pointer to GnuCash file * @param storage : pointer to MyMoneySerialize storage * * @return void * */ #ifndef _GNCFILEANON void readFile(QIODevice* pDevice, MyMoneyStorageMgr *storage); // main entry point, IODevice is gnucash file void writeFile(QIODevice*, MyMoneyStorageMgr*) { return ; }; // dummy entry needed by kmymoneywiew. we will not be writing #else void readFile(QString, QString); #endif // _GNCFILEANON QTextCodec *m_decoder; protected: friend class GncObject; // pity we can't just say GncObject. And compiler doesn't like multiple friends on one line... friend class GncFile; // there must be a better way... friend class GncDate; friend class GncCmdtySpec; friend class GncKvp; friend class GncLot; friend class GncCountData; friend class GncCommodity; friend class GncPrice; friend class GncAccount; friend class GncTransaction; friend class GncSplit; friend class GncTemplateTransaction; friend class GncTemplateSplit; friend class GncSchedule; friend class GncFreqSpec; friend class GncRecurrence; friend class XmlReader; #ifndef _GNCFILEANON /** functions to convert gnc objects to our equivalent */ void convertCommodity(const GncCommodity *); void convertPrice(const GncPrice *); void convertAccount(const GncAccount *); void convertTransaction(const GncTransaction *); void convertSplit(const GncSplit *); void saveTemplateTransaction(GncTransaction *t) { m_templateList.append(t); }; void convertSchedule(const GncSchedule *); void convertFreqSpec(const GncFreqSpec *); void convertRecurrence(const GncRecurrence *); #else /** functions to convert gnc objects to our equivalent */ void convertCommodity(const GncCommodity *) { return; }; void convertPrice(const GncPrice *) { return; }; void convertAccount(const GncAccount *) { return; }; void convertTransaction(const GncTransaction *) { return; }; void convertSplit(const GncSplit *) { return; }; void saveTemplateTransaction(GncTransaction *t) { return; }; void convertSchedule(const GncSchedule *) { return; }; void convertFreqSpec(const GncFreqSpec *) { return; }; #endif // _GNCFILEANON /** to post messages for final report */ void postMessage(const QString&, const unsigned int, const char *); void postMessage(const QString&, const unsigned int, const char *, const char *); void postMessage(const QString&, const unsigned int, const char *, const char *, const char *); void postMessage(const QString&, const unsigned int, const QStringList&); void setProgressCallback(void(*callback)(int, int, const QString&)); void signalProgress(int current, int total, const QString& = ""); /** user options */ /** Scheduled Transactions Due to differences in implementation, it is not always possible to import scheduled transactions correctly. Though best efforts are made, it may be that some imported transactions cause problems within kmymoney. An attempt is made within the importer to identify potential problem transactions, and setting this option will cause them to be dropped from the file. A report of which were dropped, and why, will be produced. m_dropSuspectSchedules - drop suspect scheduled transactions */ bool m_dropSuspectSchedules; /** Investments In kmymoney, all accounts representing investments (stocks, shares, bonds, etc.) must have an associated investment account (e.g. a broker account). The stock account holds the share balance, the investment account a money balance. Gnucash does not do this, so we cannot automate this function. If you have investments, you must select one of the following options. 0 - create a separate investment account for each stock with the same name as the stock 1 - create a single investment account to hold all stocks - you will be asked for a name 2 - create multiple investment accounts - you will be asked for a name for each stock N.B. :- option 2 doesn't really work quite as desired at present */ unsigned int m_investmentOption; /** Online quotes The user has the option to use the Finance::Quote system, as used by GnuCash, to retrieve online share price quotes */ bool m_useFinanceQuote; /** Tx Notes handling Under some usage conditions, non-split GnuCash transactions may contain residual, usually incorrect, memo data which is not normally visible to the user. When imported into KMyMoney however, due to display differences, this data can become visible. Often, these transactions will have a Notes field describing the real purpose of the transaction. If this option is selected, these notes, if present, will be used to override the extraneous memo data." */ bool m_useTxNotes; // set gnucash counts (not always accurate!) void setGncCommodityCount(int i) { m_gncCommodityCount = i; }; void setGncAccountCount(int i) { m_gncAccountCount = i; }; void setGncTransactionCount(int i) { m_gncTransactionCount = i; }; void setGncScheduleCount(int i) { m_gncScheduleCount = i; }; void setSmallBusinessFound(bool b) { m_smallBusinessFound = b; }; void setBudgetsFound(bool b) { m_budgetsFound = b; }; void setLotsFound(bool b) { m_lotsFound = b; }; /* Debug Options If you don't know what these are, best leave them alone. gncdebug - produce general debug messages xmldebug - produce a trace of the gnucash file XML bAnonymize - hide personal data (account names, payees, etc., randomize money amounts) */ bool gncdebug; // general debug messages bool xmldebug; // xml trace bool bAnonymize; // anonymize input static double m_fileHideFactor; // an overall anonymization factor to be applied to all items bool developerDebug; private: void setOptions(); // to set user options from dialog void setFileHideFactor(); // the following handles the gnucash indicator for a bad value (-1/0) which causes us probs QString convBadValue(QString gncValue) const { return (gncValue == "-1/0" ? "0/1" : gncValue); }; #ifndef _GNCFILEANON MyMoneyTransaction convertTemplateTransaction(const QString&, const GncTransaction *); void convertTemplateSplit(const QString&, const GncTemplateSplit *); #endif // _GNCFILEANON // wind up when all done void terminate(); QString buildReportSection(const QString&); bool writeReportToFile(const QList&); // main storage #ifndef _GNCFILEANON MyMoneyStorageMgr *m_storage; #else QTextStream oStream; #endif // _GNCFILEANON XmlReader *m_xr; /** to hold the callback pointer for the progress bar */ void (*m_progressCallback)(int, int, const QString&); // a map of which versions of the various elements (objects) we can import map_elementVersions m_versionList; // counters holding count data from the Gnc 'count-data' section int m_gncCommodityCount; int m_gncAccountCount; int m_gncTransactionCount; int m_gncScheduleCount; // flags indicating detection of features not (yet?) supported bool m_smallBusinessFound; bool m_budgetsFound; bool m_lotsFound; /** counters for reporting */ int m_commodityCount; int m_priceCount; int m_accountCount; int m_transactionCount; int m_templateCount; int m_scheduleCount; #ifndef _GNCFILEANON // counters for error reporting int m_ccCount, m_orCount, m_scCount; // currency counter QMap m_currencyCount; /** * Map gnucash vs. Kmm ids for accounts, equities, schedules, price sources */ QMap m_mapIds; QString m_rootId; // save the root id for terminate() QMap m_mapEquities; QMap m_mapSchedules; QMap m_mapSources; /** * A list of stock accounts (gnc ids) which will be held till the end so we can implement the user's investment option */ QList m_stockList; /** * Temporary storage areas for transaction processing */ QString m_txCommodity; // save commodity for current transaction QString m_txPayeeId; // gnc has payee at tx level, we need it at split level QDate m_txDatePosted; // ditto for post date QString m_txChequeNo; // ditto for cheque number /** In kmm, the order of splits is critical to some operations. These * areas will hold the splits until we've read them all */ QList m_splitList, m_liabilitySplitList, m_otherSplitList; bool m_potentialTransfer; // to determine whether this might be a transfer /** Schedules are processed through 3 different functions, any of which may set this flag */ bool m_suspectSchedule; /** * A holding area for template txs while we're waiting for the schedules */ QList m_templateList; /** Hold a list of suspect schedule ids for later processing? */ QList m_suspectList; /** * To hold message data till final report */ QMap m_messageList; /** * Internal utility functions */ QString createPayee(const QString&); // create a payee and return it's id QString createOrphanAccount(const QString&); // create unknown account and return the id QDate incrDate(QDate lastDate, unsigned char interval, unsigned int intervalCount); // for date calculations MyMoneyAccount checkConsistency(MyMoneyAccount& parent, MyMoneyAccount& child); // gnucash is sometimes TOO flexible void checkInvestmentOption(QString stockId); // implement user investment option void getPriceSource(MyMoneySecurity stock, QString gncSource); /** * This method loads all known currencies and saves them to the storage */ void loadAllCurrencies(); #endif // _GNCFILEANON }; #endif // MYMONEYGNCREADER_H diff --git a/kmymoney/plugins/sql/mymoneystoragesql_p.h b/kmymoney/plugins/sql/mymoneystoragesql_p.h index cfcfad5df..ded2855ee 100644 --- a/kmymoney/plugins/sql/mymoneystoragesql_p.h +++ b/kmymoney/plugins/sql/mymoneystoragesql_p.h @@ -1,2835 +1,2841 @@ /*************************************************************************** mymoneystoragesql.cpp --------------------- begin : 11 November 2005 copyright : (C) 2005 by Tony Bloomfield email : tonybloom@users.sourceforge.net : Fernando Vilas : Christian Dávid (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. * * * ***************************************************************************/ #ifndef MYMONEYSTORAGESQL_P_H #define MYMONEYSTORAGESQL_P_H #include "mymoneystoragesql.h" // ---------------------------------------------------------------------------- // System Includes #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // TODO: port KF5 (needed for payeeidentifier plugin) //#include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneystoragemgr.h" #include "kmymoneystorageplugin.h" #include "onlinejobadministration.h" #include "payeeidentifier/payeeidentifierloader.h" #include "onlinetasks/interfaces/tasks/onlinetask.h" #include "mymoneycostcenter.h" #include "mymoneyexception.h" #include "mymoneyinstitution.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneymoney.h" #include "mymoneyschedule.h" #include "mymoneypayee.h" #include "mymoneytag.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneytransactionfilter.h" #include "mymoneybudget.h" #include "mymoneyreport.h" #include "mymoneyprice.h" #include "mymoneyutils.h" #include "mymoneydbdef.h" #include "mymoneydbdriver.h" #include "payeeidentifier/payeeidentifierdata.h" #include "mymoneyenums.h" #include "mymoneystoragenames.h" using namespace eMyMoney; using namespace MyMoneyStandardAccounts; class FilterFail { public: explicit FilterFail(const MyMoneyTransactionFilter& filter) : m_filter(filter) {} inline bool operator()(const QPair& transactionPair) { return (*this)(transactionPair.second); } inline bool operator()(const MyMoneyTransaction& transaction) { return !m_filter.match(transaction); } private: MyMoneyTransactionFilter m_filter; }; //***************************************************************************** // Create a class to handle db transactions using scope // // Don't let the database object get destroyed while this object exists, // that would result in undefined behavior. class MyMoneyDbTransaction { public: explicit MyMoneyDbTransaction(MyMoneyStorageSql& db, const QString& name) : m_db(db), m_name(name) { db.startCommitUnit(name); } ~MyMoneyDbTransaction() { if (std::uncaught_exception()) { m_db.cancelCommitUnit(m_name); } else { try{ m_db.endCommitUnit(m_name); } catch(MyMoneyException&) { - m_db.cancelCommitUnit(m_name); + try { + m_db.cancelCommitUnit(m_name); + } catch (const MyMoneyException & e) { + qDebug() << e.what(); + } } } } private: MyMoneyStorageSql& m_db; QString m_name; }; /** * The MyMoneySqlQuery class is derived from QSqlQuery to provide * a way to adjust some queries based on database type and make * debugging easier by providing a place to put debug statements. */ class MyMoneySqlQuery : public QSqlQuery { public: explicit MyMoneySqlQuery(MyMoneyStorageSql* db = 0) : QSqlQuery(*db) { } virtual ~MyMoneySqlQuery() { } bool exec() { qDebug() << "start sql:" << lastQuery(); bool rc = QSqlQuery::exec(); qDebug() << "end sql:" << QSqlQuery::executedQuery(); qDebug() << "***Query returned:" << rc << ", row count:" << numRowsAffected(); return (rc); } bool exec(const QString & query) { qDebug() << "start sql:" << query; bool rc = QSqlQuery::exec(query); qDebug() << "end sql:" << QSqlQuery::executedQuery(); qDebug() << "***Query returned:" << rc << ", row count:" << numRowsAffected(); return rc; } bool prepare(const QString & query) { return (QSqlQuery::prepare(query)); } }; #define GETSTRING(a) query.value(a).toString() #define GETDATE(a) getDate(GETSTRING(a)) #define GETDATE_D(a) d->getDate(GETSTRING(a)) #define GETDATETIME(a) getDateTime(GETSTRING(a)) #define GETINT(a) query.value(a).toInt() #define GETULL(a) query.value(a).toULongLong() class MyMoneyStorageSqlPrivate { Q_DISABLE_COPY(MyMoneyStorageSqlPrivate) Q_DECLARE_PUBLIC(MyMoneyStorageSql) public: explicit MyMoneyStorageSqlPrivate(MyMoneyStorageSql* qq) : q_ptr(qq), m_dbVersion(0), m_storage(nullptr), m_loadAll(false), m_override(false), m_institutions(0), m_accounts(0), m_payees(0), m_tags(0), m_transactions(0), m_splits(0), m_securities(0), m_prices(0), m_currencies(0), m_schedules(0), m_reports(0), m_kvps(0), m_budgets(0), m_onlineJobs(0), m_payeeIdentifier(0), m_hiIdInstitutions(0), m_hiIdPayees(0), m_hiIdTags(0), m_hiIdAccounts(0), m_hiIdTransactions(0), m_hiIdSchedules(0), m_hiIdSecurities(0), m_hiIdReports(0), m_hiIdBudgets(0), m_hiIdOnlineJobs(0), m_hiIdPayeeIdentifier(0), + m_hiIdCostCenter(0), m_displayStatus(false), m_readingPrices(false), m_newDatabase(false), m_progressCallback(nullptr) { m_preferred.setReportAllSplits(false); } ~MyMoneyStorageSqlPrivate() { } /** * MyMoneyStorageSql get highest ID number from the database * * @return : highest ID number */ ulong highestNumberFromIdString(QString tableName, QString tableField, int prefixLength) { Q_Q(MyMoneyStorageSql); MyMoneyDbTransaction t(*q, Q_FUNC_INFO); QSqlQuery query(*q); if (!query.exec(m_driver->highestNumberFromIdString(tableName, tableField, prefixLength)) || !query.next()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("retrieving highest ID number"))); // krazy:exclude=crashy return query.value(0).toULongLong(); } /** * @name writeFromStorageMethods * @{ * These method write all data from m_storage to the database. Data which is * stored in the database is deleted. */ void writeUserInformation(); void writeInstitutions() { Q_Q(MyMoneyStorageSql); // first, get a list of what's on the database // anything not in the list needs to be inserted // anything which is will be updated and removed from the list // anything left over at the end will need to be deleted // this is an expensive and inconvenient way to do things; find a better way // one way would be to build the lists when reading the db // unfortunately this object does not persist between read and write // it would also be nice if we could tell which objects had been updated since we read them in QList dbList; QSqlQuery query(*q); query.prepare("SELECT id FROM kmmInstitutions;"); if (!query.exec()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "building Institution list")); // krazy:exclude=crashy while (query.next()) dbList.append(query.value(0).toString()); const QList list = m_storage->institutionList(); QList insertList; QList updateList; QSqlQuery query2(*q); query.prepare(m_db.m_tables["kmmInstitutions"].updateString()); query2.prepare(m_db.m_tables["kmmInstitutions"].insertString()); signalProgress(0, list.count(), "Writing Institutions..."); foreach (const MyMoneyInstitution& i, list) { if (dbList.contains(i.id())) { dbList.removeAll(i.id()); updateList << i; } else { insertList << i; } signalProgress(++m_institutions, 0); } if (!insertList.isEmpty()) writeInstitutionList(insertList, query2); if (!updateList.isEmpty()) writeInstitutionList(updateList, query); if (!dbList.isEmpty()) { QVariantList deleteList; // qCopy segfaults here, so do it with a hand-rolled loop foreach (const QString& it, dbList) { deleteList << it; } query.prepare("DELETE FROM kmmInstitutions WHERE id = :id"); query.bindValue(":id", deleteList); if (!query.execBatch()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "deleting Institution")); deleteKeyValuePairs("OFXSETTINGS", deleteList); } } void writePayees() { Q_Q(MyMoneyStorageSql); // first, get a list of what's on the database (see writeInstitutions) QSqlQuery query(*q); query.prepare("SELECT id FROM kmmPayees;"); if (!query.exec()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "building Payee list")); // krazy:exclude=crashy QList dbList; dbList.reserve(query.numRowsAffected()); while (query.next()) dbList.append(query.value(0).toString()); QList list = m_storage->payeeList(); MyMoneyPayee user(QString("USER"), m_storage->user()); list.prepend(user); signalProgress(0, list.count(), "Writing Payees..."); Q_FOREACH(const MyMoneyPayee& it, list) { if (dbList.contains(it.id())) { dbList.removeAll(it.id()); q->modifyPayee(it); } else { q->addPayee(it); } signalProgress(++m_payees, 0); } if (!dbList.isEmpty()) { QMap payeesToDelete = q->fetchPayees(dbList, true); Q_FOREACH(const MyMoneyPayee& payee, payeesToDelete) { q->removePayee(payee); } } } void writeTags() { Q_Q(MyMoneyStorageSql); // first, get a list of what's on the database (see writeInstitutions) QList dbList; QSqlQuery query(*q); query.prepare("SELECT id FROM kmmTags;"); if (!query.exec()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "building Tag list")); // krazy:exclude=crashy while (query.next()) dbList.append(query.value(0).toString()); QList list = m_storage->tagList(); signalProgress(0, list.count(), "Writing Tags..."); QSqlQuery query2(*q); query.prepare(m_db.m_tables["kmmTags"].updateString()); query2.prepare(m_db.m_tables["kmmTags"].insertString()); foreach (const MyMoneyTag& it, list) { if (dbList.contains(it.id())) { dbList.removeAll(it.id()); writeTag(it, query); } else { writeTag(it, query2); } signalProgress(++m_tags, 0); } if (!dbList.isEmpty()) { QVariantList deleteList; // qCopy segfaults here, so do it with a hand-rolled loop foreach (const QString& it, dbList) { deleteList << it; } query.prepare(m_db.m_tables["kmmTags"].deleteString()); query.bindValue(":id", deleteList); if (!query.execBatch()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "deleting Tag")); m_tags -= query.numRowsAffected(); } } void writeAccounts() { Q_Q(MyMoneyStorageSql); // first, get a list of what's on the database (see writeInstitutions) QList dbList; QSqlQuery query(*q); query.prepare("SELECT id FROM kmmAccounts;"); if (!query.exec()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "building Account list")); // krazy:exclude=crashy while (query.next()) dbList.append(query.value(0).toString()); QList list; m_storage->accountList(list); unsigned progress = 0; signalProgress(0, list.count(), "Writing Accounts..."); if (dbList.isEmpty()) { // new table, insert standard accounts query.prepare(m_db.m_tables["kmmAccounts"].insertString()); } else { query.prepare(m_db.m_tables["kmmAccounts"].updateString()); } // Attempt to write the standard accounts. For an empty db, this will fail. try { QList stdList; stdList << m_storage->asset(); stdList << m_storage->liability(); stdList << m_storage->expense(); stdList << m_storage->income(); stdList << m_storage->equity(); writeAccountList(stdList, query); m_accounts += stdList.size(); } catch (const MyMoneyException &) { // If the above failed, assume that the database is empty and create // the standard accounts by hand before writing them. MyMoneyAccount acc_l; acc_l.setAccountType(Account::Type::Liability); acc_l.setName("Liability"); MyMoneyAccount liability(stdAccNames[stdAccLiability], acc_l); MyMoneyAccount acc_a; acc_a.setAccountType(Account::Type::Asset); acc_a.setName("Asset"); MyMoneyAccount asset(stdAccNames[stdAccAsset], acc_a); MyMoneyAccount acc_e; acc_e.setAccountType(Account::Type::Expense); acc_e.setName("Expense"); MyMoneyAccount expense(stdAccNames[stdAccExpense], acc_e); MyMoneyAccount acc_i; acc_i.setAccountType(Account::Type::Income); acc_i.setName("Income"); MyMoneyAccount income(stdAccNames[stdAccIncome], acc_i); MyMoneyAccount acc_q; acc_q.setAccountType(Account::Type::Equity); acc_q.setName("Equity"); MyMoneyAccount equity(stdAccNames[stdAccEquity], acc_q); QList stdList; stdList << asset; stdList << liability; stdList << expense; stdList << income; stdList << equity; writeAccountList(stdList, query); m_accounts += stdList.size(); } QSqlQuery query2(*q); query.prepare(m_db.m_tables["kmmAccounts"].updateString()); query2.prepare(m_db.m_tables["kmmAccounts"].insertString()); QList updateList; QList insertList; // Update the accounts that exist; insert the ones that do not. foreach (const MyMoneyAccount& it, list) { m_transactionCountMap[it.id()] = m_storage->transactionCount(it.id()); if (dbList.contains(it.id())) { dbList.removeAll(it.id()); updateList << it; } else { insertList << it; } signalProgress(++progress, 0); ++m_accounts; } writeAccountList(updateList, query); writeAccountList(insertList, query2); // Delete the accounts that are in the db but no longer in memory. if (!dbList.isEmpty()) { QVariantList kvpList; query.prepare("DELETE FROM kmmAccounts WHERE id = :id"); foreach (const QString& it, dbList) { if (!m_storage->isStandardAccount(it)) { kvpList << it; } } query.bindValue(":id", kvpList); if (!query.execBatch()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "deleting Account")); deleteKeyValuePairs("ACCOUNT", kvpList); deleteKeyValuePairs("ONLINEBANKING", kvpList); } } void writeTransactions() { Q_Q(MyMoneyStorageSql); // first, get a list of what's on the database (see writeInstitutions) QList dbList; QSqlQuery query(*q); query.prepare("SELECT id FROM kmmTransactions WHERE txType = 'N';"); if (!query.exec()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "building Transaction list")); // krazy:exclude=crashy while (query.next()) dbList.append(query.value(0).toString()); MyMoneyTransactionFilter filter; filter.setReportAllSplits(false); QList list; m_storage->transactionList(list, filter); signalProgress(0, list.count(), "Writing Transactions..."); QList::ConstIterator it; QSqlQuery q2(*q); query.prepare(m_db.m_tables["kmmTransactions"].updateString()); q2.prepare(m_db.m_tables["kmmTransactions"].insertString()); foreach (const MyMoneyTransaction& it, list) { if (dbList.contains(it.id())) { dbList.removeAll(it.id()); writeTransaction(it.id(), it, query, "N"); } else { writeTransaction(it.id(), it, q2, "N"); } signalProgress(++m_transactions, 0); } if (!dbList.isEmpty()) { foreach (const QString& it, dbList) { deleteTransaction(it); } } } void writeSchedules() { Q_Q(MyMoneyStorageSql); // first, get a list of what's on the database (see writeInstitutions) QList dbList; QSqlQuery query(*q); query.prepare("SELECT id FROM kmmSchedules;"); if (!query.exec()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "building Schedule list")); // krazy:exclude=crashy while (query.next()) dbList.append(query.value(0).toString()); const auto list = m_storage->scheduleList(QString(), Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, QDate(), QDate(), false); QSqlQuery query2(*q); //TODO: find a way to prepare the queries outside of the loop. writeSchedule() // modifies the query passed to it, so they have to be re-prepared every pass. signalProgress(0, list.count(), "Writing Schedules..."); foreach (const MyMoneySchedule& it, list) { query.prepare(m_db.m_tables["kmmSchedules"].updateString()); query2.prepare(m_db.m_tables["kmmSchedules"].insertString()); bool insert = true; if (dbList.contains(it.id())) { dbList.removeAll(it.id()); insert = false; writeSchedule(it, query, insert); } else { writeSchedule(it, query2, insert); } signalProgress(++m_schedules, 0); } if (!dbList.isEmpty()) { foreach (const QString& it, dbList) { deleteSchedule(it); } } } void writeSecurities() { Q_Q(MyMoneyStorageSql); // first, get a list of what's on the database (see writeInstitutions) QList dbList; QSqlQuery query(*q); QSqlQuery query2(*q); query.prepare("SELECT id FROM kmmSecurities;"); if (!query.exec()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "building security list")); // krazy:exclude=crashy while (query.next()) dbList.append(query.value(0).toString()); const QList securityList = m_storage->securityList(); signalProgress(0, securityList.count(), "Writing Securities..."); query.prepare(m_db.m_tables["kmmSecurities"].updateString()); query2.prepare(m_db.m_tables["kmmSecurities"].insertString()); foreach (const MyMoneySecurity& it, securityList) { if (dbList.contains(it.id())) { dbList.removeAll(it.id()); writeSecurity(it, query); } else { writeSecurity(it, query2); } signalProgress(++m_securities, 0); } if (!dbList.isEmpty()) { QVariantList idList; // qCopy segfaults here, so do it with a hand-rolled loop foreach (const QString& it, dbList) { idList << it; } query.prepare("DELETE FROM kmmSecurities WHERE id = :id"); query2.prepare("DELETE FROM kmmPrices WHERE fromId = :id OR toId = :id"); query.bindValue(":id", idList); if (!query.execBatch()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "deleting Security")); query2.bindValue(":fromId", idList); query2.bindValue(":toId", idList); if (!query2.execBatch()) throw MYMONEYEXCEPTION(buildError(query2, Q_FUNC_INFO, "deleting Security")); deleteKeyValuePairs("SECURITY", idList); } } void writePrices() { Q_Q(MyMoneyStorageSql); // due to difficulties in matching and determining deletes // easiest way is to delete all and re-insert QSqlQuery query(*q); query.prepare("DELETE FROM kmmPrices"); if (!query.exec()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("deleting Prices"))); // krazy:exclude=crashy m_prices = 0; const MyMoneyPriceList list = m_storage->priceList(); signalProgress(0, list.count(), "Writing Prices..."); MyMoneyPriceList::ConstIterator it; for (it = list.constBegin(); it != list.constEnd(); ++it) { writePricePair(*it); } } void writeCurrencies() { Q_Q(MyMoneyStorageSql); // first, get a list of what's on the database (see writeInstitutions) QList dbList; QSqlQuery query(*q); QSqlQuery query2(*q); query.prepare("SELECT ISOCode FROM kmmCurrencies;"); if (!query.exec()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "building Currency list")); // krazy:exclude=crashy while (query.next()) dbList.append(query.value(0).toString()); const QList currencyList = m_storage->currencyList(); signalProgress(0, currencyList.count(), "Writing Currencies..."); query.prepare(m_db.m_tables["kmmCurrencies"].updateString()); query2.prepare(m_db.m_tables["kmmCurrencies"].insertString()); foreach (const MyMoneySecurity& it, currencyList) { if (dbList.contains(it.id())) { dbList.removeAll(it.id()); writeCurrency(it, query); } else { writeCurrency(it, query2); } signalProgress(++m_currencies, 0); } if (!dbList.isEmpty()) { QVariantList isoCodeList; query.prepare("DELETE FROM kmmCurrencies WHERE ISOCode = :ISOCode"); // qCopy segfaults here, so do it with a hand-rolled loop foreach (const QString& it, dbList) { isoCodeList << it; } query.bindValue(":ISOCode", isoCodeList); if (!query.execBatch()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "deleting Currency")); } } void writeFileInfo() { Q_Q(MyMoneyStorageSql); // we have no real way of knowing when these change, so re-write them every time QVariantList kvpList; kvpList << ""; QList > pairs; pairs << m_storage->pairs(); deleteKeyValuePairs("STORAGE", kvpList); writeKeyValuePairs("STORAGE", kvpList, pairs); QSqlQuery query(*q); query.prepare("SELECT count(*) FROM kmmFileInfo;"); if (!query.exec() || !query.next()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "checking fileinfo")); // krazy:exclude=crashy if (query.value(0).toInt() == 0) { // Cannot use "INSERT INTO kmmFileInfo DEFAULT VALUES;" because it is not supported by MySQL query.prepare(QLatin1String("INSERT INTO kmmFileInfo (version) VALUES (null);")); if (!query.exec()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "inserting fileinfo")); // krazy:exclude=crashy } query.prepare(QLatin1String( "UPDATE kmmFileInfo SET " "version = :version, " "fixLevel = :fixLevel, " "created = :created, " "lastModified = :lastModified, " "baseCurrency = :baseCurrency, " "dateRangeStart = :dateRangeStart, " "dateRangeEnd = :dateRangeEnd, " "hiInstitutionId = :hiInstitutionId, " "hiPayeeId = :hiPayeeId, " "hiTagId = :hiTagId, " "hiAccountId = :hiAccountId, " "hiTransactionId = :hiTransactionId, " "hiScheduleId = :hiScheduleId, " "hiSecurityId = :hiSecurityId, " "hiReportId = :hiReportId, " "hiBudgetId = :hiBudgetId, " "hiOnlineJobId = :hiOnlineJobId, " "hiPayeeIdentifierId = :hiPayeeIdentifierId, " "encryptData = :encryptData, " "updateInProgress = :updateInProgress, " "logonUser = :logonUser, " "logonAt = :logonAt, " //! @todo The following updates are for backwards compatibility only //! remove backwards compatibility in a later version "institutions = :institutions, " "accounts = :accounts, " "payees = :payees, " "tags = :tags, " "transactions = :transactions, " "splits = :splits, " "securities = :securities, " "prices = :prices, " "currencies = :currencies, " "schedules = :schedules, " "reports = :reports, " "kvps = :kvps, " "budgets = :budgets; " ) ); query.bindValue(":version", m_dbVersion); query.bindValue(":fixLevel", m_storage->fileFixVersion()); query.bindValue(":created", m_storage->creationDate().toString(Qt::ISODate)); //q.bindValue(":lastModified", m_storage->lastModificationDate().toString(Qt::ISODate)); query.bindValue(":lastModified", QDate::currentDate().toString(Qt::ISODate)); query.bindValue(":baseCurrency", m_storage->pairs()["kmm-baseCurrency"]); query.bindValue(":dateRangeStart", QDate()); query.bindValue(":dateRangeEnd", QDate()); //FIXME: This modifies all m_ used in this function. // Sometimes the memory has been updated. // Should most of these be tracked in a view? // Variables actually needed are: version, fileFixVersion, creationDate, // baseCurrency, encryption, update info, and logon info. //try { //readFileInfo(); //} catch (...) { //q->startCommitUnit(Q_FUNC_INFO); //} //! @todo The following bindings are for backwards compatibility only //! remove backwards compatibility in a later version query.bindValue(":hiInstitutionId", QVariant::fromValue(q->getNextInstitutionId())); query.bindValue(":hiPayeeId", QVariant::fromValue(q->getNextPayeeId())); query.bindValue(":hiTagId", QVariant::fromValue(q->getNextTagId())); query.bindValue(":hiAccountId", QVariant::fromValue(q->getNextAccountId())); query.bindValue(":hiTransactionId", QVariant::fromValue(q->getNextTransactionId())); query.bindValue(":hiScheduleId", QVariant::fromValue(q->getNextScheduleId())); query.bindValue(":hiSecurityId", QVariant::fromValue(q->getNextSecurityId())); query.bindValue(":hiReportId", QVariant::fromValue(q->getNextReportId())); query.bindValue(":hiBudgetId", QVariant::fromValue(q->getNextBudgetId())); query.bindValue(":hiOnlineJobId", QVariant::fromValue(q->getNextOnlineJobId())); query.bindValue(":hiPayeeIdentifierId", QVariant::fromValue(q->getNextPayeeIdentifierId())); query.bindValue(":encryptData", m_encryptData); query.bindValue(":updateInProgress", "N"); query.bindValue(":logonUser", m_logonUser); query.bindValue(":logonAt", m_logonAt.toString(Qt::ISODate)); //! @todo The following bindings are for backwards compatibility only //! remove backwards compatibility in a later version query.bindValue(":institutions", (unsigned long long) m_institutions); query.bindValue(":accounts", (unsigned long long) m_accounts); query.bindValue(":payees", (unsigned long long) m_payees); query.bindValue(":tags", (unsigned long long) m_tags); query.bindValue(":transactions", (unsigned long long) m_transactions); query.bindValue(":splits", (unsigned long long) m_splits); query.bindValue(":securities", (unsigned long long) m_securities); query.bindValue(":prices", (unsigned long long) m_prices); query.bindValue(":currencies", (unsigned long long) m_currencies); query.bindValue(":schedules", (unsigned long long) m_schedules); query.bindValue(":reports", (unsigned long long) m_reports); query.bindValue(":kvps", (unsigned long long) m_kvps); query.bindValue(":budgets", (unsigned long long) m_budgets); if (!query.exec()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("writing FileInfo"))); // krazy:exclude=crashy } void writeReports() { Q_Q(MyMoneyStorageSql); // first, get a list of what's on the database (see writeInstitutions) QList dbList; QSqlQuery query(*q); QSqlQuery query2(*q); query.prepare("SELECT id FROM kmmReportConfig;"); if (!query.exec()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "building Report list")); // krazy:exclude=crashy while (query.next()) dbList.append(query.value(0).toString()); QList list = m_storage->reportList(); signalProgress(0, list.count(), "Writing Reports..."); query.prepare(m_db.m_tables["kmmReportConfig"].updateString()); query2.prepare(m_db.m_tables["kmmReportConfig"].insertString()); foreach (const MyMoneyReport& it, list) { if (dbList.contains(it.id())) { dbList.removeAll(it.id()); writeReport(it, query); } else { writeReport(it, query2); } signalProgress(++m_reports, 0); } if (!dbList.isEmpty()) { QVariantList idList; query.prepare("DELETE FROM kmmReportConfig WHERE id = :id"); // qCopy segfaults here, so do it with a hand-rolled loop foreach (const QString& it, dbList) { idList << it; } query.bindValue(":id", idList); if (!query.execBatch()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "deleting Report")); } } void writeBudgets() { Q_Q(MyMoneyStorageSql); // first, get a list of what's on the database (see writeInstitutions) QList dbList; QSqlQuery query(*q); QSqlQuery query2(*q); query.prepare("SELECT name FROM kmmBudgetConfig;"); if (!query.exec()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "building Budget list")); // krazy:exclude=crashy while (query.next()) dbList.append(query.value(0).toString()); QList list = m_storage->budgetList(); signalProgress(0, list.count(), "Writing Budgets..."); query.prepare(m_db.m_tables["kmmBudgetConfig"].updateString()); query2.prepare(m_db.m_tables["kmmBudgetConfig"].insertString()); foreach (const MyMoneyBudget& it, list) { if (dbList.contains(it.name())) { dbList.removeAll(it.name()); writeBudget(it, query); } else { writeBudget(it, query2); } signalProgress(++m_budgets, 0); } if (!dbList.isEmpty()) { QVariantList idList; query.prepare("DELETE FROM kmmBudgetConfig WHERE id = :id"); // qCopy segfaults here, so do it with a hand-rolled loop foreach (const QString& it, dbList) { idList << it; } query.bindValue(":name", idList); if (!query.execBatch()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "deleting Budget")); } } void writeOnlineJobs() { Q_Q(MyMoneyStorageSql); QSqlQuery query(*q); if (!query.exec("DELETE FROM kmmOnlineJobs;")) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QLatin1String("Clean kmmOnlineJobs table"))); const QList jobs(m_storage->onlineJobList()); signalProgress(0, jobs.count(), i18n("Inserting online jobs.")); // Create list for onlineJobs which failed and the reason therefor QList > failedJobs; int jobCount = 0; foreach (const onlineJob& job, jobs) { try { q->addOnlineJob(job); } catch (MyMoneyException& e) { // Do not save e as this may point to an inherited class failedJobs.append(QPair(job, e.what())); qDebug() << "Failed to save onlineJob" << job.id() << "Reson:" << e.what(); } signalProgress(++jobCount, 0); } if (!failedJobs.isEmpty()) { /** @todo Improve error message */ throw MYMONEYEXCEPTION(i18np("Could not save one onlineJob.", "Could not save %1 onlineJobs.", failedJobs.count())); } } /** @} */ /** * @name writeMethods * @{ * These methods bind the data fields of MyMoneyObjects to a given query and execute the query. * This is helpfull as the query has usually an update and a insert format. */ void writeInstitutionList(const QList& iList, QSqlQuery& query) { QVariantList idList; QVariantList nameList; QVariantList managerList; QVariantList routingCodeList; QVariantList addressStreetList; QVariantList addressCityList; QVariantList addressZipcodeList; QVariantList telephoneList; QList > kvpPairsList; foreach (const MyMoneyInstitution& i, iList) { idList << i.id(); nameList << i.name(); managerList << i.manager(); routingCodeList << i.sortcode(); addressStreetList << i.street(); addressCityList << i.city(); addressZipcodeList << i.postcode(); telephoneList << i.telephone(); kvpPairsList << i.pairs(); } query.bindValue(":id", idList); query.bindValue(":name", nameList); query.bindValue(":manager", managerList); query.bindValue(":routingCode", routingCodeList); query.bindValue(":addressStreet", addressStreetList); query.bindValue(":addressCity", addressCityList); query.bindValue(":addressZipcode", addressZipcodeList); query.bindValue(":telephone", telephoneList); if (!query.execBatch()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("writing Institution"))); writeKeyValuePairs("OFXSETTINGS", idList, kvpPairsList); // Set m_hiIdInstitutions to 0 to force recalculation the next time it is requested m_hiIdInstitutions = 0; } void writePayee(const MyMoneyPayee& p, QSqlQuery& query, bool isUserInfo = false) { if (isUserInfo) { query.bindValue(":id", "USER"); } else { query.bindValue(":id", p.id()); } query.bindValue(":name", p.name()); query.bindValue(":reference", p.reference()); query.bindValue(":email", p.email()); query.bindValue(":addressStreet", p.address()); query.bindValue(":addressCity", p.city()); query.bindValue(":addressZipcode", p.postcode()); query.bindValue(":addressState", p.state()); query.bindValue(":telephone", p.telephone()); query.bindValue(":notes", p.notes()); query.bindValue(":defaultAccountId", p.defaultAccountId()); bool ignoreCase; QString matchKeys; MyMoneyPayee::payeeMatchType type = p.matchData(ignoreCase, matchKeys); query.bindValue(":matchData", static_cast(type)); if (ignoreCase) query.bindValue(":matchIgnoreCase", "Y"); else query.bindValue(":matchIgnoreCase", "N"); query.bindValue(":matchKeys", matchKeys); if (!query.exec()) // krazy:exclude=crashy throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("writing Payee"))); // krazy:exclude=crashy if (!isUserInfo) m_hiIdPayees = 0; } void writeTag(const MyMoneyTag& ta, QSqlQuery& query) { query.bindValue(":id", ta.id()); query.bindValue(":name", ta.name()); query.bindValue(":tagColor", ta.tagColor().name()); if (ta.isClosed()) query.bindValue(":closed", "Y"); else query.bindValue(":closed", "N"); query.bindValue(":notes", ta.notes()); if (!query.exec()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("writing Tag"))); // krazy:exclude=crashy m_hiIdTags = 0; } void writeAccountList(const QList& accList, QSqlQuery& query) { //MyMoneyMoney balance = m_storagePtr->balance(acc.id(), QDate()); QVariantList idList; QVariantList institutionIdList; QVariantList parentIdList; QVariantList lastReconciledList; QVariantList lastModifiedList; QVariantList openingDateList; QVariantList accountNumberList; QVariantList accountTypeList; QVariantList accountTypeStringList; QVariantList isStockAccountList; QVariantList accountNameList; QVariantList descriptionList; QVariantList currencyIdList; QVariantList balanceList; QVariantList balanceFormattedList; QVariantList transactionCountList; QList > pairs; QList > onlineBankingPairs; foreach (const MyMoneyAccount& a, accList) { idList << a.id(); institutionIdList << a.institutionId(); parentIdList << a.parentAccountId(); if (a.lastReconciliationDate() == QDate()) lastReconciledList << a.lastReconciliationDate(); else lastReconciledList << a.lastReconciliationDate().toString(Qt::ISODate); lastModifiedList << a.lastModified(); if (a.openingDate() == QDate()) openingDateList << a.openingDate(); else openingDateList << a.openingDate().toString(Qt::ISODate); accountNumberList << a.number(); accountTypeList << (int)a.accountType(); accountTypeStringList << MyMoneyAccount::accountTypeToString(a.accountType()); if (a.accountType() == Account::Type::Stock) isStockAccountList << "Y"; else isStockAccountList << "N"; accountNameList << a.name(); descriptionList << a.description(); currencyIdList << a.currencyId(); // This section attempts to get the balance from the database, if possible // That way, the balance fields are kept in sync. If that fails, then // It is assumed that the account actually knows its correct balance. //FIXME: Using exceptions for branching always feels like a kludge. // Look for a better way. try { MyMoneyMoney bal = m_storage->balance(a.id(), QDate()); balanceList << bal.toString(); balanceFormattedList << bal.formatMoney("", -1, false); } catch (const MyMoneyException &) { balanceList << a.balance().toString(); balanceFormattedList << a.balance().formatMoney("", -1, false); } transactionCountList << quint64(m_transactionCountMap[a.id()]); //MMAccount inherits from KVPContainer AND has a KVPContainer member //so handle both pairs << a.pairs(); onlineBankingPairs << a.onlineBankingSettings().pairs(); } query.bindValue(":id", idList); query.bindValue(":institutionId", institutionIdList); query.bindValue(":parentId", parentIdList); query.bindValue(":lastReconciled", lastReconciledList); query.bindValue(":lastModified", lastModifiedList); query.bindValue(":openingDate", openingDateList); query.bindValue(":accountNumber", accountNumberList); query.bindValue(":accountType", accountTypeList); query.bindValue(":accountTypeString", accountTypeStringList); query.bindValue(":isStockAccount", isStockAccountList); query.bindValue(":accountName", accountNameList); query.bindValue(":description", descriptionList); query.bindValue(":currencyId", currencyIdList); query.bindValue(":balance", balanceList); query.bindValue(":balanceFormatted", balanceFormattedList); query.bindValue(":transactionCount", transactionCountList); if (!query.execBatch()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("writing Account"))); //Add in Key-Value Pairs for accounts. writeKeyValuePairs("ACCOUNT", idList, pairs); writeKeyValuePairs("ONLINEBANKING", idList, onlineBankingPairs); m_hiIdAccounts = 0; } void writeTransaction(const QString& txId, const MyMoneyTransaction& tx, QSqlQuery& query, const QString& type) { query.bindValue(":id", txId); query.bindValue(":txType", type); query.bindValue(":postDate", tx.postDate().toString(Qt::ISODate)); query.bindValue(":memo", tx.memo()); query.bindValue(":entryDate", tx.entryDate().toString(Qt::ISODate)); query.bindValue(":currencyId", tx.commodity()); query.bindValue(":bankId", tx.bankID()); if (!query.exec()) // krazy:exclude=crashy throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("writing Transaction"))); // krazy:exclude=crashy m_txPostDate = tx.postDate(); // FIXME: TEMP till Tom puts date in split object QList splitList = tx.splits(); writeSplits(txId, type, splitList); //Add in Key-Value Pairs for transactions. QVariantList idList; idList << txId; deleteKeyValuePairs("TRANSACTION", idList); QList > pairs; pairs << tx.pairs(); writeKeyValuePairs("TRANSACTION", idList, pairs); m_hiIdTransactions = 0; } void writeSplits(const QString& txId, const QString& type, const QList& splitList) { Q_Q(MyMoneyStorageSql); // first, get a list of what's on the database (see writeInstitutions) QList dbList; QList insertList; QList updateList; QList insertIdList; QList updateIdList; QSqlQuery query(*q); query.prepare("SELECT splitId FROM kmmSplits where transactionId = :id;"); query.bindValue(":id", txId); if (!query.exec()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "building Split list")); // krazy:exclude=crashy while (query.next()) dbList.append(query.value(0).toUInt()); QList::ConstIterator it; uint i = 0; QSqlQuery query2(*q); query.prepare(m_db.m_tables["kmmSplits"].updateString()); query2.prepare(m_db.m_tables["kmmSplits"].insertString()); for (it = splitList.constBegin(), i = 0; it != splitList.constEnd(); ++it, ++i) { if (dbList.contains(i)) { dbList.removeAll(i); updateList << *it; updateIdList << i; } else { ++m_splits; insertList << *it; insertIdList << i; } } if (!insertList.isEmpty()) { writeSplitList(txId, insertList, type, insertIdList, query2); writeTagSplitsList(txId, insertList, insertIdList); } if (!updateList.isEmpty()) { writeSplitList(txId, updateList, type, updateIdList, query); deleteTagSplitsList(txId, updateIdList); writeTagSplitsList(txId, updateList, updateIdList); } if (!dbList.isEmpty()) { QVector txIdList(dbList.count(), txId); QVariantList splitIdList; query.prepare("DELETE FROM kmmSplits WHERE transactionId = :txId AND splitId = :splitId"); // qCopy segfaults here, so do it with a hand-rolled loop foreach (int it, dbList) { splitIdList << it; } query.bindValue(":txId", txIdList.toList()); query.bindValue(":splitId", splitIdList); if (!query.execBatch()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "deleting Splits")); } } void writeTagSplitsList (const QString& txId, const QList& splitList, const QList& splitIdList) { Q_Q(MyMoneyStorageSql); MyMoneyDbTransaction t(*q, Q_FUNC_INFO); QVariantList tagIdList; QVariantList txIdList; QVariantList splitIdList_TagSplits; QVariantList tagSplitsIdList; int i = 0, l = 0; foreach (const MyMoneySplit& s, splitList) { for (l = 0; l < s.tagIdList().size(); ++l) { tagIdList << s.tagIdList()[l]; splitIdList_TagSplits << splitIdList[i]; txIdList << txId; } i++; } QSqlQuery query(*q); query.prepare(m_db.m_tables["kmmTagSplits"].insertString()); query.bindValue(":tagId", tagIdList); query.bindValue(":splitId", splitIdList_TagSplits); query.bindValue(":transactionId", txIdList); if (!query.execBatch()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("writing tagSplits"))); } void writeSplitList (const QString& txId, const QList& splitList, const QString& type, const QList& splitIdList, QSqlQuery& query) { QVariantList txIdList; QVariantList typeList; QVariantList payeeIdList; QVariantList reconcileDateList; QVariantList actionList; QVariantList reconcileFlagList; QVariantList valueList; QVariantList valueFormattedList; QVariantList sharesList; QVariantList sharesFormattedList; QVariantList priceList; QVariantList priceFormattedList; QVariantList memoList; QVariantList accountIdList; QVariantList costCenterIdList; QVariantList checkNumberList; QVariantList postDateList; QVariantList bankIdList; QVariantList kvpIdList; QList > kvpPairsList; int i = 0; foreach (const MyMoneySplit& s, splitList) { txIdList << txId; typeList << type; payeeIdList << s.payeeId(); if (s.reconcileDate() == QDate()) reconcileDateList << s.reconcileDate(); else reconcileDateList << s.reconcileDate().toString(Qt::ISODate); actionList << s.action(); reconcileFlagList << (int)s.reconcileFlag(); valueList << s.value().toString(); valueFormattedList << s.value().formatMoney("", -1, false).replace(QChar(','), QChar('.')); sharesList << s.shares().toString(); MyMoneyAccount acc = m_storage->account(s.accountId()); MyMoneySecurity sec = m_storage->security(acc.currencyId()); sharesFormattedList << s.price(). formatMoney("", MyMoneyMoney::denomToPrec(sec.smallestAccountFraction()), false). replace(QChar(','), QChar('.')); MyMoneyMoney price = s.actualPrice(); if (!price.isZero()) { priceList << price.toString(); priceFormattedList << price.formatMoney ("", sec.pricePrecision(), false) .replace(QChar(','), QChar('.')); } else { priceList << QString(); priceFormattedList << QString(); } memoList << s.memo(); accountIdList << s.accountId(); costCenterIdList << s.costCenterId(); checkNumberList << s.number(); postDateList << m_txPostDate.toString(Qt::ISODate); // FIXME: when Tom puts date into split object bankIdList << s.bankID(); kvpIdList << QString(txId + QString::number(splitIdList[i])); kvpPairsList << s.pairs(); ++i; } query.bindValue(":transactionId", txIdList); query.bindValue(":txType", typeList); QVariantList iList; // qCopy segfaults here, so do it with a hand-rolled loop foreach (int it_s, splitIdList) { iList << it_s; } query.bindValue(":splitId", iList); query.bindValue(":payeeId", payeeIdList); query.bindValue(":reconcileDate", reconcileDateList); query.bindValue(":action", actionList); query.bindValue(":reconcileFlag", reconcileFlagList); query.bindValue(":value", valueList); query.bindValue(":valueFormatted", valueFormattedList); query.bindValue(":shares", sharesList); query.bindValue(":sharesFormatted", sharesFormattedList); query.bindValue(":price", priceList); query.bindValue(":priceFormatted", priceFormattedList); query.bindValue(":memo", memoList); query.bindValue(":accountId", accountIdList); query.bindValue(":costCenterId", costCenterIdList); query.bindValue(":checkNumber", checkNumberList); query.bindValue(":postDate", postDateList); query.bindValue(":bankId", bankIdList); if (!query.execBatch()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("writing Split"))); deleteKeyValuePairs("SPLIT", kvpIdList); writeKeyValuePairs("SPLIT", kvpIdList, kvpPairsList); } void writeSchedule(const MyMoneySchedule& sch, QSqlQuery& query, bool insert) { query.bindValue(":id", sch.id()); query.bindValue(":name", sch.name()); query.bindValue(":type", (int)sch.type()); query.bindValue(":typeString", MyMoneySchedule::scheduleTypeToString(sch.type())); query.bindValue(":occurence", (int)sch.occurrencePeriod()); // krazy:exclude=spelling query.bindValue(":occurenceMultiplier", sch.occurrenceMultiplier()); // krazy:exclude=spelling query.bindValue(":occurenceString", sch.occurrenceToString()); // krazy:exclude=spelling query.bindValue(":paymentType", (int)sch.paymentType()); query.bindValue(":paymentTypeString", MyMoneySchedule::paymentMethodToString(sch.paymentType())); query.bindValue(":startDate", sch.startDate().toString(Qt::ISODate)); query.bindValue(":endDate", sch.endDate().toString(Qt::ISODate)); if (sch.isFixed()) { query.bindValue(":fixed", "Y"); } else { query.bindValue(":fixed", "N"); } if (sch.lastDayInMonth()) { query.bindValue(":lastDayInMonth", "Y"); } else { query.bindValue(":lastDayInMonth", "N"); } if (sch.autoEnter()) { query.bindValue(":autoEnter", "Y"); } else { query.bindValue(":autoEnter", "N"); } query.bindValue(":lastPayment", sch.lastPayment()); query.bindValue(":nextPaymentDue", sch.nextDueDate().toString(Qt::ISODate)); query.bindValue(":weekendOption", (int)sch.weekendOption()); query.bindValue(":weekendOptionString", MyMoneySchedule::weekendOptionToString(sch.weekendOption())); if (!query.exec()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("writing Schedules"))); // krazy:exclude=crashy //store the payment history for this scheduled task. //easiest way is to delete all and re-insert; it's not a high use table query.prepare("DELETE FROM kmmSchedulePaymentHistory WHERE schedId = :id;"); query.bindValue(":id", sch.id()); if (!query.exec()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("deleting Schedule Payment History"))); // krazy:exclude=crashy query.prepare(m_db.m_tables["kmmSchedulePaymentHistory"].insertString()); foreach (const QDate& it, sch.recordedPayments()) { query.bindValue(":schedId", sch.id()); query.bindValue(":payDate", it.toString(Qt::ISODate)); if (!query.exec()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("writing Schedule Payment History"))); // krazy:exclude=crashy } //store the transaction data for this task. if (!insert) { query.prepare(m_db.m_tables["kmmTransactions"].updateString()); } else { query.prepare(m_db.m_tables["kmmTransactions"].insertString()); } writeTransaction(sch.id(), sch.transaction(), query, "S"); //FIXME: enable when schedules have KVPs. //Add in Key-Value Pairs for transactions. //deleteKeyValuePairs("SCHEDULE", sch.id()); //writeKeyValuePairs("SCHEDULE", sch.id(), sch.pairs()); } void writeSecurity(const MyMoneySecurity& security, QSqlQuery& query) { query.bindValue(":id", security.id()); query.bindValue(":name", security.name()); query.bindValue(":symbol", security.tradingSymbol()); query.bindValue(":type", static_cast(security.securityType())); query.bindValue(":typeString", MyMoneySecurity::securityTypeToString(security.securityType())); query.bindValue(":roundingMethod", static_cast(security.roundingMethod())); query.bindValue(":smallestAccountFraction", security.smallestAccountFraction()); query.bindValue(":pricePrecision", security.pricePrecision()); query.bindValue(":tradingCurrency", security.tradingCurrency()); query.bindValue(":tradingMarket", security.tradingMarket()); if (!query.exec()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("writing Securities"))); // krazy:exclude=crashy //Add in Key-Value Pairs for security QVariantList idList; idList << security.id(); QList > pairs; pairs << security.pairs(); writeKeyValuePairs("SECURITY", idList, pairs); m_hiIdSecurities = 0; } void writePricePair(const MyMoneyPriceEntries& p) { MyMoneyPriceEntries::ConstIterator it; for (it = p.constBegin(); it != p.constEnd(); ++it) { writePrice(*it); signalProgress(++m_prices, 0); } } void writePrice(const MyMoneyPrice& p) { Q_Q(MyMoneyStorageSql); QSqlQuery query(*q); query.prepare(m_db.m_tables["kmmPrices"].insertString()); query.bindValue(":fromId", p.from()); query.bindValue(":toId", p.to()); query.bindValue(":priceDate", p.date().toString(Qt::ISODate)); query.bindValue(":price", p.rate(QString()).toString()); query.bindValue(":priceFormatted", p.rate(QString()).formatMoney("", 2)); query.bindValue(":priceSource", p.source()); if (!query.exec()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("writing Prices"))); // krazy:exclude=crashy } void writeCurrency(const MyMoneySecurity& currency, QSqlQuery& query) { query.bindValue(":ISOcode", currency.id()); query.bindValue(":name", currency.name()); query.bindValue(":type", static_cast(currency.securityType())); query.bindValue(":typeString", MyMoneySecurity::securityTypeToString(currency.securityType())); // writing the symbol as three short ints is a PITA, but the // problem is that database drivers have incompatible ways of declaring UTF8 QString symbol = currency.tradingSymbol() + " "; const ushort* symutf = symbol.utf16(); //int ix = 0; //while (x[ix] != '\0') qDebug() << "symbol" << symbol << "char" << ix << "=" << x[ix++]; //q.bindValue(":symbol1", symbol.mid(0,1).unicode()->unicode()); //q.bindValue(":symbol2", symbol.mid(1,1).unicode()->unicode()); //q.bindValue(":symbol3", symbol.mid(2,1).unicode()->unicode()); query.bindValue(":symbol1", symutf[0]); query.bindValue(":symbol2", symutf[1]); query.bindValue(":symbol3", symutf[2]); query.bindValue(":symbolString", symbol); query.bindValue(":smallestCashFraction", currency.smallestCashFraction()); query.bindValue(":smallestAccountFraction", currency.smallestAccountFraction()); query.bindValue(":pricePrecision", currency.pricePrecision()); if (!query.exec()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("writing Currencies"))); // krazy:exclude=crashy } void writeReport(const MyMoneyReport& rep, QSqlQuery& query) { QDomDocument d; // create a dummy XML document QDomElement e = d.createElement("REPORTS"); d.appendChild(e); rep.writeXML(d, e); // write the XML to document query.bindValue(":id", rep.id()); query.bindValue(":name", rep.name()); query.bindValue(":XML", d.toString()); if (!query.exec()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("writing Reports"))); // krazy:exclude=crashy } void writeBudget(const MyMoneyBudget& bud, QSqlQuery& query) { QDomDocument d; // create a dummy XML document QDomElement e = d.createElement("BUDGETS"); d.appendChild(e); bud.writeXML(d, e); // write the XML to document query.bindValue(":id", bud.id()); query.bindValue(":name", bud.name()); query.bindValue(":start", bud.budgetStart()); query.bindValue(":XML", d.toString()); if (!query.exec()) // krazy:exclude=crashy throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("writing Budgets"))); // krazy:exclude=crashy } void writeKeyValuePairs(const QString& kvpType, const QVariantList& kvpId, const QList >& pairs) { Q_Q(MyMoneyStorageSql); if (pairs.empty()) return; QVariantList type; QVariantList id; QVariantList key; QVariantList value; int pairCount = 0; for (int i = 0; i < kvpId.size(); ++i) { QMap::ConstIterator it; for (it = pairs[i].constBegin(); it != pairs[i].constEnd(); ++it) { type << kvpType; id << kvpId[i]; key << it.key(); value << it.value(); } pairCount += pairs[i].size(); } QSqlQuery query(*q); query.prepare(m_db.m_tables["kmmKeyValuePairs"].insertString()); query.bindValue(":kvpType", type); query.bindValue(":kvpId", id); query.bindValue(":kvpKey", key); query.bindValue(":kvpData", value); if (!query.execBatch()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("writing KVP"))); m_kvps += pairCount; } void writeOnlineJob(const onlineJob& job, QSqlQuery& query) { Q_ASSERT(job.id().startsWith('O')); query.bindValue(":id", job.id()); query.bindValue(":type", job.taskIid()); query.bindValue(":jobSend", job.sendDate()); query.bindValue(":bankAnswerDate", job.bankAnswerDate()); switch (job.bankAnswerState()) { case onlineJob::acceptedByBank: query.bindValue(":state", QLatin1String("acceptedByBank")); break; case onlineJob::rejectedByBank: query.bindValue(":state", QLatin1String("rejectedByBank")); break; case onlineJob::abortedByUser: query.bindValue(":state", QLatin1String("abortedByUser")); break; case onlineJob::sendingError: query.bindValue(":state", QLatin1String("sendingError")); break; case onlineJob::noBankAnswer: default: query.bindValue(":state", QLatin1String("noBankAnswer")); } query.bindValue(":locked", QVariant::fromValue(job.isLocked() ? QLatin1String("Y") : QLatin1String("N"))); } void writePayeeIdentifier(const payeeIdentifier& pid, QSqlQuery& query) { query.bindValue(":id", pid.idString()); query.bindValue(":type", pid.iid()); if (!query.exec()) { // krazy:exclude=crashy qWarning() << buildError(query, Q_FUNC_INFO, QString("modifying payeeIdentifier")); throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("modifying payeeIdentifier"))); // krazy:exclude=crashy } } /** @} */ /** * @name readMethods * @{ */ void readFileInfo() { Q_Q(MyMoneyStorageSql); signalProgress(0, 1, QObject::tr("Loading file information...")); QSqlQuery query(*q); query.prepare( "SELECT " " created, lastModified, " " encryptData, logonUser, logonAt, " " (SELECT count(*) FROM kmmInstitutions) AS institutions, " " (SELECT count(*) from kmmAccounts) AS accounts, " " (SELECT count(*) FROM kmmCurrencies) AS currencies, " " (SELECT count(*) FROM kmmPayees) AS payees, " " (SELECT count(*) FROM kmmTags) AS tags, " " (SELECT count(*) FROM kmmTransactions) AS transactions, " " (SELECT count(*) FROM kmmSplits) AS splits, " " (SELECT count(*) FROM kmmSecurities) AS securities, " " (SELECT count(*) FROM kmmCurrencies) AS currencies, " " (SELECT count(*) FROM kmmSchedules) AS schedules, " " (SELECT count(*) FROM kmmPrices) AS prices, " " (SELECT count(*) FROM kmmKeyValuePairs) AS kvps, " " (SELECT count(*) FROM kmmReportConfig) AS reports, " " (SELECT count(*) FROM kmmBudgetConfig) AS budgets, " " (SELECT count(*) FROM kmmOnlineJobs) AS onlineJobs, " " (SELECT count(*) FROM kmmPayeeIdentifier) AS payeeIdentifier " "FROM kmmFileInfo;" ); if (!query.exec()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("reading FileInfo"))); // krazy:exclude=crashy if (!query.next()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("retrieving FileInfo"))); QSqlRecord rec = query.record(); m_storage->setCreationDate(GETDATE(rec.indexOf("created"))); m_storage->setLastModificationDate(GETDATE(rec.indexOf("lastModified"))); m_institutions = (ulong) GETULL(rec.indexOf("institutions")); m_accounts = (ulong) GETULL(rec.indexOf("accounts")); m_payees = (ulong) GETULL(rec.indexOf("payees")); m_tags = (ulong) GETULL(rec.indexOf("tags")); m_transactions = (ulong) GETULL(rec.indexOf("transactions")); m_splits = (ulong) GETULL(rec.indexOf("splits")); m_securities = (ulong) GETULL(rec.indexOf("securities")); m_currencies = (ulong) GETULL(rec.indexOf("currencies")); m_schedules = (ulong) GETULL(rec.indexOf("schedules")); m_prices = (ulong) GETULL(rec.indexOf("prices")); m_kvps = (ulong) GETULL(rec.indexOf("kvps")); m_reports = (ulong) GETULL(rec.indexOf("reports")); m_budgets = (ulong) GETULL(rec.indexOf("budgets")); m_onlineJobs = (ulong) GETULL(rec.indexOf("onlineJobs")); m_payeeIdentifier = (ulong) GETULL(rec.indexOf("payeeIdentifier")); m_encryptData = GETSTRING(rec.indexOf("encryptData")); m_logonUser = GETSTRING(rec.indexOf("logonUser")); m_logonAt = GETDATETIME(rec.indexOf("logonAt")); signalProgress(1, 0); m_storage->setPairs(readKeyValuePairs("STORAGE", QString("")).pairs()); } void readLogonData(); void readUserInformation(); void readInstitutions() { Q_Q(MyMoneyStorageSql); try { QMap iList = q->fetchInstitutions(); m_storage->loadInstitutions(iList); readFileInfo(); } catch (const MyMoneyException &) { throw; } } void readAccounts() { Q_Q(MyMoneyStorageSql); m_storage->loadAccounts(q->fetchAccounts()); } void readTransactions(const QString& tidList, const QString& dateClause) { Q_Q(MyMoneyStorageSql); try { m_storage->loadTransactions(q->fetchTransactions(tidList, dateClause)); } catch (const MyMoneyException &) { throw; } } void readTransactions() { readTransactions(QString(), QString()); } void readSplit(MyMoneySplit& s, const QSqlQuery& query) const { Q_Q(const MyMoneyStorageSql); // Set these up as statics, since the field numbers should not change // during execution. static const MyMoneyDbTable& t = m_db.m_tables["kmmSplits"]; static const int splitIdCol = t.fieldNumber("splitId"); static const int transactionIdCol = t.fieldNumber("transactionId"); static const int payeeIdCol = t.fieldNumber("payeeId"); static const int reconcileDateCol = t.fieldNumber("reconcileDate"); static const int actionCol = t.fieldNumber("action"); static const int reconcileFlagCol = t.fieldNumber("reconcileFlag"); static const int valueCol = t.fieldNumber("value"); static const int sharesCol = t.fieldNumber("shares"); static const int priceCol = t.fieldNumber("price"); static const int memoCol = t.fieldNumber("memo"); static const int accountIdCol = t.fieldNumber("accountId"); static const int costCenterIdCol = t.fieldNumber("costCenterId"); static const int checkNumberCol = t.fieldNumber("checkNumber"); // static const int postDateCol = t.fieldNumber("postDate"); // FIXME - when Tom puts date into split object static const int bankIdCol = t.fieldNumber("bankId"); s.clearId(); QList tagIdList; QSqlQuery query1(*const_cast (q)); query1.prepare("SELECT tagId from kmmTagSplits where splitId = :id and transactionId = :transactionId"); query1.bindValue(":id", GETSTRING(splitIdCol)); query1.bindValue(":transactionId", GETSTRING(transactionIdCol)); if (!query1.exec()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("reading tagId in Split"))); // krazy:exclude=crashy while (query1.next()) tagIdList << query1.value(0).toString(); s.setTagIdList(tagIdList); s.setPayeeId(GETSTRING(payeeIdCol)); s.setReconcileDate(GETDATE(reconcileDateCol)); s.setAction(GETSTRING(actionCol)); s.setReconcileFlag(static_cast(GETINT(reconcileFlagCol))); s.setValue(MyMoneyMoney(MyMoneyUtils::QStringEmpty(GETSTRING(valueCol)))); s.setShares(MyMoneyMoney(MyMoneyUtils::QStringEmpty(GETSTRING(sharesCol)))); s.setPrice(MyMoneyMoney(MyMoneyUtils::QStringEmpty(GETSTRING(priceCol)))); s.setMemo(GETSTRING(memoCol)); s.setAccountId(GETSTRING(accountIdCol)); s.setCostCenterId(GETSTRING(costCenterIdCol)); s.setNumber(GETSTRING(checkNumberCol)); //s.setPostDate(GETDATETIME(postDateCol)); // FIXME - when Tom puts date into split object s.setBankID(GETSTRING(bankIdCol)); return; } const MyMoneyKeyValueContainer readKeyValuePairs(const QString& kvpType, const QString& kvpId) const { Q_Q(const MyMoneyStorageSql); MyMoneyKeyValueContainer list; QSqlQuery query(*const_cast (q)); query.prepare("SELECT kvpKey, kvpData from kmmKeyValuePairs where kvpType = :type and kvpId = :id;"); query.bindValue(":type", kvpType); query.bindValue(":id", kvpId); if (!query.exec()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("reading Kvp for %1 %2").arg(kvpType) // krazy:exclude=crashy .arg(kvpId))); while (query.next()) list.setValue(query.value(0).toString(), query.value(1).toString()); return (list); } const QHash readKeyValuePairs(const QString& kvpType, const QStringList& kvpIdList) const { Q_Q(const MyMoneyStorageSql); QHash retval; QSqlQuery query(*const_cast (q)); QString idList; if (!kvpIdList.empty()) { idList = QString(" and kvpId IN ('%1')").arg(kvpIdList.join("', '")); } QString sQuery = QString("SELECT kvpId, kvpKey, kvpData from kmmKeyValuePairs where kvpType = :type %1 order by kvpId;").arg(idList); query.prepare(sQuery); query.bindValue(":type", kvpType); if (!query.exec()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("reading Kvp List for %1").arg(kvpType))); // krazy:exclude=crashy // Reserve enough space for all values. retval.reserve(kvpIdList.size()); // The loop below is designed to limit the number of calls to // QHash::operator[] in order to speed up calls to this function. This // assumes that QString::operator== is faster. /* if (q.next()) { QString oldkey = q.value(0).toString(); MyMoneyKeyValueContainer& kvpc = retval[oldkey]; kvpc.setValue(q.value(1).toString(), q.value(2).toString()); while (q.next()) { if (q.value(0).toString() != oldkey) { oldkey = q.value(0).toString(); kvpc = retval[oldkey]; } kvpc.setValue(q.value(1).toString(), q.value(2).toString()); } } */ while (query.next()) { retval[query.value(0).toString()].setValue(query.value(1).toString(), query.value(2).toString()); } return (retval); } void readSchedules() { Q_Q(MyMoneyStorageSql); try { m_storage->loadSchedules(q->fetchSchedules()); } catch (const MyMoneyException &) { throw; } } void readSecurities() { Q_Q(MyMoneyStorageSql); try { m_storage->loadSecurities(q->fetchSecurities()); } catch (const MyMoneyException &) { throw; } } void readPrices() { // try { // m_storage->addPrice(MyMoneyPrice(from, to, date, rate, source)); // } catch (const MyMoneyException &) { // throw; // } } void readCurrencies() { Q_Q(MyMoneyStorageSql); try { m_storage->loadCurrencies(q->fetchCurrencies()); } catch (const MyMoneyException &) { throw; } } void readReports() { Q_Q(MyMoneyStorageSql); try { m_storage->loadReports(q->fetchReports()); } catch (const MyMoneyException &) { throw; } } void readBudgets() { Q_Q(MyMoneyStorageSql); m_storage->loadBudgets(q->fetchBudgets()); } /** @} */ void deleteTransaction(const QString& id) { Q_Q(MyMoneyStorageSql); MyMoneyDbTransaction t(*q, Q_FUNC_INFO); QSqlQuery query(*q); QVariantList idList; idList << id; query.prepare("DELETE FROM kmmSplits WHERE transactionId = :transactionId;"); query.bindValue(":transactionId", idList); if (!query.execBatch()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "deleting Splits")); query.prepare("DELETE FROM kmmKeyValuePairs WHERE kvpType = 'SPLIT' " "AND kvpId LIKE '?%'"); query.bindValue(1, idList); if (!query.execBatch()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "deleting Splits KVP")); m_splits -= query.numRowsAffected(); deleteKeyValuePairs("TRANSACTION", idList); query.prepare(m_db.m_tables["kmmTransactions"].deleteString()); query.bindValue(":id", idList); if (!query.execBatch()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "deleting Transaction")); } void deleteTagSplitsList(const QString& txId, const QList& splitIdList) { Q_Q(MyMoneyStorageSql); MyMoneyDbTransaction t(*q, Q_FUNC_INFO); QVariantList iList; QVariantList transactionIdList; // qCopy segfaults here, so do it with a hand-rolled loop foreach (int it_s, splitIdList) { iList << it_s; transactionIdList << txId; } QSqlQuery query(*q); query.prepare("DELETE FROM kmmTagSplits WHERE transactionId = :transactionId AND splitId = :splitId"); query.bindValue(":splitId", iList); query.bindValue(":transactionId", transactionIdList); if (!query.execBatch()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("deleting tagSplits"))); } void deleteSchedule(const QString& id) { Q_Q(MyMoneyStorageSql); deleteTransaction(id); QSqlQuery query(*q); query.prepare("DELETE FROM kmmSchedulePaymentHistory WHERE schedId = :id"); query.bindValue(":id", id); if (!query.exec()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "deleting Schedule Payment History")); // krazy:exclude=crashy query.prepare(m_db.m_tables["kmmSchedules"].deleteString()); query.bindValue(":id", id); if (!query.exec()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "deleting Schedule")); // krazy:exclude=crashy //FIXME: enable when schedules have KVPs. //deleteKeyValuePairs("SCHEDULE", id); } void deleteKeyValuePairs(const QString& kvpType, const QVariantList& idList) { Q_Q(MyMoneyStorageSql); QSqlQuery query(*q); query.prepare("DELETE FROM kmmKeyValuePairs WHERE kvpType = :kvpType AND kvpId = :kvpId;"); QVariantList typeList; for (int i = 0; i < idList.size(); ++i) { typeList << kvpType; } query.bindValue(":kvpType", typeList); query.bindValue(":kvpId", idList); if (!query.execBatch()) { QString idString; for (int i = 0; i < idList.size(); ++i) { idString.append(idList[i].toString() + ' '); } throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("deleting kvp for %1 %2").arg(kvpType).arg(idString))); } m_kvps -= query.numRowsAffected(); } ulong calcHighId(ulong i, const QString& id) { QString nid = id; ulong high = (ulong) nid.remove(QRegExp("[A-Z]*")).toULongLong(); return std::max(high, i); } void setVersion(const QString& version); int splitState(const TransactionFilter::State& state) const { auto rc = (int)Split::State::NotReconciled; switch (state) { default: case TransactionFilter::State::NotReconciled: break; case TransactionFilter::State::Cleared: rc = (int)Split::State::Cleared; break; case TransactionFilter::State::Reconciled: rc = (int)Split::State::Reconciled; break; case TransactionFilter::State::Frozen: rc = (int)Split::State::Frozen; break; } return rc; } QDate getDate(const QString& date) const { return (date.isNull() ? QDate() : QDate::fromString(date, Qt::ISODate)); } QDateTime getDateTime(const QString& date) const { return (date.isNull() ? QDateTime() : QDateTime::fromString(date, Qt::ISODate)); } bool fileExists(const QString& dbName) { QFile f(dbName); if (!f.exists()) { m_error = i18n("SQLite file %1 does not exist", dbName); return (false); } return (true); } /** @brief a function to build a comprehensive error message for an SQL error */ QString& buildError(const QSqlQuery& query, const QString& function, const QString& messageb) const { Q_Q(const MyMoneyStorageSql); return (buildError(query, function, messageb, q)); } QString& buildError(const QSqlQuery& query, const QString& function, const QString& message, const QSqlDatabase* db) const { Q_Q(const MyMoneyStorageSql); QString s = QString("Error in function %1 : %2").arg(function).arg(message); s += QString("\nDriver = %1, Host = %2, User = %3, Database = %4") .arg(db->driverName()).arg(db->hostName()).arg(db->userName()).arg(db->databaseName()); QSqlError e = db->lastError(); s += QString("\nDriver Error: %1").arg(e.driverText()); s += QString("\nDatabase Error No %1: %2").arg(e.number()).arg(e.databaseText()); s += QString("\nText: %1").arg(e.text()); s += QString("\nError type %1").arg(e.type()); e = query.lastError(); s += QString("\nExecuted: %1").arg(query.executedQuery()); s += QString("\nQuery error No %1: %2").arg(e.number()).arg(e.text()); s += QString("\nError type %1").arg(e.type()); const_cast (q)->d_func()->m_error = s; qDebug("%s", qPrintable(s)); const_cast (q)->cancelCommitUnit(function); return (const_cast (q)->d_func()->m_error); } /** * MyMoneyStorageSql create database * * @param url pseudo-URL of database to be opened * * @return true - creation successful * @return false - could not create * */ bool createDatabase(const QUrl &url) { Q_Q(MyMoneyStorageSql); int rc = true; if (!m_driver->requiresCreation()) return(true); // not needed for sqlite QString dbName = url.path().right(url.path().length() - 1); // remove separator slash if (!m_driver->canAutocreate()) { m_error = i18n("Automatic database creation for type %1 is not currently implemented.\n" "Please create database %2 manually", q->driverName(), dbName); return (false); } // create the database (only works for mysql and postgre at present) { // for this code block, see QSqlDatabase API re removeDatabase QSqlDatabase maindb = QSqlDatabase::addDatabase(q->driverName(), "main"); maindb.setDatabaseName(m_driver->defaultDbName()); maindb.setHostName(url.host()); maindb.setUserName(url.userName()); maindb.setPassword(url.password()); if (!maindb.open()) { throw MYMONEYEXCEPTION(QString("opening database %1 in function %2") .arg(maindb.databaseName()).arg(Q_FUNC_INFO)); } else { QSqlQuery qm(maindb); QString qs = m_driver->createDbString(dbName) + ';'; if (!qm.exec(qs)) { // krazy:exclude=crashy buildError(qm, Q_FUNC_INFO, i18n("Error in create database %1; do you have create permissions?", dbName), &maindb); rc = false; } maindb.close(); } } QSqlDatabase::removeDatabase("main"); return (rc); } int upgradeDb() { Q_Q(MyMoneyStorageSql); //signalProgress(0, 1, QObject::tr("Upgrading database...")); QSqlQuery query(*q); query.prepare("SELECT version FROM kmmFileInfo;"); if (!query.exec() || !query.next()) { // krazy:exclude=crashy if (!m_newDatabase) { buildError(query, Q_FUNC_INFO, "Error retrieving file info (version)"); return(1); } else { m_dbVersion = m_db.currentVersion(); m_storage->setFileFixVersion(m_storage->currentFixVersion()); QSqlQuery query2(*q); query2.prepare("UPDATE kmmFileInfo SET version = :version, \ fixLevel = :fixLevel;"); query2.bindValue(":version", m_dbVersion); query2.bindValue(":fixLevel", m_storage->currentFixVersion()); if (!query2.exec()) { // krazy:exclude=crashy buildError(query2, Q_FUNC_INFO, "Error updating file info(version)"); return(1); } return (0); } } // prior to dbv6, 'version' format was 'dbversion.fixLevel+1' // as of dbv6, these are separate fields QString version = query.value(0).toString(); if (version.contains('.')) { m_dbVersion = query.value(0).toString().section('.', 0, 0).toUInt(); m_storage->setFileFixVersion(query.value(0).toString().section('.', 1, 1).toUInt() - 1); } else { m_dbVersion = version.toUInt(); query.prepare("SELECT fixLevel FROM kmmFileInfo;"); if (!query.exec() || !query.next()) { // krazy:exclude=crashy buildError(query, Q_FUNC_INFO, "Error retrieving file info (fixLevel)"); return(1); } m_storage->setFileFixVersion(query.value(0).toUInt()); } if (m_dbVersion == m_db.currentVersion()) return 0; int rc = 0; // Drop VIEWs QStringList lowerTables = tables(QSql::AllTables); for (QStringList::iterator i = lowerTables.begin(); i != lowerTables.end(); ++i) { (*i) = (*i).toLower(); } for (QMap::ConstIterator tt = m_db.viewBegin(); tt != m_db.viewEnd(); ++tt) { if (lowerTables.contains(tt.key().toLower())) { if (!query.exec("DROP VIEW " + tt.value().name() + ';')) // krazy:exclude=crashy throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("dropping view %1").arg(tt.key()))); } } while ((m_dbVersion < m_db.currentVersion()) && (rc == 0)) { switch (m_dbVersion) { case 0: if ((rc = upgradeToV1()) != 0) return (1); ++m_dbVersion; break; case 1: if ((rc = upgradeToV2()) != 0) return (1); ++m_dbVersion; break; case 2: if ((rc = upgradeToV3()) != 0) return (1); ++m_dbVersion; break; case 3: if ((rc = upgradeToV4()) != 0) return (1); ++m_dbVersion; break; case 4: if ((rc = upgradeToV5()) != 0) return (1); ++m_dbVersion; break; case 5: if ((rc = upgradeToV6()) != 0) return (1); ++m_dbVersion; break; case 6: if ((rc = upgradeToV7()) != 0) return (1); ++m_dbVersion; break; case 7: if ((rc = upgradeToV8()) != 0) return (1); ++m_dbVersion; break; case 8: if ((rc = upgradeToV9()) != 0) return (1); ++m_dbVersion; break; case 9: if ((rc = upgradeToV10()) != 0) return (1); ++m_dbVersion; break; case 10: if ((rc = upgradeToV11()) != 0) return (1); ++m_dbVersion; break; case 11: if ((rc = upgradeToV12()) != 0) return (1); ++m_dbVersion; break; default: qWarning("Unknown version number in database - %d", m_dbVersion); } } // restore VIEWs lowerTables = tables(QSql::AllTables); for (QStringList::iterator i = lowerTables.begin(); i != lowerTables.end(); ++i) { (*i) = (*i).toLower(); } for (QMap::ConstIterator tt = m_db.viewBegin(); tt != m_db.viewEnd(); ++tt) { if (!lowerTables.contains(tt.key().toLower())) { if (!query.exec(tt.value().createString())) // krazy:exclude=crashy throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("creating view %1").arg(tt.key()))); } } // write updated version to DB //setVersion(QString("%1.%2").arg(m_dbVersion).arg(m_minorVersion)) query.prepare(QString("UPDATE kmmFileInfo SET version = :version;")); query.bindValue(":version", m_dbVersion); if (!query.exec()) { // krazy:exclude=crashy buildError(query, Q_FUNC_INFO, "Error updating db version"); return (1); } //signalProgress(-1,-1); return (0); } int upgradeToV1() { Q_Q(MyMoneyStorageSql); MyMoneyDbTransaction t(*q, Q_FUNC_INFO); QSqlQuery query(*q); // change kmmSplits pkey to (transactionId, splitId) if (!query.exec("ALTER TABLE kmmSplits ADD PRIMARY KEY (transactionId, splitId);")) { // krazy:exclude=crashy buildError(query, Q_FUNC_INFO, "Error updating kmmSplits pkey"); return (1); } // change kmmSplits alter checkNumber varchar(32) if (!query.exec(m_db.m_tables["kmmSplits"].modifyColumnString(m_driver, "checkNumber", // krazy:exclude=crashy MyMoneyDbColumn("checkNumber", "varchar(32)")))) { buildError(query, Q_FUNC_INFO, "Error expanding kmmSplits.checkNumber"); return (1); } // change kmmSplits add postDate datetime if (!alterTable(m_db.m_tables["kmmSplits"], m_dbVersion)) return (1); // initialize it to same value as transaction (do it the long way round) query.prepare("SELECT id, postDate FROM kmmTransactions WHERE txType = 'N';"); if (!query.exec()) { // krazy:exclude=crashy buildError(query, Q_FUNC_INFO, "Error priming kmmSplits.postDate"); return (1); } QMap tids; while (query.next()) tids[query.value(0).toString()] = query.value(1).toDateTime(); QMap::ConstIterator it; for (it = tids.constBegin(); it != tids.constEnd(); ++it) { query.prepare("UPDATE kmmSplits SET postDate=:postDate WHERE transactionId = :id;"); query.bindValue(":postDate", it.value().toString(Qt::ISODate)); query.bindValue(":id", it.key()); if (!query.exec()) { // krazy:exclude=crashy buildError(query, Q_FUNC_INFO, "priming kmmSplits.postDate"); return(1); } } // add index to kmmKeyValuePairs to (kvpType,kvpId) QStringList list; list << "kvpType" << "kvpId"; if (!query.exec(MyMoneyDbIndex("kmmKeyValuePairs", "kmmKVPtype_id", list, false).generateDDL(m_driver) + ';')) { buildError(query, Q_FUNC_INFO, "Error adding kmmKeyValuePairs index"); return (1); } // add index to kmmSplits to (accountId, txType) list.clear(); list << "accountId" << "txType"; if (!query.exec(MyMoneyDbIndex("kmmSplits", "kmmSplitsaccount_type", list, false).generateDDL(m_driver) + ';')) { buildError(query, Q_FUNC_INFO, "Error adding kmmSplits index"); return (1); } // change kmmSchedulePaymentHistory pkey to (schedId, payDate) if (!query.exec("ALTER TABLE kmmSchedulePaymentHistory ADD PRIMARY KEY (schedId, payDate);")) { buildError(query, Q_FUNC_INFO, "Error updating kmmSchedulePaymentHistory pkey"); return (1); } // change kmmPrices pkey to (fromId, toId, priceDate) if (!query.exec("ALTER TABLE kmmPrices ADD PRIMARY KEY (fromId, toId, priceDate);")) { buildError(query, Q_FUNC_INFO, "Error updating kmmPrices pkey"); return (1); } // change kmmReportConfig pkey to (name) // There wasn't one previously, so no need to drop it. if (!query.exec("ALTER TABLE kmmReportConfig ADD PRIMARY KEY (name);")) { buildError(query, Q_FUNC_INFO, "Error updating kmmReportConfig pkey"); return (1); } // change kmmFileInfo add budgets, hiBudgetId unsigned bigint // change kmmFileInfo add logonUser // change kmmFileInfo add logonAt datetime if (!alterTable(m_db.m_tables["kmmFileInfo"], m_dbVersion)) return (1); // change kmmAccounts add transactionCount unsigned bigint as last field if (!alterTable(m_db.m_tables["kmmAccounts"], m_dbVersion)) return (1); // calculate the transaction counts. the application logic defines an account's tx count // in such a way as to count multiple splits in a tx which reference the same account as one. // this is the only way I can think of to do this which will work in sqlite too. // inefficient, but it only gets done once... // get a list of all accounts so we'll get a zero value for those without txs query.prepare("SELECT id FROM kmmAccounts"); if (!query.exec()) { // krazy:exclude=crashy buildError(query, Q_FUNC_INFO, "Error retrieving accounts for transaction counting"); return(1); } while (query.next()) { m_transactionCountMap[query.value(0).toString()] = 0; } query.prepare("SELECT accountId, transactionId FROM kmmSplits WHERE txType = 'N' ORDER BY 1, 2"); if (!query.exec()) { // krazy:exclude=crashy buildError(query, Q_FUNC_INFO, "Error retrieving splits for transaction counting"); return(1); } QString lastAcc, lastTx; while (query.next()) { QString thisAcc = query.value(0).toString(); QString thisTx = query.value(1).toString(); if ((thisAcc != lastAcc) || (thisTx != lastTx)) ++m_transactionCountMap[thisAcc]; lastAcc = thisAcc; lastTx = thisTx; } QHash::ConstIterator itm; query.prepare("UPDATE kmmAccounts SET transactionCount = :txCount WHERE id = :id;"); for (itm = m_transactionCountMap.constBegin(); itm != m_transactionCountMap.constEnd(); ++itm) { query.bindValue(":txCount", QString::number(itm.value())); query.bindValue(":id", itm.key()); if (!query.exec()) { // krazy:exclude=crashy buildError(query, Q_FUNC_INFO, "Error updating transaction count"); return (1); } } m_transactionCountMap.clear(); return (0); } int upgradeToV2() { Q_Q(MyMoneyStorageSql); MyMoneyDbTransaction t(*q, Q_FUNC_INFO); QSqlQuery query(*q); // change kmmSplits add price, priceFormatted fields if (!alterTable(m_db.m_tables["kmmSplits"], m_dbVersion)) return (1); return (0); } int upgradeToV3() { Q_Q(MyMoneyStorageSql); MyMoneyDbTransaction t(*q, Q_FUNC_INFO); QSqlQuery query(*q); // kmmSchedules - add occurrenceMultiplier // The default value is given here to populate the column. if (!query.exec("ALTER TABLE kmmSchedules ADD COLUMN " + MyMoneyDbIntColumn("occurenceMultiplier", MyMoneyDbIntColumn::SMALL, false, false, true) .generateDDL(m_driver) + " DEFAULT 0;")) { buildError(query, Q_FUNC_INFO, "Error adding kmmSchedules.occurenceMultiplier"); return (1); } //The default is less than any useful value, so as each schedule is hit, it will update //itself to the appropriate value. return 0; } int upgradeToV4() { Q_Q(MyMoneyStorageSql); MyMoneyDbTransaction t(*q, Q_FUNC_INFO); QSqlQuery query(*q); // kmmSplits - add index on transactionId + splitId QStringList list; list << "transactionId" << "splitId"; if (!query.exec(MyMoneyDbIndex("kmmSplits", "kmmTx_Split", list, false).generateDDL(m_driver) + ';')) { buildError(query, Q_FUNC_INFO, "Error adding kmmSplits index on (transactionId, splitId)"); return (1); } return 0; } int upgradeToV5() { Q_Q(MyMoneyStorageSql); MyMoneyDbTransaction dbtrans(*q, Q_FUNC_INFO); QSqlQuery query(*q); // kmmSplits - add bankId if (!alterTable(m_db.m_tables["kmmSplits"], m_dbVersion)) return (1); //kmmPayees - add columns "notes" "defaultAccountId" "matchData" "matchIgnoreCase" "matchKeys"; if (!alterTable(m_db.m_tables["kmmPayees"], m_dbVersion)) return (1); // kmmReportConfig - drop primary key on name since duplicate names are allowed if (!alterTable(m_db.m_tables["kmmReportConfig"], m_dbVersion)) return (1); //} return 0; } int upgradeToV6() { Q_Q(MyMoneyStorageSql); q->startCommitUnit(Q_FUNC_INFO); QSqlQuery query(*q); // kmmFileInfo - add fixLevel if (!alterTable(m_db.m_tables["kmmFileInfo"], m_dbVersion)) return (1); // upgrade Mysql to InnoDB transaction-safe engine // the following is not a good way to test for mysql - think of a better way if (!m_driver->tableOptionString().isEmpty()) { for (QMap::ConstIterator tt = m_db.tableBegin(); tt != m_db.tableEnd(); ++tt) { if (!query.exec(QString("ALTER TABLE %1 ENGINE = InnoDB;").arg(tt.value().name()))) { buildError(query, Q_FUNC_INFO, "Error updating to InnoDB"); return (1); } } } // the alterTable function really doesn't work too well // with adding a new column which is also to be primary key // so add the column first if (!query.exec("ALTER TABLE kmmReportConfig ADD COLUMN " + MyMoneyDbColumn("id", "varchar(32)").generateDDL(m_driver) + ';')) { buildError(query, Q_FUNC_INFO, "adding id to report table"); return(1); } QMap reportList = q->fetchReports(); // the V5 database allowed lots of duplicate reports with no // way to distinguish between them. The fetchReports call // will have effectively removed all duplicates // so we now delete from the db and re-write them if (!query.exec("DELETE FROM kmmReportConfig;")) { buildError(query, Q_FUNC_INFO, "Error deleting reports"); return (1); } // add unique id to reports table if (!alterTable(m_db.m_tables["kmmReportConfig"], m_dbVersion)) return(1); QMap::const_iterator it_r; for (it_r = reportList.constBegin(); it_r != reportList.constEnd(); ++it_r) { MyMoneyReport r = *it_r; query.prepare(m_db.m_tables["kmmReportConfig"].insertString()); writeReport(*it_r, query); } q->endCommitUnit(Q_FUNC_INFO); return 0; } int upgradeToV7() { Q_Q(MyMoneyStorageSql); MyMoneyDbTransaction dbtrans(*q, Q_FUNC_INFO); QSqlQuery query(*q); // add tags support // kmmFileInfo - add tags and hiTagId if (!alterTable(m_db.m_tables["kmmFileInfo"], m_dbVersion)) return (1); m_tags = 0; return 0; } int upgradeToV8() { Q_Q(MyMoneyStorageSql); MyMoneyDbTransaction dbtrans(*q, Q_FUNC_INFO); // Added onlineJobs and payeeIdentifier if (!alterTable(m_db.m_tables["kmmFileInfo"], m_dbVersion)) return (1); return 0; } int upgradeToV9() { Q_Q(MyMoneyStorageSql); MyMoneyDbTransaction dbtrans(*q, Q_FUNC_INFO); QSqlQuery query(*q); // kmmSplits - add bankId if (!alterTable(m_db.m_tables["kmmSplits"], m_dbVersion)) return (1); return 0; } int upgradeToV10() { Q_Q(MyMoneyStorageSql); MyMoneyDbTransaction dbtrans(*q, Q_FUNC_INFO); QSqlQuery query(*q); if (!alterTable(m_db.m_tables["kmmPayeesPayeeIdentifier"], m_dbVersion)) return (1); if (!alterTable(m_db.m_tables["kmmAccountsPayeeIdentifier"], m_dbVersion)) return (1); return 0; } int upgradeToV11() { Q_Q(MyMoneyStorageSql); MyMoneyDbTransaction dbtrans(*q, Q_FUNC_INFO); QSqlQuery query(*q); // add column roundingMethodCol to kmmSecurities if (!alterTable(m_db.m_tables["kmmSecurities"], m_dbVersion)) return 1; // add column pricePrecision to kmmCurrencies if (!alterTable(m_db.m_tables["kmmCurrencies"], m_dbVersion)) return 1; return 0; } int upgradeToV12() { Q_Q(MyMoneyStorageSql); MyMoneyDbTransaction dbtrans(*q, Q_FUNC_INFO); switch(haveColumnInTable(QLatin1String("kmmSchedules"), QLatin1String("lastDayInMonth"))) { case -1: return 1; case 1: // column exists, nothing to do break; case 0: // need update of kmmSchedules // add column lastDayInMonth. Simply redo the update for 10 .. 11 if (!alterTable(m_db.m_tables["kmmSchedules"], m_dbVersion-1)) return 1; break; } switch(haveColumnInTable(QLatin1String("kmmSecurities"), QLatin1String("roundingMethod"))) { case -1: return 1; case 1: // column exists, nothing to do break; case 0: // need update of kmmSecurities and kmmCurrencies // add column roundingMethodCol to kmmSecurities. Simply redo the update for 10 .. 11 if (!alterTable(m_db.m_tables["kmmSecurities"], m_dbVersion-1)) return 1; // add column pricePrecision to kmmCurrencies. Simply redo the update for 10 .. 11 if (!alterTable(m_db.m_tables["kmmCurrencies"], m_dbVersion-1)) return 1; break; } return 0; } int createTables() { Q_Q(MyMoneyStorageSql); // check tables, create if required // convert everything to lower case, since SQL standard is case insensitive // table and column names (when not delimited), but some DBMSs disagree. QStringList lowerTables = tables(QSql::AllTables); for (QStringList::iterator i = lowerTables.begin(); i != lowerTables.end(); ++i) { (*i) = (*i).toLower(); } for (QMap::ConstIterator tt = m_db.tableBegin(); tt != m_db.tableEnd(); ++tt) { if (!lowerTables.contains(tt.key().toLower())) { createTable(tt.value()); } } QSqlQuery query(*q); for (QMap::ConstIterator tt = m_db.viewBegin(); tt != m_db.viewEnd(); ++tt) { if (!lowerTables.contains(tt.key().toLower())) { if (!query.exec(tt.value().createString())) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("creating view %1").arg(tt.key()))); } } // The columns to store version info changed with version 6. Prior versions are not supported here but an error is prevented and // an old behaviour is used: call upgradeDb(). m_dbVersion = m_db.currentVersion(); if (m_dbVersion >= 6) { query.prepare(QLatin1String("INSERT INTO kmmFileInfo (version, fixLevel) VALUES(?,?);")); query.bindValue(0, m_dbVersion); query.bindValue(1, m_storage->fileFixVersion()); if (!query.exec()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("Saving database version"))); } return upgradeDb(); } void createTable(const MyMoneyDbTable& t, int version = std::numeric_limits::max()) { Q_Q(MyMoneyStorageSql); // create the tables QStringList ql = t.generateCreateSQL(m_driver, version).split('\n', QString::SkipEmptyParts); QSqlQuery query(*q); foreach (const QString& i, ql) { if (!query.exec(i)) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("creating table/index %1").arg(t.name()))); } } bool alterTable(const MyMoneyDbTable& t, int fromVersion) { Q_Q(MyMoneyStorageSql); const int toVersion = fromVersion + 1; QString tempTableName = t.name(); tempTableName.replace("kmm", "kmmtmp"); QSqlQuery query(*q); // drop primary key if it has one (and driver supports it) if (t.hasPrimaryKey(fromVersion)) { QString dropString = m_driver->dropPrimaryKeyString(t.name()); if (!dropString.isEmpty()) { if (!query.exec(dropString)) { buildError(query, Q_FUNC_INFO, QString("Error dropping old primary key from %1").arg(t.name())); return false; } } } for (MyMoneyDbTable::index_iterator i = t.indexBegin(); i != t.indexEnd(); ++i) { QString indexName = t.name() + '_' + i->name() + "_idx"; if (!query.exec(m_driver->dropIndexString(t.name(), indexName))) { buildError(query, Q_FUNC_INFO, QString("Error dropping index from %1").arg(t.name())); return false; } } if (!query.exec(QString("ALTER TABLE " + t.name() + " RENAME TO " + tempTableName + ';'))) { buildError(query, Q_FUNC_INFO, QString("Error renaming table %1").arg(t.name())); return false; } createTable(t, toVersion); if (q->getRecCount(tempTableName) > 0) { query.prepare(QString("INSERT INTO " + t.name() + " (" + t.columnList(fromVersion) + ") SELECT " + t.columnList(fromVersion) + " FROM " + tempTableName + ';')); if (!query.exec()) { // krazy:exclude=crashy buildError(query, Q_FUNC_INFO, QString("Error inserting into new table %1").arg(t.name())); return false; } } if (!query.exec(QString("DROP TABLE " + tempTableName + ';'))) { buildError(query, Q_FUNC_INFO, QString("Error dropping old table %1").arg(t.name())); return false; } return true; } void clean() { Q_Q(MyMoneyStorageSql); // delete all existing records QMap::ConstIterator it = m_db.tableBegin(); QSqlQuery query(*q); while (it != m_db.tableEnd()) { query.prepare(QString("DELETE from %1;").arg(it.key())); if (!query.exec()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, QString("cleaning database"))); // krazy:exclude=crashy ++it; } } int isEmpty() { Q_Q(MyMoneyStorageSql); // check all tables are empty QMap::ConstIterator tt = m_db.tableBegin(); int recordCount = 0; QSqlQuery query(*q); while ((tt != m_db.tableEnd()) && (recordCount == 0)) { query.prepare(QString("select count(*) from %1;").arg((*tt).name())); if (!query.exec()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "getting record count")); // krazy:exclude=crashy if (!query.next()) throw MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, "retrieving record count")); recordCount += query.value(0).toInt(); ++tt; } if (recordCount != 0) { return (-1); // not empty } else { return (0); } } // for bug 252841 QStringList tables(QSql::TableType tt) { Q_Q(MyMoneyStorageSql); return (m_driver->tables(tt, static_cast(*q))); } //! Returns 1 in case the @a column exists in @a table, 0 if not. In case of error, -1 is returned. int haveColumnInTable(const QString& table, const QString& column) { Q_Q(MyMoneyStorageSql); QSqlQuery query(*q); QString cmd = QString("SELECT * FROM %1 LIMIT 1").arg(table); if(!query.exec(cmd)) { buildError(query, Q_FUNC_INFO, QString("Error detecting if %1 exists in %2").arg(column).arg(table)); return -1; } QSqlRecord rec = query.record(); return (rec.indexOf(column) != -1) ? 1 : 0; } /** * @brief Ensure the storagePlugin with iid was setup * * @throws MyMoneyException in case of an error which makes the use * of the plugin unavailable. */ bool setupStoragePlugin(QString iid) { + Q_UNUSED(iid) + return false; + #if 0 Q_Q(MyMoneyStorageSql); // setupDatabase has to be called every time because this simple technique to check if was updated already // does not work if a user opens another file // also the setup is removed if the current database transaction is rolled back if (iid.isEmpty() /*|| m_loadedStoragePlugins.contains(iid)*/) return false; QString errorMsg; // TODO: port KF5 (needed for payeeidentifier plugin) - #if 0 + KMyMoneyPlugin::storagePlugin* plugin = KServiceTypeTrader::createInstanceFromQuery( QLatin1String("KMyMoney/sqlStoragePlugin"), QString("'%1' ~in [X-KMyMoney-PluginIid]").arg(iid.replace(QLatin1Char('\''), QLatin1String("\\'"))), 0, QVariantList(), &errorMsg ); - #else - KMyMoneyPlugin::storagePlugin* plugin = 0; - #endif if (plugin == 0) throw MYMONEYEXCEPTION(QString("Could not load sqlStoragePlugin '%1', (error: %2)").arg(iid, errorMsg)); MyMoneyDbTransaction t(*q, Q_FUNC_INFO); if (plugin->setupDatabase(*q)) { m_loadedStoragePlugins.insert(iid); return true; } throw MYMONEYEXCEPTION(QString("Could not install sqlStoragePlugin '%1' in database.").arg(iid)); +#endif } void insertStorableObject(const databaseStoreableObject& obj, const QString& id) { Q_Q(MyMoneyStorageSql); setupStoragePlugin(obj.storagePluginIid()); if (!obj.sqlSave(*q, id)) throw MYMONEYEXCEPTION(QString("Could not save object with id '%1' in database (plugin failed).").arg(id)); } void updateStorableObject(const databaseStoreableObject& obj, const QString& id) { Q_Q(MyMoneyStorageSql); setupStoragePlugin(obj.storagePluginIid()); if (!obj.sqlModify(*q, id)) throw MYMONEYEXCEPTION(QString("Could not modify object with id '%1' in database (plugin failed).").arg(id)); } void deleteStorableObject(const databaseStoreableObject& obj, const QString& id) { Q_Q(MyMoneyStorageSql); setupStoragePlugin(obj.storagePluginIid()); if (!obj.sqlRemove(*q, id)) throw MYMONEYEXCEPTION(QString("Could not remove object with id '%1' from database (plugin failed).").arg(id)); } void alert(QString s) const // FIXME: remove... { qDebug() << s; } void signalProgress(int current, int total, const QString& msg) const { if (m_progressCallback != 0) (*m_progressCallback)(current, total, msg); } void signalProgress(int current, int total) const { signalProgress(current, total, QString()); } template ulong getNextId(const QString& table, const QString& id, const int prefixLength) const { Q_CHECK_PTR(cache); if (this->*cache == 0) { MyMoneyStorageSqlPrivate* nonConstThis = const_cast(this); nonConstThis->*cache = 1 + nonConstThis->highestNumberFromIdString(table, id, prefixLength); } Q_ASSERT(this->*cache > 0); // everything else is never a valid id return this->*cache; } //void startCommitUnit (const QString& callingFunction); //void endCommitUnit (const QString& callingFunction); //void cancelCommitUnit (const QString& callingFunction); MyMoneyStorageSql *q_ptr; // data QExplicitlySharedDataPointer m_driver; MyMoneyDbDef m_db; uint m_dbVersion; MyMoneyStorageMgr *m_storage; // input options bool m_loadAll; // preload all data bool m_override; // override open if already in use // error message QString m_error; // record counts ulong m_institutions; ulong m_accounts; ulong m_payees; ulong m_tags; ulong m_transactions; ulong m_splits; ulong m_securities; ulong m_prices; ulong m_currencies; ulong m_schedules; ulong m_reports; ulong m_kvps; ulong m_budgets; ulong m_onlineJobs; ulong m_payeeIdentifier; // Cache for next id to use // value 0 means data is not available and has to be loaded from the database ulong m_hiIdInstitutions; ulong m_hiIdPayees; ulong m_hiIdTags; ulong m_hiIdAccounts; ulong m_hiIdTransactions; ulong m_hiIdSchedules; ulong m_hiIdSecurities; ulong m_hiIdReports; ulong m_hiIdBudgets; ulong m_hiIdOnlineJobs; ulong m_hiIdPayeeIdentifier; ulong m_hiIdCostCenter; // encrypt option - usage TBD QString m_encryptData; /** * This variable is used to suppress status messages except during * initial data load and final write */ bool m_displayStatus; /** The following keeps track of commitment units (known as transactions in SQL * though it would be confusing to use that term within KMM). It is implemented * as a stack for debug purposes. Long term, probably a count would suffice */ QStack m_commitUnitStack; /** * This member variable is used to preload transactions for preferred accounts */ MyMoneyTransactionFilter m_preferred; /** * This member variable is used because reading prices from a file uses the 'add...' function rather than a * 'load...' function which other objects use. Having this variable allows us to avoid needing to check the * database to see if this really is a new or modified price */ bool m_readingPrices; /** * This member variable holds a map of transaction counts per account, indexed by * the account id. It is used * to avoid having to scan all transactions whenever a count is needed. It should * probably be moved into the MyMoneyAccount object; maybe we will do that once * the database code has been properly checked out */ QHash m_transactionCountMap; /** * These member variables hold the user name and date/time of logon */ QString m_logonUser; QDateTime m_logonAt; QDate m_txPostDate; // FIXME: remove when Tom puts date into split object bool m_newDatabase; /** * This member keeps the current precision to be used fro prices. * @sa setPrecision() */ static int m_precision; /** * This member keeps the current start date used for transaction retrieval. * @sa setStartDate() */ static QDate m_startDate; /** * */ QSet m_loadedStoragePlugins; void (*m_progressCallback)(int, int, const QString&); }; #endif diff --git a/kmymoney/plugins/sql/sqlstorage.cpp b/kmymoney/plugins/sql/sqlstorage.cpp index 9672f8c45..177a4ef6a 100644 --- a/kmymoney/plugins/sql/sqlstorage.cpp +++ b/kmymoney/plugins/sql/sqlstorage.cpp @@ -1,317 +1,318 @@ /*************************************************************************** sqlstorage.cpp ------------------- copyright : (C) 2018 by Łukasz Wojniłowicz email : lukasz.wojnilowicz@gmail.com ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "sqlstorage.h" #include #include // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "appinterface.h" #include "viewinterface.h" #include "kselectdatabasedlg.h" #include "kgeneratesqldlg.h" #include "mymoneyfile.h" #include "mymoneystoragesql.h" #include "mymoneyexception.h" //#include "mymoneystoragemgr.h" #include "icons.h" #include "kmymoneysettings.h" using namespace Icons; SQLStorage::SQLStorage(QObject *parent, const QVariantList &args) : KMyMoneyPlugin::Plugin(parent, "sqlstorage"/*must be the same as X-KDE-PluginInfo-Name*/) { Q_UNUSED(args) setComponentName("sqlstorage", i18n("SQL storage")); setXMLFile("sqlstorage.rc"); createActions(); // For information, announce that we have been loaded. qDebug("Plugins: sqlstorage loaded"); } SQLStorage::~SQLStorage() { qDebug("Plugins: sqlstorage unloaded"); } bool SQLStorage::open(MyMoneyStorageMgr *storage, const QUrl &url) { auto reader = std::make_unique(storage, url); QUrl dbURL(url); bool retry = true; while (retry) { switch (reader->open(dbURL, QIODevice::ReadWrite)) { case 0: // opened okay retry = false; break; case 1: // permanent error KMessageBox::detailedError(nullptr, i18n("Cannot open database %1\n", dbURL.toDisplayString()), reader->lastError()); return false; case -1: // retryable error if (KMessageBox::warningYesNo(nullptr, reader->lastError(), PACKAGE) == KMessageBox::No) { return false; } else { QUrlQuery query(dbURL); const QString optionKey = QLatin1String("options"); QString options = query.queryItemValue(optionKey); if(!options.isEmpty()) { options += QLatin1Char(','); } options += QLatin1String("override"); query.removeQueryItem(QLatin1String("mode")); query.removeQueryItem(optionKey); query.addQueryItem(optionKey, options); dbURL.setQuery(query); } } } // single user mode; read some of the data into memory // FIXME - readFile no longer relevant? // tried removing it but then got no indication that loading was complete // also, didn't show home page // reader->setProgressCallback(&KMyMoneyView::progressCallback); if (!reader->readFile()) { KMessageBox::detailedError(nullptr, i18n("An unrecoverable error occurred while reading the database"), reader->lastError().toLatin1(), i18n("Database malfunction")); return false; } // reader->setProgressCallback(0); return true; } bool SQLStorage::save(const QUrl &url) { return saveDatabase(url); } QString SQLStorage::formatName() const { return QStringLiteral("SQL"); } QString SQLStorage::fileExtension() const { return QString(); } void SQLStorage::createActions() { m_openDBaction = actionCollection()->addAction("open_database"); m_openDBaction->setText(i18n("Open database...")); m_openDBaction->setIcon(Icons::get(Icon::SVNUpdate)); connect(m_openDBaction, &QAction::triggered, this, &SQLStorage::slotOpenDatabase); m_saveAsDBaction = actionCollection()->addAction("saveas_database"); m_saveAsDBaction->setText(i18n("Save as database...")); m_saveAsDBaction->setIcon(Icons::get(Icon::FileArchiver)); connect(m_saveAsDBaction, &QAction::triggered, this, &SQLStorage::slotSaveAsDatabase); m_generateDB = actionCollection()->addAction("tools_generate_sql"); m_generateDB->setText(i18n("Generate Database SQL")); connect(m_generateDB, &QAction::triggered, this, &SQLStorage::slotGenerateSql); } void SQLStorage::slotOpenDatabase() { QPointer dialog = new KSelectDatabaseDlg(QIODevice::ReadWrite); if (!dialog->checkDrivers()) { delete dialog; return; } if (dialog->exec() == QDialog::Accepted && dialog != 0) { auto url = dialog->selectedURL(); QUrl newurl = url; if ((newurl.scheme() == QLatin1String("sql"))) { const QString key = QLatin1String("driver"); // take care and convert some old url to their new counterpart QUrlQuery query(newurl); if (query.queryItemValue(key) == QLatin1String("QMYSQL3")) { // fix any old urls query.removeQueryItem(key); query.addQueryItem(key, QLatin1String("QMYSQL")); } if (query.queryItemValue(key) == QLatin1String("QSQLITE3")) { query.removeQueryItem(key); query.addQueryItem(key, QLatin1String("QSQLITE")); } newurl.setQuery(query); if (query.queryItemValue(key) == QLatin1String("QSQLITE")) { newurl.setUserInfo(QString()); newurl.setHost(QString()); } // check if a password is needed. it may be if the URL came from the last/recent file list QPointer dialog = new KSelectDatabaseDlg(QIODevice::ReadWrite, newurl); if (!dialog->checkDrivers()) { delete dialog; return; } // if we need to supply a password, then show the dialog // otherwise it isn't needed if ((query.queryItemValue("secure").toLower() == QLatin1String("yes")) && newurl.password().isEmpty()) { if (dialog->exec() == QDialog::Accepted && dialog != nullptr) { newurl = dialog->selectedURL(); } else { delete dialog; return; } } delete dialog; } appInterface()->slotFileOpenRecent(newurl); } delete dialog; } void SQLStorage::slotSaveAsDatabase() { bool rc = false; QUrl oldUrl; // in event of it being a database, ensure that all data is read into storage for saveas if (appInterface()->isDatabase()) oldUrl = appInterface()->filenameURL().isEmpty() ? appInterface()->lastOpenedURL() : appInterface()->filenameURL(); QPointer dialog = new KSelectDatabaseDlg(QIODevice::WriteOnly); QUrl url = oldUrl; if (!dialog->checkDrivers()) { delete dialog; return; } while (oldUrl == url && dialog->exec() == QDialog::Accepted && dialog != 0) { url = dialog->selectedURL(); // If the protocol is SQL for the old and new, and the hostname and database names match // Let the user know that the current database cannot be saved on top of itself. if (url.scheme() == "sql" && oldUrl.scheme() == "sql" && oldUrl.host() == url.host() && QUrlQuery(oldUrl).queryItemValue("driver") == QUrlQuery(url).queryItemValue("driver") && oldUrl.path().right(oldUrl.path().length() - 1) == url.path().right(url.path().length() - 1)) { KMessageBox::sorry(nullptr, i18n("Cannot save to current database.")); } else { try { rc = saveAsDatabase(url); } catch (const MyMoneyException &e) { KMessageBox::sorry(nullptr, i18n("Cannot save to current database: %1", e.what())); } } } delete dialog; if (rc) { //KRecentFilesAction *p = dynamic_cast(action("file_open_recent")); //if(p) appInterface()->addToRecentFiles(url); appInterface()->writeLastUsedFile(url.toDisplayString(QUrl::PreferLocalFile)); } appInterface()->autosaveTimer()->stop(); appInterface()->updateCaption(); return; } void SQLStorage::slotGenerateSql() { QPointer editor = new KGenerateSqlDlg(nullptr); editor->setObjectName("Generate Database SQL"); editor->exec(); delete editor; } bool SQLStorage::saveAsDatabase(const QUrl &url) { auto writer = new MyMoneyStorageSql(MyMoneyFile::instance()->storage(), url); auto canWrite = false; switch (writer->open(url, QIODevice::WriteOnly)) { case 0: canWrite = true; break; case -1: // dbase already has data, see if he wants to clear it out if (KMessageBox::warningContinueCancel(nullptr, i18n("Database contains data which must be removed before using Save As.\n" "Do you wish to continue?"), "Database not empty") == KMessageBox::Continue) { if (writer->open(url, QIODevice::WriteOnly, true) == 0) canWrite = true; } else { delete writer; return false; } break; } - delete writer; if (canWrite) { + delete writer; saveDatabase(url); return true; } else { KMessageBox::detailedError(nullptr, i18n("Cannot open or create database %1.\n" "Retry Save As Database and click Help" " for further info.", url.toDisplayString()), writer->lastError()); + delete writer; return false; } } bool SQLStorage::saveDatabase(const QUrl &url) { auto rc = false; if (!appInterface()->fileOpen()) { KMessageBox::error(nullptr, i18n("Tried to access a file when it has not been opened")); return (rc); } auto writer = new MyMoneyStorageSql(MyMoneyFile::instance()->storage(), url); writer->open(url, QIODevice::WriteOnly); // writer->setProgressCallback(&KMyMoneyView::progressCallback); if (!writer->writeFile()) { KMessageBox::detailedError(nullptr, i18n("An unrecoverable error occurred while writing to the database.\n" "It may well be corrupt."), writer->lastError().toLatin1(), i18n("Database malfunction")); rc = false; } else { rc = true; } writer->setProgressCallback(0); delete writer; return rc; } K_PLUGIN_FACTORY_WITH_JSON(SQLStorageFactory, "sqlstorage.json", registerPlugin();) #include "sqlstorage.moc" diff --git a/kmymoney/reports/tests/reportstestcommon.cpp b/kmymoney/reports/tests/reportstestcommon.cpp index 228b9f8e6..737c6e539 100644 --- a/kmymoney/reports/tests/reportstestcommon.cpp +++ b/kmymoney/reports/tests/reportstestcommon.cpp @@ -1,492 +1,497 @@ /*************************************************************************** reportstestcommon.cpp ------------------- copyright : (C) 2002-2005 by Thomas Baumgart email : ipwizard@users.sourceforge.net Ace Jones ***************************************************************************/ /*************************************************************************** * * * 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 "reportstestcommon.h" #include "kreportsview-test.h" #include #include #include #include #include +#include #include "pivottable.h" #include "querytable.h" using namespace reports; #include "mymoneyexception.h" #include "mymoneymoney.h" #include "mymoneysecurity.h" #include "mymoneyprice.h" #include "mymoneysplit.h" #include "mymoneystoragedump.h" #include "mymoneyreport.h" #include "mymoneypayee.h" #include "mymoneystatement.h" #include "mymoneystoragexml.h" namespace test { const MyMoneyMoney moCheckingOpen(0.0); const MyMoneyMoney moCreditOpen(-0.0); const MyMoneyMoney moConverterCheckingOpen(1418.0); const MyMoneyMoney moConverterCreditOpen(-418.0); const MyMoneyMoney moZero(0.0); const MyMoneyMoney moSolo(234.12); const MyMoneyMoney moParent1(88.01); const MyMoneyMoney moParent2(133.22); const MyMoneyMoney moParent(moParent1 + moParent2); const MyMoneyMoney moChild(14.00); const MyMoneyMoney moThomas(5.11); const MyMoneyMoney moNoPayee(8944.70); QString acAsset; QString acLiability; QString acExpense; QString acIncome; QString acChecking; QString acCredit; QString acSolo; QString acParent; QString acChild; QString acSecondChild; QString acGrandChild1; QString acGrandChild2; QString acForeign; QString acCanChecking; QString acJpyChecking; QString acCanCash; QString acJpyCash; QString inBank; QString eqStock1; QString eqStock2; QString eqStock3; QString eqStock4; QString acInvestment; QString acStock1; QString acStock2; QString acStock3; QString acStock4; QString acDividends; QString acInterest; QString acFees; QString acTax; QString acCash; TransactionHelper::TransactionHelper(const QDate& _date, const QString& _action, MyMoneyMoney _value, const QString& _accountid, const QString& _categoryid, const QString& _currencyid, const QString& _payee) { // _currencyid is the currency of the transaction, and of the _value // both the account and category can have their own currency (athough the category having // a foreign currency is not yet supported by the program, the reports will still allow it, // so it must be tested.) MyMoneyFile* file = MyMoneyFile::instance(); bool haspayee = ! _payee.isEmpty(); MyMoneyPayee payeeTest = file->payeeByName(_payee); MyMoneyFileTransaction ft; setPostDate(_date); QString currencyid = _currencyid; if (currencyid.isEmpty()) currencyid = MyMoneyFile::instance()->baseCurrency().id(); setCommodity(currencyid); MyMoneyMoney price; MyMoneySplit splitLeft; if (haspayee) splitLeft.setPayeeId(payeeTest.id()); splitLeft.setAction(_action); splitLeft.setValue(-_value); price = MyMoneyFile::instance()->price(currencyid, file->account(_accountid).currencyId(), _date).rate(file->account(_accountid).currencyId()); splitLeft.setShares(-_value * price); splitLeft.setAccountId(_accountid); addSplit(splitLeft); MyMoneySplit splitRight; if (haspayee) splitRight.setPayeeId(payeeTest.id()); splitRight.setAction(_action); splitRight.setValue(_value); price = MyMoneyFile::instance()->price(currencyid, file->account(_categoryid).currencyId(), _date).rate(file->account(_categoryid).currencyId()); splitRight.setShares(_value * price); splitRight.setAccountId(_categoryid); addSplit(splitRight); MyMoneyFile::instance()->addTransaction(*this); ft.commit(); } TransactionHelper::~TransactionHelper() { MyMoneyFileTransaction ft; - MyMoneyFile::instance()->removeTransaction(*this); - ft.commit(); + try { + MyMoneyFile::instance()->removeTransaction(*this); + ft.commit(); + } catch (const MyMoneyException & e) { + qDebug() << e.what(); + } } void TransactionHelper::update() { MyMoneyFileTransaction ft; MyMoneyFile::instance()->modifyTransaction(*this); ft.commit(); } InvTransactionHelper::InvTransactionHelper(const QDate& _date, const QString& _action, MyMoneyMoney _shares, MyMoneyMoney _price, const QString& _stockaccountid, const QString& _transferid, const QString& _categoryid, MyMoneyMoney _fee) { init(_date, _action, _shares, _price, _fee, _stockaccountid, _transferid, _categoryid); } void InvTransactionHelper::init(const QDate& _date, const QString& _action, MyMoneyMoney _shares, MyMoneyMoney _price, MyMoneyMoney _fee, const QString& _stockaccountid, const QString& _transferid, const QString& _categoryid) { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyAccount stockaccount = file->account(_stockaccountid); MyMoneyMoney value = _shares * _price; setPostDate(_date); setCommodity("USD"); MyMoneySplit s1; s1.setValue(value); s1.setAccountId(_stockaccountid); if (_action == MyMoneySplit::actionName(eMyMoney::Split::Action::ReinvestDividend)) { s1.setShares(_shares); s1.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::ReinvestDividend)); MyMoneySplit s2; s2.setAccountId(_categoryid); s2.setShares(-value); s2.setValue(-value); addSplit(s2); } else if (_action == MyMoneySplit::actionName(eMyMoney::Split::Action::Dividend) || _action == MyMoneySplit::actionName(eMyMoney::Split::Action::Yield)) { s1.setAccountId(_categoryid); s1.setShares(-value); s1.setValue(-value); // Split 2 will be the zero-amount investment split that serves to // mark this transaction as a cash dividend and note which stock account // it belongs to. MyMoneySplit s2; s2.setValue(MyMoneyMoney()); s2.setShares(MyMoneyMoney()); s2.setAction(_action); s2.setAccountId(_stockaccountid); addSplit(s2); MyMoneySplit s3; s3.setAccountId(_transferid); s3.setShares(value); s3.setValue(value); addSplit(s3); } else if (_action == MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares)) { s1.setShares(_shares); s1.setValue(value); s1.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares)); MyMoneySplit s3; s3.setAccountId(_transferid); s3.setShares(-value - _fee); s3.setValue(-value - _fee); addSplit(s3); if (!_categoryid.isEmpty() && !_fee.isZero()) { MyMoneySplit s2; s2.setAccountId(_categoryid); s2.setValue(_fee); s2.setShares(_fee); addSplit(s2); } } else if (_action == MyMoneySplit::actionName(eMyMoney::Split::Action::SplitShares)) { s1.setShares(_shares.abs()); s1.setValue(MyMoneyMoney()); s1.setPrice(MyMoneyMoney()); } addSplit(s1); //qDebug() << "created transaction, now adding..."; MyMoneyFileTransaction ft; file->addTransaction(*this); //qDebug() << "updating price..."; // update the price, while we're here if (_action != MyMoneySplit::actionName(eMyMoney::Split::Action::SplitShares)) { QString stockid = stockaccount.currencyId(); QString basecurrencyid = file->baseCurrency().id(); MyMoneyPrice price = file->price(stockid, basecurrencyid, _date, true); if (!price.isValid()) { MyMoneyPrice newprice(stockid, basecurrencyid, _date, _price, "test"); file->addPrice(newprice); } } ft.commit(); //qDebug() << "successfully added " << id(); } QString makeAccount(const QString& _name, eMyMoney::Account::Type _type, MyMoneyMoney _balance, const QDate& _open, const QString& _parent, QString _currency, bool _taxReport, bool _openingBalance) { MyMoneyAccount info; MyMoneyFileTransaction ft; info.setName(_name); info.setAccountType(_type); info.setOpeningDate(_open); if (!_currency.isEmpty()) info.setCurrencyId(_currency); else info.setCurrencyId(MyMoneyFile::instance()->baseCurrency().id()); if (_taxReport) info.setValue("Tax", "Yes"); if (_openingBalance) info.setValue("OpeningBalanceAccount", "Yes"); MyMoneyAccount parent = MyMoneyFile::instance()->account(_parent); MyMoneyFile::instance()->addAccount(info, parent); // create the opening balance transaction if any if (!_balance.isZero()) { MyMoneySecurity sec = MyMoneyFile::instance()->currency(info.currencyId()); MyMoneyFile::instance()->openingBalanceAccount(sec); MyMoneyFile::instance()->createOpeningBalanceTransaction(info, _balance); } ft.commit(); return info.id(); } void makePrice(const QString& _currencyid, const QDate& _date, const MyMoneyMoney& _price) { MyMoneyFileTransaction ft; MyMoneyFile* file = MyMoneyFile::instance(); MyMoneySecurity curr = file->currency(_currencyid); MyMoneyPrice price(_currencyid, file->baseCurrency().id(), _date, _price, "test"); file->addPrice(price); ft.commit(); } QString makeEquity(const QString& _name, const QString& _symbol) { MyMoneySecurity equity; MyMoneyFileTransaction ft; equity.setName(_name); equity.setTradingSymbol(_symbol); equity.setSmallestAccountFraction(1000); equity.setSecurityType(eMyMoney::Security::Type::None/*MyMoneyEquity::ETYPE_STOCK*/); MyMoneyFile::instance()->addSecurity(equity); ft.commit(); return equity.id(); } void makeEquityPrice(const QString& _id, const QDate& _date, const MyMoneyMoney& _price) { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyFileTransaction ft; QString basecurrencyid = file->baseCurrency().id(); MyMoneyPrice price = file->price(_id, basecurrencyid, _date, true); if (!price.isValid()) { MyMoneyPrice newprice(_id, basecurrencyid, _date, _price, "test"); file->addPrice(newprice); } ft.commit(); } void writeRCFtoXMLDoc(const MyMoneyReport& filter, QDomDocument* doc) { QDomProcessingInstruction instruct = doc->createProcessingInstruction(QString("xml"), QString("version=\"1.0\" encoding=\"utf-8\"")); doc->appendChild(instruct); QDomElement root = doc->createElement("KMYMONEY-FILE"); doc->appendChild(root); QDomElement reports = doc->createElement("REPORTS"); root.appendChild(reports); QDomElement report = doc->createElement("REPORT"); filter.write(report, doc); reports.appendChild(report); } void writeTabletoHTML(const PivotTable& table, const QString& _filename) { static unsigned filenumber = 1; QString filename = _filename; if (filename.isEmpty()) { filename = QString("report-%1%2.html").arg((filenumber < 10) ? "0" : "").arg(filenumber); ++filenumber; } QFile g(filename); g.open(QIODevice::WriteOnly); QTextStream(&g) << table.renderHTML(); g.close(); } void writeTabletoHTML(const QueryTable& table, const QString& _filename) { static unsigned filenumber = 1; QString filename = _filename; if (filename.isEmpty()) { filename = QString("report-%1%2.html").arg((filenumber < 10) ? "0" : "").arg(filenumber); ++filenumber; } QFile g(filename); g.open(QIODevice::WriteOnly); QTextStream(&g) << table.renderHTML(); g.close(); } void writeTabletoCSV(const PivotTable& table, const QString& _filename) { static unsigned filenumber = 1; QString filename = _filename; if (filename.isEmpty()) { filename = QString("report-%1%2.csv").arg((filenumber < 10) ? "0" : "").arg(filenumber); ++filenumber; } QFile g(filename); g.open(QIODevice::WriteOnly); QTextStream(&g) << table.renderCSV(); g.close(); } void writeTabletoCSV(const QueryTable& table, const QString& _filename) { static unsigned filenumber = 1; QString filename = _filename; if (filename.isEmpty()) { filename = QString("qreport-%1%2.csv").arg((filenumber < 10) ? "0" : "").arg(filenumber); ++filenumber; } QFile g(filename); g.open(QIODevice::WriteOnly); QTextStream(&g) << table.renderCSV(); g.close(); } void writeRCFtoXML(const MyMoneyReport& filter, const QString& _filename) { static unsigned filenum = 1; QString filename = _filename; if (filename.isEmpty()) { filename = QString("report-%1%2.xml").arg(QString::number(filenum).rightJustified(2, '0')); ++filenum; } QDomDocument* doc = new QDomDocument("KMYMONEY-FILE"); Q_CHECK_PTR(doc); writeRCFtoXMLDoc(filter, doc); QFile g(filename); g.open(QIODevice::WriteOnly); QTextStream stream(&g); stream.setCodec("UTF-8"); stream << doc->toString(); g.close(); delete doc; } bool readRCFfromXMLDoc(QList& list, QDomDocument* doc) { bool result = false; QDomElement rootElement = doc->documentElement(); if (!rootElement.isNull()) { QDomNode child = rootElement.firstChild(); while (!child.isNull() && child.isElement()) { QDomElement childElement = child.toElement(); if ("REPORTS" == childElement.tagName()) { result = true; QDomNode subchild = child.firstChild(); while (!subchild.isNull() && subchild.isElement()) { MyMoneyReport filter; if (filter.read(subchild.toElement())) { list += filter; } subchild = subchild.nextSibling(); } } child = child.nextSibling(); } } return result; } bool readRCFfromXML(QList& list, const QString& filename) { int result = false; QFile f(filename); f.open(QIODevice::ReadOnly); QDomDocument* doc = new QDomDocument; if (doc->setContent(&f, false)) { result = readRCFfromXMLDoc(list, doc); } delete doc; return result; } void XMLandback(MyMoneyReport& filter) { // this function writes the filter to XML, and then reads // it back from XML overwriting the original filter; // in all cases, the result should be the same if the read // & write methods are working correctly. QDomDocument* doc = new QDomDocument("KMYMONEY-FILE"); Q_CHECK_PTR(doc); writeRCFtoXMLDoc(filter, doc); QList list; if (readRCFfromXMLDoc(list, doc) && !list.isEmpty()) filter = list[0]; else throw MYMONEYEXCEPTION("Failed to load report from XML"); delete doc; } MyMoneyMoney searchHTML(const QString& _html, const QString& _search) { Q_UNUSED(_html) QRegExp re(QString("%1[<>/td]*([\\-.0-9,]*)").arg(_search)); if (re.indexIn(_html) > -1) { QString found = re.cap(1); found.remove(','); return MyMoneyMoney(found.toDouble()); } return MyMoneyMoney(); } } // end namespace test diff --git a/kmymoney/views/khomeview_p.h b/kmymoney/views/khomeview_p.h index 66db833af..f8aafe651 100644 --- a/kmymoney/views/khomeview_p.h +++ b/kmymoney/views/khomeview_p.h @@ -1,1983 +1,1983 @@ /*************************************************************************** khomeview_p.h - description ------------------- begin : Tue Jan 22 2002 copyright : (C) 2000-2002 by Michael Edwardes 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. * * * ***************************************************************************/ #ifndef KHOMEVIEW_P_H #define KHOMEVIEW_P_H #include "khomeview.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include #include #include #ifdef ENABLE_WEBENGINE #include #else #include #endif // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneyviewbase_p.h" #include "mymoneyutils.h" #include "kmymoneyutils.h" #include "kwelcomepage.h" #include "kmymoneysettings.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneyprice.h" #include "mymoneyforecast.h" #include "kreportchartview.h" #include "pivottable.h" #include "pivotgrid.h" #include "reportaccount.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "icons.h" #include "kmymoneywebpage.h" #include "mymoneyschedule.h" #include "mymoneysecurity.h" #include "mymoneyexception.h" #include "mymoneyenums.h" #include "menuenums.h" #define VIEW_LEDGER "ledger" #define VIEW_SCHEDULE "schedule" #define VIEW_WELCOME "welcome" #define VIEW_HOME "home" #define VIEW_REPORTS "reports" using namespace Icons; using namespace eMyMoney; /** * @brief Converts a QPixmap to an data URI scheme * * According to RFC 2397 * * @param pixmap Source to convert * @return full data URI */ QString QPixmapToDataUri(const QPixmap& pixmap) { QImage image(pixmap.toImage()); QByteArray byteArray; QBuffer buffer(&byteArray); buffer.open(QIODevice::WriteOnly); image.save(&buffer, "PNG"); // writes the image in PNG format inside the buffer return QLatin1String("data:image/png;base64,") + QString(byteArray.toBase64()); } bool accountNameLess(const MyMoneyAccount &acc1, const MyMoneyAccount &acc2) { return acc1.name().localeAwareCompare(acc2.name()) < 0; } using namespace reports; class KHomeViewPrivate : public KMyMoneyViewBasePrivate { Q_DECLARE_PUBLIC(KHomeView) public: explicit KHomeViewPrivate(KHomeView *qq) : KMyMoneyViewBasePrivate(), q_ptr(qq), m_view(nullptr), m_showAllSchedules(false), m_needLoad(true), m_netWorthGraphLastValidSize(400, 300), m_currentPrinter(nullptr) { } ~KHomeViewPrivate() { // if user wants to remember the font size, store it here if (KMyMoneySettings::rememberZoomFactor() && m_view) { KMyMoneySettings::setZoomFactor(m_view->zoomFactor()); KMyMoneySettings::self()->save(); } } /** * Definition of bitmap used as argument for showAccounts(). */ enum paymentTypeE { Preferred = 1, ///< show preferred accounts Payment = 2 ///< show payment accounts }; void init() { Q_Q(KHomeView); m_needLoad = false; auto vbox = new QVBoxLayout(q); q->setLayout(vbox); vbox->setSpacing(6); vbox->setMargin(0); #ifdef ENABLE_WEBENGINE m_view = new QWebEngineView(q); #else m_view = new KWebView(q); #endif m_view->setPage(new MyQWebEnginePage(m_view)); vbox->addWidget(m_view); m_view->setHtml(KWelcomePage::welcomePage(), QUrl("file://")); #ifdef ENABLE_WEBENGINE q->connect(m_view->page(), &QWebEnginePage::urlChanged, q, &KHomeView::slotOpenUrl); #else m_view->page()->setLinkDelegationPolicy(QWebPage::DelegateAllLinks); q->connect(m_view->page(), &KWebPage::linkClicked, q, &KHomeView::slotOpenUrl); #endif q->connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KHomeView::refresh); } /** * Print an account and its balance and limit */ void showAccountEntry(const MyMoneyAccount& acc, const MyMoneyMoney& value, const MyMoneyMoney& valueToMinBal, const bool showMinBal) { MyMoneyFile* file = MyMoneyFile::instance(); QString tmp; MyMoneySecurity currency = file->currency(acc.currencyId()); QString amount; QString amountToMinBal; //format amounts amount = MyMoneyUtils::formatMoney(value, acc, currency); amount.replace(QChar(' '), " "); if (showMinBal) { amountToMinBal = MyMoneyUtils::formatMoney(valueToMinBal, acc, currency); amountToMinBal.replace(QChar(' '), " "); } QString cellStatus, cellCounts, pathOK, pathTODO, pathNotOK; if (KMyMoneySettings::showBalanceStatusOfOnlineAccounts()) { //show account's online-status pathOK = QPixmapToDataUri(Icons::get(Icon::DialogOKApply).pixmap(QSize(16,16))); pathTODO = QPixmapToDataUri(Icons::get(Icon::MailReceive).pixmap(QSize(16,16))); pathNotOK = QPixmapToDataUri(Icons::get(Icon::DialogCancel).pixmap(QSize(16,16))); if (acc.value("lastImportedTransactionDate").isEmpty() || acc.value("lastStatementBalance").isEmpty()) cellStatus = '-'; else if (file->hasMatchingOnlineBalance(acc)) { if (file->hasNewerTransaction(acc.id(), QDate::fromString(acc.value("lastImportedTransactionDate"), Qt::ISODate))) cellStatus = QString("").arg(pathTODO); else cellStatus = QString("").arg(pathOK); } else cellStatus = QString("").arg(pathNotOK); tmp = QString("%1").arg(cellStatus); } tmp += QString("") + link(VIEW_LEDGER, QString("?id=%1").arg(acc.id())) + acc.name() + linkend() + ""; int countNotMarked = 0, countCleared = 0, countNotReconciled = 0; QString countStr; if (KMyMoneySettings::showCountOfUnmarkedTransactions() || KMyMoneySettings::showCountOfNotReconciledTransactions()) countNotMarked = file->countTransactionsWithSpecificReconciliationState(acc.id(), TransactionFilter::State::NotReconciled); if (KMyMoneySettings::showCountOfClearedTransactions() || KMyMoneySettings::showCountOfNotReconciledTransactions()) countCleared = file->countTransactionsWithSpecificReconciliationState(acc.id(), TransactionFilter::State::Cleared); if (KMyMoneySettings::showCountOfNotReconciledTransactions()) countNotReconciled = countNotMarked + countCleared; if (KMyMoneySettings::showCountOfUnmarkedTransactions()) { if (countNotMarked) countStr = QString("%1").arg(countNotMarked); else countStr = '-'; tmp += QString("%1").arg(countStr); } if (KMyMoneySettings::showCountOfClearedTransactions()) { if (countCleared) countStr = QString("%1").arg(countCleared); else countStr = '-'; tmp += QString("%1").arg(countStr); } if (KMyMoneySettings::showCountOfNotReconciledTransactions()) { if (countNotReconciled) countStr = QString("%1").arg(countNotReconciled); else countStr = '-'; tmp += QString("%1").arg(countStr); } if (KMyMoneySettings::showDateOfLastReconciliation()) { const auto lastReconciliationDate = acc.lastReconciliationDate().toString(Qt::SystemLocaleShortDate); tmp += QString("%1").arg(lastReconciliationDate); } //show account balance tmp += QString("%1").arg(showColoredAmount(amount, value.isNegative())); //show minimum balance column if requested if (showMinBal) { //if it is an investment, show minimum balance empty if (acc.accountType() == Account::Type::Investment) { tmp += QString(" "); } else { //show minimum balance entry tmp += QString("%1").arg(showColoredAmount(amountToMinBal, valueToMinBal.isNegative())); } } // qDebug("accountEntry = '%s'", tmp.toLatin1()); m_html += tmp; } void showAccountEntry(const MyMoneyAccount& acc) { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneySecurity currency = file->currency(acc.currencyId()); MyMoneyMoney value; bool showLimit = KMyMoneySettings::showLimitInfo(); if (acc.accountType() == Account::Type::Investment) { //investment accounts show the balances of all its subaccounts value = investmentBalance(acc); //investment accounts have no minimum balance showAccountEntry(acc, value, MyMoneyMoney(), showLimit); } else { //get balance for normal accounts value = file->balance(acc.id(), QDate::currentDate()); if (acc.currencyId() != file->baseCurrency().id()) { ReportAccount repAcc = ReportAccount(acc.id()); MyMoneyMoney curPrice = repAcc.baseCurrencyPrice(QDate::currentDate()); MyMoneyMoney baseValue = value * curPrice; baseValue = baseValue.convert(file->baseCurrency().smallestAccountFraction()); m_total += baseValue; } else { m_total += value; } //if credit card or checkings account, show maximum credit if (acc.accountType() == Account::Type::CreditCard || acc.accountType() == Account::Type::Checkings) { QString maximumCredit = acc.value("maxCreditAbsolute"); if (maximumCredit.isEmpty()) { maximumCredit = acc.value("minBalanceAbsolute"); } MyMoneyMoney maxCredit = MyMoneyMoney(maximumCredit); showAccountEntry(acc, value, value - maxCredit, showLimit); } else { //otherwise use minimum balance QString minimumBalance = acc.value("minBalanceAbsolute"); MyMoneyMoney minBalance = MyMoneyMoney(minimumBalance); showAccountEntry(acc, value, value - minBalance, showLimit); } } } /** * @param acc the investment account * @return the balance in the currency of the investment account */ MyMoneyMoney investmentBalance(const MyMoneyAccount& acc) { auto file = MyMoneyFile::instance(); auto value = file->balance(acc.id(), QDate::currentDate()); foreach (const auto accountID, acc.accountList()) { auto stock = file->account(accountID); if (!stock.isClosed()) { try { MyMoneyMoney val; MyMoneyMoney balance = file->balance(stock.id(), QDate::currentDate()); MyMoneySecurity security = file->security(stock.currencyId()); const MyMoneyPrice &price = file->price(stock.currencyId(), security.tradingCurrency()); val = (balance * price.rate(security.tradingCurrency())).convertPrecision(security.pricePrecision()); // adjust value of security to the currency of the account MyMoneySecurity accountCurrency = file->currency(acc.currencyId()); val = val * file->price(security.tradingCurrency(), accountCurrency.id()).rate(accountCurrency.id()); val = val.convert(acc.fraction()); value += val; } catch (const MyMoneyException &e) { qWarning("%s", qPrintable(QString("cannot convert stock balance of %1 to base currency: %2").arg(stock.name(), e.what()))); } } } return value; } /** * Print text in the color set for negative numbers, if @p amount is negative * abd @p isNegative is true */ QString showColoredAmount(const QString& amount, bool isNegative) { if (isNegative) { //if negative, get the settings for negative numbers return QString("%2").arg(KMyMoneySettings::schemeColor(SchemeColor::Negative).name(), amount); } //if positive, return the same string return amount; } /** * Run the forecast */ void doForecast() { //clear m_accountList because forecast is about to changed m_accountList.clear(); //reinitialize the object m_forecast = KMyMoneyUtils::forecast(); //If forecastDays lower than accountsCycle, adjust to the first cycle if (m_forecast.accountsCycle() > m_forecast.forecastDays()) m_forecast.setForecastDays(m_forecast.accountsCycle()); //Get all accounts of the right type to calculate forecast m_forecast.doForecast(); } /** * Calculate the forecast balance after a payment has been made */ MyMoneyMoney forecastPaymentBalance(const MyMoneyAccount& acc, const MyMoneyMoney& payment, QDate& paymentDate) { //if paymentDate before or equal to currentDate set it to current date plus 1 //so we get to accumulate forecast balance correctly if (paymentDate <= QDate::currentDate()) paymentDate = QDate::currentDate().addDays(1); //check if the account is already there if (m_accountList.find(acc.id()) == m_accountList.end() || m_accountList[acc.id()].find(paymentDate) == m_accountList[acc.id()].end()) { if (paymentDate == QDate::currentDate()) { m_accountList[acc.id()][paymentDate] = m_forecast.forecastBalance(acc, paymentDate); } else { m_accountList[acc.id()][paymentDate] = m_forecast.forecastBalance(acc, paymentDate.addDays(-1)); } } m_accountList[acc.id()][paymentDate] = m_accountList[acc.id()][paymentDate] + payment; return m_accountList[acc.id()][paymentDate]; } void loadView() { m_view->setZoomFactor(KMyMoneySettings::zoomFactor()); QList list; MyMoneyFile::instance()->accountList(list); if (list.count() == 0) { m_view->setHtml(KWelcomePage::welcomePage(), QUrl("file://")); } else { //clear the forecast flag so it will be reloaded m_forecast.setForecastDone(false); const QString filename = QStandardPaths::locate(QStandardPaths::AppConfigLocation, "html/kmymoney.css"); QString header = QString("\n\n").arg(QUrl::fromLocalFile(filename).url()); header += KMyMoneyUtils::variableCSS(); header += "\n"; QString footer = "\n"; m_html.clear(); m_html += header; m_html += QString("
%1
").arg(i18n("Your Financial Summary")); QStringList settings = KMyMoneySettings::listOfItems(); QStringList::ConstIterator it; for (it = settings.constBegin(); it != settings.constEnd(); ++it) { int option = (*it).toInt(); if (option > 0) { switch (option) { case 1: // payments showPayments(); break; case 2: // preferred accounts showAccounts(Preferred, i18n("Preferred Accounts")); break; case 3: // payment accounts // Check if preferred accounts are shown separately if (settings.contains("2")) { showAccounts(static_cast(Payment | Preferred), i18n("Payment Accounts")); } else { showAccounts(Payment, i18n("Payment Accounts")); } break; case 4: // favorite reports showFavoriteReports(); break; case 5: // forecast showForecast(); break; case 6: // net worth graph over all accounts showNetWorthGraph(); break; case 8: // assets and liabilities showAssetsLiabilities(); break; case 9: // budget showBudget(); break; case 10: // cash flow summary showCashFlowSummary(); break; } m_html += "
 
\n"; } } m_html += "
"; m_html += link(VIEW_WELCOME, QString()) + i18n("Show KMyMoney welcome page") + linkend(); m_html += "
"; m_html += "
"; m_html += footer; m_view->setHtml(m_html, QUrl("file://")); } } void showNetWorthGraph() { Q_Q(KHomeView); m_html += QString("
%1
\n
 
\n").arg(i18n("Net Worth Forecast")); MyMoneyReport reportCfg = MyMoneyReport( MyMoneyReport::eAssetLiability, MyMoneyReport::eMonths, TransactionFilter::Date::UserDefined, // overridden by the setDateFilter() call below MyMoneyReport::eDetailTotal, i18n("Net Worth Forecast"), i18n("Generated Report")); reportCfg.setChartByDefault(true); reportCfg.setChartCHGridLines(false); reportCfg.setChartSVGridLines(false); reportCfg.setChartDataLabels(false); reportCfg.setChartType(MyMoneyReport::eChartLine); reportCfg.setIncludingSchedules(false); reportCfg.addAccountGroup(Account::Type::Asset); reportCfg.addAccountGroup(Account::Type::Liability); reportCfg.setColumnsAreDays(true); reportCfg.setConvertCurrency(true); reportCfg.setIncludingForecast(true); reportCfg.setDateFilter(QDate::currentDate(), QDate::currentDate().addDays(+ 90)); reports::PivotTable table(reportCfg); reports::KReportChartView* chartWidget = new reports::KReportChartView(0); table.drawChart(*chartWidget); // Adjust the size QSize netWorthGraphSize = q->size(); netWorthGraphSize -= QSize(80, 30); // consider the computed size valid only if it's smaller on both axes that the applications size // if (netWorthGraphSize.width() < kmymoney->width() || netWorthGraphSize.height() < kmymoney->height()) { m_netWorthGraphLastValidSize = netWorthGraphSize; // } chartWidget->resize(m_netWorthGraphLastValidSize); //save the chart to an image QString chart = QPixmapToDataUri(chartWidget->coordinatePlane()->parent()->grab()); m_html += QString(""); m_html += QString(""); m_html += QString("").arg(chart); m_html += QString(""); m_html += QString("
\"Networth\"
"); //delete the widget since we no longer need it delete chartWidget; } void showPayments() { MyMoneyFile* file = MyMoneyFile::instance(); QList overdues; QList schedule; int i = 0; //if forecast has not been executed yet, do it. if (!m_forecast.isForecastDone()) doForecast(); schedule = file->scheduleList(QString(), Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, QDate::currentDate(), QDate::currentDate().addMonths(1), false); overdues = file->scheduleList(QString(), Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, QDate(), QDate(), true); if (schedule.empty() && overdues.empty()) return; // HACK // Remove the finished schedules QList::Iterator d_it; //regular schedules d_it = schedule.begin(); while (d_it != schedule.end()) { if ((*d_it).isFinished()) { d_it = schedule.erase(d_it); continue; } ++d_it; } //overdue schedules d_it = overdues.begin(); while (d_it != overdues.end()) { if ((*d_it).isFinished()) { d_it = overdues.erase(d_it); continue; } ++d_it; } m_html += "
"; m_html += QString("
%1
\n").arg(i18n("Payments")); if (!overdues.isEmpty()) { m_html += "
 
\n"; qSort(overdues); QList::Iterator it; QList::Iterator it_f; m_html += ""; m_html += QString("\n").arg(showColoredAmount(i18n("Overdue payments"), true)); m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; for (it = overdues.begin(); it != overdues.end(); ++it) { // determine number of overdue payments int cnt = (*it).transactionsRemainingUntil(QDate::currentDate().addDays(-1)); m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); showPaymentEntry(*it, cnt); m_html += ""; } m_html += "
%1
"; m_html += i18n("Date"); m_html += ""; m_html += i18n("Schedule"); m_html += ""; m_html += i18n("Account"); m_html += ""; m_html += i18n("Amount"); m_html += ""; m_html += i18n("Balance after"); m_html += "
"; } if (!schedule.isEmpty()) { qSort(schedule); // Extract todays payments if any QList todays; QList::Iterator t_it; for (t_it = schedule.begin(); t_it != schedule.end();) { if ((*t_it).adjustedNextDueDate() == QDate::currentDate()) { todays.append(*t_it); (*t_it).setNextDueDate((*t_it).nextPayment(QDate::currentDate())); // if adjustedNextDueDate is still currentDate then remove it from // scheduled payments if ((*t_it).adjustedNextDueDate() == QDate::currentDate()) { t_it = schedule.erase(t_it); continue; } } ++t_it; } if (todays.count() > 0) { m_html += "
 
\n"; m_html += ""; m_html += QString("\n").arg(i18n("Today's due payments")); m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; for (t_it = todays.begin(); t_it != todays.end(); ++t_it) { m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); showPaymentEntry(*t_it); m_html += ""; } m_html += "
%1
"; m_html += i18n("Date"); m_html += ""; m_html += i18n("Schedule"); m_html += ""; m_html += i18n("Account"); m_html += ""; m_html += i18n("Amount"); m_html += ""; m_html += i18n("Balance after"); m_html += "
"; } if (!schedule.isEmpty()) { m_html += "
 
\n"; QList::Iterator it; m_html += ""; m_html += QString("\n").arg(i18n("Future payments")); m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; // show all or the first 6 entries int cnt; cnt = (m_showAllSchedules) ? -1 : 6; bool needMoreLess = m_showAllSchedules; QDate lastDate = QDate::currentDate().addMonths(1); qSort(schedule); do { it = schedule.begin(); if (it == schedule.end()) break; // if the next due date is invalid (schedule is finished) // we remove it from the list QDate nextDate = (*it).nextDueDate(); if (!nextDate.isValid()) { schedule.erase(it); continue; } if (nextDate > lastDate) break; if (cnt == 0) { needMoreLess = true; break; } // in case we've shown the current recurrence as overdue, // we don't show it here again, but keep the schedule // as it might show up later in the list again if (!(*it).isOverdue()) { if (cnt > 0) --cnt; m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); showPaymentEntry(*it); m_html += ""; // for single occurrence we have reported everything so we // better get out of here. if ((*it).occurrence() == Schedule::Occurrence::Once) { schedule.erase(it); continue; } } // if nextPayment returns an invalid date, setNextDueDate will // just skip it, resulting in a loop // we check the resulting date and erase the schedule if invalid if (!((*it).nextPayment((*it).nextDueDate())).isValid()) { schedule.erase(it); continue; } (*it).setNextDueDate((*it).nextPayment((*it).nextDueDate())); qSort(schedule); } while (1); if (needMoreLess) { m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); m_html += ""; m_html += ""; } m_html += "
%1
"; m_html += i18n("Date"); m_html += ""; m_html += i18n("Schedule"); m_html += ""; m_html += i18n("Account"); m_html += ""; m_html += i18n("Amount"); m_html += ""; m_html += i18n("Balance after"); m_html += "
"; if (m_showAllSchedules) { m_html += link(VIEW_SCHEDULE, QString("?mode=%1").arg("reduced")) + i18nc("Less...", "Show fewer schedules on the list") + linkend(); } else { m_html += link(VIEW_SCHEDULE, QString("?mode=%1").arg("full")) + i18nc("More...", "Show more schedules on the list") + linkend(); } m_html += "
"; } } m_html += "
"; } void showPaymentEntry(const MyMoneySchedule& sched, int cnt = 1) { QString tmp; MyMoneyFile* file = MyMoneyFile::instance(); try { MyMoneyAccount acc = sched.account(); if (!acc.id().isEmpty()) { MyMoneyTransaction t = sched.transaction(); // only show the entry, if it is still active if (!sched.isFinished()) { MyMoneySplit sp = t.splitByAccount(acc.id(), true); QString pathEnter = QPixmapToDataUri(Icons::get(Icon::KeyEnter).pixmap(QSize(16,16))); QString pathSkip = QPixmapToDataUri(Icons::get(Icon::MediaSkipForward).pixmap(QSize(16,16))); //show payment date tmp = QString("") + QLocale().toString(sched.adjustedNextDueDate(), QLocale::ShortFormat) + ""; if (!pathEnter.isEmpty()) tmp += link(VIEW_SCHEDULE, QString("?id=%1&mode=enter").arg(sched.id()), i18n("Enter schedule")) + QString("").arg(pathEnter) + linkend(); if (!pathSkip.isEmpty()) tmp += " " + link(VIEW_SCHEDULE, QString("?id=%1&mode=skip").arg(sched.id()), i18n("Skip schedule")) + QString("").arg(pathSkip) + linkend(); tmp += QString(" "); tmp += link(VIEW_SCHEDULE, QString("?id=%1&mode=edit").arg(sched.id()), i18n("Edit schedule")) + sched.name() + linkend(); //show quantity of payments overdue if any if (cnt > 1) tmp += i18np(" (%1 payment)", " (%1 payments)", cnt); //show account of the main split tmp += ""; tmp += QString(file->account(acc.id()).name()); //show amount of the schedule tmp += ""; const MyMoneySecurity& currency = MyMoneyFile::instance()->currency(acc.currencyId()); MyMoneyMoney payment = MyMoneyMoney(sp.value(t.commodity(), acc.currencyId()) * cnt); QString amount = MyMoneyUtils::formatMoney(payment, acc, currency); amount.replace(QChar(' '), " "); tmp += showColoredAmount(amount, payment.isNegative()); tmp += ""; //show balance after payments tmp += ""; QDate paymentDate = QDate(sched.adjustedNextDueDate()); MyMoneyMoney balanceAfter = forecastPaymentBalance(acc, payment, paymentDate); QString balance = MyMoneyUtils::formatMoney(balanceAfter, acc, currency); balance.replace(QChar(' '), " "); tmp += showColoredAmount(balance, balanceAfter.isNegative()); tmp += ""; // qDebug("paymentEntry = '%s'", tmp.toLatin1()); m_html += tmp; } } } catch (const MyMoneyException &e) { qDebug("Unable to display schedule entry: %s", qPrintable(e.what())); } } void showAccounts(paymentTypeE type, const QString& header) { MyMoneyFile* file = MyMoneyFile::instance(); int prec = MyMoneyMoney::denomToPrec(file->baseCurrency().smallestAccountFraction()); QList accounts; auto showClosedAccounts = KMyMoneySettings::showAllAccounts(); // get list of all accounts file->accountList(accounts); for (QList::Iterator it = accounts.begin(); it != accounts.end();) { bool removeAccount = false; if (!(*it).isClosed() || showClosedAccounts) { switch ((*it).accountType()) { case Account::Type::Expense: case Account::Type::Income: // never show a category account // Note: This might be different in a future version when // the homepage also shows category based information removeAccount = true; break; // Asset and Liability accounts are only shown if they // have the preferred flag set case Account::Type::Asset: case Account::Type::Liability: case Account::Type::Investment: // if preferred accounts are requested, then keep in list if ((*it).value("PreferredAccount") != "Yes" || (type & Preferred) == 0) { removeAccount = true; } break; // Check payment accounts. If payment and preferred is selected, // then always show them. If only payment is selected, then // show only if preferred flag is not set. case Account::Type::Checkings: case Account::Type::Savings: case Account::Type::Cash: case Account::Type::CreditCard: switch (type & (Payment | Preferred)) { case Payment: if ((*it).value("PreferredAccount") == "Yes") removeAccount = true; break; case Preferred: if ((*it).value("PreferredAccount") != "Yes") removeAccount = true; break; case Payment | Preferred: break; default: removeAccount = true; break; } break; // filter all accounts that are not used on homepage views default: removeAccount = true; break; } } else if ((*it).isClosed() || (*it).isInvest()) { // don't show if closed or a stock account removeAccount = true; } if (removeAccount) it = accounts.erase(it); else ++it; } if (!accounts.isEmpty()) { // sort the accounts by name qStableSort(accounts.begin(), accounts.end(), accountNameLess); QString tmp; int i = 0; tmp = "
" + header + "
\n
 
\n"; m_html += tmp; m_html += ""; m_html += ""; if (KMyMoneySettings::showBalanceStatusOfOnlineAccounts()) { QString pathStatusHeader = QPixmapToDataUri(Icons::get(Icon::Download).pixmap(QSize(16,16))); m_html += QString("").arg(pathStatusHeader); } m_html += ""; if (KMyMoneySettings::showCountOfUnmarkedTransactions()) m_html += QString(""); if (KMyMoneySettings::showCountOfClearedTransactions()) m_html += QString(""); if (KMyMoneySettings::showCountOfNotReconciledTransactions()) m_html += QString(""); if (KMyMoneySettings::showDateOfLastReconciliation()) m_html += QString("").arg(i18n("Last Reconciled")); m_html += ""; //only show limit info if user chose to do so if (KMyMoneySettings::showLimitInfo()) { m_html += ""; } m_html += ""; m_total = 0; QList::const_iterator it_m; for (it_m = accounts.constBegin(); it_m != accounts.constEnd(); ++it_m) { m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); showAccountEntry(*it_m); m_html += ""; } m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); QString amount = m_total.formatMoney(file->baseCurrency().tradingSymbol(), prec); if (KMyMoneySettings::showBalanceStatusOfOnlineAccounts()) m_html += ""; m_html += QString("").arg(i18n("Total")); if (KMyMoneySettings::showCountOfUnmarkedTransactions()) m_html += ""; if (KMyMoneySettings::showCountOfClearedTransactions()) m_html += ""; if (KMyMoneySettings::showCountOfNotReconciledTransactions()) m_html += ""; if (KMyMoneySettings::showDateOfLastReconciliation()) m_html += ""; m_html += QString("").arg(showColoredAmount(amount, m_total.isNegative())); m_html += "
"; m_html += i18n("Account"); m_html += "!MC!R%1"; m_html += i18n("Current Balance"); m_html += ""; m_html += i18n("To Minimum Balance / Maximum Credit"); m_html += "
%1%1
"; } } void showFavoriteReports() { QList reports = MyMoneyFile::instance()->reportList(); if (!reports.isEmpty()) { bool firstTime = 1; int row = 0; QList::const_iterator it_report = reports.constBegin(); while (it_report != reports.constEnd()) { if ((*it_report).isFavorite()) { if (firstTime) { m_html += QString("
%1
\n
 
\n").arg(i18n("Favorite Reports")); m_html += ""; m_html += ""; firstTime = false; } m_html += QString("") .arg(row++ & 0x01 ? "even" : "odd") .arg(link(VIEW_REPORTS, QString("?id=%1").arg((*it_report).id()))) .arg((*it_report).name()) .arg(linkend()) .arg((*it_report).comment()); } ++it_report; } if (!firstTime) m_html += "
"; m_html += i18n("Report"); m_html += ""; m_html += i18n("Comment"); m_html += "
%2%3%4%5
"; } } void showForecast() { MyMoneyFile* file = MyMoneyFile::instance(); QList accList; //if forecast has not been executed yet, do it. if (!m_forecast.isForecastDone()) doForecast(); accList = m_forecast.accountList(); if (accList.count() > 0) { // sort the accounts by name qStableSort(accList.begin(), accList.end(), accountNameLess); int i = 0; int colspan = 1; //get begin day int beginDay = QDate::currentDate().daysTo(m_forecast.beginForecastDate()); //if begin day is today skip to next cycle if (beginDay == 0) beginDay = m_forecast.accountsCycle(); // Now output header m_html += QString("
%1
\n
 
\n").arg(i18n("%1 Day Forecast", m_forecast.forecastDays())); m_html += ""; m_html += ""; int colWidth = 55 / (m_forecast.forecastDays() / m_forecast.accountsCycle()); for (i = 0; (i*m_forecast.accountsCycle() + beginDay) <= m_forecast.forecastDays(); ++i) { m_html += QString(""; colspan++; } m_html += ""; // Now output entries i = 0; QList::ConstIterator it_account; for (it_account = accList.constBegin(); it_account != accList.constEnd(); ++it_account) { //MyMoneyAccount acc = (*it_n); m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); m_html += QString(""; int dropZero = -1; //account dropped below zero int dropMinimum = -1; //account dropped below minimum balance QString minimumBalance = (*it_account).value("minimumBalance"); MyMoneyMoney minBalance = MyMoneyMoney(minimumBalance); MyMoneySecurity currency; MyMoneyMoney forecastBalance; //change account to deep currency if account is an investment if ((*it_account).isInvest()) { MyMoneySecurity underSecurity = file->security((*it_account).currencyId()); currency = file->security(underSecurity.tradingCurrency()); } else { currency = file->security((*it_account).currencyId()); } for (int f = beginDay; f <= m_forecast.forecastDays(); f += m_forecast.accountsCycle()) { forecastBalance = m_forecast.forecastBalance(*it_account, QDate::currentDate().addDays(f)); QString amount; amount = MyMoneyUtils::formatMoney(forecastBalance, *it_account, currency); amount.replace(QChar(' '), " "); m_html += QString("").arg(showColoredAmount(amount, forecastBalance.isNegative())); } m_html += ""; //Check if the account is going to be below zero or below the minimal balance in the forecast period //Check if the account is going to be below minimal balance dropMinimum = m_forecast.daysToMinimumBalance(*it_account); //Check if the account is going to be below zero in the future dropZero = m_forecast.daysToZeroBalance(*it_account); // spit out possible warnings QString msg; // if a minimum balance has been specified, an appropriate warning will // only be shown, if the drop below 0 is on a different day or not present if (dropMinimum != -1 && !minBalance.isZero() && (dropMinimum < dropZero || dropZero == -1)) { switch (dropMinimum) { case 0: msg = i18n("The balance of %1 is below the minimum balance %2 today.", (*it_account).name(), MyMoneyUtils::formatMoney(minBalance, *it_account, currency)); msg = showColoredAmount(msg, true); break; default: msg = i18np("The balance of %2 will drop below the minimum balance %3 in %1 day.", "The balance of %2 will drop below the minimum balance %3 in %1 days.", dropMinimum - 1, (*it_account).name(), MyMoneyUtils::formatMoney(minBalance, *it_account, currency)); msg = showColoredAmount(msg, true); break; } if (!msg.isEmpty()) { m_html += QString("").arg(msg).arg(colspan); } } // a drop below zero is always shown msg.clear(); switch (dropZero) { case -1: break; case 0: if ((*it_account).accountGroup() == Account::Type::Asset) { msg = i18n("The balance of %1 is below %2 today.", (*it_account).name(), MyMoneyUtils::formatMoney(MyMoneyMoney(), *it_account, currency)); msg = showColoredAmount(msg, true); break; } if ((*it_account).accountGroup() == Account::Type::Liability) { msg = i18n("The balance of %1 is above %2 today.", (*it_account).name(), MyMoneyUtils::formatMoney(MyMoneyMoney(), *it_account, currency)); break; } break; default: if ((*it_account).accountGroup() == Account::Type::Asset) { msg = i18np("The balance of %2 will drop below %3 in %1 day.", "The balance of %2 will drop below %3 in %1 days.", dropZero, (*it_account).name(), MyMoneyUtils::formatMoney(MyMoneyMoney(), *it_account, currency)); msg = showColoredAmount(msg, true); break; } if ((*it_account).accountGroup() == Account::Type::Liability) { msg = i18np("The balance of %2 will raise above %3 in %1 day.", "The balance of %2 will raise above %3 in %1 days.", dropZero, (*it_account).name(), MyMoneyUtils::formatMoney(MyMoneyMoney(), *it_account, currency)); break; } } if (!msg.isEmpty()) { m_html += QString("").arg(msg).arg(colspan); } } m_html += "
"; m_html += i18n("Account"); m_html += "").arg(colWidth); m_html += i18ncp("Forecast days", "%1 day", "%1 days", i * m_forecast.accountsCycle() + beginDay); m_html += "
") + link(VIEW_LEDGER, QString("?id=%1").arg((*it_account).id())) + (*it_account).name() + linkend() + "").arg(colWidth); m_html += QString("%1
%1
%1
"; } } QString link(const QString& view, const QString& query, const QString& _title = QString()) const { QString titlePart; QString title(_title); if (!title.isEmpty()) titlePart = QString(" title=\"%1\"").arg(title.replace(QLatin1Char(' '), " ")); return QString("").arg(view, query, titlePart); } QString linkend() const { return QStringLiteral(""); } void showAssetsLiabilities() { QList accounts; QList::ConstIterator it; QList assets; QList liabilities; MyMoneyMoney netAssets; MyMoneyMoney netLiabilities; QString fontStart, fontEnd; MyMoneyFile* file = MyMoneyFile::instance(); int prec = MyMoneyMoney::denomToPrec(file->baseCurrency().smallestAccountFraction()); int i = 0; // get list of all accounts file->accountList(accounts); for (it = accounts.constBegin(); it != accounts.constEnd();) { if (!(*it).isClosed()) { switch ((*it).accountType()) { // group all assets into one list but make sure that investment accounts always show up case Account::Type::Investment: assets << *it; break; case Account::Type::Checkings: case Account::Type::Savings: case Account::Type::Cash: case Account::Type::Asset: case Account::Type::AssetLoan: // list account if it's the last in the hierarchy or has transactions in it if ((*it).accountList().isEmpty() || (file->transactionCount((*it).id()) > 0)) { assets << *it; } break; // group the liabilities into the other case Account::Type::CreditCard: case Account::Type::Liability: case Account::Type::Loan: // list account if it's the last in the hierarchy or has transactions in it if ((*it).accountList().isEmpty() || (file->transactionCount((*it).id()) > 0)) { liabilities << *it; } break; default: break; } } ++it; } //only do it if we have assets or liabilities account if (assets.count() > 0 || liabilities.count() > 0) { // sort the accounts by name qStableSort(assets.begin(), assets.end(), accountNameLess); qStableSort(liabilities.begin(), liabilities.end(), accountNameLess); QString statusHeader; if (KMyMoneySettings::showBalanceStatusOfOnlineAccounts()) { QString pathStatusHeader; pathStatusHeader = QPixmapToDataUri(Icons::get(Icon::ViewOutbox).pixmap(QSize(16,16))); statusHeader = QString("").arg(pathStatusHeader); } //print header m_html += "
" + i18n("Assets and Liabilities Summary") + "
\n
 
\n"; m_html += ""; //column titles m_html += ""; if (KMyMoneySettings::showBalanceStatusOfOnlineAccounts()) { m_html += ""; } m_html += ""; if (KMyMoneySettings::showCountOfUnmarkedTransactions()) m_html += ""; if (KMyMoneySettings::showCountOfClearedTransactions()) m_html += ""; if (KMyMoneySettings::showCountOfNotReconciledTransactions()) m_html += ""; if (KMyMoneySettings::showDateOfLastReconciliation()) m_html += ""; m_html += ""; //intermediate row to separate both columns m_html += ""; if (KMyMoneySettings::showBalanceStatusOfOnlineAccounts()) { m_html += ""; } m_html += ""; if (KMyMoneySettings::showCountOfUnmarkedTransactions()) m_html += ""; if (KMyMoneySettings::showCountOfClearedTransactions()) m_html += ""; if (KMyMoneySettings::showCountOfNotReconciledTransactions()) m_html += ""; if (KMyMoneySettings::showDateOfLastReconciliation()) m_html += ""; m_html += ""; QString placeHolder_Status, placeHolder_Counts; if (KMyMoneySettings::showBalanceStatusOfOnlineAccounts()) placeHolder_Status = ""; if (KMyMoneySettings::showCountOfUnmarkedTransactions()) placeHolder_Counts = ""; if (KMyMoneySettings::showCountOfClearedTransactions()) placeHolder_Counts += ""; if (KMyMoneySettings::showCountOfNotReconciledTransactions()) placeHolder_Counts += ""; if (KMyMoneySettings::showDateOfLastReconciliation()) placeHolder_Counts += ""; //get asset and liability accounts QList::const_iterator asset_it = assets.constBegin(); QList::const_iterator liabilities_it = liabilities.constBegin(); for (; asset_it != assets.constEnd() || liabilities_it != liabilities.constEnd();) { m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); //write an asset account if we still have any if (asset_it != assets.constEnd()) { MyMoneyMoney value; //investment accounts consolidate the balance of its subaccounts if ((*asset_it).accountType() == Account::Type::Investment) { value = investmentBalance(*asset_it); } else { value = MyMoneyFile::instance()->balance((*asset_it).id(), QDate::currentDate()); } //calculate balance for foreign currency accounts if ((*asset_it).currencyId() != file->baseCurrency().id()) { ReportAccount repAcc = ReportAccount((*asset_it).id()); MyMoneyMoney curPrice = repAcc.baseCurrencyPrice(QDate::currentDate()); MyMoneyMoney baseValue = value * curPrice; baseValue = baseValue.convert(10000); netAssets += baseValue; } else { netAssets += value; } //show the account without minimum balance showAccountEntry(*asset_it, value, MyMoneyMoney(), false); ++asset_it; } else { //write a white space if we don't m_html += QString("%1%2").arg(placeHolder_Status).arg(placeHolder_Counts); } //leave the intermediate column empty m_html += ""; //write a liability account if (liabilities_it != liabilities.constEnd()) { MyMoneyMoney value; value = MyMoneyFile::instance()->balance((*liabilities_it).id(), QDate::currentDate()); //calculate balance if foreign currency if ((*liabilities_it).currencyId() != file->baseCurrency().id()) { ReportAccount repAcc = ReportAccount((*liabilities_it).id()); MyMoneyMoney curPrice = repAcc.baseCurrencyPrice(QDate::currentDate()); MyMoneyMoney baseValue = value * curPrice; baseValue = baseValue.convert(10000); netLiabilities += baseValue; } else { netLiabilities += value; } //show the account without minimum balance showAccountEntry(*liabilities_it, value, MyMoneyMoney(), false); ++liabilities_it; } else { //leave the space empty if we run out of liabilities m_html += QString("%1%2").arg(placeHolder_Status).arg(placeHolder_Counts); } m_html += ""; } //calculate net worth MyMoneyMoney netWorth = netAssets + netLiabilities; //format assets, liabilities and net worth QString amountAssets = netAssets.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountLiabilities = netLiabilities.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountNetWorth = netWorth.formatMoney(file->baseCurrency().tradingSymbol(), prec); amountAssets.replace(QChar(' '), " "); amountLiabilities.replace(QChar(' '), " "); amountNetWorth.replace(QChar(' '), " "); m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); //print total for assets m_html += QString("%1%3").arg(placeHolder_Status).arg(i18n("Total Assets")).arg(placeHolder_Counts).arg(showColoredAmount(amountAssets, netAssets.isNegative())); //leave the intermediate column empty m_html += ""; //print total liabilities m_html += QString("%1%3").arg(placeHolder_Status).arg(i18n("Total Liabilities")).arg(placeHolder_Counts).arg(showColoredAmount(amountLiabilities, netLiabilities.isNegative())); m_html += ""; //print net worth m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); m_html += QString("%1%2").arg(placeHolder_Status).arg(placeHolder_Counts); m_html += QString("%1%3").arg(placeHolder_Status).arg(i18n("Net Worth")).arg(placeHolder_Counts).arg(showColoredAmount(amountNetWorth, netWorth.isNegative())); m_html += ""; m_html += "
"; m_html += statusHeader; m_html += ""; m_html += i18n("Asset Accounts"); m_html += "!MC!R" + i18n("Last Reconciled") + ""; m_html += i18n("Current Balance"); m_html += ""; m_html += statusHeader; m_html += ""; m_html += i18n("Liability Accounts"); m_html += "!MC!R" + i18n("Last Reconciled") + ""; m_html += i18n("Current Balance"); m_html += "
%2%4%2%4
%2%4
"; m_html += "
"; } } void showBudget() { MyMoneyFile* file = MyMoneyFile::instance(); if (file->countBudgets()) { int prec = MyMoneyMoney::denomToPrec(file->baseCurrency().smallestAccountFraction()); bool isOverrun = false; int i = 0; //config report just like "Monthly Budgeted vs Actual MyMoneyReport reportCfg = MyMoneyReport( MyMoneyReport::eBudgetActual, MyMoneyReport::eMonths, TransactionFilter::Date::CurrentMonth, MyMoneyReport::eDetailAll, i18n("Monthly Budgeted vs. Actual"), i18n("Generated Report")); reportCfg.setBudget("Any", true); reports::PivotTable table(reportCfg); PivotGrid grid = table.grid(); //div header m_html += "
" + i18n("Budget") + "
\n
 
\n"; //display budget summary m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += QString(""); MyMoneyMoney totalBudgetValue = grid.m_total[eBudget].m_total; MyMoneyMoney totalActualValue = grid.m_total[eActual].m_total; MyMoneyMoney totalBudgetDiffValue = grid.m_total[eBudgetDiff].m_total; QString totalBudgetAmount = totalBudgetValue.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString totalActualAmount = totalActualValue.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString totalBudgetDiffAmount = totalBudgetDiffValue.formatMoney(file->baseCurrency().tradingSymbol(), prec); m_html += QString("").arg(showColoredAmount(totalBudgetAmount, totalBudgetValue.isNegative())); m_html += QString("").arg(showColoredAmount(totalActualAmount, totalActualValue.isNegative())); m_html += QString("").arg(showColoredAmount(totalBudgetDiffAmount, totalBudgetDiffValue.isNegative())); m_html += ""; m_html += "
"; m_html += i18n("Current Month Summary"); m_html += "
"; m_html += i18n("Budgeted"); m_html += ""; m_html += i18n("Actual"); m_html += ""; m_html += i18n("Difference"); m_html += "
%1%1%1
"; //budget overrun m_html += "
 
\n"; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; PivotGrid::iterator it_outergroup = grid.begin(); while (it_outergroup != grid.end()) { i = 0; PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { PivotInnerGroup::iterator it_row = (*it_innergroup).begin(); while (it_row != (*it_innergroup).end()) { //column number is 1 because the report includes only current month if (it_row.value()[eBudgetDiff].value(1).isNegative()) { //get report account to get the name later ReportAccount rowname = it_row.key(); //write the outergroup if it is the first row of outergroup being shown if (i == 0) { m_html += ""; m_html += QString("").arg(MyMoneyAccount::accountTypeToString(rowname.accountType())); m_html += ""; } m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); //get values from grid MyMoneyMoney actualValue = it_row.value()[eActual][1]; MyMoneyMoney budgetValue = it_row.value()[eBudget][1]; MyMoneyMoney budgetDiffValue = it_row.value()[eBudgetDiff][1]; //format amounts QString actualAmount = actualValue.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString budgetAmount = budgetValue.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString budgetDiffAmount = budgetDiffValue.formatMoney(file->baseCurrency().tradingSymbol(), prec); //account name m_html += QString(""; //show amounts m_html += QString("").arg(showColoredAmount(budgetAmount, budgetValue.isNegative())); m_html += QString("").arg(showColoredAmount(actualAmount, actualValue.isNegative())); m_html += QString("").arg(showColoredAmount(budgetDiffAmount, budgetDiffValue.isNegative())); m_html += ""; //set the flag that there are overruns isOverrun = true; } ++it_row; } ++it_innergroup; } ++it_outergroup; } //if no negative differences are found, then inform that if (!isOverrun) { - m_html += QString("").arg(i++ & 0x01 ? "even" : "odd"); - m_html += QString("").arg(i18n("No Budget Categories have been overrun")); + m_html += QString::fromLatin1("").arg(((i++ & 1) == 1) ? QLatin1String("even") : QLatin1String("odd")); + m_html += QString::fromLatin1("").arg(i18n("No Budget Categories have been overrun")); m_html += ""; } m_html += "
"; m_html += i18n("Budget Overruns"); m_html += "
"; m_html += i18n("Account"); m_html += ""; m_html += i18n("Budgeted"); m_html += ""; m_html += i18n("Actual"); m_html += ""; m_html += i18n("Difference"); m_html += "
%1
") + link(VIEW_LEDGER, QString("?id=%1").arg(rowname.id())) + rowname.name() + linkend() + "%1%1%1
%1
%1
"; } } void showCashFlowSummary() { MyMoneyTransactionFilter filter; MyMoneyMoney incomeValue; MyMoneyMoney expenseValue; MyMoneyFile* file = MyMoneyFile::instance(); int prec = MyMoneyMoney::denomToPrec(file->baseCurrency().smallestAccountFraction()); //set start and end of month dates QDate startOfMonth = QDate(QDate::currentDate().year(), QDate::currentDate().month(), 1); QDate endOfMonth = QDate(QDate::currentDate().year(), QDate::currentDate().month(), QDate::currentDate().daysInMonth()); //Add total income and expenses for this month //get transactions for current month filter.setDateFilter(startOfMonth, endOfMonth); filter.setReportAllSplits(false); QList transactions = file->transactionList(filter); //if no transaction then skip and print total in zero if (transactions.size() > 0) { //get all transactions for this month foreach (const auto transaction, transactions) { //get the splits for each transaction foreach (const auto split, transaction.splits()) { if (!split.shares().isZero()) { ReportAccount repSplitAcc = ReportAccount(split.accountId()); //only add if it is an income or expense if (repSplitAcc.isIncomeExpense()) { MyMoneyMoney value; //convert to base currency if necessary if (repSplitAcc.currencyId() != file->baseCurrency().id()) { MyMoneyMoney curPrice = repSplitAcc.baseCurrencyPrice(transaction.postDate()); value = (split.shares() * MyMoneyMoney::MINUS_ONE) * curPrice; value = value.convert(10000); } else { value = (split.shares() * MyMoneyMoney::MINUS_ONE); } //store depending on account type if (repSplitAcc.accountType() == Account::Type::Income) { incomeValue += value; } else { expenseValue += value; } } } } } } //format income and expenses QString amountIncome = incomeValue.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountExpense = expenseValue.formatMoney(file->baseCurrency().tradingSymbol(), prec); amountIncome.replace(QChar(' '), " "); amountExpense.replace(QChar(' '), " "); //calculate schedules //Add all schedules for this month MyMoneyMoney scheduledIncome; MyMoneyMoney scheduledExpense; MyMoneyMoney scheduledLiquidTransfer; MyMoneyMoney scheduledOtherTransfer; //get overdues and schedules until the end of this month QList schedule = file->scheduleList(QString(), Schedule::Type::Any, Schedule::Occurrence::Any, Schedule::PaymentType::Any, QDate(), endOfMonth, false); //Remove the finished schedules QList::Iterator finished_it; for (finished_it = schedule.begin(); finished_it != schedule.end();) { if ((*finished_it).isFinished()) { finished_it = schedule.erase(finished_it); continue; } ++finished_it; } //add income and expenses QList::Iterator sched_it; for (sched_it = schedule.begin(); sched_it != schedule.end();) { QDate nextDate = (*sched_it).nextDueDate(); int cnt = 0; while (nextDate.isValid() && nextDate <= endOfMonth) { ++cnt; nextDate = (*sched_it).nextPayment(nextDate); // for single occurrence nextDate will not change, so we // better get out of here. if ((*sched_it).occurrence() == Schedule::Occurrence::Once) break; } MyMoneyAccount acc = (*sched_it).account(); if (!acc.id().isEmpty()) { MyMoneyTransaction transaction = (*sched_it).transaction(); // only show the entry, if it is still active MyMoneySplit sp = transaction.splitByAccount(acc.id(), true); // take care of the autoCalc stuff if ((*sched_it).type() == Schedule::Type::LoanPayment) { QDate nextDate = (*sched_it).nextPayment((*sched_it).lastPayment()); //make sure we have all 'starting balances' so that the autocalc works QMap balanceMap; foreach (const auto split, transaction.splits()) { auto acc = file->account(split.accountId()); // collect all overdues on the first day QDate schedDate = nextDate; if (QDate::currentDate() >= nextDate) schedDate = QDate::currentDate().addDays(1); balanceMap[acc.id()] += file->balance(acc.id(), QDate::currentDate()); } KMyMoneyUtils::calculateAutoLoan(*sched_it, transaction, balanceMap); } //go through the splits and assign to liquid or other transfers const QList splits = transaction.splits(); QList::const_iterator split_it; for (split_it = splits.constBegin(); split_it != splits.constEnd(); ++split_it) { if ((*split_it).accountId() != acc.id()) { ReportAccount repSplitAcc = ReportAccount((*split_it).accountId()); //get the shares and multiply by the quantity of occurrences in the period MyMoneyMoney value = (*split_it).shares() * cnt; //convert to foreign currency if needed if (repSplitAcc.currencyId() != file->baseCurrency().id()) { MyMoneyMoney curPrice = repSplitAcc.baseCurrencyPrice(QDate::currentDate()); value = value * curPrice; value = value.convert(10000); } if ((repSplitAcc.isLiquidLiability() || repSplitAcc.isLiquidAsset()) && acc.accountGroup() != repSplitAcc.accountGroup()) { scheduledLiquidTransfer += value; } else if (repSplitAcc.isAssetLiability() && !repSplitAcc.isLiquidLiability() && !repSplitAcc.isLiquidAsset()) { scheduledOtherTransfer += value; } else if (repSplitAcc.isIncomeExpense()) { //income and expenses are stored as negative values if (repSplitAcc.accountType() == Account::Type::Income) scheduledIncome -= value; if (repSplitAcc.accountType() == Account::Type::Expense) scheduledExpense -= value; } } } } ++sched_it; } //format the currency strings QString amountScheduledIncome = scheduledIncome.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountScheduledExpense = scheduledExpense.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountScheduledLiquidTransfer = scheduledLiquidTransfer.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountScheduledOtherTransfer = scheduledOtherTransfer.formatMoney(file->baseCurrency().tradingSymbol(), prec); amountScheduledIncome.replace(QChar(' '), " "); amountScheduledExpense.replace(QChar(' '), " "); amountScheduledLiquidTransfer.replace(QChar(' '), " "); amountScheduledOtherTransfer.replace(QChar(' '), " "); //get liquid assets and liabilities QList accounts; QList::const_iterator account_it; MyMoneyMoney liquidAssets; MyMoneyMoney liquidLiabilities; // get list of all accounts file->accountList(accounts); for (account_it = accounts.constBegin(); account_it != accounts.constEnd();) { if (!(*account_it).isClosed()) { switch ((*account_it).accountType()) { //group all assets into one list case Account::Type::Checkings: case Account::Type::Savings: case Account::Type::Cash: { MyMoneyMoney value = MyMoneyFile::instance()->balance((*account_it).id(), QDate::currentDate()); //calculate balance for foreign currency accounts if ((*account_it).currencyId() != file->baseCurrency().id()) { ReportAccount repAcc = ReportAccount((*account_it).id()); MyMoneyMoney curPrice = repAcc.baseCurrencyPrice(QDate::currentDate()); MyMoneyMoney baseValue = value * curPrice; liquidAssets += baseValue; liquidAssets = liquidAssets.convert(10000); } else { liquidAssets += value; } break; } //group the liabilities into the other case Account::Type::CreditCard: { MyMoneyMoney value; value = MyMoneyFile::instance()->balance((*account_it).id(), QDate::currentDate()); //calculate balance if foreign currency if ((*account_it).currencyId() != file->baseCurrency().id()) { ReportAccount repAcc = ReportAccount((*account_it).id()); MyMoneyMoney curPrice = repAcc.baseCurrencyPrice(QDate::currentDate()); MyMoneyMoney baseValue = value * curPrice; liquidLiabilities += baseValue; liquidLiabilities = liquidLiabilities.convert(10000); } else { liquidLiabilities += value; } break; } default: break; } } ++account_it; } //calculate net worth MyMoneyMoney liquidWorth = liquidAssets + liquidLiabilities; //format assets, liabilities and net worth QString amountLiquidAssets = liquidAssets.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountLiquidLiabilities = liquidLiabilities.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountLiquidWorth = liquidWorth.formatMoney(file->baseCurrency().tradingSymbol(), prec); amountLiquidAssets.replace(QChar(' '), " "); amountLiquidLiabilities.replace(QChar(' '), " "); amountLiquidWorth.replace(QChar(' '), " "); //show the summary m_html += "
" + i18n("Cash Flow Summary") + "
\n
 
\n"; //print header m_html += ""; //income and expense title m_html += ""; m_html += ""; //column titles m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; //add row with banding m_html += QString(""); //print current income m_html += QString("").arg(showColoredAmount(amountIncome, incomeValue.isNegative())); //print the scheduled income m_html += QString("").arg(showColoredAmount(amountScheduledIncome, scheduledIncome.isNegative())); //print current expenses m_html += QString("").arg(showColoredAmount(amountExpense, expenseValue.isNegative())); //print the scheduled expenses m_html += QString("").arg(showColoredAmount(amountScheduledExpense, scheduledExpense.isNegative())); m_html += ""; m_html += "
"; m_html += i18n("Income and Expenses of Current Month"); m_html += "
"; m_html += i18n("Income"); m_html += ""; m_html += i18n("Scheduled Income"); m_html += ""; m_html += i18n("Expenses"); m_html += ""; m_html += i18n("Scheduled Expenses"); m_html += "
%2%2%2%2
"; //print header of assets and liabilities m_html += "
 
\n"; m_html += ""; //assets and liabilities title m_html += ""; m_html += ""; //column titles m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; //add row with banding m_html += QString(""); //print current liquid assets m_html += QString("").arg(showColoredAmount(amountLiquidAssets, liquidAssets.isNegative())); //print the scheduled transfers m_html += QString("").arg(showColoredAmount(amountScheduledLiquidTransfer, scheduledLiquidTransfer.isNegative())); //print current liabilities m_html += QString("").arg(showColoredAmount(amountLiquidLiabilities, liquidLiabilities.isNegative())); //print the scheduled transfers m_html += QString("").arg(showColoredAmount(amountScheduledOtherTransfer, scheduledOtherTransfer.isNegative())); m_html += ""; m_html += "
"; m_html += i18n("Liquid Assets and Liabilities"); m_html += "
"; m_html += i18n("Liquid Assets"); m_html += ""; m_html += i18n("Transfers to Liquid Liabilities"); m_html += ""; m_html += i18n("Liquid Liabilities"); m_html += ""; m_html += i18n("Other Transfers"); m_html += "
%2%2%2%2
"; //final conclusion MyMoneyMoney profitValue = incomeValue + expenseValue + scheduledIncome + scheduledExpense; MyMoneyMoney expectedAsset = liquidAssets + scheduledIncome + scheduledExpense + scheduledLiquidTransfer + scheduledOtherTransfer; MyMoneyMoney expectedLiabilities = liquidLiabilities + scheduledLiquidTransfer; QString amountExpectedAsset = expectedAsset.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountExpectedLiabilities = expectedLiabilities.formatMoney(file->baseCurrency().tradingSymbol(), prec); QString amountProfit = profitValue.formatMoney(file->baseCurrency().tradingSymbol(), prec); amountProfit.replace(QChar(' '), " "); amountExpectedAsset.replace(QChar(' '), " "); amountExpectedLiabilities.replace(QChar(' '), " "); //print header of cash flow status m_html += "
 
\n"; m_html += ""; //income and expense title m_html += ""; m_html += ""; //column titles m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; m_html += ""; //add row with banding m_html += QString(""); m_html += ""; //print expected assets m_html += QString("").arg(showColoredAmount(amountExpectedAsset, expectedAsset.isNegative())); //print expected liabilities m_html += QString("").arg(showColoredAmount(amountExpectedLiabilities, expectedLiabilities.isNegative())); //print expected profit m_html += QString("").arg(showColoredAmount(amountProfit, profitValue.isNegative())); m_html += ""; m_html += "
"; m_html += i18n("Cash Flow Status"); m_html += "
 "; m_html += i18n("Expected Liquid Assets"); m_html += ""; m_html += i18n("Expected Liquid Liabilities"); m_html += ""; m_html += i18n("Expected Profit/Loss"); m_html += "
 %2%2%2
"; m_html += "
"; } KHomeView *q_ptr; /** * daily balances of an account */ typedef QMap dailyBalances; #ifdef ENABLE_WEBENGINE QWebEngineView *m_view; #else KWebView *m_view; #endif QString m_html; bool m_showAllSchedules; bool m_needLoad; MyMoneyForecast m_forecast; MyMoneyMoney m_total; /** * Hold the last valid size of the net worth graph * for the times when the needed size can't be computed. */ QSize m_netWorthGraphLastValidSize; /** * daily forecast balance of accounts */ QMap m_accountList; QPrinter *m_currentPrinter; }; #endif diff --git a/kmymoney/views/kscheduledview_p.h b/kmymoney/views/kscheduledview_p.h index 7121d0182..52c67d55b 100644 --- a/kmymoney/views/kscheduledview_p.h +++ b/kmymoney/views/kscheduledview_p.h @@ -1,683 +1,684 @@ /*************************************************************************** kscheduledview_p.h - description ------------------- begin : Sun Jan 27 2002 copyright : (C) 2000-2002 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. * * * ***************************************************************************/ #ifndef KSCHEDULEDVIEW_P_H #define KSCHEDULEDVIEW_P_H #include "kscheduledview.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "ui_kscheduledview.h" #include "kmymoneyviewbase_p.h" #include "kenterscheduledlg.h" #include "kbalancewarning.h" #include "transactioneditor.h" #include "kconfirmmanualenterdlg.h" #include "kmymoneymvccombo.h" #include "kmymoneyutils.h" #include "kmymoneysettings.h" #include "mymoneyexception.h" #include "kscheduletreeitem.h" #include "ktreewidgetfilterlinewidget.h" #include "icons/icons.h" #include "mymoneyutils.h" #include "mymoneyaccount.h" #include "mymoneymoney.h" #include "mymoneysecurity.h" #include "mymoneyschedule.h" #include "mymoneyfile.h" #include "mymoneypayee.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneyenums.h" #include "menuenums.h" #include "dialogenums.h" using namespace Icons; class KScheduledViewPrivate : public KMyMoneyViewBasePrivate { Q_DECLARE_PUBLIC(KScheduledView) public: explicit KScheduledViewPrivate(KScheduledView *qq) : KMyMoneyViewBasePrivate(), q_ptr(qq), ui(new Ui::KScheduledView), m_kaccPopup(nullptr), m_openBills(true), m_openDeposits(true), m_openTransfers(true), m_openLoans(true), m_needLoad(true), + m_searchWidget(nullptr), m_balanceWarning(nullptr) { } ~KScheduledViewPrivate() { if(!m_needLoad) writeConfig(); delete ui; } void init() { Q_Q(KScheduledView); m_needLoad = false; ui->setupUi(q); // create the searchline widget // and insert it into the existing layout m_searchWidget = new KTreeWidgetFilterLineWidget(q, ui->m_scheduleTree); ui->vboxLayout->insertWidget(1, m_searchWidget); //enable custom context menu ui->m_scheduleTree->setContextMenuPolicy(Qt::CustomContextMenu); ui->m_scheduleTree->setSelectionMode(QAbstractItemView::SingleSelection); readConfig(); q->connect(ui->m_qbuttonNew, &QAbstractButton::clicked, pActions[eMenu::Action::NewSchedule], &QAction::trigger); // attach popup to 'Filter...' button m_kaccPopup = new QMenu(q); ui->m_accountsCombo->setMenu(m_kaccPopup); q->connect(m_kaccPopup, &QMenu::triggered, q, &KScheduledView::slotAccountActivated); KGuiItem::assign(ui->m_qbuttonNew, KMyMoneyUtils::scheduleNewGuiItem()); KGuiItem::assign(ui->m_accountsCombo, KMyMoneyUtils::accountsFilterGuiItem()); q->connect(ui->m_scheduleTree, &QWidget::customContextMenuRequested, q, &KScheduledView::customContextMenuRequested); q->connect(ui->m_scheduleTree, &QTreeWidget::itemSelectionChanged, q, &KScheduledView::slotSetSelectedItem); q->connect(ui->m_scheduleTree, &QTreeWidget::itemDoubleClicked, q, &KScheduledView::slotListItemExecuted); q->connect(ui->m_scheduleTree, &QTreeWidget::itemExpanded, q, &KScheduledView::slotListViewExpanded); q->connect(ui->m_scheduleTree, &QTreeWidget::itemCollapsed, q, &KScheduledView::slotListViewCollapsed); q->connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KScheduledView::refresh); } static bool accountNameLessThan(const MyMoneyAccount& acc1, const MyMoneyAccount& acc2) { return acc1.name().toLower() < acc2.name().toLower(); } void refreshSchedule(bool full, const QString& schedId) { Q_Q(KScheduledView); ui->m_scheduleTree->header()->setFont(KMyMoneySettings::listHeaderFontEx()); ui->m_scheduleTree->clear(); try { if (full) { try { m_kaccPopup->clear(); MyMoneyFile* file = MyMoneyFile::instance(); // extract a list of all accounts under the asset group // and sort them by name QList list; QStringList accountList = file->asset().accountList(); accountList.append(file->liability().accountList()); file->accountList(list, accountList, true); qStableSort(list.begin(), list.end(), accountNameLessThan); QList::ConstIterator it_a; for (it_a = list.constBegin(); it_a != list.constEnd(); ++it_a) { if (!(*it_a).isClosed()) { QAction* act; act = m_kaccPopup->addAction((*it_a).name()); act->setCheckable(true); act->setChecked(true); } } } catch (const MyMoneyException &e) { KMessageBox::detailedError(q, i18n("Unable to load accounts: "), e.what()); } } MyMoneyFile *file = MyMoneyFile::instance(); QList scheduledItems = file->scheduleList(); if (scheduledItems.count() == 0) return; //disable sorting for performance ui->m_scheduleTree->setSortingEnabled(false); KScheduleTreeItem *itemBills = new KScheduleTreeItem(ui->m_scheduleTree); itemBills->setIcon(0, Icons::get(Icon::ViewExpense)); itemBills->setText(0, i18n("Bills")); itemBills->setData(0, KScheduleTreeItem::OrderRole, QVariant("0")); itemBills->setFirstColumnSpanned(true); itemBills->setFlags(Qt::ItemIsEnabled); QFont bold = itemBills->font(0); bold.setBold(true); itemBills->setFont(0, bold); KScheduleTreeItem *itemDeposits = new KScheduleTreeItem(ui->m_scheduleTree); itemDeposits->setIcon(0, Icons::get(Icon::ViewIncome)); itemDeposits->setText(0, i18n("Deposits")); itemDeposits->setData(0, KScheduleTreeItem::OrderRole, QVariant("1")); itemDeposits->setFirstColumnSpanned(true); itemDeposits->setFlags(Qt::ItemIsEnabled); itemDeposits->setFont(0, bold); KScheduleTreeItem *itemLoans = new KScheduleTreeItem(ui->m_scheduleTree); itemLoans->setIcon(0, Icons::get(Icon::ViewLoan)); itemLoans->setText(0, i18n("Loans")); itemLoans->setData(0, KScheduleTreeItem::OrderRole, QVariant("2")); itemLoans->setFirstColumnSpanned(true); itemLoans->setFlags(Qt::ItemIsEnabled); itemLoans->setFont(0, bold); KScheduleTreeItem *itemTransfers = new KScheduleTreeItem(ui->m_scheduleTree); itemTransfers->setIcon(0, Icons::get(Icon::ViewFinancialTransfer)); itemTransfers->setText(0, i18n("Transfers")); itemTransfers->setData(0, KScheduleTreeItem::OrderRole, QVariant("3")); itemTransfers->setFirstColumnSpanned(true); itemTransfers->setFlags(Qt::ItemIsEnabled); itemTransfers->setFont(0, bold); QList::Iterator it; QTreeWidgetItem *openItem = 0; for (it = scheduledItems.begin(); it != scheduledItems.end(); ++it) { MyMoneySchedule schedData = (*it); QTreeWidgetItem* item = 0; bool bContinue = true; QStringList::iterator accIt; for (accIt = m_filterAccounts.begin(); accIt != m_filterAccounts.end(); ++accIt) { if (*accIt == schedData.account().id()) { bContinue = false; // Filter it out break; } } if (!bContinue) continue; QTreeWidgetItem* parent = 0; switch (schedData.type()) { case eMyMoney::Schedule::Type::Any: // Should we display an error ? // We just sort it as bill and fall through here case eMyMoney::Schedule::Type::Bill: parent = itemBills; break; case eMyMoney::Schedule::Type::Deposit: parent = itemDeposits; break; case eMyMoney::Schedule::Type::Transfer: parent = itemTransfers; break; case eMyMoney::Schedule::Type::LoanPayment: parent = itemLoans; break; } if (parent) { if (!KMyMoneySettings::hideFinishedSchedules() || !schedData.isFinished()) { item = addScheduleItem(parent, schedData); if (schedData.id() == schedId) openItem = item; } } } if (openItem) { ui->m_scheduleTree->setCurrentItem(openItem); } // using a timeout is the only way, I got the 'ensureTransactionVisible' // working when coming from hidden form to visible form. I assume, this // has something to do with the delayed update of the display somehow. q->resize(q->width(), q->height() - 1); QTimer::singleShot(10, q, SLOT(slotTimerDone())); ui->m_scheduleTree->update(); // force repaint in case the filter is set m_searchWidget->searchLine()->updateSearch(QString()); if (m_openBills) itemBills->setExpanded(true); if (m_openDeposits) itemDeposits->setExpanded(true); if (m_openTransfers) itemTransfers->setExpanded(true); if (m_openLoans) itemLoans->setExpanded(true); } catch (const MyMoneyException &e) { KMessageBox::error(q, e.what()); } for (int i = 0; i < ui->m_scheduleTree->columnCount(); ++i) { ui->m_scheduleTree->resizeColumnToContents(i); } //reenable sorting after loading items ui->m_scheduleTree->setSortingEnabled(true); } void readConfig() { KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup grp = config->group("Last Use Settings"); m_openBills = grp.readEntry("KScheduleView_openBills", true); m_openDeposits = grp.readEntry("KScheduleView_openDeposits", true); m_openTransfers = grp.readEntry("KScheduleView_openTransfers", true); m_openLoans = grp.readEntry("KScheduleView_openLoans", true); QByteArray columns; columns = grp.readEntry("KScheduleView_treeState", columns); ui->m_scheduleTree->header()->restoreState(columns); ui->m_scheduleTree->header()->setFont(KMyMoneySettings::listHeaderFontEx()); } void writeConfig() { KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup grp = config->group("Last Use Settings"); grp.writeEntry("KScheduleView_openBills", m_openBills); grp.writeEntry("KScheduleView_openDeposits", m_openDeposits); grp.writeEntry("KScheduleView_openTransfers", m_openTransfers); grp.writeEntry("KScheduleView_openLoans", m_openLoans); QByteArray columns = ui->m_scheduleTree->header()->saveState(); grp.writeEntry("KScheduleView_treeState", columns); config->sync(); } QTreeWidgetItem* addScheduleItem(QTreeWidgetItem* parent, MyMoneySchedule& schedule) { KScheduleTreeItem* item = new KScheduleTreeItem(parent); item->setData(0, Qt::UserRole, QVariant::fromValue(schedule)); item->setData(0, KScheduleTreeItem::OrderRole, schedule.name()); if (!schedule.isFinished()) { if (schedule.isOverdue()) { item->setIcon(0, Icons::get(Icon::ViewUpcominEvents)); QBrush brush = item->foreground(0); brush.setColor(Qt::red); for (int i = 0; i < ui->m_scheduleTree->columnCount(); ++i) { item->setForeground(i, brush); } } else { item->setIcon(0, Icons::get(Icon::ViewCalendarDay)); } } else { item->setIcon(0, Icons::get(Icon::DialogClose)); QBrush brush = item->foreground(0); brush.setColor(Qt::darkGreen); for (int i = 0; i < ui->m_scheduleTree->columnCount(); ++i) { item->setForeground(i, brush); } } try { MyMoneyTransaction transaction = schedule.transaction(); MyMoneySplit s1 = (transaction.splits().size() < 1) ? MyMoneySplit() : transaction.splits()[0]; MyMoneySplit s2 = (transaction.splits().size() < 2) ? MyMoneySplit() : transaction.splits()[1]; MyMoneySplit split; MyMoneyAccount acc; switch (schedule.type()) { case eMyMoney::Schedule::Type::Deposit: if (s1.value().isNegative()) split = s2; else split = s1; break; case eMyMoney::Schedule::Type::LoanPayment: { auto found = false; foreach (const auto it_split, transaction.splits()) { acc = MyMoneyFile::instance()->account(it_split.accountId()); if (acc.accountGroup() == eMyMoney::Account::Type::Asset || acc.accountGroup() == eMyMoney::Account::Type::Liability) { if (acc.accountType() != eMyMoney::Account::Type::Loan && acc.accountType() != eMyMoney::Account::Type::AssetLoan) { split = it_split; found = true; break; } } } if (!found) { qWarning("Split for payment account not found in %s:%d.", __FILE__, __LINE__); } break; } default: if (!s1.value().isPositive()) split = s1; else split = s2; break; } acc = MyMoneyFile::instance()->account(split.accountId()); item->setText(0, schedule.name()); MyMoneySecurity currency = MyMoneyFile::instance()->currency(acc.currencyId()); QString accName = acc.name(); if (!accName.isEmpty()) { item->setText(1, accName); } else { item->setText(1, "---"); } item->setData(1, KScheduleTreeItem::OrderRole, QVariant(accName)); QString payeeName; if (!s1.payeeId().isEmpty()) { payeeName = MyMoneyFile::instance()->payee(s1.payeeId()).name(); item->setText(2, payeeName); } else { item->setText(2, "---"); } item->setData(2, KScheduleTreeItem::OrderRole, QVariant(payeeName)); MyMoneyMoney amount = split.shares().abs(); item->setData(3, Qt::UserRole, QVariant::fromValue(amount)); if (!accName.isEmpty()) { item->setText(3, QString("%1 ").arg(MyMoneyUtils::formatMoney(amount, acc, currency))); } else { //there are some cases where the schedule does not have an account //in those cases the account will not have a fraction //use base currency instead item->setText(3, QString("%1 ").arg(MyMoneyUtils::formatMoney(amount, MyMoneyFile::instance()->baseCurrency()))); } item->setTextAlignment(3, Qt::AlignRight | Qt::AlignVCenter); item->setData(3, KScheduleTreeItem::OrderRole, QVariant::fromValue(amount)); // Do the real next payment like ms-money etc QDate nextDueDate; if (schedule.isFinished()) { item->setText(4, i18nc("Finished schedule", "Finished")); } else { nextDueDate = schedule.adjustedNextDueDate(); item->setText(4, QLocale().toString(schedule.adjustedNextDueDate(), QLocale::ShortFormat)); } item->setData(4, KScheduleTreeItem::OrderRole, QVariant(nextDueDate)); item->setText(5, i18nc("Frequency of schedule", schedule.occurrenceToString().toLatin1())); item->setText(6, KMyMoneyUtils::paymentMethodToString(schedule.paymentType())); } catch (const MyMoneyException &e) { item->setText(0, "Error:"); item->setText(1, e.what()); } return item; } /** * This method allows to enter the next scheduled transaction of * the given schedule @a s. In case @a extendedKeys is @a true, * the given schedule can also be skipped or ignored. * If @a autoEnter is @a true and the schedule does not contain * an estimated value, the schedule is entered as is without further * interaction with the user. In all other cases, the user will * be presented a dialog and allowed to adjust the values for this * instance of the schedule. * * The transaction will be created and entered into the ledger * and the schedule updated. */ eDialogs::ScheduleResultCode enterSchedule(MyMoneySchedule& schedule, bool autoEnter = false, bool extendedKeys = false) { Q_Q(KScheduledView); auto rc = eDialogs::ScheduleResultCode::Cancel; if (!schedule.id().isEmpty()) { try { schedule = MyMoneyFile::instance()->schedule(schedule.id()); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(q, i18n("Unable to enter scheduled transaction '%1'", schedule.name()), e.what()); return rc; } QPointer dlg = new KEnterScheduleDlg(q, schedule); try { QDate origDueDate = schedule.nextDueDate(); dlg->showExtendedKeys(extendedKeys); QPointer transactionEditor = dlg->startEdit(); if (transactionEditor) { KMyMoneyMVCCombo::setSubstringSearchForChildren(dlg, !KMyMoneySettings::stringMatchFromStart()); MyMoneyTransaction torig, taccepted; transactionEditor->createTransaction(torig, dlg->transaction(), schedule.transaction().splits().isEmpty() ? MyMoneySplit() : schedule.transaction().splits().front(), true); // force actions to be available no matter what (will be updated according to the state during // slotTransactionsEnter or slotTransactionsCancel) pActions[eMenu::Action::CancelTransaction]->setEnabled(true); pActions[eMenu::Action::EnterTransaction]->setEnabled(true); KConfirmManualEnterDlg::Action action = KConfirmManualEnterDlg::ModifyOnce; if (!autoEnter || !schedule.isFixed()) { for (; dlg != 0;) { rc = eDialogs::ScheduleResultCode::Cancel; if (dlg->exec() == QDialog::Accepted && dlg != 0) { rc = dlg->resultCode(); if (rc == eDialogs::ScheduleResultCode::Enter) { transactionEditor->createTransaction(taccepted, torig, torig.splits().isEmpty() ? MyMoneySplit() : torig.splits().front(), true); // make sure to suppress comparison of some data: postDate torig.setPostDate(taccepted.postDate()); if (torig != taccepted) { QPointer cdlg = new KConfirmManualEnterDlg(schedule, q); cdlg->loadTransactions(torig, taccepted); if (cdlg->exec() == QDialog::Accepted) { action = cdlg->action(); delete cdlg; break; } delete cdlg; // the user has chosen 'cancel' during confirmation, // we go back to the editor continue; } } else if (rc == eDialogs::ScheduleResultCode::Skip) { slotTransactionsCancel(transactionEditor, schedule); skipSchedule(schedule); } else { slotTransactionsCancel(transactionEditor, schedule); } } else { if (autoEnter) { if (KMessageBox::warningYesNo(q, i18n("Are you sure you wish to stop this scheduled transaction from being entered into the register?\n\nKMyMoney will prompt you again next time it starts unless you manually enter it later.")) == KMessageBox::No) { // the user has chosen 'No' for the above question, // we go back to the editor continue; } } slotTransactionsCancel(transactionEditor, schedule); } break; } } // if we still have the editor around here, the user did not cancel if ((transactionEditor != 0) && (dlg != 0)) { MyMoneyFileTransaction ft; try { MyMoneyTransaction t; // add the new transaction switch (action) { case KConfirmManualEnterDlg::UseOriginal: // setup widgets with original transaction data transactionEditor->setTransaction(dlg->transaction(), dlg->transaction().splits().isEmpty() ? MyMoneySplit() : dlg->transaction().splits().front()); // and create a transaction based on that data taccepted = MyMoneyTransaction(); transactionEditor->createTransaction(taccepted, dlg->transaction(), dlg->transaction().splits().isEmpty() ? MyMoneySplit() : dlg->transaction().splits().front(), true); break; case KConfirmManualEnterDlg::ModifyAlways: torig = taccepted; torig.setPostDate(origDueDate); schedule.setTransaction(torig); break; case KConfirmManualEnterDlg::ModifyOnce: break; } QString newId; q->connect(transactionEditor, SIGNAL(balanceWarning(QWidget*,MyMoneyAccount,QString)), m_balanceWarning.data(), SLOT(slotShowMessage(QWidget*,MyMoneyAccount,QString))); if (transactionEditor->enterTransactions(newId, false)) { if (!newId.isEmpty()) { MyMoneyTransaction t = MyMoneyFile::instance()->transaction(newId); schedule.setLastPayment(t.postDate()); } // in case the next due date is invalid, the schedule is finished // we mark it as such by setting the next due date to one day past the end QDate nextDueDate = schedule.nextPayment(origDueDate); if (!nextDueDate.isValid()) { schedule.setNextDueDate(schedule.endDate().addDays(1)); } else { schedule.setNextDueDate(nextDueDate); } MyMoneyFile::instance()->modifySchedule(schedule); rc = eDialogs::ScheduleResultCode::Enter; // delete the editor before we emit the dataChanged() signal from the // engine. Calling this twice in a row does not hurt. delete transactionEditor; ft.commit(); } } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(q, i18n("Unable to enter scheduled transaction '%1'", schedule.name()), e.what()); } delete transactionEditor; } } } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(q, i18n("Unable to enter scheduled transaction '%1'", schedule.name()), e.what()); } delete dlg; } return rc; } void slotTransactionsCancel(TransactionEditor* editor, const MyMoneySchedule& schedule) { Q_Q(KScheduledView); // since we jump here via code, we have to make sure to react only // if the action is enabled if (pActions[eMenu::Action::CancelTransaction]->isEnabled()) { // make sure, we block the enter function pActions[eMenu::Action::EnterTransaction]->setEnabled(false); // qDebug("KMyMoneyApp::slotTransactionsCancel"); delete editor; emit q->selectByObject(schedule, eView::Intent::None); } } /** * This method allows to skip the next scheduled transaction of * the given schedule @a s. * */ void skipSchedule(MyMoneySchedule& schedule) { Q_Q(KScheduledView); if (!schedule.id().isEmpty()) { try { schedule = MyMoneyFile::instance()->schedule(schedule.id()); if (!schedule.isFinished()) { if (schedule.occurrence() != eMyMoney::Schedule::Occurrence::Once) { QDate next = schedule.nextDueDate(); if (!schedule.isFinished() && (KMessageBox::questionYesNo(q, i18n("Do you really want to skip the %1 transaction scheduled for %2?", schedule.name(), QLocale().toString(next, QLocale::ShortFormat)))) == KMessageBox::Yes) { MyMoneyFileTransaction ft; schedule.setLastPayment(next); schedule.setNextDueDate(schedule.nextPayment(next)); MyMoneyFile::instance()->modifySchedule(schedule); ft.commit(); } } } } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(q, i18n("Unable to skip scheduled transaction %1.", schedule.name()), e.what()); } } } KScheduledView *q_ptr; Ui::KScheduledView *ui; /// The selected schedule id in the list view. QMenu *m_kaccPopup; QStringList m_filterAccounts; bool m_openBills; bool m_openDeposits; bool m_openTransfers; bool m_openLoans; /** * This member holds the load state of page */ bool m_needLoad; /** * Search widget for the list */ KTreeWidgetSearchLineWidget* m_searchWidget; MyMoneySchedule m_currentSchedule; QScopedPointer m_balanceWarning; }; #endif diff --git a/kmymoney/views/ledgerdelegate.cpp b/kmymoney/views/ledgerdelegate.cpp index bd2a0da1f..b12c93986 100644 --- a/kmymoney/views/ledgerdelegate.cpp +++ b/kmymoney/views/ledgerdelegate.cpp @@ -1,798 +1,799 @@ /*************************************************************************** ledgerdelegate.cpp ------------------- begin : Sat Aug 8 2015 copyright : (C) 2015 by Thomas Baumgart email : Thomas Baumgart ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "ledgerdelegate.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include // ---------------------------------------------------------------------------- // Project Includes #include "ledgerview.h" #include "ledgermodel.h" #include "newtransactioneditor.h" static unsigned char attentionSign[] = { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x14, 0x08, 0x06, 0x00, 0x00, 0x00, 0x8D, 0x89, 0x1D, 0x0D, 0x00, 0x00, 0x00, 0x04, 0x73, 0x42, 0x49, 0x54, 0x08, 0x08, 0x08, 0x08, 0x7C, 0x08, 0x64, 0x88, 0x00, 0x00, 0x00, 0x19, 0x74, 0x45, 0x58, 0x74, 0x53, 0x6F, 0x66, 0x74, 0x77, 0x61, 0x72, 0x65, 0x00, 0x77, 0x77, 0x77, 0x2E, 0x69, 0x6E, 0x6B, 0x73, 0x63, 0x61, 0x70, 0x65, 0x2E, 0x6F, 0x72, 0x67, 0x9B, 0xEE, 0x3C, 0x1A, 0x00, 0x00, 0x02, 0x05, 0x49, 0x44, 0x41, 0x54, 0x38, 0x8D, 0xAD, 0x95, 0xBF, 0x4B, 0x5B, 0x51, 0x14, 0xC7, 0x3F, 0x2F, 0xBC, 0x97, 0x97, 0x97, 0x97, 0x77, 0xF3, 0xF2, 0x1C, 0xA4, 0x54, 0x6B, 0x70, 0x10, 0x44, 0x70, 0x2A, 0x91, 0x2E, 0x52, 0x02, 0x55, 0x8A, 0xB5, 0xA3, 0xAB, 0x38, 0x08, 0x66, 0xCC, 0xEE, 0xE0, 0xE2, 0x20, 0xB8, 0x38, 0xB8, 0xB8, 0xF8, 0x1F, 0x38, 0x29, 0xA5, 0x29, 0x74, 0x90, 0x0E, 0x0D, 0x0E, 0x22, 0x1D, 0x44, 0xA8, 0xD0, 0xD4, 0xB4, 0x58, 0x4B, 0x09, 0xF9, 0xF1, 0x4A, 0x3B, 0xD4, 0xD3, 0xE1, 0x55, 0xD3, 0x34, 0xAF, 0x49, 0x6C, 0x3D, 0xF0, 0x85, 0x7B, 0xCF, 0xFD, 0x9E, 0xEF, 0x3D, 0xE7, 0xFE, 0xD4, 0x44, 0x84, 0xDB, 0xB4, 0x48, 0x2F, 0xA4, 0x94, 0xAB, 0xE5, 0x52, 0xAE, 0x96, 0xEB, 0x49, 0x51, 0x44, 0x3A, 0x02, 0x18, 0x88, 0xC7, 0xF1, 0xE3, 0x71, 0x7C, 0x60, 0xA0, 0x1B, 0xBF, 0x6B, 0x86, 0x49, 0xC5, 0x46, 0x3E, 0x47, 0x34, 0x9F, 0x23, 0x9A, 0x54, 0x6C, 0xFC, 0x57, 0x86, 0x40, 0xC6, 0x4B, 0xE1, 0x37, 0xCA, 0x48, 0xA3, 0x8C, 0x78, 0x29, 0x7C, 0x20, 0xD3, 0x31, 0xA6, 0xD3, 0xA0, 0x52, 0x1C, 0x6D, 0x6F, 0x72, 0xD9, 0x28, 0x23, 0xFE, 0x07, 0x64, 0x7B, 0x93, 0x4B, 0xA5, 0x38, 0xFA, 0x27, 0x41, 0x60, 0x6E, 0x74, 0x84, 0x7A, 0xE5, 0x1D, 0x92, 0x54, 0x88, 0xE7, 0x22, 0xD5, 0x12, 0x32, 0x3A, 0x42, 0x1D, 0x98, 0xBB, 0x91, 0x20, 0x60, 0xDA, 0x36, 0x17, 0xFB, 0x7B, 0xC8, 0xC1, 0x4B, 0x04, 0x02, 0xBC, 0x7E, 0x81, 0xEC, 0xEF, 0x21, 0xB6, 0xCD, 0x05, 0x60, 0xF6, 0x2C, 0x68, 0x9A, 0x2C, 0xCF, 0x4C, 0xE1, 0x4B, 0x05, 0x39, 0x3F, 0x69, 0x0A, 0xBE, 0x7F, 0x83, 0x48, 0x05, 0x99, 0x99, 0xC2, 0x37, 0x4D, 0x96, 0x7B, 0x12, 0x04, 0xFA, 0x2D, 0x8B, 0xC6, 0xE9, 0x61, 0x10, 0x2C, 0x15, 0xC4, 0x8A, 0x21, 0x86, 0x8E, 0xFC, 0xF8, 0x12, 0xF4, 0x4F, 0x0F, 0x11, 0xCB, 0xA2, 0x01, 0xF4, 0x77, 0x3D, 0x36, 0x4E, 0x82, 0xF5, 0xA5, 0x05, 0x8C, 0xE1, 0x74, 0xD3, 0x37, 0x34, 0x18, 0x20, 0xF2, 0x8B, 0x3D, 0x9C, 0x86, 0xA5, 0x05, 0x0C, 0x27, 0xC1, 0x7A, 0xC7, 0x63, 0x03, 0x8C, 0x2B, 0x07, 0xBF, 0x5A, 0x6A, 0x66, 0x27, 0x15, 0x64, 0x3A, 0x8B, 0x3C, 0x7A, 0xD8, 0xEA, 0xAB, 0x96, 0x10, 0xE5, 0xE0, 0x03, 0xE3, 0x7F, 0xCD, 0x50, 0x39, 0x6C, 0xAD, 0xAD, 0x10, 0x53, 0xAA, 0x75, 0xD2, 0xF4, 0xBD, 0x00, 0x2D, 0x5C, 0x05, 0x6B, 0x2B, 0xC4, 0x94, 0xC3, 0xD6, 0xEF, 0xFE, 0x6B, 0x41, 0x4D, 0xD3, 0x66, 0xFB, 0x3C, 0xC6, 0x16, 0xE7, 0xDB, 0x97, 0x61, 0xE2, 0x3E, 0x3C, 0xC8, 0xB4, 0x15, 0xC7, 0xE2, 0x3C, 0x91, 0x3E, 0x8F, 0x31, 0x4D, 0xD3, 0x66, 0x5B, 0x4A, 0x06, 0x8C, 0x84, 0xCD, 0x59, 0x61, 0xA7, 0xB5, 0xAC, 0x2B, 0x9C, 0x1C, 0x04, 0x08, 0x1B, 0x2B, 0xEC, 0x20, 0x09, 0x9B, 0x33, 0xC0, 0xB8, 0xDE, 0x65, 0x43, 0x27, 0x9F, 0x9D, 0xA4, 0x1E, 0x16, 0xF0, 0xF9, 0x6D, 0xB0, 0xC3, 0x86, 0x1E, 0xB4, 0xC3, 0x38, 0xD9, 0x49, 0xEA, 0x86, 0x4E, 0xFE, 0xEA, 0x29, 0xF4, 0x2C, 0x8B, 0xDA, 0x71, 0x31, 0x9C, 0xFC, 0xF5, 0x23, 0x32, 0x34, 0x88, 0xDC, 0xBD, 0x13, 0x5C, 0xBF, 0x30, 0xCE, 0x71, 0x11, 0xB1, 0x2C, 0x6A, 0x80, 0xA7, 0xDB, 0x36, 0xAB, 0x4F, 0xA6, 0x89, 0xBA, 0x49, 0x38, 0xFF, 0xD4, 0xBE, 0x4E, 0x00, 0xAF, 0x9E, 0x81, 0x08, 0xD4, 0xEA, 0x01, 0xFE, 0x34, 0x37, 0x09, 0x4F, 0x1F, 0x13, 0xDD, 0x7D, 0xCE, 0xAA, 0x96, 0x72, 0x29, 0x7C, 0xFB, 0xCE, 0x44, 0xB8, 0xD4, 0xCD, 0x2C, 0x66, 0x52, 0xD4, 0x6E, 0xFB, 0x0B, 0xF8, 0x09, 0x63, 0x63, 0x31, 0xE4, 0x85, 0x76, 0x2E, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82 }; QColor LedgerDelegate::m_erroneousColor = QColor(Qt::red); QColor LedgerDelegate::m_importedColor = QColor(Qt::yellow); QColor LedgerDelegate::m_separatorColor = QColor(0xff, 0xf2, 0x9b); class LedgerSeparatorDate : public LedgerSeparator { public: LedgerSeparatorDate(eLedgerModel::Role role); virtual ~LedgerSeparatorDate() {} virtual bool rowHasSeparator(const QModelIndex& index) const; virtual QString separatorText(const QModelIndex& index) const; virtual void adjustBackgroundScheme(QPalette& palette, const QModelIndex& index) const; protected: QString getEntry(const QModelIndex& index, const QModelIndex& nextIndex) const; QMap m_entries; }; class LedgerSeparatorOnlineBalance : public LedgerSeparatorDate { public: LedgerSeparatorOnlineBalance(eLedgerModel::Role role); virtual ~LedgerSeparatorOnlineBalance() {} virtual bool rowHasSeparator(const QModelIndex& index) const; virtual QString separatorText(const QModelIndex& index) const; virtual void adjustBackgroundScheme(QPalette& palette, const QModelIndex& index) const; void setSeparatorData(const QDate& date, const MyMoneyMoney& amount, int fraction); private: QString m_balanceAmount; }; QDate LedgerSeparator::firstFiscalDate; bool LedgerSeparator::showFiscalDate = true; bool LedgerSeparator::showFancyDate = true; void LedgerSeparator::setFirstFiscalDate(int firstMonth, int firstDay) { firstFiscalDate = QDate(QDate::currentDate().year(), firstMonth, firstDay); if (QDate::currentDate() < firstFiscalDate) firstFiscalDate = firstFiscalDate.addYears(-1); } QModelIndex LedgerSeparator::nextIndex(const QModelIndex& index) const { const int nextRow = index.row() + 1; if (index.isValid() && (nextRow < index.model()->rowCount(QModelIndex()))) { const QAbstractItemModel* model = index.model(); return model->index(nextRow, 0, QModelIndex()); } return QModelIndex(); } LedgerSeparatorDate::LedgerSeparatorDate(eLedgerModel::Role role) : LedgerSeparator(role) { const QDate today = QDate::currentDate(); const QDate thisMonth(today.year(), today.month(), 1); const QDate lastMonth = thisMonth.addMonths(-1); const QDate yesterday = today.addDays(-1); // a = QDate::dayOfWeek() todays weekday (1 = Monday, 7 = Sunday) // b = QLocale().firstDayOfWeek() first day of week (1 = Monday, 7 = Sunday) int weekStartOfs = today.dayOfWeek() - QLocale().firstDayOfWeek(); if (weekStartOfs < 0) { weekStartOfs = 7 + weekStartOfs; } const QDate thisWeek = today.addDays(-weekStartOfs); const QDate lastWeek = thisWeek.addDays(-7); const QDate thisYear(today.year(), 1, 1); m_entries[thisYear] = i18n("This year"); m_entries[lastMonth] = i18n("Last month"); m_entries[thisMonth] = i18n("This month"); m_entries[lastWeek] = i18n("Last week"); m_entries[thisWeek] = i18n("This week"); m_entries[yesterday] = i18n("Yesterday"); m_entries[today] = i18n("Today"); m_entries[today.addDays(1)] = i18n("Future transactions"); m_entries[thisWeek.addDays(7)] = i18n("Next week"); m_entries[thisMonth.addMonths(1)] = i18n("Next month"); if (showFiscalDate && firstFiscalDate.isValid()) { m_entries[firstFiscalDate] = i18n("Current fiscal year"); m_entries[firstFiscalDate.addYears(-1)] = i18n("Previous fiscal year"); m_entries[firstFiscalDate.addYears(1)] = i18n("Next fiscal year"); } } QString LedgerSeparatorDate::getEntry(const QModelIndex& index, const QModelIndex& nextIndex) const { Q_ASSERT(index.isValid()); Q_ASSERT(nextIndex.isValid()); Q_ASSERT(index.model() == nextIndex.model()); const QAbstractItemModel* model = index.model(); QString rc; if(!m_entries.isEmpty()) { if (model->data(index, (int)m_role).toDate() != model->data(nextIndex, (int)m_role).toDate()) { const QDate key = model->data(index, (int)m_role).toDate(); const QDate endKey = model->data(nextIndex, (int)m_role).toDate(); QMap::const_iterator it = m_entries.upperBound(key); while((it != m_entries.cend()) && (it.key() <= endKey)) { rc = *it; ++it; } } } return rc; } bool LedgerSeparatorDate::rowHasSeparator(const QModelIndex& index) const { bool rc = false; if(!m_entries.isEmpty()) { QModelIndex nextIdx = nextIndex(index); if(nextIdx.isValid() ) { const QString id = nextIdx.model()->data(nextIdx, (int)eLedgerModel::Role::TransactionSplitId).toString(); // For a new transaction the id is completely empty, for a split view the transaction // part is filled but the split id is empty and the string ends with a dash // and we never draw a separator in front of that row if(!id.isEmpty() && !id.endsWith('-')) { rc = !getEntry(index, nextIdx).isEmpty(); } } } return rc; } QString LedgerSeparatorDate::separatorText(const QModelIndex& index) const { QModelIndex nextIdx = nextIndex(index); if(nextIdx.isValid()) { return getEntry(index, nextIdx); } return QString(); } void LedgerSeparatorDate::adjustBackgroundScheme(QPalette& palette, const QModelIndex& index) const { Q_UNUSED(index); KColorScheme::adjustBackground(palette, KColorScheme::ActiveBackground, QPalette::Base, KColorScheme::Button, KSharedConfigPtr()); } LedgerSeparatorOnlineBalance::LedgerSeparatorOnlineBalance(eLedgerModel::Role role) : LedgerSeparatorDate(role) { // we don't need the standard values m_entries.clear(); } void LedgerSeparatorOnlineBalance::setSeparatorData(const QDate& date, const MyMoneyMoney& amount, int fraction) { m_entries.clear(); if (date.isValid()) { m_balanceAmount = amount.formatMoney(fraction); m_entries[date] = i18n("Online statement balance: %1", m_balanceAmount); } } bool LedgerSeparatorOnlineBalance::rowHasSeparator(const QModelIndex& index) const { bool rc = false; if(!m_entries.isEmpty()) { QModelIndex nextIdx = nextIndex(index); const QAbstractItemModel* model = index.model(); const QDate date = model->data(index, (int)m_role).toDate(); // only a real transaction can have an online balance separator if(model->data(index, (int) eLedgerModel::Role::ScheduleId).toString().isEmpty()) { // if this is not the last entry and not a schedule? if(nextIdx.isValid()) { // index points to the last entry of a date rc = (date != model->data(nextIdx, (int)m_role).toDate()); if (!rc) { // in case it's the same date, we need to check if this is the last real transaction // and the next one is a scheduled transaction if(!model->data(nextIdx, (int) eLedgerModel::Role::ScheduleId).toString().isEmpty() ) { rc = true; } } if (rc) { // check if this the spot for the online balance data rc &= ((date <= m_entries.firstKey()) && (model->data(nextIdx, (int)m_role).toDate() >= m_entries.firstKey())); } } else { rc = (date <= m_entries.firstKey()); } } } return rc; } QString LedgerSeparatorOnlineBalance::separatorText(const QModelIndex& index) const { if(rowHasSeparator(index)) { return m_entries.first(); } return QString(); } void LedgerSeparatorOnlineBalance::adjustBackgroundScheme(QPalette& palette, const QModelIndex& index) const { const QAbstractItemModel* model = index.model(); QModelIndex amountIndex = model->index(index.row(), (int)eLedgerModel::Column::Balance); QString amount = model->data(amountIndex).toString(); KColorScheme::BackgroundRole role = KColorScheme::PositiveBackground; if (!m_entries.isEmpty()) { if(amount != m_balanceAmount) { role = KColorScheme::NegativeBackground; } } KColorScheme::adjustBackground(palette, role, QPalette::Base, KColorScheme::Button, KSharedConfigPtr()); } class LedgerDelegate::Private { public: Private() : m_editor(0) , m_view(0) , m_editorRow(-1) , m_separator(0) , m_onlineBalanceSeparator(0) {} ~Private() { delete m_separator; } inline bool displaySeparator(const QModelIndex& index) const { return m_separator && m_separator->rowHasSeparator(index); } inline bool displayOnlineBalanceSeparator(const QModelIndex& index) const { return m_onlineBalanceSeparator && m_onlineBalanceSeparator->rowHasSeparator(index); } NewTransactionEditor* m_editor; LedgerView* m_view; int m_editorRow; LedgerSeparator* m_separator; LedgerSeparatorOnlineBalance* m_onlineBalanceSeparator; }; LedgerDelegate::LedgerDelegate(LedgerView* parent) : QStyledItemDelegate(parent) , d(new Private) { d->m_view = parent; } LedgerDelegate::~LedgerDelegate() { delete d; } void LedgerDelegate::setSortRole(eLedgerModel::Role role) { delete d->m_separator; delete d->m_onlineBalanceSeparator; d->m_separator = 0; d->m_onlineBalanceSeparator = 0; switch(role) { case eLedgerModel::Role::PostDate: d->m_separator = new LedgerSeparatorDate(role); d->m_onlineBalanceSeparator = new LedgerSeparatorOnlineBalance(role); break; default: qDebug() << "LedgerDelegate::setSortRole role" << (int)role << "not implemented"; break; } } void LedgerDelegate::setErroneousColor(const QColor& color) { m_erroneousColor = color; } void LedgerDelegate::setOnlineBalance(const QDate& date, const MyMoneyMoney& amount, int fraction) { if(d->m_onlineBalanceSeparator) { d->m_onlineBalanceSeparator->setSeparatorData(date, amount, fraction); } } QWidget* LedgerDelegate::createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const { Q_UNUSED(option); if(index.isValid()) { if(d->m_view->selectionModel()->selectedRows().count() > 1) { qDebug() << "Editing multiple transactions at once is not yet supported"; /** * @todo replace the following three lines with the creation of a special * editor that can handle multiple transactions at once */ d->m_editor = 0; LedgerDelegate* const that = const_cast(this); emit that->closeEditor(d->m_editor, NoHint); } else { d->m_editor = new NewTransactionEditor(parent, d->m_view->accountId()); } if(d->m_editor) { d->m_editorRow = index.row(); connect(d->m_editor, SIGNAL(done()), this, SLOT(endEdit())); emit sizeHintChanged(index); } } else { qFatal("LedgerDelegate::createEditor(): we should never end up here"); } return d->m_editor; } int LedgerDelegate::editorRow() const { return d->m_editorRow; } void LedgerDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { QStyleOptionViewItem opt = option; initStyleOption(&opt, index); // never change the background of the cell the mouse is hovering over opt.state &= ~QStyle::State_MouseOver; // show the focus only on the detail column opt.state &= ~QStyle::State_HasFocus; // if selected, always show as active, so that the // background does not change when the editor is shown if (opt.state & QStyle::State_Selected) { opt.state |= QStyle::State_Active; } painter->save(); QAbstractItemView* view = qobject_cast< QAbstractItemView* >(parent()); const bool editWidgetIsVisible = d->m_view && d->m_view->indexWidget(index); const bool rowHasSeparator = d->displaySeparator(index); const bool rowHasOnlineBalance = d->displayOnlineBalanceSeparator(index); // Background QStyle *style = opt.widget ? opt.widget->style() : QApplication::style(); const int margin = style->pixelMetric(QStyle::PM_FocusFrameHMargin); const int lineHeight = opt.fontMetrics.lineSpacing() + 2; if (rowHasSeparator) { // don't draw over the separator space opt.rect.setHeight(opt.rect.height() - lineHeight ); } if (rowHasOnlineBalance) { // don't draw over the online balance space opt.rect.setHeight(opt.rect.height() - lineHeight ); } style->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, painter, opt.widget); QPalette::ColorGroup cg; // Do not paint text if the edit widget is shown if (!editWidgetIsVisible) { if(view && (index.column() == (int)eLedgerModel::Column::Detail)) { if(view->currentIndex().row() == index.row()) { opt.state |= QStyle::State_HasFocus; } } const QRect textArea = QRect(opt.rect.x() + margin, opt.rect.y() + margin, opt.rect.width() - 2 * margin, opt.rect.height() - 2 * margin); const bool selected = opt.state & QStyle::State_Selected; QStringList lines; if(index.column() == (int)eLedgerModel::Column::Detail) { lines << index.model()->data(index, (int)eLedgerModel::Role::PayeeName).toString(); if(selected) { lines << index.model()->data(index, (int)eLedgerModel::Role::CounterAccount).toString(); lines << index.model()->data(index, (int)eLedgerModel::Role::SingleLineMemo).toString(); } else { if(lines.at(0).isEmpty()) { lines.clear(); lines << index.model()->data(index, (int)eLedgerModel::Role::SingleLineMemo).toString(); } if(lines.at(0).isEmpty()) { lines << index.model()->data(index, (int)eLedgerModel::Role::CounterAccount).toString(); } } lines.removeAll(QString()); } const bool erroneous = index.model()->data(index, (int)eLedgerModel::Role::Erroneous).toBool(); // draw the text items if(!opt.text.isEmpty() || !lines.isEmpty()) { // check if it is a scheduled transaction and display it as inactive if(!index.model()->data(index, (int)eLedgerModel::Role::ScheduleId).toString().isEmpty()) { opt.state &= ~QStyle::State_Enabled; } cg = (opt.state & QStyle::State_Enabled) ? QPalette::Normal : QPalette::Disabled; if (cg == QPalette::Normal && !(opt.state & QStyle::State_Active)) { cg = QPalette::Inactive; } if (selected) { // always use the normal palette since the background is also in normal painter->setPen(opt.palette.color(QPalette::ColorGroup(QPalette::Normal), QPalette::HighlightedText)); } else if (erroneous) { painter->setPen(m_erroneousColor); } else { painter->setPen(opt.palette.color(cg, QPalette::Text)); } if (opt.state & QStyle::State_Editing) { painter->setPen(opt.palette.color(cg, QPalette::Text)); painter->drawRect(textArea.adjusted(0, 0, -1, -1)); } // collect data for the various columns if(index.column() == (int)eLedgerModel::Column::Detail) { for(int i = 0; i < lines.count(); ++i) { painter->drawText(textArea.adjusted(0, lineHeight * i, 0, 0), opt.displayAlignment, lines[i]); } } else { painter->drawText(textArea, opt.displayAlignment, opt.text); } } // draw the focus rect if(opt.state & QStyle::State_HasFocus) { QStyleOptionFocusRect o; o.QStyleOption::operator=(opt); o.rect = style->proxy()->subElementRect(QStyle::SE_ItemViewItemFocusRect, &opt, opt.widget); o.state |= QStyle::State_KeyboardFocusChange; o.state |= QStyle::State_Item; cg = (opt.state & QStyle::State_Enabled) ? QPalette::Normal : QPalette::Disabled; o.backgroundColor = opt.palette.color(cg, (opt.state & QStyle::State_Selected) ? QPalette::Highlight : QPalette::Window); style->proxy()->drawPrimitive(QStyle::PE_FrameFocusRect, &o, painter, opt.widget); } // draw the attention mark if((index.column() == (int)eLedgerModel::Column::Detail) && erroneous) { QPixmap attention; attention.loadFromData(attentionSign, sizeof(attentionSign), 0, 0); style->proxy()->drawItemPixmap(painter, option.rect, Qt::AlignRight | Qt::AlignTop, attention); } } // draw a separator if any if (rowHasOnlineBalance) { opt.rect.setY(opt.rect.y() + opt.rect.height()); opt.rect.setHeight(lineHeight); d->m_onlineBalanceSeparator->adjustBackgroundScheme(opt.palette, index); opt.backgroundBrush = opt.palette.base(); // never draw it as selected but always enabled opt.state &= ~QStyle::State_Selected; style->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, painter, opt.widget); // when the editor is shown, the row has only a single column // so we need to paint the separator if we get here in this casee bool needPaint = editWidgetIsVisible; if(!needPaint && (index.column() == (int)eLedgerModel::Column::Detail)) { needPaint = true; // adjust the rect to cover all columns if(view && view->viewport()) { opt.rect.setX(0); opt.rect.setWidth(view->viewport()->width()); } } if(needPaint) { painter->setPen(opt.palette.color(QPalette::Normal, QPalette::Text)); painter->drawText(opt.rect, Qt::AlignCenter, d->m_onlineBalanceSeparator->separatorText(index)); } } if (rowHasSeparator) { opt.rect.setY(opt.rect.y() + opt.rect.height()); opt.rect.setHeight(lineHeight); d->m_separator->adjustBackgroundScheme(opt.palette, index); opt.backgroundBrush = opt.palette.base(); // never draw it as selected but always enabled opt.state &= ~QStyle::State_Selected; style->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, painter, opt.widget); // when the editor is shown, the row has only a single column // so we need to paint the separator if we get here in this casee bool needPaint = editWidgetIsVisible; if(!needPaint && (index.column() == (int)eLedgerModel::Column::Detail)) { needPaint = true; // adjust the rect to cover all columns if(view && view->viewport()) { opt.rect.setX(0); opt.rect.setWidth(view->viewport()->width()); } } if(needPaint) { painter->setPen(opt.palette.foreground().color()); painter->drawText(opt.rect, Qt::AlignCenter, d->m_separator->separatorText(index)); } } painter->restore(); } QSize LedgerDelegate::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const { bool fullDisplay = false; if(d->m_view) { QModelIndex currentIndex = d->m_view->currentIndex(); if(currentIndex.isValid()) { QString currentId = currentIndex.model()->data(currentIndex, (int)eLedgerModel::Role::TransactionSplitId).toString(); QString myId = index.model()->data(index, (int)eLedgerModel::Role::TransactionSplitId).toString(); fullDisplay = (currentId == myId); } } QSize size; QStyleOptionViewItem opt = option; int rows = 1; initStyleOption(&opt, index); QStyle *style = opt.widget ? opt.widget->style() : QApplication::style(); const int margin = style->pixelMetric(QStyle::PM_FocusFrameHMargin); const int lineHeight = opt.fontMetrics.lineSpacing(); if(index.isValid()) { // check if we are showing the edit widget // const QAbstractItemView *view = qobject_cast(opt.widget); if (d->m_view) { QModelIndex editIndex = d->m_view->model()->index(index.row(), 0); if(editIndex.isValid()) { QWidget* editor = d->m_view->indexWidget(editIndex); if(editor) { size = editor->minimumSizeHint(); if(d->displaySeparator(index)) { // don't draw over the separator space size += QSize(0, lineHeight + margin); } if(d->displayOnlineBalanceSeparator(index)) { // don't draw over the separator space size += QSize(0, lineHeight + margin); } return size; } } } } size = QSize(100, lineHeight + 2*margin); if(fullDisplay) { auto payee = index.data((int)eLedgerModel::Role::PayeeName).toString(); auto counterAccount = index.data((int)eLedgerModel::Role::CounterAccount).toString(); auto memo = index.data((int)eLedgerModel::Role::SingleLineMemo).toString(); rows = (payee.length() > 0 ? 1 : 0) + (counterAccount.length() > 0 ? 1 : 0) + (memo.length() > 0 ? 1 : 0); // make sure we show at least one row if(!rows) { rows = 1; } // leave a few pixels as margin for each space between rows size.setHeight((size.height() * rows) - (margin * (rows - 1))); } if (d->m_separator && d->m_separator->rowHasSeparator(index)) { size.setHeight(size.height() + lineHeight + margin); } if (d->m_onlineBalanceSeparator && d->m_onlineBalanceSeparator->rowHasSeparator(index)) { size.setHeight(size.height() + lineHeight + margin); } return size; } void LedgerDelegate::updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option, const QModelIndex& index) const { Q_UNUSED(index); QStyle *style = option.widget ? option.widget->style() : QApplication::style(); const int margin = style->pixelMetric(QStyle::PM_FocusFrameHMargin); const int lineHeight = option.fontMetrics.lineSpacing(); int ofs = 8; if(d->m_view) { if(d->m_view->verticalScrollBar()->isVisible()) { ofs += d->m_view->verticalScrollBar()->width(); } } QRect r(option.rect); - r.setWidth(option.widget->width() - ofs); + if (option.widget) + r.setWidth(option.widget->width() - ofs); if(d->displaySeparator(index)) { // consider the separator space r.setHeight(r.height() - lineHeight - margin); } if(d->displayOnlineBalanceSeparator(index)) { // consider the separator space r.setHeight(r.height() - lineHeight - margin); } editor->setGeometry(r); editor->update(); } void LedgerDelegate::endEdit() { if(d->m_editor) { if(d->m_editor->accepted()) { emit commitData(d->m_editor); } emit closeEditor(d->m_editor, NoHint); d->m_editorRow = -1; } } /** * This eventfilter seems to do nothing but it prevents that selecting a * different row with the mouse closes the editor */ bool LedgerDelegate::eventFilter(QObject* o, QEvent* event) { return QAbstractItemDelegate::eventFilter(o, event); } void LedgerDelegate::setEditorData(QWidget* editWidget, const QModelIndex& index) const { NewTransactionEditor* editor = qobject_cast(editWidget); if(editor) { editor->loadTransaction(index.model()->data(index, (int)eLedgerModel::Role::TransactionSplitId).toString()); } } void LedgerDelegate::setModelData(QWidget* editWidget, QAbstractItemModel* model, const QModelIndex& index) const { Q_UNUSED(model) Q_UNUSED(index) NewTransactionEditor* editor = qobject_cast(editWidget); if(editor) { editor->saveTransaction(); } } diff --git a/kmymoney/views/ledgerview.cpp b/kmymoney/views/ledgerview.cpp index 9cfc748bb..89bacca0b 100644 --- a/kmymoney/views/ledgerview.cpp +++ b/kmymoney/views/ledgerview.cpp @@ -1,477 +1,477 @@ /*************************************************************************** ledgerview.cpp ------------------- begin : Sat Aug 8 2015 copyright : (C) 2015 by Thomas Baumgart email : Thomas Baumgart ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "ledgerview.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "ledgerproxymodel.h" #include "ledgerdelegate.h" #include "ledgermodel.h" #include "models.h" #include "mymoneymoney.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "accountsmodel.h" class LedgerView::Private { public: Private(LedgerView* p) : q(p) , delegate(0) , filterModel(new LedgerProxyModel(p)) , adjustableColumn((int)eLedgerModel::Column::Detail) , adjustingColumn(false) , showValuesInverted(false) , balanceCalculationPending(false) { filterModel->setFilterRole((int)eLedgerModel::Role::AccountId); filterModel->setSourceModel(Models::instance()->ledgerModel()); } void setDelegate(LedgerDelegate* _delegate) { delete delegate; delegate = _delegate; } void setSortRole(eLedgerModel::Role role, int column) { Q_ASSERT(delegate); Q_ASSERT(filterModel); delegate->setSortRole(role); filterModel->setSortRole((int)role); filterModel->sort(column); } void recalculateBalances() { const auto start = filterModel->index(0, 0); const auto indexes = filterModel->match(start, (int)eLedgerModel::Role::AccountId, account.id(), -1); MyMoneyMoney balance; for(const auto &index : indexes) { if(showValuesInverted) { balance -= filterModel->data(index, (int)eLedgerModel::Role::SplitShares).value(); } else { balance += filterModel->data(index, (int)eLedgerModel::Role::SplitShares).value(); } const auto txt = balance.formatMoney(account.fraction()); const auto dispIndex = filterModel->index(index.row(), (int)eLedgerModel::Column::Balance); filterModel->setData(dispIndex, txt, Qt::DisplayRole); } // filterModel->invalidate(); const QModelIndex top = filterModel->index(0, (int)eLedgerModel::Column::Balance); const QModelIndex bottom = filterModel->index(filterModel->rowCount()-1, (int)eLedgerModel::Column::Balance); q->dataChanged(top, bottom); balanceCalculationPending = false; } LedgerView* q; LedgerDelegate* delegate; LedgerProxyModel* filterModel; MyMoneyAccount account; int adjustableColumn; bool adjustingColumn; bool showValuesInverted; bool balanceCalculationPending; }; LedgerView::LedgerView(QWidget* parent) : QTableView(parent) , d(new Private(this)) { verticalHeader()->setDefaultSectionSize(15); verticalHeader()->setMinimumSectionSize(15); verticalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); verticalHeader()->hide(); // since we don't have a vertical header, it does not make sense // to use the first column to select all items in the view setCornerButtonEnabled(false); // This will allow the user to move the columns, but // the delegate cannot handle it yet and it requires to // reset the spans as well. // horizontalHeader()->setMovable(true); // make sure to get informed about resize operations on the columns connect(horizontalHeader(), SIGNAL(sectionResized(int,int,int)), this, SLOT(adjustDetailColumn())); // we don't need autoscroll as we do not support drag/drop setAutoScroll(false); setAlternatingRowColors(true); setSelectionBehavior(SelectRows); setTabKeyNavigation(false); setModel(d->filterModel); } LedgerView::~LedgerView() { delete d; } bool LedgerView::showValuesInverted() const { return d->showValuesInverted; } void LedgerView::setAccount(const MyMoneyAccount& acc) { d->account = acc; switch(acc.accountType()) { case eMyMoney::Account::Type::Investment: break; default: setColumnHidden((int)eLedgerModel::Column::Security, true); setColumnHidden((int)eLedgerModel::Column::CostCenter, true); setColumnHidden((int)eLedgerModel::Column::Quantity, true); setColumnHidden((int)eLedgerModel::Column::Price, true); setColumnHidden((int)eLedgerModel::Column::Amount, true); setColumnHidden((int)eLedgerModel::Column::Value, true); horizontalHeader()->resizeSection((int)eLedgerModel::Column::Reconciliation, 20); d->setDelegate(new LedgerDelegate(this)); setItemDelegate(d->delegate); break; } d->showValuesInverted = false; if(acc.accountGroup() == eMyMoney::Account::Type::Liability || acc.accountGroup() == eMyMoney::Account::Type::Income) { d->showValuesInverted = true; } d->filterModel->setFilterRole((int)eLedgerModel::Role::AccountId); d->filterModel->setFilterKeyColumn(0); d->filterModel->setFilterFixedString(acc.id()); d->filterModel->setAccountType(acc.accountType()); d->setSortRole(eLedgerModel::Role::PostDate, (int)eLedgerModel::Column::Date); if (acc.hasOnlineMapping()) { connect(Models::instance()->accountsModel(), &AccountsModel::dataChanged, this, &LedgerView::accountChanged); } else { disconnect(Models::instance()->accountsModel(), &AccountsModel::dataChanged, this, &LedgerView::accountChanged); d->delegate->setOnlineBalance(QDate(), MyMoneyMoney()); } accountChanged(); // if balance calculation has not been triggered, then run it immediately if(!d->balanceCalculationPending) { recalculateBalances(); } if(d->filterModel->rowCount() > 0) { // we need to check that the last row may contain a scheduled transaction or // the row that is shown for new transacations. // in that case, we need to go back to find the actual last transaction int row = d->filterModel->rowCount()-1; while(row >= 0) { const QModelIndex index = d->filterModel->index(row, 0); if(d->filterModel->data(index, (int)eLedgerModel::Role::ScheduleId).toString().isEmpty() && !d->filterModel->data(index, (int)eLedgerModel::Role::TransactionSplitId).toString().isEmpty() ) { setCurrentIndex(index); selectRow(index.row()); scrollTo(index, PositionAtBottom); break; } row--; } } } QString LedgerView::accountId() const { QString id; if(d->filterModel->filterRole() == (int)eLedgerModel::Role::AccountId) id = d->account.id(); return id; } void LedgerView::accountChanged() { QString id = accountId(); if(!id.isEmpty()) { d->account = MyMoneyFile::instance()->account(id); QDate onlineBalanceDate = QDate::fromString(d->account.value(QLatin1String("lastImportedTransactionDate")), Qt::ISODate); MyMoneyMoney amount(d->account.value(QLatin1String("lastStatementBalance"))); if (d->showValuesInverted) { amount = -amount; } d->delegate->setOnlineBalance(onlineBalanceDate, amount, d->account.fraction()); } else { d->delegate->setOnlineBalance(QDate(), MyMoneyMoney()); } // force redraw d->filterModel->invalidate(); } void LedgerView::recalculateBalances() { d->recalculateBalances(); } void LedgerView::rowsAboutToBeRemoved(const QModelIndex& index, int start, int end) { QAbstractItemView::rowsAboutToBeRemoved(index, start, end); // make sure the balances are recalculated but trigger only once if(!d->balanceCalculationPending) { d->balanceCalculationPending = true; QMetaObject::invokeMethod(this, "recalculateBalances", Qt::QueuedConnection); } } void LedgerView::rowsInserted(const QModelIndex& index, int start, int end) { QTableView::rowsInserted(index, start, end); // make sure the balances are recalculated but trigger only once if(!d->balanceCalculationPending) { d->balanceCalculationPending = true; QMetaObject::invokeMethod(this, "recalculateBalances", Qt::QueuedConnection); } } bool LedgerView::edit(const QModelIndex& index, QAbstractItemView::EditTrigger trigger, QEvent* event) { bool rc = QTableView::edit(index, trigger, event); if(rc) { // editing started, but we need the editor to cover all columns // so we close it, set the span to have a single row and recreate // the editor in that single cell closeEditor(indexWidget(index), QAbstractItemDelegate::NoHint); - bool haveEditorInOtherView = false; +// bool haveEditorInOtherView = false; /// @todo Here we need to make sure that only a single editor can be started at a time - if(!haveEditorInOtherView) { +// if(!haveEditorInOtherView) { emit aboutToStartEdit(); setSpan(index.row(), 0, 1, horizontalHeader()->count()); QModelIndex editIndex = model()->index(index.row(), 0); rc = QTableView::edit(editIndex, trigger, event); // make sure that the row gets resized according to the requirements of the editor // and is completely visible resizeRowToContents(index.row()); QMetaObject::invokeMethod(this, "ensureCurrentItemIsVisible", Qt::QueuedConnection); - } else { - rc = false; - } +// } else { +// rc = false; +// } } return rc; } void LedgerView::closeEditor(QWidget* editor, QAbstractItemDelegate::EndEditHint hint) { QTableView::closeEditor(editor, hint); clearSpans(); // we need to resize the row that contained the editor. resizeRowsToContents(); emit aboutToFinishEdit(); QMetaObject::invokeMethod(this, "ensureCurrentItemIsVisible", Qt::QueuedConnection); } void LedgerView::mousePressEvent(QMouseEvent* event) { // qDebug() << "mousePressEvent"; if(state() != QAbstractItemView::EditingState) { QTableView::mousePressEvent(event); } } void LedgerView::mouseMoveEvent(QMouseEvent* event) { // qDebug() << "mouseMoveEvent"; QTableView::mouseMoveEvent(event); } void LedgerView::mouseDoubleClickEvent(QMouseEvent* event) { // qDebug() << "mouseDoubleClickEvent"; QTableView::mouseDoubleClickEvent(event); } void LedgerView::wheelEvent(QWheelEvent* e) { // qDebug() << "wheelEvent"; QTableView::wheelEvent(e); } void LedgerView::currentChanged(const QModelIndex& current, const QModelIndex& previous) { // qDebug() << "currentChanged"; QTableView::currentChanged(current, previous); if(current.isValid()) { QModelIndex index = current.model()->index(current.row(), 0); scrollTo(index, EnsureVisible); QString id = current.model()->data(index, (int)eLedgerModel::Role::TransactionSplitId).toString(); // For a new transaction the id is completely empty, for a split view the transaction // part is filled but the split id is empty and the string ends with a dash if(id.isEmpty() || id.endsWith('-')) { edit(index); } else { emit transactionSelected(id); } QMetaObject::invokeMethod(this, "doItemsLayout", Qt::QueuedConnection); } } void LedgerView::moveEvent(QMoveEvent* event) { // qDebug() << "moveEvent"; QWidget::moveEvent(event); } void LedgerView::paintEvent(QPaintEvent* event) { QTableView::paintEvent(event); // the base class implementation paints the regular grid in case there // is room below the last line and the bottom of the viewport. We check // here if that is the case and fill that part with the base color to // remove the false painted grid. const QHeaderView *verticalHeader = this->verticalHeader(); if(verticalHeader->count() == 0) return; int lastVisualRow = verticalHeader->visualIndexAt(verticalHeader->viewport()->height()); if (lastVisualRow == -1) lastVisualRow = model()->rowCount(QModelIndex()) - 1; while(lastVisualRow >= model()->rowCount(QModelIndex())) --lastVisualRow; while ((lastVisualRow > -1) && verticalHeader->isSectionHidden(verticalHeader->logicalIndex(lastVisualRow))) --lastVisualRow; int top = 0; if(lastVisualRow != -1) top = verticalHeader->sectionViewportPosition(lastVisualRow) + verticalHeader->sectionSize(lastVisualRow); if(top < viewport()->height()) { QPainter painter(viewport()); QRect rect(0, top, viewport()->width(), viewport()->height()-top); painter.fillRect(rect, QBrush(palette().base())); } } int LedgerView::sizeHintForRow(int row) const { // we can optimize the sizeHintForRow() operation by asking the // delegate about the height. There's no need to use the std // method which scans over all items in a column and takes a long // time in large ledgers. In case the editor is open in the row, we // use the regular method. QModelIndex index = d->filterModel->index(row, 0); if(d->delegate && (d->delegate->editorRow() != row)) { QStyleOptionViewItem opt; int hint = d->delegate->sizeHint(opt, index).height(); if(showGrid()) hint += 1; return hint; } return QTableView::sizeHintForRow(row); } void LedgerView::resizeEvent(QResizeEvent* event) { // qDebug() << "resizeEvent, old:" << event->oldSize() << "new:" << event->size() << "viewport:" << viewport()->width(); QTableView::resizeEvent(event); adjustDetailColumn(event->size().width()); } void LedgerView::adjustDetailColumn() { adjustDetailColumn(viewport()->width()); } void LedgerView::adjustDetailColumn(int newViewportWidth) { // make sure we don't get here recursively if(d->adjustingColumn) return; d->adjustingColumn = true; QHeaderView* header = horizontalHeader(); int totalColumnWidth = 0; for(int i=0; i < header->count(); ++i) { if(header->isSectionHidden(i)) { continue; } totalColumnWidth += header->sectionSize(i); } const int delta = newViewportWidth - totalColumnWidth; const int newWidth = header->sectionSize(d->adjustableColumn) + delta; if(newWidth > 10) { header->resizeSection(d->adjustableColumn, newWidth); } // remember that we're done this time d->adjustingColumn = false; } void LedgerView::ensureCurrentItemIsVisible() { scrollTo(currentIndex(), EnsureVisible); } void LedgerView::setShowEntryForNewTransaction(bool show) { d->filterModel->setShowNewTransaction(show); } SplitView::SplitView(QWidget* parent) : LedgerView(parent) { } SplitView::~SplitView() { } diff --git a/kmymoney/views/ledgerviewpage.cpp b/kmymoney/views/ledgerviewpage.cpp index ab889a876..73b9827a8 100644 --- a/kmymoney/views/ledgerviewpage.cpp +++ b/kmymoney/views/ledgerviewpage.cpp @@ -1,148 +1,154 @@ /*************************************************************************** ledgerviewpage.cpp ------------------- begin : Sat Aug 8 2015 copyright : (C) 2015 by Thomas Baumgart email : Thomas Baumgart ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "ledgerviewpage.h" #include "mymoneyaccount.h" // ---------------------------------------------------------------------------- // QT Includes // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "newtransactionform.h" #include "models.h" #include "ledgermodel.h" #include "ui_ledgerviewpage.h" #include "mymoneyenums.h" class LedgerViewPage::Private { public: Private(QWidget* parent) : ui(new Ui_LedgerViewPage) , form(0) { ui->setupUi(parent); // make sure, we can disable the detail form but not the ledger view ui->splitter->setCollapsible(0, false); ui->splitter->setCollapsible(1, true); // make sure the ledger gets all the stretching ui->splitter->setStretchFactor(0, 3); ui->splitter->setStretchFactor(1, 1); ui->splitter->setSizes(QList() << 10000 << ui->formWidget->sizeHint().height()); } + + ~Private() + { + delete ui; + } + Ui_LedgerViewPage* ui; NewTransactionForm* form; QSet hideFormReasons; }; LedgerViewPage::LedgerViewPage(QWidget* parent) : QWidget(parent) , d(new Private(this)) { connect(d->ui->ledgerView, &LedgerView::transactionSelected, this, &LedgerViewPage::transactionSelected); connect(d->ui->ledgerView, &LedgerView::aboutToStartEdit, this, &LedgerViewPage::aboutToStartEdit); connect(d->ui->ledgerView, &LedgerView::aboutToFinishEdit, this, &LedgerViewPage::aboutToFinishEdit); connect(d->ui->ledgerView, &LedgerView::aboutToStartEdit, this, &LedgerViewPage::startEdit); connect(d->ui->ledgerView, &LedgerView::aboutToFinishEdit, this, &LedgerViewPage::finishEdit); connect(d->ui->splitter, &QSplitter::splitterMoved, this, &LedgerViewPage::splitterChanged); } LedgerViewPage::~LedgerViewPage() { delete d; } QString LedgerViewPage::accountId() const { return d->ui->ledgerView->accountId(); } void LedgerViewPage::setAccount(const MyMoneyAccount& acc) { // get rid of current form delete d->form; d->form = 0; d->hideFormReasons.insert(QLatin1String("FormAvailable")); switch(acc.accountType()) { case eMyMoney::Account::Type::Investment: break; default: d->form = new NewTransactionForm(d->ui->formWidget); break; } if(d->form) { d->hideFormReasons.remove(QLatin1String("FormAvailable")); // make sure we have a layout if(!d->ui->formWidget->layout()) { d->ui->formWidget->setLayout(new QHBoxLayout(d->ui->formWidget)); } d->ui->formWidget->layout()->addWidget(d->form); connect(d->ui->ledgerView, &LedgerView::transactionSelected, d->form, &NewTransactionForm::showTransaction); connect(Models::instance()->ledgerModel(), &LedgerModel::dataChanged, d->form, &NewTransactionForm::modelDataChanged); } d->ui->formWidget->setVisible(d->hideFormReasons.isEmpty()); d->ui->ledgerView->setAccount(acc); } void LedgerViewPage::showTransactionForm(bool show) { if(show) { d->hideFormReasons.remove(QLatin1String("General")); } else { d->hideFormReasons.insert(QLatin1String("General")); } d->ui->formWidget->setVisible(d->hideFormReasons.isEmpty()); } void LedgerViewPage::startEdit() { d->hideFormReasons.insert(QLatin1String("Edit")); d->ui->formWidget->hide(); } void LedgerViewPage::finishEdit() { d->hideFormReasons.remove(QLatin1String("Edit")); d->ui->formWidget->setVisible(d->hideFormReasons.isEmpty()); // the focus should be on the ledger view once editing ends d->ui->ledgerView->setFocus(); } void LedgerViewPage::splitterChanged(int pos, int index) { Q_UNUSED(pos); Q_UNUSED(index); d->ui->ledgerView->ensureCurrentItemIsVisible(); } void LedgerViewPage::setShowEntryForNewTransaction(bool show) { d->ui->ledgerView->setShowEntryForNewTransaction(show); } diff --git a/kmymoney/views/newspliteditor.cpp b/kmymoney/views/newspliteditor.cpp index 4d25e8d7a..e8f4412f2 100644 --- a/kmymoney/views/newspliteditor.cpp +++ b/kmymoney/views/newspliteditor.cpp @@ -1,385 +1,391 @@ /*************************************************************************** newspliteditor.cpp ------------------- begin : Sat Apr 9 2016 copyright : (C) 2016 by Thomas Baumgart email : Thomas Baumgart ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "newspliteditor.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "creditdebithelper.h" #include "kmymoneyutils.h" #include "kmymoneyaccountcombo.h" #include "models.h" #include "accountsmodel.h" #include "costcentermodel.h" #include "ledgermodel.h" #include "splitmodel.h" #include "mymoneyaccount.h" #include "mymoneyexception.h" #include "ui_newspliteditor.h" #include "widgethintframe.h" #include "ledgerview.h" #include "icons/icons.h" #include "mymoneyenums.h" #include "modelenums.h" using namespace Icons; struct NewSplitEditor::Private { Private(NewSplitEditor* parent) : ui(new Ui_NewSplitEditor) , accountsModel(new AccountNamesFilterProxyModel(parent)) , costCenterModel(new QSortFilterProxyModel(parent)) , splitModel(0) , accepted(false) , costCenterRequired(false) , costCenterOk(false) , showValuesInverted(false) + , amountHelper(nullptr) { accountsModel->setObjectName("AccountNamesFilterProxyModel"); costCenterModel->setObjectName("SortedCostCenterModel"); statusModel.setObjectName("StatusModel"); costCenterModel->setSortLocaleAware(true); costCenterModel->setSortCaseSensitivity(Qt::CaseInsensitive); createStatusEntry(eMyMoney::Split::State::NotReconciled); createStatusEntry(eMyMoney::Split::State::Cleared); createStatusEntry(eMyMoney::Split::State::Reconciled); // createStatusEntry(eMyMoney::Split::State::Frozen); } + ~Private() + { + delete ui; + } + void createStatusEntry(eMyMoney::Split::State status); bool checkForValidSplit(bool doUserInteraction = true); bool costCenterChanged(int costCenterIndex); bool categoryChanged(const QString& accountId); bool numberChanged(const QString& newNumber); bool amountChanged(CreditDebitHelper* valueHelper); Ui_NewSplitEditor* ui; AccountNamesFilterProxyModel* accountsModel; QSortFilterProxyModel* costCenterModel; SplitModel* splitModel; bool accepted; bool costCenterRequired; bool costCenterOk; bool showValuesInverted; QStandardItemModel statusModel; QString transactionSplitId; MyMoneyAccount counterAccount; MyMoneyAccount category; CreditDebitHelper* amountHelper; }; void NewSplitEditor::Private::createStatusEntry(eMyMoney::Split::State status) { QStandardItem* p = new QStandardItem(KMyMoneyUtils::reconcileStateToString(status, true)); p->setData((int)status); statusModel.appendRow(p); } bool NewSplitEditor::Private::checkForValidSplit(bool doUserInteraction) { QStringList infos; bool rc = true; if(!costCenterChanged(ui->costCenterCombo->currentIndex())) { infos << ui->costCenterCombo->toolTip(); rc = false; } if(doUserInteraction) { /// @todo add dialog here that shows the @a infos } return rc; } bool NewSplitEditor::Private::costCenterChanged(int costCenterIndex) { bool rc = true; WidgetHintFrame::hide(ui->costCenterCombo, i18n("The cost center this transaction should be assigned to.")); if(costCenterIndex != -1) { if(costCenterRequired && ui->costCenterCombo->currentText().isEmpty()) { WidgetHintFrame::show(ui->costCenterCombo, i18n("A cost center assignment is required for a transaction in the selected category.")); rc = false; } } return rc; } bool NewSplitEditor::Private::categoryChanged(const QString& accountId) { bool rc = true; if(!accountId.isEmpty()) { try { QModelIndex index = Models::instance()->accountsModel()->accountById(accountId); category = Models::instance()->accountsModel()->data(index, (int)eAccountsModel::Role::Account).value(); const bool isIncomeExpense = category.isIncomeExpense(); ui->costCenterCombo->setEnabled(isIncomeExpense); ui->costCenterLabel->setEnabled(isIncomeExpense); ui->numberEdit->setDisabled(isIncomeExpense); ui->numberLabel->setDisabled(isIncomeExpense); costCenterRequired = category.isCostCenterRequired(); rc &= costCenterChanged(ui->costCenterCombo->currentIndex()); } catch (MyMoneyException &e) { qDebug() << "Ooops: invalid account id" << accountId << "in" << Q_FUNC_INFO; } } return rc; } bool NewSplitEditor::Private::numberChanged(const QString& newNumber) { bool rc = true; WidgetHintFrame::hide(ui->numberEdit, i18n("The check number used for this transaction.")); if(!newNumber.isEmpty()) { const LedgerModel* model = Models::instance()->ledgerModel(); QModelIndexList list = model->match(model->index(0, 0), (int)eLedgerModel::Role::Number, QVariant(newNumber), -1, // all splits Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive | Qt::MatchRecursive)); foreach(QModelIndex index, list) { if(model->data(index, (int)eLedgerModel::Role::AccountId) == ui->accountCombo->getSelected() && model->data(index, (int)eLedgerModel::Role::TransactionSplitId) != transactionSplitId) { WidgetHintFrame::show(ui->numberEdit, i18n("The check number %1 has already been used in this account.", newNumber)); rc = false; break; } } } return rc; } bool NewSplitEditor::Private::amountChanged(CreditDebitHelper* valueHelper) { Q_UNUSED(valueHelper); bool rc = true; return rc; } NewSplitEditor::NewSplitEditor(QWidget* parent, const QString& counterAccountId) : QFrame(parent, Qt::FramelessWindowHint /* | Qt::X11BypassWindowManagerHint */) , d(new Private(this)) { SplitView* view = qobject_cast(parent->parentWidget()); Q_ASSERT(view != 0); d->splitModel = qobject_cast(view->model()); QModelIndex index = Models::instance()->accountsModel()->accountById(counterAccountId); d->counterAccount = Models::instance()->accountsModel()->data(index, (int)eAccountsModel::Role::Account).value(); d->ui->setupUi(this); d->ui->enterButton->setIcon(Icons::get(Icon::DialogOK)); d->ui->cancelButton->setIcon(Icons::get(Icon::DialogCancel)); d->accountsModel->addAccountGroup(QVector {eMyMoney::Account::Type::Asset, eMyMoney::Account::Type::Liability, eMyMoney::Account::Type::Income, eMyMoney::Account::Type::Expense, eMyMoney::Account::Type::Equity}); d->accountsModel->setHideEquityAccounts(false); auto const model = Models::instance()->accountsModel(); d->accountsModel->setSourceModel(model); d->accountsModel->setSourceColumns(model->getColumns()); d->accountsModel->sort((int)eAccountsModel::Column::Account); d->ui->accountCombo->setModel(d->accountsModel); d->costCenterModel->setSortRole(Qt::DisplayRole); d->costCenterModel->setSourceModel(Models::instance()->costCenterModel()); d->costCenterModel->sort((int)eAccountsModel::Column::Account); d->ui->costCenterCombo->setEditable(true); d->ui->costCenterCombo->setModel(d->costCenterModel); d->ui->costCenterCombo->setModelColumn(0); d->ui->costCenterCombo->completer()->setFilterMode(Qt::MatchContains); WidgetHintFrameCollection* frameCollection = new WidgetHintFrameCollection(this); frameCollection->addFrame(new WidgetHintFrame(d->ui->costCenterCombo)); frameCollection->addFrame(new WidgetHintFrame(d->ui->numberEdit, WidgetHintFrame::Warning)); frameCollection->addWidget(d->ui->enterButton); d->amountHelper = new CreditDebitHelper(this, d->ui->amountEditCredit, d->ui->amountEditDebit); connect(d->ui->numberEdit, SIGNAL(textChanged(QString)), this, SLOT(numberChanged(QString))); connect(d->ui->costCenterCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(costCenterChanged(int))); connect(d->ui->accountCombo, SIGNAL(accountSelected(QString)), this, SLOT(categoryChanged(QString))); connect(d->amountHelper, SIGNAL(valueChanged()), this, SLOT(amountChanged())); connect(d->ui->cancelButton, SIGNAL(clicked(bool)), this, SLOT(reject())); connect(d->ui->enterButton, SIGNAL(clicked(bool)), this, SLOT(acceptEdit())); } NewSplitEditor::~NewSplitEditor() { } void NewSplitEditor::setShowValuesInverted(bool inverse) { d->showValuesInverted = inverse; } bool NewSplitEditor::showValuesInverted() { return d->showValuesInverted; } bool NewSplitEditor::accepted() const { return d->accepted; } void NewSplitEditor::acceptEdit() { if(d->checkForValidSplit()) { d->accepted = true; emit done(); } } void NewSplitEditor::reject() { emit done(); } void NewSplitEditor::keyPressEvent(QKeyEvent* e) { if (!e->modifiers() || (e->modifiers() & Qt::KeypadModifier && e->key() == Qt::Key_Enter)) { switch (e->key()) { case Qt::Key_Enter: case Qt::Key_Return: { if(focusWidget() == d->ui->cancelButton) { reject(); } else { if(d->ui->enterButton->isEnabled()) { d->ui->enterButton->click(); } return; } } break; case Qt::Key_Escape: reject(); break; default: e->ignore(); return; } } else { e->ignore(); } } QString NewSplitEditor::accountId() const { return d->ui->accountCombo->getSelected(); } void NewSplitEditor::setAccountId(const QString& id) { d->ui->accountCombo->clearEditText(); d->ui->accountCombo->setSelected(id); } QString NewSplitEditor::memo() const { return d->ui->memoEdit->toPlainText(); } void NewSplitEditor::setMemo(const QString& memo) { d->ui->memoEdit->setPlainText(memo); } MyMoneyMoney NewSplitEditor::amount() const { return d->amountHelper->value(); } void NewSplitEditor::setAmount(MyMoneyMoney value) { d->amountHelper->setValue(value); } QString NewSplitEditor::costCenterId() const { const int row = d->ui->costCenterCombo->currentIndex(); QModelIndex index = d->ui->costCenterCombo->model()->index(row, 0); return d->ui->costCenterCombo->model()->data(index, CostCenterModel::CostCenterIdRole).toString(); } void NewSplitEditor::setCostCenterId(const QString& id) { QModelIndex index = Models::indexById(d->costCenterModel, CostCenterModel::CostCenterIdRole, id); if(index.isValid()) { d->ui->costCenterCombo->setCurrentIndex(index.row()); } } QString NewSplitEditor::number() const { return d->ui->numberEdit->text(); } void NewSplitEditor::setNumber(const QString& number) { d->ui->numberEdit->setText(number); } QString NewSplitEditor::splitId() const { return d->transactionSplitId; } void NewSplitEditor::numberChanged(const QString& newNumber) { d->numberChanged(newNumber); } void NewSplitEditor::categoryChanged(const QString& accountId) { d->categoryChanged(accountId); } void NewSplitEditor::costCenterChanged(int costCenterIndex) { d->costCenterChanged(costCenterIndex); } void NewSplitEditor::amountChanged() { - d->amountChanged(d->amountHelper); +// d->amountChanged(d->amountHelper); // useless call as reported by coverity scan } diff --git a/kmymoney/views/newtransactioneditor.cpp b/kmymoney/views/newtransactioneditor.cpp index 6f8bd00d8..b148d93dc 100644 --- a/kmymoney/views/newtransactioneditor.cpp +++ b/kmymoney/views/newtransactioneditor.cpp @@ -1,722 +1,728 @@ /*************************************************************************** newtransactioneditor.cpp ------------------- begin : Sat Aug 8 2015 copyright : (C) 2015 by Thomas Baumgart email : Thomas Baumgart ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "newtransactioneditor.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "creditdebithelper.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneyexception.h" #include "kmymoneyutils.h" #include "kmymoneyaccountcombo.h" #include "models.h" #include "accountsmodel.h" #include "costcentermodel.h" #include "ledgermodel.h" #include "splitmodel.h" #include "payeesmodel.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "ui_newtransactioneditor.h" #include "splitdialog.h" #include "widgethintframe.h" #include "icons/icons.h" #include "modelenums.h" #include "mymoneyenums.h" using namespace Icons; Q_GLOBAL_STATIC(QDate, lastUsedPostDate) class NewTransactionEditor::Private { public: Private(NewTransactionEditor* parent) : ui(new Ui_NewTransactionEditor) , accountsModel(new AccountNamesFilterProxyModel(parent)) , costCenterModel(new QSortFilterProxyModel(parent)) , payeesModel(new QSortFilterProxyModel(parent)) , accepted(false) , costCenterRequired(false) + , amountHelper(nullptr) { accountsModel->setObjectName("NewTransactionEditor::accountsModel"); costCenterModel->setObjectName("SortedCostCenterModel"); payeesModel->setObjectName("SortedPayeesModel"); statusModel.setObjectName("StatusModel"); splitModel.setObjectName("SplitModel"); costCenterModel->setSortLocaleAware(true); costCenterModel->setSortCaseSensitivity(Qt::CaseInsensitive); payeesModel->setSortLocaleAware(true); payeesModel->setSortCaseSensitivity(Qt::CaseInsensitive); createStatusEntry(eMyMoney::Split::State::NotReconciled); createStatusEntry(eMyMoney::Split::State::Cleared); createStatusEntry(eMyMoney::Split::State::Reconciled); // createStatusEntry(eMyMoney::Split::State::Frozen); } + ~Private() + { + delete ui; + } + void createStatusEntry(eMyMoney::Split::State status); void updateWidgetState(); bool checkForValidTransaction(bool doUserInteraction = true); bool isDatePostOpeningDate(const QDate& date, const QString& accountId); bool postdateChanged(const QDate& date); bool costCenterChanged(int costCenterIndex); bool categoryChanged(const QString& accountId); bool numberChanged(const QString& newNumber); bool valueChanged(CreditDebitHelper* valueHelper); Ui_NewTransactionEditor* ui; AccountNamesFilterProxyModel* accountsModel; QSortFilterProxyModel* costCenterModel; QSortFilterProxyModel* payeesModel; bool accepted; bool costCenterRequired; bool costCenterOk; SplitModel splitModel; QStandardItemModel statusModel; QString transactionSplitId; MyMoneyAccount account; MyMoneyTransaction transaction; MyMoneySplit split; CreditDebitHelper* amountHelper; }; void NewTransactionEditor::Private::createStatusEntry(eMyMoney::Split::State status) { QStandardItem* p = new QStandardItem(KMyMoneyUtils::reconcileStateToString(status, true)); p->setData((int)status); statusModel.appendRow(p); } void NewTransactionEditor::Private::updateWidgetState() { // just in case it is disabled we turn it on ui->costCenterCombo->setEnabled(true); // setup the category/account combo box. If we have a split transaction, we disable the // combo box altogether. Changes can only be made via the split dialog editor bool blocked = false; QModelIndex index; // update the category combo box ui->accountCombo->setEnabled(true); switch(splitModel.rowCount()) { case 0: ui->accountCombo->setSelected(QString()); break; case 1: index = splitModel.index(0, 0); ui->accountCombo->setSelected(splitModel.data(index, (int)eLedgerModel::Role::AccountId).toString()); break; default: index = splitModel.index(0, 0); blocked = ui->accountCombo->lineEdit()->blockSignals(true); ui->accountCombo->lineEdit()->setText(i18n("Split transaction")); ui->accountCombo->setDisabled(true); ui->accountCombo->lineEdit()->blockSignals(blocked); ui->costCenterCombo->setDisabled(true); ui->costCenterLabel->setDisabled(true); break; } ui->accountCombo->hidePopup(); // update the costcenter combo box if(ui->costCenterCombo->isEnabled()) { // extract the cost center index = splitModel.index(0, 0); QModelIndexList ccList = costCenterModel->match(costCenterModel->index(0, 0), CostCenterModel::CostCenterIdRole, splitModel.data(index, (int)eLedgerModel::Role::CostCenterId), 1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive | Qt::MatchRecursive)); if (ccList.count() > 0) { index = ccList.front(); ui->costCenterCombo->setCurrentIndex(index.row()); } } } bool NewTransactionEditor::Private::checkForValidTransaction(bool doUserInteraction) { QStringList infos; bool rc = true; if(!postdateChanged(ui->dateEdit->date())) { infos << ui->dateEdit->toolTip(); rc = false; } if(!costCenterChanged(ui->costCenterCombo->currentIndex())) { infos << ui->costCenterCombo->toolTip(); rc = false; } if(doUserInteraction) { /// @todo add dialog here that shows the @a infos about the problem } return rc; } bool NewTransactionEditor::Private::isDatePostOpeningDate(const QDate& date, const QString& accountId) { bool rc = true; try { MyMoneyAccount account = MyMoneyFile::instance()->account(accountId); const bool isIncomeExpense = account.isIncomeExpense(); // we don't check for categories if(!isIncomeExpense) { if(date < account.openingDate()) rc = false; } } catch (MyMoneyException &e) { qDebug() << "Ooops: invalid account id" << accountId << "in" << Q_FUNC_INFO; } return rc; } bool NewTransactionEditor::Private::postdateChanged(const QDate& date) { bool rc = true; WidgetHintFrame::hide(ui->dateEdit, i18n("The posting date of the transaction.")); // collect all account ids QStringList accountIds; accountIds << account.id(); for(int row = 0; row < splitModel.rowCount(); ++row) { QModelIndex index = splitModel.index(row, 0); accountIds << splitModel.data(index, (int)eLedgerModel::Role::AccountId).toString();; } Q_FOREACH(QString accountId, accountIds) { if(!isDatePostOpeningDate(date, accountId)) { MyMoneyAccount account = MyMoneyFile::instance()->account(accountId); WidgetHintFrame::show(ui->dateEdit, i18n("The posting date is prior to the opening date of account %1.", account.name())); rc = false; break; } } return rc; } bool NewTransactionEditor::Private::costCenterChanged(int costCenterIndex) { bool rc = true; WidgetHintFrame::hide(ui->costCenterCombo, i18n("The cost center this transaction should be assigned to.")); if(costCenterIndex != -1) { if(costCenterRequired && ui->costCenterCombo->currentText().isEmpty()) { WidgetHintFrame::show(ui->costCenterCombo, i18n("A cost center assignment is required for a transaction in the selected category.")); rc = false; } if(rc == true && splitModel.rowCount() == 1) { QModelIndex index = costCenterModel->index(costCenterIndex, 0); QString costCenterId = costCenterModel->data(index, CostCenterModel::CostCenterIdRole).toString(); index = splitModel.index(0, 0); splitModel.setData(index, costCenterId, (int)eLedgerModel::Role::CostCenterId); } } return rc; } bool NewTransactionEditor::Private::categoryChanged(const QString& accountId) { bool rc = true; if(!accountId.isEmpty() && splitModel.rowCount() <= 1) { try { MyMoneyAccount category = MyMoneyFile::instance()->account(accountId); const bool isIncomeExpense = category.isIncomeExpense(); ui->costCenterCombo->setEnabled(isIncomeExpense); ui->costCenterLabel->setEnabled(isIncomeExpense); costCenterRequired = category.isCostCenterRequired(); rc &= costCenterChanged(ui->costCenterCombo->currentIndex()); rc &= postdateChanged(ui->dateEdit->date()); // make sure we have a split in the model bool newSplit = false; if(splitModel.rowCount() == 0) { splitModel.addEmptySplitEntry(); newSplit = true; } const QModelIndex index = splitModel.index(0, 0); splitModel.setData(index, accountId, (int)eLedgerModel::Role::AccountId); if(newSplit) { costCenterChanged(ui->costCenterCombo->currentIndex()); if(amountHelper->haveValue()) { splitModel.setData(index, QVariant::fromValue(-amountHelper->value()), (int)eLedgerModel::Role::SplitValue); /// @todo make sure to convert initial value to shares according to price information splitModel.setData(index, QVariant::fromValue(-amountHelper->value()), (int)eLedgerModel::Role::SplitShares); } } /// @todo we need to make sure to support multiple currencies here } catch (MyMoneyException &e) { qDebug() << "Ooops: invalid account id" << accountId << "in" << Q_FUNC_INFO; } } return rc; } bool NewTransactionEditor::Private::numberChanged(const QString& newNumber) { bool rc = true; WidgetHintFrame::hide(ui->numberEdit, i18n("The check number used for this transaction.")); if(!newNumber.isEmpty()) { const LedgerModel* model = Models::instance()->ledgerModel(); QModelIndexList list = model->match(model->index(0, 0), (int)eLedgerModel::Role::Number, QVariant(newNumber), -1, // all splits Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive | Qt::MatchRecursive)); foreach(QModelIndex index, list) { if(model->data(index, (int)eLedgerModel::Role::AccountId) == account.id() && model->data(index, (int)eLedgerModel::Role::TransactionSplitId) != transactionSplitId) { WidgetHintFrame::show(ui->numberEdit, i18n("The check number %1 has already been used in this account.", newNumber)); rc = false; break; } } } return rc; } bool NewTransactionEditor::Private::valueChanged(CreditDebitHelper* valueHelper) { bool rc = true; if(valueHelper->haveValue() && splitModel.rowCount() <= 1) { rc = false; try { MyMoneyMoney shares; if(splitModel.rowCount() == 1) { const QModelIndex index = splitModel.index(0, 0); splitModel.setData(index, QVariant::fromValue(-amountHelper->value()), (int)eLedgerModel::Role::SplitValue); /// @todo make sure to support multiple currencies splitModel.setData(index, QVariant::fromValue(-amountHelper->value()), (int)eLedgerModel::Role::SplitShares); } else { /// @todo ask what to do: if the rest of the splits is the same amount we could simply reverse the sign /// of all splits, otherwise we could ask if the user wants to start the split editor or anything else. } rc = true; } catch (MyMoneyException &e) { qDebug() << "Ooops: somwthing went wrong in" << Q_FUNC_INFO; } } return rc; } NewTransactionEditor::NewTransactionEditor(QWidget* parent, const QString& accountId) : QFrame(parent, Qt::FramelessWindowHint /* | Qt::X11BypassWindowManagerHint */) , d(new Private(this)) { auto const model = Models::instance()->accountsModel(); // extract account information from model const auto index = model->accountById(accountId); d->account = model->data(index, (int)eAccountsModel::Role::Account).value(); d->ui->setupUi(this); d->accountsModel->addAccountGroup(QVector {eMyMoney::Account::Type::Asset, eMyMoney::Account::Type::Liability, eMyMoney::Account::Type::Income, eMyMoney::Account::Type::Expense, eMyMoney::Account::Type::Equity}); d->accountsModel->setHideEquityAccounts(false); d->accountsModel->setSourceModel(model); d->accountsModel->setSourceColumns(model->getColumns()); d->accountsModel->sort((int)eAccountsModel::Column::Account); d->ui->accountCombo->setModel(d->accountsModel); d->costCenterModel->setSortRole(Qt::DisplayRole); d->costCenterModel->setSourceModel(Models::instance()->costCenterModel()); d->costCenterModel->sort(0); d->ui->costCenterCombo->setEditable(true); d->ui->costCenterCombo->setModel(d->costCenterModel); d->ui->costCenterCombo->setModelColumn(0); d->ui->costCenterCombo->completer()->setFilterMode(Qt::MatchContains); d->payeesModel->setSortRole(Qt::DisplayRole); d->payeesModel->setSourceModel(Models::instance()->payeesModel()); d->payeesModel->sort(0); d->ui->payeeEdit->setEditable(true); d->ui->payeeEdit->setModel(d->payeesModel); d->ui->payeeEdit->setModelColumn(0); d->ui->payeeEdit->completer()->setFilterMode(Qt::MatchContains); d->ui->enterButton->setIcon(Icons::get(Icon::DialogOK)); d->ui->cancelButton->setIcon(Icons::get(Icon::DialogCancel)); d->ui->statusCombo->setModel(&d->statusModel); d->ui->dateEdit->setDisplayFormat(QLocale().dateFormat(QLocale::ShortFormat)); d->ui->amountEditCredit->setAllowEmpty(true); d->ui->amountEditDebit->setAllowEmpty(true); d->amountHelper = new CreditDebitHelper(this, d->ui->amountEditCredit, d->ui->amountEditDebit); WidgetHintFrameCollection* frameCollection = new WidgetHintFrameCollection(this); frameCollection->addFrame(new WidgetHintFrame(d->ui->dateEdit)); frameCollection->addFrame(new WidgetHintFrame(d->ui->costCenterCombo)); frameCollection->addFrame(new WidgetHintFrame(d->ui->numberEdit, WidgetHintFrame::Warning)); frameCollection->addWidget(d->ui->enterButton); connect(d->ui->numberEdit, SIGNAL(textChanged(QString)), this, SLOT(numberChanged(QString))); connect(d->ui->costCenterCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(costCenterChanged(int))); connect(d->ui->accountCombo, SIGNAL(accountSelected(QString)), this, SLOT(categoryChanged(QString))); connect(d->ui->dateEdit, SIGNAL(dateChanged(QDate)), this, SLOT(postdateChanged(QDate))); connect(d->amountHelper, SIGNAL(valueChanged()), this, SLOT(valueChanged())); connect(d->ui->cancelButton, SIGNAL(clicked(bool)), this, SLOT(reject())); connect(d->ui->enterButton, SIGNAL(clicked(bool)), this, SLOT(acceptEdit())); connect(d->ui->splitEditorButton, SIGNAL(clicked(bool)), this, SLOT(editSplits())); // handle some events in certain conditions different from default d->ui->payeeEdit->installEventFilter(this); d->ui->costCenterCombo->installEventFilter(this); d->ui->tagComboBox->installEventFilter(this); d->ui->statusCombo->installEventFilter(this); // setup tooltip // setWindowFlags(Qt::FramelessWindowHint | Qt::X11BypassWindowManagerHint); } NewTransactionEditor::~NewTransactionEditor() { } bool NewTransactionEditor::accepted() const { return d->accepted; } void NewTransactionEditor::acceptEdit() { if(d->checkForValidTransaction()) { d->accepted = true; emit done(); } } void NewTransactionEditor::reject() { emit done(); } void NewTransactionEditor::keyPressEvent(QKeyEvent* e) { if (!e->modifiers() || (e->modifiers() & Qt::KeypadModifier && e->key() == Qt::Key_Enter)) { switch (e->key()) { case Qt::Key_Enter: case Qt::Key_Return: { if(focusWidget() == d->ui->cancelButton) { reject(); } else { if(d->ui->enterButton->isEnabled()) { d->ui->enterButton->click(); } return; } } break; case Qt::Key_Escape: reject(); break; default: e->ignore(); return; } } else { e->ignore(); } } void NewTransactionEditor::loadTransaction(const QString& id) { const LedgerModel* model = Models::instance()->ledgerModel(); const QString transactionId = model->transactionIdFromTransactionSplitId(id); if(id.isEmpty()) { d->transactionSplitId.clear(); d->transaction = MyMoneyTransaction(); if(lastUsedPostDate()->isValid()) { d->ui->dateEdit->setDate(*lastUsedPostDate()); } else { d->ui->dateEdit->setDate(QDate::currentDate()); } bool blocked = d->ui->accountCombo->lineEdit()->blockSignals(true); d->ui->accountCombo->lineEdit()->clear(); d->ui->accountCombo->lineEdit()->blockSignals(blocked); } else { // find which item has this id and set is as the current item QModelIndexList list = model->match(model->index(0, 0), (int)eLedgerModel::Role::TransactionId, QVariant(transactionId), -1, // all splits Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive | Qt::MatchRecursive)); Q_FOREACH(QModelIndex index, list) { // the selected split? const QString transactionSplitId = model->data(index, (int)eLedgerModel::Role::TransactionSplitId).toString(); if(transactionSplitId == id) { d->transactionSplitId = id; d->transaction = model->data(index, (int)eLedgerModel::Role::Transaction).value(); d->split = model->data(index, (int)eLedgerModel::Role::Split).value(); d->ui->dateEdit->setDate(model->data(index, (int)eLedgerModel::Role::PostDate).toDate()); d->ui->payeeEdit->lineEdit()->setText(model->data(index, (int)eLedgerModel::Role::PayeeName).toString()); d->ui->memoEdit->clear(); d->ui->memoEdit->insertPlainText(model->data(index, (int)eLedgerModel::Role::Memo).toString()); d->ui->memoEdit->moveCursor(QTextCursor::Start); d->ui->memoEdit->ensureCursorVisible(); // The calculator for the amount field can simply be added as an icon to the line edit widget. // See http://stackoverflow.com/questions/11381865/how-to-make-an-extra-icon-in-qlineedit-like-this howto do it d->ui->amountEditCredit->setText(model->data(model->index(index.row(), (int)eLedgerModel::Column::Payment)).toString()); d->ui->amountEditDebit->setText(model->data(model->index(index.row(), (int)eLedgerModel::Column::Deposit)).toString()); d->ui->numberEdit->setText(model->data(index, (int)eLedgerModel::Role::Number).toString()); d->ui->statusCombo->setCurrentIndex(model->data(index, (int)eLedgerModel::Role::Number).toInt()); QModelIndexList stList = d->statusModel.match(d->statusModel.index(0, 0), Qt::UserRole+1, model->data(index, (int)eLedgerModel::Role::Reconciliation).toInt()); if(stList.count()) { QModelIndex stIndex = stList.front(); d->ui->statusCombo->setCurrentIndex(stIndex.row()); } } else { d->splitModel.addSplit(transactionSplitId); } } d->updateWidgetState(); } // set focus to date edit once we return to event loop QMetaObject::invokeMethod(d->ui->dateEdit, "setFocus", Qt::QueuedConnection); } void NewTransactionEditor::numberChanged(const QString& newNumber) { d->numberChanged(newNumber); } void NewTransactionEditor::categoryChanged(const QString& accountId) { d->categoryChanged(accountId); } void NewTransactionEditor::costCenterChanged(int costCenterIndex) { d->costCenterChanged(costCenterIndex); } void NewTransactionEditor::postdateChanged(const QDate& date) { d->postdateChanged(date); } void NewTransactionEditor::valueChanged() { d->valueChanged(d->amountHelper); } void NewTransactionEditor::editSplits() { SplitModel splitModel; splitModel.deepCopy(d->splitModel, true); // create an empty split at the end splitModel.addEmptySplitEntry(); QPointer splitDialog = new SplitDialog(d->account, transactionAmount(), this); splitDialog->setModel(&splitModel); int rc = splitDialog->exec(); if(splitDialog && (rc == QDialog::Accepted)) { // remove that empty split again before we update the splits splitModel.removeEmptySplitEntry(); // copy the splits model contents d->splitModel.deepCopy(splitModel, true); // update the transaction amount d->amountHelper->setValue(splitDialog->transactionAmount()); d->updateWidgetState(); QWidget *next = d->ui->tagComboBox; if(d->ui->costCenterCombo->isEnabled()) { next = d->ui->costCenterCombo; } next->setFocus(); } if(splitDialog) { splitDialog->deleteLater(); } } MyMoneyMoney NewTransactionEditor::transactionAmount() const { return d->amountHelper->value(); } void NewTransactionEditor::saveTransaction() { MyMoneyTransaction t; if(!d->transactionSplitId.isEmpty()) { t = d->transaction; } else { // we keep the date when adding a new transaction // for the next new one *lastUsedPostDate() = d->ui->dateEdit->date(); } QList splits = t.splits(); // first remove the splits that are gone foreach (const auto split, t.splits()) { if(split.id() == d->split.id()) { continue; } int row; for(row = 0; row < d->splitModel.rowCount(); ++row) { QModelIndex index = d->splitModel.index(row, 0); if(d->splitModel.data(index, (int)eLedgerModel::Role::SplitId).toString() == split.id()) { break; } } // if the split is not in the model, we get rid of it if(d->splitModel.rowCount() == row) { t.removeSplit(split); } } MyMoneyFileTransaction ft; try { // new we update the split we are opened for MyMoneySplit sp(d->split); sp.setNumber(d->ui->numberEdit->text()); sp.setMemo(d->ui->memoEdit->toPlainText()); sp.setShares(d->amountHelper->value()); if(t.commodity().isEmpty()) { t.setCommodity(d->account.currencyId()); sp.setValue(d->amountHelper->value()); } else { /// @todo check that the transactions commodity is the same /// as the one of the account this split references. If /// that is not the case, the next statement would create /// a problem sp.setValue(d->amountHelper->value()); } if(sp.reconcileFlag() != eMyMoney::Split::State::Reconciled && !sp.reconcileDate().isValid() && d->ui->statusCombo->currentIndex() == (int)eMyMoney::Split::State::Reconciled) { sp.setReconcileDate(QDate::currentDate()); } sp.setReconcileFlag(static_cast(d->ui->statusCombo->currentIndex())); // sp.setPayeeId(d->ui->payeeEdit->cu) if(sp.id().isEmpty()) { t.addSplit(sp); } else { t.modifySplit(sp); } t.setPostDate(d->ui->dateEdit->date()); // now update and add what we have in the model const SplitModel * model = &d->splitModel; for(int row = 0; row < model->rowCount(); ++row) { QModelIndex index = model->index(row, 0); MyMoneySplit s; const QString splitId = model->data(index, (int)eLedgerModel::Role::SplitId).toString(); if(!SplitModel::isNewSplitId(splitId)) { s = t.splitById(splitId); } s.setNumber(model->data(index, (int)eLedgerModel::Role::Number).toString()); s.setMemo(model->data(index, (int)eLedgerModel::Role::Memo).toString()); s.setAccountId(model->data(index, (int)eLedgerModel::Role::AccountId).toString()); s.setShares(model->data(index, (int)eLedgerModel::Role::SplitShares).value()); s.setValue(model->data(index, (int)eLedgerModel::Role::SplitValue).value()); s.setCostCenterId(model->data(index, (int)eLedgerModel::Role::CostCenterId).toString()); s.setPayeeId(model->data(index, (int)eLedgerModel::Role::PayeeId).toString()); // reconcile flag and date if(s.id().isEmpty()) { t.addSplit(s); } else { t.modifySplit(s); } } if(t.id().isEmpty()) { MyMoneyFile::instance()->addTransaction(t); } else { MyMoneyFile::instance()->modifyTransaction(t); } ft.commit(); } catch (const MyMoneyException &e) { qDebug() << Q_FUNC_INFO << "something went wrong" << e.what(); } } bool NewTransactionEditor::eventFilter(QObject* o, QEvent* e) { auto cb = qobject_cast(o); if (o) { // filter out wheel events for combo boxes if the popup view is not visible if ((e->type() == QEvent::Wheel) && !cb->view()->isVisible()) { return true; } } return QFrame::eventFilter(o, e); } // kate: space-indent on; indent-width 2; remove-trailing-space on; remove-trailing-space-save on; diff --git a/kmymoney/views/newtransactionform.cpp b/kmymoney/views/newtransactionform.cpp index 06ea50064..e8076efa8 100644 --- a/kmymoney/views/newtransactionform.cpp +++ b/kmymoney/views/newtransactionform.cpp @@ -1,102 +1,108 @@ /*************************************************************************** newtransactioneform.cpp ------------------- begin : Sat Aug 8 2015 copyright : (C) 2015 by Thomas Baumgart email : Thomas Baumgart ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "newtransactionform.h" // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "models.h" #include "ledgermodel.h" #include "modelenums.h" #include "ui_newtransactionform.h" using namespace eLedgerModel; class NewTransactionForm::Private { public: Private() : ui(new Ui_NewTransactionForm) { } + + ~Private() + { + delete ui; + } + Ui_NewTransactionForm* ui; QString transactionSplitId; }; NewTransactionForm::NewTransactionForm(QWidget* parent) : QFrame(parent) , d(new Private) { d->ui->setupUi(this); } NewTransactionForm::~NewTransactionForm() { delete d; } void NewTransactionForm::showTransaction(const QString& transactionSplitId) { d->transactionSplitId = transactionSplitId; const QAbstractItemModel* model = Models::instance()->ledgerModel(); const QModelIndexList indexes = model->match(model->index(0, 0, QModelIndex()), (int)Role::TransactionSplitId, QVariant(transactionSplitId), 1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive)); if(indexes.count() == 1) { const QModelIndex index = indexes.first(); d->ui->dateEdit->setText(QLocale().toString(model->data(index, (int)Role::PostDate).toDate(), QLocale::ShortFormat)); d->ui->payeeEdit->setText(model->data(index, (int)Role::PayeeName).toString()); d->ui->memoEdit->clear(); d->ui->memoEdit->insertPlainText(model->data(index, (int)Role::Memo).toString()); d->ui->memoEdit->moveCursor(QTextCursor::Start); d->ui->memoEdit->ensureCursorVisible(); d->ui->accountEdit->setText(model->data(index, (int)Role::CounterAccount).toString()); d->ui->statusEdit->setText(model->data(index, (int)Role::ReconciliationLong).toString()); QString amount = QString("%1 %2").arg(model->data(index, (int)Role::ShareAmount).toString()) .arg(model->data(index, (int)Role::ShareAmountSuffix).toString()); d->ui->amountEdit->setText(amount); d->ui->numberEdit->setText(model->data(index, (int)Role::Number).toString()); } } void NewTransactionForm::modelDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { const QAbstractItemModel * const model = topLeft.model(); const int startRow = topLeft.row(); const int lastRow = bottomRight.row(); for(int row = startRow; row <= lastRow; ++row) { QModelIndex index = model->index(row, 0); if(model->data(index, (int)eLedgerModel::Role::TransactionSplitId).toString() == d->transactionSplitId) { showTransaction(d->transactionSplitId); break; } } } diff --git a/kmymoney/views/simpleledgerview.cpp b/kmymoney/views/simpleledgerview.cpp index 5ecb173e9..d3dea7b6b 100644 --- a/kmymoney/views/simpleledgerview.cpp +++ b/kmymoney/views/simpleledgerview.cpp @@ -1,258 +1,259 @@ /*************************************************************************** simpleledgerview.cpp ------------------- begin : Sat Aug 8 2015 copyright : (C) 2015 by Thomas Baumgart email : Thomas Baumgart ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "simpleledgerview.h" // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneyviewbase_p.h" #include "ledgerviewpage.h" #include "models.h" #include "accountsmodel.h" #include "kmymoneyaccountcombo.h" #include "ui_simpleledgerview.h" #include "icons/icons.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneyenums.h" #include "modelenums.h" using namespace Icons; class SimpleLedgerViewPrivate : public KMyMoneyViewBasePrivate { Q_DECLARE_PUBLIC(SimpleLedgerView) public: explicit SimpleLedgerViewPrivate(SimpleLedgerView* qq) : q_ptr(qq) , ui(new Ui_SimpleLedgerView) - , newTabWidget(0) + , accountsModel(nullptr) + , newTabWidget(nullptr) , lastIdx(-1) , inModelUpdate(false) , m_needLoad(true) {} ~SimpleLedgerViewPrivate() { delete ui; } void init() { Q_Q(SimpleLedgerView); m_needLoad = false; ui->setupUi(q); ui->ledgerTab->setTabIcon(0, Icons::get(Icon::ListAdd)); ui->ledgerTab->setTabText(0, QString()); newTabWidget = ui->ledgerTab->widget(0); accountsModel= new AccountNamesFilterProxyModel(q); // remove close button from new page QTabBar* bar = ui->ledgerTab->findChild(); if(bar) { QTabBar::ButtonPosition closeSide = (QTabBar::ButtonPosition)q->style()->styleHint(QStyle::SH_TabBar_CloseButtonPosition, 0, newTabWidget); QWidget *w = bar->tabButton(0, closeSide); bar->setTabButton(0, closeSide, 0); w->deleteLater(); q->connect(bar, SIGNAL(tabMoved(int,int)), q, SLOT(checkTabOrder(int,int))); } q->connect(ui->accountCombo, SIGNAL(accountSelected(QString)), q, SLOT(openNewLedger(QString))); q->connect(ui->ledgerTab, &QTabWidget::currentChanged, q, &SimpleLedgerView::tabSelected); q->connect(Models::instance(), &Models::modelsLoaded, q, &SimpleLedgerView::updateModels); q->connect(ui->ledgerTab, &QTabWidget::tabCloseRequested, q, &SimpleLedgerView::closeLedger); accountsModel->addAccountGroup(QVector {eMyMoney::Account::Type::Asset, eMyMoney::Account::Type::Liability, eMyMoney::Account::Type::Equity}); accountsModel->setHideEquityAccounts(false); auto const model = Models::instance()->accountsModel(); accountsModel->setSourceModel(model); accountsModel->setSourceColumns(model->getColumns()); accountsModel->sort((int)eAccountsModel::Column::Account); ui->accountCombo->setModel(accountsModel); q->tabSelected(0); q->updateModels(); q->openFavoriteLedgers(); } SimpleLedgerView* q_ptr; Ui_SimpleLedgerView* ui; AccountNamesFilterProxyModel* accountsModel; QWidget* newTabWidget; int lastIdx; bool inModelUpdate; bool m_needLoad; }; SimpleLedgerView::SimpleLedgerView(QWidget *parent) : KMyMoneyViewBase(*new SimpleLedgerViewPrivate(this), parent) { } SimpleLedgerView::~SimpleLedgerView() { } void SimpleLedgerView::openNewLedger(QString accountId) { Q_D(SimpleLedgerView); if(d->inModelUpdate || accountId.isEmpty()) return; LedgerViewPage* view = 0; // check if ledger is already opened for(int idx=0; idx < d->ui->ledgerTab->count()-1; ++idx) { view = qobject_cast(d->ui->ledgerTab->widget(idx)); if(view) { if(accountId == view->accountId()) { d->ui->ledgerTab->setCurrentIndex(idx); return; } } } // need a new tab, we insert it before the rightmost one QModelIndex index = Models::instance()->accountsModel()->accountById(accountId); if(index.isValid()) { // create new ledger view page MyMoneyAccount acc = Models::instance()->accountsModel()->data(index, (int)eAccountsModel::Role::Account).value(); view = new LedgerViewPage(this); view->setShowEntryForNewTransaction(); view->setAccount(acc); /// @todo setup current global setting for form visibility // view->showTransactionForm(...); // insert new ledger view page in tab view int newIdx = d->ui->ledgerTab->insertTab(d->ui->ledgerTab->count()-1, view, acc.name()); d->ui->ledgerTab->setCurrentIndex(d->ui->ledgerTab->count()-1); d->ui->ledgerTab->setCurrentIndex(newIdx); } } void SimpleLedgerView::tabSelected(int idx) { Q_D(SimpleLedgerView); // qDebug() << "tabSelected" << idx << (d->ui->ledgerTab->count()-1); if(idx != (d->ui->ledgerTab->count()-1)) { d->lastIdx = idx; } } void SimpleLedgerView::updateModels() { Q_D(SimpleLedgerView); d->inModelUpdate = true; // d->ui->accountCombo-> d->ui->accountCombo->expandAll(); d->ui->accountCombo->setSelected(MyMoneyFile::instance()->asset().id()); d->inModelUpdate = false; } void SimpleLedgerView::closeLedger(int idx) { Q_D(SimpleLedgerView); // don't react on the close request for the new ledger function if(idx != (d->ui->ledgerTab->count()-1)) { d->ui->ledgerTab->removeTab(idx); } } void SimpleLedgerView::checkTabOrder(int from, int to) { Q_D(SimpleLedgerView); if(d->inModelUpdate) return; QTabBar* bar = d->ui->ledgerTab->findChild(); if(bar) { const int rightMostIdx = d->ui->ledgerTab->count()-1; if(from == rightMostIdx) { // someone tries to move the new account tab away from the rightmost position d->inModelUpdate = true; bar->moveTab(to, from); d->inModelUpdate = false; } } } void SimpleLedgerView::showTransactionForm(bool show) { emit showForms(show); } void SimpleLedgerView::closeLedgers() { Q_D(SimpleLedgerView); if (d->m_needLoad) return; auto tabCount = d->ui->ledgerTab->count(); // check that we have a least one tab that can be closed if(tabCount > 1) { // we keep the tab with the selector open at all times // which is located in the right most position --tabCount; do { --tabCount; closeLedger(tabCount); } while(tabCount > 0); } } void SimpleLedgerView::openFavoriteLedgers() { Q_D(SimpleLedgerView); if (d->m_needLoad) return; AccountsModel* model = Models::instance()->accountsModel(); QModelIndex start = model->index(0, 0); QModelIndexList indexes = model->match(start, (int)eAccountsModel::Role::Favorite, QVariant(true), -1, Qt::MatchRecursive); // indexes now has a list of favorite accounts but two entries for each. // that doesn't matter here, since openNewLedger() can handle duplicates Q_FOREACH(QModelIndex index, indexes) { openNewLedger(model->data(index, (int)eAccountsModel::Role::ID).toString()); } d->ui->ledgerTab->setCurrentIndex(0); } void SimpleLedgerView::showEvent(QShowEvent* event) { Q_D(SimpleLedgerView); if (d->m_needLoad) d->init(); // don't forget base class implementation QWidget::showEvent(event); } diff --git a/kmymoney/views/splitdialog.cpp b/kmymoney/views/splitdialog.cpp index 0d45c7848..c5b9e7d48 100644 --- a/kmymoney/views/splitdialog.cpp +++ b/kmymoney/views/splitdialog.cpp @@ -1,371 +1,378 @@ /*************************************************************************** splitdialog.cpp ------------------- begin : Sun Apr 3 2016 copyright : (C) 2015 by Thomas Baumgart email : Thomas Baumgart ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "splitdialog.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "ui_splitdialog.h" #include "mymoneyaccount.h" #include "splitdelegate.h" #include "newtransactioneditor.h" #include "splitadjustdialog.h" #include "modelenums.h" #include "icons/icons.h" using namespace Icons; class SplitDialog::Private { public: Private(SplitDialog* p) : parent(p) , ui(new Ui_SplitDialog) + , splitDelegate(nullptr) + , transactionEditor(nullptr) { } + ~Private() + { + delete ui; + } + void deleteSplits(QModelIndexList indexList); SplitDialog* parent; Ui_SplitDialog* ui; SplitDelegate* splitDelegate; /** * The account in which this split editor was opened */ MyMoneyAccount account; /** * The parent transaction editor which opened the split editor */ NewTransactionEditor* transactionEditor; MyMoneyMoney transactionTotal; MyMoneyMoney splitsTotal; }; static const int SumRow = 0; static const int DiffRow = 1; static const int AmountRow = 2; static const int HeaderCol = 0; static const int ValueCol = 1; void SplitDialog::Private::deleteSplits(QModelIndexList indexList) { if (indexList.isEmpty()) { return; } // remove from the end so that the row information stays // consistent and is not changed due to index changes QMap sortedList; foreach(auto index, indexList) { sortedList[index.row()] = index.row(); } QMap::const_iterator it = sortedList.constEnd(); do { --it; ui->splitView->model()->removeRow(*it); } while(it != sortedList.constBegin()); } SplitDialog::SplitDialog(const MyMoneyAccount& account, const MyMoneyMoney& amount, NewTransactionEditor* parent, Qt::WindowFlags f) : QDialog(parent, f) , d(new Private(this)) { d->transactionEditor = parent; d->account = account; d->transactionTotal = amount; d->ui->setupUi(this); d->ui->splitView->setColumnHidden((int)eLedgerModel::Column::Date, true); d->ui->splitView->setColumnHidden((int)eLedgerModel::Column::Number, true); d->ui->splitView->setColumnHidden((int)eLedgerModel::Column::Security, true); d->ui->splitView->setColumnHidden((int)eLedgerModel::Column::Reconciliation, true); d->ui->splitView->setColumnHidden((int)eLedgerModel::Column::Payment, false); d->ui->splitView->setColumnHidden((int)eLedgerModel::Column::Deposit, false); d->ui->splitView->setColumnHidden((int)eLedgerModel::Column::Quantity, true); d->ui->splitView->setColumnHidden((int)eLedgerModel::Column::Price, true); d->ui->splitView->setColumnHidden((int)eLedgerModel::Column::Amount, true); d->ui->splitView->setColumnHidden((int)eLedgerModel::Column::Value, true); d->ui->splitView->setColumnHidden((int)eLedgerModel::Column::Balance, true); d->ui->splitView->setSelectionMode(QAbstractItemView::ExtendedSelection); d->ui->splitView->setSelectionBehavior(QAbstractItemView::SelectRows); // setup the item delegate d->splitDelegate = new SplitDelegate(d->ui->splitView); d->ui->splitView->setItemDelegate(d->splitDelegate); d->ui->okButton->setIcon(Icons::get(Icon::DialogOK)); d->ui->cancelButton->setIcon(Icons::get(Icon::DialogCancel)); // setup some connections connect(d->ui->splitView, &LedgerView::aboutToStartEdit, this, &SplitDialog::disableButtons); connect(d->ui->splitView, &LedgerView::aboutToFinishEdit, this, &SplitDialog::enableButtons); connect(d->ui->deleteAllButton, &QAbstractButton::pressed, this, &SplitDialog::deleteAllSplits); connect(d->ui->deleteButton, &QAbstractButton::pressed, this, &SplitDialog::deleteSelectedSplits); connect(d->ui->deleteZeroButton, &QAbstractButton::pressed, this, &SplitDialog::deleteZeroSplits); connect(d->ui->mergeButton, &QAbstractButton::pressed, this, &SplitDialog::mergeSplits); connect(d->ui->newSplitButton, &QAbstractButton::pressed, this, &SplitDialog::newSplit); // finish polishing the widgets QMetaObject::invokeMethod(this, "adjustSummary", Qt::QueuedConnection); } SplitDialog::~SplitDialog() { } int SplitDialog::exec() { if(!d->ui->splitView->model()) { qWarning() << "SplitDialog::exec() executed without a model. Use setModel() before calling exec()."; return QDialog::Rejected; } return QDialog::exec(); } void SplitDialog::accept() { adjustSummary(); bool accept = true; if(d->transactionTotal != d->splitsTotal) { QPointer dlg = new SplitAdjustDialog(this); dlg->setValues(d->ui->summaryView->item(AmountRow, ValueCol)->data(Qt::DisplayRole).toString(), d->ui->summaryView->item(SumRow, ValueCol)->data(Qt::DisplayRole).toString(), d->ui->summaryView->item(DiffRow, ValueCol)->data(Qt::DisplayRole).toString(), d->ui->splitView->model()->rowCount()); accept = false; if(dlg->exec() == QDialog::Accepted && dlg) { switch(dlg->selectedOption()) { case SplitAdjustDialog::SplitAdjustContinue: break; case SplitAdjustDialog::SplitAdjustChange: d->transactionTotal = d->splitsTotal; accept = true; break; case SplitAdjustDialog::SplitAdjustDistribute: qWarning() << "SplitDialog::accept needs to implement the case SplitAdjustDialog::SplitAdjustDistribute"; accept = true; break; case SplitAdjustDialog::SplitAdjustLeaveAsIs: accept = true; break; } } delete dlg; updateButtonState(); } if(accept) QDialog::accept(); } void SplitDialog::enableButtons() { d->ui->buttonContainer->setEnabled(true); } void SplitDialog::disableButtons() { d->ui->buttonContainer->setEnabled(false); } void SplitDialog::setModel(QAbstractItemModel* model) { d->ui->splitView->setModel(model); if(model->rowCount() > 0) { QModelIndex index = model->index(0, 0); d->ui->splitView->setCurrentIndex(index); } adjustSummary(); // force an update of the summary if data changes in the model connect(model, &QAbstractItemModel::dataChanged, this, &SplitDialog::adjustSummary); connect(d->ui->splitView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &SplitDialog::selectionChanged); } void SplitDialog::adjustSummary() { d->splitsTotal = 0; for(int row = 0; row < d->ui->splitView->model()->rowCount(); ++row) { QModelIndex index = d->ui->splitView->model()->index(row, 0); if(index.isValid()) { d->splitsTotal += d->ui->splitView->model()->data(index, (int)eLedgerModel::Role::SplitValue).value(); } } QString formattedValue = d->splitsTotal.formatMoney(d->account.fraction()); d->ui->summaryView->item(SumRow, ValueCol)->setData(Qt::DisplayRole, formattedValue); if(d->transactionEditor) { d->transactionTotal = d->transactionEditor->transactionAmount(); formattedValue = d->transactionTotal.formatMoney(d->account.fraction()); d->ui->summaryView->item(AmountRow, ValueCol)->setData(Qt::DisplayRole, formattedValue); if((d->transactionTotal - d->splitsTotal).isNegative()) { d->ui->summaryView->item(DiffRow, HeaderCol)->setData(Qt::DisplayRole, i18nc("Split editor summary", "Assigned too much")); } else { d->ui->summaryView->item(DiffRow, HeaderCol)->setData(Qt::DisplayRole, i18nc("Split editor summary", "Unassigned")); } formattedValue = (d->transactionTotal - d->splitsTotal).abs().formatMoney(d->account.fraction()); d->ui->summaryView->item(DiffRow, ValueCol)->setData(Qt::DisplayRole, formattedValue); } else { d->ui->summaryView->item(SumRow, ValueCol)->setData(Qt::DisplayRole, QString()); d->ui->summaryView->item(AmountRow, ValueCol)->setData(Qt::DisplayRole, QString()); } adjustSummaryWidth(); updateButtonState(); } void SplitDialog::resizeEvent(QResizeEvent* ev) { QDialog::resizeEvent(ev); adjustSummaryWidth(); } void SplitDialog::adjustSummaryWidth() { d->ui->summaryView->resizeColumnToContents(1); d->ui->summaryView->horizontalHeader()->resizeSection(0, d->ui->summaryView->width() - d->ui->summaryView->horizontalHeader()->sectionSize(1) - 10); } void SplitDialog::newSplit() { // creating a new split is easy, because we simply // need to select the last entry in the view. If we // are on this row already with the editor closed things // are a bit more complicated. QModelIndex index = d->ui->splitView->currentIndex(); if(index.isValid()) { int row = index.row(); if(row != d->ui->splitView->model()->rowCount()-1) { d->ui->splitView->selectRow(d->ui->splitView->model()->rowCount()-1); } else { d->ui->splitView->edit(index); } } else { d->ui->splitView->selectRow(d->ui->splitView->model()->rowCount()-1); } } MyMoneyMoney SplitDialog::transactionAmount() const { return d->transactionTotal; } void SplitDialog::selectionChanged() { updateButtonState(); } void SplitDialog::updateButtonState() { d->ui->deleteButton->setEnabled(false); d->ui->deleteAllButton->setEnabled(false); d->ui->mergeButton->setEnabled(false); d->ui->deleteZeroButton->setEnabled(false); if(d->ui->splitView->selectionModel()->selectedRows().count() >= 1) { d->ui->deleteButton->setEnabled(true); } if(d->ui->splitView->model()->rowCount() > 2) { d->ui->deleteAllButton->setEnabled(true); } QAbstractItemModel* model = d->ui->splitView->model(); QSet accountIDs; for(int row = 0; row < model->rowCount(); ++row) { const QModelIndex index = model->index(row,0); // don't check the empty line at the end if(model->data(index, (int)eLedgerModel::Role::SplitId).toString().isEmpty()) continue; const QString accountID = model->data(index, (int)eLedgerModel::Role::AccountId).toString(); const MyMoneyMoney value = model->data(index, (int)eLedgerModel::Role::SplitShares).value(); if(accountIDs.contains(accountID)) { d->ui->mergeButton->setEnabled(true); } if(value.isZero()) { d->ui->deleteZeroButton->setEnabled(true); } } } void SplitDialog::deleteSelectedSplits() { d->deleteSplits(d->ui->splitView->selectionModel()->selectedRows()); adjustSummary(); } void SplitDialog::deleteAllSplits() { QAbstractItemModel* model = d->ui->splitView->model(); QModelIndexList list = model->match(model->index(0,0), (int)eLedgerModel::Role::SplitId, QLatin1String(".+"), -1, Qt::MatchRegExp ); d->deleteSplits(list); adjustSummary(); } void SplitDialog::deleteZeroSplits() { QAbstractItemModel* model = d->ui->splitView->model(); QModelIndexList list = model->match(model->index(0,0), (int)eLedgerModel::Role::SplitId, QLatin1String(".+"), -1, Qt::MatchRegExp ); for(int idx = 0; idx < list.count();) { QModelIndex index = list.at(idx); if(!model->data(index, (int)eLedgerModel::Role::SplitShares).value().isZero()) { list.removeAt(idx); } else { ++idx; } } d->deleteSplits(list); adjustSummary(); } void SplitDialog::mergeSplits() { qDebug() << "Merge splits not yet implemented."; adjustSummary(); } diff --git a/kmymoney/views/widgethintframe.cpp b/kmymoney/views/widgethintframe.cpp index 9ddca08eb..0c207d5fd 100644 --- a/kmymoney/views/widgethintframe.cpp +++ b/kmymoney/views/widgethintframe.cpp @@ -1,235 +1,236 @@ /*************************************************************************** widgethintframe.cpp ------------------- begin : Sat Aug 8 2015 copyright : (C) 2015 by Thomas Baumgart email : Thomas Baumgart ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "widgethintframe.h" // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes class WidgetHintFrameCollection::Private { public: QList widgetList; QList frameList; }; WidgetHintFrameCollection::WidgetHintFrameCollection(QObject* parent) : QObject(parent) , d(new Private) { } void WidgetHintFrameCollection::addFrame(WidgetHintFrame* frame) { if(!d->frameList.contains(frame)) { connect(frame, SIGNAL(destroyed(QObject*)), this, SLOT(frameDestroyed(QObject*))); connect(frame, SIGNAL(changed()), this, SLOT(inputChange())); d->frameList.append(frame); } } void WidgetHintFrameCollection::addWidget(QWidget* w) { if(!d->widgetList.contains(w)) { d->widgetList.append(w); updateWidgets(); } } void WidgetHintFrameCollection::removeWidget(QWidget* w) { d->widgetList.removeAll(w); w->setEnabled(true); } void WidgetHintFrameCollection::frameDestroyed(QObject* o) { WidgetHintFrame* frame = qobject_cast< WidgetHintFrame* >(o); if(frame) { d->frameList.removeAll(frame); } } void WidgetHintFrameCollection::inputChange() { QMetaObject::invokeMethod(this, "updateWidgets", Qt::QueuedConnection); } void WidgetHintFrameCollection::updateWidgets() { bool enabled = true; Q_FOREACH(WidgetHintFrame* frame, d->frameList) { enabled &= !frame->isErroneous(); if(!enabled) { break; } } Q_FOREACH(QWidget* w, d->widgetList) { w->setEnabled(enabled); } } class WidgetHintFrame::Private { public: QWidget* editWidget; bool status; FrameStyle style; }; WidgetHintFrame::WidgetHintFrame(QWidget* editWidget, FrameStyle style, Qt::WindowFlags f) : QFrame(editWidget->parentWidget(), f) , d(new Private) { d->editWidget = 0; d->status = false; d->style = style; switch(style) { case Error: setStyleSheet("QFrame { background-color: none; padding: 1px; border: 2px solid red; border-radius: 4px; }"); break; case Warning: case Info: setStyleSheet("QFrame { background-color: none; padding: 1px; border: 2px dashed red; border-radius: 4px; }"); break; } attachToWidget(editWidget); } WidgetHintFrame::~WidgetHintFrame() { + delete d; } bool WidgetHintFrame::isErroneous() const { return (d->style == Error) && (d->status == true); } static WidgetHintFrame* frame(QWidget* editWidget) { QList allErrorFrames = editWidget->parentWidget()->findChildren(); QList::const_iterator it; foreach(WidgetHintFrame* f, allErrorFrames) { if(f->editWidget() == editWidget) { return f; } } return 0; } void WidgetHintFrame::show(QWidget* editWidget, const QString& tooltip) { WidgetHintFrame* f = frame(editWidget); if(f) { f->QWidget::show(); f->d->status = true; emit f->changed(); } if(!tooltip.isNull()) editWidget->setToolTip(tooltip); } void WidgetHintFrame::hide(QWidget* editWidget, const QString& tooltip) { WidgetHintFrame* f = frame(editWidget); if(f) { f->QWidget::hide(); f->d->status = false; emit f->changed(); } if(!tooltip.isNull()) editWidget->setToolTip(tooltip); } QWidget* WidgetHintFrame::editWidget() const { return d->editWidget; } void WidgetHintFrame::detachFromWidget() { if(d->editWidget) { d->editWidget->removeEventFilter(this); d->editWidget = 0; } } void WidgetHintFrame::attachToWidget(QWidget* w) { // detach first detachFromWidget(); if(w) { d->editWidget = w; // make sure we receive changes in position and size w->installEventFilter(this); // place frame around widget move(w->pos() - QPoint(2, 2)); resize(w->width()+4, w->height()+4); // make sure widget is on top of frame w->raise(); // and hide frame for now QWidget::hide(); } } bool WidgetHintFrame::eventFilter(QObject* o, QEvent* e) { if(o == d->editWidget) { QMoveEvent* mev = 0; QResizeEvent* sev = 0; switch(e->type()) { case QEvent::EnabledChange: case QEvent::Hide: case QEvent::Show: /** * @todo think about what to do when widget is enabled/disabled * hidden or shown */ break; case QEvent::Move: mev = static_cast(e); move(mev->pos() - QPoint(2, 2)); break; case QEvent::Resize: sev = static_cast(e); resize(sev->size().width()+4, sev->size().height()+4); break; default: break; } } return QObject::eventFilter(o, e); } diff --git a/kmymoney/views/widgethintframe.h b/kmymoney/views/widgethintframe.h index c27729381..d69d1ad00 100644 --- a/kmymoney/views/widgethintframe.h +++ b/kmymoney/views/widgethintframe.h @@ -1,108 +1,108 @@ /*************************************************************************** widgethintframe.h ------------------- begin : Sat Aug 8 2015 copyright : (C) 2015 by Thomas Baumgart email : Thomas Baumgart ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #ifndef WIDGETHINTFRAME_H #define WIDGETHINTFRAME_H // ---------------------------------------------------------------------------- // QT Includes #include class QWidget; // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes class WidgetHintFrame : public QFrame { Q_OBJECT Q_ENUMS(FrameStyle) public: enum FrameStyle { Error = 0, Warning, Info }; explicit WidgetHintFrame(QWidget* editWidget, FrameStyle style = Error, Qt::WindowFlags f = 0); - virtual ~WidgetHintFrame(); + ~WidgetHintFrame(); void attachToWidget(QWidget* w); void detachFromWidget(); bool isErroneous() const; QWidget* editWidget() const; /** * Shows the info frame around @a editWidget and in case @a tooltip * is not null (@sa QString::isNull()) the respective message will * be loaded into the @a editWidget's tooltip. In case @a tooltip is null * (the default) the @a editWidget's tooltip will not be changed. */ static void show(QWidget* editWidget, const QString& tooltip = QString()); /** * Hides the info frame around @a editWidget and in case @a tooltip * is not null (@sa QString::isNull()) the respective message will * be loaded into the @a editWidget's tooltip. In case @a tooltip is null * (the default) the @a editWidget's tooltip will not be changed. */ static void hide(QWidget* editWidget, const QString& tooltip = QString()); protected: virtual bool eventFilter(QObject* o, QEvent* e); Q_SIGNALS: void changed(); private: class Private; Private * const d; }; class WidgetHintFrameCollection : public QObject { Q_OBJECT public: explicit WidgetHintFrameCollection(QObject* parent = 0); void addFrame(WidgetHintFrame* frame); void addWidget(QWidget* w); void removeWidget(QWidget* w); public Q_SLOTS: void inputChange(); protected Q_SLOTS: virtual void frameDestroyed(QObject* o); virtual void updateWidgets(); Q_SIGNALS: void inputIsValid(bool valid); private: class Private; Private * const d; }; #endif // WIDGETHINTFRAME_H diff --git a/kmymoney/widgets/groupmarkers.cpp b/kmymoney/widgets/groupmarkers.cpp index 4a5870097..bcc410af8 100644 --- a/kmymoney/widgets/groupmarkers.cpp +++ b/kmymoney/widgets/groupmarkers.cpp @@ -1,237 +1,240 @@ /*************************************************************************** groupmarkers.cpp - description ------------------- begin : Fri Mar 10 2006 copyright : (C) 2006 by Thomas Baumgart email : Thomas Baumgart (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 "groupmarkers.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "groupmarker.h" #include "groupmarker_p.h" #include "itemptrvector.h" #include "mymoneyaccount.h" #include "mymoneyenums.h" #include "widgetenums.h" using namespace KMyMoneyRegister; using namespace eWidgets; using namespace eMyMoney; namespace KMyMoneyRegister { class TypeGroupMarkerPrivate : public GroupMarkerPrivate { public: eRegister::CashFlowDirection m_dir; }; } TypeGroupMarker::TypeGroupMarker(KMyMoneyRegister::Register* parent, eRegister::CashFlowDirection dir, Account::Type accType) : GroupMarker(*new TypeGroupMarkerPrivate, parent, QString()) { Q_D(TypeGroupMarker); d->m_dir = dir; switch (dir) { case eRegister::CashFlowDirection::Deposit: d->m_txt = i18nc("Deposits onto account", "Deposits"); if (accType == Account::Type::CreditCard) { d->m_txt = i18nc("Payments towards credit card", "Payments"); } break; case eRegister::CashFlowDirection::Payment: d->m_txt = i18nc("Payments made from account", "Payments"); if (accType == Account::Type::CreditCard) { d->m_txt = i18nc("Payments made with credit card", "Charges"); } break; default: qDebug("Unknown CashFlowDirection %d for TypeGroupMarker constructor", (int)dir); break; } } TypeGroupMarker::~TypeGroupMarker() { } eRegister::CashFlowDirection TypeGroupMarker::sortType() const { Q_D(const TypeGroupMarker); return d->m_dir; } PayeeGroupMarker::PayeeGroupMarker(KMyMoneyRegister::Register* parent, const QString& name) : GroupMarker(parent, name) { } PayeeGroupMarker::~PayeeGroupMarker() { } const QString& PayeeGroupMarker::sortPayee() const { Q_D(const GroupMarker); return d->m_txt; } CategoryGroupMarker::CategoryGroupMarker(KMyMoneyRegister::Register* parent, const QString& category) : GroupMarker(parent, category) { } CategoryGroupMarker::~CategoryGroupMarker() { } const QString& CategoryGroupMarker::sortCategory() const { Q_D(const GroupMarker); return d->m_txt; } const QString CategoryGroupMarker::sortSecurity() const { Q_D(const GroupMarker); return d->m_txt; } const char* CategoryGroupMarker::className() { return "CategoryGroupMarker"; } namespace KMyMoneyRegister { class ReconcileGroupMarkerPrivate : public GroupMarkerPrivate { public: eMyMoney::Split::State m_state; }; } ReconcileGroupMarker::ReconcileGroupMarker(KMyMoneyRegister::Register* parent, eMyMoney::Split::State state) : GroupMarker(*new ReconcileGroupMarkerPrivate, parent, QString()) { Q_D(ReconcileGroupMarker); d->m_state = state; switch (state) { case eMyMoney::Split::State::NotReconciled: d->m_txt = i18nc("Reconcile state 'Not reconciled'", "Not reconciled"); break; case eMyMoney::Split::State::Cleared: d->m_txt = i18nc("Reconcile state 'Cleared'", "Cleared"); break; case eMyMoney::Split::State::Reconciled: d->m_txt = i18nc("Reconcile state 'Reconciled'", "Reconciled"); break; case eMyMoney::Split::State::Frozen: d->m_txt = i18nc("Reconcile state 'Frozen'", "Frozen"); break; default: d->m_txt = i18nc("Unknown reconcile state", "Unknown"); break; } } ReconcileGroupMarker::~ReconcileGroupMarker() { } eMyMoney::Split::State ReconcileGroupMarker::sortReconcileState() const { Q_D(const ReconcileGroupMarker); return d->m_state; } namespace KMyMoneyRegister { class RegisterPrivate { public: RegisterPrivate() : - m_selectAnchor(0), - m_focusItem(0), - m_firstItem(0), - m_lastItem(0), - m_firstErroneous(0), - m_lastErroneous(0), + m_selectAnchor(nullptr), + m_focusItem(nullptr), + m_ensureVisibleItem(nullptr), + m_firstItem(nullptr), + m_lastItem(nullptr), + m_firstErroneous(nullptr), + m_lastErroneous(nullptr), + m_markErroneousTransactions(0), m_rowHeightHint(0), m_ledgerLensForced(false), m_selectionMode(QTableWidget::MultiSelection), m_needResize(true), m_listsDirty(false), m_ignoreNextButtonRelease(false), m_needInitialColumnResize(false), m_usedWithEditor(false), m_mouseButton(Qt::MouseButtons(Qt::NoButton)), m_modifiers(Qt::KeyboardModifiers(Qt::NoModifier)), + m_lastCol(eTransaction::Column::Account), m_detailsColumnType(eRegister::DetailColumn::PayeeFirst) { } ~RegisterPrivate() { } ItemPtrVector m_items; QVector m_itemIndex; RegisterItem* m_selectAnchor; RegisterItem* m_focusItem; RegisterItem* m_ensureVisibleItem; RegisterItem* m_firstItem; RegisterItem* m_lastItem; RegisterItem* m_firstErroneous; RegisterItem* m_lastErroneous; int m_markErroneousTransactions; int m_rowHeightHint; MyMoneyAccount m_account; bool m_ledgerLensForced; QAbstractItemView::SelectionMode m_selectionMode; bool m_needResize; bool m_listsDirty; bool m_ignoreNextButtonRelease; bool m_needInitialColumnResize; bool m_usedWithEditor; Qt::MouseButtons m_mouseButton; Qt::KeyboardModifiers m_modifiers; eTransaction::Column m_lastCol; QList m_sortOrder; QRect m_lastRepaintRect; eRegister::DetailColumn m_detailsColumnType; }; } diff --git a/kmymoney/widgets/kbudgetvalues.cpp b/kmymoney/widgets/kbudgetvalues.cpp index 6acb49cd3..44508ff83 100644 --- a/kmymoney/widgets/kbudgetvalues.cpp +++ b/kmymoney/widgets/kbudgetvalues.cpp @@ -1,385 +1,386 @@ /*************************************************************************** kbudgetvalues - description ------------------- begin : Wed Nov 28 2007 copyright : (C) 2007 by Thomas Baumgart email : Thomas Baumgart (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 "kbudgetvalues.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "ui_kbudgetvalues.h" #include "mymoneybudget.h" #include "kmymoneyedit.h" #include "kmymoneysettings.h" class KBudgetValuesPrivate { Q_DISABLE_COPY(KBudgetValuesPrivate) public: KBudgetValuesPrivate() : ui(new Ui::KBudgetValues), m_currentTab(nullptr) { for (int i = 0; i < 12; ++i) { m_label[i] = nullptr; m_field[i] = nullptr; } } ~KBudgetValuesPrivate() { delete ui; } void enableMonths(bool enabled) { for (int i = 1; i < 12; ++i) { m_label[i]->setEnabled(enabled); m_field[i]->setEnabled(enabled); } } void fillMonthLabels() { QDate date(m_budgetDate); for (auto i = 0; i < 12; ++i) { m_label[i]->setText(QLocale().standaloneMonthName(date.month(), QLocale::ShortFormat)); date = date.addMonths(1); } } Ui::KBudgetValues *ui; KMyMoneyEdit* m_field[12]; QLabel* m_label[12]; QWidget* m_currentTab; QDate m_budgetDate; }; KBudgetValues::KBudgetValues(QWidget* parent) : QWidget(parent), d_ptr(new KBudgetValuesPrivate) { Q_D(KBudgetValues); d->ui->setupUi(this); d->m_currentTab = d->ui->m_monthlyButton; d->m_budgetDate = QDate(2007, KMyMoneySettings::firstFiscalMonth(), KMyMoneySettings::firstFiscalDay()); d->m_field[0] = d->ui->m_amount1; d->m_field[1] = d->ui->m_amount2; d->m_field[2] = d->ui->m_amount3; d->m_field[3] = d->ui->m_amount4; d->m_field[4] = d->ui->m_amount5; d->m_field[5] = d->ui->m_amount6; d->m_field[6] = d->ui->m_amount7; d->m_field[7] = d->ui->m_amount8; d->m_field[8] = d->ui->m_amount9; d->m_field[9] = d->ui->m_amount10; d->m_field[10] = d->ui->m_amount11; d->m_field[11] = d->ui->m_amount12; d->m_label[0] = d->ui->m_label1; d->m_label[1] = d->ui->m_label2; d->m_label[2] = d->ui->m_label3; d->m_label[3] = d->ui->m_label4; d->m_label[4] = d->ui->m_label5; d->m_label[5] = d->ui->m_label6; d->m_label[6] = d->ui->m_label7; d->m_label[7] = d->ui->m_label8; d->m_label[8] = d->ui->m_label9; d->m_label[9] = d->ui->m_label10; d->m_label[10] = d->ui->m_label11; d->m_label[11] = d->ui->m_label12; // fill with standard labels d->ui->m_monthlyButton->setChecked(true); d->ui->m_periodGroup->setId(d->ui->m_monthlyButton, 0); d->ui->m_periodGroup->setId(d->ui->m_yearlyButton, 1); d->ui->m_periodGroup->setId(d->ui->m_individualButton, 2); slotChangePeriod(d->ui->m_periodGroup->id(d->ui->m_monthlyButton)); connect(d->ui->m_amountMonthly, &KMyMoneyEdit::valueChanged, this, &KBudgetValues::slotNeedUpdate); connect(d->ui->m_amountYearly, &KMyMoneyEdit::valueChanged, this, &KBudgetValues::slotNeedUpdate); d->ui->m_amountMonthly->installEventFilter(this); d->ui->m_amountYearly->installEventFilter(this); for (auto i = 0; i < 12; ++i) { connect(d->m_field[i], &KMyMoneyEdit::valueChanged, this, &KBudgetValues::slotNeedUpdate); d->m_field[i]->installEventFilter(this); } connect(d->ui->m_clearButton, &QAbstractButton::clicked, this, &KBudgetValues::slotClearAllValues); connect(d->ui->m_periodGroup, static_cast(&QButtonGroup::buttonClicked), this, &KBudgetValues::slotChangePeriod); connect(this, &KBudgetValues::valuesChanged, this, &KBudgetValues::slotUpdateClearButton); KGuiItem clearItem(KStandardGuiItem::clear()); KGuiItem::assign(d->ui->m_clearButton, clearItem); d->ui->m_clearButton->setText(QString()); d->ui->m_clearButton->setToolTip(clearItem.toolTip()); } KBudgetValues::~KBudgetValues() { Q_D(KBudgetValues); delete d; } bool KBudgetValues::eventFilter(QObject* o, QEvent* e) { auto rc = false; if (o->isWidgetType() && (e->type() == QEvent::KeyPress)) { - auto k = dynamic_cast(e); - if ((k && k->modifiers() & Qt::KeyboardModifierMask) == 0 - || (k && k->modifiers() & Qt::KeypadModifier) != 0) { - QKeyEvent evt(e->type(), - Qt::Key_Tab, k->modifiers(), QString(), - k->isAutoRepeat(), k->count()); - switch (k->key()) { - case Qt::Key_Return: - case Qt::Key_Enter: - // send out a TAB key event - QApplication::sendEvent(o, &evt); - // don't process this one any further - rc = true; - break; - default: - break; + if (auto k = dynamic_cast(e)) { + if ((k->modifiers() & Qt::KeyboardModifierMask) == 0 + || (k->modifiers() & Qt::KeypadModifier) != 0) { + QKeyEvent evt(e->type(), + Qt::Key_Tab, k->modifiers(), QString(), + k->isAutoRepeat(), k->count()); + switch (k->key()) { + case Qt::Key_Return: + case Qt::Key_Enter: + // send out a TAB key event + QApplication::sendEvent(o, &evt); + // don't process this one any further + rc = true; + break; + default: + break; + } } } } return rc; } void KBudgetValues::clear() { Q_D(KBudgetValues); blockSignals(true); for (auto i = 0; i < 12; ++i) d->m_field[i]->setValue(MyMoneyMoney()); d->ui->m_amountMonthly->setValue(MyMoneyMoney()); d->ui->m_amountYearly->setValue(MyMoneyMoney()); blockSignals(false); } void KBudgetValues::slotClearAllValues() { Q_D(KBudgetValues); int tab = d->ui->m_periodGroup->checkedId(); if (tab == d->ui->m_periodGroup->id(d->ui->m_monthlyButton)) { d->ui->m_amountMonthly->setValue(MyMoneyMoney()); } else if (tab == d->ui->m_periodGroup->id(d->ui->m_yearlyButton)) { d->ui->m_amountYearly->setValue(MyMoneyMoney()); } else if (tab == d->ui->m_periodGroup->id(d->ui->m_individualButton)) { for (auto i = 0; i < 12; ++i) d->m_field[i]->setValue(MyMoneyMoney()); } emit valuesChanged(); } void KBudgetValues::slotChangePeriod(int id) { Q_D(KBudgetValues); // Prevent a recursive entry of this method due to widget changes // performed during execution of this method static bool inside = false; if (inside) return; inside = true; QWidget *tab = d->ui->m_periodGroup->button(id); d->fillMonthLabels(); MyMoneyMoney newValue; if (tab == d->ui->m_monthlyButton) { d->ui->m_firstItemStack->setCurrentIndex(d->ui->m_firstItemStack->indexOf(d->ui->m_monthlyPage)); d->enableMonths(false); d->m_label[0]->setText(" "); if (d->ui->m_amountMonthly->value().isZero()) { if (d->m_currentTab == d->ui->m_yearlyButton) { newValue = (d->ui->m_amountYearly->value() / MyMoneyMoney(12, 1)).convert(); } else if (d->m_currentTab == d->ui->m_individualButton) { for (auto i = 0; i < 12; ++i) newValue += d->m_field[i]->value(); newValue = (newValue / MyMoneyMoney(12, 1)).convert(); } if (!newValue.isZero()) { if (KMessageBox::questionYesNo(this, QString("") + i18n("You have entered budget values using a different base which would result in a monthly budget of %1. Should this value be used to fill the monthly budget?", newValue.formatMoney(QString(), 2)) + QString(""), i18nc("Auto assignment (caption)", "Auto assignment"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "use_previous_budget_values") == KMessageBox::Yes) { d->ui->m_amountMonthly->setValue(newValue); } } } } else if (tab == d->ui->m_yearlyButton) { d->ui->m_firstItemStack->setCurrentIndex(d->ui->m_firstItemStack->indexOf(d->ui->m_yearlyPage)); d->enableMonths(false); d->m_label[0]->setText(" "); if (d->ui->m_amountYearly->value().isZero()) { if (d->m_currentTab == d->ui->m_monthlyButton) { newValue = (d->ui->m_amountMonthly->value() * MyMoneyMoney(12, 1)).convert(); } else if (d->m_currentTab == d->ui->m_individualButton) { for (auto i = 0; i < 12; ++i) newValue += d->m_field[i]->value(); } if (!newValue.isZero()) { if (KMessageBox::questionYesNo(this, QString("") + i18n("You have entered budget values using a different base which would result in a yearly budget of %1. Should this value be used to fill the monthly budget?", newValue.formatMoney(QString(), 2)) + QString(""), i18nc("Auto assignment (caption)", "Auto assignment"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "use_previous_budget_values") == KMessageBox::Yes) { d->ui->m_amountYearly->setValue(newValue); } } } } else if (tab == d->ui->m_individualButton) { d->ui->m_firstItemStack->setCurrentIndex(d->ui->m_firstItemStack->indexOf(d->ui->m_individualPage)); d->enableMonths(true); for (auto i = 0; i < 12; ++i) newValue += d->m_field[i]->value(); if (newValue.isZero()) { if (d->m_currentTab == d->ui->m_monthlyButton) { newValue = d->ui->m_amountMonthly->value(); } else if (d->m_currentTab == d->ui->m_yearlyButton) { newValue = (d->ui->m_amountYearly->value() / MyMoneyMoney(12, 1)).convert(); } if (!newValue.isZero()) { if (KMessageBox::questionYesNo(this, QString("") + i18n("You have entered budget values using a different base which would result in an individual monthly budget of %1. Should this value be used to fill the monthly budgets?", newValue.formatMoney(QString(), 2)) + QString(""), i18nc("Auto assignment (caption)", "Auto assignment"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "use_previous_budget_values") == KMessageBox::Yes) { for (auto i = 0; i < 12; ++i) d->m_field[i]->setValue(newValue); } } } } slotNeedUpdate(); d->m_currentTab = tab; inside = false; } void KBudgetValues::slotNeedUpdate() { if (!signalsBlocked()) QTimer::singleShot(0, this, SIGNAL(valuesChanged())); } void KBudgetValues::setBudgetValues(const MyMoneyBudget& budget, const MyMoneyBudget::AccountGroup& budgetAccount) { Q_D(KBudgetValues); MyMoneyBudget::PeriodGroup period; d->m_budgetDate = budget.budgetStart(); QDate date; // make sure all values are zero so that slotChangePeriod() // doesn't check for anything. clear(); blockSignals(true); switch (budgetAccount.budgetLevel()) { case MyMoneyBudget::AccountGroup::eMonthly: default: d->ui->m_monthlyButton->setChecked(true); slotChangePeriod(d->ui->m_periodGroup->id(d->ui->m_monthlyButton)); d->ui->m_amountMonthly->setValue(budgetAccount.period(d->m_budgetDate).amount()); break; case MyMoneyBudget::AccountGroup::eYearly: d->ui->m_yearlyButton->setChecked(true); slotChangePeriod(d->ui->m_periodGroup->id(d->ui->m_yearlyButton)); d->ui->m_amountYearly->setValue(budgetAccount.period(d->m_budgetDate).amount()); break; case MyMoneyBudget::AccountGroup::eMonthByMonth: d->ui->m_individualButton->setChecked(true); slotChangePeriod(d->ui->m_periodGroup->id(d->ui->m_individualButton)); date.setDate(d->m_budgetDate.year(), d->m_budgetDate.month(), d->m_budgetDate.day()); for (auto i = 0; i < 12; ++i) { d->m_field[i]->setValue(budgetAccount.period(date).amount()); date = date.addMonths(1); } break; } slotUpdateClearButton(); blockSignals(false); } void KBudgetValues::budgetValues(const MyMoneyBudget& budget, MyMoneyBudget::AccountGroup& budgetAccount) { Q_D(KBudgetValues); MyMoneyBudget::PeriodGroup period; d->m_budgetDate = budget.budgetStart(); period.setStartDate(d->m_budgetDate); QDate date; budgetAccount.clearPeriods(); int tab = d->ui->m_periodGroup->checkedId(); if (tab == d->ui->m_periodGroup->id(d->ui->m_monthlyButton)) { budgetAccount.setBudgetLevel(MyMoneyBudget::AccountGroup::eMonthly); period.setAmount(d->ui->m_amountMonthly->value()); budgetAccount.addPeriod(d->m_budgetDate, period); } else if (tab == d->ui->m_periodGroup->id(d->ui->m_yearlyButton)) { budgetAccount.setBudgetLevel(MyMoneyBudget::AccountGroup::eYearly); period.setAmount(d->ui->m_amountYearly->value()); budgetAccount.addPeriod(d->m_budgetDate, period); } else if (tab == d->ui->m_periodGroup->id(d->ui->m_individualButton)) { budgetAccount.setBudgetLevel(MyMoneyBudget::AccountGroup::eMonthByMonth); date.setDate(d->m_budgetDate.year(), d->m_budgetDate.month(), d->m_budgetDate.day()); for (auto i = 0; i < 12; ++i) { period.setStartDate(date); period.setAmount(d->m_field[i]->value()); budgetAccount.addPeriod(date, period); date = date.addMonths(1); } } } void KBudgetValues::slotUpdateClearButton() { Q_D(KBudgetValues); auto rc = false; int tab = d->ui->m_periodGroup->checkedId(); if (tab == d->ui->m_periodGroup->id(d->ui->m_monthlyButton)) { rc = !d->ui->m_amountMonthly->value().isZero(); } else if (tab == d->ui->m_periodGroup->id(d->ui->m_yearlyButton)) { rc = !d->ui->m_amountYearly->value().isZero(); } else if (tab == d->ui->m_periodGroup->id(d->ui->m_individualButton)) { for (auto i = 0; (i < 12) && (rc == false); ++i) { rc |= !d->m_field[i]->value().isZero(); } } d->ui->m_clearButton->setEnabled(rc); } diff --git a/kmymoney/widgets/kmymoneycalculator.cpp b/kmymoney/widgets/kmymoneycalculator.cpp index 67ddee18c..5172b0525 100644 --- a/kmymoney/widgets/kmymoneycalculator.cpp +++ b/kmymoney/widgets/kmymoneycalculator.cpp @@ -1,575 +1,577 @@ /*************************************************************************** kmymoneycalculator.cpp - description ------------------- begin : Sat Oct 19 2002 copyright : (C) 2000-2002 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 "kmymoneycalculator.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes class KMyMoneyCalculatorPrivate { Q_DISABLE_COPY(KMyMoneyCalculatorPrivate) public: KMyMoneyCalculatorPrivate() : op0(0.0), op1(0.0), op(0), stackedOp(0), display(nullptr), m_clearOperandOnDigit(false) { + for (auto& button : buttons) + button = nullptr; } /** * This member variable stores the current (second) operand */ QString operand; /** * This member variable stores the last result */ QString m_result; /** * This member variable stores the representation of the * character to be used to separate the integer and fractional * part of numbers. The internal representation is always a period. */ QChar m_comma; /** * The numeric representation of a stacked first operand */ double op0; /** * The numeric representation of the first operand */ double op1; /** * This member stores the operation to be performed between * the first and the second operand. */ int op; /** * This member stores a pending addition operation */ int stackedOp; /** * This member stores a pointer to the display area */ QLabel *display; /** * This member array stores the pointers to the various * buttons of the calculator. It is setup during the * constructor of this object */ QPushButton *buttons[20]; /** * This enumeration type stores the values used for the * various keys internally */ enum { /* 0-9 are used by digits */ COMMA = 10, /* * make sure, that PLUS through EQUAL remain in * the order they are. Otherwise, check the calculation * signal mapper */ PLUS, MINUS, SLASH, STAR, EQUAL, PLUSMINUS, PERCENT, CLEAR, CLEARALL, /* insert new buttons before this line */ MAX_BUTTONS }; /** * This flag signals, if the operand should be replaced upon * a digit key pressure. Defaults to false and will be set, if * setInitialValues() is called without an operation. */ bool m_clearOperandOnDigit; }; KMyMoneyCalculator::KMyMoneyCalculator(QWidget* parent) : QFrame(parent), d_ptr(new KMyMoneyCalculatorPrivate) { Q_D(KMyMoneyCalculator); d->m_comma = QLocale().decimalPoint(); d->m_clearOperandOnDigit = false; QGridLayout* grid = new QGridLayout(this); d->display = new QLabel(this); QPalette palette; palette.setColor(d->display->backgroundRole(), QColor("#BDFFB4")); d->display->setPalette(palette); d->display->setFrameStyle(QFrame::Panel | QFrame::Sunken); d->display->setAlignment(Qt::AlignRight | Qt::AlignVCenter); grid->addWidget(d->display, 0, 0, 1, 5); d->buttons[0] = new QPushButton("0", this); d->buttons[1] = new QPushButton("1", this); d->buttons[2] = new QPushButton("2", this); d->buttons[3] = new QPushButton("3", this); d->buttons[4] = new QPushButton("4", this); d->buttons[5] = new QPushButton("5", this); d->buttons[6] = new QPushButton("6", this); d->buttons[7] = new QPushButton("7", this); d->buttons[8] = new QPushButton("8", this); d->buttons[9] = new QPushButton("9", this); d->buttons[KMyMoneyCalculatorPrivate::PLUS] = new QPushButton("+", this); d->buttons[KMyMoneyCalculatorPrivate::MINUS] = new QPushButton("-", this); d->buttons[KMyMoneyCalculatorPrivate::STAR] = new QPushButton("X", this); d->buttons[KMyMoneyCalculatorPrivate::COMMA] = new QPushButton(d->m_comma, this); d->buttons[KMyMoneyCalculatorPrivate::EQUAL] = new QPushButton("=", this); d->buttons[KMyMoneyCalculatorPrivate::SLASH] = new QPushButton("/", this); d->buttons[KMyMoneyCalculatorPrivate::CLEAR] = new QPushButton("C", this); d->buttons[KMyMoneyCalculatorPrivate::CLEARALL] = new QPushButton("AC", this); d->buttons[KMyMoneyCalculatorPrivate::PLUSMINUS] = new QPushButton("+-", this); d->buttons[KMyMoneyCalculatorPrivate::PERCENT] = new QPushButton("%", this); grid->addWidget(d->buttons[7], 1, 0); grid->addWidget(d->buttons[8], 1, 1); grid->addWidget(d->buttons[9], 1, 2); grid->addWidget(d->buttons[4], 2, 0); grid->addWidget(d->buttons[5], 2, 1); grid->addWidget(d->buttons[6], 2, 2); grid->addWidget(d->buttons[1], 3, 0); grid->addWidget(d->buttons[2], 3, 1); grid->addWidget(d->buttons[3], 3, 2); grid->addWidget(d->buttons[0], 4, 1); grid->addWidget(d->buttons[KMyMoneyCalculatorPrivate::COMMA], 4, 0); grid->addWidget(d->buttons[KMyMoneyCalculatorPrivate::PLUS], 3, 3); grid->addWidget(d->buttons[KMyMoneyCalculatorPrivate::MINUS], 4, 3); grid->addWidget(d->buttons[KMyMoneyCalculatorPrivate::STAR], 3, 4); grid->addWidget(d->buttons[KMyMoneyCalculatorPrivate::SLASH], 4, 4); grid->addWidget(d->buttons[KMyMoneyCalculatorPrivate::EQUAL], 4, 2); grid->addWidget(d->buttons[KMyMoneyCalculatorPrivate::PLUSMINUS], 2, 3); grid->addWidget(d->buttons[KMyMoneyCalculatorPrivate::PERCENT], 2, 4); grid->addWidget(d->buttons[KMyMoneyCalculatorPrivate::CLEAR], 1, 3); grid->addWidget(d->buttons[KMyMoneyCalculatorPrivate::CLEARALL], 1, 4); d->buttons[KMyMoneyCalculatorPrivate::EQUAL]->setFocus(); d->op1 = 0.0; d->stackedOp = d->op = d->op0 = 0; d->operand.clear(); changeDisplay("0"); // connect the digit signals through a signal mapper QSignalMapper* mapper = new QSignalMapper(this); for (auto i = 0; i < 10; ++i) { mapper->setMapping(d->buttons[i], i); connect(d->buttons[i], &QAbstractButton::clicked, mapper, static_cast(&QSignalMapper::map)); } connect(mapper, static_cast(&QSignalMapper::mapped), this, &KMyMoneyCalculator::digitClicked); // connect the calculation operations through another mapper mapper = new QSignalMapper(this); for (int i = KMyMoneyCalculatorPrivate::PLUS; i <= KMyMoneyCalculatorPrivate::EQUAL; ++i) { mapper->setMapping(d->buttons[i], i); connect(d->buttons[i], &QAbstractButton::clicked, mapper, static_cast(&QSignalMapper::map)); } connect(mapper, static_cast(&QSignalMapper::mapped), this, &KMyMoneyCalculator::calculationClicked); // connect all remaining signals connect(d->buttons[KMyMoneyCalculatorPrivate::COMMA], &QAbstractButton::clicked, this, &KMyMoneyCalculator::commaClicked); connect(d->buttons[KMyMoneyCalculatorPrivate::PLUSMINUS], &QAbstractButton::clicked, this, &KMyMoneyCalculator::plusminusClicked); connect(d->buttons[KMyMoneyCalculatorPrivate::PERCENT], &QAbstractButton::clicked, this, &KMyMoneyCalculator::percentClicked); connect(d->buttons[KMyMoneyCalculatorPrivate::CLEAR], &QAbstractButton::clicked, this, &KMyMoneyCalculator::clearClicked); connect(d->buttons[KMyMoneyCalculatorPrivate::CLEARALL], &QAbstractButton::clicked, this, &KMyMoneyCalculator::clearAllClicked); for (auto i = 0; i < KMyMoneyCalculatorPrivate::MAX_BUTTONS; ++i) { d->buttons[i]->setMinimumSize(40, 30); d->buttons[i]->setMaximumSize(40, 30); } // keep the size determined by the size of the contained buttons no matter what setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); } KMyMoneyCalculator::~KMyMoneyCalculator() { Q_D(KMyMoneyCalculator); delete d; } void KMyMoneyCalculator::digitClicked(int button) { Q_D(KMyMoneyCalculator); if (d->m_clearOperandOnDigit) { d->operand.clear(); d->m_clearOperandOnDigit = false; } d->operand += QChar(button + 0x30); if (d->operand.length() > 16) d->operand = d->operand.left(16); changeDisplay(d->operand); } void KMyMoneyCalculator::commaClicked() { Q_D(KMyMoneyCalculator); if (d->operand.length() == 0) d->operand = '0'; if (d->operand.contains('.', Qt::CaseInsensitive) == 0) d->operand.append('.'); if (d->operand.length() > 16) d->operand = d->operand.left(16); changeDisplay(d->operand); } void KMyMoneyCalculator::plusminusClicked() { Q_D(KMyMoneyCalculator); if (d->operand.length() == 0 && d->m_result.length() > 0) d->operand = d->m_result; if (d->operand.length() > 0) { if (d->operand.indexOf('-') != -1) d->operand.remove('-'); else d->operand.prepend('-'); changeDisplay(d->operand); } } void KMyMoneyCalculator::calculationClicked(int button) { Q_D(KMyMoneyCalculator); if (d->operand.length() == 0 && d->op != 0 && button == KMyMoneyCalculatorPrivate::EQUAL) { d->op = 0; d->m_result = normalizeString(d->op1); changeDisplay(d->m_result); } else if (d->operand.length() > 0 && d->op != 0) { // perform operation double op2 = d->operand.toDouble(); bool error = false; // if the pending operation is addition and we now do multiplication // we just stack op1 and remember the operation in if ((d->op == KMyMoneyCalculatorPrivate::PLUS || d->op == KMyMoneyCalculatorPrivate::MINUS) && (button == KMyMoneyCalculatorPrivate::STAR || button == KMyMoneyCalculatorPrivate::SLASH)) { d->op0 = d->op1; d->stackedOp = d->op; d->op = 0; } switch (d->op) { case KMyMoneyCalculatorPrivate::PLUS: op2 = d->op1 + op2; break; case KMyMoneyCalculatorPrivate::MINUS: op2 = d->op1 - op2; break; case KMyMoneyCalculatorPrivate::STAR: op2 = d->op1 * op2; break; case KMyMoneyCalculatorPrivate::SLASH: if (op2 == 0.0) error = true; else op2 = d->op1 / op2; break; } // if we have a pending addition operation, and the next operation is // not multiplication, we calculate the stacked operation if (d->stackedOp && button != KMyMoneyCalculatorPrivate::STAR && button != KMyMoneyCalculatorPrivate::SLASH) { switch (d->stackedOp) { case KMyMoneyCalculatorPrivate::PLUS: op2 = d->op0 + op2; break; case KMyMoneyCalculatorPrivate::MINUS: op2 = d->op0 - op2; break; } d->stackedOp = 0; } if (error) { d->op = 0; changeDisplay("Error"); d->operand.clear(); } else { d->op1 = op2; d->m_result = normalizeString(d->op1); changeDisplay(d->m_result); } } else if (d->operand.length() > 0 && d->op == 0) { d->op1 = d->operand.toDouble(); d->m_result = normalizeString(d->op1); changeDisplay(d->m_result); } if (button != KMyMoneyCalculatorPrivate::EQUAL) { d->op = button; } else { d->op = 0; emit signalResultAvailable(); } d->operand.clear(); } QString KMyMoneyCalculator::normalizeString(const double& val) { QString str; str.setNum(val, 'f'); int i = str.length(); while (i > 1 && str[i-1] == '0') { --i; } // cut off trailing 0's str.remove(i, str.length()); if (str.length() > 0) { // possibly remove trailing period if (str[str.length()-1] == '.') { str.remove(str.length() - 1, 1); } } return str; } void KMyMoneyCalculator::clearClicked() { Q_D(KMyMoneyCalculator); if (d->operand.length() > 0) { d->operand = d->operand.left(d->operand.length() - 1); } if (d->operand.length() == 0) changeDisplay("0"); else changeDisplay(d->operand); } void KMyMoneyCalculator::clearAllClicked() { Q_D(KMyMoneyCalculator); d->operand.clear(); d->op = 0; changeDisplay("0"); } void KMyMoneyCalculator::percentClicked() { Q_D(KMyMoneyCalculator); if (d->op != 0) { double op2 = d->operand.toDouble(); switch (d->op) { case KMyMoneyCalculatorPrivate::PLUS: case KMyMoneyCalculatorPrivate::MINUS: op2 = (d->op1 * op2) / 100; break; case KMyMoneyCalculatorPrivate::STAR: case KMyMoneyCalculatorPrivate::SLASH: op2 /= 100; break; } d->operand = normalizeString(op2); changeDisplay(d->operand); } } QString KMyMoneyCalculator::result() const { Q_D(const KMyMoneyCalculator); auto txt = d->m_result; txt.replace(QRegExp("\\."), d->m_comma); if (txt[0] == '-') { txt = txt.mid(1); // get rid of the minus sign QString mask; // TODO: port this to kf5 (support for paren around negative numbers) #if 0 switch (KLocale::global()->negativeMonetarySignPosition()) { case KLocale::ParensAround: mask = "(%1)"; break; case KLocale::AfterQuantityMoney: mask = "%1-"; break; case KLocale::AfterMoney: case KLocale::BeforeMoney: mask = "%1 -"; break; case KLocale::BeforeQuantityMoney: mask = "-%1"; break; } #else mask = "-%1"; #endif txt = QString(mask).arg(txt); } return txt; } void KMyMoneyCalculator::setComma(const QChar ch) { Q_D(KMyMoneyCalculator); d->m_comma = ch; } void KMyMoneyCalculator::changeDisplay(const QString& str) { Q_D(KMyMoneyCalculator); auto txt = str; txt.replace(QRegExp("\\."), d->m_comma); d->display->setText("" + txt + ""); } void KMyMoneyCalculator::keyPressEvent(QKeyEvent* ev) { Q_D(KMyMoneyCalculator); int button = -1; switch (ev->key()) { case Qt::Key_0: case Qt::Key_1: case Qt::Key_2: case Qt::Key_3: case Qt::Key_4: case Qt::Key_5: case Qt::Key_6: case Qt::Key_7: case Qt::Key_8: case Qt::Key_9: if (d->m_clearOperandOnDigit) { d->operand.clear(); d->m_clearOperandOnDigit = false; } button = ev->key() - Qt::Key_0; break; case Qt::Key_Plus: button = KMyMoneyCalculatorPrivate::PLUS; break; case Qt::Key_Minus: button = KMyMoneyCalculatorPrivate::MINUS; break; case Qt::Key_Comma: case Qt::Key_Period: if (d->m_clearOperandOnDigit) { d->operand.clear(); d->m_clearOperandOnDigit = false; } button = KMyMoneyCalculatorPrivate::COMMA; break; case Qt::Key_Slash: button = KMyMoneyCalculatorPrivate::SLASH; break; case Qt::Key_Backspace: button = KMyMoneyCalculatorPrivate::CLEAR; if(ev->modifiers() & Qt::ShiftModifier) { button = KMyMoneyCalculatorPrivate::CLEARALL; } break; case Qt::Key_Asterisk: button = KMyMoneyCalculatorPrivate::STAR; break; case Qt::Key_Return: case Qt::Key_Enter: case Qt::Key_Equal: button = KMyMoneyCalculatorPrivate::EQUAL; break; case Qt::Key_Escape: emit signalQuit(); break; case Qt::Key_Percent: button = KMyMoneyCalculatorPrivate::PERCENT; break; default: ev->ignore(); break; } if (button != -1) d->buttons[button]->animateClick(); d->m_clearOperandOnDigit = false; } void KMyMoneyCalculator::setInitialValues(const QString& value, QKeyEvent* ev) { Q_D(KMyMoneyCalculator); bool negative = false; // setup operand d->operand = value; // make sure the group/thousands separator is removed ... d->operand.replace(QRegExp(QString("\\%1").arg(QLocale().groupSeparator())), QChar()); // ... and the decimal is represented by a dot d->operand.replace(QRegExp(QString("\\%1").arg(d->m_comma)), QChar('.')); if (d->operand.contains('(')) { negative = true; d->operand.remove('('); d->operand.remove(')'); } if (d->operand.contains('-')) { negative = true; d->operand.remove('-'); } if (d->operand.isEmpty()) d->operand = '0'; else if (negative) d->operand = QString("-%1").arg(d->operand); changeDisplay(d->operand); // and operation d->op = 0; if (ev) keyPressEvent(ev); else d->m_clearOperandOnDigit = true; } diff --git a/kmymoney/widgets/kmymoneyselector_p.h b/kmymoney/widgets/kmymoneyselector_p.h index dcba92c41..e5d041bac 100644 --- a/kmymoney/widgets/kmymoneyselector_p.h +++ b/kmymoney/widgets/kmymoneyselector_p.h @@ -1,92 +1,93 @@ /*************************************************************************** kmymoneyselector_p.h ------------------- begin : Thu Jun 29 2006 copyright : (C) 2006 by Thomas Baumgart email : Thomas Baumgart (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. * * * ***************************************************************************/ #ifndef KMYMONEYSELECTOR_P_H #define KMYMONEYSELECTOR_P_H #include "kmymoneyselector.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes class QHBoxLayout; class KMyMoneySelectorPrivate { Q_DISABLE_COPY(KMyMoneySelectorPrivate) Q_DECLARE_PUBLIC(KMyMoneySelector) public: explicit KMyMoneySelectorPrivate(KMyMoneySelector *qq) : q_ptr(qq), m_treeWidget(nullptr), + m_selMode(QTreeWidget::SingleSelection), m_layout(nullptr) { } void init() { Q_Q(KMyMoneySelector); q->setAutoFillBackground(true); m_selMode = QTreeWidget::SingleSelection; m_treeWidget = new QTreeWidget(q); // don't show horizontal scroll bar m_treeWidget->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); m_treeWidget->setSortingEnabled(false); m_treeWidget->setAlternatingRowColors(true); m_treeWidget->setAllColumnsShowFocus(true); m_layout = new QHBoxLayout(q); m_layout->setSpacing(0); m_layout->setMargin(0); m_treeWidget->header()->hide(); m_layout->addWidget(m_treeWidget); // force init m_selMode = QTreeWidget::MultiSelection; q->setSelectionMode(QTreeWidget::SingleSelection); q->connect(m_treeWidget, &QTreeWidget::itemPressed, q, &KMyMoneySelector::slotItemPressed); q->connect(m_treeWidget, &QTreeWidget::itemChanged, q, &KMyMoneySelector::stateChanged); } KMyMoneySelector *q_ptr; QTreeWidget* m_treeWidget; QStringList m_itemList; QString m_baseName; QTreeWidget::SelectionMode m_selMode; QHBoxLayout* m_layout; }; #endif diff --git a/kmymoney/widgets/register.cpp b/kmymoney/widgets/register.cpp index 993eb5aae..a40bf4075 100644 --- a/kmymoney/widgets/register.cpp +++ b/kmymoney/widgets/register.cpp @@ -1,1887 +1,1892 @@ /*************************************************************************** register.cpp - description ------------------- begin : Fri Mar 10 2006 copyright : (C) 2006 by Thomas Baumgart email : Thomas Baumgart (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 "register.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneyexception.h" #include "mymoneyaccount.h" #include "stdtransactiondownloaded.h" #include "stdtransactionmatched.h" #include "selectedtransactions.h" #include "scheduledtransaction.h" #include "kmymoneysettings.h" #include "mymoneymoney.h" #include "mymoneyfile.h" #include "groupmarkers.h" #include "fancydategroupmarkers.h" #include "registeritemdelegate.h" #include "itemptrvector.h" #include "mymoneyenums.h" #include "widgetenums.h" using namespace KMyMoneyRegister; using namespace eWidgets; using namespace eMyMoney; namespace KMyMoneyRegister { class RegisterPrivate { public: RegisterPrivate() : m_selectAnchor(nullptr), m_focusItem(nullptr), m_ensureVisibleItem(nullptr), m_firstItem(nullptr), m_lastItem(nullptr), m_firstErroneous(nullptr), m_lastErroneous(nullptr), m_rowHeightHint(0), m_ledgerLensForced(false), m_selectionMode(QTableWidget::MultiSelection), m_needResize(true), m_listsDirty(false), m_ignoreNextButtonRelease(false), m_needInitialColumnResize(false), m_usedWithEditor(false), m_mouseButton(Qt::MouseButtons(Qt::NoButton)), m_modifiers(Qt::KeyboardModifiers(Qt::NoModifier)), m_lastCol(eTransaction::Column::Account), m_detailsColumnType(eRegister::DetailColumn::PayeeFirst) { } ~RegisterPrivate() { } ItemPtrVector m_items; QVector m_itemIndex; RegisterItem* m_selectAnchor; RegisterItem* m_focusItem; RegisterItem* m_ensureVisibleItem; RegisterItem* m_firstItem; RegisterItem* m_lastItem; RegisterItem* m_firstErroneous; RegisterItem* m_lastErroneous; int m_markErroneousTransactions; int m_rowHeightHint; MyMoneyAccount m_account; bool m_ledgerLensForced; QAbstractItemView::SelectionMode m_selectionMode; bool m_needResize; bool m_listsDirty; bool m_ignoreNextButtonRelease; bool m_needInitialColumnResize; bool m_usedWithEditor; Qt::MouseButtons m_mouseButton; Qt::KeyboardModifiers m_modifiers; eTransaction::Column m_lastCol; QList m_sortOrder; QRect m_lastRepaintRect; eRegister::DetailColumn m_detailsColumnType; }; Register::Register(QWidget *parent) : TransactionEditorContainer(parent), d_ptr(new RegisterPrivate) { // used for custom coloring with the help of the application's stylesheet setObjectName(QLatin1String("register")); setItemDelegate(new RegisterItemDelegate(this)); setEditTriggers(QAbstractItemView::NoEditTriggers); setColumnCount((int)eTransaction::Column::LastColumn); setSelectionBehavior(QAbstractItemView::SelectRows); setAcceptDrops(true); setShowGrid(false); setContextMenuPolicy(Qt::DefaultContextMenu); setHorizontalHeaderItem((int)eTransaction::Column::Number, new QTableWidgetItem()); setHorizontalHeaderItem((int)eTransaction::Column::Date, new QTableWidgetItem()); setHorizontalHeaderItem((int)eTransaction::Column::Account, new QTableWidgetItem()); setHorizontalHeaderItem((int)eTransaction::Column::Security, new QTableWidgetItem()); setHorizontalHeaderItem((int)eTransaction::Column::Detail, new QTableWidgetItem()); setHorizontalHeaderItem((int)eTransaction::Column::ReconcileFlag, new QTableWidgetItem()); setHorizontalHeaderItem((int)eTransaction::Column::Payment, new QTableWidgetItem()); setHorizontalHeaderItem((int)eTransaction::Column::Deposit, new QTableWidgetItem()); setHorizontalHeaderItem((int)eTransaction::Column::Quantity, new QTableWidgetItem()); setHorizontalHeaderItem((int)eTransaction::Column::Price, new QTableWidgetItem()); setHorizontalHeaderItem((int)eTransaction::Column::Value, new QTableWidgetItem()); setHorizontalHeaderItem((int)eTransaction::Column::Balance, new QTableWidgetItem()); // keep the following list in sync with KMyMoneyRegister::Column in transaction.h horizontalHeaderItem((int)eTransaction::Column::Number)->setText(i18nc("Cheque Number", "No.")); horizontalHeaderItem((int)eTransaction::Column::Date)->setText(i18n("Date")); horizontalHeaderItem((int)eTransaction::Column::Account)->setText(i18n("Account")); horizontalHeaderItem((int)eTransaction::Column::Security)->setText(i18n("Security")); horizontalHeaderItem((int)eTransaction::Column::Detail)->setText(i18n("Details")); horizontalHeaderItem((int)eTransaction::Column::ReconcileFlag)->setText(i18n("C")); horizontalHeaderItem((int)eTransaction::Column::Payment)->setText(i18n("Payment")); horizontalHeaderItem((int)eTransaction::Column::Deposit)->setText(i18n("Deposit")); horizontalHeaderItem((int)eTransaction::Column::Quantity)->setText(i18n("Quantity")); horizontalHeaderItem((int)eTransaction::Column::Price)->setText(i18n("Price")); horizontalHeaderItem((int)eTransaction::Column::Value)->setText(i18n("Value")); horizontalHeaderItem((int)eTransaction::Column::Balance)->setText(i18n("Balance")); verticalHeader()->hide(); horizontalHeader()->setSectionResizeMode(QHeaderView::Fixed); horizontalHeader()->setSortIndicatorShown(false); horizontalHeader()->setSectionsMovable(false); horizontalHeader()->setSectionsClickable(false); horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu); connect(this, &QTableWidget::cellClicked, this, static_cast(&Register::selectItem)); connect(this, &QTableWidget::cellDoubleClicked, this, &Register::slotDoubleClicked); } Register::~Register() { Q_D(Register); clear(); delete d; } bool Register::eventFilter(QObject* o, QEvent* e) { if (o == this && e->type() == QEvent::KeyPress) { auto ke = dynamic_cast(e); if (ke && ke->key() == Qt::Key_Menu) { emit openContextMenu(); return true; } } return QTableWidget::eventFilter(o, e); } void Register::setupRegister(const MyMoneyAccount& account, const QList& cols) { Q_D(Register); d->m_account = account; setUpdatesEnabled(false); for (auto i = 0; i < (int)eTransaction::Column::LastColumn; ++i) hideColumn(i); d->m_needInitialColumnResize = true; d->m_lastCol = static_cast(0); QList::const_iterator it_c; for (it_c = cols.begin(); it_c != cols.end(); ++it_c) { if ((*it_c) > eTransaction::Column::LastColumn) continue; showColumn((int)*it_c); if (*it_c > d->m_lastCol) d->m_lastCol = *it_c; } setUpdatesEnabled(true); } void Register::setupRegister(const MyMoneyAccount& account, bool showAccountColumn) { Q_D(Register); d->m_account = account; setUpdatesEnabled(false); for (auto i = 0; i < (int)eTransaction::Column::LastColumn; ++i) hideColumn(i); horizontalHeaderItem((int)eTransaction::Column::Payment)->setText(i18nc("Payment made from account", "Payment")); horizontalHeaderItem((int)eTransaction::Column::Deposit)->setText(i18nc("Deposit into account", "Deposit")); if (account.id().isEmpty()) { setUpdatesEnabled(true); return; } d->m_needInitialColumnResize = true; // turn on standard columns showColumn((int)eTransaction::Column::Date); showColumn((int)eTransaction::Column::Detail); showColumn((int)eTransaction::Column::ReconcileFlag); // balance switch (account.accountType()) { case Account::Type::Stock: break; default: showColumn((int)eTransaction::Column::Balance); break; } // Number column switch (account.accountType()) { case Account::Type::Savings: case Account::Type::Cash: case Account::Type::Loan: case Account::Type::AssetLoan: case Account::Type::Asset: case Account::Type::Liability: case Account::Type::Equity: if (KMyMoneySettings::alwaysShowNrField()) showColumn((int)eTransaction::Column::Number); break; case Account::Type::Checkings: case Account::Type::CreditCard: showColumn((int)eTransaction::Column::Number); break; default: hideColumn((int)eTransaction::Column::Number); break; } switch (account.accountType()) { case Account::Type::Income: case Account::Type::Expense: showAccountColumn = true; break; default: break; } if (showAccountColumn) showColumn((int)eTransaction::Column::Account); // Security, activity, payment, deposit, amount, price and value column switch (account.accountType()) { default: showColumn((int)eTransaction::Column::Payment); showColumn((int)eTransaction::Column::Deposit); break; case Account::Type::Investment: showColumn((int)eTransaction::Column::Security); showColumn((int)eTransaction::Column::Quantity); showColumn((int)eTransaction::Column::Price); showColumn((int)eTransaction::Column::Value); break; } // headings switch (account.accountType()) { case Account::Type::CreditCard: horizontalHeaderItem((int)eTransaction::Column::Payment)->setText(i18nc("Payment made with credit card", "Charge")); horizontalHeaderItem((int)eTransaction::Column::Deposit)->setText(i18nc("Payment towards credit card", "Payment")); break; case Account::Type::Asset: case Account::Type::AssetLoan: horizontalHeaderItem((int)eTransaction::Column::Payment)->setText(i18nc("Decrease of asset/liability value", "Decrease")); horizontalHeaderItem((int)eTransaction::Column::Deposit)->setText(i18nc("Increase of asset/liability value", "Increase")); break; case Account::Type::Liability: case Account::Type::Loan: horizontalHeaderItem((int)eTransaction::Column::Payment)->setText(i18nc("Increase of asset/liability value", "Increase")); horizontalHeaderItem((int)eTransaction::Column::Deposit)->setText(i18nc("Decrease of asset/liability value", "Decrease")); break; case Account::Type::Income: case Account::Type::Expense: horizontalHeaderItem((int)eTransaction::Column::Payment)->setText(i18n("Income")); horizontalHeaderItem((int)eTransaction::Column::Deposit)->setText(i18n("Expense")); break; default: break; } d->m_lastCol = eTransaction::Column::Balance; setUpdatesEnabled(true); } bool Register::focusNextPrevChild(bool next) { return QFrame::focusNextPrevChild(next); } void Register::setSortOrder(const QString& order) { Q_D(Register); const QStringList orderList = order.split(',', QString::SkipEmptyParts); QStringList::const_iterator it; d->m_sortOrder.clear(); for (it = orderList.constBegin(); it != orderList.constEnd(); ++it) { d->m_sortOrder << static_cast((*it).toInt()); } } const QList& Register::sortOrder() const { Q_D(const Register); return d->m_sortOrder; } void Register::sortItems() { Q_D(Register); if (d->m_items.count() == 0) return; // sort the array of pointers to the transactions d->m_items.sort(); // update the next/prev item chains RegisterItem* prev = 0; RegisterItem* item; d->m_firstItem = d->m_lastItem = 0; for (QVector::size_type i = 0; i < d->m_items.size(); ++i) { item = d->m_items[i]; if (!item) continue; if (!d->m_firstItem) d->m_firstItem = item; d->m_lastItem = item; if (prev) prev->setNextItem(item); item->setPrevItem(prev); item->setNextItem(0); prev = item; } // update the balance visibility settings item = d->m_lastItem; bool showBalance = true; while (item) { auto t = dynamic_cast(item); if (t) { t->setShowBalance(showBalance); if (!t->isVisible()) { showBalance = false; } } item = item->prevItem(); } // force update of the item index (row to item array) d->m_listsDirty = true; } eTransaction::Column Register::lastCol() const { Q_D(const Register); return d->m_lastCol; } SortField Register::primarySortKey() const { Q_D(const Register); if (!d->m_sortOrder.isEmpty()) return static_cast(d->m_sortOrder.first()); return SortField::Unknown; } void Register::clear() { Q_D(Register); d->m_firstErroneous = d->m_lastErroneous = 0; d->m_ensureVisibleItem = 0; d->m_items.clear(); RegisterItem* p; while ((p = firstItem()) != 0) { delete p; } d->m_firstItem = d->m_lastItem = 0; d->m_listsDirty = true; d->m_selectAnchor = 0; d->m_focusItem = 0; #ifndef KMM_DESIGNER // recalculate row height hint QFontMetrics fm(KMyMoneySettings::listCellFontEx()); d->m_rowHeightHint = fm.lineSpacing() + 6; #endif d->m_needInitialColumnResize = true; d->m_needResize = true; updateRegister(true); } void Register::insertItemAfter(RegisterItem*p, RegisterItem* prev) { Q_D(Register); RegisterItem* next = 0; if (!prev) prev = lastItem(); if (prev) { next = prev->nextItem(); prev->setNextItem(p); } if (next) next->setPrevItem(p); p->setPrevItem(prev); p->setNextItem(next); if (!d->m_firstItem) d->m_firstItem = p; if (!d->m_lastItem) d->m_lastItem = p; if (prev == d->m_lastItem) d->m_lastItem = p; d->m_listsDirty = true; d->m_needResize = true; } void Register::addItem(RegisterItem* p) { Q_D(Register); RegisterItem* q = lastItem(); if (q) q->setNextItem(p); p->setPrevItem(q); p->setNextItem(0); d->m_items.append(p); if (!d->m_firstItem) d->m_firstItem = p; d->m_lastItem = p; d->m_listsDirty = true; d->m_needResize = true; } void Register::removeItem(RegisterItem* p) { Q_D(Register); // remove item from list if (p->prevItem()) p->prevItem()->setNextItem(p->nextItem()); if (p->nextItem()) p->nextItem()->setPrevItem(p->prevItem()); // update first and last pointer if required if (p == d->m_firstItem) d->m_firstItem = p->nextItem(); if (p == d->m_lastItem) d->m_lastItem = p->prevItem(); // make sure we don't do it twice p->setNextItem(0); p->setPrevItem(0); // remove it from the m_items array int i = d->m_items.indexOf(p); if (-1 != i) { d->m_items[i] = 0; } d->m_listsDirty = true; d->m_needResize = true; } RegisterItem* Register::firstItem() const { Q_D(const Register); return d->m_firstItem; } RegisterItem* Register::nextItem(RegisterItem* item) const { return item->nextItem(); } RegisterItem* Register::lastItem() const { Q_D(const Register); return d->m_lastItem; } void Register::setupItemIndex(int rowCount) { Q_D(Register); // setup index array d->m_itemIndex.clear(); d->m_itemIndex.reserve(rowCount); // fill index array rowCount = 0; RegisterItem* prev = 0; d->m_firstItem = d->m_lastItem = 0; for (QVector::size_type i = 0; i < d->m_items.size(); ++i) { RegisterItem* item = d->m_items[i]; if (!item) continue; if (!d->m_firstItem) d->m_firstItem = item; d->m_lastItem = item; if (prev) prev->setNextItem(item); item->setPrevItem(prev); item->setNextItem(0); prev = item; for (int j = item->numRowsRegister(); j; --j) { d->m_itemIndex.push_back(item); } } } void Register::updateAlternate() const { Q_D(const Register); bool alternate = false; for (QVector::size_type i = 0; i < d->m_items.size(); ++i) { RegisterItem* item = d->m_items[i]; if (!item) continue; if (item->isVisible()) { item->setAlternate(alternate); alternate ^= true; } } } void Register::suppressAdjacentMarkers() { bool lastWasGroupMarker = false; KMyMoneyRegister::RegisterItem* p = lastItem(); auto t = dynamic_cast(p); if (t && t->transaction().id().isEmpty()) { lastWasGroupMarker = true; p = p->prevItem(); } while (p) { auto m = dynamic_cast(p); if (m) { // make adjacent group marker invisible except those that show statement information if (lastWasGroupMarker && (dynamic_cast(m) == 0)) { m->setVisible(false); } lastWasGroupMarker = true; } else if (p->isVisible()) lastWasGroupMarker = false; p = p->prevItem(); } } void Register::updateRegister(bool forceUpdateRowHeight) { Q_D(Register); if (d->m_listsDirty || forceUpdateRowHeight) { // don't get in here recursively d->m_listsDirty = false; int rowCount = 0; // determine the number of rows we need to display all items // while going through the list, check for erroneous transactions for (QVector::size_type i = 0; i < d->m_items.size(); ++i) { RegisterItem* item = d->m_items[i]; if (!item) continue; item->setStartRow(rowCount); item->setNeedResize(); rowCount += item->numRowsRegister(); if (item->isErroneous()) { if (!d->m_firstErroneous) d->m_firstErroneous = item; d->m_lastErroneous = item; } } updateAlternate(); // create item index setupItemIndex(rowCount); bool needUpdateHeaders = (QTableWidget::rowCount() != rowCount) | forceUpdateRowHeight; // setup QTable. Make sure to suppress screen updates for now setRowCount(rowCount); // if we need to update the headers, we do it now for all rows // again we make sure to suppress screen updates if (needUpdateHeaders) { for (auto i = 0; i < rowCount; ++i) { RegisterItem* item = itemAtRow(i); if (item->isVisible()) { showRow(i); } else { hideRow(i); } verticalHeader()->resizeSection(i, item->rowHeightHint()); } verticalHeader()->setUpdatesEnabled(true); } // force resizeing of the columns if necessary if (d->m_needInitialColumnResize) { QTimer::singleShot(0, this, SLOT(resize())); d->m_needInitialColumnResize = false; } else { update(); // if the number of rows changed, we might need to resize the register // to make sure we reflect the current visibility of the scrollbars. if (needUpdateHeaders) QTimer::singleShot(0, this, SLOT(resize())); } } } int Register::rowHeightHint() const { Q_D(const Register); if (!d->m_rowHeightHint) { qDebug("Register::rowHeightHint(): m_rowHeightHint is zero!!"); } return d->m_rowHeightHint; } void Register::focusInEvent(QFocusEvent* ev) { Q_D(const Register); QTableWidget::focusInEvent(ev); if (d->m_focusItem) { d->m_focusItem->setFocus(true, false); } } bool Register::event(QEvent* event) { if (event->type() == QEvent::ToolTip) { QHelpEvent *helpEvent = static_cast(event); // get the row, if it's the header, then we're done // otherwise, adjust the row to be 0 based. int row = rowAt(helpEvent->y()); if (!row) return true; --row; int col = columnAt(helpEvent->x()); RegisterItem* item = itemAtRow(row); if (!item) return true; row = row - item->startRow(); QString msg; QRect rect; if (!item->maybeTip(helpEvent->pos(), row, col, rect, msg)) return true; if (!msg.isEmpty()) { QToolTip::showText(helpEvent->globalPos(), msg); } else { QToolTip::hideText(); event->ignore(); } return true; } return TransactionEditorContainer::event(event); } void Register::focusOutEvent(QFocusEvent* ev) { Q_D(Register); if (d->m_focusItem) { d->m_focusItem->setFocus(false, false); } QTableWidget::focusOutEvent(ev); } void Register::resizeEvent(QResizeEvent* ev) { TransactionEditorContainer::resizeEvent(ev); resize((int)eTransaction::Column::Detail, true); } void Register::resize() { resize((int)eTransaction::Column::Detail); } void Register::resize(int col, bool force) { Q_D(Register); if (!d->m_needResize && !force) return; d->m_needResize = false; // resize the register int w = viewport()->width(); // TODO I was playing a bit with manual ledger resizing but could not get // a good solution. I just leave the code around, so that maybe others // pick it up again. So far, it's not clear to me where to store the // size of the sections: // // a) with the account (as it is done now) // b) with the application for the specific account type // c) ???? // // Ideas are welcome (ipwizard: 2007-07-19) // Note: currently there's no way to switch back to automatic // column sizing once the manual sizing option has been saved #if 0 if (m_account.value("kmm-ledger-column-width").isEmpty()) { #endif // check which space we need if (columnWidth((int)eTransaction::Column::Number)) adjustColumn((int)eTransaction::Column::Number); if (columnWidth((int)eTransaction::Column::Account)) adjustColumn((int)eTransaction::Column::Account); if (columnWidth((int)eTransaction::Column::Payment)) adjustColumn((int)eTransaction::Column::Payment); if (columnWidth((int)eTransaction::Column::Deposit)) adjustColumn((int)eTransaction::Column::Deposit); if (columnWidth((int)eTransaction::Column::Quantity)) adjustColumn((int)eTransaction::Column::Quantity); if (columnWidth((int)eTransaction::Column::Balance)) adjustColumn((int)eTransaction::Column::Balance); if (columnWidth((int)eTransaction::Column::Price)) adjustColumn((int)eTransaction::Column::Price); if (columnWidth((int)eTransaction::Column::Value)) adjustColumn((int)eTransaction::Column::Value); // make amount columns all the same size // only extend the entry columns to make sure they fit // the widget int dwidth = 0; int ewidth = 0; if (ewidth < columnWidth((int)eTransaction::Column::Payment)) ewidth = columnWidth((int)eTransaction::Column::Payment); if (ewidth < columnWidth((int)eTransaction::Column::Deposit)) ewidth = columnWidth((int)eTransaction::Column::Deposit); if (ewidth < columnWidth((int)eTransaction::Column::Quantity)) ewidth = columnWidth((int)eTransaction::Column::Quantity); if (dwidth < columnWidth((int)eTransaction::Column::Balance)) dwidth = columnWidth((int)eTransaction::Column::Balance); if (ewidth < columnWidth((int)eTransaction::Column::Price)) ewidth = columnWidth((int)eTransaction::Column::Price); if (dwidth < columnWidth((int)eTransaction::Column::Value)) dwidth = columnWidth((int)eTransaction::Column::Value); int swidth = columnWidth((int)eTransaction::Column::Security); if (swidth > 0) { adjustColumn((int)eTransaction::Column::Security); swidth = columnWidth((int)eTransaction::Column::Security); } adjustColumn((int)eTransaction::Column::Date); #ifndef KMM_DESIGNER // Resize the date and money fields to either // a) the size required by the input widget if no transaction form is shown and the register is used with an editor // b) the adjusted value for the input widget if the transaction form is visible or an editor is not used if (d->m_usedWithEditor && !KMyMoneySettings::transactionForm()) { QPushButton *pushButton = new QPushButton; const int pushButtonSpacing = pushButton->sizeHint().width() + 5; setColumnWidth((int)eTransaction::Column::Date, columnWidth((int)eTransaction::Column::Date) + pushButtonSpacing + 4/* space for the spinbox arrows */); ewidth += pushButtonSpacing; if (swidth > 0) { // extend the security width to make space for the selector arrow swidth = columnWidth((int)eTransaction::Column::Security) + 40; } delete pushButton; } #endif if (columnWidth((int)eTransaction::Column::Payment)) setColumnWidth((int)eTransaction::Column::Payment, ewidth); if (columnWidth((int)eTransaction::Column::Deposit)) setColumnWidth((int)eTransaction::Column::Deposit, ewidth); if (columnWidth((int)eTransaction::Column::Quantity)) setColumnWidth((int)eTransaction::Column::Quantity, ewidth); if (columnWidth((int)eTransaction::Column::Balance)) setColumnWidth((int)eTransaction::Column::Balance, dwidth); if (columnWidth((int)eTransaction::Column::Price)) setColumnWidth((int)eTransaction::Column::Price, ewidth); if (columnWidth((int)eTransaction::Column::Value)) setColumnWidth((int)eTransaction::Column::Value, dwidth); if (columnWidth((int)eTransaction::Column::ReconcileFlag)) setColumnWidth((int)eTransaction::Column::ReconcileFlag, 20); if (swidth > 0) setColumnWidth((int)eTransaction::Column::Security, swidth); #if 0 // see comment above } else { QStringList colSizes = QStringList::split(",", m_account.value("kmm-ledger-column-width"), true); for (int i; i < colSizes.count(); ++i) { int colWidth = colSizes[i].toInt(); if (colWidth == 0) continue; setColumnWidth(i, w * colWidth / 100); } } #endif for (auto i = 0; i < columnCount(); ++i) { if (i == col) continue; w -= columnWidth(i); } setColumnWidth(col, w); } void Register::forceUpdateLists() { Q_D(Register); d->m_listsDirty = true; } int Register::minimumColumnWidth(int col) { Q_D(Register); QHeaderView *topHeader = horizontalHeader(); int w = topHeader->fontMetrics().width(horizontalHeaderItem(col) ? horizontalHeaderItem(col)->text() : QString()) + 10; w = qMax(w, 20); #ifdef KMM_DESIGNER return w; #else int maxWidth = 0; int minWidth = 0; QFontMetrics cellFontMetrics(KMyMoneySettings::listCellFontEx()); switch (col) { case (int)eTransaction::Column::Date: minWidth = cellFontMetrics.width(QLocale().toString(QDate(6999, 12, 29), QLocale::ShortFormat) + " "); break; default: break; } // scan through the transactions for (auto i = 0; i < d->m_items.size(); ++i) { RegisterItem* const item = d->m_items[i]; if (!item) continue; auto t = dynamic_cast(item); if (t) { int nw = 0; try { nw = t->registerColWidth(col, cellFontMetrics); } catch (const MyMoneyException &) { // This should only be reached if the data in the file disappeared // from under us, such as when the account was deleted from a // different view, then this view is restored. In this case, new // data is about to be loaded into the view anyway, so just remove // the item from the register and swallow the exception. //qDebug("%s", qPrintable(e.what())); removeItem(t); } w = qMax(w, nw); if (maxWidth) { if (w > maxWidth) { w = maxWidth; break; } } if (w < minWidth) { w = minWidth; break; } } } return w; #endif } void Register::adjustColumn(int col) { setColumnWidth(col, minimumColumnWidth(col)); } void Register::clearSelection() { unselectItems(); TransactionEditorContainer::clearSelection(); } void Register::doSelectItems(int from, int to, bool selected) { Q_D(Register); int start, end; // make sure start is smaller than end if (from <= to) { start = from; end = to; } else { start = to; end = from; } // make sure we stay in bounds if (start < 0) start = 0; if ((end <= -1) || (end > (d->m_items.size() - 1))) end = d->m_items.size() - 1; RegisterItem* firstItem; RegisterItem* lastItem; firstItem = lastItem = 0; for (int i = start; i <= end; ++i) { RegisterItem* const item = d->m_items[i]; if (item) { if (selected != item->isSelected()) { if (!firstItem) firstItem = item; item->setSelected(selected); lastItem = item; } } } } RegisterItem* Register::itemAtRow(int row) const { Q_D(const Register); if (row >= 0 && row < d->m_itemIndex.size()) { return d->m_itemIndex[row]; } return 0; } int Register::rowToIndex(int row) const { Q_D(const Register); for (auto i = 0; i < d->m_items.size(); ++i) { RegisterItem* const item = d->m_items[i]; if (!item) continue; if (row >= item->startRow() && row < (item->startRow() + item->numRowsRegister())) return i; } return -1; } void Register::selectedTransactions(SelectedTransactions& list) const { Q_D(const Register); if (d->m_focusItem && d->m_focusItem->isSelected() && d->m_focusItem->isVisible()) { auto t = dynamic_cast(d->m_focusItem); if (t) { QString id; if (t->isScheduled()) id = t->transaction().id(); SelectedTransaction s(t->transaction(), t->split(), id); list << s; } } for (auto i = 0; i < d->m_items.size(); ++i) { RegisterItem* const item = d->m_items[i]; // make sure, we don't include the focus item twice if (item == d->m_focusItem) continue; if (item && item->isSelected() && item->isVisible()) { auto t = dynamic_cast(item); if (t) { QString id; if (t->isScheduled()) id = t->transaction().id(); SelectedTransaction s(t->transaction(), t->split(), id); list << s; } } } } QList Register::selectedItems() const { Q_D(const Register); QList list; RegisterItem* item = d->m_firstItem; while (item) { if (item && item->isSelected() && item->isVisible()) { list << item; } item = item->nextItem(); } return list; } int Register::selectedItemsCount() const { Q_D(const Register); auto cnt = 0; RegisterItem* item = d->m_firstItem; while (item) { if (item->isSelected() && item->isVisible()) ++cnt; item = item->nextItem(); } return cnt; } void Register::mouseReleaseEvent(QMouseEvent *e) { Q_D(Register); if (e->button() == Qt::RightButton) { // see the comment in Register::contextMenuEvent // on Linux we never get here but on Windows this // event is fired before the contextMenuEvent which // causes the loss of the multiple selection; to avoid // this just ignore the event and act like on Linux return; } if (d->m_ignoreNextButtonRelease) { d->m_ignoreNextButtonRelease = false; return; } d->m_mouseButton = e->button(); d->m_modifiers = QApplication::keyboardModifiers(); QTableWidget::mouseReleaseEvent(e); } void Register::contextMenuEvent(QContextMenuEvent *e) { Q_D(Register); if (e->reason() == QContextMenuEvent::Mouse) { // since mouse release event is not called, we need // to reset the mouse button and the modifiers here d->m_mouseButton = Qt::NoButton; d->m_modifiers = Qt::NoModifier; // if a selected item is clicked don't change the selection RegisterItem* item = itemAtRow(rowAt(e->y())); if (item && !item->isSelected()) selectItem(rowAt(e->y()), columnAt(e->x())); } openContextMenu(); } void Register::unselectItems(int from, int to) { doSelectItems(from, to, false); } void Register::selectItems(int from, int to) { doSelectItems(from, to, true); } void Register::selectItem(int row, int col) { Q_D(Register); if (row >= 0 && row < d->m_itemIndex.size()) { RegisterItem* item = d->m_itemIndex[row]; // don't support selecting when the item has an editor // or the item itself is not selectable if (item->hasEditorOpen() || !item->isSelectable()) { d->m_mouseButton = Qt::NoButton; return; } QString id = item->id(); selectItem(item); // selectItem() might have changed the pointers, so we // need to reconstruct it here item = itemById(id); auto t = dynamic_cast(item); if (t) { if (!id.isEmpty()) { if (t && col == (int)eTransaction::Column::ReconcileFlag && selectedItemsCount() == 1 && !t->isScheduled()) emit reconcileStateColumnClicked(t); } else { emit emptyItemSelected(); } } } } void Register::setAnchorItem(RegisterItem* anchorItem) { Q_D(Register); d->m_selectAnchor = anchorItem; } bool Register::setFocusItem(RegisterItem* focusItem) { Q_D(Register); if (focusItem && focusItem->canHaveFocus()) { if (d->m_focusItem) { d->m_focusItem->setFocus(false); } auto item = dynamic_cast(focusItem); if (d->m_focusItem != focusItem && item) { emit focusChanged(item); } d->m_focusItem = focusItem; d->m_focusItem->setFocus(true); if (d->m_listsDirty) updateRegister(KMyMoneySettings::ledgerLens() | !KMyMoneySettings::transactionForm()); ensureItemVisible(d->m_focusItem); return true; } else return false; } bool Register::setFocusToTop() { Q_D(Register); RegisterItem* rgItem = d->m_firstItem; while (rgItem) { if (setFocusItem(rgItem)) return true; rgItem = rgItem->nextItem(); } return false; } void Register::selectItem(RegisterItem* item, bool dontChangeSelections) { Q_D(Register); if (!item) return; Qt::MouseButtons buttonState = d->m_mouseButton; Qt::KeyboardModifiers modifiers = d->m_modifiers; d->m_mouseButton = Qt::NoButton; d->m_modifiers = Qt::NoModifier; if (d->m_selectionMode == NoSelection) return; if (item->isSelectable()) { QString id = item->id(); QList itemList = selectedItems(); bool okToSelect = true; auto cnt = itemList.count(); - const bool scheduledTransactionSelected = (cnt > 0 && itemList.front() && (typeid(*(itemList.front())) == typeid(StdTransactionScheduled))); + auto scheduledTransactionSelected = false; + if (cnt > 0) { + auto& r = *(itemList.front()); + scheduledTransactionSelected = (typeid(r) == typeid(StdTransactionScheduled)); + } if (buttonState & Qt::LeftButton) { if (!(modifiers & (Qt::ShiftModifier | Qt::ControlModifier)) || (d->m_selectAnchor == 0)) { if ((cnt != 1) || ((cnt == 1) && !item->isSelected())) { emit aboutToSelectItem(item, okToSelect); if (okToSelect) { // pointer 'item' might have changed. reconstruct it. item = itemById(id); unselectItems(); item->setSelected(true); setFocusItem(item); } } if (okToSelect) d->m_selectAnchor = item; } if (d->m_selectionMode == MultiSelection) { switch (modifiers & (Qt::ShiftModifier | Qt::ControlModifier)) { case Qt::ControlModifier: if (scheduledTransactionSelected || typeid(*item) == typeid(StdTransactionScheduled)) okToSelect = false; // toggle selection state of current item emit aboutToSelectItem(item, okToSelect); if (okToSelect) { // pointer 'item' might have changed. reconstruct it. item = itemById(id); item->setSelected(!item->isSelected()); setFocusItem(item); } break; case Qt::ShiftModifier: if (scheduledTransactionSelected || typeid(*item) == typeid(StdTransactionScheduled)) okToSelect = false; emit aboutToSelectItem(item, okToSelect); if (okToSelect) { // pointer 'item' might have changed. reconstruct it. item = itemById(id); unselectItems(); - selectItems(rowToIndex(d->m_selectAnchor->startRow()), rowToIndex(item->startRow())); + if (d->m_selectAnchor) + selectItems(rowToIndex(d->m_selectAnchor->startRow()), rowToIndex(item->startRow())); setFocusItem(item); } break; } } } else { // we get here when called by application logic emit aboutToSelectItem(item, okToSelect); if (okToSelect) { // pointer 'item' might have changed. reconstruct it. item = itemById(id); if (!dontChangeSelections) unselectItems(); item->setSelected(true); setFocusItem(item); d->m_selectAnchor = item; } } if (okToSelect) { SelectedTransactions list(this); emit transactionsSelected(list); } } } void Register::ensureFocusItemVisible() { Q_D(Register); ensureItemVisible(d->m_focusItem); } void Register::ensureItemVisible(RegisterItem* item) { Q_D(Register); if (!item) return; d->m_ensureVisibleItem = item; QTimer::singleShot(0, this, SLOT(slotEnsureItemVisible())); } void Register::slotDoubleClicked(int row, int) { Q_D(Register); if (row >= 0 && row < d->m_itemIndex.size()) { RegisterItem* p = d->m_itemIndex[row]; if (p->isSelectable()) { d->m_ignoreNextButtonRelease = true; // double click to start editing only works if the focus // item is among the selected ones if (!focusItem()) { setFocusItem(p); if (d->m_selectionMode != NoSelection) p->setSelected(true); } if (d->m_focusItem->isSelected()) { // don't emit the signal right away but wait until // we come back to the Qt main loop QTimer::singleShot(0, this, SIGNAL(editTransaction())); } } } } void Register::slotEnsureItemVisible() { Q_D(Register); // if clear() has been called since the timer was // started, we just ignore the call if (!d->m_ensureVisibleItem) return; // make sure to catch latest changes setUpdatesEnabled(false); updateRegister(); setUpdatesEnabled(true); // since the item will be made visible at the top of the viewport make the bottom index visible first to make the whole item visible scrollTo(model()->index(d->m_ensureVisibleItem->startRow() + d->m_ensureVisibleItem->numRowsRegister() - 1, (int)eTransaction::Column::Detail)); scrollTo(model()->index(d->m_ensureVisibleItem->startRow(), (int)eTransaction::Column::Detail)); } QString Register::text(int /*row*/, int /*col*/) const { return QString("a"); } QWidget* Register::createEditor(int /*row*/, int /*col*/, bool /*initFromCell*/) const { return 0; } void Register::setCellContentFromEditor(int /*row*/, int /*col*/) { } void Register::endEdit(int /*row*/, int /*col*/, bool /*accept*/, bool /*replace*/) { } RegisterItem* Register::focusItem() const { Q_D(const Register); return d->m_focusItem; } RegisterItem* Register::anchorItem() const { Q_D(const Register); return d->m_selectAnchor; } void Register::arrangeEditWidgets(QMap& editWidgets, KMyMoneyRegister::Transaction* t) { t->arrangeWidgetsInRegister(editWidgets); ensureItemVisible(t); // updateContents(); } void Register::tabOrder(QWidgetList& tabOrderWidgets, KMyMoneyRegister::Transaction* t) const { t->tabOrderInRegister(tabOrderWidgets); } void Register::removeEditWidgets(QMap& editWidgets) { // remove pointers from map QMap::iterator it; for (it = editWidgets.begin(); it != editWidgets.end();) { if ((*it)->parentWidget() == this) { editWidgets.erase(it); it = editWidgets.begin(); } else ++it; } // now delete the widgets if (auto t = dynamic_cast(focusItem())) { for (int row = t->startRow(); row < t->startRow() + t->numRowsRegister(true); ++row) { for (int col = 0; col < columnCount(); ++col) { if (cellWidget(row, col)) { cellWidget(row, col)->hide(); setCellWidget(row, col, 0); } } // make sure to reduce the possibly size to what it was before editing started setRowHeight(row, t->rowHeightHint()); } } } RegisterItem* Register::itemById(const QString& id) const { Q_D(const Register); if (id.isEmpty()) return d->m_lastItem; for (QVector::size_type i = 0; i < d->m_items.size(); ++i) { RegisterItem* item = d->m_items[i]; if (!item) continue; if (item->id() == id) return item; } return 0; } void Register::handleItemChange(RegisterItem* old, bool shift, bool control) { Q_D(Register); if (d->m_selectionMode == MultiSelection) { if (shift) { selectRange(d->m_selectAnchor ? d->m_selectAnchor : old, d->m_focusItem, false, true, (d->m_selectAnchor && !control) ? true : false); } else if (!control) { selectItem(d->m_focusItem, false); } } } void Register::selectRange(RegisterItem* from, RegisterItem* to, bool invert, bool includeFirst, bool clearSel) { if (!from || !to) return; if (from == to && !includeFirst) return; bool swap = false; if (to == from->prevItem()) swap = true; RegisterItem* item; if (!swap && from != to && from != to->prevItem()) { bool found = false; for (item = from; item; item = item->nextItem()) { if (item == to) { found = true; break; } } if (!found) swap = true; } if (swap) { item = from; from = to; to = item; if (!includeFirst) to = to->prevItem(); } else if (!includeFirst) { from = from->nextItem(); } if (clearSel) { for (item = firstItem(); item; item = item->nextItem()) { if (item->isSelected() && item->isVisible()) { item->setSelected(false); } } } for (item = from; item; item = item->nextItem()) { if (item->isSelectable()) { if (!invert) { if (!item->isSelected() && item->isVisible()) { item->setSelected(true); } } else { bool sel = !item->isSelected(); if ((item->isSelected() != sel) && item->isVisible()) { item->setSelected(sel); } } } if (item == to) break; } } void Register::scrollPage(int key, Qt::KeyboardModifiers modifiers) { Q_D(Register); RegisterItem* oldFocusItem = d->m_focusItem; // make sure we have a focus item if (!d->m_focusItem) setFocusItem(d->m_firstItem); if (!d->m_focusItem && d->m_firstItem) setFocusItem(d->m_firstItem->nextItem()); if (!d->m_focusItem) return; RegisterItem* item = d->m_focusItem; int height = 0; switch (key) { case Qt::Key_PageUp: while (height < viewport()->height() && item->prevItem()) { do { item = item->prevItem(); if (item->isVisible()) height += item->rowHeightHint(); } while ((!item->isSelectable() || !item->isVisible()) && item->prevItem()); while ((!item->isSelectable() || !item->isVisible()) && item->nextItem()) item = item->nextItem(); } break; case Qt::Key_PageDown: while (height < viewport()->height() && item->nextItem()) { do { if (item->isVisible()) height += item->rowHeightHint(); item = item->nextItem(); } while ((!item->isSelectable() || !item->isVisible()) && item->nextItem()); while ((!item->isSelectable() || !item->isVisible()) && item->prevItem()) item = item->prevItem(); } break; case Qt::Key_Up: if (item->prevItem()) { do { item = item->prevItem(); } while ((!item->isSelectable() || !item->isVisible()) && item->prevItem()); } break; case Qt::Key_Down: if (item->nextItem()) { do { item = item->nextItem(); } while ((!item->isSelectable() || !item->isVisible()) && item->nextItem()); } break; case Qt::Key_Home: item = d->m_firstItem; while ((!item->isSelectable() || !item->isVisible()) && item->nextItem()) item = item->nextItem(); break; case Qt::Key_End: item = d->m_lastItem; while ((!item->isSelectable() || !item->isVisible()) && item->prevItem()) item = item->prevItem(); break; } // make sure to avoid selecting a possible empty transaction at the end auto t = dynamic_cast(item); if (t && t->transaction().id().isEmpty()) { if (t->prevItem()) { item = t->prevItem(); } } if (!(modifiers & Qt::ShiftModifier) || !d->m_selectAnchor) d->m_selectAnchor = item; setFocusItem(item); if (item->isSelectable()) { handleItemChange(oldFocusItem, modifiers & Qt::ShiftModifier, modifiers & Qt::ControlModifier); // tell the world about the changes in selection SelectedTransactions list(this); emit transactionsSelected(list); } if (d->m_focusItem && !d->m_focusItem->isSelected() && d->m_selectionMode == SingleSelection) selectItem(item); } void Register::keyPressEvent(QKeyEvent* ev) { Q_D(Register); switch (ev->key()) { case Qt::Key_Space: if (d->m_selectionMode != NoSelection) { // get the state out of the event ... d->m_modifiers = ev->modifiers(); // ... and pretend that we have pressed the left mouse button ;) d->m_mouseButton = Qt::LeftButton; selectItem(d->m_focusItem); } break; case Qt::Key_PageUp: case Qt::Key_PageDown: case Qt::Key_Home: case Qt::Key_End: case Qt::Key_Down: case Qt::Key_Up: scrollPage(ev->key(), ev->modifiers()); break; case Qt::Key_Enter: case Qt::Key_Return: // don't emit the signal right away but wait until // we come back to the Qt main loop QTimer::singleShot(0, this, SIGNAL(editTransaction())); break; default: QTableWidget::keyPressEvent(ev); break; } } Transaction* Register::transactionFactory(Register *parent, const MyMoneyTransaction& transaction, const MyMoneySplit& split, int uniqueId) { Transaction* t = 0; MyMoneySplit s = split; if (parent->account() == MyMoneyAccount()) { t = new KMyMoneyRegister::StdTransaction(parent, transaction, s, uniqueId); return t; } switch (parent->account().accountType()) { case Account::Type::Checkings: case Account::Type::Savings: case Account::Type::Cash: case Account::Type::CreditCard: case Account::Type::Loan: case Account::Type::Asset: case Account::Type::Liability: case Account::Type::Currency: case Account::Type::Income: case Account::Type::Expense: case Account::Type::AssetLoan: case Account::Type::Equity: if (s.accountId().isEmpty()) s.setAccountId(parent->account().id()); if (s.isMatched()) t = new KMyMoneyRegister::StdTransactionMatched(parent, transaction, s, uniqueId); else if (transaction.isImported()) t = new KMyMoneyRegister::StdTransactionDownloaded(parent, transaction, s, uniqueId); else t = new KMyMoneyRegister::StdTransaction(parent, transaction, s, uniqueId); break; case Account::Type::Investment: if (s.isMatched()) t = new KMyMoneyRegister::InvestTransaction/* Matched */(parent, transaction, s, uniqueId); else if (transaction.isImported()) t = new KMyMoneyRegister::InvestTransactionDownloaded(parent, transaction, s, uniqueId); else t = new KMyMoneyRegister::InvestTransaction(parent, transaction, s, uniqueId); break; case Account::Type::CertificateDep: case Account::Type::MoneyMarket: case Account::Type::Stock: default: qDebug("Register::transactionFactory: invalid accountTypeE %d", (int)parent->account().accountType()); break; } return t; } const MyMoneyAccount& Register::account() const { Q_D(const Register); return d->m_account; } void Register::addGroupMarkers() { Q_D(Register); QMap list; QMap::const_iterator it; KMyMoneyRegister::RegisterItem* p = firstItem(); KMyMoneyRegister::Transaction* t; QString name; QDate today; QDate yesterday, thisWeek, lastWeek; QDate thisMonth, lastMonth; QDate thisYear; int weekStartOfs; switch (primarySortKey()) { case SortField::PostDate: case SortField::EntryDate: today = QDate::currentDate(); thisMonth.setDate(today.year(), today.month(), 1); lastMonth = thisMonth.addMonths(-1); yesterday = today.addDays(-1); // a = QDate::dayOfWeek() todays weekday (1 = Monday, 7 = Sunday) // b = QLocale().firstDayOfWeek() first day of week (1 = Monday, 7 = Sunday) weekStartOfs = today.dayOfWeek() - QLocale().firstDayOfWeek(); if (weekStartOfs < 0) { weekStartOfs = 7 + weekStartOfs; } thisWeek = today.addDays(-weekStartOfs); lastWeek = thisWeek.addDays(-7); thisYear.setDate(today.year(), 1, 1); if (KMyMoneySettings::startDate().date() != QDate(1900, 1, 1)) new KMyMoneyRegister::FancyDateGroupMarker(this, KMyMoneySettings::startDate().date(), i18n("Prior transactions possibly filtered")); if (KMyMoneySettings::showFancyMarker()) { if (d->m_account.lastReconciliationDate().isValid()) new KMyMoneyRegister::StatementGroupMarker(this, eRegister::CashFlowDirection::Deposit, d->m_account.lastReconciliationDate(), i18n("Last reconciliation")); if (!d->m_account.value("lastImportedTransactionDate").isEmpty() && !d->m_account.value("lastStatementBalance").isEmpty()) { MyMoneyMoney balance(d->m_account.value("lastStatementBalance")); if (d->m_account.accountGroup() == Account::Type::Liability) balance = -balance; auto txt = i18n("Online Statement Balance: %1", balance.formatMoney(d->m_account.fraction())); KMyMoneyRegister::StatementGroupMarker *p = new KMyMoneyRegister::StatementGroupMarker(this, eRegister::CashFlowDirection::Deposit, QDate::fromString(d->m_account.value("lastImportedTransactionDate"), Qt::ISODate), txt); p->setErroneous(!MyMoneyFile::instance()->hasMatchingOnlineBalance(d->m_account)); } new KMyMoneyRegister::FancyDateGroupMarker(this, thisYear, i18n("This year")); new KMyMoneyRegister::FancyDateGroupMarker(this, lastMonth, i18n("Last month")); new KMyMoneyRegister::FancyDateGroupMarker(this, thisMonth, i18n("This month")); new KMyMoneyRegister::FancyDateGroupMarker(this, lastWeek, i18n("Last week")); new KMyMoneyRegister::FancyDateGroupMarker(this, thisWeek, i18n("This week")); new KMyMoneyRegister::FancyDateGroupMarker(this, yesterday, i18n("Yesterday")); new KMyMoneyRegister::FancyDateGroupMarker(this, today, i18n("Today")); new KMyMoneyRegister::FancyDateGroupMarker(this, today.addDays(1), i18n("Future transactions")); new KMyMoneyRegister::FancyDateGroupMarker(this, thisWeek.addDays(7), i18n("Next week")); new KMyMoneyRegister::FancyDateGroupMarker(this, thisMonth.addMonths(1), i18n("Next month")); } else { new KMyMoneyRegister::SimpleDateGroupMarker(this, today.addDays(1), i18n("Future transactions")); } if (KMyMoneySettings::showFiscalMarker()) { QDate currentFiscalYear = KMyMoneySettings::firstFiscalDate(); new KMyMoneyRegister::FiscalYearGroupMarker(this, currentFiscalYear, i18n("Current fiscal year")); new KMyMoneyRegister::FiscalYearGroupMarker(this, currentFiscalYear.addYears(-1), i18n("Previous fiscal year")); new KMyMoneyRegister::FiscalYearGroupMarker(this, currentFiscalYear.addYears(1), i18n("Next fiscal year")); } break; case SortField::Type: if (KMyMoneySettings::showFancyMarker()) { new KMyMoneyRegister::TypeGroupMarker(this, eRegister::CashFlowDirection::Deposit, d->m_account.accountType()); new KMyMoneyRegister::TypeGroupMarker(this, eRegister::CashFlowDirection::Payment, d->m_account.accountType()); } break; case SortField::ReconcileState: if (KMyMoneySettings::showFancyMarker()) { new KMyMoneyRegister::ReconcileGroupMarker(this, eMyMoney::Split::State::NotReconciled); new KMyMoneyRegister::ReconcileGroupMarker(this, eMyMoney::Split::State::Cleared); new KMyMoneyRegister::ReconcileGroupMarker(this, eMyMoney::Split::State::Reconciled); new KMyMoneyRegister::ReconcileGroupMarker(this, eMyMoney::Split::State::Frozen); } break; case SortField::Payee: if (KMyMoneySettings::showFancyMarker()) { while (p) { if ((t = dynamic_cast(p))) list[t->sortPayee()] = 1; p = p->nextItem(); } for (it = list.constBegin(); it != list.constEnd(); ++it) { name = it.key(); if (name.isEmpty()) { name = i18nc("Unknown payee", "Unknown"); } new KMyMoneyRegister::PayeeGroupMarker(this, name); } } break; case SortField::Category: if (KMyMoneySettings::showFancyMarker()) { while (p) { if ((t = dynamic_cast(p))) list[t->sortCategory()] = 1; p = p->nextItem(); } for (it = list.constBegin(); it != list.constEnd(); ++it) { name = it.key(); if (name.isEmpty()) { name = i18nc("Unknown category", "Unknown"); } new KMyMoneyRegister::CategoryGroupMarker(this, name); } } break; case SortField::Security: if (KMyMoneySettings::showFancyMarker()) { while (p) { if ((t = dynamic_cast(p))) list[t->sortSecurity()] = 1; p = p->nextItem(); } for (it = list.constBegin(); it != list.constEnd(); ++it) { name = it.key(); if (name.isEmpty()) { name = i18nc("Unknown security", "Unknown"); } new KMyMoneyRegister::CategoryGroupMarker(this, name); } } break; default: // no markers supported break; } } void Register::removeUnwantedGroupMarkers() { // remove all trailing group markers except statement markers KMyMoneyRegister::RegisterItem* q; KMyMoneyRegister::RegisterItem* p = lastItem(); while (p) { q = p; if (dynamic_cast(p) || dynamic_cast(p)) break; p = p->prevItem(); delete q; } // remove all adjacent group markers bool lastWasGroupMarker = false; p = lastItem(); while (p) { q = p; auto m = dynamic_cast(p); p = p->prevItem(); if (m) { m->markVisible(true); // make adjacent group marker invisible except those that show statement information if (lastWasGroupMarker && (dynamic_cast(m) == 0)) { m->markVisible(false); } lastWasGroupMarker = true; } else if (q->isVisible()) lastWasGroupMarker = false; } } void Register::setLedgerLensForced(bool forced) { Q_D(Register); d->m_ledgerLensForced = forced; } bool Register::ledgerLens() const { Q_D(const Register); return d->m_ledgerLensForced; } void Register::setSelectionMode(SelectionMode mode) { Q_D(Register); d->m_selectionMode = mode; } void Register::setUsedWithEditor(bool value) { Q_D(Register); d->m_usedWithEditor = value; } eRegister::DetailColumn Register::getDetailsColumnType() const { Q_D(const Register); return d->m_detailsColumnType; } void Register::setDetailsColumnType(eRegister::DetailColumn detailsColumnType) { Q_D(Register); d->m_detailsColumnType = detailsColumnType; } } diff --git a/kmymoney/widgets/stdtransaction.cpp b/kmymoney/widgets/stdtransaction.cpp index 91a33f3da..660036edd 100644 --- a/kmymoney/widgets/stdtransaction.cpp +++ b/kmymoney/widgets/stdtransaction.cpp @@ -1,702 +1,702 @@ /*************************************************************************** stdtransaction.cpp - description ------------------- begin : Tue Jun 13 2006 copyright : (C) 2000-2006 by Thomas Baumgart (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 "stdtransaction.h" #include "stdtransaction_p.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneypayeecombo.h" #include "kmymoneycombo.h" #include "kmymoneytagcombo.h" #include "tabbar.h" #include "ktagcontainer.h" #include "mymoneytransaction.h" #include "mymoneysplit.h" #include "mymoneyexception.h" #include "mymoneyfile.h" #include "register.h" #include "transactionform.h" #include "kmymoneylineedit.h" #include "kmymoneyutils.h" #ifndef KMM_DESIGNER #include "stdtransactioneditor.h" #endif #include "kmymoneysettings.h" #include "widgetenums.h" #include "mymoneyenums.h" using namespace eWidgets; using namespace KMyMoneyRegister; using namespace KMyMoneyTransactionForm; StdTransaction::StdTransaction(Register *parent, const MyMoneyTransaction& transaction, const MyMoneySplit& split, int uniqueId) : Transaction(*new StdTransactionPrivate, parent, transaction, split, uniqueId) { Q_D(StdTransaction); d->m_showAccountRow = false; try { d->m_categoryHeader = i18n("Category"); switch (transaction.splitCount()) { default: d->m_category = i18nc("Split transaction (category replacement)", "Split transaction"); break; case 0: // the empty transaction case 1: break; case 2: setupFormHeader(d->m_transaction.splitByAccount(d->m_split.accountId(), false).accountId()); break; } } catch (const MyMoneyException &e) { qDebug() << "Problem determining the category for transaction '" << d->m_transaction.id() << "'. Reason: " << e.what() << "\n"; } d->m_rowsForm = 6; if (KMyMoneyUtils::transactionType(d->m_transaction) == KMyMoneyUtils::InvestmentTransaction) { MyMoneySplit split = KMyMoneyUtils::stockSplit(d->m_transaction); d->m_payee = MyMoneyFile::instance()->account(split.accountId()).name(); QString addon; if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares)) { if (split.value().isNegative()) { addon = i18n("Sell"); } else { addon = i18n("Buy"); } } else if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Dividend)) { addon = i18n("Dividend"); } else if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Yield)) { addon = i18n("Yield"); } else if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::InterestIncome)) { addon = i18n("Interest Income"); } if (!addon.isEmpty()) { d->m_payee += QString(" (%1)").arg(addon); } d->m_payeeHeader = i18n("Activity"); d->m_category = i18n("Investment transaction"); } // setup initial size setNumRowsRegister(numRowsRegister(KMyMoneySettings::showRegisterDetailed())); emit parent->itemAdded(this); } StdTransaction::~StdTransaction() { } const char* StdTransaction::className() { return "StdTransaction"; } void StdTransaction::setupFormHeader(const QString& id) { Q_D(StdTransaction); d->m_category = MyMoneyFile::instance()->accountToCategory(id); switch (MyMoneyFile::instance()->account(id).accountGroup()) { case eMyMoney::Account::Type::Asset: case eMyMoney::Account::Type::Liability: d->m_categoryHeader = d->m_split.shares().isNegative() ? i18n("Transfer to") : i18n("Transfer from"); break; default: d->m_categoryHeader = i18n("Category"); break; } } eRegister::Action StdTransaction::actionType() const { Q_D(const StdTransaction); eRegister::Action action = eRegister::Action::None; // if at least one split is referencing an income or // expense account, we will not call it a transfer auto found = false; foreach (const auto split, d->m_transaction.splits()) { if (split.accountId() == d->m_split.accountId()) continue; auto acc = MyMoneyFile::instance()->account(split.accountId()); if (acc.accountGroup() == eMyMoney::Account::Type::Income || acc.accountGroup() == eMyMoney::Account::Type::Expense) { // otherwise, we have to determine between deposit and withdrawal action = d->m_split.shares().isNegative() ? eRegister::Action::Withdrawal : eRegister::Action::Deposit; found = true; break; } } // otherwise, it's a transfer if (!found) action = eRegister::Action::Transfer; return action; } void StdTransaction::loadTab(TransactionForm* form) { Q_D(StdTransaction); KMyMoneyTransactionForm::TabBar* bar = form->getTabBar(); bar->setSignalEmission(eTabBar::SignalEmission::Never); for (auto i = 0; i < bar->count(); ++i) { bar->setTabEnabled(i, true); } if (d->m_transaction.splitCount() > 0) { bar->setCurrentIndex((int)actionType()); } bar->setSignalEmission(eTabBar::SignalEmission::Always); } int StdTransaction::numColsForm() const { return 4; } void StdTransaction::setupForm(TransactionForm* form) { Transaction::setupForm(form); form->setSpan(4, (int)eTransactionForm::Column::Value1, 3, 1); } bool StdTransaction::showRowInForm(int row) const { Q_D(const StdTransaction); return row == 0 ? d->m_showAccountRow : true; } void StdTransaction::setShowRowInForm(int row, bool show) { Q_D(StdTransaction); if (row == 0) d->m_showAccountRow = show; } bool StdTransaction::formCellText(QString& txt, Qt::Alignment& align, int row, int col, QPainter* /* painter */) { Q_D(const StdTransaction); // if(m_transaction != MyMoneyTransaction()) { switch (row) { case 0: switch (col) { case (int)eTransactionForm::Column::Label1: align |= Qt::AlignLeft; txt = i18n("Account"); break; } break; case 1: switch (col) { case (int)eTransactionForm::Column::Label1: align |= Qt::AlignLeft; txt = d->m_payeeHeader; break; case (int)eTransactionForm::Column::Value1: align |= Qt::AlignLeft; txt = d->m_payee; break; case (int)eTransactionForm::Column::Label2: align |= Qt::AlignLeft; if (haveNumberField()) txt = i18n("Number"); break; case (int)eTransactionForm::Column::Value2: align |= Qt::AlignRight; if (haveNumberField()) txt = d->m_split.number(); break; } break; case 2: switch (col) { case (int)eTransactionForm::Column::Label1: align |= Qt::AlignLeft; txt = d->m_categoryHeader; break; case (int)eTransactionForm::Column::Value1: align |= Qt::AlignLeft; txt = d->m_category; if (d->m_transaction != MyMoneyTransaction()) { if (txt.isEmpty() && !d->m_split.value().isZero()) txt = i18n("*** UNASSIGNED ***"); } break; case (int)eTransactionForm::Column::Label2: align |= Qt::AlignLeft; txt = i18n("Date"); break; case (int)eTransactionForm::Column::Value2: align |= Qt::AlignRight; if (d->m_transaction != MyMoneyTransaction()) txt = QLocale().toString(d->m_transaction.postDate(), QLocale::ShortFormat); break; } break; case 3: switch (col) { case (int)eTransactionForm::Column::Label1: align |= Qt::AlignLeft; txt = i18n("Tags"); break; case (int)eTransactionForm::Column::Value1: align |= Qt::AlignLeft; if (!d->m_tagList.isEmpty()) { for (auto i = 0; i < d->m_tagList.size() - 1; ++i) txt += d->m_tagList[i] + ", "; txt += d->m_tagList.last(); } //if (m_transaction != MyMoneyTransaction()) // txt = m_split.tagId(); break; case (int)eTransactionForm::Column::Label2: align |= Qt::AlignLeft; txt = i18n("Amount"); break; case (int)eTransactionForm::Column::Value2: align |= Qt::AlignRight; if (d->m_transaction != MyMoneyTransaction()) { txt = (d->m_split.value(d->m_transaction.commodity(), d->m_splitCurrencyId).abs()).formatMoney(d->m_account.fraction()); } break; } break; case 4: switch (col) { case (int)eTransactionForm::Column::Label1: align |= Qt::AlignLeft; txt = i18n("Memo"); break; case (int)eTransactionForm::Column::Value1: align &= ~Qt::AlignVCenter; align |= Qt::AlignTop; align |= Qt::AlignLeft; if (d->m_transaction != MyMoneyTransaction()) txt = d->m_split.memo().section('\n', 0, 2); break; } break; case 5: switch (col) { case (int)eTransactionForm::Column::Label2: align |= Qt::AlignLeft; txt = i18n("Status"); break; case (int)eTransactionForm::Column::Value2: align |= Qt::AlignRight; txt = reconcileState(); break; } } // } if (col == (int)eTransactionForm::Column::Value2 && row == 1) { return haveNumberField(); } return (col == (int)eTransactionForm::Column::Value1 && row < 5) || (col == (int)eTransactionForm::Column::Value2 && row > 0 && row != 4); } void StdTransaction::registerCellText(QString& txt, Qt::Alignment& align, int row, int col, QPainter* painter) { Q_D(const StdTransaction); switch (row) { case 0: switch (col) { case (int)eTransaction::Column::Number: align |= Qt::AlignLeft; if (haveNumberField()) txt = d->m_split.number(); break; case (int)eTransaction::Column::Date: align |= Qt::AlignLeft; txt = QLocale().toString(d->m_transaction.postDate(), QLocale::ShortFormat); break; case (int)eTransaction::Column::Detail: switch (d->m_parent->getDetailsColumnType()) { case eRegister::DetailColumn::PayeeFirst: txt = d->m_payee; break; case eRegister::DetailColumn::AccountFirst: txt = d->m_category; if (!d->m_tagList.isEmpty()) { txt += " ( "; for (auto i = 0; i < d->m_tagList.size() - 1; ++i) { txt += " " + d->m_tagList[i] + ", "; } txt += " " + d->m_tagList.last() + " )"; } break; } align |= Qt::AlignLeft; if (txt.isEmpty() && d->m_rowsRegister < 3) { singleLineMemo(txt, d->m_split); } if (txt.isEmpty() && d->m_rowsRegister < 2) { if (d->m_account.accountType() != eMyMoney::Account::Type::Income && d->m_account.accountType() != eMyMoney::Account::Type::Expense) { txt = d->m_category; if (txt.isEmpty() && !d->m_split.value().isZero()) { txt = i18n("*** UNASSIGNED ***"); if (painter) painter->setPen(KMyMoneySettings::schemeColor(SchemeColor::TransactionErroneous)); } } } break; case (int)eTransaction::Column::ReconcileFlag: align |= Qt::AlignHCenter; txt = reconcileState(false); break; case (int)eTransaction::Column::Payment: align |= Qt::AlignRight; if (d->m_split.value().isNegative()) { txt = (-d->m_split.value(d->m_transaction.commodity(), d->m_splitCurrencyId)).formatMoney(d->m_account.fraction()); } break; case (int)eTransaction::Column::Deposit: align |= Qt::AlignRight; if (!d->m_split.value().isNegative()) { txt = d->m_split.value(d->m_transaction.commodity(), d->m_splitCurrencyId).formatMoney(d->m_account.fraction()); } break; case (int)eTransaction::Column::Balance: align |= Qt::AlignRight; if (d->m_showBalance) txt = d->m_balance.formatMoney(d->m_account.fraction()); else txt = "----"; break; case (int)eTransaction::Column::Account: // txt = m_objects->account(m_transaction.splits()[0].accountId()).name(); txt = MyMoneyFile::instance()->account(d->m_split.accountId()).name(); break; default: break; } break; case 1: switch (col) { case (int)eTransaction::Column::Detail: switch (d->m_parent->getDetailsColumnType()) { case eRegister::DetailColumn::PayeeFirst: txt = d->m_category; if (!d->m_tagList.isEmpty()) { txt += " ( "; for (auto i = 0; i < d->m_tagList.size() - 1; ++i) { txt += " " + d->m_tagList[i] + ", "; } txt += " " + d->m_tagList.last() + " )"; } break; case eRegister::DetailColumn::AccountFirst: txt = d->m_payee; break; } align |= Qt::AlignLeft; if (txt.isEmpty() && !d->m_split.value().isZero()) { txt = i18n("*** UNASSIGNED ***"); if (painter) painter->setPen(KMyMoneySettings::schemeColor(SchemeColor::TransactionErroneous)); } break; default: break; } break; case 2: switch (col) { case (int)eTransaction::Column::Detail: align |= Qt::AlignLeft; singleLineMemo(txt, d->m_split); break; default: break; } break; } } int StdTransaction::registerColWidth(int col, const QFontMetrics& cellFontMetrics) { QString txt; int firstRow = 0, lastRow = numRowsRegister(); int nw = 0; for (int i = firstRow; i <= lastRow; ++i) { Qt::Alignment align; registerCellText(txt, align, i, col, 0); int w = cellFontMetrics.width(txt + " "); if (w > nw) nw = w; } return nw; } void StdTransaction::arrangeWidgetsInForm(QMap& editWidgets) { Q_D(StdTransaction); if (!d->m_form || !d->m_parent) return; setupFormPalette(editWidgets); arrangeWidget(d->m_form, 0, (int)eTransactionForm::Column::Label1, editWidgets["account-label"]); arrangeWidget(d->m_form, 0, (int)eTransactionForm::Column::Value1, editWidgets["account"]); arrangeWidget(d->m_form, 1, (int)eTransactionForm::Column::Label1, editWidgets["cashflow"]); arrangeWidget(d->m_form, 1, (int)eTransactionForm::Column::Value1, editWidgets["payee"]); arrangeWidget(d->m_form, 2, (int)eTransactionForm::Column::Label1, editWidgets["category-label"]); arrangeWidget(d->m_form, 2, (int)eTransactionForm::Column::Value1, editWidgets["category"]->parentWidget()); arrangeWidget(d->m_form, 3, (int)eTransactionForm::Column::Label1, editWidgets["tag-label"]); arrangeWidget(d->m_form, 3, (int)eTransactionForm::Column::Value1, editWidgets["tag"]); arrangeWidget(d->m_form, 4, (int)eTransactionForm::Column::Label1, editWidgets["memo-label"]); arrangeWidget(d->m_form, 4, (int)eTransactionForm::Column::Value1, editWidgets["memo"]); if (haveNumberField()) { arrangeWidget(d->m_form, 1, (int)eTransactionForm::Column::Label2, editWidgets["number-label"]); arrangeWidget(d->m_form, 1, (int)eTransactionForm::Column::Value2, editWidgets["number"]); } arrangeWidget(d->m_form, 2, (int)eTransactionForm::Column::Label2, editWidgets["date-label"]); arrangeWidget(d->m_form, 2, (int)eTransactionForm::Column::Value2, editWidgets["postdate"]); arrangeWidget(d->m_form, 3, (int)eTransactionForm::Column::Label2, editWidgets["amount-label"]); arrangeWidget(d->m_form, 3, (int)eTransactionForm::Column::Value2, editWidgets["amount"]); arrangeWidget(d->m_form, 5, (int)eTransactionForm::Column::Label2, editWidgets["status-label"]); arrangeWidget(d->m_form, 5, (int)eTransactionForm::Column::Value2, editWidgets["status"]); // get rid of the hints. we don't need them for the form QMap::iterator it; for (it = editWidgets.begin(); it != editWidgets.end(); ++it) { KMyMoneyCombo* combo = dynamic_cast(*it); KMyMoneyLineEdit* edit = dynamic_cast(*it); KMyMoneyPayeeCombo* payee = dynamic_cast(*it); KTagContainer* tag = dynamic_cast(*it); if (combo) combo->setPlaceholderText(QString()); if (edit) edit->setPlaceholderText(QString()); if (payee) payee->setPlaceholderText(QString()); if (tag) tag->tagCombo()->setPlaceholderText(QString()); } auto form = dynamic_cast(d->m_form); auto w = dynamic_cast(editWidgets["tabbar"]); - if (w) { + if (w && form) { // insert the tabbar in the boxlayout so it will take the place of the original tabbar which was hidden - QBoxLayout* boxLayout = dynamic_cast(form->getTabBar()->parentWidget()->layout()); - boxLayout->insertWidget(0, w); + if (auto boxLayout = dynamic_cast(form->getTabBar()->parentWidget()->layout())) + boxLayout->insertWidget(0, w); } } void StdTransaction::tabOrderInForm(QWidgetList& tabOrderWidgets) const { Q_D(const StdTransaction); QStringList taborder = KMyMoneySettings::stdTransactionFormTabOrder().split(',', QString::SkipEmptyParts); QStringList::const_iterator it_s = taborder.constBegin(); QWidget* w; while (it_s != taborder.constEnd()) { if (*it_s == "account") { tabOrderWidgets.append(focusWidget(d->m_form->cellWidget(0, (int)eTransactionForm::Column::Value1))); } else if (*it_s == "cashflow") { tabOrderWidgets.append(focusWidget(d->m_form->cellWidget(1, (int)eTransactionForm::Column::Label1))); } else if (*it_s == "payee") { tabOrderWidgets.append(focusWidget(d->m_form->cellWidget(1, (int)eTransactionForm::Column::Value1))); } else if (*it_s == "category") { // make sure to have the category field and the split button as separate tab order widgets // ok, we have to have some internal knowledge about the KMyMoneyCategory object, but // it's one of our own widgets, so we actually don't care. Just make sure, that we don't // go haywire when someone changes the KMyMoneyCategory object ... QWidget* w = d->m_form->cellWidget(2, (int)eTransactionForm::Column::Value1); tabOrderWidgets.append(focusWidget(w)); w = w->findChild("splitButton"); if (w) tabOrderWidgets.append(w); } else if (*it_s == "tag") { tabOrderWidgets.append(focusWidget(d->m_form->cellWidget(3, (int)eTransactionForm::Column::Value1))); } else if (*it_s == "memo") { tabOrderWidgets.append(focusWidget(d->m_form->cellWidget(4, (int)eTransactionForm::Column::Value1))); } else if (*it_s == "number") { if (haveNumberField()) { if ((w = focusWidget(d->m_form->cellWidget(1, (int)eTransactionForm::Column::Value2)))) tabOrderWidgets.append(w); } } else if (*it_s == "date") { tabOrderWidgets.append(focusWidget(d->m_form->cellWidget(2, (int)eTransactionForm::Column::Value2))); } else if (*it_s == "amount") { tabOrderWidgets.append(focusWidget(d->m_form->cellWidget(3, (int)eTransactionForm::Column::Value2))); } else if (*it_s == "state") { tabOrderWidgets.append(focusWidget(d->m_form->cellWidget(5, (int)eTransactionForm::Column::Value2))); } ++it_s; } } void StdTransaction::arrangeWidgetsInRegister(QMap& editWidgets) { Q_D(StdTransaction); if (!d->m_parent) return; setupRegisterPalette(editWidgets); if (haveNumberField()) arrangeWidget(d->m_parent, d->m_startRow + 0, (int)eTransaction::Column::Number, editWidgets["number"]); arrangeWidget(d->m_parent, d->m_startRow + 0, (int)eTransaction::Column::Date, editWidgets["postdate"]); arrangeWidget(d->m_parent, d->m_startRow + 1, (int)eTransaction::Column::Date, editWidgets["status"]); arrangeWidget(d->m_parent, d->m_startRow + 0, (int)eTransaction::Column::Detail, editWidgets["payee"]); arrangeWidget(d->m_parent, d->m_startRow + 1, (int)eTransaction::Column::Detail, editWidgets["category"]->parentWidget()); arrangeWidget(d->m_parent, d->m_startRow + 2, (int)eTransaction::Column::Detail, editWidgets["tag"]); arrangeWidget(d->m_parent, d->m_startRow + 3, (int)eTransaction::Column::Detail, editWidgets["memo"]); arrangeWidget(d->m_parent, d->m_startRow + 0, (int)eTransaction::Column::Payment, editWidgets["payment"]); arrangeWidget(d->m_parent, d->m_startRow + 0, (int)eTransaction::Column::Deposit, editWidgets["deposit"]); // increase the height of the row containing the memo widget d->m_parent->setRowHeight(d->m_startRow + 3, d->m_parent->rowHeightHint() * 3); } void StdTransaction::tabOrderInRegister(QWidgetList& tabOrderWidgets) const { Q_D(const StdTransaction); QStringList taborder = KMyMoneySettings::stdTransactionRegisterTabOrder().split(',', QString::SkipEmptyParts); QStringList::const_iterator it_s = taborder.constBegin(); QWidget* w; while (it_s != taborder.constEnd()) { if (*it_s == "number") { if (haveNumberField()) { if ((w = focusWidget(d->m_parent->cellWidget(d->m_startRow + 0, (int)eTransaction::Column::Number)))) tabOrderWidgets.append(w); } } else if (*it_s == "date") { tabOrderWidgets.append(focusWidget(d->m_parent->cellWidget(d->m_startRow + 0, (int)eTransaction::Column::Date))); } else if (*it_s == "payee") { tabOrderWidgets.append(focusWidget(d->m_parent->cellWidget(d->m_startRow + 0, (int)eTransaction::Column::Detail))); } else if (*it_s == "category") { // make sure to have the category field and the split button as separate tab order widgets // ok, we have to have some internal knowledge about the KMyMoneyCategory object, but // it's one of our own widgets, so we actually don't care. Just make sure, that we don't // go haywire when someone changes the KMyMoneyCategory object ... w = d->m_parent->cellWidget(d->m_startRow + 1, (int)eTransaction::Column::Detail); tabOrderWidgets.append(focusWidget(w)); w = w->findChild("splitButton"); if (w) tabOrderWidgets.append(w); } else if (*it_s == "tag") { tabOrderWidgets.append(focusWidget(d->m_parent->cellWidget(d->m_startRow + 2, (int)eTransaction::Column::Detail))); } else if (*it_s == "memo") { tabOrderWidgets.append(focusWidget(d->m_parent->cellWidget(d->m_startRow + 3, (int)eTransaction::Column::Detail))); } else if (*it_s == "payment") { tabOrderWidgets.append(focusWidget(d->m_parent->cellWidget(d->m_startRow + 0, (int)eTransaction::Column::Payment))); } else if (*it_s == "deposit") { tabOrderWidgets.append(focusWidget(d->m_parent->cellWidget(d->m_startRow + 0, (int)eTransaction::Column::Deposit))); } else if (*it_s == "state") { tabOrderWidgets.append(focusWidget(d->m_parent->cellWidget(d->m_startRow + 1, (int)eTransaction::Column::Date))); } ++it_s; } } int StdTransaction::numRowsRegister(bool expanded) const { Q_D(const StdTransaction); int numRows = 1; if (expanded) { numRows = 4; if (!d->m_inEdit) { //When not in edit Tags haven't a separate row; numRows--; if (d->m_payee.isEmpty()) { numRows--; } if (d->m_split.memo().isEmpty()) { numRows--; } // For income and expense accounts that only have // two splits we only show one line, because the // account name is already contained in the account column. if (d->m_account.accountType() == eMyMoney::Account::Type::Income || d->m_account.accountType() == eMyMoney::Account::Type::Expense) { if (numRows > 2 && d->m_transaction.splitCount() == 2) numRows = 1; } } } return numRows; } int StdTransaction::numRowsRegister() const { return RegisterItem::numRowsRegister(); } TransactionEditor* StdTransaction::createEditor(TransactionEditorContainer* regForm, const KMyMoneyRegister::SelectedTransactions& list, const QDate& lastPostDate) { #ifndef KMM_DESIGNER Q_D(StdTransaction); d->m_inRegisterEdit = regForm == d->m_parent; return new StdTransactionEditor(regForm, this, list, lastPostDate); #else return NULL; #endif } diff --git a/kmymoney/widgets/transactionform.cpp b/kmymoney/widgets/transactionform.cpp index 6a57d1690..4926f0bff 100644 --- a/kmymoney/widgets/transactionform.cpp +++ b/kmymoney/widgets/transactionform.cpp @@ -1,393 +1,394 @@ /*************************************************************************** transactionform.cpp ------------------- begin : Sun May 14 2006 copyright : (C) 2006 by Thomas Baumgart email : Thomas Baumgart (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 "transactionform.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "transactionformitemdelegate.h" #include "tabbar.h" #include "mymoneyaccount.h" #include "kmymoneydateinput.h" #include "kmymoneyedit.h" #include "kmymoneycategory.h" #include "transaction.h" #include "kmymoneysettings.h" #include "widgetenums.h" #include "mymoneyenums.h" using namespace eWidgets; using namespace KMyMoneyTransactionForm; namespace KMyMoneyTransactionForm { class TransactionFormPrivate { Q_DISABLE_COPY(TransactionFormPrivate) public: TransactionFormPrivate() : m_transaction(nullptr), - m_tabBar(nullptr) + m_tabBar(nullptr), + m_itemDelegate(nullptr) { } KMyMoneyRegister::Transaction *m_transaction; KMyMoneyTransactionForm::TabBar *m_tabBar; TransactionFormItemDelegate *m_itemDelegate; }; } TransactionForm::TransactionForm(QWidget *parent) : TransactionEditorContainer(parent), d_ptr(new TransactionFormPrivate) { Q_D(TransactionForm); d->m_itemDelegate = new TransactionFormItemDelegate(this); setFrameShape(QTableWidget::NoFrame); setShowGrid(false); setSelectionMode(QTableWidget::NoSelection); verticalHeader()->hide(); horizontalHeader()->hide(); setEditTriggers(QAbstractItemView::NoEditTriggers); // make sure, that the table is 'invisible' by setting up the right background // keep the original color group for painting the cells though QPalette p = palette(); QBrush brush = p.brush(QPalette::Background); QColor color = brush.color(); color.setAlpha(0); brush.setColor(color); p.setBrush(QPalette::Active, QPalette::Base, brush); p.setBrush(QPalette::Inactive, QPalette::Base, brush); p.setBrush(QPalette::Disabled, QPalette::Base, brush); setPalette(p); slotSetTransaction(0); } TransactionForm::~TransactionForm() { Q_D(TransactionForm); delete d; } bool TransactionForm::focusNextPrevChild(bool next) { return QFrame::focusNextPrevChild(next); } void TransactionForm::clear() { slotSetTransaction(0); } void TransactionForm::enableTabBar(bool b) { Q_D(TransactionForm); d->m_tabBar->setEnabled(b); } void TransactionForm::contentsMousePressEvent(QMouseEvent* ev) { ev->ignore(); } void TransactionForm::contentsMouseMoveEvent(QMouseEvent* ev) { ev->ignore(); } void TransactionForm::contentsMouseReleaseEvent(QMouseEvent* ev) { ev->ignore(); } void TransactionForm::contentsMouseDoubleClickEvent(QMouseEvent* ev) { ev->ignore(); } void TransactionForm::keyPressEvent(QKeyEvent* ev) { ev->ignore(); } void TransactionForm::slotSetTransaction(KMyMoneyRegister::Transaction* transaction) { Q_D(TransactionForm); d->m_transaction = transaction; setUpdatesEnabled(false); if (d->m_transaction) { // the next call sets up a back pointer to the form and also sets up the col and row span // as well as the tab of the form d->m_transaction->setupForm(this); } else { setRowCount(5); setColumnCount(1); } KMyMoneyDateInput dateInput; KMyMoneyCategory category(true, nullptr); // extract the maximal sizeHint height int height = qMax(dateInput.sizeHint().height(), category.sizeHint().height()); for (int row = 0; row < rowCount(); ++row) { if (!transaction || transaction->showRowInForm(row)) { showRow(row); QTableWidget::setRowHeight(row, height); } else hideRow(row); } // adjust vertical size of form table height *= rowCount(); setMaximumHeight(height); setMinimumHeight(height); setUpdatesEnabled(true); // see the call to setUpdatesEnabled(false) above for (auto i = 0; i < rowCount(); ++i) { setItemDelegateForRow(i, d->m_itemDelegate); } // force resizeing of the columns QMetaObject::invokeMethod(this, "resize", Qt::QueuedConnection, QGenericReturnArgument(), Q_ARG(int, (int)eTransactionForm::Column::Value1)); } void TransactionForm::paintCell(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) { Q_D(TransactionForm); if (d->m_transaction) { d->m_transaction->paintFormCell(painter, option, index); } } void TransactionForm::setCurrentCell(int, int) { } KMyMoneyTransactionForm::TabBar* TransactionForm::getTabBar(QWidget* parent) { Q_D(TransactionForm); if (!d->m_tabBar && parent) { // determine the height of the objects in the table // create the tab bar d->m_tabBar = new TabBar(parent); d->m_tabBar->setSignalEmission(eTabBar::SignalEmission::Always); QSizePolicy sizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); sizePolicy.setHeightForWidth(d->m_tabBar->sizePolicy().hasHeightForWidth()); d->m_tabBar->setSizePolicy(sizePolicy); connect(d->m_tabBar, &TabBar::tabCurrentChanged, this, &TransactionForm::slotActionSelected); } return d->m_tabBar; } void TransactionForm::slotActionSelected(int id) { emit newTransaction(static_cast(id)); } void TransactionForm::setupForm(const MyMoneyAccount& acc) { Q_D(TransactionForm); bool blocked = d->m_tabBar->blockSignals(true); // remove all tabs from the tabbar while (d->m_tabBar->count()) d->m_tabBar->removeTab(0); d->m_tabBar->show(); // important: one needs to add the new tabs first and then // change the identifier. Otherwise, addTab() will assign // a different value switch (acc.accountType()) { default: d->m_tabBar->insertTab((int)eRegister::Action::Deposit, i18n("&Deposit")); d->m_tabBar->insertTab((int)eRegister::Action::Transfer, i18n("&Transfer")); d->m_tabBar->insertTab((int)eRegister::Action::Withdrawal, i18n("&Withdrawal")); break; case eMyMoney::Account::Type::CreditCard: d->m_tabBar->insertTab((int)eRegister::Action::Deposit, i18n("&Payment")); d->m_tabBar->insertTab((int)eRegister::Action::Transfer, i18n("&Transfer")); d->m_tabBar->insertTab((int)eRegister::Action::Withdrawal, i18n("&Charge")); break; case eMyMoney::Account::Type::Liability: case eMyMoney::Account::Type::Loan: d->m_tabBar->insertTab((int)eRegister::Action::Deposit, i18n("&Decrease")); d->m_tabBar->insertTab((int)eRegister::Action::Transfer, i18n("&Transfer")); d->m_tabBar->insertTab((int)eRegister::Action::Withdrawal, i18n("&Increase")); break; case eMyMoney::Account::Type::Asset: case eMyMoney::Account::Type::AssetLoan: d->m_tabBar->insertTab((int)eRegister::Action::Deposit, i18n("&Increase")); d->m_tabBar->insertTab((int)eRegister::Action::Transfer, i18n("&Transfer")); d->m_tabBar->insertTab((int)eRegister::Action::Withdrawal, i18n("&Decrease")); break; case eMyMoney::Account::Type::Income: case eMyMoney::Account::Type::Expense: case eMyMoney::Account::Type::Investment: case eMyMoney::Account::Type::Stock: d->m_tabBar->hide(); break; } d->m_tabBar->blockSignals(blocked); } void TransactionForm::resize(int col) { setUpdatesEnabled(false); // resize the register int w = viewport()->width(); int nc = columnCount(); // check which space we need if (nc >= (int)eTransactionForm::Column::Label1 && columnWidth((int)eTransactionForm::Column::Label1)) adjustColumn(eTransactionForm::Column::Label1); if (nc >= (int)eTransactionForm::Column::Value1 && columnWidth((int)eTransactionForm::Column::Value1)) adjustColumn(eTransactionForm::Column::Value1); if (nc >= (int)eTransactionForm::Column::Label2 && columnWidth((int)eTransactionForm::Column::Label2)) adjustColumn(eTransactionForm::Column::Label2); if (nc >= (int)eTransactionForm::Column::Value2 && columnWidth((int)eTransactionForm::Column::Value2)) adjustColumn(eTransactionForm::Column::Value2); for (auto i = 0; i < nc; ++i) { if (i == col) continue; w -= columnWidth(i); } if (col < nc && w >= 0) setColumnWidth(col, w); setUpdatesEnabled(true); } void TransactionForm::paintFocus(QPainter* /*p*/, const QRect& /*cr*/) { } void TransactionForm::adjustColumn(eTransactionForm::Column col) { Q_D(TransactionForm); int w = 0; // preset the width of the right value column with the width of // the possible edit widgets so that they fit if they pop up if (col == eTransactionForm::Column::Value2) { KMyMoneyDateInput dateInput; KMyMoneyEdit valInput; w = qMax(dateInput.sizeHint().width(), valInput.sizeHint().width()); } if (d->m_transaction) { QString txt; QFontMetrics fontMetrics(KMyMoneySettings::listCellFontEx()); // scan through the rows for (int i = rowCount() - 1; i >= 0; --i) { Qt::Alignment align; int spacing = 10; d->m_transaction->formCellText(txt, align, i, static_cast(col), 0); QWidget* cw = cellWidget(i, (int)col); if (cw) { w = qMax(w, cw->sizeHint().width() + spacing); // if the cell widget contains a push button increase the spacing used // for the cell text value to consider the size of the push button if (QPushButton *pushButton = cw->findChild()) { spacing += pushButton->sizeHint().width() + 5; } } w = qMax(w, fontMetrics.width(txt) + spacing); } } if ((int)col < columnCount()) setColumnWidth((int)col, w); } void TransactionForm::arrangeEditWidgets(QMap& editWidgets, KMyMoneyRegister::Transaction* t) { t->arrangeWidgetsInForm(editWidgets); resize((int)eTransactionForm::Column::Value1); } void TransactionForm::tabOrder(QWidgetList& tabOrderWidgets, KMyMoneyRegister::Transaction* t) const { t->tabOrderInForm(tabOrderWidgets); } void TransactionForm::removeEditWidgets(QMap& editWidgets) { QMap::iterator it; for (it = editWidgets.begin(); it != editWidgets.end();) { if ((*it)->parentWidget() == this) { editWidgets.erase(it); it = editWidgets.begin(); } else ++it; } for (int row = 0; row < rowCount(); ++row) { for (int col = 0; col < columnCount(); ++col) { if (cellWidget(row, col)) { cellWidget(row, col)->hide(); setCellWidget(row, col, 0); } } } resize((int)eTransactionForm::Column::Value1); // delete all remaining edit widgets (e.g. tabbar) for (it = editWidgets.begin(); it != editWidgets.end();) { delete(*it); // ->deleteLater(); editWidgets.erase(it); it = editWidgets.begin(); } } diff --git a/kmymoney/wizards/kmymoneywizardpage_p.h b/kmymoney/wizards/kmymoneywizardpage_p.h index 50d3b91cb..c697904de 100644 --- a/kmymoney/wizards/kmymoneywizardpage_p.h +++ b/kmymoney/wizards/kmymoneywizardpage_p.h @@ -1,73 +1,74 @@ /*************************************************************************** kmymoneywizardpage_p.h ------------------- copyright : (C) 2006 by Thomas Baumagrt email : ipwizard@users.sourceforge.net (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. * * * ***************************************************************************/ #ifndef KMYMONEYWIZARDPAGE_P_H #define KMYMONEYWIZARDPAGE_P_H // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes class KMandatoryFieldGroup; /** * @author Thomas Baumgart (C) 2006 * * This class represents a helper object required * to be able to use Qt's signal/slot mechanism within * the KMyMoneyWizardPage object which cannot be * derived from QObject directly. */ class KMyMoneyWizardPagePrivate : public QObject { Q_OBJECT Q_DISABLE_COPY(KMyMoneyWizardPagePrivate) public: explicit KMyMoneyWizardPagePrivate(QObject* parent) : QObject(parent), + m_step(0), m_widget(nullptr), m_mandatoryGroup(nullptr) { } virtual ~KMyMoneyWizardPagePrivate() { } void emitCompleteStateChanged() { emit completeStateChanged(); } uint m_step; QWidget *m_widget; KMandatoryFieldGroup *m_mandatoryGroup; Q_SIGNALS: void completeStateChanged(); }; #endif