diff --git a/kmymoney/dialogs/investtransactioneditor.cpp b/kmymoney/dialogs/investtransactioneditor.cpp index 55e67a1be..cc5c7b885 100644 --- a/kmymoney/dialogs/investtransactioneditor.cpp +++ b/kmymoney/dialogs/investtransactioneditor.cpp @@ -1,1227 +1,1229 @@ /* * Copyright 2007-2019 Thomas Baumgart * Copyright 2017-2018 Łukasz Wojniłowicz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "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: explicit InvestTransactionEditorPrivate(InvestTransactionEditor* qq) : TransactionEditorPrivate(qq), m_activity(0), m_phonyAccount(MyMoneyAccount("Phony-ID", MyMoneyAccount())), m_transactionType(eMyMoney::Split::InvestmentTransactionType::BuyShares) { } ~InvestTransactionEditorPrivate() { delete m_activity; } void showCategory(const QString& name, bool visible = true) { Q_Q(InvestTransactionEditor); if (auto cat = dynamic_cast(q->haveWidget(name))) { if (Q_LIKELY(cat->splitButton())) { cat->parentWidget()->setVisible(visible); // show or hide the enclosing QFrame; } else { cat->setVisible(visible); // show or hide the enclosing QFrame; } } } 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 geometries 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(); activity->setObjectName("activity"); d->m_editWidgets["activity"] = activity; connect(activity, &KMyMoneyActivityCombo::activitySelected, this, &InvestTransactionEditor::slotUpdateActivity); connect(activity, &KMyMoneyActivityCombo::activitySelected, this, &InvestTransactionEditor::slotUpdateButtonState); auto postDate = d->m_editWidgets["postdate"] = new KMyMoneyDateInput; connect(postDate, SIGNAL(dateChanged(QDate)), this, SLOT(slotUpdateButtonState())); auto security = new KMyMoneySecurity; security->setObjectName("security"); 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->setObjectName("asset-account"); 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->setObjectName("fee-account"); 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, &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")); interest->setObjectName("interest-account"); d->m_editWidgets["interest-account"] = interest; connect(interest, &KMyMoneyCombo::itemSelected, this, &InvestTransactionEditor::slotUpdateInterestCategory); connect(interest, &QComboBox::editTextChanged, this, &InvestTransactionEditor::slotUpdateButtonState); 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->setObjectName("memo"); 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->setObjectName("shares"); 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->setObjectName("price"); 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; value->setObjectName("fee-amount"); // 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; value->setObjectName("interest-amount"); // 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; reconcile->setObjectName("reconcile"); 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(QString()); 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(QString()); 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); slotReloadEditWidgets(); } } } 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::slotUpdateInterestCategory(const QString& id) { haveWidget("interest-amount")->setDisabled(id.isEmpty()); } 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())); // fee-account aSet.clear(); aSet.addAccountGroup(Account::Type::Expense); aSet.load(fees->selector()); setupCategoryWidget(fees, d->m_feeSplits, id, SLOT(slotEditFeeSplits())); // 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)); + value->setPrecision(MyMoneyMoney::denomToPrec(d->m_currency.smallestAccountFraction())); // interest amount value = dynamic_cast(haveWidget("interest-amount")); if (!value) return; value->setValue(-d->subtotal(d->m_interestSplits)); + value->setPrecision(MyMoneyMoney::denomToPrec(d->m_currency.smallestAccountFraction())); // 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. const QString& action = d->m_item->split().action(); bool isNegative = d->m_item->split().shares().isNegative(); bool allSameActivity = true; for (auto 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"; for (auto 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"; for (auto 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(); auto postDate = dynamic_cast(d->m_editWidgets["postdate"]); if (postDate) { QDate accountOpeningDate = d->m_account.openingDate(); auto asset = dynamic_cast(haveWidget("asset-account")); if (asset && asset->isVisible()) { if (!isMultiSelection() || !asset->currentText().isEmpty()) { const auto assetId = asset->selectedItem(); if (!assetId.isEmpty()) { try { const auto acc = MyMoneyFile::instance()->account(assetId); if (acc.openingDate() > accountOpeningDate) accountOpeningDate = acc.openingDate(); } catch(MyMoneyException& e) { qDebug() << "opening date check failed on account" << assetId << e.what(); } } } } 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(QString()); } 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; } MyMoneyMoney InvestTransactionEditor::totalAmount() const { MyMoneyMoney amount; 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 amount; 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); } return amount; } void InvestTransactionEditor::slotUpdateTotalAmount() { Q_D(InvestTransactionEditor); auto total = dynamic_cast(haveWidget("total")); if (total && total->isVisible()) { total->setText(totalAmount().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->showCategory("interest-account", false); d->showCategory("fee-account", false); 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(QStringLiteral(" ")); } // 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(); } 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 (sec && (!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() || d->m_activity->memoChanged()) s0.setMemo(memo->toPlainText()); } MyMoneySplit assetAccountSplit; QList feeSplits; QList interestSplits; MyMoneySecurity security; MyMoneySecurity currency = file->security(t.commodity()); eMyMoney::Split::InvestmentTransactionType transactionType; // extract the splits from the original transaction, but only // if there is one because otherwise the currency is overridden if (t.commodity().isEmpty()) { 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); } // Don't do any rounding on a split factor if (d->m_activity->type() != eMyMoney::Split::InvestmentTransactionType::SplitShares) { 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/kcurrencycalculator.cpp b/kmymoney/dialogs/kcurrencycalculator.cpp index bf5874d2e..47d89a558 100644 --- a/kmymoney/dialogs/kcurrencycalculator.cpp +++ b/kmymoney/dialogs/kcurrencycalculator.cpp @@ -1,381 +1,379 @@ /* * Copyright 2004-2018 Thomas Baumgart * Copyright 2017 Łukasz Wojniłowicz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "kcurrencycalculator.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "ui_kcurrencycalculator.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" -#include "kmymoneyedit.h" -#include "kmymoneydateinput.h" #include "mymoneyprice.h" #include "mymoneymoney.h" #include "mymoneyexception.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "kmymoneysettings.h" class KCurrencyCalculatorPrivate { Q_DISABLE_COPY(KCurrencyCalculatorPrivate) Q_DECLARE_PUBLIC(KCurrencyCalculator) public: explicit KCurrencyCalculatorPrivate(KCurrencyCalculator *qq, const MyMoneySecurity& from, const MyMoneySecurity& to, const MyMoneyMoney& value, const MyMoneyMoney& shares, const QDate& date, const signed64 resultFraction) : q_ptr(qq), ui(new Ui::KCurrencyCalculator), m_fromCurrency(from), m_toCurrency(to), m_result(shares.abs()), m_value(value.abs()), m_date(date), m_resultFraction(resultFraction) { } ~KCurrencyCalculatorPrivate() { delete ui; } void init() { Q_Q(KCurrencyCalculator); ui->setupUi(q); auto file = MyMoneyFile::instance(); //set main widget of QDialog ui->buttonGroup1->setId(ui->m_amountButton, 0); ui->buttonGroup1->setId(ui->m_rateButton, 1); ui->m_dateFrame->hide(); if (m_date.isValid()) ui->m_dateEdit->setDate(m_date); else ui->m_dateEdit->setDate(QDate::currentDate()); ui->m_fromCurrencyText->setText(QString(MyMoneySecurity::securityTypeToString(m_fromCurrency.securityType()) + ' ' + (m_fromCurrency.isCurrency() ? m_fromCurrency.id() : m_fromCurrency.tradingSymbol()))); ui->m_toCurrencyText->setText(QString(MyMoneySecurity::securityTypeToString(m_toCurrency.securityType()) + ' ' + (m_toCurrency.isCurrency() ? m_toCurrency.id() : m_toCurrency.tradingSymbol()))); //set bold font auto boldFont = ui->m_fromCurrencyText->font(); boldFont.setBold(true); ui->m_fromCurrencyText->setFont(boldFont); boldFont = ui->m_toCurrencyText->font(); boldFont.setBold(true); ui->m_toCurrencyText->setFont(boldFont); ui->m_fromAmount->setText(m_value.formatMoney(QString(), MyMoneyMoney::denomToPrec(m_fromCurrency.smallestAccountFraction()))); ui->m_dateText->setText(QLocale().toString(m_date)); ui->m_updateButton->setChecked(KMyMoneySettings::priceHistoryUpdate()); // setup initial result if (m_result == MyMoneyMoney() && !m_value.isZero()) { const MyMoneyPrice &pr = file->price(m_fromCurrency.id(), m_toCurrency.id(), m_date); if (pr.isValid()) { m_result = m_value * pr.rate(m_toCurrency.id()); } } // fill in initial values - ui->m_toAmount->loadText(m_result.formatMoney(QString(), MyMoneyMoney::denomToPrec(m_resultFraction))); ui->m_toAmount->setPrecision(MyMoneyMoney::denomToPrec(m_resultFraction)); + ui->m_toAmount->setValue(m_result); ui->m_conversionRate->setPrecision(m_fromCurrency.pricePrecision()); q->connect(ui->m_amountButton, &QAbstractButton::clicked, q, &KCurrencyCalculator::slotSetToAmount); q->connect(ui->m_rateButton, &QAbstractButton::clicked, q, &KCurrencyCalculator::slotSetExchangeRate); - q->connect(ui->m_toAmount, &KMyMoneyEdit::valueChanged, q, &KCurrencyCalculator::slotUpdateResult); - q->connect(ui->m_conversionRate, &KMyMoneyEdit::valueChanged, q, &KCurrencyCalculator::slotUpdateRate); + q->connect(ui->m_toAmount, &AmountEdit::valueChanged, q, &KCurrencyCalculator::slotUpdateResult); + q->connect(ui->m_conversionRate, &AmountEdit::valueChanged, q, &KCurrencyCalculator::slotUpdateRate); // use this as the default ui->m_amountButton->animateClick(); q->slotUpdateResult(ui->m_toAmount->text()); // If the from security is not a currency, we only allow entering a price if (!m_fromCurrency.isCurrency()) { ui->m_rateButton->animateClick(); ui->m_amountButton->hide(); ui->m_toAmount->hide(); } } void updateExample(const MyMoneyMoney& price) { QString msg; if (price.isZero()) { msg = QString("1 %1 = ? %2").arg(m_fromCurrency.tradingSymbol()) .arg(m_toCurrency.tradingSymbol()); if (m_fromCurrency.isCurrency()) { msg += QString("\n"); msg += QString("1 %1 = ? %2").arg(m_toCurrency.tradingSymbol()) .arg(m_fromCurrency.tradingSymbol()); } } else { msg = QString("1 %1 = %2 %3").arg(m_fromCurrency.tradingSymbol()) .arg(price.formatMoney(QString(), m_fromCurrency.pricePrecision())) .arg(m_toCurrency.tradingSymbol()); if (m_fromCurrency.isCurrency()) { msg += QString("\n"); msg += QString("1 %1 = %2 %3").arg(m_toCurrency.tradingSymbol()) .arg((MyMoneyMoney::ONE / price).formatMoney(QString(), m_toCurrency.pricePrecision())) .arg(m_fromCurrency.tradingSymbol()); } } ui->m_conversionExample->setText(msg); ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!price.isZero()); } KCurrencyCalculator *q_ptr; Ui::KCurrencyCalculator *ui; MyMoneySecurity m_fromCurrency; MyMoneySecurity m_toCurrency; MyMoneyMoney m_result; MyMoneyMoney m_value; QDate m_date; signed64 m_resultFraction; }; KCurrencyCalculator::KCurrencyCalculator(const MyMoneySecurity& from, const MyMoneySecurity& to, const MyMoneyMoney& value, const MyMoneyMoney& shares, const QDate& date, const signed64 resultFraction, QWidget *parent) : QDialog(parent), d_ptr(new KCurrencyCalculatorPrivate(this, from, to, value, shares, date, resultFraction)) { Q_D(KCurrencyCalculator); d->init(); } KCurrencyCalculator::~KCurrencyCalculator() { Q_D(KCurrencyCalculator); delete d; } bool KCurrencyCalculator::setupSplitPrice(MyMoneyMoney& shares, const MyMoneyTransaction& t, const MyMoneySplit& s, const QMap& priceInfo, QWidget* parentWidget) { auto rc = true; auto file = MyMoneyFile::instance(); if (!s.value().isZero()) { auto cat = file->account(s.accountId()); MyMoneySecurity toCurrency; toCurrency = file->security(cat.currencyId()); // determine the fraction required for this category/account int fract = cat.fraction(toCurrency); if (cat.currencyId() != t.commodity()) { MyMoneyMoney toValue; auto fromCurrency = file->security(t.commodity()); // display only positive values to the user auto fromValue = s.value().abs(); // if we had a price info in the beginning, we use it here if (priceInfo.find(cat.currencyId()) != priceInfo.end()) { toValue = (fromValue * priceInfo[cat.currencyId()]).convert(fract); } // if the shares are still 0, we need to change that if (toValue.isZero()) { const MyMoneyPrice &price = file->price(fromCurrency.id(), toCurrency.id(), t.postDate()); // if the price is valid calculate the shares. If it is invalid // assume a conversion rate of 1.0 if (price.isValid()) { toValue = (price.rate(toCurrency.id()) * fromValue).convert(fract); } else { toValue = fromValue; } } // now present all that to the user QPointer calc = new KCurrencyCalculator(fromCurrency, toCurrency, fromValue, toValue, t.postDate(), fract, parentWidget); if (calc->exec() == QDialog::Rejected) { rc = false; } else shares = (s.value() * calc->price()).convert(fract); delete calc; } else { shares = s.value().convert(fract); } } else shares = s.value(); return rc; } void KCurrencyCalculator::setupPriceEditor() { Q_D(KCurrencyCalculator); d->ui->m_dateFrame->show(); d->ui->m_amountDateFrame->hide(); d->ui->m_updateButton->setChecked(true); d->ui->m_updateButton->hide(); } void KCurrencyCalculator::slotSetToAmount() { Q_D(KCurrencyCalculator); d->ui->m_rateButton->setChecked(false); d->ui->m_toAmount->setEnabled(true); d->ui->m_conversionRate->setEnabled(false); } void KCurrencyCalculator::slotSetExchangeRate() { Q_D(KCurrencyCalculator); d->ui->m_amountButton->setChecked(false); d->ui->m_toAmount->setEnabled(false); d->ui->m_conversionRate->setEnabled(true); } void KCurrencyCalculator::slotUpdateResult(const QString& /*txt*/) { Q_D(KCurrencyCalculator); MyMoneyMoney result = d->ui->m_toAmount->value(); MyMoneyMoney price(0, 1); if (result.isNegative()) { d->ui->m_toAmount->setValue(-result); slotUpdateResult(QString()); return; } if (!result.isZero()) { price = result / d->m_value; - d->ui->m_conversionRate->loadText(price.formatMoney(QString(), d->m_fromCurrency.pricePrecision())); + d->ui->m_conversionRate->setValue(price); d->m_result = (d->m_value * price).convert(d->m_resultFraction); - d->ui->m_toAmount->loadText(d->m_result.formatMoney(d->m_resultFraction)); + d->ui->m_toAmount->setValue(d->m_result); } d->updateExample(price); } void KCurrencyCalculator::slotUpdateRate(const QString& /*txt*/) { Q_D(KCurrencyCalculator); auto price = d->ui->m_conversionRate->value(); if (price.isNegative()) { d->ui->m_conversionRate->setValue(-price); slotUpdateRate(QString()); return; } if (!price.isZero()) { - d->ui->m_conversionRate->loadText(price.formatMoney(QString(), d->m_fromCurrency.pricePrecision())); + d->ui->m_conversionRate->setValue(price); d->m_result = (d->m_value * price).convert(d->m_resultFraction); - d->ui->m_toAmount->loadText(d->m_result.formatMoney(QString(), MyMoneyMoney::denomToPrec(d->m_resultFraction))); + d->ui->m_toAmount->setValue(d->m_result); } d->updateExample(price); } void KCurrencyCalculator::accept() { Q_D(KCurrencyCalculator); if (d->ui->m_conversionRate->isEnabled()) slotUpdateRate(QString()); else slotUpdateResult(QString()); if (d->ui->m_updateButton->isChecked()) { auto pr = MyMoneyFile::instance()->price(d->m_fromCurrency.id(), d->m_toCurrency.id(), d->ui->m_dateEdit->date()); if (!pr.isValid() || pr.date() != d->ui->m_dateEdit->date() || (pr.date() == d->ui->m_dateEdit->date() && pr.rate(d->m_fromCurrency.id()) != price())) { pr = MyMoneyPrice(d->m_fromCurrency.id(), d->m_toCurrency.id(), d->ui->m_dateEdit->date(), price(), i18n("User")); MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->addPrice(pr); ft.commit(); } catch (const MyMoneyException &) { qDebug("Cannot add price"); } } } // remember setting for next round KMyMoneySettings::setPriceHistoryUpdate(d->ui->m_updateButton->isChecked()); QDialog::accept(); } MyMoneyMoney KCurrencyCalculator::price() const { Q_D(const KCurrencyCalculator); // This should fix https://bugs.kde.org/show_bug.cgi?id=205254 and // https://bugs.kde.org/show_bug.cgi?id=325953 as well as // https://bugs.kde.org/show_bug.cgi?id=300965 if (d->ui->m_amountButton->isChecked()) return d->ui->m_toAmount->value().abs() / d->m_value.abs(); else return d->ui->m_conversionRate->value(); } diff --git a/kmymoney/dialogs/kcurrencycalculator.ui b/kmymoney/dialogs/kcurrencycalculator.ui index a4b72c361..8a46056ed 100644 --- a/kmymoney/dialogs/kcurrencycalculator.ui +++ b/kmymoney/dialogs/kcurrencycalculator.ui @@ -1,400 +1,406 @@ KCurrencyCalculator true 0 0 387 - 268 + 336 Exchange Rate/Price Editor true QFrame::NoFrame QFrame::Plain 0 0 0 0 Amount false xxx false Qt::Horizontal QSizePolicy::Expanding 80 20 Date false xxx false Qt::Horizontal QSizePolicy::Expanding 145 20 Convert 0 Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter false to Qt::AlignCenter false Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter false - - - true 0 0 0 0 QFrame::NoFrame QFrame::Plain 0 0 0 0 To a&mount buttonGroup1 Exchange &rate / Price buttonGroup1 + + + + + + + + + + xx +xx + + + false + + + QFrame::NoFrame QFrame::Plain 0 0 0 0 Date false - + + + true + + - - - - xx -xx - - - false - - - - - - Update price history true Qt::Vertical QSizePolicy::Expanding 20 16 QDialogButtonBox::Cancel|QDialogButtonBox::Ok - KMyMoneyDateInput - QWidget -
kmymoneydateinput.h
- 1 + AmountEdit + QLineEdit +
amountedit.h
- KMyMoneyEdit - QWidget -
kmymoneyedit.h
+ KMyMoneyDateEdit + QDateEdit +
kmymoneydateedit.h
m_amountButton m_rateButton + m_toAmount + m_conversionRate + m_dateEdit m_updateButton buttonBox accepted() KCurrencyCalculator accept() 189 233 193 133 buttonBox rejected() KCurrencyCalculator reject() 189 233 193 133
diff --git a/kmymoney/dialogs/knewaccountdlg.cpp b/kmymoney/dialogs/knewaccountdlg.cpp index af39d24ce..0470e4040 100644 --- a/kmymoney/dialogs/knewaccountdlg.cpp +++ b/kmymoney/dialogs/knewaccountdlg.cpp @@ -1,963 +1,962 @@ /* * Copyright 2000-2003 Michael Edwardes * Copyright 2005-2018 Thomas Baumgart * Copyright 2017-2018 Łukasz Wojniłowicz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "knewaccountdlg.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Headers #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "ui_knewaccountdlg.h" #include "kmymoneyedit.h" #include "kmymoneydateinput.h" #include #include "mymoneyfile.h" #include "mymoneyinstitution.h" #include "mymoneyaccount.h" #include "kmymoneysettings.h" #include "kmymoneycurrencyselector.h" #include "knewbankdlg.h" #include "models.h" #include "accountsmodel.h" #include "hierarchyfilterproxymodel.h" #include "mymoneyenums.h" #include "modelenums.h" using namespace eMyMoney; class KNewAccountDlgPrivate { Q_DISABLE_COPY(KNewAccountDlgPrivate) Q_DECLARE_PUBLIC(KNewAccountDlg) public: explicit KNewAccountDlgPrivate(KNewAccountDlg *qq) : q_ptr(qq), ui(new Ui::KNewAccountDlg), m_filterProxyModel(nullptr), m_categoryEditor(false), m_isEditing(false) { } ~KNewAccountDlgPrivate() { delete ui; } void init() { Q_Q(KNewAccountDlg); ui->setupUi(q); auto file = MyMoneyFile::instance(); // initialize the m_parentAccount member QVector filterAccountGroup {m_account.accountGroup()}; switch (m_account.accountGroup()) { case Account::Type::Asset: m_parentAccount = file->asset(); break; case Account::Type::Liability: m_parentAccount = file->liability(); break; case Account::Type::Income: m_parentAccount = file->income(); break; case Account::Type::Expense: m_parentAccount = file->expense(); break; case Account::Type::Equity: m_parentAccount = file->equity(); break; default: qDebug("Seems we have an account that hasn't been mapped to the top five"); if (m_categoryEditor) { m_parentAccount = file->income(); filterAccountGroup[0] = Account::Type::Income; } else { m_parentAccount = file->asset(); filterAccountGroup[0] = Account::Type::Asset; } } ui->m_amountGroup->setId(ui->m_grossAmount, 0); ui->m_amountGroup->setId(ui->m_netAmount, 1); // the proxy filter model m_filterProxyModel = new HierarchyFilterProxyModel(q); m_filterProxyModel->setHideClosedAccounts(true); m_filterProxyModel->setHideEquityAccounts(!KMyMoneySettings::expertMode()); m_filterProxyModel->addAccountGroup(filterAccountGroup); m_filterProxyModel->setCurrentAccountId(m_account.id()); auto const model = Models::instance()->accountsModel(); m_filterProxyModel->setSourceModel(model); m_filterProxyModel->setSourceColumns(model->getColumns()); m_filterProxyModel->setDynamicSortFilter(true); ui->m_parentAccounts->setModel(m_filterProxyModel); ui->m_parentAccounts->sortByColumn((int)eAccountsModel::Column::Account, Qt::AscendingOrder); ui->m_subAccountLabel->setText(i18n("Is a sub account")); ui->accountNameEdit->setText(m_account.name()); ui->descriptionEdit->setText(m_account.description()); ui->typeCombo->setEnabled(true); // load the price mode combo ui->m_priceMode->insertItem(i18nc("default price mode", "(default)"), 0); ui->m_priceMode->insertItem(i18n("Price per share"), 1); ui->m_priceMode->insertItem(i18n("Total for all shares"), 2); int priceMode = 0; if (m_account.accountType() == Account::Type::Investment) { ui->m_priceMode->setEnabled(true); if (!m_account.value("priceMode").isEmpty()) priceMode = m_account.value("priceMode").toInt(); } ui->m_priceMode->setCurrentItem(priceMode); bool haveMinBalance = false; bool haveMaxCredit = false; if (!m_account.openingDate().isValid()) { m_account.setOpeningDate(KMyMoneySettings::firstFiscalDate()); } ui->m_openingDateEdit->setDate(m_account.openingDate()); handleOpeningBalanceCheckbox(m_account.currencyId()); if (m_categoryEditor) { // get rid of the tabs that are not used for categories int tab = ui->m_tab->indexOf(ui->m_institutionTab); if (tab != -1) ui->m_tab->removeTab(tab); tab = ui->m_tab->indexOf(ui->m_limitsTab); if (tab != -1) ui->m_tab->removeTab(tab); //m_qlistviewParentAccounts->setEnabled(true); ui->accountNoEdit->setEnabled(false); ui->m_institutionBox->hide(); ui->m_qcheckboxNoVat->hide(); ui->typeCombo->addItem(MyMoneyAccount::accountTypeToString(Account::Type::Income), (int)Account::Type::Income); ui->typeCombo->addItem(MyMoneyAccount::accountTypeToString(Account::Type::Expense), (int)Account::Type::Expense); // Hardcoded but acceptable - if above we set the default to income do the same here switch (m_account.accountType()) { case Account::Type::Expense: ui->typeCombo->setCurrentItem(MyMoneyAccount::accountTypeToString(Account::Type::Expense), false); break; case Account::Type::Income: default: ui->typeCombo->setCurrentItem(MyMoneyAccount::accountTypeToString(Account::Type::Income), false); break; } ui->m_currency->setEnabled(true); if (m_isEditing) { ui->typeCombo->setEnabled(false); ui->m_currency->setDisabled(MyMoneyFile::instance()->isReferenced(m_account)); } ui->m_qcheckboxPreferred->hide(); ui->m_qcheckboxTax->setChecked(m_account.value("Tax").toLower() == "yes"); ui->m_costCenterRequiredCheckBox->setChecked(m_account.isCostCenterRequired()); loadVatAccounts(); } else { // get rid of the tabs that are not used for accounts int taxtab = ui->m_tab->indexOf(ui->m_taxTab); if (taxtab != -1) { ui->m_vatCategory->setText(i18n("VAT account")); ui->m_qcheckboxTax->setChecked(m_account.value("Tax") == "Yes"); loadVatAccounts(); } else { ui->m_tab->removeTab(taxtab); } ui->m_costCenterRequiredCheckBox->hide(); switch (m_account.accountType()) { case Account::Type::Savings: case Account::Type::Cash: haveMinBalance = true; break; case Account::Type::Checkings: haveMinBalance = true; haveMaxCredit = true; break; case Account::Type::CreditCard: haveMaxCredit = true; break; default: // no limit available, so we might get rid of the tab int tab = ui->m_tab->indexOf(ui->m_limitsTab); if (tab != -1) ui->m_tab->removeTab(tab); // don't try to hide the widgets we just wiped // in the next step haveMaxCredit = haveMinBalance = true; break; } if (!haveMaxCredit) { ui->m_maxCreditLabel->setEnabled(false); ui->m_maxCreditLabel->hide(); ui->m_maxCreditEarlyEdit->hide(); ui->m_maxCreditAbsoluteEdit->hide(); } if (!haveMinBalance) { ui->m_minBalanceLabel->setEnabled(false); ui->m_minBalanceLabel->hide(); ui->m_minBalanceEarlyEdit->hide(); ui->m_minBalanceAbsoluteEdit->hide(); } QString typeString = MyMoneyAccount::accountTypeToString(m_account.accountType()); if (m_isEditing) { if (m_account.isLiquidAsset()) { ui->typeCombo->addItem(MyMoneyAccount::accountTypeToString(Account::Type::Checkings), (int)Account::Type::Checkings); ui->typeCombo->addItem(MyMoneyAccount::accountTypeToString(Account::Type::Savings), (int)Account::Type::Savings); ui->typeCombo->addItem(MyMoneyAccount::accountTypeToString(Account::Type::Cash), (int)Account::Type::Cash); } else { ui->typeCombo->addItem(typeString, (int)m_account.accountType()); // Once created, accounts of other account types are not // allowed to be changed. ui->typeCombo->setEnabled(false); } // Once created, a currency cannot be changed if it is referenced. ui->m_currency->setDisabled(MyMoneyFile::instance()->isReferenced(m_account)); } else { ui->typeCombo->addItem(MyMoneyAccount::accountTypeToString(Account::Type::Checkings), (int)Account::Type::Checkings); ui->typeCombo->addItem(MyMoneyAccount::accountTypeToString(Account::Type::Savings), (int)Account::Type::Savings); ui->typeCombo->addItem(MyMoneyAccount::accountTypeToString(Account::Type::Cash), (int)Account::Type::Cash); ui->typeCombo->addItem(MyMoneyAccount::accountTypeToString(Account::Type::CreditCard), (int)Account::Type::CreditCard); ui->typeCombo->addItem(MyMoneyAccount::accountTypeToString(Account::Type::Loan), (int)Account::Type::Loan); ui->typeCombo->addItem(MyMoneyAccount::accountTypeToString(Account::Type::Investment), (int)Account::Type::Investment); ui->typeCombo->addItem(MyMoneyAccount::accountTypeToString(Account::Type::Asset), (int)Account::Type::Asset); ui->typeCombo->addItem(MyMoneyAccount::accountTypeToString(Account::Type::Liability), (int)Account::Type::Liability); ui->typeCombo->addItem(MyMoneyAccount::accountTypeToString(Account::Type::Stock), (int)Account::Type::Stock); /* ui->typeCombo->addItem(MyMoneyAccount::accountTypeToString(Account::Type::CertificateDep), (int)Account::Type::CertificateDep); ui->typeCombo->addItem(MyMoneyAccount::accountTypeToString(Account::Type::MoneyMarket), (int)Account::Type::MoneyMarket); ui->typeCombo->addItem(MyMoneyAccount::accountTypeToString(Account::Type::Currency), (int)Account::Type::Currency); */ // Do not create account types that are not supported // by the current engine. if (m_account.accountType() == Account::Type::Unknown || m_account.accountType() == Account::Type::CertificateDep || m_account.accountType() == Account::Type::MoneyMarket || m_account.accountType() == Account::Type::Currency) typeString = MyMoneyAccount::accountTypeToString(Account::Type::Checkings); } ui->typeCombo->setCurrentItem(typeString, false); if (m_account.isInvest()) ui->m_institutionBox->hide(); ui->accountNoEdit->setText(m_account.number()); ui->m_qcheckboxPreferred->setChecked(m_account.value("PreferredAccount") == "Yes"); ui->m_qcheckboxNoVat->setChecked(m_account.value("NoVat") == "Yes"); loadKVP("iban", ui->ibanEdit); loadKVP("minBalanceAbsolute", ui->m_minBalanceAbsoluteEdit); loadKVP("minBalanceEarly", ui->m_minBalanceEarlyEdit); loadKVP("maxCreditAbsolute", ui->m_maxCreditAbsoluteEdit); loadKVP("maxCreditEarly", ui->m_maxCreditEarlyEdit); // reverse the sign for display purposes if (!ui->m_maxCreditAbsoluteEdit->lineedit()->text().isEmpty()) ui->m_maxCreditAbsoluteEdit->setValue(ui->m_maxCreditAbsoluteEdit->value()*MyMoneyMoney::MINUS_ONE); if (!ui->m_maxCreditEarlyEdit->lineedit()->text().isEmpty()) ui->m_maxCreditEarlyEdit->setValue(ui->m_maxCreditEarlyEdit->value()*MyMoneyMoney::MINUS_ONE); loadKVP("lastNumberUsed", ui->m_lastCheckNumberUsed); if (m_account.isInvest()) { ui->typeCombo->setEnabled(false); ui->m_qcheckboxPreferred->hide(); ui->m_currencyText->hide(); ui->m_currency->hide(); } else { // use the old field and override a possible new value if (!MyMoneyMoney(m_account.value("minimumBalance")).isZero()) { ui->m_minBalanceAbsoluteEdit->setValue(MyMoneyMoney(m_account.value("minimumBalance"))); } } // ui->m_qcheckboxTax->hide(); TODO should only be visible for VAT category/account } ui->m_currency->setSecurity(file->currency(m_account.currencyId())); // Load the institutions // then the accounts QString institutionName; try { if (m_isEditing && !m_account.institutionId().isEmpty()) institutionName = file->institution(m_account.institutionId()).name(); else institutionName.clear(); } catch (const MyMoneyException &e) { qDebug("exception in init for account dialog: %s", e.what()); } if (m_account.isInvest()) ui->m_parentAccounts->setEnabled(false); if (!m_categoryEditor) q->slotLoadInstitutions(institutionName); ui->accountNameEdit->setFocus(); q->connect(ui->buttonBox, &QDialogButtonBox::rejected, q, &QDialog::reject); q->connect(ui->buttonBox, &QDialogButtonBox::accepted, q, &KNewAccountDlg::okClicked); q->connect(ui->m_parentAccounts->selectionModel(), &QItemSelectionModel::selectionChanged, q, &KNewAccountDlg::slotSelectionChanged); q->connect(ui->m_qbuttonNew, &QAbstractButton::clicked, q, &KNewAccountDlg::slotNewClicked); q->connect(ui->typeCombo, static_cast(&QComboBox::currentIndexChanged), q, &KNewAccountDlg::slotAccountTypeChanged); q->connect(ui->accountNameEdit, &QLineEdit::textChanged, q, &KNewAccountDlg::slotCheckFinished); q->connect(ui->m_vatCategory, &QAbstractButton::toggled, q, &KNewAccountDlg::slotVatChanged); q->connect(ui->m_vatAssignment, &QAbstractButton::toggled, q, &KNewAccountDlg::slotVatAssignmentChanged); q->connect(ui->m_vatCategory, &QAbstractButton::toggled, q, &KNewAccountDlg::slotCheckFinished); q->connect(ui->m_vatAssignment, &QAbstractButton::toggled, q, &KNewAccountDlg::slotCheckFinished); q->connect(ui->m_vatRate, &KMyMoneyEdit::textChanged, q, &KNewAccountDlg::slotCheckFinished); q->connect(ui->m_vatAccount, &KMyMoneySelector::stateChanged, q, &KNewAccountDlg::slotCheckFinished); q->connect(ui->m_currency, static_cast(&QComboBox::activated), q, &KNewAccountDlg::slotCheckCurrency); q->connect(ui->m_minBalanceEarlyEdit, &KMyMoneyEdit::valueChanged, q, &KNewAccountDlg::slotAdjustMinBalanceAbsoluteEdit); q->connect(ui->m_minBalanceAbsoluteEdit, &KMyMoneyEdit::valueChanged, q, &KNewAccountDlg::slotAdjustMinBalanceEarlyEdit); q->connect(ui->m_maxCreditEarlyEdit, &KMyMoneyEdit::valueChanged, q, &KNewAccountDlg::slotAdjustMaxCreditAbsoluteEdit); q->connect(ui->m_maxCreditAbsoluteEdit, &KMyMoneyEdit::valueChanged, q, &KNewAccountDlg::slotAdjustMaxCreditEarlyEdit); q->connect(ui->m_qcomboboxInstitutions, static_cast(&QComboBox::activated), q, &KNewAccountDlg::slotLoadInstitutions); auto parentIndex = m_filterProxyModel->getSelectedParentAccountIndex(); ui->m_parentAccounts->expand(parentIndex); ui->m_parentAccounts->selectionModel()->select(parentIndex, QItemSelectionModel::SelectCurrent); ui->m_parentAccounts->scrollTo(parentIndex, QAbstractItemView::PositionAtTop); ui->m_vatCategory->setChecked(false); ui->m_vatAssignment->setChecked(false); // make sure our account does not have an id and no parent assigned // and certainly no children in case we create a new account if (!m_isEditing) { m_account.clearId(); m_account.setParentAccountId(QString()); m_account.removeAccountIds(); } else { if (!m_account.value("VatRate").isEmpty()) { ui->m_vatCategory->setChecked(true); ui->m_vatRate->setValue(MyMoneyMoney(m_account.value("VatRate"))*MyMoneyMoney(100, 1)); } else { if (!m_account.value("VatAccount").isEmpty()) { QString accId = m_account.value("VatAccount").toLatin1(); try { // make sure account exists MyMoneyFile::instance()->account(accId); ui->m_vatAssignment->setChecked(true); ui->m_vatAccount->setSelected(accId); ui->m_grossAmount->setChecked(true); if (m_account.value("VatAmount") == "Net") ui->m_netAmount->setChecked(true); } catch (const MyMoneyException &) { } } } } q->slotVatChanged(ui->m_vatCategory->isChecked()); q->slotVatAssignmentChanged(ui->m_vatAssignment->isChecked()); q->slotCheckFinished(); auto requiredFields = new KMandatoryFieldGroup(q); requiredFields->setOkButton(ui->buttonBox->button(QDialogButtonBox::Ok)); // button to be enabled when all fields present requiredFields->add(ui->accountNameEdit); } void loadKVP(const QString& key, KMyMoneyEdit* widget) { if (!widget) return; if (m_account.value(key).isEmpty()) { widget->clearText(); } else { widget->setValue(MyMoneyMoney(m_account.value(key))); } } void loadKVP(const QString& key, KLineEdit* widget) { if (!widget) return; widget->setText(m_account.value(key)); } void storeKVP(const QString& key, const QString& text, const QString& value) { if (text.isEmpty()) m_account.deletePair(key); else m_account.setValue(key, value); } void storeKVP(const QString& key, QCheckBox* widget) { if (widget) { if(widget->isChecked()) { m_account.setValue(key, "Yes");; } else { m_account.deletePair(key); } } } void storeKVP(const QString& key, KMyMoneyEdit* widget) { storeKVP(key, widget->lineedit()->text(), widget->text()); } void storeKVP(const QString& key, KLineEdit* widget) { storeKVP(key, widget->text(), widget->text()); } void loadVatAccounts() { QList list; MyMoneyFile::instance()->accountList(list); QList::Iterator it; QStringList loadListExpense; QStringList loadListIncome; QStringList loadListAsset; QStringList loadListLiability; for (it = list.begin(); it != list.end(); ++it) { if (!(*it).value("VatRate").isEmpty()) { if ((*it).accountType() == Account::Type::Expense) loadListExpense += (*it).id(); else if ((*it).accountType() == Account::Type::Income) loadListIncome += (*it).id(); else if ((*it).accountType() == Account::Type::Asset) loadListAsset += (*it).id(); else if ((*it).accountType() == Account::Type::Liability) loadListLiability += (*it).id(); } } AccountSet vatSet; if (!loadListAsset.isEmpty()) vatSet.load(ui->m_vatAccount, i18n("Asset"), loadListAsset, true); if (!loadListLiability.isEmpty()) vatSet.load(ui->m_vatAccount, i18n("Liability"), loadListLiability, false); if (!loadListIncome.isEmpty()) vatSet.load(ui->m_vatAccount, i18n("Income"), loadListIncome, false); if (!loadListExpense.isEmpty()) vatSet.load(ui->m_vatAccount, i18n("Expense"), loadListExpense, false); } void adjustEditWidgets(KMyMoneyEdit* dst, KMyMoneyEdit* src, char mode, int corr) { MyMoneyMoney factor(corr, 1); if (m_account.accountGroup() == Account::Type::Asset) factor = -factor; switch (mode) { case '<': if (src->value()*factor < dst->value()*factor) dst->setValue(src->value()); break; case '>': if (src->value()*factor > dst->value()*factor) dst->setValue(src->value()); break; } } void handleOpeningBalanceCheckbox(const QString ¤cyId) { if (m_account.accountType() == Account::Type::Equity) { // check if there is another opening balance account with the same currency bool isOtherOpenBalancingAccount = false; QList list; MyMoneyFile::instance()->accountList(list); QList::Iterator it; for (it = list.begin(); it != list.end(); ++it) { if (it->id() == m_account.id() || currencyId != it->currencyId() || it->accountType() != Account::Type::Equity) continue; if (it->value("OpeningBalanceAccount") == "Yes") { isOtherOpenBalancingAccount = true; break; } } if (!isOtherOpenBalancingAccount) { bool isOpenBalancingAccount = m_account.value("OpeningBalanceAccount") == "Yes"; ui->m_qcheckboxOpeningBalance->setChecked(isOpenBalancingAccount); if (isOpenBalancingAccount) { // let only allow state change if no transactions are assigned to this account bool hasTransactions = MyMoneyFile::instance()->transactionCount(m_account.id()) != 0; ui->m_qcheckboxOpeningBalance->setEnabled(!hasTransactions); if (hasTransactions) ui->m_qcheckboxOpeningBalance->setToolTip(i18n("Option has been disabled because there are transactions assigned to this account")); } } else { ui->m_qcheckboxOpeningBalance->setChecked(false); ui->m_qcheckboxOpeningBalance->setEnabled(false); ui->m_qcheckboxOpeningBalance->setToolTip(i18n("Option has been disabled because there is another account flagged to be an opening balance account for this currency")); } } else { ui->m_qcheckboxOpeningBalance->setVisible(false); } } KNewAccountDlg *q_ptr; Ui::KNewAccountDlg *ui; MyMoneyAccount m_account; MyMoneyAccount m_parentAccount; HierarchyFilterProxyModel *m_filterProxyModel; bool m_categoryEditor; bool m_isEditing; }; KNewAccountDlg::KNewAccountDlg(const MyMoneyAccount& account, bool isEditing, bool categoryEditor, QWidget *parent, const QString& title) : QDialog(parent), d_ptr(new KNewAccountDlgPrivate(this)) { Q_D(KNewAccountDlg); d->m_account = account; d->m_categoryEditor = categoryEditor; d->m_isEditing = isEditing; d->init(); if (!title.isEmpty()) setWindowTitle(title); } MyMoneyMoney KNewAccountDlg::openingBalance() const { Q_D(const KNewAccountDlg); return d->ui->m_openingBalanceEdit->value(); } void KNewAccountDlg::setOpeningBalance(const MyMoneyMoney& balance) { Q_D(KNewAccountDlg); d->ui->m_openingBalanceEdit->setValue(balance); } void KNewAccountDlg::setOpeningBalanceShown(bool shown) { Q_D(KNewAccountDlg); d->ui->m_openingBalanceLabel->setVisible(shown); d->ui->m_openingBalanceEdit->setVisible(shown); } void KNewAccountDlg::setOpeningDateShown(bool shown) { Q_D(KNewAccountDlg); d->ui->m_openingDateLabel->setVisible(shown); d->ui->m_openingDateEdit->setVisible(shown); } void KNewAccountDlg::okClicked() { Q_D(KNewAccountDlg); auto file = MyMoneyFile::instance(); QString accountNameText = d->ui->accountNameEdit->text(); if (accountNameText.isEmpty()) { KMessageBox::error(this, i18n("You have not specified a name.\nPlease fill in this field.")); d->ui->accountNameEdit->setFocus(); return; } MyMoneyAccount parent = parentAccount(); if (parent.name().length() == 0) { KMessageBox::error(this, i18n("Please select a parent account.")); return; } if (!d->m_categoryEditor) { QString institutionNameText = d->ui->m_qcomboboxInstitutions->currentText(); if (institutionNameText != i18n("(No Institution)")) { try { QList list = file->institutionList(); QList::ConstIterator institutionIterator; for (institutionIterator = list.constBegin(); institutionIterator != list.constEnd(); ++institutionIterator) { if ((*institutionIterator).name() == institutionNameText) d->m_account.setInstitutionId((*institutionIterator).id()); } } catch (const MyMoneyException &e) { qDebug("Exception in account institution set: %s", e.what()); } } else { d->m_account.setInstitutionId(QString()); } } d->m_account.setName(accountNameText); d->m_account.setNumber(d->ui->accountNoEdit->text()); d->storeKVP("iban", d->ui->ibanEdit); d->storeKVP("minBalanceAbsolute", d->ui->m_minBalanceAbsoluteEdit); d->storeKVP("minBalanceEarly", d->ui->m_minBalanceEarlyEdit); // the figures for credit line with reversed sign if (!d->ui->m_maxCreditAbsoluteEdit->lineedit()->text().isEmpty()) d->ui->m_maxCreditAbsoluteEdit->setValue(d->ui->m_maxCreditAbsoluteEdit->value()*MyMoneyMoney::MINUS_ONE); if (!d->ui->m_maxCreditEarlyEdit->lineedit()->text().isEmpty()) d->ui->m_maxCreditEarlyEdit->setValue(d->ui->m_maxCreditEarlyEdit->value()*MyMoneyMoney::MINUS_ONE); d->storeKVP("maxCreditAbsolute", d->ui->m_maxCreditAbsoluteEdit); d->storeKVP("maxCreditEarly", d->ui->m_maxCreditEarlyEdit); if (!d->ui->m_maxCreditAbsoluteEdit->lineedit()->text().isEmpty()) d->ui->m_maxCreditAbsoluteEdit->setValue(d->ui->m_maxCreditAbsoluteEdit->value()*MyMoneyMoney::MINUS_ONE); if (!d->ui->m_maxCreditEarlyEdit->lineedit()->text().isEmpty()) d->ui->m_maxCreditEarlyEdit->setValue(d->ui->m_maxCreditEarlyEdit->value()*MyMoneyMoney::MINUS_ONE); d->storeKVP("lastNumberUsed", d->ui->m_lastCheckNumberUsed); // delete a previous version of the minimumbalance information d->storeKVP("minimumBalance", QString(), QString()); Account::Type acctype; if (!d->m_categoryEditor) { acctype = static_cast(d->ui->typeCombo->currentData().toInt()); // If it's a loan, check if the parent is asset or liability. In // case of asset, we change the account type to be AssetLoan if (acctype == Account::Type::Loan && parent.accountGroup() == Account::Type::Asset) acctype = Account::Type::AssetLoan; } else { acctype = parent.accountGroup(); QString newName; if (!MyMoneyFile::instance()->isStandardAccount(parent.id())) { newName = MyMoneyFile::instance()->accountToCategory(parent.id()) + MyMoneyFile::AccountSeparator; } newName += accountNameText; if (!file->categoryToAccount(newName, acctype).isEmpty() && (file->categoryToAccount(newName, acctype) != d->m_account.id())) { KMessageBox::error(this, QString("") + i18n("A category named %1 already exists. You cannot create a second category with the same name.", newName) + QString("")); return; } } d->m_account.setAccountType(acctype); d->m_account.setDescription(d->ui->descriptionEdit->toPlainText()); d->m_account.setOpeningDate(d->ui->m_openingDateEdit->date()); if (!d->m_categoryEditor) { d->m_account.setCurrencyId(d->ui->m_currency->security().id()); d->storeKVP("PreferredAccount", d->ui->m_qcheckboxPreferred); d->storeKVP("NoVat", d->ui->m_qcheckboxNoVat); if (d->ui->m_minBalanceAbsoluteEdit->isVisible()) { d->m_account.setValue("minimumBalance", d->ui->m_minBalanceAbsoluteEdit->value().toString()); } } else { if (KMyMoneySettings::hideUnusedCategory() && !d->m_isEditing) { KMessageBox::information(this, i18n("You have selected to suppress the display of unused categories in the KMyMoney configuration dialog. The category you just created will therefore only be shown if it is used. Otherwise, it will be hidden in the accounts/categories view."), i18n("Hidden categories"), "NewHiddenCategory"); } d->m_account.setCostCenterRequired(d->ui->m_costCenterRequiredCheckBox->isChecked()); } d->storeKVP("Tax", d->ui->m_qcheckboxTax); if (d->ui->m_qcheckboxOpeningBalance->isChecked()) d->m_account.setValue("OpeningBalanceAccount", "Yes"); else d->m_account.deletePair("OpeningBalanceAccount"); d->m_account.deletePair("VatAccount"); d->m_account.deletePair("VatAmount"); d->m_account.deletePair("VatRate"); if (d->ui->m_vatCategory->isChecked()) { d->m_account.setValue("VatRate", (d->ui->m_vatRate->value().abs() / MyMoneyMoney(100, 1)).toString()); } else { if (d->ui->m_vatAssignment->isChecked() && !d->ui->m_vatAccount->selectedItems().isEmpty()) { d->m_account.setValue("VatAccount", d->ui->m_vatAccount->selectedItems().first()); if (d->ui->m_netAmount->isChecked()) d->m_account.setValue("VatAmount", "Net"); } } accept(); } MyMoneyAccount KNewAccountDlg::account() { Q_D(KNewAccountDlg); // assign the right currency to the account d->m_account.setCurrencyId(d->ui->m_currency->security().id()); // and the price mode switch (d->ui->m_priceMode->currentItem()) { case 0: d->m_account.deletePair("priceMode"); break; case 1: case 2: d->m_account.setValue("priceMode", QString("%1").arg(d->ui->m_priceMode->currentItem())); break; } return d->m_account; } MyMoneyAccount KNewAccountDlg::parentAccount() const { Q_D(const KNewAccountDlg); return d->m_parentAccount; } void KNewAccountDlg::slotSelectionChanged(const QItemSelection ¤t, const QItemSelection &previous) { Q_UNUSED(previous) Q_D(KNewAccountDlg); if (!current.indexes().empty()) { QVariant account = d->ui->m_parentAccounts->model()->data(current.indexes().front(), (int)eAccountsModel::Role::Account); if (account.isValid()) { d->m_parentAccount = account.value(); d->ui->m_subAccountLabel->setText(i18n("Is a sub account of %1", d->m_parentAccount.name())); } } } void KNewAccountDlg::slotLoadInstitutions(const QString& name) { Q_D(KNewAccountDlg); d->ui->m_qcomboboxInstitutions->clear(); - QString bic; // Are we forcing the user to use institutions? d->ui->m_qcomboboxInstitutions->addItem(i18n("(No Institution)")); d->ui->m_bicValue->setText(" "); d->ui->ibanEdit->setEnabled(false); d->ui->accountNoEdit->setEnabled(false); try { auto file = MyMoneyFile::instance(); QList list = file->institutionList(); QList::ConstIterator institutionIterator; for (institutionIterator = list.constBegin(); institutionIterator != list.constEnd(); ++institutionIterator) { if ((*institutionIterator).name() == name) { d->ui->ibanEdit->setEnabled(true); d->ui->accountNoEdit->setEnabled(true); d->ui->m_bicValue->setText((*institutionIterator).value("bic")); } d->ui->m_qcomboboxInstitutions->addItem((*institutionIterator).name()); } d->ui->m_qcomboboxInstitutions->setCurrentItem(name, false); } catch (const MyMoneyException &e) { qDebug("Exception in institution load: %s", e.what()); } } void KNewAccountDlg::slotNewClicked() { MyMoneyInstitution institution; QPointer dlg = new KNewBankDlg(institution, this); if (dlg->exec()) { MyMoneyFileTransaction ft; try { auto file = MyMoneyFile::instance(); institution = dlg->institution(); file->addInstitution(institution); ft.commit(); slotLoadInstitutions(institution.name()); } catch (const MyMoneyException &) { KMessageBox::information(this, i18n("Cannot add institution")); } } delete dlg; } void KNewAccountDlg::slotAccountTypeChanged(int index) { Q_D(KNewAccountDlg); Account::Type oldType; auto type = d->ui->typeCombo->itemData(index).value(); try { oldType = d->m_account.accountType(); if (oldType != type) { d->m_account.setAccountType(type); // update the account group displayed in the accounts hierarchy d->m_filterProxyModel->clear(); d->m_filterProxyModel->addAccountGroup(QVector {d->m_account.accountGroup()}); } } catch (const MyMoneyException &) { qWarning("Unexpected exception in KNewAccountDlg::slotAccountTypeChanged()"); } } void KNewAccountDlg::slotCheckFinished() { Q_D(KNewAccountDlg); auto showButton = true; if (d->ui->accountNameEdit->text().length() == 0) { showButton = false; } if (d->ui->m_vatCategory->isChecked() && d->ui->m_vatRate->value() <= MyMoneyMoney()) { showButton = false; } else { if (d->ui->m_vatAssignment->isChecked() && d->ui->m_vatAccount->selectedItems().isEmpty()) showButton = false; } d->ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(showButton); } void KNewAccountDlg::slotVatChanged(bool state) { Q_D(KNewAccountDlg); if (state) { d->ui->m_vatCategoryFrame->show(); d->ui->m_vatAssignmentFrame->hide(); } else { d->ui->m_vatCategoryFrame->hide(); if (!d->m_account.isAssetLiability()) { d->ui->m_vatAssignmentFrame->show(); } } } void KNewAccountDlg::slotVatAssignmentChanged(bool state) { Q_D(KNewAccountDlg); d->ui->m_vatAccount->setEnabled(state); d->ui->m_amountGroupBox->setEnabled(state); } void KNewAccountDlg::slotAdjustMinBalanceAbsoluteEdit(const QString&) { Q_D(KNewAccountDlg); d->adjustEditWidgets(d->ui->m_minBalanceAbsoluteEdit, d->ui->m_minBalanceEarlyEdit, '<', -1); } void KNewAccountDlg::slotAdjustMinBalanceEarlyEdit(const QString&) { Q_D(KNewAccountDlg); d->adjustEditWidgets(d->ui->m_minBalanceEarlyEdit, d->ui->m_minBalanceAbsoluteEdit, '>', -1); } void KNewAccountDlg::slotAdjustMaxCreditAbsoluteEdit(const QString&) { Q_D(KNewAccountDlg); d->adjustEditWidgets(d->ui->m_maxCreditAbsoluteEdit, d->ui->m_maxCreditEarlyEdit, '>', 1); } void KNewAccountDlg::slotAdjustMaxCreditEarlyEdit(const QString&) { Q_D(KNewAccountDlg); d->adjustEditWidgets(d->ui->m_maxCreditEarlyEdit, d->ui->m_maxCreditAbsoluteEdit, '<', 1); } void KNewAccountDlg::slotCheckCurrency(int index) { Q_D(KNewAccountDlg); Q_UNUSED(index) d->handleOpeningBalanceCheckbox(d->ui->m_currency->security().id()); } void KNewAccountDlg::addTab(QWidget* w, const QString& name) { Q_D(KNewAccountDlg); if (w) { w->setParent(d->ui->m_tab); d->ui->m_tab->addTab(w, name); } } void KNewAccountDlg::newCategory(MyMoneyAccount& account, const MyMoneyAccount& parent) { if (KMessageBox::questionYesNo(nullptr, QString::fromLatin1("%1").arg(i18n("

The category %1 currently does not exist. Do you want to create it?

The parent account will default to %2 but can be changed in the following dialog.

", account.name(), parent.name())), i18n("Create category"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "CreateNewCategories") == KMessageBox::Yes) { KNewAccountDlg::createCategory(account, parent); } else { // we should not keep the 'no' setting because that can confuse people like // I have seen in some usability tests. So we just delete it right away. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); if (kconfig) { kconfig->group(QLatin1String("Notification Messages")).deleteEntry(QLatin1String("CreateNewCategories")); } } } void KNewAccountDlg::createCategory(MyMoneyAccount& account, const MyMoneyAccount& parent) { if (!parent.id().isEmpty()) { try { // make sure parent account exists MyMoneyFile::instance()->account(parent.id()); account.setParentAccountId(parent.id()); account.setAccountType(parent.accountType()); } catch (const MyMoneyException &) { } } QPointer dialog = new KNewAccountDlg(account, false, true, 0, i18n("Create a new Category")); dialog->setOpeningBalanceShown(false); dialog->setOpeningDateShown(false); if (dialog->exec() == QDialog::Accepted && dialog != 0) { MyMoneyAccount parentAccount, brokerageAccount; account = dialog->account(); parentAccount = dialog->parentAccount(); MyMoneyFile::instance()->createAccount(account, parentAccount, brokerageAccount, MyMoneyMoney()); } delete dialog; } diff --git a/kmymoney/dialogs/knewbankdlg.cpp b/kmymoney/dialogs/knewbankdlg.cpp index 71b3d0d18..b5bee9070 100644 --- a/kmymoney/dialogs/knewbankdlg.cpp +++ b/kmymoney/dialogs/knewbankdlg.cpp @@ -1,260 +1,262 @@ /* * Copyright 2000-2002 Michael Edwardes * Copyright 2017 Łukasz Wojniłowicz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "knewbankdlg.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "ui_knewbankdlg.h" #include "mymoneyinstitution.h" #include "kmymoneyutils.h" #include "icons.h" #include class KNewBankDlgPrivate { Q_DISABLE_COPY(KNewBankDlgPrivate) public: KNewBankDlgPrivate() : ui(new Ui::KNewBankDlg) { m_iconLoadTimer.setSingleShot(true); } ~KNewBankDlgPrivate() { delete ui; } Ui::KNewBankDlg* ui; MyMoneyInstitution m_institution; QTimer m_iconLoadTimer; QPointer m_favIconJob; QIcon m_favIcon; QString m_iconName; QUrl m_url; }; KNewBankDlg::KNewBankDlg(MyMoneyInstitution& institution, QWidget *parent) : QDialog(parent), d_ptr(new KNewBankDlgPrivate) { Q_D(KNewBankDlg); d->ui->setupUi(this); d->m_institution = institution; setModal(true); d->ui->nameEdit->setFocus(); d->ui->nameEdit->setText(institution.name()); d->ui->cityEdit->setText(institution.city()); d->ui->streetEdit->setText(institution.street()); d->ui->postcodeEdit->setText(institution.postcode()); d->ui->telephoneEdit->setText(institution.telephone()); d->ui->sortCodeEdit->setText(institution.sortcode()); d->ui->bicEdit->setText(institution.value(QStringLiteral("bic"))); d->ui->urlEdit->setText(institution.value(QStringLiteral("url"))); if (!institution.value(QStringLiteral("icon")).isEmpty()) { d->m_favIcon = Icons::loadIconFromApplicationCache(institution.value(QStringLiteral("icon"))); } if (!d->m_favIcon.isNull()) { d->ui->iconButton->setEnabled(true); d->ui->iconButton->setIcon(d->m_favIcon); } d->ui->messageWidget->hide(); connect(d->ui->buttonBox, &QDialogButtonBox::accepted, this, &KNewBankDlg::okClicked); connect(d->ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); connect(d->ui->nameEdit, &QLineEdit::textChanged, this, &KNewBankDlg::institutionNameChanged); connect(d->ui->urlEdit, &QLineEdit::textChanged, this, &KNewBankDlg::slotUrlChanged); connect(&d->m_iconLoadTimer, &QTimer::timeout, this, &KNewBankDlg::slotLoadIcon); connect(d->ui->iconButton, &QToolButton::pressed, this, [=] { QUrl url; url.setUrl(QString::fromLatin1("https://%1/").arg(d->ui->urlEdit->text())); QDesktopServices::openUrl(url); }); institutionNameChanged(d->ui->nameEdit->text()); slotUrlChanged(d->ui->urlEdit->text()); auto requiredFields = new KMandatoryFieldGroup(this); requiredFields->setOkButton(d->ui->buttonBox->button(QDialogButtonBox::Ok)); // button to be enabled when all fields present requiredFields->add(d->ui->nameEdit); } void KNewBankDlg::institutionNameChanged(const QString &_text) { Q_D(KNewBankDlg); d->ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!_text.isEmpty()); } KNewBankDlg::~KNewBankDlg() { Q_D(KNewBankDlg); delete d; } void KNewBankDlg::okClicked() { Q_D(KNewBankDlg); if (d->ui->nameEdit->text().isEmpty()) { KMessageBox::information(this, i18n("The institution name field is empty. Please enter the name."), i18n("Adding New Institution")); d->ui->nameEdit->setFocus(); return; } d->m_institution.setName(d->ui->nameEdit->text()); d->m_institution.setTown(d->ui->cityEdit->text()); d->m_institution.setStreet(d->ui->streetEdit->text()); d->m_institution.setPostcode(d->ui->postcodeEdit->text()); d->m_institution.setTelephone(d->ui->telephoneEdit->text()); d->m_institution.setSortcode(d->ui->sortCodeEdit->text()); d->m_institution.setValue(QStringLiteral("bic"), d->ui->bicEdit->text()); d->m_institution.setValue(QStringLiteral("url"), d->ui->urlEdit->text()); d->m_institution.deletePair(QStringLiteral("icon")); if (d->ui->iconButton->isEnabled()) { d->m_institution.setValue(QStringLiteral("icon"), d->m_iconName); Icons::storeIconInApplicationCache(d->m_iconName, d->m_favIcon); } accept(); } const MyMoneyInstitution& KNewBankDlg::institution() { Q_D(KNewBankDlg); return d->m_institution; } void KNewBankDlg::newInstitution(MyMoneyInstitution& institution) { institution.clearId(); QPointer dlg = new KNewBankDlg(institution); if (dlg->exec() == QDialog::Accepted && dlg != 0) { institution = dlg->institution(); KMyMoneyUtils::newInstitution(institution); } delete dlg; } void KNewBankDlg::slotUrlChanged(const QString& newUrl) { Q_D(KNewBankDlg); // remove a possible leading protocol since we only provide https for now QRegularExpression protocol(QStringLiteral("^[a-zA-Z]+://(?.*)"), QRegularExpression::CaseInsensitiveOption); QRegularExpressionMatch matcher = protocol.match(newUrl); if (matcher.hasMatch()) { d->ui->urlEdit->setText(matcher.captured(QStringLiteral("url"))); d->ui->messageWidget->setText(QLatin1String("The protocol part has been removed by KMyMoney because it is fixed to https.")); d->ui->messageWidget->setMessageType(KMessageWidget::Information); d->ui->messageWidget->animatedShow(); } d->m_iconLoadTimer.start(200); } void KNewBankDlg::slotLoadIcon() { Q_D(KNewBankDlg); // if currently a check is running, retry later if (d->m_favIconJob) { d->m_iconLoadTimer.start(200); return; } const auto path = d->ui->urlEdit->text(); QRegularExpression urlRe(QStringLiteral("^(.*\\.)?[^\\.]{2,}\\.[a-z]{2,}"), QRegularExpression::CaseInsensitiveOption); QRegularExpressionMatch matcher = urlRe.match(path); d->ui->iconButton->setEnabled(false); if (matcher.hasMatch()) { d->ui->iconButton->setEnabled(true); d->m_url = QUrl(QString::fromLatin1("https://%1").arg(path)); KIO::Scheduler::checkSlaveOnHold(true); d->m_favIconJob = new KIO::FavIconRequestJob(d->m_url); connect(d->m_favIconJob, &KIO::FavIconRequestJob::result, this, &KNewBankDlg::slotIconLoaded); // we force to end the job after 1 second to avoid blocking this mechanism in case the thing fails QTimer::singleShot(1000, this, &KNewBankDlg::killIconLoad); } } void KNewBankDlg::killIconLoad() { Q_D(KNewBankDlg); if (d->m_favIconJob) { d->m_favIconJob->kill(); d->m_favIconJob->deleteLater(); } } void KNewBankDlg::slotIconLoaded(KJob* job) { Q_D(KNewBankDlg); switch(job->error()) { case ECONNREFUSED: // There is an answer from the server, but no favicon. In case we // already have one, we keep it d->ui->iconButton->setEnabled(true); d->m_favIcon = Icons::get(Icons::Icon::ViewBank); d->m_iconName = QStringLiteral("enum:ViewBank"); break; case 0: // There is an answer from the server, and the favicon is found d->ui->iconButton->setEnabled(true); d->m_favIcon = QIcon(dynamic_cast(job)->iconFile()); d->m_iconName = QStringLiteral("favicon:%1").arg(d->m_url.host()); break; default: // There is problem with the URL from qDebug() << "KIO::FavIconRequestJob error" << job->error(); + // intentional fall through + case EALREADY: // invalid URL, no server response d->ui->iconButton->setEnabled(false); d->m_favIcon = QIcon(); d->m_iconName.clear(); break; } d->ui->iconButton->setIcon(d->m_favIcon); } diff --git a/kmymoney/kmymoney.cpp b/kmymoney/kmymoney.cpp index a2b1493f3..18fb31692 100644 --- a/kmymoney/kmymoney.cpp +++ b/kmymoney/kmymoney.cpp @@ -1,3764 +1,3763 @@ /*************************************************************************** kmymoney.cpp ------------------- copyright : (C) 2000 by Michael Edwardes (C) 2007 by 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 #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 #ifdef ENABLE_HOLIDAYS #include #include #endif #ifdef ENABLE_ACTIVITIES #include #endif // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneysettings.h" #include "kmymoneyadaptor.h" #include "dialogs/settings/ksettingskmymoney.h" #include "dialogs/kbackupdlg.h" #include "dialogs/kconfirmmanualenterdlg.h" #include "dialogs/kmymoneypricedlg.h" #include "dialogs/kcurrencyeditdlg.h" #include "dialogs/kequitypriceupdatedlg.h" #include "dialogs/kmymoneyfileinfodlg.h" #include "dialogs/knewbankdlg.h" #include "dialogs/ksaveasquestion.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/kloadtemplatedlg.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/amountedit.h" #include "widgets/kmymoneyedit.h" #include "widgets/kmymoneymvccombo.h" #include "views/kmymoneyview.h" #include "models/models.h" #include "models/accountsmodel.h" #include "models/equitiesmodel.h" #include "models/securitiesmodel.h" #ifdef ENABLE_UNFINISHEDFEATURES #include "models/ledgermodel.h" #endif #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 "mymoneyexception.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 "kmymoneyplugin.h" #include "tasks/credittransfer.h" #include "icons/icons.h" #include "misc/webconnect.h" #include "storage/mymoneystoragemgr.h" #include "imymoneystorageformat.h" #include "transactioneditor.h" #include #include #include "kmymoneyutils.h" #include "kcreditswindow.h" #include "ledgerdelegate.h" #include "storageenums.h" #include "mymoneyenums.h" #include "dialogenums.h" #include "viewenums.h" #include "menuenums.h" #include "kmymoneyenums.h" #include "misc/platformtools.h" #ifdef ENABLE_SQLCIPHER #include "sqlcipher/sqlite3.h" #endif #ifdef KMM_DEBUG #include "mymoney/storage/mymoneystoragedump.h" #include "mymoneytracer.h" #endif using namespace Icons; using namespace eMenu; enum backupStateE { BACKUP_IDLE = 0, BACKUP_MOUNTING, BACKUP_COPYING, BACKUP_UNMOUNTING }; class KMyMoneyApp::Private { public: explicit Private(KMyMoneyApp *app) : q(app), m_backupState(backupStateE::BACKUP_IDLE), m_backupResult(0), m_backupMount(0), m_ignoreBackupExitCode(false), m_myMoneyView(nullptr), m_startDialog(false), m_progressBar(nullptr), m_statusLabel(nullptr), m_autoSaveEnabled(true), m_autoSaveTimer(nullptr), m_progressTimer(nullptr), m_autoSavePeriod(0), m_inAutoSaving(false), m_recentFiles(nullptr), #ifdef ENABLE_HOLIDAYS m_holidayRegion(nullptr), #endif #ifdef ENABLE_ACTIVITIES m_activityResourceInstance(nullptr), #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 unlinkStatementXML(); void moveInvestmentTransaction(const QString& fromId, const QString& toId, const MyMoneyTransaction& t); QList > automaticReconciliation(const MyMoneyAccount &account, const QList > &transactions, const MyMoneyMoney &amount); struct storageInfo { eKMyMoney::StorageType type {eKMyMoney::StorageType::None}; bool isOpened {false}; QUrl url; }; storageInfo m_storageInfo; /** * The public interface. */ KMyMoneyApp * const q; /** the configuration object of the application */ KSharedConfigPtr m_config; /** * 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; KProcess m_proc; /// A pointer to the view holding the tabs. KMyMoneyView *m_myMoneyView; 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; // This is Auto Saving related bool m_autoSaveEnabled; QTimer* m_autoSaveTimer; QTimer* m_progressTimer; int m_autoSavePeriod; bool m_inAutoSaving; // id's that need to be remembered QString m_accountGoto, m_payeeGoto; KRecentFilesAction* m_recentFiles; #ifdef ENABLE_HOLIDAYS // used by the calendar interface for schedules KHolidays::HolidayRegion* m_holidayRegion; #endif #ifdef ENABLE_ACTIVITIES KActivities::ResourceInstance * m_activityResourceInstance; #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", 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 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; case 4: fixFile_4(); s->setFileFixVersion(5); break; // add new levels above. Don't forget to increase currentFixVersion() for all // the storage backends this fix applies to default: throw MYMONEYEXCEPTION(QString::fromLatin1("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() { const auto file = MyMoneyFile::instance(); const auto accountsModel = Models::instance()->accountsModel(); q->connect(file, &MyMoneyFile::objectAdded, accountsModel, &AccountsModel::slotObjectAdded); q->connect(file, &MyMoneyFile::objectModified, accountsModel, &AccountsModel::slotObjectModified); q->connect(file, &MyMoneyFile::objectRemoved, accountsModel, &AccountsModel::slotObjectRemoved); q->connect(file, &MyMoneyFile::balanceChanged, accountsModel, &AccountsModel::slotBalanceOrValueChanged); q->connect(file, &MyMoneyFile::valueChanged, accountsModel, &AccountsModel::slotBalanceOrValueChanged); const auto institutionsModel = Models::instance()->institutionsModel(); q->connect(file, &MyMoneyFile::objectAdded, institutionsModel, &InstitutionsModel::slotObjectAdded); q->connect(file, &MyMoneyFile::objectModified, institutionsModel, &InstitutionsModel::slotObjectModified); q->connect(file, &MyMoneyFile::objectRemoved, institutionsModel, &InstitutionsModel::slotObjectRemoved); q->connect(file, &MyMoneyFile::balanceChanged, institutionsModel, &AccountsModel::slotBalanceOrValueChanged); q->connect(file, &MyMoneyFile::valueChanged, institutionsModel, &AccountsModel::slotBalanceOrValueChanged); const auto equitiesModel = Models::instance()->equitiesModel(); q->connect(file, &MyMoneyFile::objectAdded, equitiesModel, &EquitiesModel::slotObjectAdded); q->connect(file, &MyMoneyFile::objectModified, equitiesModel, &EquitiesModel::slotObjectModified); q->connect(file, &MyMoneyFile::objectRemoved, equitiesModel, &EquitiesModel::slotObjectRemoved); q->connect(file, &MyMoneyFile::balanceChanged, equitiesModel, &EquitiesModel::slotBalanceOrValueChanged); q->connect(file, &MyMoneyFile::valueChanged, equitiesModel, &EquitiesModel::slotBalanceOrValueChanged); const auto securitiesModel = Models::instance()->securitiesModel(); q->connect(file, &MyMoneyFile::objectAdded, securitiesModel, &SecuritiesModel::slotObjectAdded); q->connect(file, &MyMoneyFile::objectModified, securitiesModel, &SecuritiesModel::slotObjectModified); q->connect(file, &MyMoneyFile::objectRemoved, securitiesModel, &SecuritiesModel::slotObjectRemoved); #ifdef ENABLE_UNFINISHEDFEATURES const auto ledgerModel = Models::instance()->ledgerModel(); q->connect(file, &MyMoneyFile::objectAdded, ledgerModel, &LedgerModel::slotAddTransaction); q->connect(file, &MyMoneyFile::objectModified, ledgerModel, &LedgerModel::slotModifyTransaction); q->connect(file, &MyMoneyFile::objectRemoved, ledgerModel, &LedgerModel::slotRemoveTransaction); q->connect(file, &MyMoneyFile::objectAdded, ledgerModel, &LedgerModel::slotAddSchedule); q->connect(file, &MyMoneyFile::objectModified, ledgerModel, &LedgerModel::slotModifySchedule); q->connect(file, &MyMoneyFile::objectRemoved, ledgerModel, &LedgerModel::slotRemoveSchedule); #endif } void disconnectStorageFromModels() { const auto file = MyMoneyFile::instance(); q->disconnect(file, nullptr, Models::instance()->accountsModel(), nullptr); q->disconnect(file, nullptr, Models::instance()->institutionsModel(), nullptr); q->disconnect(file, nullptr, Models::instance()->equitiesModel(), nullptr); q->disconnect(file, nullptr, Models::instance()->securitiesModel(), nullptr); #ifdef ENABLE_UNFINISHEDFEATURES q->disconnect(file, nullptr, Models::instance()->ledgerModel(), nullptr); #endif } bool askAboutSaving() { const auto isFileNotSaved = q->actionCollection()->action(QString::fromLatin1(KStandardAction::name(KStandardAction::Save)))->isEnabled(); const auto isNewFileNotSaved = m_storageInfo.isOpened && m_storageInfo.url.isEmpty(); auto fileNeedsToBeSaved = false; if (isFileNotSaved && KMyMoneySettings::autoSaveOnClose()) { fileNeedsToBeSaved = true; } else if (isFileNotSaved || isNewFileNotSaved) { switch (KMessageBox::warningYesNoCancel(q, i18n("The file has been changed, save it?"))) { case KMessageBox::ButtonCode::Yes: fileNeedsToBeSaved = true; break; case KMessageBox::ButtonCode::No: fileNeedsToBeSaved = false; break; case KMessageBox::ButtonCode::Cancel: default: return false; break; } } if (fileNeedsToBeSaved) { if (isFileNotSaved) return q->slotFileSave(); else if (isNewFileNotSaved) return q->slotFileSaveAs(); } 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", 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", 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()), e.what()); } } } } } /** * 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_storageInfo.isOpened) 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_4() { auto file = MyMoneyFile::instance(); QList currencies = file->currencyList(); static const QStringList symbols = { QStringLiteral("XAU"), QStringLiteral("XPD"), QStringLiteral("XPT"), QStringLiteral("XAG") }; foreach(auto currency, currencies) { if (symbols.contains(currency.id())) { if (currency.smallestAccountFraction() != currency.smallestCashFraction()) { currency.setSmallestAccountFraction(currency.smallestCashFraction()); file->modifyCurrency(currency); } } } } 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; try { bool updated = false; // 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", 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_CSTRING("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())); } /** * This method is used to update the caption of the application window. * It sets the caption to "filename [modified] - KMyMoney". * * @param skipActions if true, the actions will not be updated. This * is usually onyl required by some early calls when * these widgets are not yet created (the default is false). */ void updateCaption(); void updateActions(); bool canFileSaveAs() const; bool canUpdateAllAccounts() const; void fileAction(eKMyMoney::FileAction action); }; 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"); #ifdef ENABLE_SQLCIPHER /* Issues: * 1) libsqlite3 loads implicitly before libsqlcipher * thus making the second one loaded but non-functional, * 2) libsqlite3 gets linked into kmymoney target implicitly * and it's not possible to unload or unlink it explicitly * * Solution: * Use e.g. dummy sqlite3_key call, so that libsqlcipher gets loaded implicitly before libsqlite3 * thus making the first one functional. * * Additional info: * 1) loading libsqlcipher explicitly doesn't solve the issue, * 2) using sqlite3_key only in sqlstorage plugin doesn't solve the issue, * 3) in a separate, minimal test case, loading libsqlite3 explicitly * with QLibrary::ExportExternalSymbolsHint makes libsqlcipher non-functional */ sqlite3_key(nullptr, nullptr, 0); #endif // 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()); 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); { const auto customIconRelativePath = QString(QStringLiteral("icons/hicolor/16x16/actions/account-add.png")); #ifndef IS_APPIMAGE // find where our custom icons were installed based on an custom icon that we know should exist after installation auto customIconAbsolutePath = QStandardPaths::locate(QStandardPaths::AppDataLocation, customIconRelativePath); if (customIconAbsolutePath.isEmpty()) { qWarning("Custom icons were not found in any of the following QStandardPaths::AppDataLocation:"); for (const auto &standardPath : QStandardPaths::standardLocations(QStandardPaths::AppDataLocation)) qWarning() << standardPath; } #else // according to https://docs.appimage.org/packaging-guide/ingredients.html#open-source-applications // QStandardPaths::AppDataLocation is unreliable on AppImages, so apply workaround here in case we fail to find icons QString customIconAbsolutePath; const auto appImageAppDataLocation = QString("%1%2%3").arg(QCoreApplication::applicationDirPath(), QString("/../share/kmymoney/"), customIconRelativePath); if (QFile::exists(appImageAppDataLocation )) { customIconAbsolutePath = appImageAppDataLocation ; } else { qWarning("Custom icons were not found in the following location:"); qWarning() << appImageAppDataLocation ; } #endif // add our custom icons path to icons search path if (!customIconAbsolutePath.isEmpty()) { customIconAbsolutePath.chop(customIconRelativePath.length()); customIconAbsolutePath.append(QLatin1String("icons")); auto paths = QIcon::themeSearchPaths(); paths.append(customIconAbsolutePath); QIcon::setThemeSearchPaths(paths); } #if defined(Q_OS_WIN) || defined(Q_OS_MACOS) 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->m_myMoneyView = new KMyMoneyView; layout->addWidget(d->m_myMoneyView, 10); connect(d->m_myMoneyView, &KMyMoneyView::statusMsg, this, &KMyMoneyApp::slotStatusMsg); connect(d->m_myMoneyView, &KMyMoneyView::statusProgress, this, &KMyMoneyApp::slotStatusProgressBar); // Initialize kactivities resource instance #ifdef ENABLE_ACTIVITIES d->m_activityResourceInstance = new KActivities::ResourceInstance(window()->winId(), this); #endif const auto viewActions = d->m_myMoneyView->actionsToBeConnected(); actionCollection()->addActions(viewActions.values()); for (auto it = viewActions.cbegin(); it != viewActions.cend(); ++it) pActions.insert(it.key(), it.value()); /////////////////////////////////////////////////////////////////// // call inits to invoke all other construction parts readOptions(); // now initialize the plugin structure createInterfaces(); KMyMoneyPlugin::pluginHandling(KMyMoneyPlugin::Action::Load, pPlugins, this, guiFactory()); onlineJobAdministration::instance()->setOnlinePlugins(pPlugins.extended); d->m_myMoneyView->setOnlinePlugins(pPlugins.online); 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; for (auto const& weekDay: locale.weekdays()) { d->m_processingDays.setBit(static_cast(weekDay)); } 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())); // connect the WebConnect server connect(d->m_webConnect, &WebConnect::gotUrl, this, &KMyMoneyApp::webConnectUrl); // setup the initial configuration slotUpdateConfiguration(QString()); // kickstart date change timer slotDateChanged(); d->fileAction(eKMyMoney::FileAction::Closed); } KMyMoneyApp::~KMyMoneyApp() { // 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, pPlugins, this, guiFactory()); d->removeStorage(); #ifdef ENABLE_HOLIDAYS delete d->m_holidayRegion; #endif #ifdef ENABLE_ACTIVITIES delete d->m_activityResourceInstance; #endif // make sure all settings are written to disk KMyMoneySettings::self()->save(); delete d; } QUrl KMyMoneyApp::lastOpenedURL() { QUrl url = d->m_startDialog ? QUrl() : d->m_storageInfo.url; if (!url.isValid()) { url = QUrl::fromUserInput(readLastUsedFile()); } ready(); return url; } 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(); /* Look-up table for all custom and standard 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 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); lutActions.insert(Action::Print, KStandardAction::print(this, &KMyMoneyApp::slotPrintView, aC)); KStandardAction::preferences(this, &KMyMoneyApp::slotSettings, aC); // ************* // 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 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(KMyMoneySettings::showAllAccounts()); // ************* // 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); } #ifdef KMM_DEBUG void KMyMoneyApp::resizeEvent(QResizeEvent* ev) { KMainWindow::resizeEvent(ev); d->updateCaption(); } #endif bool KMyMoneyApp::queryClose() { if (!isReady()) return false; if (!slotFileClose()) return false; 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(); } bool KMyMoneyApp::isDatabase() { return (d->m_storageInfo.isOpened && ((d->m_storageInfo.type == eKMyMoney::StorageType::SQL))); } bool KMyMoneyApp::isNativeFile() { return (d->m_storageInfo.isOpened && (d->m_storageInfo.type == eKMyMoney::StorageType::SQL || d->m_storageInfo.type == eKMyMoney::StorageType::XML)); } bool KMyMoneyApp::fileOpen() const { return d->m_storageInfo.isOpened; } KMyMoneyAppCallback KMyMoneyApp::progressCallback() { return &KMyMoneyApp::progressCallback; } void KMyMoneyApp::consistencyCheck(bool alwaysDisplayResult) { d->consistencyCheck(alwaysDisplayResult); } 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 = pPlugins.importer.constBegin(); while (it_plugin != pPlugins.importer.constEnd()) { if ((*it_plugin)->isMyFormat(url.toLocalFile())) { 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 == pPlugins.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::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_storageInfo.isOpened) { 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_storageInfo.isOpened) { 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", QString::fromLatin1(e.what()))); } } delete editPersonalDataDlg; } void KMyMoneyApp::slotLoadAccountTemplates() { KMSTATUS(i18n("Importing account templates.")); QPointer dlg = new KLoadTemplateDlg(); if (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(this, i18n("Unable to import template(s)"), QString::fromLatin1(e.what())); } } delete dlg; } void KMyMoneyApp::slotSaveAccountTemplates() { KMSTATUS(i18n("Exporting account templates.")); QString savePath = QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/templates/" + QLocale().name(); QDir templatesDir(savePath); if (!templatesDir.exists()) templatesDir.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, pPlugins, this, guiFactory()); actionCollection()->action(QString::fromLatin1(KStandardAction::name(KStandardAction::SaveAs)))->setEnabled(d->canFileSaveAs()); onlineJobAdministration::instance()->updateActions(); onlineJobAdministration::instance()->setOnlinePlugins(pPlugins.extended); d->m_myMoneyView->setOnlinePlugins(pPlugins.online); d->updateActions(); d->m_myMoneyView->slotRefreshViews(); 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(); } 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_storageInfo.url.isEmpty()) return; if (!d->m_storageInfo.url.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_storageInfo.url.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_storageInfo.url.fileName()); QString today = QDate::currentDate().toString("-yyyy-MM-dd.") + fi.suffix(); QString backupfile = d->m_mountpoint + '/' + d->m_storageInfo.url.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_storageInfo.url.toLocalFile()) << "+" << "nul" << QDir::toNativeSeparators(backupfile); #else d->m_proc << "cp" << "-f"; d->m_proc << d->m_storageInfo.url.toLocalFile() << backupfile; #endif d->m_backupState = BACKUP_COPYING; qDebug() << "Backup cmd:" << d->m_proc.program(); 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::slotViewSelected(View view) { KMyMoneySettings::setLastViewSelected(static_cast(view)); } 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::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", QString::fromLatin1(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_CSTRING("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", QString::fromLatin1(e.what()))); } } catch (const MyMoneyException &e) { KMessageBox::information(this, i18n("Unable to add scheduled transaction: %1", QString::fromLatin1(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; q->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; } q->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++; } q->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) { q->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); } } } q->slotStatusProgressBar(NR_OF_STEPS_LIMIT / PROGRESSBAR_STEPS, 0); if (sumToComponentsMap.contains(amount)) { QListIterator > itTransactionSplit2(transactions); while (itTransactionSplit2.hasNext()) { const QPair &transactionSplit = itTransactionSplit2.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 q->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(), QString::fromLatin1(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(), QString::fromLatin1(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::Private::updateCaption() { auto caption = m_storageInfo.url.isEmpty() && m_myMoneyView && m_storageInfo.isOpened ? i18n("Untitled") : m_storageInfo.url.fileName(); #ifdef KMM_DEBUG caption += QString(" (%1 x %2)").arg(q->width()).arg(q->height()); #endif q->setCaption(caption, MyMoneyFile::instance()->dirty()); } void KMyMoneyApp::Private::updateActions() { const QVector actions { Action::FilePersonalData, Action::FileInformation, Action::FileImportTemplate, Action::FileExportTemplate, #ifdef KMM_DEBUG Action::FileDump, #endif Action::EditFindTransaction, Action::NewCategory, Action::ToolCurrencies, Action::ToolPrices, Action::ToolUpdatePrices, Action::ToolConsistency, Action::ToolPerformance, Action::NewAccount, Action::NewInstitution, Action::NewSchedule }; for (const auto &action : actions) pActions[action]->setEnabled(m_storageInfo.isOpened); pActions[Action::FileBackup]->setEnabled(m_storageInfo.isOpened && m_storageInfo.type == eKMyMoney::StorageType::XML); auto aC = q->actionCollection(); aC->action(QString::fromLatin1(KStandardAction::name(KStandardAction::SaveAs)))->setEnabled(canFileSaveAs()); aC->action(QString::fromLatin1(KStandardAction::name(KStandardAction::Close)))->setEnabled(m_storageInfo.isOpened); pActions[eMenu::Action::UpdateAllAccounts]->setEnabled(KMyMoneyUtils::canUpdateAllAccounts()); } bool KMyMoneyApp::Private::canFileSaveAs() const { return (m_storageInfo.isOpened && (!pPlugins.storage.isEmpty() && !(pPlugins.storage.count() == 1 && pPlugins.storage.first()->storageType() == eKMyMoney::StorageType::GNC))); } void KMyMoneyApp::slotDataChanged() { d->fileAction(eKMyMoney::FileAction::Changed); } 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); } 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 erroneous 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/"); QStringList defaultCSSDirs; #ifndef IS_APPIMAGE defaultCSSDirs = QStandardPaths::locateAll(QStandardPaths::AppDataLocation, rcDir, QStandardPaths::LocateDirectory); #else // according to https://docs.appimage.org/packaging-guide/ingredients.html#open-source-applications // QStandardPaths::AppDataLocation is unreliable on AppImages, so apply workaround here in case we fail to find icons // watch out for QStringBuilder here; for yet unknown reason it causes segmentation fault on startup const auto appImageAppDataLocation = QString("%1%2%3").arg(QCoreApplication::applicationDirPath(), QString("/../share/kmymoney"), rcDir); if (QFile::exists(appImageAppDataLocation + CSSnames.first())) { defaultCSSDirs.append(appImageAppDataLocation); } else { qWarning("CSS file was not found in the following location:"); qWarning() << appImageAppDataLocation; } #endif // 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; } } } } 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_storageInfo.url.url(); } QUrl KMyMoneyApp::filenameURL() const { return d->m_storageInfo.url; } void KMyMoneyApp::writeFilenameURL(const QUrl &url) { d->m_storageInfo.url = url; } 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 somewhere 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::webConnectUrl(const QUrl url) { QMetaObject::invokeMethod(this, "webConnect", Qt::QueuedConnection, Q_ARG(QString, url.toLocalFile()), Q_ARG(QByteArray, QByteArray())); } 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) { MyMoneyStatementReader::clearResultMessages(); auto statementCount = 0; while (!d->m_importUrlsQueue.isEmpty()) { ++statementCount; // 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. //KStartupInfo::setNewStartupId(this, asn_id); // Make sure we have an open file if (! d->m_storageInfo.isOpened && KMessageBox::warningContinueCancel(this, i18n("You must first select a KMyMoney file before you can import a statement.")) == KMessageBox::Continue) slotFileOpen(); // only continue if the user really did open a file. if (d->m_storageInfo.isOpened) { KMSTATUS(i18n("Importing a statement via Web Connect")); // remove the statement files d->unlinkStatementXML(); QMap::const_iterator it_plugin = pPlugins.importer.constBegin(); while (it_plugin != pPlugins.importer.constEnd()) { if ((*it_plugin)->isMyFormat(url)) { - QList statements; if (!(*it_plugin)->import(url) && !(*it_plugin)->lastError().isEmpty()) { 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 == pPlugins.importer.constEnd()) if (MyMoneyStatement::isStatementFile(url)) MyMoneyStatementReader::importStatement(url, false, progressCallback); } // remove the current processed item from the queue d->m_importUrlsQueue.dequeue(); } KMyMoneyUtils::showStatementImportResult(MyMoneyStatementReader::resultMessages(), statementCount); } } 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((static_cast(dt.secsTo(nextDay)) + 1)*1000, this, SLOT(slotDateChanged())); d->m_myMoneyView->slotRefreshViews(); } void KMyMoneyApp::setHolidayRegion(const QString& holidayRegion) { #ifdef ENABLE_HOLIDAYS //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 { if (!d->m_processingDays.testBit(date.dayOfWeek())) return false; #ifdef ENABLE_HOLIDAYS 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 return true; #endif } void KMyMoneyApp::preloadHolidays() { #ifdef ENABLE_HOLIDAYS //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 auto 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 } bool KMyMoneyApp::slotFileNew() { KMSTATUS(i18n("Creating new document...")); if (!slotFileClose()) return false; NewUserWizard::Wizard wizard; if (wizard.exec() != QDialog::Accepted) return false; d->m_storageInfo.isOpened = true; d->m_storageInfo.type = eKMyMoney::StorageType::None; d->m_storageInfo.url = QUrl(); try { auto storage = new MyMoneyStorageMgr; MyMoneyFile::instance()->attachStorage(storage); MyMoneyFileTransaction ft; auto file = MyMoneyFile::instance(); // 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 for (auto &tmpl : wizard.templates()) tmpl.importTemplate(progressCallback); ft.commit(); KMyMoneySettings::setFirstTimeRun(false); d->fileAction(eKMyMoney::FileAction::Opened); if (actionCollection()->action(QString::fromLatin1(KStandardAction::name(KStandardAction::SaveAs)))->isEnabled()) slotFileSaveAs(); } catch (const MyMoneyException & e) { slotFileClose(); d->removeStorage(); KMessageBox::detailedError(this, i18n("Couldn't create a new file."), e.what()); return false; } if (wizard.startSettingsAfterFinished()) slotSettings(); return true; } void KMyMoneyApp::slotFileOpen() { KMSTATUS(i18n("Open a file.")); const QVector desiredFileExtensions {eKMyMoney::StorageType::XML, eKMyMoney::StorageType::GNC}; QString fileExtensions; for (const auto &extension : desiredFileExtensions) { for (const auto &plugin : pPlugins.storage) { if (plugin->storageType() == extension) { fileExtensions += plugin->fileExtension() + QLatin1String(";;"); break; } } } if (fileExtensions.isEmpty()) { KMessageBox::error(this, i18n("Couldn't find any plugin for opening storage.")); return; } fileExtensions.append(i18n("All files (*)")); QPointer dialog = new QFileDialog(this, QString(), readLastUsedDir(), fileExtensions); dialog->setFileMode(QFileDialog::ExistingFile); dialog->setAcceptMode(QFileDialog::AcceptOpen); if (dialog->exec() == QDialog::Accepted && dialog != nullptr) slotFileOpenRecent(dialog->selectedUrls().first()); delete dialog; } bool KMyMoneyApp::slotFileOpenRecent(const QUrl &url) { KMSTATUS(i18n("Loading file...")); if (!url.isValid()) throw MYMONEYEXCEPTION(QString::fromLatin1("Invalid URL %1").arg(qPrintable(url.url()))); 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 false; } 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 false; } if (d->m_storageInfo.isOpened) if (!slotFileClose()) return false; // open the database d->m_storageInfo.type = eKMyMoney::StorageType::None; for (auto &plugin : pPlugins.storage) { try { if (auto pStorage = plugin->open(url)) { MyMoneyFile::instance()->attachStorage(pStorage); d->m_storageInfo.type = plugin->storageType(); if (plugin->storageType() != eKMyMoney::StorageType::GNC) { d->m_storageInfo.url = plugin->openUrl(); 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); } d->m_storageInfo.isOpened = true; break; } } catch (const MyMoneyException &e) { KMessageBox::detailedError(this, i18n("Cannot open file as requested."), QString::fromLatin1(e.what())); return false; } } if(d->m_storageInfo.type == eKMyMoney::StorageType::None) { KMessageBox::error(this, i18n("Could not read your data source. Please check the KMyMoney settings that the necessary plugin is enabled.")); return false; } d->fileAction(eKMyMoney::FileAction::Opened); return true; } bool KMyMoneyApp::slotFileSave() { KMSTATUS(i18n("Saving file...")); for (const auto& plugin : pPlugins.storage) { if (plugin->storageType() == d->m_storageInfo.type) { d->consistencyCheck(false); try { if (plugin->save(d->m_storageInfo.url)) { d->fileAction(eKMyMoney::FileAction::Saved); return true; } return false; } catch (const MyMoneyException &e) { KMessageBox::detailedError(this, i18n("Failed to save your storage."), e.what()); return false; } } } KMessageBox::error(this, i18n("Couldn't find suitable plugin to save your storage.")); return false; } bool KMyMoneyApp::slotFileSaveAs() { KMSTATUS(i18n("Saving file as....")); QVector availableFileTypes; for (const auto& plugin : pPlugins.storage) { switch (plugin->storageType()) { case eKMyMoney::StorageType::GNC: break; default: availableFileTypes.append(plugin->storageType()); break; } } auto chosenFileType = eKMyMoney::StorageType::None; switch (availableFileTypes.count()) { case 0: KMessageBox::error(this, i18n("Couldn't find any plugin for saving storage.")); return false; case 1: chosenFileType = availableFileTypes.first(); break; default: { QPointer dlg = new KSaveAsQuestion(availableFileTypes, this); auto rc = dlg->exec(); if (dlg) { auto fileType = dlg->fileType(); delete dlg; if (rc != QDialog::Accepted) return false; chosenFileType = fileType; } } } for (const auto &plugin : pPlugins.storage) { if (chosenFileType == plugin->storageType()) { try { d->consistencyCheck(false); if (plugin->saveAs()) { d->fileAction(eKMyMoney::FileAction::Saved); d->m_storageInfo.type = plugin->storageType(); return true; } } catch (const MyMoneyException &e) { KMessageBox::detailedError(this, i18n("Failed to save your storage."), e.what()); } } } return false; } bool KMyMoneyApp::slotFileClose() { if (!d->m_storageInfo.isOpened) return true; if (!d->askAboutSaving()) return false; d->fileAction(eKMyMoney::FileAction::Closing); d->removeStorage(); d->m_storageInfo = KMyMoneyApp::Private::storageInfo(); d->fileAction(eKMyMoney::FileAction::Closed); return true; } 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::Private::fileAction(eKMyMoney::FileAction action) { switch(action) { case eKMyMoney::FileAction::Opened: q->actionCollection()->action(QString::fromLatin1(KStandardAction::name(KStandardAction::Save)))->setEnabled(false); updateAccountNames(); updateCurrencyNames(); selectBaseCurrency(); // setup the standard precision AmountEdit::setStandardPrecision(MyMoneyMoney::denomToPrec(MyMoneyFile::instance()->baseCurrency().smallestAccountFraction())); KMyMoneyEdit::setStandardPrecision(MyMoneyMoney::denomToPrec(MyMoneyFile::instance()->baseCurrency().smallestAccountFraction())); applyFileFixes(); Models::instance()->fileOpened(); connectStorageToModels(); // inform everyone about new data MyMoneyFile::instance()->forceDataChanged(); // Enable save in case the fix changed the contents q->actionCollection()->action(QString::fromLatin1(KStandardAction::name(KStandardAction::Save)))->setEnabled(dirty()); updateActions(); m_myMoneyView->slotFileOpened(); onlineJobAdministration::instance()->updateActions(); m_myMoneyView->enableViewsIfFileOpen(m_storageInfo.isOpened); m_myMoneyView->slotRefreshViews(); onlineJobAdministration::instance()->updateOnlineTaskProperties(); q->connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KMyMoneyApp::slotDataChanged); #ifdef ENABLE_ACTIVITIES { // make sure that we don't store the DB password in activity QUrl url(m_storageInfo.url); url.setPassword(QString()); m_activityResourceInstance->setUri(url); } #endif // start the check for scheduled transactions that need to be // entered as soon as the event loop becomes active. QMetaObject::invokeMethod(q, "slotCheckSchedules", Qt::QueuedConnection); // make sure to catch view activations connect(m_myMoneyView, &KMyMoneyView::viewActivated, q, &KMyMoneyApp::slotViewSelected); break; case eKMyMoney::FileAction::Saved: q->connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KMyMoneyApp::slotDataChanged); q->actionCollection()->action(QString::fromLatin1(KStandardAction::name(KStandardAction::Save)))->setEnabled(false); m_autoSaveTimer->stop(); break; case eKMyMoney::FileAction::Closing: disconnect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KMyMoneyApp::slotDataChanged); // make sure to not catch view activations anymore disconnect(m_myMoneyView, &KMyMoneyView::viewActivated, q, &KMyMoneyApp::slotViewSelected); m_myMoneyView->slotFileClosed(); // 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(); break; case eKMyMoney::FileAction::Closed: q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KMyMoneyApp::slotDataChanged); disconnectStorageFromModels(); q->actionCollection()->action(QString::fromLatin1(KStandardAction::name(KStandardAction::Save)))->setEnabled(false); m_myMoneyView->enableViewsIfFileOpen(m_storageInfo.isOpened); updateActions(); break; case eKMyMoney::FileAction::Changed: q->disconnect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KMyMoneyApp::slotDataChanged); q->actionCollection()->action(QString::fromLatin1(KStandardAction::name(KStandardAction::Save)))->setEnabled(true && !m_storageInfo.url.isEmpty()); // 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 (m_autoSaveEnabled && !m_autoSaveTimer->isActive()) { m_autoSaveTimer->setSingleShot(true); m_autoSaveTimer->start(m_autoSavePeriod * 60 * 1000); //miliseconds } pActions[eMenu::Action::UpdateAllAccounts]->setEnabled(KMyMoneyUtils::canUpdateAllAccounts()); break; default: break; } updateCaption(); } 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])); } } diff --git a/kmymoney/kmymoneyutils.cpp b/kmymoney/kmymoneyutils.cpp index 1646ed9db..a69844a06 100644 --- a/kmymoney/kmymoneyutils.cpp +++ b/kmymoney/kmymoneyutils.cpp @@ -1,827 +1,829 @@ /*************************************************************************** kmymoneyutils.cpp - description ------------------- begin : Wed Feb 5 2003 copyright : (C) 2000-2003 by Michael Edwardes email : mte@users.sourceforge.net Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio (C) 2017 by Łukasz Wojniłowicz ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "kmymoneyutils.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Headers #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneymoney.h" #include "mymoneyexception.h" #include "mymoneytransactionfilter.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneyschedule.h" #include "mymoneypayee.h" #include "mymoneytag.h" #include "mymoneyprice.h" #include "mymoneystatement.h" #include "mymoneyforecast.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "kmymoneysettings.h" #include "icons.h" #include "storageenums.h" #include "mymoneyenums.h" #include "kmymoneyplugin.h" using namespace Icons; KMyMoneyUtils::KMyMoneyUtils() { } KMyMoneyUtils::~KMyMoneyUtils() { } const QString KMyMoneyUtils::occurrenceToString(const eMyMoney::Schedule::Occurrence occurrence) { return i18nc("Frequency of schedule", MyMoneySchedule::occurrenceToString(occurrence).toLatin1()); } const QString KMyMoneyUtils::paymentMethodToString(eMyMoney::Schedule::PaymentType paymentType) { return i18nc("Scheduled Transaction payment type", MyMoneySchedule::paymentMethodToString(paymentType).toLatin1()); } const QString KMyMoneyUtils::weekendOptionToString(eMyMoney::Schedule::WeekendOption weekendOption) { return i18n(MyMoneySchedule::weekendOptionToString(weekendOption).toLatin1()); } const QString KMyMoneyUtils::scheduleTypeToString(eMyMoney::Schedule::Type type) { return i18nc("Scheduled transaction type", MyMoneySchedule::scheduleTypeToString(type).toLatin1()); } KGuiItem KMyMoneyUtils::scheduleNewGuiItem() { KGuiItem splitGuiItem(i18n("&New Schedule..."), Icons::get(Icon::DocumentNew), i18n("Create a new schedule."), i18n("Use this to create a new schedule.")); return splitGuiItem; } KGuiItem KMyMoneyUtils::accountsFilterGuiItem() { KGuiItem splitGuiItem(i18n("&Filter"), Icons::get(Icon::ViewFilter), i18n("Filter out accounts"), i18n("Use this to filter out accounts")); return splitGuiItem; } const char* homePageItems[] = { I18N_NOOP("Payments"), I18N_NOOP("Preferred accounts"), I18N_NOOP("Payment accounts"), I18N_NOOP("Favorite reports"), I18N_NOOP("Forecast (schedule)"), I18N_NOOP("Net worth forecast"), I18N_NOOP("Forecast (history)"), I18N_NOOP("Assets and Liabilities"), I18N_NOOP("Budget"), I18N_NOOP("CashFlow"), // insert new items above this comment 0 }; const QString KMyMoneyUtils::homePageItemToString(const int idx) { QString rc; if (abs(idx) > 0 && abs(idx) < static_cast(sizeof(homePageItems) / sizeof(homePageItems[0]))) { rc = i18n(homePageItems[abs(idx-1)]); } return rc; } int KMyMoneyUtils::stringToHomePageItem(const QString& txt) { int idx = 0; for (idx = 0; homePageItems[idx] != 0; ++idx) { if (txt == i18n(homePageItems[idx])) return idx + 1; } return 0; } bool KMyMoneyUtils::appendCorrectFileExt(QString& str, const QString& strExtToUse) { bool rc = false; if (!str.isEmpty()) { //find last . deliminator int nLoc = str.lastIndexOf('.'); if (nLoc != -1) { QString strExt, strTemp; strTemp = str.left(nLoc + 1); strExt = str.right(str.length() - (nLoc + 1)); if (strExt.indexOf(strExtToUse, 0, Qt::CaseInsensitive) == -1) { // if the extension given contains a period, we remove ours if (strExtToUse.indexOf('.') != -1) strTemp = strTemp.left(strTemp.length() - 1); //append extension to make complete file name strTemp.append(strExtToUse); str = strTemp; rc = true; } } else { str.append(QLatin1Char('.')); str.append(strExtToUse); rc = true; } } return rc; } void KMyMoneyUtils::checkConstants() { // TODO: port to kf5 #if 0 Q_ASSERT(static_cast(KLocale::ParensAround) == static_cast(MyMoneyMoney::ParensAround)); Q_ASSERT(static_cast(KLocale::BeforeQuantityMoney) == static_cast(MyMoneyMoney::BeforeQuantityMoney)); Q_ASSERT(static_cast(KLocale::AfterQuantityMoney) == static_cast(MyMoneyMoney::AfterQuantityMoney)); Q_ASSERT(static_cast(KLocale::BeforeMoney) == static_cast(MyMoneyMoney::BeforeMoney)); Q_ASSERT(static_cast(KLocale::AfterMoney) == static_cast(MyMoneyMoney::AfterMoney)); #endif } QString KMyMoneyUtils::variableCSS() { QColor tcolor = KColorScheme(QPalette::Active).foreground(KColorScheme::NormalText).color(); QColor link = KColorScheme(QPalette::Active).foreground(KColorScheme::LinkText).color(); QString css; css += "\n"; return css; } QString KMyMoneyUtils::findResource(QStandardPaths::StandardLocation type, const QString& filename) { QLocale locale; QString country; QString localeName = locale.bcp47Name(); QString language = localeName; // extract language and country from the bcp47name QRegularExpression regExp(QLatin1String("(\\w+)_(\\w+)")); QRegularExpressionMatch match = regExp.match(localeName); if (match.hasMatch()) { language = match.captured(1); country = match.captured(2); } QString rc; // check that the placeholder is present and set things up if (filename.indexOf("%1") != -1) { /// @fixme somehow I have the impression, that language and country /// mappings to the filename are not correct. This certainly must /// be overhauled at some point in time (ipwizard, 2017-10-22) QString mask = filename.arg("_%1.%2"); - rc = QStandardPaths::locate(type, mask.arg(country).arg(language)); + rc = QStandardPaths::locate(type, mask.arg(country, language)); // search the given resource if (rc.isEmpty()) { mask = filename.arg("_%1"); rc = QStandardPaths::locate(type, mask.arg(language)); } if (rc.isEmpty()) { // qDebug(QString("html/home_%1.html not found").arg(country).toLatin1()); rc = QStandardPaths::locate(type, mask.arg(country)); } if (rc.isEmpty()) { rc = QStandardPaths::locate(type, filename.arg("")); } } else { rc = QStandardPaths::locate(type, filename); } if (rc.isEmpty()) { qWarning("No resource found for (%s,%s)", qPrintable(QStandardPaths::displayName(type)), qPrintable(filename)); } return rc; } const MyMoneySplit KMyMoneyUtils::stockSplit(const MyMoneyTransaction& t) { MyMoneySplit investmentAccountSplit; foreach (const auto split, t.splits()) { if (!split.accountId().isEmpty()) { auto acc = MyMoneyFile::instance()->account(split.accountId()); if (acc.isInvest()) { return split; } // if we have a reference to an investment account, we remember it here if (acc.accountType() == eMyMoney::Account::Type::Investment) investmentAccountSplit = split; } } // if we haven't found a stock split, we see if we've seen // an investment account on the way. If so, we return it. if (!investmentAccountSplit.id().isEmpty()) return investmentAccountSplit; // if none was found, we return an empty split. return MyMoneySplit(); } KMyMoneyUtils::transactionTypeE KMyMoneyUtils::transactionType(const MyMoneyTransaction& t) { if (!stockSplit(t).id().isEmpty()) return InvestmentTransaction; if (t.splitCount() < 2) { return Unknown; } else if (t.splitCount() > 2) { // FIXME check for loan transaction here return SplitTransaction; } QString ida, idb; - if (t.splits().size() > 0) - ida = t.splits()[0].accountId(); - if (t.splits().size() > 1) - idb = t.splits()[1].accountId(); + const auto & splits = t.splits(); + if (splits.size() > 0) + ida = splits[0].accountId(); + if (splits.size() > 1) + idb = splits[1].accountId(); if (ida.isEmpty() || idb.isEmpty()) return Unknown; MyMoneyAccount a, b; a = MyMoneyFile::instance()->account(ida); b = MyMoneyFile::instance()->account(idb); if ((a.accountGroup() == eMyMoney::Account::Type::Asset || a.accountGroup() == eMyMoney::Account::Type::Liability) && (b.accountGroup() == eMyMoney::Account::Type::Asset || b.accountGroup() == eMyMoney::Account::Type::Liability)) return Transfer; return Normal; } void KMyMoneyUtils::calculateAutoLoan(const MyMoneySchedule& schedule, MyMoneyTransaction& transaction, const QMap& balances) { try { MyMoneyForecast::calculateAutoLoan(schedule, transaction, balances); } catch (const MyMoneyException &e) { KMessageBox::detailedError(0, i18n("Unable to load schedule details"), QString::fromLatin1(e.what())); } } QString KMyMoneyUtils::nextCheckNumber(const MyMoneyAccount& acc) { return getAdjacentNumber(acc.value("lastNumberUsed"), 1); } QString KMyMoneyUtils::nextFreeCheckNumber(const MyMoneyAccount& acc) { auto file = MyMoneyFile::instance(); auto num = acc.value("lastNumberUsed"); if (num.isEmpty()) num = QStringLiteral("1"); // now check if this number has been used already if (file->checkNoUsed(acc.id(), num)) { // if a number has been entered which is immediately prior to // an existing number, the next new number produced would clash // so need to look ahead for free next number // we limit that to a number of tries which depends on the // number of splits in that account (we can't have more) MyMoneyTransactionFilter filter(acc.id()); QList transactions; file->transactionList(transactions, filter); const int maxNumber = transactions.count(); for (int i = 0; i < maxNumber; i++) { if (file->checkNoUsed(acc.id(), num)) { // increment and try again num = getAdjacentNumber(num); } else { // found a free number break; } } } return num; } QString KMyMoneyUtils::getAdjacentNumber(const QString& number, int offset) { // make sure the offset is either -1 or 1 offset = (offset >= 0) ? 1 : -1; QString num = number; // +-#1--+ +#2++-#3-++-#4--+ QRegExp exp(QString("(.*\\D)?(0*)(\\d+)(\\D.*)?")); if (exp.indexIn(num) != -1) { QString arg1 = exp.cap(1); QString arg2 = exp.cap(2); QString arg3 = QString::number(exp.cap(3).toULong() + offset); QString arg4 = exp.cap(4); - num = QString("%1%2%3%4").arg(arg1).arg(arg2).arg(arg3).arg(arg4); + num = QString("%1%2%3%4").arg(arg1, arg2, arg3, arg4); } else { num = QStringLiteral("1"); } return num; } quint64 KMyMoneyUtils::numericPart(const QString & num) { quint64 num64 = 0; QRegExp exp(QString("(.*\\D)?(0*)(\\d+)(\\D.*)?")); if (exp.indexIn(num) != -1) { - QString arg1 = exp.cap(1); + // QString arg1 = exp.cap(1); QString arg2 = exp.cap(2); QString arg3 = QString::number(exp.cap(3).toULongLong()); - QString arg4 = exp.cap(4); - num64 = QString("%2%3").arg(arg2).arg(arg3).toULongLong(); + // QString arg4 = exp.cap(4); + num64 = QString("%2%3").arg(arg2, arg3).toULongLong(); } return num64; } QString KMyMoneyUtils::reconcileStateToString(eMyMoney::Split::State flag, bool text) { QString txt; if (text) { switch (flag) { case eMyMoney::Split::State::NotReconciled: txt = i18nc("Reconciliation state 'Not reconciled'", "Not reconciled"); break; case eMyMoney::Split::State::Cleared: txt = i18nc("Reconciliation state 'Cleared'", "Cleared"); break; case eMyMoney::Split::State::Reconciled: txt = i18nc("Reconciliation state 'Reconciled'", "Reconciled"); break; case eMyMoney::Split::State::Frozen: txt = i18nc("Reconciliation state 'Frozen'", "Frozen"); break; default: txt = i18nc("Unknown reconciliation state", "Unknown"); break; } } else { switch (flag) { case eMyMoney::Split::State::NotReconciled: break; case eMyMoney::Split::State::Cleared: txt = i18nc("Reconciliation flag C", "C"); break; case eMyMoney::Split::State::Reconciled: txt = i18nc("Reconciliation flag R", "R"); break; case eMyMoney::Split::State::Frozen: txt = i18nc("Reconciliation flag F", "F"); break; default: txt = i18nc("Flag for unknown reconciliation state", "?"); break; } } return txt; } MyMoneyTransaction KMyMoneyUtils::scheduledTransaction(const MyMoneySchedule& schedule) { MyMoneyTransaction t = schedule.transaction(); try { if (schedule.type() == eMyMoney::Schedule::Type::LoanPayment) { calculateAutoLoan(schedule, t, QMap()); } } catch (const MyMoneyException &e) { qDebug("Unable to load schedule details for '%s' during transaction match: %s", qPrintable(schedule.name()), e.what()); } t.clearId(); t.setEntryDate(QDate()); return t; } KXmlGuiWindow* KMyMoneyUtils::mainWindow() { foreach (QWidget *widget, QApplication::topLevelWidgets()) { KXmlGuiWindow* result = dynamic_cast(widget); if (result) return result; } return 0; } void KMyMoneyUtils::updateWizardButtons(QWizard* wizard) { // setup text on buttons wizard->setButtonText(QWizard::NextButton, i18nc("Go to next page of the wizard", "&Next")); wizard->setButtonText(QWizard::BackButton, KStandardGuiItem::back().text()); // setup icons wizard->button(QWizard::FinishButton)->setIcon(KStandardGuiItem::ok().icon()); wizard->button(QWizard::CancelButton)->setIcon(KStandardGuiItem::cancel().icon()); wizard->button(QWizard::NextButton)->setIcon(KStandardGuiItem::forward(KStandardGuiItem::UseRTL).icon()); wizard->button(QWizard::BackButton)->setIcon(KStandardGuiItem::back(KStandardGuiItem::UseRTL).icon()); } void KMyMoneyUtils::dissectTransaction(const MyMoneyTransaction& transaction, const MyMoneySplit& split, MyMoneySplit& assetAccountSplit, QList& feeSplits, QList& interestSplits, MyMoneySecurity& security, MyMoneySecurity& currency, eMyMoney::Split::InvestmentTransactionType& transactionType) { // collect the splits. split references the stock account and should already // be set up. assetAccountSplit references the corresponding asset account (maybe // empty), feeSplits is the list of all expenses and interestSplits // the list of all incomes assetAccountSplit = MyMoneySplit(); // set to none to check later if it was assigned auto file = MyMoneyFile::instance(); foreach (const auto tsplit, transaction.splits()) { auto acc = file->account(tsplit.accountId()); if (tsplit.id() == split.id()) { security = file->security(acc.currencyId()); } else if (acc.accountGroup() == eMyMoney::Account::Type::Expense) { feeSplits.append(tsplit); // feeAmount += tsplit.value(); } else if (acc.accountGroup() == eMyMoney::Account::Type::Income) { interestSplits.append(tsplit); // interestAmount += tsplit.value(); } else { if (assetAccountSplit == MyMoneySplit()) // first asset Account should be our requested brokerage account assetAccountSplit = tsplit; else if (tsplit.value().isNegative()) // the rest (if present) is handled as fee or interest feeSplits.append(tsplit); // and shouldn't be allowed to override assetAccountSplit else if (tsplit.value().isPositive()) interestSplits.append(tsplit); } } // determine transaction type if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::AddShares)) { transactionType = (!split.shares().isNegative()) ? eMyMoney::Split::InvestmentTransactionType::AddShares : eMyMoney::Split::InvestmentTransactionType::RemoveShares; } else if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::BuyShares)) { transactionType = (!split.value().isNegative()) ? eMyMoney::Split::InvestmentTransactionType::BuyShares : eMyMoney::Split::InvestmentTransactionType::SellShares; } else if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Dividend)) { transactionType = eMyMoney::Split::InvestmentTransactionType::Dividend; } else if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::ReinvestDividend)) { transactionType = eMyMoney::Split::InvestmentTransactionType::ReinvestDividend; } else if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::Yield)) { transactionType = eMyMoney::Split::InvestmentTransactionType::Yield; } else if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::SplitShares)) { transactionType = eMyMoney::Split::InvestmentTransactionType::SplitShares; } else if (split.action() == MyMoneySplit::actionName(eMyMoney::Split::Action::InterestIncome)) { transactionType = eMyMoney::Split::InvestmentTransactionType::InterestIncome; } else transactionType = eMyMoney::Split::InvestmentTransactionType::BuyShares; currency.setTradingSymbol("???"); try { currency = file->security(transaction.commodity()); } catch (const MyMoneyException &) { } } void KMyMoneyUtils::processPriceList(const MyMoneyStatement &st) { auto file = MyMoneyFile::instance(); QHash secBySymbol; QHash secByName; - for (const auto& sec : file->securityList()) { + const auto securityList = file->securityList(); + for (const auto& sec : securityList) { secBySymbol[sec.tradingSymbol()] = sec; secByName[sec.name()] = sec; } for (const auto& stPrice : st.m_listPrices) { auto currency = file->baseCurrency().id(); QString security; if (!stPrice.m_strCurrency.isEmpty()) { security = stPrice.m_strSecurity; currency = stPrice.m_strCurrency; } else if (secBySymbol.contains(stPrice.m_strSecurity)) { security = secBySymbol[stPrice.m_strSecurity].id(); currency = file->security(file->security(security).tradingCurrency()).id(); } else if (secByName.contains(stPrice.m_strSecurity)) { security = secByName[stPrice.m_strSecurity].id(); currency = file->security(file->security(security).tradingCurrency()).id(); } else return; MyMoneyPrice price(security, currency, stPrice.m_date, stPrice.m_amount, stPrice.m_sourceName.isEmpty() ? i18n("Prices Importer") : stPrice.m_sourceName); file->addPrice(price); } } void KMyMoneyUtils::deleteSecurity(const MyMoneySecurity& security, QWidget* parent) { QString msg, msg2; QString dontAsk, dontAsk2; if (security.isCurrency()) { msg = i18n("

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

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

All exchange rates for currency %1 will be lost.

Do you still want to continue?

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

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

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

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

Do you still want to continue?

", MyMoneySecurity::securityTypeToString(security.securityType()), security.name()); dontAsk = "DeleteSecurity"; dontAsk2 = "DeleteSecurityPrices"; } if (KMessageBox::questionYesNo(parent, msg, i18n("Delete security"), KStandardGuiItem::yes(), KStandardGuiItem::no(), dontAsk) == KMessageBox::Yes) { MyMoneyFileTransaction ft; auto file = MyMoneyFile::instance(); QBitArray skip((int)eStorage::Reference::Count); skip.fill(true); skip.clearBit((int)eStorage::Reference::Price); if (file->isReferenced(security, skip)) { if (KMessageBox::questionYesNo(parent, msg2, i18n("Delete prices"), KStandardGuiItem::yes(), KStandardGuiItem::no(), dontAsk2) == KMessageBox::Yes) { try { QString secID = security.id(); foreach (auto priceEntry, file->priceList()) { const MyMoneyPrice& price = priceEntry.first(); if (price.from() == secID || price.to() == secID) file->removePrice(price); } ft.commit(); ft.restart(); } catch (const MyMoneyException &) { qDebug("Cannot delete price"); return; } } else return; } try { if (security.isCurrency()) file->removeCurrency(security); else file->removeSecurity(security); ft.commit(); } catch (const MyMoneyException &) { } } } bool KMyMoneyUtils::fileExists(const QUrl &url) { bool fileExists = false; if (url.isValid()) { short int detailLevel = 0; // Lowest level: file/dir/symlink/none KIO::StatJob* statjob = KIO::stat(url, KIO::StatJob::SourceSide, detailLevel); bool noerror = statjob->exec(); if (noerror) { // We want a file fileExists = !statjob->statResult().isDir(); } statjob->kill(); } return fileExists; } QString KMyMoneyUtils::downloadFile(const QUrl &url) { QString filename; KIO::StoredTransferJob *transferjob = KIO::storedGet (url); // KJobWidgets::setWindow(transferjob, this); if (! transferjob->exec()) { KMessageBox::detailedError(nullptr, i18n("Error while loading file '%1'.", url.url()), transferjob->errorString(), i18n("File access error")); return filename; } QTemporaryFile file; file.setAutoRemove(false); file.open(); file.write(transferjob->data()); filename = file.fileName(); file.close(); return filename; } bool KMyMoneyUtils::newPayee(const QString& newnameBase, QString& id) { bool doit = true; if (newnameBase != i18n("New Payee")) { // Ask the user if that is what he intended to do? const auto msg = i18n("Do you want to add %1 as payer/receiver?", newnameBase); if (KMessageBox::questionYesNo(nullptr, msg, i18n("New payee/receiver"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "NewPayee") == KMessageBox::No) { doit = false; // we should not keep the 'no' setting because that can confuse people like // I have seen in some usability tests. So we just delete it right away. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); if (kconfig) { kconfig->group(QLatin1String("Notification Messages")).deleteEntry(QLatin1String("NewPayee")); } } } if (doit) { MyMoneyFileTransaction ft; try { QString newname(newnameBase); // adjust name until a unique name has been created int count = 0; for (;;) { try { MyMoneyFile::instance()->payeeByName(newname); newname = QString::fromLatin1("%1 [%2]").arg(newnameBase).arg(++count); } catch (const MyMoneyException &) { break; } } MyMoneyPayee p; p.setName(newname); MyMoneyFile::instance()->addPayee(p); id = p.id(); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(nullptr, i18n("Unable to add payee"), QString::fromLatin1(e.what())); doit = false; } } return doit; } void KMyMoneyUtils::newTag(const QString& newnameBase, QString& id) { bool doit = true; if (newnameBase != i18n("New Tag")) { // Ask the user if that is what he intended to do? const auto msg = i18n("Do you want to add %1 as tag?", newnameBase); if (KMessageBox::questionYesNo(nullptr, msg, i18n("New tag"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "NewTag") == KMessageBox::No) { doit = false; // we should not keep the 'no' setting because that can confuse people like // I have seen in some usability tests. So we just delete it right away. KSharedConfigPtr kconfig = KSharedConfig::openConfig(); if (kconfig) { kconfig->group(QLatin1String("Notification Messages")).deleteEntry(QLatin1String("NewTag")); } } } if (doit) { MyMoneyFileTransaction ft; try { QString newname(newnameBase); // adjust name until a unique name has been created int count = 0; for (;;) { try { MyMoneyFile::instance()->tagByName(newname); newname = QString::fromLatin1("%1 [%2]").arg(newnameBase, ++count); } catch (const MyMoneyException &) { break; } } MyMoneyTag ta; ta.setName(newname); MyMoneyFile::instance()->addTag(ta); id = ta.id(); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(nullptr, i18n("Unable to add tag"), QString::fromLatin1(e.what())); } } } void KMyMoneyUtils::newInstitution(MyMoneyInstitution& institution) { auto file = MyMoneyFile::instance(); MyMoneyFileTransaction ft; try { file->addInstitution(institution); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::information(nullptr, i18n("Cannot add institution: %1", QString::fromLatin1(e.what()))); } } QDebug KMyMoneyUtils::debug() { return qDebug() << QDateTime::currentDateTime().toString(QStringLiteral("HH:mm:ss.zzz")); } MyMoneyForecast KMyMoneyUtils::forecast() { MyMoneyForecast forecast; // override object defaults with those of the application forecast.setForecastCycles(KMyMoneySettings::forecastCycles()); forecast.setAccountsCycle(KMyMoneySettings::forecastAccountCycle()); forecast.setHistoryStartDate(QDate::currentDate().addDays(-forecast.forecastCycles()*forecast.accountsCycle())); forecast.setHistoryEndDate(QDate::currentDate().addDays(-1)); forecast.setForecastDays(KMyMoneySettings::forecastDays()); forecast.setBeginForecastDay(KMyMoneySettings::beginForecastDay()); forecast.setForecastMethod(KMyMoneySettings::forecastMethod()); forecast.setHistoryMethod(KMyMoneySettings::historyMethod()); forecast.setIncludeFutureTransactions(KMyMoneySettings::includeFutureTransactions()); forecast.setIncludeScheduledTransactions(KMyMoneySettings::includeScheduledTransactions()); return forecast; } bool KMyMoneyUtils::canUpdateAllAccounts() { const auto file = MyMoneyFile::instance(); auto rc = false; if (!file->storageAttached()) return rc; QList accList; file->accountList(accList); QList::const_iterator it_a; auto it_p = pPlugins.online.constEnd(); for (it_a = accList.constBegin(); (it_p == pPlugins.online.constEnd()) && (it_a != accList.constEnd()); ++it_a) { if ((*it_a).hasOnlineMapping()) { // check if provider is available it_p = pPlugins.online.constFind((*it_a).onlineBankingSettings().value("provider").toLower()); if (it_p != pPlugins.online.constEnd()) { QStringList protocols; (*it_p)->protocols(protocols); if (!protocols.isEmpty()) { rc = true; break; } } } } return rc; } void KMyMoneyUtils::showStatementImportResult(const QStringList& resultMessages, uint statementCount) { KMessageBox::informationList(nullptr, i18np("One statement has been processed with the following results:", "%1 statements have been processed with the following results:", statementCount), !resultMessages.isEmpty() ? resultMessages : QStringList { i18np("No new transaction has been imported.", "No new transactions have been imported.", statementCount) }, i18n("Statement import statistics")); } diff --git a/kmymoney/main.cpp b/kmymoney/main.cpp index 1ed97bcea..0452ecf2b 100644 --- a/kmymoney/main.cpp +++ b/kmymoney/main.cpp @@ -1,386 +1,386 @@ /*************************************************************************** main.cpp ------------------- copyright : (C) 2001 by Michael Edwardes email : mte@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 #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #ifdef KMM_DBUS #include #include #include #endif // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoney/mymoneyfile.h" #include "mymoneyexception.h" #include "kmymoney.h" #include "kstartuplogo.h" #include "kcreditswindow.h" #include "kmymoneyutils.h" #include "kmymoneysettings.h" #include "misc/webconnect.h" #include "platformtools.h" #ifdef KMM_DEBUG #include "mymoneyutils.h" #include "mymoneytracer.h" #endif bool timersOn = false; KMyMoneyApp* kmymoney; static int runKMyMoney(QApplication& a, std::unique_ptr splash, const QUrl & file, bool noFile); static void migrateConfigFiles(); int main(int argc, char *argv[]) { /** * Create application first */ QApplication app(argc, argv); KLocalizedString::setApplicationDomain("kmymoney"); migrateConfigFiles(); /** * construct and register about data */ KAboutData aboutData(QStringLiteral("kmymoney"), i18n("KMyMoney"), QStringLiteral(VERSION)); aboutData.setOrganizationDomain("kde.org"); KAboutData::setApplicationData(aboutData); QStringList fileUrls; bool isNoCatchOption = false; bool isNoFileOption = false; #ifdef KMM_DEBUG bool isDumpActionsOption = false; #endif if (argc != 0) { /** * Create command line parser and feed it with known options */ QCommandLineParser parser; aboutData.setupCommandLine(&parser); // language // const QCommandLineOption langOption(QStringLiteral("lang"), i18n("language to be used")); // parser.addOption(langOption); // no file const QCommandLineOption noFileOption(QStringLiteral("n"), i18n("do not open last used file")); parser.addOption(noFileOption); // timers const QCommandLineOption timersOption(QStringLiteral("timers"), i18n("enable performance timers")); parser.addOption(timersOption); // no catch const QCommandLineOption noCatchOption(QStringLiteral("nocatch"), i18n("do not globally catch uncaught exceptions")); parser.addOption(noCatchOption); #ifdef KMM_DEBUG // The following options are only available when compiled in debug mode // trace const QCommandLineOption traceOption(QStringLiteral("trace"), i18n("turn on program traces")); parser.addOption(traceOption); // dump actions const QCommandLineOption dumpActionsOption(QStringLiteral("dump-actions"), i18n("dump the names of all defined QAction objects to stdout and quit")); parser.addOption(dumpActionsOption); #endif // INSERT YOUR COMMANDLINE OPTIONS HERE // url to open parser.addPositionalArgument(QStringLiteral("url"), i18n("file to open")); /** * do the command line parsing */ parser.parse(QApplication::arguments()); bool ishelpSet = parser.isSet(QStringLiteral("help")); if (ishelpSet || parser.isSet(QStringLiteral("author")) || parser.isSet(QStringLiteral("license"))) { aboutData = initializeCreditsData(); if (ishelpSet) parser.showHelp(); } if (parser.isSet(QStringLiteral("version"))) parser.showVersion(); /** * handle standard options */ aboutData.processCommandLine(&parser); #ifdef KMM_DEBUG if (parser.isSet(traceOption)) MyMoneyTracer::on(); timersOn = parser.isSet(timersOption); isDumpActionsOption = parser.isSet(dumpActionsOption); #endif isNoCatchOption = parser.isSet(noCatchOption); isNoFileOption = parser.isSet(noFileOption); fileUrls = parser.positionalArguments(); } // create the singletons before we start memory checking // to avoid false error reports auto file = MyMoneyFile::instance(); Q_UNUSED(file) KMyMoneyUtils::checkConstants(); // show startup logo std::unique_ptr splash(KMyMoneySettings::showSplash() ? createStartupLogo() : nullptr); app.processEvents(); // setup the MyMoneyMoney locale settings according to the KDE settings MyMoneyMoney::setThousandSeparator(QLocale().groupSeparator()); MyMoneyMoney::setDecimalSeparator(QLocale().decimalPoint()); // TODO: port to kf5 (negative numbers in parens) //MyMoneyMoney::setNegativeMonetarySignPosition(static_cast(KLocale::global()->negativeMonetarySignPosition())); //MyMoneyMoney::setPositiveMonetarySignPosition(static_cast(KLocale::global()->positiveMonetarySignPosition())); MyMoneyMoney::setNegativePrefixCurrencySymbol(platformTools::currencySymbolPosition(true) < platformTools::AfterQuantityMoney); MyMoneyMoney::setPositivePrefixCurrencySymbol(platformTools::currencySymbolPosition(false) < platformTools::AfterQuantityMoney); // QString language = parser.value(langOption); // if (!language.isEmpty()) { //if (!KLocale::global()->setLanguage(QStringList() << language)) { // qWarning("Unable to select language '%s'. This has one of two reasons:\n\ta) the standard KDE message catalog is not installed\n\tb) the KMyMoney message catalog is not installed", qPrintable(language)); //} // } kmymoney = new KMyMoneyApp(); #ifdef KMM_DEBUG if (isDumpActionsOption) { kmymoney->dumpActions(); // Before we delete the application, we make sure that we destroy all // widgets by running the event loop for some time to catch all those // widgets that are requested to be destroyed using the deleteLater() method. //QApplication::eventLoop()->processEvents(QEventLoop::ExcludeUserInput, 10); QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 10); delete kmymoney; exit(0); } #endif QString fname; // in case a filename is provided we need to check if it is a local // file. In case the name does not start with "file://" or "./" or "/" // we need to prepend "./" to fake a relative filename. Otherwise, QUrl prepends // "http://" and uses the full path which will not work. // On MS-Windows we also need to check if the filename starts with a // drive letter or the backslash variants. // // The handling might be different on other OSes if (!fileUrls.isEmpty()) { fname = fileUrls.front(); QFileInfo fi(fname); auto needLeadIn = fi.isFile(); #ifdef Q_OS_WIN QRegularExpression exp("^[a-z]:", QRegularExpression::CaseInsensitiveOption); needLeadIn &= !exp.match(fname).hasMatch() && !fname.startsWith(QLatin1String(".\\")) && !fname.startsWith(QLatin1String("\\")); #endif needLeadIn &= !fname.startsWith(QLatin1String("file://")) && !fname.startsWith(QLatin1String("./")) && !fname.startsWith(QLatin1String("/")); if (needLeadIn) { fname.prepend(QLatin1String("./")); } } const QUrl url = QUrl::fromUserInput(fname, QLatin1String("."), QUrl::AssumeLocalFile); int rc = 0; if (isNoCatchOption) { qDebug("Running w/o global try/catch block"); rc = runKMyMoney(app, std::move(splash), url, isNoFileOption); } else { try { rc = runKMyMoney(app, std::move(splash), url, isNoFileOption); } catch (const MyMoneyException &e) { KMessageBox::detailedError(0, i18n("Uncaught error. Please report the details to the developers"), QString::fromLatin1(e.what())); throw; } } return rc; } int runKMyMoney(QApplication& a, std::unique_ptr splash, const QUrl & file, bool noFile) { bool instantQuit = false; /** * enable high dpi icons */ a.setAttribute(Qt::AA_UseHighDpiPixmaps); if (kmymoney->webConnect()->isClient()) { // If the user launches a second copy of the app and includes a file to // open, they are probably attempting a "WebConnect" session. In this case, // we'll check to make sure it's an importable file that's passed in, and if so, we'll // notify the primary instance of the file and kill ourselves. if (file.isValid()) { if (kmymoney->isImportableFile(file)) { instantQuit = true; kmymoney->webConnect()->loadFile(file); } } } kmymoney->centralWidget()->setEnabled(false); // force complete paint of widgets qApp->processEvents(); if (!instantQuit) { QString importfile; QUrl url; // make sure, we take the file provided on the command // line before we go and open the last one used if (file.isValid()) { // Check to see if this is an importable file, as opposed to a loadable // file. If it is importable, what we really want to do is load the // last used file anyway and then immediately import this file. This // implements a "web connect" session where there is not already an // instance of the program running. if (kmymoney->isImportableFile(file)) { importfile = file.path(); url = QUrl::fromUserInput(kmymoney->readLastUsedFile()); } else { url = file; } } else { url = QUrl::fromUserInput(kmymoney->readLastUsedFile()); } if (url.isValid() && !noFile) { if (importfile.isEmpty()) { KTipDialog::showTip(kmymoney, QString(), false); } kmymoney->slotFileOpenRecent(url); } else if (KMyMoneySettings::firstTimeRun()) { // resetting the splash here is needed for ms-windows to have access // to the new file wizard splash.reset(); kmymoney->slotFileNew(); } KMyMoneySettings::setFirstTimeRun(false); if (!importfile.isEmpty()) { // resetting the splash here is needed for ms-windows to have access // to the web connect widgets splash.reset(); kmymoney->webConnect(importfile, QByteArray()); } } else { // the instantQuit flag is set, so we force the app to quit right away kmymoney->slotFileQuit(); } kmymoney->centralWidget()->setEnabled(true); kmymoney->show(); splash.reset(); const int rc = a.exec(); //krazy:exclude=crashy return rc; } static void migrateConfigFiles() { const QString sMainConfigName(QStringLiteral("kmymoneyrc")); const QString sMainConfigSubdirectory(QStringLiteral("kmymoney/")); // all KMM config files should be in ~/.config/kmymoney/ const QString sMainConfigPath = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QLatin1Char('/') + sMainConfigSubdirectory; if (!QFile::exists(sMainConfigPath + sMainConfigName)) { // if main config file doesn't exist, then it's first run // it could be migration from KDE4 to KF5 so prepare list of configuration files to migrate QStringList sConfigNames { sMainConfigName, QStringLiteral("csvimporterrc"), QStringLiteral("printcheckpluginrc"), QStringLiteral("icalendarexportpluginrc"), QStringLiteral("kbankingrc"), }; // Copy KDE 4 config files to the KF5 location Kdelibs4ConfigMigrator migrator(QStringLiteral("kmymoney")); migrator.setConfigFiles(sConfigNames); migrator.setUiFiles(QStringList{QStringLiteral("kmymoneyui.rc")}); migrator.migrate(); QFileInfo fileInfo(sMainConfigPath + sMainConfigName); QDir().mkpath(fileInfo.absolutePath()); const QString sOldMainConfigPath = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QLatin1Char('/'); // some files have changed their names during switch to KF5, so prepare map for name replacements QMap configNamesChange { {QStringLiteral("printcheckpluginrc"), QStringLiteral("checkprintingrc")}, {QStringLiteral("icalendarexportpluginrc"), QStringLiteral("icalendarexporterrc")} }; - for (const auto& sConfigName : sConfigNames) { + for (const auto& sConfigName : qAsConst(sConfigNames)) { const auto sOldConfigFilename = sOldMainConfigPath + sConfigName; const auto sNewConfigFilename = sMainConfigPath + configNamesChange.value(sConfigName, sConfigName); if (QFile::exists(sOldConfigFilename)) { if (QFile::copy(sOldConfigFilename, sNewConfigFilename)) QFile::remove(sOldConfigFilename); } } } KConfig::setMainConfigName(sMainConfigSubdirectory + sMainConfigName); // otherwise it would be ~/.config/kmymoneyrc and not ~/.config/kmymoney/kmymoneyrc } diff --git a/kmymoney/models/accountsmodel.cpp b/kmymoney/models/accountsmodel.cpp index 1b8173d44..f5f58311a 100644 --- a/kmymoney/models/accountsmodel.cpp +++ b/kmymoney/models/accountsmodel.cpp @@ -1,1253 +1,1254 @@ /* * Copyright 2010-2014 Cristian Oneț * Copyright 2017-2018 Łukasz Wojniłowicz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "accountsmodel.h" // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyutils.h" #include "mymoneymoney.h" #include "mymoneyexception.h" #include "mymoneyfile.h" #include "mymoneyinstitution.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneyprice.h" #include "kmymoneysettings.h" #include "icons.h" #include "modelenums.h" #include "mymoneyenums.h" #include "viewenums.h" using namespace Icons; using namespace eAccountsModel; using namespace eMyMoney; class AccountsModelPrivate { Q_DECLARE_PUBLIC(AccountsModel) public: /** * The pimpl. */ AccountsModelPrivate(AccountsModel *qq) : q_ptr(qq), m_file(MyMoneyFile::instance()) { m_columns.append(Column::Account); } virtual ~AccountsModelPrivate() { } void init() { Q_Q(AccountsModel); QStringList headerLabels; - for (const auto& column : m_columns) + for (const auto& column : qAsConst(m_columns)) headerLabels.append(q->getHeaderName(column)); q->setHorizontalHeaderLabels(headerLabels); } void loadPreferredAccount(const MyMoneyAccount &acc, QStandardItem *fromNode /*accounts' regular node*/, const int row, QStandardItem *toNode /*accounts' favourite node*/) { if (acc.value(QStringLiteral("PreferredAccount")) != QLatin1String("Yes")) return; auto favRow = toNode->rowCount(); if (auto favItem = itemFromAccountId(toNode, acc.id())) favRow = favItem->row(); for (auto i = 0; i < fromNode->columnCount(); ++i) { auto itemToClone = fromNode->child(row, i); if (itemToClone) toNode->setChild(favRow, i, itemToClone->clone()); } } /** * Load all the sub-accounts recursively. * * @param model The model in which to load the data. * @param accountsItem The item from the model of the parent account of the sub-accounts which are being loaded. * @param favoriteAccountsItem The item of the favorites accounts groups so favorite accounts can be added here also. * @param list The list of the account id's of the sub-accounts which are being loaded. * */ void loadSubaccounts(QStandardItem *node, QStandardItem *favoriteAccountsItem, const QStringList& subaccounts) { for (const auto& subaccStr : subaccounts) { const auto subacc = m_file->account(subaccStr); auto item = new QStandardItem(subacc.name()); // initialize first column of subaccount node->appendRow(item); // add subaccount row to node item->setEditable(false); item->setData(node->data((int)Role::DisplayOrder), (int)Role::DisplayOrder); // inherit display order role from node loadSubaccounts(item, favoriteAccountsItem, subacc.accountList()); // subaccount may have subaccounts as well // set the account data after the children have been loaded const auto row = item->row(); setAccountData(node, row, subacc, m_columns); // initialize rest of columns of subaccount loadPreferredAccount(subacc, node, row, favoriteAccountsItem); // add to favourites node if preferred } } /** * Note: this functions should only be called after the child account data has been set. */ void setAccountData(QStandardItem *node, const int row, const MyMoneyAccount &account, const QList &columns) { QStandardItem *cell; auto getCell = [&, row](const auto column) { cell = node->child(row, column); // try to get QStandardItem if (!cell) { // it may be uninitialized cell = new QStandardItem; // so create one node->setChild(row, column, cell); // and add it under the node } }; auto colNum = m_columns.indexOf(Column::Account); if (colNum == -1) return; getCell(colNum); auto font = cell->data(Qt::FontRole).value(); // display the names of closed accounts with strikeout font if (account.isClosed() != font.strikeOut()) font.setStrikeOut(account.isClosed()); if (columns.contains(Column::Account)) { // setting account column cell->setData(account.name(), Qt::DisplayRole); // cell->setData(QVariant::fromValue(account), (int)Role::Account); // is set in setAccountBalanceAndValue cell->setData(QVariant(account.id()), (int)Role::ID); cell->setData(QVariant(account.value("PreferredAccount") == QLatin1String("Yes")), (int)Role::Favorite); cell->setData(QVariant(QIcon(account.accountPixmap(m_reconciledAccount.id().isEmpty() ? false : account.id() == m_reconciledAccount.id()))), Qt::DecorationRole); cell->setData(MyMoneyFile::instance()->accountToCategory(account.id(), true), (int)Role::FullName); cell->setData(font, Qt::FontRole); } // Type if (columns.contains(Column::Type)) { colNum = m_columns.indexOf(Column::Type); if (colNum != -1) { getCell(colNum); cell->setData(account.accountTypeToString(account.accountType()), Qt::DisplayRole); cell->setData(font, Qt::FontRole); } } // Account's number if (columns.contains(Column::AccountNumber)) { colNum = m_columns.indexOf(Column::AccountNumber); if (colNum != -1) { getCell(colNum); cell->setData(account.number(), Qt::DisplayRole); cell->setData(font, Qt::FontRole); } } // Account's sort code if (columns.contains(Column::AccountSortCode)) { colNum = m_columns.indexOf(Column::AccountSortCode); if (colNum != -1) { getCell(colNum); cell->setData(account.value("iban"), Qt::DisplayRole); cell->setData(font, Qt::FontRole); } } const auto checkMark = Icons::get(Icon::DialogOK); switch (account.accountType()) { case Account::Type::Income: case Account::Type::Expense: case Account::Type::Asset: case Account::Type::Liability: // Tax if (columns.contains(Column::Tax)) { colNum = m_columns.indexOf(Column::Tax); if (colNum != -1) { getCell(colNum); if (account.value("Tax").toLower() == "yes") cell->setData(checkMark, Qt::DecorationRole); else cell->setData(QIcon(), Qt::DecorationRole); } } // VAT Account if (columns.contains(Column::VAT)) { colNum = m_columns.indexOf(Column::VAT); if (colNum != -1) { getCell(colNum); if (!account.value("VatAccount").isEmpty()) { const auto vatAccount = MyMoneyFile::instance()->account(account.value("VatAccount")); cell->setData(vatAccount.name(), Qt::DisplayRole); cell->setData(QVariant(Qt::AlignLeft | Qt::AlignVCenter), Qt::TextAlignmentRole); // VAT Rate } else if (!account.value("VatRate").isEmpty()) { const auto vatRate = MyMoneyMoney(account.value("VatRate")) * MyMoneyMoney(100, 1); cell->setData(QString::fromLatin1("%1 %").arg(vatRate.formatMoney(QString(), 1)), Qt::DisplayRole); cell->setData(QVariant(Qt::AlignRight | Qt::AlignVCenter), Qt::TextAlignmentRole); } else { cell->setData(QString(), Qt::DisplayRole); } } } // CostCenter if (columns.contains(Column::CostCenter)) { colNum = m_columns.indexOf(Column::CostCenter); if (colNum != -1) { getCell(colNum); if (account.isCostCenterRequired()) cell->setData(checkMark, Qt::DecorationRole); else cell->setData(QIcon(), Qt::DecorationRole); } } break; default: break; } // balance and value setAccountBalanceAndValue(node, row, account, columns); } void setInstitutionTotalValue(QStandardItem *node, const int row) { const auto colInstitution = m_columns.indexOf(Column::Account); auto itInstitution = node->child(row, colInstitution); const auto valInstitution = childrenTotalValue(itInstitution, true); itInstitution->setData(QVariant::fromValue(valInstitution ), (int)Role::TotalValue); const auto colTotalValue = m_columns.indexOf(Column::TotalValue); if (colTotalValue == -1) return; auto cell = node->child(row, colTotalValue); if (!cell) { cell = new QStandardItem; node->setChild(row, colTotalValue, cell); } const auto fontColor = KMyMoneySettings::schemeColor(valInstitution.isNegative() ? SchemeColor::Negative : SchemeColor::Positive); cell->setData(QVariant(fontColor), Qt::ForegroundRole); cell->setData(QVariant(itInstitution->data(Qt::FontRole).value()), Qt::FontRole); cell->setData(QVariant(Qt::AlignRight | Qt::AlignVCenter), Qt::TextAlignmentRole); cell->setData(MyMoneyUtils::formatMoney(valInstitution, m_file->baseCurrency()), Qt::DisplayRole); } void setAccountBalanceAndValue(QStandardItem *node, const int row, const MyMoneyAccount &account, const QList &columns) { QStandardItem *cell; auto getCell = [&, row](auto column) { cell = node->child(row, column); if (!cell) { cell = new QStandardItem; node->setChild(row, column, cell); } }; // setting account column auto colNum = m_columns.indexOf(Column::Account); if (colNum == -1) return; getCell(colNum); MyMoneyMoney accountBalance, accountValue, accountTotalValue; if (columns.contains(Column::Account)) { // update values only when requested accountBalance = balance(account); accountValue = value(account, accountBalance); accountTotalValue = childrenTotalValue(cell) + accountValue; cell->setData(QVariant::fromValue(account), (int)Role::Account); cell->setData(QVariant::fromValue(accountBalance), (int)Role::Balance); cell->setData(QVariant::fromValue(accountValue), (int)Role::Value); cell->setData(QVariant::fromValue(accountTotalValue), (int)Role::TotalValue); } else { // otherwise save up on tedious calculations accountBalance = cell->data((int)Role::Balance).value(); accountValue = cell->data((int)Role::Value).value(); accountTotalValue = cell->data((int)Role::TotalValue).value(); } const auto font = QVariant(cell->data(Qt::FontRole).value()); const auto alignment = QVariant(Qt::AlignRight | Qt::AlignVCenter); // setting total balance column if (columns.contains(Column::TotalBalance)) { colNum = m_columns.indexOf(Column::TotalBalance); if (colNum != -1) { const auto accountBalanceStr = QVariant::fromValue(MyMoneyUtils::formatMoney(accountBalance, m_file->security(account.currencyId()))); getCell(colNum); // only show the balance, if its a different security/currency if (m_file->security(account.currencyId()) != m_file->baseCurrency()) { cell->setData(accountBalanceStr, Qt::DisplayRole); } cell->setData(font, Qt::FontRole); cell->setData(alignment, Qt::TextAlignmentRole); } } // setting posted value column if (columns.contains(Column::PostedValue)) { colNum = m_columns.indexOf(Column::PostedValue); if (colNum != -1) { const auto accountValueStr = QVariant::fromValue(MyMoneyUtils::formatMoney(accountValue, m_file->baseCurrency())); getCell(colNum); const auto fontColor = KMyMoneySettings::schemeColor(accountValue.isNegative() ? SchemeColor::Negative : SchemeColor::Positive); cell->setData(QVariant(fontColor), Qt::ForegroundRole); cell->setData(accountValueStr, Qt::DisplayRole); cell->setData(font, Qt::FontRole); cell->setData(alignment, Qt::TextAlignmentRole); } } // setting total value column if (columns.contains(Column::TotalValue)) { colNum = m_columns.indexOf(Column::TotalValue); if (colNum != -1) { const auto accountTotalValueStr = QVariant::fromValue(MyMoneyUtils::formatMoney(accountTotalValue, m_file->baseCurrency())); getCell(colNum); const auto fontColor = KMyMoneySettings::schemeColor(accountTotalValue.isNegative() ? SchemeColor::Negative : SchemeColor::Positive); cell->setData(accountTotalValueStr, Qt::DisplayRole); cell->setData(font, Qt::FontRole); cell->setData(QVariant(fontColor), Qt::ForegroundRole); cell->setData(alignment, Qt::TextAlignmentRole); } } } /** * Compute the balance of the given account. * * @param account The account for which the balance is being computed. */ MyMoneyMoney balance(const MyMoneyAccount &account) { MyMoneyMoney balance; // a closed account has a zero balance by definition if (!account.isClosed()) { // account.balance() is not compatible with stock accounts if (account.isInvest()) balance = m_file->balance(account.id()); else balance = account.balance(); } // for income and liability accounts, we reverse the sign switch (account.accountGroup()) { case Account::Type::Income: case Account::Type::Liability: case Account::Type::Equity: balance = -balance; break; default: break; } return balance; } /** * Compute the value of the given account using the provided balance. * The value is defined as the balance of the account converted to the base currency. * * @param account The account for which the value is being computed. * @param balance The balance which should be used. * * @see balance */ MyMoneyMoney value(const MyMoneyAccount &account, const MyMoneyMoney &balance) { if (account.isClosed()) return MyMoneyMoney(); QList prices; MyMoneySecurity security = m_file->baseCurrency(); try { if (account.isInvest()) { security = m_file->security(account.currencyId()); prices += m_file->price(account.currencyId(), security.tradingCurrency()); if (security.tradingCurrency() != m_file->baseCurrency().id()) { MyMoneySecurity sec = m_file->security(security.tradingCurrency()); prices += m_file->price(sec.id(), m_file->baseCurrency().id()); } } else if (account.currencyId() != m_file->baseCurrency().id()) { security = m_file->security(account.currencyId()); prices += m_file->price(account.currencyId(), m_file->baseCurrency().id()); } } catch (const MyMoneyException &e) { qDebug() << Q_FUNC_INFO << " caught exception while adding " << account.name() << "[" << account.id() << "]: " << e.what(); } MyMoneyMoney value = balance; { QList::const_iterator it_p; QString securityID = account.currencyId(); for (it_p = prices.constBegin(); it_p != prices.constEnd(); ++it_p) { value = (value * (MyMoneyMoney::ONE / (*it_p).rate(securityID))).convertPrecision(m_file->security(securityID).pricePrecision()); if ((*it_p).from() == securityID) securityID = (*it_p).to(); else securityID = (*it_p).from(); } value = value.convert(m_file->baseCurrency().smallestAccountFraction()); } return value; } /** * Compute the total value of the child accounts of the given account. * Note that the value of the current account is not in this sum. Also, * before calling this function, the caller must make sure that the values * of all sub-account must be already in the model in the @ref Role::Value. * * @param index The index of the account in the model. * @see value */ MyMoneyMoney childrenTotalValue(const QStandardItem *node, const bool isInstitutionsModel = false) { MyMoneyMoney totalValue; if (!node) return totalValue; for (auto i = 0; i < node->rowCount(); ++i) { const auto childNode = node->child(i, (int)Column::Account); if (childNode->hasChildren()) totalValue += childrenTotalValue(childNode, isInstitutionsModel); const auto data = childNode->data((int)Role::Value); if (data.isValid()) { auto value = data.value(); if (isInstitutionsModel) { const auto account = childNode->data((int)Role::Account).value(); if (account.accountGroup() == Account::Type::Liability) value = -value; } totalValue += value; } } return totalValue; } /** * Function to get the item from an account id. * * @param parent The parent to localize the search in the child items of this parameter. * @param accountId Search based on this parameter. * * @return The item corresponding to the given account id, NULL if the account was not found. */ QStandardItem *itemFromAccountId(QStandardItem *parent, const QString &accountId) { auto const model = parent->model(); const auto list = model->match(model->index(0, 0, parent->index()), (int)Role::ID, QVariant(accountId), 1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive)); if (!list.isEmpty()) return model->itemFromIndex(list.front()); // TODO: if not found at this item search for it in the model and if found reparent it. return nullptr; } /** * Function to get the item from an account id without knowing it's parent item. * Note that for the accounts which have two items in the model (favorite accounts) * the account item which is not the child of the favorite accounts item is always returned. * * @param model The model in which to search. * @param accountId Search based on this parameter. * * @return The item corresponding to the given account id, NULL if the account was not found. */ QStandardItem *itemFromAccountId(QStandardItemModel *model, const QString &accountId) { const auto list = model->match(model->index(0, 0), (int)Role::ID, QVariant(accountId), -1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive | Qt::MatchRecursive)); for (const auto& index : list) { // always return the account which is not the child of the favorite accounts item if (index.parent().data((int)Role::ID).toString() != AccountsModel::favoritesAccountId) return model->itemFromIndex(index); } return nullptr; } AccountsModel *q_ptr; /** * Used to load the accounts data. */ MyMoneyFile *m_file; /** * Used to emit the @ref netWorthChanged signal. */ MyMoneyMoney m_lastNetWorth; /** * Used to emit the @ref profitChanged signal. */ MyMoneyMoney m_lastProfit; /** * Used to set the reconciliation flag. */ MyMoneyAccount m_reconciledAccount; QList m_columns; static const QString m_accountsModelConfGroup; static const QString m_accountsModelColumnSelection; }; const QString AccountsModelPrivate::m_accountsModelConfGroup = QStringLiteral("AccountsModel"); const QString AccountsModelPrivate::m_accountsModelColumnSelection = QStringLiteral("ColumnSelection"); const QString AccountsModel::favoritesAccountId(QStringLiteral("Favorites")); /** * The constructor is private so that only the @ref Models object can create such an object. */ AccountsModel::AccountsModel(QObject *parent) : QStandardItemModel(parent), d_ptr(new AccountsModelPrivate(this)) { Q_D(AccountsModel); d->init(); } AccountsModel::AccountsModel(AccountsModelPrivate &dd, QObject *parent) : QStandardItemModel(parent), d_ptr(&dd) { Q_D(AccountsModel); d->init(); } AccountsModel::~AccountsModel() { Q_D(AccountsModel); delete d; } /** * Perform the initial load of the model data * from the @ref MyMoneyFile. * */ void AccountsModel::load() { Q_D(AccountsModel); blockSignals(true); QStandardItem *rootItem = invisibleRootItem(); QFont font; font.setBold(true); // adding favourite accounts node auto favoriteAccountsItem = new QStandardItem(); favoriteAccountsItem->setEditable(false); rootItem->appendRow(favoriteAccountsItem); { QMap itemData; itemData[Qt::DisplayRole] = itemData[Qt::EditRole] = itemData[(int)Role::FullName] = i18n("Favorites"); itemData[Qt::FontRole] = font; itemData[Qt::DecorationRole] = Icons::get(Icon::ViewBankAccount); itemData[(int)Role::ID] = favoritesAccountId; itemData[(int)Role::DisplayOrder] = 0; this->setItemData(favoriteAccountsItem->index(), itemData); } // adding account categories (asset, liability, etc.) node const QVector categories { Account::Type::Asset, Account::Type::Liability, Account::Type::Income, Account::Type::Expense, Account::Type::Equity }; for (const auto category : categories) { MyMoneyAccount account; QString accountName; int displayOrder; switch (category) { case Account::Type::Asset: // Asset accounts account = d->m_file->asset(); accountName = i18n("Asset accounts"); displayOrder = 1; break; case Account::Type::Liability: // Liability accounts account = d->m_file->liability(); accountName = i18n("Liability accounts"); displayOrder = 2; break; case Account::Type::Income: // Income categories account = d->m_file->income(); accountName = i18n("Income categories"); displayOrder = 3; break; case Account::Type::Expense: // Expense categories account = d->m_file->expense(); accountName = i18n("Expense categories"); displayOrder = 4; break; case Account::Type::Equity: // Equity accounts account = d->m_file->equity(); accountName = i18n("Equity accounts"); displayOrder = 5; break; default: continue; } auto accountsItem = new QStandardItem(accountName); accountsItem->setEditable(false); rootItem->appendRow(accountsItem); { QMap itemData; itemData[Qt::DisplayRole] = accountName; itemData[(int)Role::FullName] = itemData[Qt::EditRole] = QVariant::fromValue(MyMoneyFile::instance()->accountToCategory(account.id(), true)); itemData[Qt::FontRole] = font; itemData[(int)Role::DisplayOrder] = displayOrder; this->setItemData(accountsItem->index(), itemData); } // adding accounts (specific bank/investment accounts) belonging to given accounts category - for (const auto& accStr : account.accountList()) { + const auto& accountList = account.accountList(); + for (const auto& accStr : accountList) { const auto acc = d->m_file->account(accStr); auto item = new QStandardItem(acc.name()); accountsItem->appendRow(item); item->setEditable(false); auto subaccountsStr = acc.accountList(); // filter out stocks with zero balance if requested by user for (auto subaccStr = subaccountsStr.begin(); subaccStr != subaccountsStr.end();) { const auto subacc = d->m_file->account(*subaccStr); if (subacc.isInvest() && KMyMoneySettings::hideZeroBalanceEquities() && subacc.balance().isZero()) subaccStr = subaccountsStr.erase(subaccStr); else ++subaccStr; } // adding subaccounts (e.g. stocks under given investment account) belonging to given account d->loadSubaccounts(item, favoriteAccountsItem, subaccountsStr); const auto row = item->row(); d->setAccountData(accountsItem, row, acc, d->m_columns); d->loadPreferredAccount(acc, accountsItem, row, favoriteAccountsItem); } d->setAccountData(rootItem, accountsItem->row(), account, d->m_columns); } blockSignals(false); checkNetWorth(); checkProfit(); } QModelIndex AccountsModel::accountById(const QString& id) const { QModelIndexList accountList = match(index(0, 0), (int)Role::ID, id, 1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchRecursive)); if(accountList.count() == 1) { return accountList.first(); } return QModelIndex(); } QList *AccountsModel::getColumns() { Q_D(AccountsModel); return &d->m_columns; } void AccountsModel::setColumnVisibility(const Column column, const bool show) { Q_D(AccountsModel); const auto ixCol = d->m_columns.indexOf(column); // get column index in our column's map if (!show && ixCol != -1) { // start removing column row by row from bottom to up d->m_columns.removeOne(column); // remove it from our column's map blockSignals(true); // block signals to not emit resources consuming dataChanged for (auto i = 0; i < rowCount(); ++i) { // recursive lambda function to remove cell belonging to unwanted column from all rows auto removeCellFromRow = [=](auto &&self, QStandardItem *item) -> bool { for(auto j = 0; j < item->rowCount(); ++j) { auto childItem = item->child(j); if (childItem->hasChildren()) self(self, childItem); childItem->removeColumn(ixCol); } return true; }; auto topItem = item(i); if (topItem->hasChildren()) removeCellFromRow(removeCellFromRow, topItem); topItem->removeColumn(ixCol); } blockSignals(false); // unblock signals, so model can update itself with new column removeColumn(ixCol); // remove column from invisible root item which triggers model's view update } else if (show && ixCol == -1) { // start inserting columns row by row from up to bottom (otherwise columns will be inserted automatically) auto model = qobject_cast(this); const auto isInstitutionsModel = model ? true : false; // if it's institution's model, then don't set any data on institution nodes auto newColPos = 0; for(; newColPos < d->m_columns.count(); ++newColPos) { if (d->m_columns.at(newColPos) > column) break; } d->m_columns.insert(newColPos, column); // insert columns according to enum order for cleanliness insertColumn(newColPos); setHorizontalHeaderItem(newColPos, new QStandardItem(getHeaderName(column))); blockSignals(true); for (auto i = 0; i < rowCount(); ++i) { // recursive lambda function to remove cell belonging to unwanted column from all rows auto addCellToRow = [&, newColPos](auto &&self, QStandardItem *item) -> bool { for(auto j = 0; j < item->rowCount(); ++j) { auto childItem = item->child(j); childItem->insertColumns(newColPos, 1); if (childItem->hasChildren()) self(self, childItem); d->setAccountData(item, j, childItem->data((int)Role::Account).value(), QList {column}); } return true; }; auto topItem = item(i); topItem->insertColumns(newColPos, 1); if (topItem->hasChildren()) addCellToRow(addCellToRow, topItem); if (isInstitutionsModel) d->setInstitutionTotalValue(invisibleRootItem(), i); else if (i !=0) // favourites node doesn't play well here, so exclude it from update d->setAccountData(invisibleRootItem(), i, topItem->data((int)Role::Account).value(), QList {column}); } blockSignals(false); } } QString AccountsModel::getHeaderName(const Column column) { switch(column) { case Column::Account: return i18n("Account"); case Column::Type: return i18n("Type"); case Column::Tax: return i18nc("Column heading for category in tax report", "Tax"); case Column::VAT: return i18nc("Column heading for VAT category", "VAT"); case Column::CostCenter: return i18nc("Column heading for Cost Center", "CC"); case Column::TotalBalance: return i18n("Total Balance"); case Column::PostedValue: return i18n("Posted Value"); case Column::TotalValue: return i18n("Total Value"); case Column::AccountNumber: return i18n("Number"); case Column::AccountSortCode: return i18nc("IBAN, SWIFT, etc.", "Sort Code"); default: return QString(); } } /** * Check if netWorthChanged should be emitted. */ void AccountsModel::checkNetWorth() { Q_D(AccountsModel); // compute the net worth QModelIndexList assetList = match(index(0, 0), (int)Role::ID, MyMoneyFile::instance()->asset().id(), 1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive)); QModelIndexList liabilityList = match(index(0, 0), (int)Role::ID, MyMoneyFile::instance()->liability().id(), 1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive)); MyMoneyMoney netWorth; if (!assetList.isEmpty() && !liabilityList.isEmpty()) { const auto assetValue = data(assetList.front(), (int)Role::TotalValue); const auto liabilityValue = data(liabilityList.front(), (int)Role::TotalValue); if (assetValue.isValid() && liabilityValue.isValid()) netWorth = assetValue.value() - liabilityValue.value(); } if (d->m_lastNetWorth != netWorth) { d->m_lastNetWorth = netWorth; emit netWorthChanged(QVariantList {QVariant::fromValue(d->m_lastNetWorth)}, eView::Intent::UpdateNetWorth); } } /** * Check if profitChanged should be emitted. */ void AccountsModel::checkProfit() { Q_D(AccountsModel); // compute the profit const auto incomeList = match(index(0, 0), (int)Role::ID, MyMoneyFile::instance()->income().id(), 1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive)); const auto expenseList = match(index(0, 0), (int)Role::ID, MyMoneyFile::instance()->expense().id(), 1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive)); MyMoneyMoney profit; if (!incomeList.isEmpty() && !expenseList.isEmpty()) { const auto incomeValue = data(incomeList.front(), (int)Role::TotalValue); const auto expenseValue = data(expenseList.front(), (int)Role::TotalValue); if (incomeValue.isValid() && expenseValue.isValid()) profit = incomeValue.value() - expenseValue.value(); } if (d->m_lastProfit != profit) { d->m_lastProfit = profit; emit profitChanged(QVariantList {QVariant::fromValue(d->m_lastProfit)}, eView::Intent::UpdateProfit); } } MyMoneyMoney AccountsModel::accountValue(const MyMoneyAccount &account, const MyMoneyMoney &balance) { Q_D(AccountsModel); return d->value(account, balance); } /** * This slot should be connected so that the model will be notified which account is being reconciled. */ void AccountsModel::slotReconcileAccount(const MyMoneyAccount &account, const QDate &reconciliationDate, const MyMoneyMoney &endingBalance) { Q_D(AccountsModel); Q_UNUSED(reconciliationDate) Q_UNUSED(endingBalance) if (d->m_reconciledAccount.id() != account.id()) { // first clear the flag of the old reconciliation account if (!d->m_reconciledAccount.id().isEmpty()) { const auto list = match(index(0, 0), (int)Role::ID, QVariant(d->m_reconciledAccount.id()), -1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive | Qt::MatchRecursive)); for (const auto& index : list) setData(index, QVariant(QIcon(account.accountPixmap(false))), Qt::DecorationRole); } // then set the reconciliation flag of the new reconciliation account const auto list = match(index(0, 0), (int)Role::ID, QVariant(account.id()), -1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive | Qt::MatchRecursive)); for (const auto& index : list) setData(index, QVariant(QIcon(account.accountPixmap(true))), Qt::DecorationRole); d->m_reconciledAccount = account; } } /** * Notify the model that an object has been added. An action is performed only if the object is an account. * */ void AccountsModel::slotObjectAdded(File::Object objType, const QString& id) { Q_D(AccountsModel); if (objType != File::Object::Account) return; const auto account = MyMoneyFile::instance()->account(id); auto favoriteAccountsItem = d->itemFromAccountId(this, favoritesAccountId); auto parentAccountItem = d->itemFromAccountId(this, account.parentAccountId()); auto item = d->itemFromAccountId(parentAccountItem, account.id()); if (!item) { item = new QStandardItem(account.name()); parentAccountItem->appendRow(item); item->setEditable(false); } // load the sub-accounts if there are any - there could be sub accounts if this is an add operation // that was triggered in slotObjectModified on an already existing account which went trough a hierarchy change d->loadSubaccounts(item, favoriteAccountsItem, account.accountList()); const auto row = item->row(); d->setAccountData(parentAccountItem, row, account, d->m_columns); d->loadPreferredAccount(account, parentAccountItem, row, favoriteAccountsItem); checkNetWorth(); checkProfit(); } /** * Notify the model that an object has been modified. An action is performed only if the object is an account. * */ void AccountsModel::slotObjectModified(File::Object objType, const QString& id) { Q_D(AccountsModel); if (objType != File::Object::Account) return; const auto account = MyMoneyFile::instance()->account(id); auto accountItem = d->itemFromAccountId(this, id); if (!accountItem) { qDebug() << "Unexpected null accountItem in AccountsModel::slotObjectModified"; return; } const auto oldAccount = accountItem->data((int)Role::Account).value(); if (oldAccount.parentAccountId() == account.parentAccountId()) { // the hierarchy did not change so update the account data auto parentAccountItem = accountItem->parent(); if (!parentAccountItem) parentAccountItem = this->invisibleRootItem(); const auto row = accountItem->row(); d->setAccountData(parentAccountItem, row, account, d->m_columns); // and the child of the favorite item if the account is a favorite account or it's favorite status has just changed if (auto favoriteAccountsItem = d->itemFromAccountId(this, favoritesAccountId)) { if (account.value("PreferredAccount") == QLatin1String("Yes")) d->loadPreferredAccount(account, parentAccountItem, row, favoriteAccountsItem); else if (auto favItem = d->itemFromAccountId(favoriteAccountsItem, account.id())) favoriteAccountsItem->removeRow(favItem->row()); // it's not favorite anymore } } else { // this means that the hierarchy was changed - simulate this with a remove followed by and add operation slotObjectRemoved(File::Object::Account, oldAccount.id()); slotObjectAdded(File::Object::Account, id); } checkNetWorth(); checkProfit(); } /** * Notify the model that an object has been removed. An action is performed only if the object is an account. * */ void AccountsModel::slotObjectRemoved(File::Object objType, const QString& id) { if (objType != File::Object::Account) return; const auto list = match(index(0, 0), (int)Role::ID, id, -1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchRecursive)); for (const auto& index : list) removeRow(index.row(), index.parent()); checkNetWorth(); checkProfit(); } /** * Notify the model that the account balance has been changed. */ void AccountsModel::slotBalanceOrValueChanged(const MyMoneyAccount &account) { Q_D(AccountsModel); auto itParent = d->itemFromAccountId(this, account.id()); // get node of account in model auto isTopLevel = false; // it could be top-level but we don't know it yet while (itParent && !isTopLevel) { // loop in which we set total values and balances from the bottom to the top auto itCurrent = itParent; const auto accCurrent = d->m_file->account(itCurrent->data((int)Role::Account).value().id()); if (accCurrent.id().isEmpty()) { // this is institution d->setInstitutionTotalValue(invisibleRootItem(), itCurrent->row()); break; // it's top-level node so nothing above that; } itParent = itCurrent->parent(); if (!itParent) { itParent = this->invisibleRootItem(); isTopLevel = true; } d->setAccountBalanceAndValue(itParent, itCurrent->row(), accCurrent, d->m_columns); } checkNetWorth(); checkProfit(); } /** * The pimpl of the @ref InstitutionsModel derived from the pimpl of the @ref AccountsModel. */ class InstitutionsModelPrivate : public AccountsModelPrivate { public: InstitutionsModelPrivate(InstitutionsModel *qq) : AccountsModelPrivate(qq) { } ~InstitutionsModelPrivate() override { } /** * Function to get the institution item from an institution id. * * @param model The model in which to look for the item. * @param institutionId Search based on this parameter. * * @return The item corresponding to the given institution id, NULL if the institution was not found. */ QStandardItem *institutionItemFromId(QStandardItemModel *model, const QString &institutionId) { const auto list = model->match(model->index(0, 0), (int)Role::ID, QVariant(institutionId), 1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive)); if (!list.isEmpty()) return model->itemFromIndex(list.front()); return nullptr; // this should rarely fail as we add all institutions early on } /** * Function to add the account item to it's corresponding institution item. * * @param model The model where to add the item. * @param account The account for which to create the item. * */ void loadInstitution(QStandardItemModel *model, const MyMoneyAccount &account) { if (!account.isAssetLiability() && !account.isInvest()) return; // we've got account but don't know under which institution it should be added, so we find it out auto idInstitution = account.institutionId(); if (account.isInvest()) { // if it's stock account then... const auto investmentAccount = m_file->account(account.parentAccountId()); // ...get investment account it's under and... idInstitution = investmentAccount.institutionId(); // ...get institution from investment account } auto itInstitution = institutionItemFromId(model, idInstitution); auto itAccount = itemFromAccountId(itInstitution, account.id()); // check if account already exists under institution // only stock accounts are added to their parent in the institutions view // this makes hierarchy maintenance a lot easier since the stock accounts // are the only ones that always have the same institution as their parent auto itInvestmentAccount = account.isInvest() ? itemFromAccountId(itInstitution, account.parentAccountId()) : nullptr; if (!itAccount) { itAccount = new QStandardItem(account.name()); if (itInvestmentAccount) // stock account nodes go under investment account nodes and... itInvestmentAccount->appendRow(itAccount); else if (itInstitution) // ...the rest goes under institution's node itInstitution->appendRow(itAccount); else return; itAccount->setEditable(false); } if (itInvestmentAccount) { setAccountData(itInvestmentAccount, itAccount->row(), account, m_columns); // set data for stock account node setAccountData(itInstitution, itInvestmentAccount->row(), m_file->account(account.parentAccountId()), m_columns); // set data for investment account node } else if (itInstitution) { setAccountData(itInstitution, itAccount->row(), account, m_columns); } } /** * Function to add an institution item to the model. * * @param model The model in which to add the item. * @param institution The institution object which should be represented by the item. * */ void addInstitutionItem(QStandardItemModel *model, const MyMoneyInstitution &institution) { QFont font; font.setBold(true); auto itInstitution = new QStandardItem(Icons::get(Icon::ViewInstitutions), institution.name()); itInstitution->setFont(font); itInstitution->setData(QVariant::fromValue(MyMoneyMoney()), (int)Role::TotalValue); itInstitution->setData(institution.id(), (int)Role::ID); itInstitution->setData(QVariant::fromValue(institution), (int)Role::Account); itInstitution->setData(6, (int)Role::DisplayOrder); itInstitution->setEditable(false); model->invisibleRootItem()->appendRow(itInstitution); setInstitutionTotalValue(model->invisibleRootItem(), itInstitution->row()); } }; /** * The institution model contains the accounts grouped by institution. * */ InstitutionsModel::InstitutionsModel(QObject *parent) : AccountsModel(*new InstitutionsModelPrivate(this), parent) { } InstitutionsModel::~InstitutionsModel() { } /** * Perform the initial load of the model data * from the @ref MyMoneyFile. * */ void InstitutionsModel::load() { Q_D(InstitutionsModel); // create items for all the institutions auto institutionList = d->m_file->institutionList(); MyMoneyInstitution none; none.setName(i18n("Accounts with no institution assigned")); institutionList.append(none); for (const auto& institution : institutionList) // add all known institutions as top-level nodes d->addInstitutionItem(this, institution); QList accountsList; QList stocksList; d->m_file->accountList(accountsList); for (const auto& account : accountsList) { // add account nodes under institution nodes... if (account.isInvest()) // ...but wait with stocks until investment accounts appear stocksList.append(account); else d->loadInstitution(this, account); } for (const auto& stock : stocksList) { if (!(KMyMoneySettings::hideZeroBalanceEquities() && stock.balance().isZero())) d->loadInstitution(this, stock); } for (auto i = 0 ; i < rowCount(); ++i) d->setInstitutionTotalValue(invisibleRootItem(), i); } /** * Notify the model that an object has been added. An action is performed only if the object is an account or an institution. * */ void InstitutionsModel::slotObjectAdded(File::Object objType, const QString& id) { Q_D(InstitutionsModel); if (objType == File::Object::Institution) { // if an institution was added then add the item which will represent it const auto institution = MyMoneyFile::instance()->institution(id); d->addInstitutionItem(this, institution); } if (objType != File::Object::Account) return; // if an account was added then add the item which will represent it only for real accounts const auto account = MyMoneyFile::instance()->account(id); // nothing to do for root accounts and categories if (account.parentAccountId().isEmpty() || account.isIncomeExpense()) return; // load the account into the institution d->loadInstitution(this, account); // load the investment sub-accounts if there are any - there could be sub-accounts if this is an add operation // that was triggered in slotObjectModified on an already existing account which went trough a hierarchy change - const auto sAccounts = account.accountList(); + const auto& sAccounts = account.accountList(); if (!sAccounts.isEmpty()) { QList subAccounts; d->m_file->accountList(subAccounts, sAccounts); for (const auto& subAccount : subAccounts) { if (subAccount.isInvest()) { d->loadInstitution(this, subAccount); } } } } /** * Notify the model that an object has been modified. An action is performed only if the object is an account or an institution. * */ void InstitutionsModel::slotObjectModified(File::Object objType, const QString& id) { Q_D(InstitutionsModel); if (objType == File::Object::Institution) { // if an institution was modified then modify the item which represents it const auto institution = MyMoneyFile::instance()->institution(id); if (auto institutionItem = d->institutionItemFromId(this, id)) { institutionItem->setData(institution.name(), Qt::DisplayRole); institutionItem->setData(QVariant::fromValue(institution), (int)Role::Account); institutionItem->setIcon(MyMoneyInstitution::pixmap()); } } if (objType != File::Object::Account) return; // if an account was modified then modify the item which represents it const auto account = MyMoneyFile::instance()->account(id); // nothing to do for root accounts, categories and equity accounts since they don't have a representation in this model if (account.parentAccountId().isEmpty() || account.isIncomeExpense() || account.accountType() == Account::Type::Equity) return; auto accountItem = d->itemFromAccountId(this, account.id()); const auto oldAccount = accountItem->data((int)Role::Account).value(); if (oldAccount.institutionId() == account.institutionId()) { // the hierarchy did not change so update the account data d->setAccountData(accountItem->parent(), accountItem->row(), account, d->m_columns); } else { // this means that the hierarchy was changed - simulate this with a remove followed by and add operation slotObjectRemoved(File::Object::Account, oldAccount.id()); slotObjectAdded(File::Object::Account, id); } } /** * Notify the model that an object has been removed. An action is performed only if the object is an account or an institution. * */ void InstitutionsModel::slotObjectRemoved(File::Object objType, const QString& id) { Q_D(InstitutionsModel); if (objType == File::Object::Institution) { // if an institution was removed then remove the item which represents it if (auto itInstitution = d->institutionItemFromId(this, id)) removeRow(itInstitution->row(), itInstitution->index().parent()); } if (objType != File::Object::Account) return; // if an account was removed then remove the item which represents it and recompute the institution's value auto itAccount = d->itemFromAccountId(this, id); if (!itAccount) return; // this could happen if the account isIncomeExpense const auto account = itAccount->data((int)Role::Account).value(); if (auto itInstitution = d->itemFromAccountId(this, account.institutionId())) { AccountsModel::slotObjectRemoved(objType, id); d->setInstitutionTotalValue(invisibleRootItem(), itInstitution->row()); } } diff --git a/kmymoney/models/accountsproxymodel.cpp b/kmymoney/models/accountsproxymodel.cpp index a8eafb82f..8c4deeec1 100644 --- a/kmymoney/models/accountsproxymodel.cpp +++ b/kmymoney/models/accountsproxymodel.cpp @@ -1,358 +1,358 @@ /* * Copyright 2010-2014 Cristian Oneț * Copyright 2017-2018 Łukasz Wojniłowicz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "accountsproxymodel.h" #include "accountsproxymodel_p.h" // ---------------------------------------------------------------------------- // QT Includes // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "modelenums.h" #include "mymoneyenums.h" #include "mymoneyinstitution.h" #include "mymoneyaccount.h" #include "mymoneymoney.h" using namespace eAccountsModel; AccountsProxyModel::AccountsProxyModel(QObject *parent) : KRecursiveFilterProxyModel(parent), d_ptr(new AccountsProxyModelPrivate) { setDynamicSortFilter(true); setSortLocaleAware(true); setFilterCaseSensitivity(Qt::CaseInsensitive); } AccountsProxyModel::AccountsProxyModel(AccountsProxyModelPrivate &dd, QObject *parent) : KRecursiveFilterProxyModel(parent), d_ptr(&dd) { } AccountsProxyModel::~AccountsProxyModel() { } /** * This function was re-implemented so we could have a special display order (favorites first) */ bool AccountsProxyModel::lessThan(const QModelIndex &left, const QModelIndex &right) const { Q_D(const AccountsProxyModel); if (!left.isValid() || !right.isValid()) return false; // different sorting based on the column which is being sorted switch (d->m_mdlColumns->at(left.column())) { // for the accounts column sort based on the DisplayOrderRole case Column::Account: { const auto leftData = sourceModel()->data(left, (int)Role::DisplayOrder); const auto rightData = sourceModel()->data(right, (int)Role::DisplayOrder); if (leftData.toInt() == rightData.toInt()) { // sort items of the same display order alphabetically return QSortFilterProxyModel::lessThan(left, right); } return leftData.toInt() < rightData.toInt(); } // the total balance and value columns are sorted based on the value of the account case Column::TotalBalance: case Column::TotalValue: { const auto leftData = sourceModel()->data(sourceModel()->index(left.row(), (int)Column::Account, left.parent()), (int)Role::TotalValue); const auto rightData = sourceModel()->data(sourceModel()->index(right.row(), (int)Column::Account, right.parent()), (int)Role::TotalValue); return leftData.value() < rightData.value(); } default: break; } return QSortFilterProxyModel::lessThan(left, right); } /** * This function was re-implemented to consider all the filtering aspects that we need in the application. */ bool AccountsProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const { const auto index = sourceModel()->index(source_row, (int)Column::Account, source_parent); return acceptSourceItem(index) && filterAcceptsRowOrChildRows(source_row, source_parent); } /** * This function implements a recursive matching. It is used to match a row even if it's values * don't match the current filtering criteria but it has at least one child row that does match. */ bool AccountsProxyModel::filterAcceptsRowOrChildRows(int source_row, const QModelIndex &source_parent) const { if (QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent)) return true; const auto index = sourceModel()->index(source_row, (int)Column::Account, source_parent); for (auto i = 0; i < sourceModel()->rowCount(index); ++i) { if (filterAcceptsRowOrChildRows(i, index)) return true; } return false; } /** * Add the given account group to the filter. * @param group The account group to be added. * @see eMyMoney::Account */ void AccountsProxyModel::addAccountGroup(const QVector &groups) { Q_D(AccountsProxyModel); foreach (const auto group, groups) { switch (group) { case eMyMoney::Account::Type::Asset: d->m_typeList << eMyMoney::Account::Type::Checkings; d->m_typeList << eMyMoney::Account::Type::Savings; d->m_typeList << eMyMoney::Account::Type::Cash; d->m_typeList << eMyMoney::Account::Type::AssetLoan; d->m_typeList << eMyMoney::Account::Type::CertificateDep; d->m_typeList << eMyMoney::Account::Type::Investment; d->m_typeList << eMyMoney::Account::Type::Stock; d->m_typeList << eMyMoney::Account::Type::MoneyMarket; d->m_typeList << eMyMoney::Account::Type::Asset; d->m_typeList << eMyMoney::Account::Type::Currency; break; case eMyMoney::Account::Type::Liability: d->m_typeList << eMyMoney::Account::Type::CreditCard; d->m_typeList << eMyMoney::Account::Type::Loan; d->m_typeList << eMyMoney::Account::Type::Liability; break; case eMyMoney::Account::Type::Income: d->m_typeList << eMyMoney::Account::Type::Income; break; case eMyMoney::Account::Type::Expense: d->m_typeList << eMyMoney::Account::Type::Expense; break; case eMyMoney::Account::Type::Equity: d->m_typeList << eMyMoney::Account::Type::Equity; break; default: d->m_typeList << group; break; } } invalidateFilter(); } /** * Add the given account type to the filter. * @param type The account type to be added. * @see eMyMoney::Account */ void AccountsProxyModel::addAccountType(eMyMoney::Account::Type type) { Q_D(AccountsProxyModel); d->m_typeList << type; invalidateFilter(); } /** * Remove the given account type from the filter. * @param type The account type to be removed. * @see eMyMoney::Account */ void AccountsProxyModel::removeAccountType(eMyMoney::Account::Type type) { Q_D(AccountsProxyModel); if (d->m_typeList.removeAll(type) > 0) { invalidateFilter(); } } /** * Use this to reset the filter. */ void AccountsProxyModel::clear() { Q_D(AccountsProxyModel); d->m_typeList.clear(); invalidateFilter(); } /** * Implementation function that performs the actual filtering. */ bool AccountsProxyModel::acceptSourceItem(const QModelIndex &source) const { Q_D(const AccountsProxyModel); if (source.isValid()) { const auto data = sourceModel()->data(source, (int)Role::Account); if (data.isValid()) { if (data.canConvert()) { const auto account = data.value(); if ((hideClosedAccounts() && account.isClosed())) return false; // we hide stock accounts if not in expert mode if (account.isInvest() && hideEquityAccounts()) return false; // we hide equity accounts if not in expert mode if (account.accountType() == eMyMoney::Account::Type::Equity && hideEquityAccounts()) return false; // we hide unused income and expense accounts if the specific flag is set if ((account.accountType() == eMyMoney::Account::Type::Income || account.accountType() == eMyMoney::Account::Type::Expense) && hideUnusedIncomeExpenseAccounts()) { const auto totalValue = sourceModel()->data(source, (int)Role::TotalValue); if (totalValue.isValid() && totalValue.value().isZero()) { - emit unusedIncomeExpenseAccountHidden(); + emit const_cast(this)->unusedIncomeExpenseAccountHidden(); return false; } } if (d->m_typeList.contains(account.accountType())) return true; } else if (data.canConvert() && sourceModel()->rowCount(source) == 0) { // if this is an institution that has no children show it only if hide unused institutions (hide closed accounts for now) is not checked return !hideClosedAccounts(); } // let the visibility of all other institutions (the ones with children) be controlled by the visibility of their children } // all parents that have at least one visible child must be visible const auto rowCount = sourceModel()->rowCount(source); for (auto i = 0; i < rowCount; ++i) { const auto index = sourceModel()->index(i, (int)(int)Column::Account, source); if (acceptSourceItem(index)) return true; } } return false; } /** * Set if closed accounts should be hidden or not. * @param hideClosedAccounts */ void AccountsProxyModel::setHideClosedAccounts(bool hideClosedAccounts) { Q_D(AccountsProxyModel); if (d->m_hideClosedAccounts != hideClosedAccounts) { d->m_hideClosedAccounts = hideClosedAccounts; invalidateFilter(); } } /** * Check if closed accounts are hidden or not. */ bool AccountsProxyModel::hideClosedAccounts() const { Q_D(const AccountsProxyModel); return d->m_hideClosedAccounts; } /** * Set if equity and investment accounts should be hidden or not. * @param hideEquityAccounts */ void AccountsProxyModel::setHideEquityAccounts(bool hideEquityAccounts) { Q_D(AccountsProxyModel); if (d->m_hideEquityAccounts != hideEquityAccounts) { d->m_hideEquityAccounts = hideEquityAccounts; invalidateFilter(); } } /** * Check if equity and investment accounts are hidden or not. */ bool AccountsProxyModel::hideEquityAccounts() const { Q_D(const AccountsProxyModel); return d->m_hideEquityAccounts; } /** * Set if empty categories should be hidden or not. * @param hideUnusedIncomeExpenseAccounts */ void AccountsProxyModel::setHideUnusedIncomeExpenseAccounts(bool hideUnusedIncomeExpenseAccounts) { Q_D(AccountsProxyModel); if (d->m_hideUnusedIncomeExpenseAccounts != hideUnusedIncomeExpenseAccounts) { d->m_hideUnusedIncomeExpenseAccounts = hideUnusedIncomeExpenseAccounts; invalidateFilter(); } } /** * Check if empty categories are hidden or not. */ bool AccountsProxyModel::hideUnusedIncomeExpenseAccounts() const { Q_D(const AccountsProxyModel); return d->m_hideUnusedIncomeExpenseAccounts; } /** * Returns the number of visible items after filtering. In case @a includeBaseAccounts * is set to @c true, the 5 base accounts (asset, liability, income, expense and equity) * will also be counted. The default is @c false. */ int AccountsProxyModel::visibleItems(bool includeBaseAccounts) const { auto rows = 0; for (auto i = 0; i < rowCount(QModelIndex()); ++i) { if(includeBaseAccounts) { ++rows; } const auto childIndex = index(i, 0); if (hasChildren(childIndex)) { rows += visibleItems(childIndex); } } return rows; } /** * Returns the number of visible items under the given @a index. * The column of the @a index must be 0, otherwise no count will * be returned (returns 0). */ int AccountsProxyModel::visibleItems(const QModelIndex& index) const { auto rows = 0; if (index.isValid() && index.column() == (int)Column::Account) { // CAUTION! Assumption is being made that Account column number is always 0 const auto *model = index.model(); const auto rowCount = model->rowCount(index); for (auto i = 0; i < rowCount; ++i) { ++rows; const auto childIndex = model->index(i, index.column(), index); if (model->hasChildren(childIndex)) rows += visibleItems(childIndex); } } return rows; } void AccountsProxyModel::setSourceColumns(QList *columns) { Q_D(AccountsProxyModel); d->m_mdlColumns = columns; } diff --git a/kmymoney/models/accountsproxymodel.h b/kmymoney/models/accountsproxymodel.h index 0664ea5b9..05cc880e5 100644 --- a/kmymoney/models/accountsproxymodel.h +++ b/kmymoney/models/accountsproxymodel.h @@ -1,108 +1,108 @@ /* * Copyright 2010-2014 Cristian Oneț * Copyright 2017-2018 Łukasz Wojniłowicz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef ACCOUNTSPROXYMODEL_H #define ACCOUNTSPROXYMODEL_H #include "kmm_models_export.h" // ---------------------------------------------------------------------------- // QT Includes // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes /** * A proxy model to provide various sorting and filtering operations for @ref AccountsModel. * * Here is an example of how to use this class in combination with the @ref AccountsModel. * (in the example @a widget is a pointer to a model/view widget): * * @code * AccountsFilterProxyModel *filterModel = new AccountsFilterProxyModel(widget); * filterModel->addAccountGroup(eMyMoney::Account::Type::Asset); * filterModel->addAccountGroup(eMyMoney::Account::Type::Liability); * filterModel->setSourceModel(Models::instance()->accountsModel()); * filterModel->sort(0); * * widget->setModel(filterModel); * @endcode * * @see AccountsModel * * @author Cristian Onet 2010 * */ namespace eMyMoney { namespace Account {enum class Type;} } namespace eAccountsModel { enum class Column; } class AccountsProxyModelPrivate; class KMM_MODELS_EXPORT AccountsProxyModel : public KRecursiveFilterProxyModel { Q_OBJECT Q_DISABLE_COPY(AccountsProxyModel) public: explicit AccountsProxyModel(QObject *parent = nullptr); virtual ~AccountsProxyModel(); void addAccountType(eMyMoney::Account::Type type); void addAccountGroup(const QVector &groups); void removeAccountType(eMyMoney::Account::Type type); void clear(); void setHideClosedAccounts(bool hideClosedAccounts); bool hideClosedAccounts() const; void setHideEquityAccounts(bool hideEquityAccounts); bool hideEquityAccounts() const; void setHideUnusedIncomeExpenseAccounts(bool hideUnusedIncomeExpenseAccounts); bool hideUnusedIncomeExpenseAccounts() const; int visibleItems(bool includeBaseAccounts = false) const; void setSourceColumns(QList *columns); protected: const QScopedPointer d_ptr; AccountsProxyModel(AccountsProxyModelPrivate &dd, QObject *parent); bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; bool acceptSourceItem(const QModelIndex &source) const; bool filterAcceptsRowOrChildRows(int source_row, const QModelIndex &source_parent) const; int visibleItems(const QModelIndex& index) const; Q_SIGNALS: - void unusedIncomeExpenseAccountHidden() const; + void unusedIncomeExpenseAccountHidden(); private: Q_DECLARE_PRIVATE(AccountsProxyModel) }; #endif diff --git a/kmymoney/models/ledgertransaction.cpp b/kmymoney/models/ledgertransaction.cpp index b1824b5cf..d00fb6124 100644 --- a/kmymoney/models/ledgertransaction.cpp +++ b/kmymoney/models/ledgertransaction.cpp @@ -1,315 +1,315 @@ /* * Copyright 2016-2018 Thomas Baumgart * Copyright 2017-2018 Łukasz Wojniłowicz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "ledgertransaction.h" #include "ledgertransaction_p.h" // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneymoney.h" #include "mymoneyenums.h" using namespace eMyMoney; LedgerTransaction::LedgerTransaction() : LedgerItem(), d_ptr(new LedgerTransactionPrivate) { } LedgerTransaction::LedgerTransaction(const MyMoneyTransaction& t, const MyMoneySplit& s) : LedgerItem(), d_ptr(new LedgerTransactionPrivate) { Q_D(LedgerTransaction); d->init(t,s); } LedgerTransaction::LedgerTransaction(LedgerTransactionPrivate &dd) : d_ptr(&dd) { } LedgerTransaction::LedgerTransaction(LedgerTransactionPrivate &dd, const MyMoneyTransaction& t, const MyMoneySplit& s) : d_ptr(&dd) { Q_D(LedgerTransaction); d->init(t,s); } LedgerTransaction::LedgerTransaction(const LedgerTransaction& other) : d_ptr(new LedgerTransactionPrivate(*other.d_func())) { } LedgerTransaction::~LedgerTransaction() { delete d_ptr; } QDate LedgerTransaction::postDate() const { Q_D(const LedgerTransaction); return d->m_transaction.postDate(); } MyMoneyTransaction LedgerTransaction::transaction() const { Q_D(const LedgerTransaction); return d->m_transaction; } const MyMoneySplit& LedgerTransaction::split() const { Q_D(const LedgerTransaction); return d->m_split; } QString LedgerTransaction::accountId() const { Q_D(const LedgerTransaction); return d->m_split.accountId(); } QString LedgerTransaction::account() const { Q_D(const LedgerTransaction); return d->m_account; } QString LedgerTransaction::counterAccountId() const { Q_D(const LedgerTransaction); return d->m_counterAccountId; } QString LedgerTransaction::counterAccount() const { Q_D(const LedgerTransaction); return d->m_counterAccount; } QString LedgerTransaction::costCenterId() const { Q_D(const LedgerTransaction); return d->m_costCenterId; } QString LedgerTransaction::payeeName() const { Q_D(const LedgerTransaction); return d->m_payeeName; } QString LedgerTransaction::payeeId() const { Q_D(const LedgerTransaction); return d->m_payeeId; } QString LedgerTransaction::transactionNumber() const { Q_D(const LedgerTransaction); return d->m_split.number(); } Qt::ItemFlags LedgerTransaction::flags() const { return (Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable); } QString LedgerTransaction::transactionSplitId() const { Q_D(const LedgerTransaction); QString rc; if(!d->m_transaction.id().isEmpty()) { - rc = QString("%1-%2").arg(d->m_transaction.id()).arg(d->m_split.id()); + rc = QString("%1-%2").arg(d->m_transaction.id(), d->m_split.id()); } return rc; } int LedgerTransaction::splitCount() const { Q_D(const LedgerTransaction); return d->m_transaction.splitCount(); } QString LedgerTransaction::transactionId() const { Q_D(const LedgerTransaction); return d->m_transaction.id(); } eMyMoney::Split::State LedgerTransaction::reconciliationState() const { Q_D(const LedgerTransaction); return d->m_split.reconcileFlag(); } QString LedgerTransaction::reconciliationStateShort() const { Q_D(const LedgerTransaction); QString rc; switch(d->m_split.reconcileFlag()) { case eMyMoney::Split::State::NotReconciled: default: break; case eMyMoney::Split::State::Cleared: rc = i18nc("Reconciliation flag C", "C"); break; case eMyMoney::Split::State::Reconciled: rc = i18nc("Reconciliation flag R", "R"); break; case eMyMoney::Split::State::Frozen: rc = i18nc("Reconciliation flag F", "F"); break; } return rc; } QString LedgerTransaction::reconciliationStateLong() const { Q_D(const LedgerTransaction); QString rc; switch(d->m_split.reconcileFlag()) { case eMyMoney::Split::State::NotReconciled: default: rc = i18nc("Reconciliation flag empty", "Not reconciled"); break; case eMyMoney::Split::State::Cleared: rc = i18nc("Reconciliation flag C", "Cleared"); break; case eMyMoney::Split::State::Reconciled: rc = i18nc("Reconciliation flag R", "Reconciled"); break; case eMyMoney::Split::State::Frozen: rc = i18nc("Reconciliation flag F", "Frozen"); break; } return rc; } QString LedgerTransaction::payment() const { Q_D(const LedgerTransaction); return d->m_payment; } QString LedgerTransaction::deposit() const { Q_D(const LedgerTransaction); return d->m_deposit; } void LedgerTransaction::setBalance(QString txt) { Q_D(LedgerTransaction); d->m_balance = txt; } QString LedgerTransaction::balance() const { Q_D(const LedgerTransaction); return d->m_balance; } MyMoneyMoney LedgerTransaction::shares() const { Q_D(const LedgerTransaction); return d->m_split.shares(); } QString LedgerTransaction::sharesAmount() const { Q_D(const LedgerTransaction); return d->m_shares; } QString LedgerTransaction::signedSharesAmount() const { Q_D(const LedgerTransaction); return d->m_signedShares; } QString LedgerTransaction::sharesSuffix() const { Q_D(const LedgerTransaction); return d->m_sharesSuffix; } MyMoneyMoney LedgerTransaction::value() const { Q_D(const LedgerTransaction); return d->m_split.value(); } QString LedgerTransaction::memo() const { Q_D(const LedgerTransaction); auto memo = d->m_split.memo(); if(memo.isEmpty()) { memo = d->m_transaction.memo(); } return memo; } bool LedgerTransaction::isErroneous() const { Q_D(const LedgerTransaction); return d->m_erroneous; } bool LedgerTransaction::isImported() const { Q_D(const LedgerTransaction); return d->m_transaction.isImported(); } LedgerTransaction LedgerTransaction::newTransactionEntry() { // create a dummy entry for new transactions MyMoneyTransaction t; t.setPostDate(QDate(2900,12,31)); return LedgerTransaction(t, MyMoneySplit()); } bool LedgerTransaction::isNewTransactionEntry() const { Q_D(const LedgerTransaction); return d->m_transaction.id().isEmpty() && d->m_split.id().isEmpty(); } QString LedgerTransaction::transactionCommodity() const { Q_D(const LedgerTransaction); return d->m_transaction.commodity(); } diff --git a/kmymoney/models/splitmodel.cpp b/kmymoney/models/splitmodel.cpp index 5552bd0b4..fcebc5427 100644 --- a/kmymoney/models/splitmodel.cpp +++ b/kmymoney/models/splitmodel.cpp @@ -1,447 +1,447 @@ /* * Copyright 2016-2018 Thomas Baumgart * Copyright 2017-2018 Łukasz Wojniłowicz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "splitmodel.h" // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "models.h" #include "costcentermodel.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneypayee.h" #include "mymoneymoney.h" #include "mymoneyexception.h" #include "kmymoneyutils.h" #include "modelenums.h" using namespace eLedgerModel; using namespace eMyMoney; class SplitModelPrivate { public: SplitModelPrivate() : m_invertValues(false) {} bool isCreateSplitEntry(const QString& id) const { return id.isEmpty(); } MyMoneyTransaction m_transaction; QVector m_splits; bool m_invertValues; }; SplitModel::SplitModel(QObject* parent) : QAbstractTableModel(parent), d_ptr(new SplitModelPrivate) { } SplitModel::~SplitModel() { } QString SplitModel::newSplitId() { return QLatin1String("New-ID"); } bool SplitModel::isNewSplitId(const QString& id) { return id.compare(newSplitId()) == 0; } int SplitModel::rowCount(const QModelIndex& parent) const { Q_D(const SplitModel); Q_UNUSED(parent); return d->m_splits.count(); } int SplitModel::columnCount(const QModelIndex& parent) const { Q_UNUSED(parent); return (int)Column::LastColumn; } void SplitModel::deepCopy(const SplitModel& right, bool revertSplitSign) { Q_D(SplitModel); beginInsertRows(QModelIndex(), 0, right.rowCount()); d->m_splits = right.d_func()->m_splits; d->m_transaction = right.d_func()->m_transaction; if(revertSplitSign) { for(int idx = 0; idx < d->m_splits.count(); ++idx) { MyMoneySplit& split = d->m_splits[idx]; split.setShares(-split.shares()); split.setValue(-split.value()); } } endInsertRows(); } QVariant SplitModel::headerData(int section, Qt::Orientation orientation, int role) const { if(orientation == Qt::Horizontal && role == Qt::DisplayRole) { switch(section) { case (int)Column::CostCenter: return i18n("Cost Center"); case (int)Column::Detail: return i18n("Category"); case (int)Column::Number: return i18n("No"); case (int)Column::Date: return i18n("Date"); case (int)Column::Security: return i18n("Security"); case (int)Column::Reconciliation: return i18n("C"); case (int)Column::Payment: return i18n("Payment"); case (int)Column::Deposit: return i18n("Deposit"); case (int)Column::Quantity: return i18n("Quantity"); case (int)Column::Price: return i18n("Price"); case (int)Column::Amount: return i18n("Amount"); case (int)Column::Value: return i18n("Value"); case (int)Column::Balance: return i18n("Balance"); } } return QAbstractItemModel::headerData(section, orientation, role); } QVariant SplitModel::data(const QModelIndex& index, int role) const { Q_D(const SplitModel); if(!index.isValid()) return QVariant(); if(index.row() < 0 || index.row() >= d->m_splits.count()) return QVariant(); QVariant rc; MyMoneyAccount acc; MyMoneyMoney value; const MyMoneySplit& split = d->m_splits[index.row()]; QModelIndex subIndex; CostCenterModel* ccModel = Models::instance()->costCenterModel(); switch(role) { case Qt::DisplayRole: // make sure to never return any displayable text for the dummy entry if(!d->isCreateSplitEntry(split.id())) { switch(index.column()) { case (int)Column::Detail: rc = MyMoneyFile::instance()->accountToCategory(split.accountId()); break; case (int)Column::CostCenter: subIndex = Models::indexById(ccModel, CostCenterModel::CostCenterIdRole, split.costCenterId()); rc = ccModel->data(subIndex); break; case (int)Column::Number: rc = split.number(); break; case (int)Column::Reconciliation: rc = KMyMoneyUtils::reconcileStateToString(split.reconcileFlag(), false); break; case (int)Column::Payment: if(split.value().isNegative()) { acc = MyMoneyFile::instance()->account(split.accountId()); rc = (-split).value(d->m_transaction.commodity(), acc.currencyId()).formatMoney(acc.fraction()); } break; case (int)Column::Deposit: if(!split.value().isNegative()) { acc = MyMoneyFile::instance()->account(split.accountId()); rc = split.value(d->m_transaction.commodity(), acc.currencyId()).formatMoney(acc.fraction()); } break; default: break; } } break; case Qt::TextAlignmentRole: switch(index.column()) { case (int)Column::Payment: case (int)Column::Deposit: case (int)Column::Amount: case (int)Column::Balance: case (int)Column::Value: rc = QVariant(Qt::AlignRight| Qt::AlignTop); break; case (int)Column::Reconciliation: rc = QVariant(Qt::AlignHCenter | Qt::AlignTop); break; default: rc = QVariant(Qt::AlignLeft | Qt::AlignTop); break; } break; case (int)Role::AccountId: rc = split.accountId(); break; case (int)Role::Account: rc = MyMoneyFile::instance()->accountToCategory(split.accountId()); break; case (int)Role::TransactionId: rc = QString("%1").arg(d->m_transaction.id()); break; case (int)Role::TransactionSplitId: - rc = QString("%1-%2").arg(d->m_transaction.id()).arg(split.id()); + rc = QString("%1-%2").arg(d->m_transaction.id(), split.id()); break; case (int)Role::SplitId: rc = split.id(); break; case (int)Role::Memo: case (int)Role::SingleLineMemo: rc = split.memo(); if(role == (int)Role::SingleLineMemo) { QString txt = rc.toString(); // remove empty lines txt.replace("\n\n", "\n"); // replace '\n' with ", " txt.replace('\n', ", "); rc = txt; } break; case (int)Role::SplitShares: rc = QVariant::fromValue(split.shares()); break; case (int)Role::SplitValue: acc = MyMoneyFile::instance()->account(split.accountId()); rc = QVariant::fromValue(split.value(d->m_transaction.commodity(), acc.currencyId())); break; case (int)Role::PayeeName: try { rc = MyMoneyFile::instance()->payee(split.payeeId()).name(); } catch (const MyMoneyException &e) { } break; case (int)Role::CostCenterId: rc = split.costCenterId(); break; case (int)Role::TransactionCommodity: return d->m_transaction.commodity(); break; case (int)Role::Number: rc = split.number(); break; case (int)Role::PayeeId: rc = split.payeeId(); break; default: if(role >= Qt::UserRole) { qWarning() << "Undefined role" << role << "(" << role-Qt::UserRole << ") in SplitModel::data"; } break; } return rc; } bool SplitModel::setData(const QModelIndex& index, const QVariant& value, int role) { Q_D(SplitModel); bool rc = false; if(index.isValid()) { MyMoneySplit& split = d->m_splits[index.row()]; if(split.id().isEmpty()) { split = MyMoneySplit(newSplitId(), split); } QString val; rc = true; switch(role) { case (int)Role::PayeeId: split.setPayeeId(value.toString()); break; case (int)Role::AccountId: split.setAccountId(value.toString()); break; case (int)Role::Memo: split.setMemo(value.toString()); break; case (int)Role::CostCenterId: val = value.toString(); split.setCostCenterId(value.toString()); break; case (int)Role::Number: split.setNumber(value.toString()); break; case (int)Role::SplitShares: split.setShares(value.value()); break; case (int)Role::SplitValue: split.setValue(value.value()); break; case (int)Role::EmitDataChanged: { // the whole row changed QModelIndex topLeft = this->index(index.row(), 0); QModelIndex bottomRight = this->index(index.row(), this->columnCount()-1); emit dataChanged(topLeft, bottomRight); } break; default: rc = false; break; } } return rc; } void SplitModel::addSplit(const QString& transactionSplitId) { Q_D(SplitModel); QRegExp transactionSplitIdExp("^(\\w+)-(\\w+)$"); if(transactionSplitIdExp.exactMatch(transactionSplitId)) { const QString transactionId = transactionSplitIdExp.cap(1); const QString splitId = transactionSplitIdExp.cap(2); if(transactionId != d->m_transaction.id()) { try { d->m_transaction = MyMoneyFile::instance()->transaction(transactionId); } catch (const MyMoneyException &e) { d->m_transaction = MyMoneyTransaction(); } } try { beginInsertRows(QModelIndex(), rowCount(), rowCount()); d->m_splits.append(d->m_transaction.splitById(splitId)); endInsertRows(); } catch (const MyMoneyException &e) { d->m_transaction = MyMoneyTransaction(); } } } void SplitModel::addEmptySplitEntry() { Q_D(SplitModel); QModelIndexList list = match(index(0, 0), (int)Role::SplitId, QString(), -1, Qt::MatchExactly); if(list.count() == 0) { beginInsertRows(QModelIndex(), rowCount(), rowCount()); // d->m_splits.append(MyMoneySplit(d->newSplitEntryId(), MyMoneySplit())); d->m_splits.append(MyMoneySplit()); endInsertRows(); } } void SplitModel::removeEmptySplitEntry() { Q_D(SplitModel); // QModelIndexList list = match(index(0, 0), SplitIdRole, d->newSplitEntryId(), -1, Qt::MatchExactly); QModelIndexList list = match(index(0, 0), (int)Role::SplitId, QString(), -1, Qt::MatchExactly); if(list.count()) { QModelIndex index = list.at(0); beginRemoveRows(QModelIndex(), index.row(), index.row()); d->m_splits.remove(index.row(), 1); endRemoveRows(); } } bool SplitModel::removeRows(int row, int count, const QModelIndex& parent) { Q_D(SplitModel); bool rc = false; if(count > 0) { beginRemoveRows(parent, row, row + count - 1); d->m_splits.remove(row, count); endRemoveRows(); rc = true; } return rc; } Qt::ItemFlags SplitModel::flags(const QModelIndex& index) const { Q_D(const SplitModel); Qt::ItemFlags flags; if(!index.isValid()) return flags; if(index.row() < 0 || index.row() >= d->m_splits.count()) return flags; return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable; } #if 0 void SplitModel::removeSplit(const LedgerTransaction& t) { Q_D(SplitModel); QModelIndexList list = match(index(0, 0), TransactionSplitIdRole, t.transactionSplitId(), -1, Qt::MatchExactly); if(list.count()) { QModelIndex index = list.at(0); beginRemoveRows(QModelIndex(), index.row(), index.row()); delete d->m_ledgerItems[index.row()]; d->m_ledgerItems.remove(index.row(), 1); endRemoveRows(); // just make sure we're in sync Q_ASSERT(d->m_ledgerItems.count() == rowCount()); } } #endif diff --git a/kmymoney/mymoney/mymoneyfile.cpp b/kmymoney/mymoney/mymoneyfile.cpp index 159c77efc..fb24a0ab7 100644 --- a/kmymoney/mymoney/mymoneyfile.cpp +++ b/kmymoney/mymoney/mymoneyfile.cpp @@ -1,3581 +1,3582 @@ /* * Copyright 2000-2003 Michael Edwardes * Copyright 2001-2002 Felix Rodriguez * Copyright 2002-2004 Kevin Tambascio * Copyright 2004-2005 Ace Jones * Copyright 2006-2019 Thomas Baumgart * Copyright 2006 Darren Gould * Copyright 2017-2018 Łukasz Wojniłowicz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "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 "mymoneyenums.h" // include the following line to get a 'cout' for debug purposes // #include using namespace eMyMoney; 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_CSTRING("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::fromLatin1("No transaction started for %1").arg(QString::fromLatin1(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_CSTRING("Storage already attached"); if (storage == 0) throw MYMONEYEXCEPTION_CSTRING("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_CSTRING("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; // collect notifications about removed objects QStringList removedObjects; const auto& set = d->m_changeSet; for (const auto& change : set) { switch (change.notificationMode()) { case File::Mode::Remove: removedObjects += change.id(); break; default: break; } } // 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: if (!removedObjects.contains(change.id())) { emit objectAdded(change.objectType(), change.id()); } break; case File::Mode::Modify: if (!removedObjects.contains(change.id())) { 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 (!removedObjects.contains(id)) { // 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) { if (!removedObjects.contains(id)) { 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_CSTRING("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_CSTRING("Cannot store split with no account assigned"); if (isStandardAccount(split.accountId())) throw MYMONEYEXCEPTION_CSTRING("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_CSTRING("Unable to modify the standard account groups"); } if (account.accountType() != acc.accountType() && !account.isLiquidAsset() && !acc.isLiquidAsset()) throw MYMONEYEXCEPTION_CSTRING("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_CSTRING("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_CSTRING("Unable to reparent Stock to non-investment account"); if (parent.accountType() == Account::Type::Investment && !acc.isInvest()) throw MYMONEYEXCEPTION_CSTRING("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_CSTRING("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& account, const QString& name) const { static MyMoneyAccount nullAccount; const auto accounts = account.accountList(); for (const auto& acc : accounts) { const auto sacc = MyMoneyFile::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(QString::fromLatin1("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_CSTRING("Unable to remove the standard account groups"); if (hasActiveSplits(account.id())) { throw MYMONEYEXCEPTION_CSTRING("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_CSTRING("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_CSTRING("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_CSTRING("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")); 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", e.what()); throw; } } 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_CSTRING("Account has no name"); if (account.id().length() != 0) throw MYMONEYEXCEPTION_CSTRING("New account must have no id"); if (account.accountList().count() != 0) throw MYMONEYEXCEPTION_CSTRING("New account must have no sub-accounts"); if (!account.parentAccountId().isEmpty()) throw MYMONEYEXCEPTION_CSTRING("New account must have no parent-id"); if (account.accountType() == Account::Type::Unknown) throw MYMONEYEXCEPTION_CSTRING("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_CSTRING("Stock account cannot be parent account"); if (account.isInvest() && parent.accountType() != Account::Type::Investment) throw MYMONEYEXCEPTION_CSTRING("Stock account must have investment account as parent "); if (!account.isInvest() && parent.accountType() == Account::Type::Investment) throw MYMONEYEXCEPTION_CSTRING("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_CSTRING("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_CSTRING("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::fromLatin1("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_CSTRING("Unable to add transaction with id set"); if (!transaction.postDate().isValid()) throw MYMONEYEXCEPTION_CSTRING("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_CSTRING("Cannot add split with no account assigned"); if (acc.isLoan()) loanAccountAffected = true; if (isStandardAccount(split.accountId())) throw MYMONEYEXCEPTION_CSTRING("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 { d->checkStorage(); 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(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Liability)); } MyMoneyAccount MyMoneyFile::asset() const { d->checkStorage(); return account(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Asset)); } MyMoneyAccount MyMoneyFile::expense() const { d->checkStorage(); return account(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Expense)); } MyMoneyAccount MyMoneyFile::income() const { d->checkStorage(); return account(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Income)); } MyMoneyAccount MyMoneyFile::equity() const { d->checkStorage(); return account(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Equity)); } 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", 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_CSTRING("Cannot add split with no account assigned"); if (isStandardAccount(split.accountId())) throw MYMONEYEXCEPTION_CSTRING("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_CSTRING("Cannot store split with no account assigned"); if (isStandardAccount(split.accountId())) throw MYMONEYEXCEPTION_CSTRING("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_CSTRING("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 payeeList; (*it_r).payees(payeeList); bool rChanged = false; for (auto it_payee = payeeList.begin(); it_payee != payeeList.end(); ++it_payee) { if (payeeConversionMap.find(*it_payee) != payeeConversionMap.end()) { rc << i18n(" * Payee id updated in report '%1'.", (*it_r).name()); ++problemCount; r.removeReference(*it_payee); r.addPayee(payeeConversionMap[*it_payee]); 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 foreign 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_CSTRING("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), 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 arbitrary 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 parentAccount; if (newAccount.parentAccountId().isEmpty()) { if (!value.isNegative() && value2.isNegative()) parentAccount = income(); else parentAccount = expense(); } else { parentAccount = 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, parentAccount, 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_CSTRING("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("AOA", i18n("Angolan Kwanza"), "Kz")); 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("CUC", i18n("Cuban Convertible 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(); } void MyMoneyFile::updateVAT(MyMoneyTransaction& transaction) const { // check if transaction qualifies const auto splitCount = transaction.splits().count(); if (splitCount > 1 && splitCount <= 3) { MyMoneyMoney amount; MyMoneyAccount assetLiability; MyMoneyAccount category; MyMoneySplit taxSplit; const QString currencyId = transaction.commodity(); foreach (const auto& split, transaction.splits()) { const auto acc = account(split.accountId()); // all splits must reference accounts denoted in the same currency if (acc.currencyId() != currencyId) { return; } if (acc.isAssetLiability() && assetLiability.id().isEmpty()) { amount = split.shares(); assetLiability = acc; continue; } if (acc.isAssetLiability()) { return; } if (category.id().isEmpty() && !acc.value("VatAccount").isEmpty()) { category = acc; continue; } else if(taxSplit.id().isEmpty() && !acc.value("Tax").toLower().compare(QLatin1String("yes"))) { taxSplit = split; continue; } return; } if (!category.id().isEmpty()) { // remove a possibly found tax split - we create a new one // but only if it is the same tax category if (!taxSplit.id().isEmpty()) { if (category.value("VatAccount").compare(taxSplit.accountId())) return; transaction.removeSplit(taxSplit); } addVATSplit(transaction, assetLiability, category, amount); } } } bool MyMoneyFile::addVATSplit(MyMoneyTransaction& transaction, const MyMoneyAccount& acc, const MyMoneyAccount& category, const MyMoneyMoney& amount) const { bool rc = false; try { MyMoneySplit cat; // category MyMoneySplit tax; // tax if (category.value("VatAccount").isEmpty()) return false; MyMoneyAccount vatAcc = account(category.value("VatAccount")); const MyMoneySecurity& asec = security(acc.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 = acc.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(acc.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(acc.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 = true; if (t.splitCount() == 2) { foreach (const auto split, t.splits()) { auto acc = account(split.accountId()); if (acc.isIncomeExpense()) { rc = false; 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(); } QMap > MyMoneyFile::countTransactionsWithSpecificReconciliationState() const { QMap > result; MyMoneyTransactionFilter filter; filter.setReportAllSplits(false); d->checkStorage(); QList list; accountList(list); - for (const auto account : list) { + for (const auto& account : list) { result[account.id()] = QVector((int)eMyMoney::Split::State::MaxReconcileState, 0); } const auto transactions = d->m_storage->transactionList(filter); for (const auto& transaction : transactions) { - for (const auto& split : transaction.splits()) { + const auto& splits = transaction.splits(); + for (const auto& split : splits) { if (!result.contains(split.accountId())) { result[split.accountId()] = QVector((int)eMyMoney::Split::State::MaxReconcileState, 0); } const auto flag = split.reconcileFlag(); switch(flag) { case eMyMoney::Split::State::NotReconciled: case eMyMoney::Split::State::Cleared: case eMyMoney::Split::State::Reconciled: case eMyMoney::Split::State::Frozen: result[split.accountId()][(int)flag]++; break; default: break; } } } return result; } /** * 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); } // Don't do any rounding on a split factor if (split.action() != MyMoneySplit::actionName(eMyMoney::Split::Action::SplitShares)) { 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() { 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/mymoneystatement.cpp b/kmymoney/mymoney/mymoneystatement.cpp index c414bbb1c..bdcfcc9c8 100644 --- a/kmymoney/mymoney/mymoneystatement.cpp +++ b/kmymoney/mymoney/mymoneystatement.cpp @@ -1,393 +1,393 @@ /* * Copyright 2004-2006 Ace Jones * Copyright 2005-2018 Thomas Baumgart * Copyright 2017-2018 Łukasz Wojniłowicz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "mymoneystatement.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes namespace eMyMoney { namespace Statement { enum class Element { KMMStatement, Statement, Transaction, Split, Price, Security }; uint qHash(const Element key, uint seed) { return ::qHash(static_cast(key), seed); } enum class Attribute { Name, Symbol, ID, Version, AccountName, AccountNumber, RoutingNumber, Currency, BeginDate, EndDate, ClosingBalance, Type, AccountID, SkipCategoryMatching, DatePosted, Payee, Memo, Number, Amount, BankID, Reconcile, Action, Shares, Security, BrokerageAccount, Category, }; uint qHash(const Attribute key, uint seed) { return ::qHash(static_cast(key), seed); } } } using namespace eMyMoney; const QHash txAccountType { {Statement::Type::None, QStringLiteral("none")}, {Statement::Type::Checkings, QStringLiteral("checkings")}, {Statement::Type::Savings, QStringLiteral("savings")}, {Statement::Type::Investment, QStringLiteral("investment")}, {Statement::Type::CreditCard, QStringLiteral("creditcard")}, {Statement::Type::Invalid, QStringLiteral("invalid")} }; const QHash txAction { {Transaction::Action::None, QStringLiteral("none")}, {Transaction::Action::Buy, QStringLiteral("buy")}, {Transaction::Action::Sell, QStringLiteral("sell")}, {Transaction::Action::ReinvestDividend, QStringLiteral("reinvestdividend")}, {Transaction::Action::CashDividend, QStringLiteral("cashdividend")}, {Transaction::Action::Shrsin, QStringLiteral("add")}, {Transaction::Action::Shrsout, QStringLiteral("remove")}, {Transaction::Action::Stksplit, QStringLiteral("stocksplit")}, {Transaction::Action::Fees, QStringLiteral("fees")}, {Transaction::Action::Interest, QStringLiteral("interest")}, {Transaction::Action::Invalid, QStringLiteral("invalid")} }; QString getElName(const Statement::Element el) { static const QHash elNames { {Statement::Element::KMMStatement, QStringLiteral("KMYMONEY-STATEMENT")}, {Statement::Element::Statement, QStringLiteral("STATEMENT")}, {Statement::Element::Transaction, QStringLiteral("TRANSACTION")}, {Statement::Element::Split, QStringLiteral("SPLIT")}, {Statement::Element::Price, QStringLiteral("PRICE")}, {Statement::Element::Security, QStringLiteral("SECURITY")} }; return elNames[el]; } QString getAttrName(const Statement::Attribute attr) { static const QHash attrNames { {Statement::Attribute::Name, QStringLiteral("name")}, {Statement::Attribute::Symbol, QStringLiteral("symbol")}, {Statement::Attribute::ID, QStringLiteral("id")}, {Statement::Attribute::Version, QStringLiteral("version")}, {Statement::Attribute::AccountName, QStringLiteral("accountname")}, {Statement::Attribute::AccountNumber, QStringLiteral("accountnumber")}, {Statement::Attribute::RoutingNumber, QStringLiteral("routingnumber")}, {Statement::Attribute::Currency, QStringLiteral("currency")}, {Statement::Attribute::BeginDate, QStringLiteral("begindate")}, {Statement::Attribute::EndDate, QStringLiteral("enddate")}, {Statement::Attribute::ClosingBalance, QStringLiteral("closingbalance")}, {Statement::Attribute::Type, QStringLiteral("type")}, {Statement::Attribute::AccountID, QStringLiteral("accountid")}, {Statement::Attribute::SkipCategoryMatching, QStringLiteral("skipCategoryMatching")}, {Statement::Attribute::DatePosted, QStringLiteral("dateposted")}, {Statement::Attribute::Payee, QStringLiteral("payee")}, {Statement::Attribute::Memo, QStringLiteral("memo")}, {Statement::Attribute::Number, QStringLiteral("number")}, {Statement::Attribute::Amount, QStringLiteral("amount")}, {Statement::Attribute::BankID, QStringLiteral("bankid")}, {Statement::Attribute::Reconcile, QStringLiteral("reconcile")}, {Statement::Attribute::Action, QStringLiteral("action")}, {Statement::Attribute::Shares, QStringLiteral("shares")}, {Statement::Attribute::Security, QStringLiteral("security")}, {Statement::Attribute::BrokerageAccount, QStringLiteral("brokerageaccount")}, {Statement::Attribute::Category, QStringLiteral("version")}, }; return attrNames[attr]; } void MyMoneyStatement::write(QDomElement& _root, QDomDocument* _doc) const { QDomElement e = _doc->createElement(getElName(Statement::Element::Statement)); _root.appendChild(e); e.setAttribute(getAttrName(Statement::Attribute::Version), QStringLiteral("1.1")); e.setAttribute(getAttrName(Statement::Attribute::AccountName), m_strAccountName); e.setAttribute(getAttrName(Statement::Attribute::AccountNumber), m_strAccountNumber); e.setAttribute(getAttrName(Statement::Attribute::RoutingNumber), m_strRoutingNumber); e.setAttribute(getAttrName(Statement::Attribute::Currency), m_strCurrency); e.setAttribute(getAttrName(Statement::Attribute::BeginDate), m_dateBegin.toString(Qt::ISODate)); e.setAttribute(getAttrName(Statement::Attribute::EndDate), m_dateEnd.toString(Qt::ISODate)); e.setAttribute(getAttrName(Statement::Attribute::ClosingBalance), m_closingBalance.toString()); e.setAttribute(getAttrName(Statement::Attribute::Type), txAccountType[m_eType]); e.setAttribute(getAttrName(Statement::Attribute::AccountID), m_accountId); e.setAttribute(getAttrName(Statement::Attribute::SkipCategoryMatching), m_skipCategoryMatching); // iterate over transactions, and add each one foreach (const auto tansaction, m_listTransactions) { auto p = _doc->createElement(getElName(Statement::Element::Transaction)); p.setAttribute(getAttrName(Statement::Attribute::DatePosted), tansaction.m_datePosted.toString(Qt::ISODate)); p.setAttribute(getAttrName(Statement::Attribute::Payee), tansaction.m_strPayee); p.setAttribute(getAttrName(Statement::Attribute::Memo), tansaction.m_strMemo); p.setAttribute(getAttrName(Statement::Attribute::Number), tansaction.m_strNumber); p.setAttribute(getAttrName(Statement::Attribute::Amount), tansaction.m_amount.toString()); p.setAttribute(getAttrName(Statement::Attribute::BankID), tansaction.m_strBankID); p.setAttribute(getAttrName(Statement::Attribute::Reconcile), (int)tansaction.m_reconcile); p.setAttribute(getAttrName(Statement::Attribute::Action), txAction[tansaction.m_eAction]); if (m_eType == eMyMoney::Statement::Type::Investment) { p.setAttribute(getAttrName(Statement::Attribute::Shares), tansaction.m_shares.toString()); p.setAttribute(getAttrName(Statement::Attribute::Security), tansaction.m_strSecurity); p.setAttribute(getAttrName(Statement::Attribute::BrokerageAccount), tansaction.m_strBrokerageAccount); } // add all the splits we know of (might be empty) foreach (const auto split, tansaction.m_listSplits) { auto el = _doc->createElement(getElName(Statement::Element::Split)); el.setAttribute(getAttrName(Statement::Attribute::AccountID), split.m_accountId); el.setAttribute(getAttrName(Statement::Attribute::Amount), split.m_amount.toString()); el.setAttribute(getAttrName(Statement::Attribute::Reconcile), (int)split.m_reconcile); el.setAttribute(getAttrName(Statement::Attribute::Category), split.m_strCategoryName); el.setAttribute(getAttrName(Statement::Attribute::Memo), split.m_strMemo); el.setAttribute(getAttrName(Statement::Attribute::Reconcile), (int)split.m_reconcile); p.appendChild(el); } e.appendChild(p); } // iterate over prices, and add each one foreach (const auto price, m_listPrices) { auto p = _doc->createElement(getElName(Statement::Element::Price)); p.setAttribute(getAttrName(Statement::Attribute::DatePosted), price.m_date.toString(Qt::ISODate)); p.setAttribute(getAttrName(Statement::Attribute::Security), price.m_strSecurity); p.setAttribute(getAttrName(Statement::Attribute::Amount), price.m_amount.toString()); e.appendChild(p); } // iterate over securities, and add each one foreach (const auto security, m_listSecurities) { auto p = _doc->createElement(getElName(Statement::Element::Security)); p.setAttribute(getAttrName(Statement::Attribute::Name), security.m_strName); p.setAttribute(getAttrName(Statement::Attribute::Symbol), security.m_strSymbol); p.setAttribute(getAttrName(Statement::Attribute::ID), security.m_strId); e.appendChild(p); } } bool MyMoneyStatement::read(const QDomElement& _e) { bool result = false; if (_e.tagName() == getElName(Statement::Element::Statement)) { result = true; m_strAccountName = _e.attribute(getAttrName(Statement::Attribute::AccountName)); m_strAccountNumber = _e.attribute(getAttrName(Statement::Attribute::AccountNumber)); m_strRoutingNumber = _e.attribute(getAttrName(Statement::Attribute::RoutingNumber)); m_strCurrency = _e.attribute(getAttrName(Statement::Attribute::Currency)); m_dateBegin = QDate::fromString(_e.attribute(getAttrName(Statement::Attribute::BeginDate)), Qt::ISODate); m_dateEnd = QDate::fromString(_e.attribute(getAttrName(Statement::Attribute::EndDate)), Qt::ISODate); m_closingBalance = MyMoneyMoney(_e.attribute(getAttrName(Statement::Attribute::ClosingBalance))); m_accountId = _e.attribute(getAttrName(Statement::Attribute::AccountID)); m_skipCategoryMatching = _e.attribute(getAttrName(Statement::Attribute::SkipCategoryMatching)).isEmpty(); auto txt = _e.attribute(getAttrName(Statement::Attribute::Type), txAccountType[Statement::Type::Checkings]); m_eType = txAccountType.key(txt, m_eType); QDomNode child = _e.firstChild(); while (!child.isNull() && child.isElement()) { QDomElement c = child.toElement(); if (c.tagName() == getElName(Statement::Element::Transaction)) { MyMoneyStatement::Transaction t; t.m_datePosted = QDate::fromString(c.attribute(getAttrName(Statement::Attribute::DatePosted)), Qt::ISODate); t.m_amount = MyMoneyMoney(c.attribute(getAttrName(Statement::Attribute::Amount))); t.m_strMemo = c.attribute(getAttrName(Statement::Attribute::Memo)); t.m_strNumber = c.attribute(getAttrName(Statement::Attribute::Number)); t.m_strPayee = c.attribute(getAttrName(Statement::Attribute::Payee)); t.m_strBankID = c.attribute(getAttrName(Statement::Attribute::BankID)); t.m_reconcile = static_cast(c.attribute(getAttrName(Statement::Attribute::Reconcile)).toInt()); txt = c.attribute(getAttrName(Statement::Attribute::Action), txAction[eMyMoney::Transaction::Action::Buy]); t.m_eAction = txAction.key(txt, t.m_eAction); if (m_eType == eMyMoney::Statement::Type::Investment) { t.m_shares = MyMoneyMoney(c.attribute(getAttrName(Statement::Attribute::Shares))); t.m_strSecurity = c.attribute(getAttrName(Statement::Attribute::Security)); t.m_strBrokerageAccount = c.attribute(getAttrName(Statement::Attribute::BrokerageAccount)); } // process splits (if any) child = c.firstChild(); while (!child.isNull() && child.isElement()) { c = child.toElement(); if (c.tagName() == getElName(Statement::Element::Split)) { MyMoneyStatement::Split s; s.m_accountId = c.attribute(getAttrName(Statement::Attribute::AccountID)); s.m_amount = MyMoneyMoney(c.attribute(getAttrName(Statement::Attribute::Amount))); s.m_reconcile = static_cast(c.attribute(getAttrName(Statement::Attribute::Reconcile)).toInt()); s.m_strCategoryName = c.attribute(getAttrName(Statement::Attribute::Category)); s.m_strMemo = c.attribute(getAttrName(Statement::Attribute::Memo)); t.m_listSplits += s; } child = child.nextSibling(); } m_listTransactions += t; } else if (c.tagName() == getElName(Statement::Element::Price)) { MyMoneyStatement::Price p; p.m_date = QDate::fromString(c.attribute(getAttrName(Statement::Attribute::DatePosted)), Qt::ISODate); p.m_strSecurity = c.attribute(getAttrName(Statement::Attribute::Security)); p.m_amount = MyMoneyMoney(c.attribute(getAttrName(Statement::Attribute::Amount))); m_listPrices += p; } else if (c.tagName() == getElName(Statement::Element::Security)) { MyMoneyStatement::Security s; s.m_strName = c.attribute(getAttrName(Statement::Attribute::Name)); s.m_strSymbol = c.attribute(getAttrName(Statement::Attribute::Symbol)); s.m_strId = c.attribute(getAttrName(Statement::Attribute::ID)); m_listSecurities += s; } child = child.nextSibling(); } } return result; } bool MyMoneyStatement::isStatementFile(const QString& _filename) { // filename is considered a statement file if it contains // the tag "" in the first 20 lines. bool result = false; QFile f(_filename); if (f.open(QIODevice::ReadOnly)) { QTextStream ts(&f); auto lineCount = 20; while (!ts.atEnd() && !result && lineCount != 0) { if (ts.readLine().contains(QLatin1String(""), Qt::CaseInsensitive)) result = true; --lineCount; } f.close(); } return result; } void MyMoneyStatement::writeXMLFile(const MyMoneyStatement& _s, const QString& _filename) { static unsigned filenum = 1; auto filename = _filename; if (filename.isEmpty()) { filename = QString::fromLatin1("statement-%1%2.xml").arg((filenum < 10) ? QStringLiteral("0") : QString()).arg(filenum); filenum++; } auto doc = new QDomDocument(getElName(Statement::Element::KMMStatement)); Q_CHECK_PTR(doc); //writeStatementtoXMLDoc(_s,doc); QDomProcessingInstruction instruct = doc->createProcessingInstruction(QStringLiteral("xml"), QStringLiteral("version=\"1.0\" encoding=\"utf-8\"")); doc->appendChild(instruct); auto eroot = doc->createElement(getElName(Statement::Element::KMMStatement)); doc->appendChild(eroot); _s.write(eroot, doc); QFile g(filename); if (g.open(QIODevice::WriteOnly)) { QTextStream stream(&g); stream.setCodec("UTF-8"); stream << doc->toString(); g.close(); } delete doc; } bool MyMoneyStatement::readXMLFile(MyMoneyStatement& _s, const QString& _filename) { bool result = false; QFile f(_filename); f.open(QIODevice::ReadOnly); QDomDocument* doc = new QDomDocument; if (doc->setContent(&f, false)) { QDomElement rootElement = doc->documentElement(); if (!rootElement.isNull()) { QDomNode child = rootElement.firstChild(); while (!child.isNull() && child.isElement()) { result = true; QDomElement childElement = child.toElement(); _s.read(childElement); child = child.nextSibling(); } } } delete doc; return result; } QDate MyMoneyStatement::statementEndDate() const { if (m_dateEnd.isValid()) return m_dateEnd; QDate postDate; - for(auto t : m_listTransactions) { + for(auto& t : m_listTransactions) { if (t.m_datePosted > postDate) { postDate = t.m_datePosted; } } return postDate; } diff --git a/kmymoney/mymoney/mymoneytransaction.cpp b/kmymoney/mymoney/mymoneytransaction.cpp index 72b5ffe46..911198148 100644 --- a/kmymoney/mymoney/mymoneytransaction.cpp +++ b/kmymoney/mymoney/mymoneytransaction.cpp @@ -1,460 +1,460 @@ /* * Copyright 2000-2002 Michael Edwardes * Copyright 2001-2017 Thomas Baumgart * Copyright 2001 Felix Rodriguez * Copyright 2003 Kevin Tambascio * Copyright 2004-2005 Ace Jones * Copyright 2006 Darren Gould * * 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 "mymoneytransaction.h" #include "mymoneytransaction_p.h" // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyutils.h" #include "mymoneymoney.h" #include "mymoneyexception.h" #include "mymoneyenums.h" MyMoneyTransaction::MyMoneyTransaction() : MyMoneyObject(*new MyMoneyTransactionPrivate) { Q_D(MyMoneyTransaction); d->m_nextSplitID = 1; d->m_entryDate = QDate(); d->m_postDate = QDate(); } MyMoneyTransaction::MyMoneyTransaction(const QString &id) : MyMoneyObject(*new MyMoneyTransactionPrivate, id) { Q_D(MyMoneyTransaction); d->m_nextSplitID = 1; d->m_entryDate = QDate(); d->m_postDate = QDate(); } MyMoneyTransaction::MyMoneyTransaction(const MyMoneyTransaction& other) : MyMoneyObject(*new MyMoneyTransactionPrivate(*other.d_func()), other.id()), MyMoneyKeyValueContainer(other) { } MyMoneyTransaction::MyMoneyTransaction(const QString& id, const MyMoneyTransaction& other) : MyMoneyObject(*new MyMoneyTransactionPrivate(*other.d_func()), id), MyMoneyKeyValueContainer(other) { Q_D(MyMoneyTransaction); if (d->m_entryDate == QDate()) d->m_entryDate = QDate::currentDate(); foreach (auto split, d->m_splits) split.setTransactionId(id); } MyMoneyTransaction::~MyMoneyTransaction() { } QDate MyMoneyTransaction::entryDate() const { Q_D(const MyMoneyTransaction); return d->m_entryDate; } void MyMoneyTransaction::setEntryDate(const QDate& date) { Q_D(MyMoneyTransaction); d->m_entryDate = date; } QDate MyMoneyTransaction::postDate() const { Q_D(const MyMoneyTransaction); return d->m_postDate; } void MyMoneyTransaction::setPostDate(const QDate& date) { Q_D(MyMoneyTransaction); d->m_postDate = date; } QString MyMoneyTransaction::memo() const { Q_D(const MyMoneyTransaction); return d->m_memo; } void MyMoneyTransaction::setMemo(const QString& memo) { Q_D(MyMoneyTransaction); d->m_memo = memo; } QList MyMoneyTransaction::splits() const { Q_D(const MyMoneyTransaction); return d->m_splits; } QList& MyMoneyTransaction::splits() { Q_D(MyMoneyTransaction); return d->m_splits; } MyMoneySplit MyMoneyTransaction::firstSplit() const { Q_D(const MyMoneyTransaction); return d->m_splits.first(); } uint MyMoneyTransaction::splitCount() const { Q_D(const MyMoneyTransaction); return d->m_splits.count(); } QString MyMoneyTransaction::commodity() const { Q_D(const MyMoneyTransaction); return d->m_commodity; } void MyMoneyTransaction::setCommodity(const QString& commodityId) { Q_D(MyMoneyTransaction); d->m_commodity = commodityId; } QString MyMoneyTransaction::bankID() const { Q_D(const MyMoneyTransaction); return d->m_bankID; } void MyMoneyTransaction::setBankID(const QString& bankID) { Q_D(MyMoneyTransaction); d->m_bankID = bankID; } bool MyMoneyTransaction::operator == (const MyMoneyTransaction& right) const { Q_D(const MyMoneyTransaction); auto d2 = static_cast(right.d_func()); return (MyMoneyObject::operator==(right) && MyMoneyKeyValueContainer::operator==(right) && (d->m_commodity == d2->m_commodity) && ((d->m_memo.length() == 0 && d2->m_memo.length() == 0) || (d->m_memo == d2->m_memo)) && (d->m_splits == d2->m_splits) && (d->m_entryDate == d2->m_entryDate) && (d->m_postDate == d2->m_postDate)); } bool MyMoneyTransaction::operator != (const MyMoneyTransaction& r) const { return !(*this == r); } bool MyMoneyTransaction::operator< (const MyMoneyTransaction& r) const { return postDate() < r.postDate(); } bool MyMoneyTransaction::operator<= (const MyMoneyTransaction& r) const { return postDate() <= r.postDate(); } bool MyMoneyTransaction::operator> (const MyMoneyTransaction& r) const { return postDate() > r.postDate(); } bool MyMoneyTransaction::accountReferenced(const QString& id) const { Q_D(const MyMoneyTransaction); foreach (const auto split, d->m_splits) { if (split.accountId() == id) return true; } return false; } void MyMoneyTransaction::addSplit(MyMoneySplit &split) { if (!split.id().isEmpty()) throw MYMONEYEXCEPTION(QString::fromLatin1("Cannot add split with assigned id '%1'").arg(split.id())); if (split.accountId().isEmpty()) throw MYMONEYEXCEPTION(QString::fromLatin1("Cannot add split that does not contain an account reference")); Q_D(MyMoneyTransaction); MyMoneySplit newSplit(d->nextSplitID(), split); split = newSplit; split.setTransactionId(id()); d->m_splits.append(split); } void MyMoneyTransaction::modifySplit(const MyMoneySplit& split) { // This is the other version which allows having more splits referencing // the same account. if (split.accountId().isEmpty()) throw MYMONEYEXCEPTION_CSTRING("Cannot modify split that does not contain an account reference"); Q_D(MyMoneyTransaction); for (auto& it_split : d->m_splits) { if (split.id() == it_split.id()) { it_split = split; return; } } throw MYMONEYEXCEPTION(QString::fromLatin1("Invalid split id '%1'").arg(split.id())); } void MyMoneyTransaction::removeSplit(const MyMoneySplit& split) { Q_D(MyMoneyTransaction); for (auto end = d->m_splits.size(), i = 0; i < end; ++i) { if (split.id() == d->m_splits.at(i).id()) { d->m_splits.removeAt(i); return; } } throw MYMONEYEXCEPTION(QString::fromLatin1("Invalid split id '%1'").arg(split.id())); } void MyMoneyTransaction::removeSplits() { Q_D(MyMoneyTransaction); d->m_splits.clear(); d->m_nextSplitID = 1; } MyMoneySplit MyMoneyTransaction::splitByPayee(const QString& payeeId) const { Q_D(const MyMoneyTransaction); foreach (const auto split, d->m_splits) { if (split.payeeId() == payeeId) return split; } throw MYMONEYEXCEPTION(QString::fromLatin1("Split not found for payee '%1'").arg(QString(payeeId))); } MyMoneySplit MyMoneyTransaction::splitByAccount(const QString& accountId, const bool match) const { Q_D(const MyMoneyTransaction); foreach (const auto split, d->m_splits) { if ((match == true && split.accountId() == accountId) || (match == false && split.accountId() != accountId)) return split; } - throw MYMONEYEXCEPTION(QString::fromLatin1("Split not found for account %1%2").arg(match ? "" : "!").arg(QString(accountId))); + throw MYMONEYEXCEPTION(QString::fromLatin1("Split not found for account %1%2").arg(match ? "" : "!", accountId)); } MyMoneySplit MyMoneyTransaction::splitByAccount(const QStringList& accountIds, const bool match) const { Q_D(const MyMoneyTransaction); foreach (const auto split, d->m_splits) { if ((match == true && accountIds.contains(split.accountId())) || (match == false && !accountIds.contains(split.accountId()))) return split; } - throw MYMONEYEXCEPTION(QString::fromLatin1("Split not found for account %1%1...%2").arg(match ? "" : "!").arg(accountIds.front(), accountIds.back())); + throw MYMONEYEXCEPTION(QString::fromLatin1("Split not found for account %1%2...%3").arg(match ? "" : "!", accountIds.front(), accountIds.back())); } MyMoneySplit MyMoneyTransaction::splitById(const QString& splitId) const { Q_D(const MyMoneyTransaction); foreach (const auto split, d->m_splits) { if (split.id() == splitId) return split; } throw MYMONEYEXCEPTION(QString::fromLatin1("Split not found for id '%1'").arg(QString(splitId))); } QString MyMoneyTransaction::firstSplitID() { QString id; id = 'S' + id.setNum(1).rightJustified(MyMoneyTransactionPrivate::SPLIT_ID_SIZE, '0'); return id; } MyMoneyMoney MyMoneyTransaction::splitSum() const { MyMoneyMoney result; Q_D(const MyMoneyTransaction); foreach (const auto split, d->m_splits) result += split.value(); return result; } bool MyMoneyTransaction::isLoanPayment() const { try { Q_D(const MyMoneyTransaction); foreach (const auto split, d->m_splits) { if (split.isAmortizationSplit()) return true; } } catch (const MyMoneyException &) { } return false; } MyMoneySplit MyMoneyTransaction::amortizationSplit() const { static MyMoneySplit nullSplit; Q_D(const MyMoneyTransaction); foreach (const auto split, d->m_splits) { if (split.isAmortizationSplit() && split.isAutoCalc()) return split; } return nullSplit; } MyMoneySplit MyMoneyTransaction::interestSplit() const { static MyMoneySplit nullSplit; Q_D(const MyMoneyTransaction); foreach (const auto split, d->m_splits) { if (split.isInterestSplit() && split.isAutoCalc()) return split; } return nullSplit; } unsigned long MyMoneyTransaction::hash(const QString& txt, unsigned long h) { unsigned long g; for (int i = 0; i < txt.length(); ++i) { unsigned short uc = txt[i].unicode(); for (unsigned j = 0; j < 2; ++j) { unsigned char c = uc & 0xff; // if either the cell or the row of the Unicode char is 0, stop processing if (!c) break; h = (h << 4) + c; if ((g = (h & 0xf0000000))) { h = h ^(g >> 24); h = h ^ g; } uc >>= 8; } } return h; } bool MyMoneyTransaction::isStockSplit() const { Q_D(const MyMoneyTransaction); return (d->m_splits.count() == 1 && d->m_splits.first().action() == MyMoneySplit::actionName(eMyMoney::Split::Action::SplitShares)); } bool MyMoneyTransaction::isImported() const { return value("Imported").toLower() == QString("true"); } void MyMoneyTransaction::setImported(bool state) { if (state) setValue("Imported", "true"); else deletePair("Imported"); } bool MyMoneyTransaction::hasReferenceTo(const QString& id) const { Q_D(const MyMoneyTransaction); if (id == d->m_commodity) return true; foreach (const auto split, d->m_splits) { if (split.hasReferenceTo(id)) return true; } return false; } bool MyMoneyTransaction::hasAutoCalcSplit() const { Q_D(const MyMoneyTransaction); foreach (const auto split, d->m_splits) if (split.isAutoCalc()) return true; return false; } QString MyMoneyTransaction::accountSignature(bool includeSplitCount) const { Q_D(const MyMoneyTransaction); QMap accountList; foreach (const auto split, d->m_splits) accountList[split.accountId()] += 1; QMap::const_iterator it_a; QString rc; for (it_a = accountList.constBegin(); it_a != accountList.constEnd(); ++it_a) { if (it_a != accountList.constBegin()) rc += '-'; rc += it_a.key(); if (includeSplitCount) rc += QString("*%1").arg(*it_a); } return rc; } QString MyMoneyTransaction::uniqueSortKey() const { Q_D(const MyMoneyTransaction); QString year, month, day, key; const auto postdate = postDate(); year = year.setNum(postdate.year()).rightJustified(MyMoneyTransactionPrivate::YEAR_SIZE, '0'); month = month.setNum(postdate.month()).rightJustified(MyMoneyTransactionPrivate::MONTH_SIZE, '0'); day = day.setNum(postdate.day()).rightJustified(MyMoneyTransactionPrivate::DAY_SIZE, '0'); key = QString::fromLatin1("%1-%2-%3-%4").arg(year, month, day, d->m_id); return key; } bool MyMoneyTransaction::replaceId(const QString& newId, const QString& oldId) { auto changed = false; Q_D(MyMoneyTransaction); for (MyMoneySplit& split : d->m_splits) changed |= split.replaceId(newId, oldId); return changed; } diff --git a/kmymoney/mymoney/mymoneytransactionfilter.cpp b/kmymoney/mymoney/mymoneytransactionfilter.cpp index 17975ef34..46bb4a736 100644 --- a/kmymoney/mymoney/mymoneytransactionfilter.cpp +++ b/kmymoney/mymoney/mymoneytransactionfilter.cpp @@ -1,1012 +1,1013 @@ /* * Copyright 2003-2019 Thomas Baumgart * Copyright 2004 Ace Jones * Copyright 2008-2010 Alvaro Soliverez * Copyright 2017-2018 Łukasz Wojniłowicz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "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; } 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 = 0; extendedFilter.singleFilter.accountFilter = 0; 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 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()) + const auto& tagIdList = s.tagIdList(); + for (const auto& tag : 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()) + const auto& splits = t.splits(); + for (const auto& split : 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::validities(QList& list) const { Q_D(const MyMoneyTransactionFilter); auto result = d->m_filterSet.singleFilter.validityFilter; if (result) { QHashIterator it_validity(d->m_validity); while (it_validity.hasNext()) { it_validity.next(); list += it_validity.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::firstValidity(int&i) const { Q_D(const MyMoneyTransactionFilter); auto result = d->m_filterSet.singleFilter.validityFilter; if (result) { QHashIterator it_validity(d->m_validity); if (it_validity.hasNext()) { it_validity.next(); i = it_validity.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/mymoneystoragemgr.cpp b/kmymoney/mymoney/storage/mymoneystoragemgr.cpp index 6f2f7e9df..763f49694 100644 --- a/kmymoney/mymoney/storage/mymoneystoragemgr.cpp +++ b/kmymoney/mymoney/storage/mymoneystoragemgr.cpp @@ -1,1897 +1,1899 @@ /* * Copyright 2002-2003 Michael Edwardes * Copyright 2002-2018 Thomas Baumgart * Copyright 2004 Kevin Tambascio * Copyright 2004-2006 Ace Jones * Copyright 2006 Darren Gould * Copyright 2017-2018 Łukasz Wojniłowicz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "mymoneystoragemgr_p.h" // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyprice.h" MyMoneyStorageMgr::MyMoneyStorageMgr() : d_ptr(new MyMoneyStorageMgrPrivate(this)) { Q_D(MyMoneyStorageMgr); d->init(); } MyMoneyStorageMgr::~MyMoneyStorageMgr() { Q_D(MyMoneyStorageMgr); delete d; } MyMoneyPayee MyMoneyStorageMgr::user() const { Q_D(const MyMoneyStorageMgr); return d->m_user; } QDate MyMoneyStorageMgr::creationDate() const { Q_D(const MyMoneyStorageMgr); return d->m_creationDate; } QDate MyMoneyStorageMgr::lastModificationDate() const { Q_D(const MyMoneyStorageMgr); return d->m_lastModificationDate; } uint MyMoneyStorageMgr::currentFixVersion() const { Q_D(const MyMoneyStorageMgr); return d->m_currentFixVersion; } uint MyMoneyStorageMgr::fileFixVersion() const { Q_D(const MyMoneyStorageMgr); return d->m_fileFixVersion; } void MyMoneyStorageMgr::setUser(const MyMoneyPayee& user) { Q_D(MyMoneyStorageMgr); d->m_user = user; d->touch(); } void MyMoneyStorageMgr::setCreationDate(const QDate& val) { Q_D(MyMoneyStorageMgr); d->m_creationDate = val; d->touch(); } void MyMoneyStorageMgr::setLastModificationDate(const QDate& val) { Q_D(MyMoneyStorageMgr); d->m_lastModificationDate = val; d->m_dirty = false; } void MyMoneyStorageMgr::setFileFixVersion(uint v) { Q_D(MyMoneyStorageMgr); d->m_fileFixVersion = v; } bool MyMoneyStorageMgr::isStandardAccount(const QString& id) const { return id == MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Liability) || id == MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Asset) || id == MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Expense) || id == MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Income) || id == MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Equity); } void MyMoneyStorageMgr::setAccountName(const QString& id, const QString& name) { Q_D(MyMoneyStorageMgr); if (!isStandardAccount(id)) throw MYMONEYEXCEPTION_CSTRING("Only standard accounts can be modified using setAccountName()"); auto acc = d->m_accountList[id]; acc.setName(name); d->m_accountList.modify(acc.id(), acc); } MyMoneyAccount MyMoneyStorageMgr::account(const QString& id) const { Q_D(const MyMoneyStorageMgr); // locate the account and if present, return it's data if (d->m_accountList.find(id) != d->m_accountList.end()) { auto acc = d->m_accountList[id]; // is that needed at all? if (acc.fraction() == -1) { const auto& sec = security(acc.currencyId()); acc.fraction(sec); } return acc; } // throw an exception, if it does not exist throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown account id '%1'").arg(id)); } MyMoneyAccount MyMoneyStorageMgr::accountByName(const QString& name) const { Q_D(const MyMoneyStorageMgr); if (name.isEmpty()) return MyMoneyAccount(); QMap::ConstIterator it_a; for (it_a = d->m_accountList.begin(); it_a != d->m_accountList.end(); ++it_a) { if ((*it_a).name() == name) { return *it_a; } } throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown account '%1'").arg(name)); } void MyMoneyStorageMgr::accountList(QList& list) const { Q_D(const MyMoneyStorageMgr); foreach(const QString& accountId, d->m_accountList.keys()) { if (!isStandardAccount(accountId)) { list.append(account(accountId)); } } } void MyMoneyStorageMgr::addAccount(MyMoneyAccount& account) { Q_D(MyMoneyStorageMgr); // create the account. MyMoneyAccount newAccount(d->nextAccountID(), account); d->m_accountList.insert(newAccount.id(), newAccount); account = newAccount; } void MyMoneyStorageMgr::addPayee(MyMoneyPayee& payee) { Q_D(MyMoneyStorageMgr); // create the payee MyMoneyPayee newPayee(d->nextPayeeID(), payee); d->m_payeeList.insert(newPayee.id(), newPayee); payee = newPayee; } /** * @brief Add onlineJob to storage * @param job caller stays owner of the object, but id will be set */ void MyMoneyStorageMgr::addOnlineJob(onlineJob &job) { Q_D(MyMoneyStorageMgr); onlineJob newJob = onlineJob(d->nextOnlineJobID(), job); d->m_onlineJobList.insert(newJob.id(), newJob); job = newJob; } void MyMoneyStorageMgr::removeOnlineJob(const onlineJob& job) { Q_D(MyMoneyStorageMgr); if (!d->m_onlineJobList.contains(job.id())) throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown onlineJob '%1' should be removed.").arg(job.id())); d->m_onlineJobList.remove(job.id()); } void MyMoneyStorageMgr::modifyOnlineJob(const onlineJob &job) { Q_D(MyMoneyStorageMgr); QMap::ConstIterator iter = d->m_onlineJobList.find(job.id()); if (iter == d->m_onlineJobList.end()) throw MYMONEYEXCEPTION(QString::fromLatin1("Got unknown onlineJob '%1' for modifying").arg(job.id())); onlineJob oldJob = iter.value(); d->m_onlineJobList.modify((*iter).id(), job); } onlineJob MyMoneyStorageMgr::getOnlineJob(const QString& id) const { Q_D(const MyMoneyStorageMgr); if (d->m_onlineJobList.contains(id)) { return d->m_onlineJobList[id]; } throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown online Job '%1'").arg(id)); } ulong MyMoneyStorageMgr::onlineJobId() const { return 1; } MyMoneyPayee MyMoneyStorageMgr::payee(const QString& id) const { Q_D(const MyMoneyStorageMgr); QMap::ConstIterator it; it = d->m_payeeList.find(id); if (it == d->m_payeeList.end()) throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown payee '%1'").arg(id)); return *it; } MyMoneyPayee MyMoneyStorageMgr::payeeByName(const QString& payee) const { Q_D(const MyMoneyStorageMgr); if (payee.isEmpty()) return MyMoneyPayee::null; QMap::ConstIterator it_p; for (it_p = d->m_payeeList.begin(); it_p != d->m_payeeList.end(); ++it_p) { if ((*it_p).name() == payee) { return *it_p; } } throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown payee '%1'").arg(payee)); } void MyMoneyStorageMgr::modifyPayee(const MyMoneyPayee& payee) { Q_D(MyMoneyStorageMgr); QMap::ConstIterator it; it = d->m_payeeList.find(payee.id()); if (it == d->m_payeeList.end()) throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown payee '%1'").arg(payee.id())); d->m_payeeList.modify((*it).id(), payee); } void MyMoneyStorageMgr::removePayee(const MyMoneyPayee& payee) { Q_D(MyMoneyStorageMgr); QMap::ConstIterator it_t; QMap::ConstIterator it_s; QMap::ConstIterator it_p; it_p = d->m_payeeList.find(payee.id()); if (it_p == d->m_payeeList.end()) throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown payee '%1'").arg(payee.id())); // scan all transactions to check if the payee is still referenced for (it_t = d->m_transactionList.begin(); it_t != d->m_transactionList.end(); ++it_t) { if ((*it_t).hasReferenceTo(payee.id())) { throw MYMONEYEXCEPTION(QString::fromLatin1("Cannot remove payee that is still referenced to a %1").arg("transaction")); } } // check referential integrity in schedules for (it_s = d->m_scheduleList.begin(); it_s != d->m_scheduleList.end(); ++it_s) { if ((*it_s).hasReferenceTo(payee.id())) { throw MYMONEYEXCEPTION(QString::fromLatin1("Cannot remove payee that is still referenced to a %1").arg("schedule")); } } // remove any reference to report and/or budget d->removeReferences(payee.id()); d->m_payeeList.remove((*it_p).id()); } QList MyMoneyStorageMgr::payeeList() const { Q_D(const MyMoneyStorageMgr); return d->m_payeeList.values(); } void MyMoneyStorageMgr::addTag(MyMoneyTag& tag) { Q_D(MyMoneyStorageMgr); // create the tag MyMoneyTag newTag(d->nextTagID(), tag); d->m_tagList.insert(newTag.id(), newTag); tag = newTag; } MyMoneyTag MyMoneyStorageMgr::tag(const QString& id) const { Q_D(const MyMoneyStorageMgr); QMap::ConstIterator it; it = d->m_tagList.find(id); if (it == d->m_tagList.end()) throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown tag '%1'").arg(id)); return *it; } MyMoneyTag MyMoneyStorageMgr::tagByName(const QString& tag) const { Q_D(const MyMoneyStorageMgr); if (tag.isEmpty()) return MyMoneyTag::null; QMap::ConstIterator it_ta; for (it_ta = d->m_tagList.begin(); it_ta != d->m_tagList.end(); ++it_ta) { if ((*it_ta).name() == tag) { return *it_ta; } } throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown tag '%1'").arg(tag)); } void MyMoneyStorageMgr::modifyTag(const MyMoneyTag& tag) { Q_D(MyMoneyStorageMgr); QMap::ConstIterator it; it = d->m_tagList.find(tag.id()); if (it == d->m_tagList.end()) throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown tag '%1'").arg(tag.id())); d->m_tagList.modify((*it).id(), tag); } void MyMoneyStorageMgr::removeTag(const MyMoneyTag& tag) { Q_D(MyMoneyStorageMgr); QMap::ConstIterator it_t; QMap::ConstIterator it_s; QMap::ConstIterator it_ta; it_ta = d->m_tagList.find(tag.id()); if (it_ta == d->m_tagList.end()) throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown tag '%1'").arg(tag.id())); // scan all transactions to check if the tag is still referenced for (it_t = d->m_transactionList.begin(); it_t != d->m_transactionList.end(); ++it_t) { if ((*it_t).hasReferenceTo(tag.id())) { throw MYMONEYEXCEPTION(QString::fromLatin1("Cannot remove tag that is still referenced to a %1").arg("transaction")); } } // check referential integrity in schedules for (it_s = d->m_scheduleList.begin(); it_s != d->m_scheduleList.end(); ++it_s) { if ((*it_s).hasReferenceTo(tag.id())) { throw MYMONEYEXCEPTION(QString::fromLatin1("Cannot remove tag that is still referenced to a %1").arg("schedule")); } } // remove any reference to report and/or budget d->removeReferences(tag.id()); d->m_tagList.remove((*it_ta).id()); } QList MyMoneyStorageMgr::tagList() const { Q_D(const MyMoneyStorageMgr); return d->m_tagList.values(); } void MyMoneyStorageMgr::addAccount(MyMoneyAccount& parent, MyMoneyAccount& account) { Q_D(MyMoneyStorageMgr); QMap::ConstIterator theParent; QMap::ConstIterator theChild; theParent = d->m_accountList.find(parent.id()); if (theParent == d->m_accountList.end()) throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown parent account '%1'").arg(parent.id())); theChild = d->m_accountList.find(account.id()); if (theChild == d->m_accountList.end()) throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown child account '%1'").arg(account.id())); auto acc = *theParent; acc.addAccountId(account.id()); d->m_accountList.modify(acc.id(), acc); parent = acc; acc = *theChild; acc.setParentAccountId(parent.id()); d->m_accountList.modify(acc.id(), acc); account = acc; } void MyMoneyStorageMgr::addInstitution(MyMoneyInstitution& institution) { Q_D(MyMoneyStorageMgr); MyMoneyInstitution newInstitution(d->nextInstitutionID(), institution); d->m_institutionList.insert(newInstitution.id(), newInstitution); // return new data institution = newInstitution; } uint MyMoneyStorageMgr::transactionCount(const QString& account) const { Q_D(const MyMoneyStorageMgr); uint cnt = 0; if (account.length() == 0) { cnt = d->m_transactionList.count(); } else { // scan all transactions foreach (const auto transaction, d->m_transactionList) { // scan all splits of this transaction auto found = false; foreach (const auto split, transaction.splits()) { // is it a split in our account? if (split.accountId() == account) { // since a transaction can only have one split referencing // each account, we're done with the splits here! found = true; break; } } // if no split contains the account id, continue with the // next transaction if (!found) continue; // otherwise count it ++cnt; } } return cnt; } QMap MyMoneyStorageMgr::transactionCountMap() const { Q_D(const MyMoneyStorageMgr); QMap map; // scan all transactions foreach (const auto transaction, d->m_transactionList) { // scan all splits of this transaction foreach (const auto split, transaction.splits()) { map[split.accountId()]++; } } return map; } uint MyMoneyStorageMgr::institutionCount() const { Q_D(const MyMoneyStorageMgr); return d->m_institutionList.count(); } uint MyMoneyStorageMgr::accountCount() const { Q_D(const MyMoneyStorageMgr); return d->m_accountList.count(); } void MyMoneyStorageMgr::addTransaction(MyMoneyTransaction& transaction, bool skipAccountUpdate) { Q_D(MyMoneyStorageMgr); // 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_CSTRING("transaction already contains an id"); if (!transaction.postDate().isValid()) throw MYMONEYEXCEPTION_CSTRING("invalid post date"); // now check the splits foreach (const auto split, transaction.splits()) { // the following lines will throw an exception if the // account or payee do not exist account(split.accountId()); if (!split.payeeId().isEmpty()) payee(split.payeeId()); } MyMoneyTransaction newTransaction(d->nextTransactionID(), transaction); QString key = newTransaction.uniqueSortKey(); d->m_transactionList.insert(key, newTransaction); d->m_transactionKeys.insert(newTransaction.id(), key); transaction = newTransaction; // adjust the balance of all affected accounts foreach (const auto split, transaction.splits()) { auto acc = d->m_accountList[split.accountId()]; d->adjustBalance(acc, split, false); if (!skipAccountUpdate) { acc.touch(); } d->m_accountList.modify(acc.id(), acc); } } bool MyMoneyStorageMgr::hasActiveSplits(const QString& id) const { Q_D(const MyMoneyStorageMgr); QMap::ConstIterator it; for (it = d->m_transactionList.begin(); it != d->m_transactionList.end(); ++it) { if ((*it).accountReferenced(id)) { return true; } } return false; } MyMoneyInstitution MyMoneyStorageMgr::institution(const QString& id) const { Q_D(const MyMoneyStorageMgr); QMap::ConstIterator pos; pos = d->m_institutionList.find(id); if (pos != d->m_institutionList.end()) return *pos; throw MYMONEYEXCEPTION_CSTRING("unknown institution"); } bool MyMoneyStorageMgr::dirty() const { Q_D(const MyMoneyStorageMgr); return d->m_dirty; } void MyMoneyStorageMgr::setDirty() { Q_D(MyMoneyStorageMgr); d->m_dirty = true; } QList MyMoneyStorageMgr::institutionList() const { Q_D(const MyMoneyStorageMgr); return d->m_institutionList.values(); } void MyMoneyStorageMgr::modifyAccount(const MyMoneyAccount& account, bool skipCheck) { Q_D(MyMoneyStorageMgr); QMap::ConstIterator pos; // locate the account in the file global pool pos = d->m_accountList.find(account.id()); if (pos != d->m_accountList.end()) { // check if the new info is based on the old one. // this is the case, when the file and the id // as well as the type are equal. if (((*pos).parentAccountId() == account.parentAccountId() && ((*pos).accountType() == account.accountType() || ((*pos).isLiquidAsset() && account.isLiquidAsset()))) || skipCheck == true) { // make sure that all the referenced objects exist if (!account.institutionId().isEmpty()) institution(account.institutionId()); foreach (const auto sAccount, account.accountList()) this->account(sAccount); // update information in account list d->m_accountList.modify(account.id(), account); } else throw MYMONEYEXCEPTION_CSTRING("Invalid information for update"); } else throw MYMONEYEXCEPTION_CSTRING("Unknown account id"); } void MyMoneyStorageMgr::modifyInstitution(const MyMoneyInstitution& institution) { Q_D(MyMoneyStorageMgr); QMap::ConstIterator pos; // locate the institution in the file global pool pos = d->m_institutionList.find(institution.id()); if (pos != d->m_institutionList.end()) { d->m_institutionList.modify(institution.id(), institution); } else throw MYMONEYEXCEPTION_CSTRING("unknown institution"); } void MyMoneyStorageMgr::modifyTransaction(const MyMoneyTransaction& transaction) { Q_D(MyMoneyStorageMgr); // perform some checks to see that the transaction stuff is OK. For // now we assume that // * ids are assigned // * the pointer to the MyMoneyFile object is not 0 // * the date valid (must not be empty) // * the splits must have valid account ids // first perform all the checks if (transaction.id().isEmpty() // || transaction.file() != this || !transaction.postDate().isValid()) throw MYMONEYEXCEPTION_CSTRING("invalid transaction to be modified"); // now check the splits foreach (const auto split, transaction.splits()) { // the following lines will throw an exception if the // account or payee do not exist account(split.accountId()); if (!split.payeeId().isEmpty()) payee(split.payeeId()); foreach (const auto tagId, split.tagIdList()) { if (!tagId.isEmpty()) tag(tagId); } } // new data seems to be ok. find old version of transaction // in our pool. Throw exception if unknown. if (!d->m_transactionKeys.contains(transaction.id())) throw MYMONEYEXCEPTION_CSTRING("invalid transaction id"); QString oldKey = d->m_transactionKeys[transaction.id()]; if (!d->m_transactionList.contains(oldKey)) throw MYMONEYEXCEPTION_CSTRING("invalid transaction key"); QMap::ConstIterator it_t; it_t = d->m_transactionList.find(oldKey); if (it_t == d->m_transactionList.end()) throw MYMONEYEXCEPTION_CSTRING("invalid transaction key"); foreach (const auto split, (*it_t).splits()) { auto acc = d->m_accountList[split.accountId()]; // we only need to adjust non-investment accounts here // as for investment accounts the balance will be recalculated // after the transaction has been added. if (!acc.isInvest()) { d->adjustBalance(acc, split, true); acc.touch(); d->m_accountList.modify(acc.id(), acc); } } // remove old transaction from lists d->m_transactionList.remove(oldKey); // add new transaction to lists QString newKey = transaction.uniqueSortKey(); d->m_transactionList.insert(newKey, transaction); d->m_transactionKeys.modify(transaction.id(), newKey); // adjust account balances foreach (const auto split, transaction.splits()) { auto acc = d->m_accountList[split.accountId()]; d->adjustBalance(acc, split, false); acc.touch(); d->m_accountList.modify(acc.id(), acc); } } void MyMoneyStorageMgr::reparentAccount(MyMoneyAccount &account, MyMoneyAccount& parent) { Q_D(MyMoneyStorageMgr); d->reparentAccount(account, parent, true); } void MyMoneyStorageMgr::close() { } void MyMoneyStorageMgr::removeTransaction(const MyMoneyTransaction& transaction) { Q_D(MyMoneyStorageMgr); // first perform all the checks if (transaction.id().isEmpty()) throw MYMONEYEXCEPTION_CSTRING("invalid transaction to be deleted"); QMap::ConstIterator it_k; QMap::ConstIterator it_t; it_k = d->m_transactionKeys.find(transaction.id()); if (it_k == d->m_transactionKeys.end()) throw MYMONEYEXCEPTION_CSTRING("invalid transaction to be deleted"); it_t = d->m_transactionList.find(*it_k); if (it_t == d->m_transactionList.end()) throw MYMONEYEXCEPTION_CSTRING("invalid transaction key"); // keep a copy so that we still have the data after removal MyMoneyTransaction t(*it_t); // FIXME: check if any split is frozen and throw exception // remove the transaction from the two lists d->m_transactionList.remove(*it_k); d->m_transactionKeys.remove(transaction.id()); // scan the splits and collect all accounts that need // to be updated after the removal of this transaction foreach (const auto split, t.splits()) { auto acc = d->m_accountList[split.accountId()]; d->adjustBalance(acc, split, true); acc.touch(); d->m_accountList.modify(acc.id(), acc); } } void MyMoneyStorageMgr::removeAccount(const MyMoneyAccount& account) { Q_D(MyMoneyStorageMgr); MyMoneyAccount parent; // check that the account and it's parent exist // this will throw an exception if the id is unknown MyMoneyStorageMgr::account(account.id()); parent = MyMoneyStorageMgr::account(account.parentAccountId()); // check that it's not one of the standard account groups if (isStandardAccount(account.id())) throw MYMONEYEXCEPTION_CSTRING("Unable to remove the standard account groups"); if (hasActiveSplits(account.id())) { throw MYMONEYEXCEPTION_CSTRING("Unable to remove account with active splits"); } // re-parent all sub-ordinate accounts to the parent of the account // to be deleted. First round check that all accounts exist, second // round do the re-parenting. foreach (const auto accountID, account.accountList()) MyMoneyStorageMgr::account(accountID); // if one of the accounts did not exist, an exception had been // thrown and we would not make it until here. QMap::ConstIterator it_a; QMap::ConstIterator it_p; // locate the account in the file global pool it_a = d->m_accountList.find(account.id()); if (it_a == d->m_accountList.end()) throw MYMONEYEXCEPTION_CSTRING("Internal error: account not found in list"); it_p = d->m_accountList.find(parent.id()); if (it_p == d->m_accountList.end()) throw MYMONEYEXCEPTION_CSTRING("Internal error: parent account not found in list"); if (!account.institutionId().isEmpty()) throw MYMONEYEXCEPTION_CSTRING("Cannot remove account still attached to an institution"); d->removeReferences(account.id()); // FIXME: check referential integrity for the account to be removed // check if the new info is based on the old one. // this is the case, when the file and the id // as well as the type are equal. if ((*it_a).id() == account.id() && (*it_a).accountType() == account.accountType()) { // second round over sub-ordinate accounts: do re-parenting // but only if the list contains at least one entry // FIXME: move this logic to MyMoneyFile foreach (const auto accountID, (*it_a).accountList()) { MyMoneyAccount acc(MyMoneyStorageMgr::account(accountID)); d->reparentAccount(acc, parent, false); } // remove account from parent's list parent.removeAccountId(account.id()); d->m_accountList.modify(parent.id(), parent); // remove account from the global account pool d->m_accountList.remove(account.id()); } } void MyMoneyStorageMgr::removeInstitution(const MyMoneyInstitution& institution) { Q_D(MyMoneyStorageMgr); QMap::ConstIterator it_i; it_i = d->m_institutionList.find(institution.id()); if (it_i != d->m_institutionList.end()) { d->m_institutionList.remove(institution.id()); } else throw MYMONEYEXCEPTION_CSTRING("invalid institution"); } void MyMoneyStorageMgr::transactionList(QList& list, MyMoneyTransactionFilter& filter) const { Q_D(const MyMoneyStorageMgr); list.clear(); const auto& transactions = d->m_transactionList; for (const auto& transaction : transactions) { // This code is used now. It adds the transaction to the list for // each matching split exactly once. This allows to show information // about different splits in the same register view (e.g. search result) // // I have no idea, if this has some impact on the functionality. So far, // I could not see it. (ipwizard 9/5/2003) const auto cnt = filter.matchingSplitsCount(transaction); for (uint i = 0; i < cnt; ++i) list.append(transaction); } } void MyMoneyStorageMgr::transactionList(QList< QPair >& list, MyMoneyTransactionFilter& filter) const { Q_D(const MyMoneyStorageMgr); list.clear(); - for (const auto& transaction : d->m_transactionList) - for (const auto& split : filter.matchingSplits(transaction)) + for (const auto& transaction : d->m_transactionList) { + const auto& splits = filter.matchingSplits(transaction); + for (const auto& split : splits) list.append(qMakePair(transaction, split)); + } } QList MyMoneyStorageMgr::transactionList(MyMoneyTransactionFilter& filter) const { QList list; transactionList(list, filter); return list; } QList MyMoneyStorageMgr::onlineJobList() const { Q_D(const MyMoneyStorageMgr); return d->m_onlineJobList.values(); } QList< MyMoneyCostCenter > MyMoneyStorageMgr::costCenterList() const { Q_D(const MyMoneyStorageMgr); return d->m_costCenterList.values(); } MyMoneyCostCenter MyMoneyStorageMgr::costCenter(const QString& id) const { Q_D(const MyMoneyStorageMgr); if (!d->m_costCenterList.contains(id)) throw MYMONEYEXCEPTION(QString::fromLatin1("Invalid cost center id '%1'").arg(id)); return d->m_costCenterList[id]; } bool MyMoneyStorageMgr::isDuplicateTransaction(const QString& id) const { Q_D(const MyMoneyStorageMgr); return d->m_transactionKeys.contains(id); } MyMoneyTransaction MyMoneyStorageMgr::transaction(const QString& id) const { Q_D(const MyMoneyStorageMgr); // get the full key of this transaction, throw exception // if it's invalid (unknown) if (!d->m_transactionKeys.contains(id)) { throw MYMONEYEXCEPTION(QString::fromLatin1("Invalid transaction id '%1'").arg(id)); } // check if this key is in the list, throw exception if not QString key = d->m_transactionKeys[id]; if (!d->m_transactionList.contains(key)) throw MYMONEYEXCEPTION(QString::fromLatin1("Invalid transaction key '%1'").arg(key)); return d->m_transactionList[key]; } MyMoneyTransaction MyMoneyStorageMgr::transaction(const QString& account, const int idx) const { Q_D(const MyMoneyStorageMgr); /* removed with MyMoneyAccount::Transaction QMap::ConstIterator acc; // find account object in list, throw exception if unknown acc = m_accountList.find(account); if(acc == m_accountList.end()) throw MYMONEYEXCEPTION_CSTRING("unknown account id"); // get the transaction info from the account MyMoneyAccount::Transaction t = (*acc).transaction(idx); // return the transaction, throw exception if not found return transaction(t.transactionID()); */ // new implementation if the above code does not work anymore auto acc = d->m_accountList[account]; MyMoneyTransactionFilter filter; if (acc.accountGroup() == eMyMoney::Account::Type::Income || acc.accountGroup() == eMyMoney::Account::Type::Expense) filter.addCategory(account); else filter.addAccount(account); const auto list = transactionList(filter); if (idx < 0 || idx >= static_cast(list.count())) throw MYMONEYEXCEPTION_CSTRING("Unknown idx for transaction"); return transaction(list[idx].id()); } MyMoneyMoney MyMoneyStorageMgr::balance(const QString& id, const QDate& date) const { Q_D(const MyMoneyStorageMgr); if (!d->m_accountList.contains(id)) throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown account id '%1'").arg(id)); // the balance of all transactions for this account has // been requested. no need to calculate anything as we // have this number with the account object already. if (!date.isValid()) return d->m_accountList[id].balance(); else return d->calculateBalance(id, date); } MyMoneyMoney MyMoneyStorageMgr::totalBalance(const QString& id, const QDate& date) const { MyMoneyMoney result(balance(id, date)); foreach (const auto sAccount, account(id).accountList()) result += totalBalance(sAccount, date); return result; } MyMoneyAccount MyMoneyStorageMgr::liability() const { return account(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Liability)); } MyMoneyAccount MyMoneyStorageMgr::asset() const { return account(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Asset)); } MyMoneyAccount MyMoneyStorageMgr::expense() const { return account(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Expense)); } MyMoneyAccount MyMoneyStorageMgr::income() const { return account(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Income)); } MyMoneyAccount MyMoneyStorageMgr::equity() const { return account(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Equity)); } void MyMoneyStorageMgr::loadAccounts(const QMap& map) { Q_D(MyMoneyStorageMgr); d->m_accountList = map; // scan the map to identify the last used id d->m_nextAccountID = 0; const QRegularExpression idExp("A(\\d+)$"); auto end = map.constEnd(); for (auto iter = map.constBegin(); iter != end; ++iter) { if (!isStandardAccount((*iter).id())) { const auto id = d->extractId(idExp, (*iter).id()); if (id > d->m_nextAccountID) { d->m_nextAccountID = id; } } } } void MyMoneyStorageMgr::loadTransactions(const QMap& map) { Q_D(MyMoneyStorageMgr); d->m_transactionList = map; // now fill the key map and // identify the last used id QMap keys; d->m_nextTransactionID = 0; const QRegularExpression idExp("T(\\d+)$"); auto end = map.constEnd(); for (auto iter = map.constBegin(); iter != end; ++iter) { keys[(*iter).id()] = iter.key(); const auto id = d->extractId(idExp, (*iter).id()); if (id > d->m_nextTransactionID) { d->m_nextTransactionID = id; } } d->m_transactionKeys = keys; } void MyMoneyStorageMgr::loadInstitutions(const QMap& map) { Q_D(MyMoneyStorageMgr); d->m_institutionList = map; // scan the map to identify the last used id d->m_nextInstitutionID = 0; const QRegularExpression idExp("I(\\d+)$"); auto end = map.constEnd(); for (auto iter = map.constBegin(); iter != end; ++iter) { const auto id = d->extractId(idExp, (*iter).id()); if (id > d->m_nextInstitutionID) { d->m_nextInstitutionID = id; } } } void MyMoneyStorageMgr::loadPayees(const QMap& map) { Q_D(MyMoneyStorageMgr); d->m_payeeList = map; // scan the map to identify the last used id d->m_nextPayeeID = 0; const QRegularExpression idExp("P(\\d+)$"); auto end = map.constEnd(); for (auto iter = map.constBegin(); iter != end; ++iter) { const auto id = d->extractId(idExp, (*iter).id()); if (id > d->m_nextPayeeID) { d->m_nextPayeeID = id; } } } void MyMoneyStorageMgr::loadTags(const QMap& map) { Q_D(MyMoneyStorageMgr); d->m_tagList = map; // scan the map to identify the last used id d->m_nextTagID = 0; const QRegularExpression idExp("G(\\d+)$"); auto end = map.constEnd(); for (auto iter = map.constBegin(); iter != end; ++iter) { const auto id = d->extractId(idExp, (*iter).id()); if (id > d->m_nextTagID) { d->m_nextTagID = id; } } } void MyMoneyStorageMgr::loadSecurities(const QMap& map) { Q_D(MyMoneyStorageMgr); d->m_securitiesList = map; // scan the map to identify the last used id d->m_nextSecurityID = 0; const QRegularExpression idExp("E(\\d+)$"); auto end = map.constEnd(); for (auto iter = map.constBegin(); iter != end; ++iter) { const auto id = d->extractId(idExp, (*iter).id()); if (id > d->m_nextSecurityID) { d->m_nextSecurityID = id; } } } void MyMoneyStorageMgr::loadCurrencies(const QMap& map) { Q_D(MyMoneyStorageMgr); d->m_currencyList = map; } void MyMoneyStorageMgr::loadPrices(const MyMoneyPriceList& list) { Q_D(MyMoneyStorageMgr); d->m_priceList = list; } void MyMoneyStorageMgr::loadOnlineJobs(const QMap< QString, onlineJob >& onlineJobs) { Q_D(MyMoneyStorageMgr); d->m_onlineJobList = onlineJobs; d->m_nextOnlineJobID = 0; const QRegularExpression idExp("O(\\d+)$"); auto end = onlineJobs.constEnd(); for (auto iter = onlineJobs.constBegin(); iter != end; ++iter) { const auto id = d->extractId(idExp, (*iter).id()); if (id > d->m_nextOnlineJobID) { d->m_nextOnlineJobID = id; } } } void MyMoneyStorageMgr::loadCostCenters(const QMap< QString, MyMoneyCostCenter >& costCenters) { Q_D(MyMoneyStorageMgr); d->m_costCenterList = costCenters; // scan the map to identify the last used id d->m_nextCostCenterID = 0; const QRegularExpression idExp("C(\\d+)$"); auto end = costCenters.constEnd(); for (auto iter = costCenters.constBegin(); iter != end; ++iter) { const auto id = d->extractId(idExp, (*iter).id()); if (id > d->m_nextCostCenterID) { d->m_nextCostCenterID = id; } } } void MyMoneyStorageMgr::setValue(const QString& key, const QString& val) { Q_D(MyMoneyStorageMgr); MyMoneyKeyValueContainer::setValue(key, val); d->touch(); } void MyMoneyStorageMgr::deletePair(const QString& key) { Q_D(MyMoneyStorageMgr); MyMoneyKeyValueContainer::deletePair(key); d->touch(); } void MyMoneyStorageMgr::setPairs(const QMap& list) { Q_D(MyMoneyStorageMgr); MyMoneyKeyValueContainer::setPairs(list); d->touch(); } void MyMoneyStorageMgr::addSchedule(MyMoneySchedule& sched) { Q_D(MyMoneyStorageMgr); // first perform all the checks if (!sched.id().isEmpty()) throw MYMONEYEXCEPTION_CSTRING("schedule already contains an id"); // The following will throw an exception when it fails sched.validate(false); // it is expected in mymoneygenericstorage-test const auto splits = sched.transaction().splits(); for (const auto& split : splits) if (!d->m_accountList.contains(split.accountId())) throw MYMONEYEXCEPTION_CSTRING("bad account id"); MyMoneySchedule newSched(d->nextScheduleID(), sched); d->m_scheduleList.insert(newSched.id(), newSched); sched = newSched; } void MyMoneyStorageMgr::modifySchedule(const MyMoneySchedule& sched) { Q_D(MyMoneyStorageMgr); QMap::ConstIterator it; it = d->m_scheduleList.find(sched.id()); if (it == d->m_scheduleList.end()) throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown schedule '%1'").arg(sched.id())); d->m_scheduleList.modify(sched.id(), sched); } void MyMoneyStorageMgr::removeSchedule(const MyMoneySchedule& sched) { Q_D(MyMoneyStorageMgr); QMap::ConstIterator it; it = d->m_scheduleList.find(sched.id()); if (it == d->m_scheduleList.end()) throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown schedule '%1'").arg(sched.id())); // FIXME: check referential integrity for loan accounts d->m_scheduleList.remove(sched.id()); } MyMoneySchedule MyMoneyStorageMgr::schedule(const QString& id) const { Q_D(const MyMoneyStorageMgr); QMap::ConstIterator pos; // locate the schedule and if present, return it's data pos = d->m_scheduleList.find(id); if (pos != d->m_scheduleList.end()) return (*pos); // throw an exception, if it does not exist throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown schedule '%1'").arg(id)); } QList MyMoneyStorageMgr::scheduleList(const QString& accountId, eMyMoney::Schedule::Type type, eMyMoney::Schedule::Occurrence occurrence, eMyMoney::Schedule::PaymentType paymentType, const QDate& startDate, const QDate& endDate, bool overdue) const { Q_D(const MyMoneyStorageMgr); QMap::ConstIterator pos; QList list; // qDebug("scheduleList()"); for (pos = d->m_scheduleList.begin(); pos != d->m_scheduleList.end(); ++pos) { // qDebug(" '%s'", qPrintable((*pos).id())); if (type != eMyMoney::Schedule::Type::Any) { if (type != (*pos).type()) { continue; } } if (occurrence != eMyMoney::Schedule::Occurrence::Any) { if (occurrence != (*pos).occurrence()) { continue; } } if (paymentType != eMyMoney::Schedule::PaymentType::Any) { if (paymentType != (*pos).paymentType()) { continue; } } if (!accountId.isEmpty()) { MyMoneyTransaction t = (*pos).transaction(); QList::ConstIterator it; QList splits; splits = t.splits(); for (it = splits.constBegin(); it != splits.constEnd(); ++it) { if ((*it).accountId() == accountId) break; } if (it == splits.constEnd()) { continue; } } if (startDate.isValid() && endDate.isValid()) { if ((*pos).paymentDates(startDate, endDate).count() == 0) { continue; } } if (startDate.isValid() && !endDate.isValid()) { if (!(*pos).nextPayment(startDate.addDays(-1)).isValid()) { continue; } } if (!startDate.isValid() && endDate.isValid()) { if ((*pos).startDate() > endDate) { continue; } } if (overdue) { if (!(*pos).isOverdue()) continue; } // qDebug("Adding '%s'", (*pos).name().toLatin1()); list << *pos; } return list; } void MyMoneyStorageMgr::loadSchedules(const QMap& map) { Q_D(MyMoneyStorageMgr); d->m_scheduleList = map; // scan the map to identify the last used id d->m_nextScheduleID = 0; const QRegularExpression idExp("SCH(\\d+)$"); auto end = map.constEnd(); for (auto iter = map.constBegin(); iter != end; ++iter) { const auto id = d->extractId(idExp, (*iter).id()); if (id > d->m_nextScheduleID) { d->m_nextScheduleID = id; } } } QList MyMoneyStorageMgr::scheduleListEx(int scheduleTypes, int scheduleOcurrences, int schedulePaymentTypes, QDate date, const QStringList& accounts) const { Q_D(const MyMoneyStorageMgr); // qDebug("scheduleListEx"); QMap::ConstIterator pos; QList list; if (!date.isValid()) return list; for (pos = d->m_scheduleList.begin(); pos != d->m_scheduleList.end(); ++pos) { if (scheduleTypes && !(scheduleTypes & (int)(*pos).type())) continue; if (scheduleOcurrences && !(scheduleOcurrences & (int)(*pos).occurrence())) continue; if (schedulePaymentTypes && !(schedulePaymentTypes & (int)(*pos).paymentType())) continue; if ((*pos).paymentDates(date, date).count() == 0) continue; if ((*pos).isFinished()) continue; if ((*pos).hasRecordedPayment(date)) continue; if (accounts.count() > 0) { if (accounts.contains((*pos).account().id())) continue; } // qDebug("\tAdding '%s'", (*pos).name().toLatin1()); list << *pos; } return list; } void MyMoneyStorageMgr::addSecurity(MyMoneySecurity& security) { Q_D(MyMoneyStorageMgr); // create the account MyMoneySecurity newSecurity(d->nextSecurityID(), security); d->m_securitiesList.insert(newSecurity.id(), newSecurity); security = newSecurity; } void MyMoneyStorageMgr::modifySecurity(const MyMoneySecurity& security) { Q_D(MyMoneyStorageMgr); QMap::ConstIterator it; it = d->m_securitiesList.find(security.id()); if (it == d->m_securitiesList.end()) throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown security '%1'").arg(security.id())); d->m_securitiesList.modify(security.id(), security); } void MyMoneyStorageMgr::removeSecurity(const MyMoneySecurity& security) { Q_D(MyMoneyStorageMgr); QMap::ConstIterator it; // FIXME: check referential integrity it = d->m_securitiesList.find(security.id()); if (it == d->m_securitiesList.end()) throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown security '%1'").arg(security.id())); d->m_securitiesList.remove(security.id()); } MyMoneySecurity MyMoneyStorageMgr::security(const QString& id) const { Q_D(const MyMoneyStorageMgr); QMap::ConstIterator it = d->m_securitiesList.find(id); if (it != d->m_securitiesList.end()) { return it.value(); } // FIXME: in places where a currency is needed, a currency method should be called even if the currency is in fact a security it = d->m_currencyList.find(id); if (it != d->m_currencyList.end()) { return it.value(); } return MyMoneySecurity(); } QList MyMoneyStorageMgr::securityList() const { Q_D(const MyMoneyStorageMgr); //qDebug("securityList: Security list size is %d, this=%8p", m_equitiesList.size(), (void*)this); return d->m_securitiesList.values(); } void MyMoneyStorageMgr::addCurrency(const MyMoneySecurity& currency) { Q_D(MyMoneyStorageMgr); QMap::ConstIterator it; it = d->m_currencyList.find(currency.id()); if (it != d->m_currencyList.end()) throw MYMONEYEXCEPTION(QString::fromLatin1("Cannot add currency with existing id %1").arg(currency.id())); d->m_currencyList.insert(currency.id(), currency); } void MyMoneyStorageMgr::modifyCurrency(const MyMoneySecurity& currency) { Q_D(MyMoneyStorageMgr); QMap::ConstIterator it; it = d->m_currencyList.find(currency.id()); if (it == d->m_currencyList.end()) throw MYMONEYEXCEPTION(QString::fromLatin1("Cannot modify currency with unknown id %1").arg(currency.id())); d->m_currencyList.modify(currency.id(), currency); } void MyMoneyStorageMgr::removeCurrency(const MyMoneySecurity& currency) { Q_D(MyMoneyStorageMgr); QMap::ConstIterator it; // FIXME: check referential integrity it = d->m_currencyList.find(currency.id()); if (it == d->m_currencyList.end()) throw MYMONEYEXCEPTION(QString::fromLatin1("Cannot remove currency with unknown id %1").arg(currency.id())); d->m_currencyList.remove(currency.id()); } MyMoneySecurity MyMoneyStorageMgr::currency(const QString& id) const { Q_D(const MyMoneyStorageMgr); if (id.isEmpty()) { } QMap::ConstIterator it; it = d->m_currencyList.find(id); if (it == d->m_currencyList.end()) throw MYMONEYEXCEPTION(QString::fromLatin1("Cannot retrieve currency with unknown id '%1'").arg(id)); return *it; } QList MyMoneyStorageMgr::currencyList() const { Q_D(const MyMoneyStorageMgr); return d->m_currencyList.values(); } QList MyMoneyStorageMgr::reportList() const { Q_D(const MyMoneyStorageMgr); return d->m_reportList.values(); } void MyMoneyStorageMgr::addReport(MyMoneyReport& report) { Q_D(MyMoneyStorageMgr); if (!report.id().isEmpty()) throw MYMONEYEXCEPTION(QString::fromLatin1("report already contains an id")); MyMoneyReport newReport(d->nextReportID(), report); d->m_reportList.insert(newReport.id(), newReport); report = newReport; } void MyMoneyStorageMgr::loadReports(const QMap& map) { Q_D(MyMoneyStorageMgr); d->m_reportList = map; // scan the map to identify the last used id d->m_nextReportID = 0; const QRegularExpression idExp("R(\\d+)$"); auto end = map.constEnd(); for (auto iter = map.constBegin(); iter != end; ++iter) { const auto id = d->extractId(idExp, (*iter).id()); if (id > d->m_nextReportID) { d->m_nextReportID = id; } } } void MyMoneyStorageMgr::modifyReport(const MyMoneyReport& report) { Q_D(MyMoneyStorageMgr); QMap::ConstIterator it; it = d->m_reportList.find(report.id()); if (it == d->m_reportList.end()) throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown report '%1'").arg(report.id())); d->m_reportList.modify(report.id(), report); } uint MyMoneyStorageMgr::countReports() const { Q_D(const MyMoneyStorageMgr); return d->m_reportList.count(); } MyMoneyReport MyMoneyStorageMgr::report(const QString& _id) const { Q_D(const MyMoneyStorageMgr); return d->m_reportList[_id]; } void MyMoneyStorageMgr::removeReport(const MyMoneyReport& report) { Q_D(MyMoneyStorageMgr); QMap::ConstIterator it; it = d->m_reportList.find(report.id()); if (it == d->m_reportList.end()) throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown report '%1'").arg(report.id())); d->m_reportList.remove(report.id()); } QList MyMoneyStorageMgr::budgetList() const { Q_D(const MyMoneyStorageMgr); return d->m_budgetList.values(); } void MyMoneyStorageMgr::addBudget(MyMoneyBudget& budget) { Q_D(MyMoneyStorageMgr); MyMoneyBudget newBudget(d->nextBudgetID(), budget); d->m_budgetList.insert(newBudget.id(), newBudget); budget = newBudget; } void MyMoneyStorageMgr::loadBudgets(const QMap& map) { Q_D(MyMoneyStorageMgr); d->m_budgetList = map; // scan the map to identify the last used id d->m_nextBudgetID = 0; const QRegularExpression idExp("B(\\d+)$"); auto end = map.constEnd(); for (auto iter = map.constBegin(); iter != end; ++iter) { const auto id = d->extractId(idExp, (*iter).id()); if (id > d->m_nextBudgetID) { d->m_nextBudgetID = id; } } } MyMoneyBudget MyMoneyStorageMgr::budgetByName(const QString& budget) const { Q_D(const MyMoneyStorageMgr); QMap::ConstIterator it_p; for (it_p = d->m_budgetList.begin(); it_p != d->m_budgetList.end(); ++it_p) { if ((*it_p).name() == budget) { return *it_p; } } throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown budget '%1'").arg(budget)); } void MyMoneyStorageMgr::modifyBudget(const MyMoneyBudget& budget) { Q_D(MyMoneyStorageMgr); QMap::ConstIterator it; it = d->m_budgetList.find(budget.id()); if (it == d->m_budgetList.end()) throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown budget '%1'").arg(budget.id())); d->m_budgetList.modify(budget.id(), budget); } uint MyMoneyStorageMgr::countBudgets() const { Q_D(const MyMoneyStorageMgr); return d->m_budgetList.count(); } MyMoneyBudget MyMoneyStorageMgr::budget(const QString& _id) const { Q_D(const MyMoneyStorageMgr); return d->m_budgetList[_id]; } void MyMoneyStorageMgr::removeBudget(const MyMoneyBudget& budget) { Q_D(MyMoneyStorageMgr); QMap::ConstIterator it; it = d->m_budgetList.find(budget.id()); if (it == d->m_budgetList.end()) throw MYMONEYEXCEPTION(QString::fromLatin1("Unknown budget '%1'").arg(budget.id())); d->m_budgetList.remove(budget.id()); } void MyMoneyStorageMgr::addPrice(const MyMoneyPrice& price) { Q_D(MyMoneyStorageMgr); MyMoneySecurityPair pricePair(price.from(), price.to()); QMap::ConstIterator it_m; it_m = d->m_priceList.find(pricePair); MyMoneyPriceEntries entries; if (it_m != d->m_priceList.end()) { entries = (*it_m); } // entries contains the current entries for this security pair // in case it_m points to m_priceList.end() we need to create a // new entry in the priceList, otherwise we need to modify // an existing one. MyMoneyPriceEntries::ConstIterator it; it = entries.constFind(price.date()); if (it != entries.constEnd()) { if ((*it).rate(QString()) == price.rate(QString()) && (*it).source() == price.source()) // in case the information did not change, we don't do anything return; } // store new value in local copy entries[price.date()] = price; if (it_m != d->m_priceList.end()) { d->m_priceList.modify(pricePair, entries); } else { d->m_priceList.insert(pricePair, entries); } } void MyMoneyStorageMgr::removePrice(const MyMoneyPrice& price) { Q_D(MyMoneyStorageMgr); MyMoneySecurityPair pricePair(price.from(), price.to()); QMap::ConstIterator it_m; it_m = d->m_priceList.find(pricePair); MyMoneyPriceEntries entries; if (it_m != d->m_priceList.end()) { entries = (*it_m); } // store new value in local copy entries.remove(price.date()); if (entries.count() != 0) { d->m_priceList.modify(pricePair, entries); } else { d->m_priceList.remove(pricePair); } } MyMoneyPriceList MyMoneyStorageMgr::priceList() const { Q_D(const MyMoneyStorageMgr); MyMoneyPriceList list; d->m_priceList.map(list); return list; } MyMoneyPrice MyMoneyStorageMgr::price(const QString& fromId, const QString& toId, const QDate& _date, bool exactDate) const { Q_D(const MyMoneyStorageMgr); // if the caller selected an exact entry, we can search for it using the date as the key QMap::const_iterator itm = d->m_priceList.find(qMakePair(fromId, toId)); if (itm != d->m_priceList.end()) { // if no valid date is passed, we use today's date. const QDate &date = _date.isValid() ? _date : QDate::currentDate(); const MyMoneyPriceEntries &entries = itm.value(); // regardless of the exactDate flag if the exact date is present return it's value since it's the correct value MyMoneyPriceEntries::const_iterator it = entries.find(date); if (it != entries.end()) return it.value(); // the exact date was not found look for the latest date before the requested date if the flag allows it if (!exactDate && !entries.empty()) { // if there are entries get the lower bound of the date it = entries.lowerBound(date); // since lower bound returns the first item with a larger key (we already know that key is not present) // if it's not the first item then we need to return the previous item (the map is not empty so there is one) if (it != entries.begin()) { return (--it).value(); } } } return MyMoneyPrice(); } void MyMoneyStorageMgr::rebuildAccountBalances() { Q_D(MyMoneyStorageMgr); // reset the balance of all accounts to 0 QMap map; d->m_accountList.map(map); QMap::iterator it_a; for (it_a = map.begin(); it_a != map.end(); ++it_a) { (*it_a).setBalance(MyMoneyMoney()); } // now scan over all transactions and all splits and setup the balances foreach (const auto transaction, d->m_transactionList) { foreach (const auto split, transaction.splits()) { if (!split.shares().isZero()) { const QString& id = split.accountId(); // locate the account and if present, update data if (map.find(id) != map.end()) { map[id].adjustBalance(split); } } } } d->m_accountList = map; } bool MyMoneyStorageMgr::isReferenced(const MyMoneyObject& obj, const QBitArray& skipCheck) const { Q_D(const MyMoneyStorageMgr); Q_ASSERT(skipCheck.count() == (int)Reference::Count); // We delete all references in reports when an object // is deleted, so we don't need to check here. See // MyMoneyStorageMgr::removeReferences(). In case // you miss the report checks in the following lines ;) const auto& id = obj.id(); // FIXME optimize the list of objects we have to checks // with a bit of knowledge of the internal structure, we // could optimize the number of objects we check for references // Scan all engine objects for a reference if (!skipCheck.testBit((int)Reference::Transaction)) foreach (const auto it, d->m_transactionList) if (it.hasReferenceTo(id)) return true; if (!skipCheck.testBit((int)Reference::Account)) foreach (const auto it, d->m_accountList) if (it.hasReferenceTo(id)) return true; if (!skipCheck.testBit((int)Reference::Institution)) foreach (const auto it, d->m_institutionList) if (it.hasReferenceTo(id)) return true; if (!skipCheck.testBit((int)Reference::Payee)) foreach (const auto it, d->m_payeeList) if (it.hasReferenceTo(id)) return true; if (!skipCheck.testBit((int)Reference::Tag)) foreach (const auto it, d->m_tagList) if (it.hasReferenceTo(id)) return true; if (!skipCheck.testBit((int)Reference::Budget)) foreach (const auto it, d->m_budgetList) if (it.hasReferenceTo(id)) return true; if (!skipCheck.testBit((int)Reference::Schedule)) foreach (const auto it, d->m_scheduleList) if (it.hasReferenceTo(id)) return true; if (!skipCheck.testBit((int)Reference::Security)) foreach (const auto it, d->m_securitiesList) if (it.hasReferenceTo(id)) return true; if (!skipCheck.testBit((int)Reference::Currency)) foreach (const auto it, d->m_currencyList) if (it.hasReferenceTo(id)) return true; // within the pricelist we don't have to scan each entry. Checking the QPair // members of the MyMoneySecurityPair is enough as they are identical to the // two security ids if (!skipCheck.testBit((int)Reference::Price)) { for (auto it_pr = d->m_priceList.begin(); it_pr != d->m_priceList.end(); ++it_pr) { if ((it_pr.key().first == id) || (it_pr.key().second == id)) return true; } } return false; } void MyMoneyStorageMgr::startTransaction() { Q_D(MyMoneyStorageMgr); d->m_payeeList.startTransaction(&d->m_nextPayeeID); d->m_tagList.startTransaction(&d->m_nextTagID); d->m_institutionList.startTransaction(&d->m_nextInstitutionID); d->m_accountList.startTransaction(&d->m_nextPayeeID); d->m_transactionList.startTransaction(&d->m_nextTransactionID); d->m_transactionKeys.startTransaction(); d->m_scheduleList.startTransaction(&d->m_nextScheduleID); d->m_securitiesList.startTransaction(&d->m_nextSecurityID); d->m_currencyList.startTransaction(); d->m_reportList.startTransaction(&d->m_nextReportID); d->m_budgetList.startTransaction(&d->m_nextBudgetID); d->m_priceList.startTransaction(); d->m_onlineJobList.startTransaction(&d->m_nextOnlineJobID); } bool MyMoneyStorageMgr::commitTransaction() { Q_D(MyMoneyStorageMgr); bool rc = false; rc |= d->m_payeeList.commitTransaction(); rc |= d->m_tagList.commitTransaction(); rc |= d->m_institutionList.commitTransaction(); rc |= d->m_accountList.commitTransaction(); rc |= d->m_transactionList.commitTransaction(); rc |= d->m_transactionKeys.commitTransaction(); rc |= d->m_scheduleList.commitTransaction(); rc |= d->m_securitiesList.commitTransaction(); rc |= d->m_currencyList.commitTransaction(); rc |= d->m_reportList.commitTransaction(); rc |= d->m_budgetList.commitTransaction(); rc |= d->m_priceList.commitTransaction(); rc |= d->m_onlineJobList.commitTransaction(); // if there was a change, touch the whole storage object if (rc) d->touch(); return rc; } void MyMoneyStorageMgr::rollbackTransaction() { Q_D(MyMoneyStorageMgr); d->m_payeeList.rollbackTransaction(); d->m_tagList.rollbackTransaction(); d->m_institutionList.rollbackTransaction(); d->m_accountList.rollbackTransaction(); d->m_transactionList.rollbackTransaction(); d->m_transactionKeys.rollbackTransaction(); d->m_scheduleList.rollbackTransaction(); d->m_securitiesList.rollbackTransaction(); d->m_currencyList.rollbackTransaction(); d->m_reportList.rollbackTransaction(); d->m_budgetList.rollbackTransaction(); d->m_priceList.rollbackTransaction(); d->m_onlineJobList.rollbackTransaction(); } diff --git a/kmymoney/mymoney/tests/mymoneyaccount-test.cpp b/kmymoney/mymoney/tests/mymoneyaccount-test.cpp index bafb09d0e..8bd92ff61 100644 --- a/kmymoney/mymoney/tests/mymoneyaccount-test.cpp +++ b/kmymoney/mymoney/tests/mymoneyaccount-test.cpp @@ -1,398 +1,397 @@ /* * Copyright 2002-2017 Thomas Baumgart * Copyright 2004 Kevin Tambascio * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * 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 "mymoneyaccount-test.h" #include #define KMM_MYMONEY_UNIT_TESTABLE friend class MyMoneyAccountTest; #include "mymoneymoney.h" #include "mymoneyaccount.h" #include "mymoneyaccount_p.h" #include "mymoneysplit.h" #include "mymoneyexception.h" #include "mymoneyenums.h" QTEST_GUILESS_MAIN(MyMoneyAccountTest) void MyMoneyAccountTest::init() { } void MyMoneyAccountTest::cleanup() { } void MyMoneyAccountTest::testEmptyConstructor() { MyMoneyAccount a; QVERIFY(a.id().isEmpty()); QVERIFY(a.name().isEmpty()); QVERIFY(a.accountType() == eMyMoney::Account::Type::Unknown); QVERIFY(a.openingDate() == QDate()); QVERIFY(a.lastModified() == QDate()); QVERIFY(a.lastReconciliationDate() == QDate()); QVERIFY(a.accountList().count() == 0); QVERIFY(a.balance().isZero()); } void MyMoneyAccountTest::testConstructor() { QString id = "A000001"; - QString institutionid = "B000001"; QString parent = "Parent"; MyMoneyAccount r; MyMoneySplit s; r.setAccountType(eMyMoney::Account::Type::Asset); r.setOpeningDate(QDate::currentDate()); r.setLastModified(QDate::currentDate()); r.setDescription("Desc"); r.setNumber("465500"); r.setParentAccountId(parent); r.setValue(QString("key"), "value"); s.setShares(MyMoneyMoney::ONE); r.adjustBalance(s); QVERIFY(r.pairs().count() == 1); QVERIFY(r.value("key") == "value"); MyMoneyAccount a(id, r); QVERIFY(a.id() == id); QVERIFY(a.institutionId().isEmpty()); QVERIFY(a.accountType() == eMyMoney::Account::Type::Asset); QVERIFY(a.openingDate() == QDate::currentDate()); QVERIFY(a.lastModified() == QDate::currentDate()); QVERIFY(a.number() == "465500"); QVERIFY(a.description() == "Desc"); QVERIFY(a.accountList().count() == 0); QVERIFY(a.parentAccountId() == "Parent"); QVERIFY(a.balance() == MyMoneyMoney::ONE); QMap copy; copy = r.pairs(); QVERIFY(copy.count() == 1); QVERIFY(copy[QString("key")] == "value"); } void MyMoneyAccountTest::testSetFunctions() { MyMoneyAccount a; QDate today(QDate::currentDate()); QVERIFY(a.name().isEmpty()); QVERIFY(a.lastModified() == QDate()); QVERIFY(a.description().isEmpty()); a.setName("Account"); a.setInstitutionId("Institution1"); a.setLastModified(today); a.setDescription("Desc"); a.setNumber("123456"); a.setAccountType(eMyMoney::Account::Type::MoneyMarket); QVERIFY(a.name() == "Account"); QVERIFY(a.institutionId() == "Institution1"); QVERIFY(a.lastModified() == today); QVERIFY(a.description() == "Desc"); QVERIFY(a.number() == "123456"); QVERIFY(a.accountType() == eMyMoney::Account::Type::MoneyMarket); } void MyMoneyAccountTest::testCopyConstructor() { QString id = "A000001"; QString institutionid = "B000001"; QString parent = "ParentAccount"; MyMoneyAccount r; r.setAccountType(eMyMoney::Account::Type::Expense); r.setOpeningDate(QDate::currentDate()); r.setLastModified(QDate::currentDate()); r.setName("Account"); r.setInstitutionId("Inst1"); r.setDescription("Desc1"); r.setNumber("Number"); r.setParentAccountId(parent); r.setValue("Key", "Value"); MyMoneyAccount a(id, r); a.setInstitutionId(institutionid); MyMoneyAccount b(a); QVERIFY(b.name() == "Account"); QVERIFY(b.institutionId() == institutionid); QVERIFY(b.accountType() == eMyMoney::Account::Type::Expense); QVERIFY(b.lastModified() == QDate::currentDate()); QVERIFY(b.openingDate() == QDate::currentDate()); QVERIFY(b.description() == "Desc1"); QVERIFY(b.number() == "Number"); QVERIFY(b.parentAccountId() == "ParentAccount"); QVERIFY(b.value("Key") == "Value"); } void MyMoneyAccountTest::testAssignmentConstructor() { MyMoneyAccount a; a.setAccountType(eMyMoney::Account::Type::Checkings); a.setName("Account"); a.setInstitutionId("Inst1"); a.setDescription("Bla"); a.setNumber("assigned Number"); a.setValue("Key", "Value"); a.addAccountId("ChildAccount"); MyMoneyAccount b; b.setLastModified(QDate::currentDate()); b = a; QVERIFY(b.name() == "Account"); QVERIFY(b.institutionId() == "Inst1"); QVERIFY(b.accountType() == eMyMoney::Account::Type::Checkings); QVERIFY(b.lastModified() == QDate()); QVERIFY(b.openingDate() == a.openingDate()); QVERIFY(b.description() == "Bla"); QVERIFY(b.number() == "assigned Number"); QVERIFY(b.value("Key") == "Value"); QVERIFY(b.accountList().count() == 1); QVERIFY(b.accountList()[0] == "ChildAccount"); } void MyMoneyAccountTest::testAdjustBalance() { MyMoneyAccount a; MyMoneySplit s; s.setShares(MyMoneyMoney(3, 1)); a.adjustBalance(s); QVERIFY(a.balance() == MyMoneyMoney(3, 1)); s.setShares(MyMoneyMoney(5, 1)); a.adjustBalance(s, true); QVERIFY(a.balance() == MyMoneyMoney(-2, 1)); s.setShares(MyMoneyMoney(2, 1)); s.setAction(MyMoneySplit::actionName(eMyMoney::Split::Action::SplitShares)); a.adjustBalance(s); QVERIFY(a.balance() == MyMoneyMoney(-4, 1)); s.setShares(MyMoneyMoney(4, 1)); s.setAction(QString()); a.adjustBalance(s); QVERIFY(a.balance().isZero()); } void MyMoneyAccountTest::testSubAccounts() { MyMoneyAccount a; a.setAccountType(eMyMoney::Account::Type::Checkings); a.addAccountId("Subaccount1"); QVERIFY(a.accountList().count() == 1); a.addAccountId("Subaccount1"); QVERIFY(a.accountList().count() == 1); a.addAccountId("Subaccount2"); QVERIFY(a.accountList().count() == 2); } void MyMoneyAccountTest::testEquality() { MyMoneyAccount a; a.setLastModified(QDate::currentDate()); a.setName("Name"); a.setNumber("Number"); a.setDescription("Desc"); a.setInstitutionId("I-ID"); a.setOpeningDate(QDate::currentDate()); a.setLastReconciliationDate(QDate::currentDate()); a.setAccountType(eMyMoney::Account::Type::Asset); a.setParentAccountId("P-ID"); a.d_func()->setId("A-ID"); a.setCurrencyId("C-ID"); a.setValue("Key", "Value"); MyMoneyAccount b; b = a; QVERIFY(b == a); a.setName("Noname"); QVERIFY(!(b == a)); b = a; a.setLastModified(QDate::currentDate().addDays(-1)); QVERIFY(!(b == a)); b = a; a.setNumber("Nonumber"); QVERIFY(!(b == a)); b = a; a.setDescription("NoDesc"); QVERIFY(!(b == a)); b = a; a.setInstitutionId("I-noID"); QVERIFY(!(b == a)); b = a; a.setOpeningDate(QDate::currentDate().addDays(-1)); QVERIFY(!(b == a)); b = a; a.setLastReconciliationDate(QDate::currentDate().addDays(-1)); QVERIFY(!(b == a)); b = a; a.setAccountType(eMyMoney::Account::Type::Liability); QVERIFY(!(b == a)); b = a; a.setParentAccountId("P-noID"); QVERIFY(!(b == a)); b = a; a.d_func()->setId("A-noID"); QVERIFY(!(b == a)); b = a; a.setCurrencyId("C-noID"); QVERIFY(!(b == a)); b = a; a.setValue("Key", "noValue"); QVERIFY(!(b == a)); b = a; a.setValue("noKey", "Value"); QVERIFY(!(b == a)); b = a; } void MyMoneyAccountTest::testHasReferenceTo() { MyMoneyAccount a; a.setInstitutionId("I0001"); a.addAccountId("A_001"); a.addAccountId("A_002"); a.setParentAccountId("A_Parent"); a.setCurrencyId("Currency"); QVERIFY(a.hasReferenceTo("I0001") == true); QVERIFY(a.hasReferenceTo("I0002") == false); QVERIFY(a.hasReferenceTo("A_001") == false); QVERIFY(a.hasReferenceTo("A_Parent") == true); QVERIFY(a.hasReferenceTo("Currency") == true); } void MyMoneyAccountTest::testSetClosed() { MyMoneyAccount a; QVERIFY(a.isClosed() == false); a.setClosed(true); QVERIFY(a.isClosed() == true); a.setClosed(false); QVERIFY(a.isClosed() == false); } void MyMoneyAccountTest::specialAccountTypes_data() { QTest::addColumn("accountType"); QTest::addColumn("incomeExpense"); QTest::addColumn("assetLibility"); QTest::addColumn("loan"); // positive and null is debit QTest::newRow("unknown") << eMyMoney::Account::Type::Unknown << false << false << false; QTest::newRow("checking") << eMyMoney::Account::Type::Checkings << false << true << false; QTest::newRow("savings") << eMyMoney::Account::Type::Savings << false << true << false; QTest::newRow("cash") << eMyMoney::Account::Type::Cash << false << true << false; QTest::newRow("investment") << eMyMoney::Account::Type::Investment << false << true << false; QTest::newRow("asset") << eMyMoney::Account::Type::Asset << false << true << false; QTest::newRow("currency") << eMyMoney::Account::Type::Currency << false << true << false; QTest::newRow("expense") << eMyMoney::Account::Type::Expense << true << false << false; QTest::newRow("moneymarket") << eMyMoney::Account::Type::MoneyMarket << false << true << false; QTest::newRow("certificatedeposit") << eMyMoney::Account::Type::CertificateDep << false << true << false; QTest::newRow("assetloan") << eMyMoney::Account::Type::AssetLoan << false << true << true; QTest::newRow("stock") << eMyMoney::Account::Type::Stock << false << true << false; QTest::newRow("creditcard") << eMyMoney::Account::Type::CreditCard << false << true << false; QTest::newRow("loan") << eMyMoney::Account::Type::Loan << false << true << true; QTest::newRow("liability") << eMyMoney::Account::Type::Liability << false << true << false; QTest::newRow("income") << eMyMoney::Account::Type::Income << true << false << false; QTest::newRow("equity") << eMyMoney::Account::Type::Equity << false << false << false; } void MyMoneyAccountTest::specialAccountTypes() { QFETCH(eMyMoney::Account::Type, accountType); QFETCH(bool, incomeExpense); QFETCH(bool, assetLibility); QFETCH(bool, loan); MyMoneyAccount a; a.setAccountType(accountType); QCOMPARE(a.isIncomeExpense(), incomeExpense); QCOMPARE(a.isAssetLiability(), assetLibility); QCOMPARE(a.isLoan(), loan); } void MyMoneyAccountTest::addReconciliation() { MyMoneyAccount a; QVERIFY(a.addReconciliation(QDate(2011, 1, 2), MyMoneyMoney(123, 100)) == true); QVERIFY(a.reconciliationHistory().count() == 1); QVERIFY(a.addReconciliation(QDate(2011, 2, 1), MyMoneyMoney(456, 100)) == true); QVERIFY(a.reconciliationHistory().count() == 2); QVERIFY(a.addReconciliation(QDate(2011, 2, 1), MyMoneyMoney(789, 100)) == true); QVERIFY(a.reconciliationHistory().count() == 2); QVERIFY(a.reconciliationHistory().values().last() == MyMoneyMoney(789, 100)); } void MyMoneyAccountTest::reconciliationHistory() { MyMoneyAccount a; QVERIFY(a.reconciliationHistory().isEmpty() == true); QVERIFY(a.addReconciliation(QDate(2011, 1, 2), MyMoneyMoney(123, 100)) == true); QVERIFY(a.reconciliationHistory()[QDate(2011, 1, 2)] == MyMoneyMoney(123, 100)); QVERIFY(a.reconciliationHistory()[QDate(2011, 1, 1)] == MyMoneyMoney()); QVERIFY(a.reconciliationHistory()[QDate(2011, 1, 3)] == MyMoneyMoney()); QVERIFY(a.addReconciliation(QDate(2011, 2, 1), MyMoneyMoney(456, 100)) == true); QVERIFY(a.reconciliationHistory()[QDate(2011, 1, 2)] == MyMoneyMoney(123, 100)); QVERIFY(a.reconciliationHistory()[QDate(2011, 1, 1)] == MyMoneyMoney()); QVERIFY(a.reconciliationHistory()[QDate(2011, 1, 3)] == MyMoneyMoney()); QVERIFY(a.reconciliationHistory()[QDate(2011, 2, 1)] == MyMoneyMoney(456, 100)); QVERIFY(a.reconciliationHistory().count() == 2); } void MyMoneyAccountTest::testHasOnlineMapping() { MyMoneyAccount a; QCOMPARE(a.hasOnlineMapping(), false); MyMoneyKeyValueContainer kvp = a.onlineBankingSettings(); kvp.setValue(QLatin1String("provider"), QLatin1String("bla")); a.setOnlineBankingSettings(kvp); QCOMPARE(a.hasOnlineMapping(), true); } diff --git a/kmymoney/mymoney/tests/mymoneyfile-test.cpp b/kmymoney/mymoney/tests/mymoneyfile-test.cpp index 295a2cfca..de418d2ac 100644 --- a/kmymoney/mymoney/tests/mymoneyfile-test.cpp +++ b/kmymoney/mymoney/tests/mymoneyfile-test.cpp @@ -1,2834 +1,2829 @@ /* * Copyright 2002-2018 Thomas Baumgart * Copyright 2004 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. * * 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 "mymoneyfile-test.h" #include #include #include #include #include #include #include "mymoneystoragemgr_p.h" #include "mymoneytestutils.h" #include "mymoneymoney.h" #include "mymoneyinstitution.h" #include "mymoneyaccount.h" #include "mymoneysecurity.h" #include "mymoneytransaction.h" #include "mymoneytransactionfilter.h" #include "mymoneysplit.h" #include "mymoneyprice.h" #include "mymoneypayee.h" #include "mymoneyenums.h" #include "onlinejob.h" #include "payeeidentifier/ibanbic/ibanbic.h" #include "payeeidentifiertyped.h" QTEST_GUILESS_MAIN(MyMoneyFileTest) MyMoneyFileTest::MyMoneyFileTest() : m(nullptr), storage(nullptr) { } void MyMoneyFileTest::objectAdded(eMyMoney::File::Object type, const QString& id) { Q_UNUSED(type); m_objectsAdded += id; } void MyMoneyFileTest::objectRemoved(eMyMoney::File::Object type, const QString& id) { Q_UNUSED(type); m_objectsRemoved += id; } void MyMoneyFileTest::objectModified(eMyMoney::File::Object type, const QString& id) { Q_UNUSED(type); m_objectsModified += id; } void MyMoneyFileTest::clearObjectLists() { m_objectsAdded.clear(); m_objectsModified.clear(); m_objectsRemoved.clear(); m_balanceChanged.clear(); m_valueChanged.clear(); } void MyMoneyFileTest::balanceChanged(const MyMoneyAccount& account) { m_balanceChanged += account.id(); } void MyMoneyFileTest::valueChanged(const MyMoneyAccount& account) { m_valueChanged += account.id(); } // this method will be called once at the beginning of the test void MyMoneyFileTest::initTestCase() { m = MyMoneyFile::instance(); connect(m, &MyMoneyFile::objectAdded, this, &MyMoneyFileTest::objectAdded); connect(m, &MyMoneyFile::objectRemoved, this, &MyMoneyFileTest::objectRemoved); connect(m, &MyMoneyFile::objectModified, this, &MyMoneyFileTest::objectModified); connect(m, &MyMoneyFile::balanceChanged, this, &MyMoneyFileTest::balanceChanged); connect(m, &MyMoneyFile::valueChanged, this, &MyMoneyFileTest::valueChanged); } // this method will be called before each testfunction void MyMoneyFileTest::init() { storage = new MyMoneyStorageMgr; m->attachStorage(storage); clearObjectLists(); } // this method will be called after each testfunction void MyMoneyFileTest::cleanup() { m->detachStorage(storage); delete storage; } void MyMoneyFileTest::testEmptyConstructor() { MyMoneyPayee user = m->user(); QCOMPARE(user.name(), QString()); QCOMPARE(user.address(), QString()); QCOMPARE(user.city(), QString()); QCOMPARE(user.state(), QString()); QCOMPARE(user.postcode(), QString()); QCOMPARE(user.telephone(), QString()); QCOMPARE(user.email(), QString()); QCOMPARE(m->institutionCount(), static_cast(0)); QCOMPARE(m->dirty(), false); QCOMPARE(m->accountCount(), static_cast(5)); } void MyMoneyFileTest::testAddOneInstitution() { MyMoneyInstitution institution; institution.setName("institution1"); institution.setTown("town"); institution.setStreet("street"); institution.setPostcode("postcode"); institution.setTelephone("telephone"); institution.setManager("manager"); institution.setSortcode("sortcode"); // MyMoneyInstitution institution_file("", institution); MyMoneyInstitution institution_id("I000002", institution); MyMoneyInstitution institution_noname(institution); institution_noname.setName(QString()); - QString id; - QCOMPARE(m->institutionCount(), static_cast(0)); storage->d_func()->m_dirty = false; clearObjectLists(); MyMoneyFileTransaction ft; try { m->addInstitution(institution); ft.commit(); QCOMPARE(institution.id(), QLatin1String("I000001")); QCOMPARE(m->institutionCount(), static_cast(1)); QCOMPARE(m->dirty(), true); QCOMPARE(m_objectsRemoved.count(), 0); QCOMPARE(m_objectsAdded.count(), 1); QCOMPARE(m_objectsModified.count(), 0); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); QCOMPARE(m_objectsAdded[0], QLatin1String("I000001")); } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } clearObjectLists(); ft.restart(); try { m->addInstitution(institution_id); QFAIL("Missing expected exception"); } catch (const MyMoneyException &) { ft.commit(); QCOMPARE(m->institutionCount(), static_cast(1)); } ft.restart(); try { m->addInstitution(institution_noname); QFAIL("Missing expected exception"); } catch (const MyMoneyException &) { ft.commit(); QCOMPARE(m->institutionCount(), static_cast(1)); } QCOMPARE(m_objectsRemoved.count(), 0); QCOMPARE(m_objectsAdded.count(), 0); QCOMPARE(m_objectsModified.count(), 0); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); } void MyMoneyFileTest::testAddTwoInstitutions() { testAddOneInstitution(); MyMoneyInstitution institution; institution.setName("institution2"); institution.setTown("town"); institution.setStreet("street"); institution.setPostcode("postcode"); institution.setTelephone("telephone"); institution.setManager("manager"); institution.setSortcode("sortcode"); - QString id; - storage->d_func()->m_dirty = false; MyMoneyFileTransaction ft; try { m->addInstitution(institution); ft.commit(); QCOMPARE(institution.id(), QLatin1String("I000002")); QCOMPARE(m->institutionCount(), static_cast(2)); QCOMPARE(m->dirty(), true); } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } storage->d_func()->m_dirty = false; try { institution = m->institution("I000001"); QCOMPARE(institution.id(), QLatin1String("I000001")); QCOMPARE(m->institutionCount(), static_cast(2)); QCOMPARE(m->dirty(), false); institution = m->institution("I000002"); QCOMPARE(institution.id(), QLatin1String("I000002")); QCOMPARE(m->institutionCount(), static_cast(2)); QCOMPARE(m->dirty(), false); } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } } void MyMoneyFileTest::testRemoveInstitution() { testAddTwoInstitutions(); MyMoneyInstitution i; QCOMPARE(m->institutionCount(), static_cast(2)); i = m->institution("I000001"); QCOMPARE(i.id(), QLatin1String("I000001")); QCOMPARE(i.accountCount(), static_cast(0)); clearObjectLists(); storage->d_func()->m_dirty = false; MyMoneyFileTransaction ft; try { m->removeInstitution(i); QCOMPARE(m_objectsRemoved.count(), 0); ft.commit(); QCOMPARE(m->institutionCount(), static_cast(1)); QCOMPARE(m->dirty(), true); QCOMPARE(m_objectsRemoved.count(), 1); QCOMPARE(m_objectsAdded.count(), 0); QCOMPARE(m_objectsModified.count(), 0); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); QCOMPARE(m_objectsRemoved[0], QLatin1String("I000001")); } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } storage->d_func()->m_dirty = false; try { m->institution("I000001"); QFAIL("Missing expected exception"); } catch (const MyMoneyException &) { QCOMPARE(m->institutionCount(), static_cast(1)); QCOMPARE(m->dirty(), false); } clearObjectLists(); ft.restart(); try { m->removeInstitution(i); QFAIL("Missing expected exception"); } catch (const MyMoneyException &) { ft.commit(); QCOMPARE(m->institutionCount(), static_cast(1)); QCOMPARE(m->dirty(), false); QCOMPARE(m_objectsRemoved.count(), 0); } } void MyMoneyFileTest::testInstitutionRetrieval() { testAddOneInstitution(); storage->d_func()->m_dirty = false; MyMoneyInstitution institution; QCOMPARE(m->institutionCount(), static_cast(1)); try { institution = m->institution("I000001"); QCOMPARE(institution.id(), QLatin1String("I000001")); QCOMPARE(m->institutionCount(), static_cast(1)); } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } try { institution = m->institution("I000002"); QFAIL("Missing expected exception"); } catch (const MyMoneyException &) { QCOMPARE(m->institutionCount(), static_cast(1)); } QCOMPARE(m->dirty(), false); } void MyMoneyFileTest::testInstitutionListRetrieval() { QList list; storage->d_func()->m_dirty = false; list = m->institutionList(); QCOMPARE(m->dirty(), false); QCOMPARE(list.count(), 0); testAddTwoInstitutions(); storage->d_func()->m_dirty = false; list = m->institutionList(); QCOMPARE(m->dirty(), false); QCOMPARE(list.count(), 2); QList::ConstIterator it; it = list.constBegin(); QVERIFY((*it).name() == "institution1" || (*it).name() == "institution2"); ++it; QVERIFY((*it).name() == "institution2" || (*it).name() == "institution1"); ++it; QVERIFY(it == list.constEnd()); } void MyMoneyFileTest::testInstitutionModify() { testAddTwoInstitutions(); MyMoneyInstitution institution; institution = m->institution("I000001"); institution.setStreet("new street"); institution.setTown("new town"); institution.setPostcode("new postcode"); institution.setTelephone("new telephone"); institution.setManager("new manager"); institution.setName("new name"); institution.setSortcode("new sortcode"); storage->d_func()->m_dirty = false; clearObjectLists(); MyMoneyFileTransaction ft; try { m->modifyInstitution(institution); ft.commit(); QCOMPARE(institution.id(), QLatin1String("I000001")); QCOMPARE(m->institutionCount(), static_cast(2)); QCOMPARE(m->dirty(), true); QCOMPARE(m_objectsRemoved.count(), 0); QCOMPARE(m_objectsAdded.count(), 0); QCOMPARE(m_objectsModified.count(), 1); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); QCOMPARE(m_objectsModified[0], QLatin1String("I000001")); } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } MyMoneyInstitution newInstitution; newInstitution = m->institution("I000001"); QCOMPARE(newInstitution.id(), QLatin1String("I000001")); QCOMPARE(newInstitution.street(), QLatin1String("new street")); QCOMPARE(newInstitution.town(), QLatin1String("new town")); QCOMPARE(newInstitution.postcode(), QLatin1String("new postcode")); QCOMPARE(newInstitution.telephone(), QLatin1String("new telephone")); QCOMPARE(newInstitution.manager(), QLatin1String("new manager")); QCOMPARE(newInstitution.name(), QLatin1String("new name")); QCOMPARE(newInstitution.sortcode(), QLatin1String("new sortcode")); storage->d_func()->m_dirty = false; ft.restart(); MyMoneyInstitution failInstitution2("I000003", newInstitution); try { m->modifyInstitution(failInstitution2); QFAIL("Exception expected"); } catch (const MyMoneyException &) { ft.commit(); QCOMPARE(failInstitution2.id(), QLatin1String("I000003")); QCOMPARE(m->institutionCount(), static_cast(2)); QCOMPARE(m->dirty(), false); } } void MyMoneyFileTest::testSetFunctions() { MyMoneyPayee user = m->user(); QCOMPARE(user.name(), QString()); QCOMPARE(user.address(), QString()); QCOMPARE(user.city(), QString()); QCOMPARE(user.state(), QString()); QCOMPARE(user.postcode(), QString()); QCOMPARE(user.telephone(), QString()); QCOMPARE(user.email(), QString()); MyMoneyFileTransaction ft; storage->d_func()->m_dirty = false; user.setName("Name"); m->setUser(user); QCOMPARE(m->dirty(), true); storage->d_func()->m_dirty = false; user.setAddress("Street"); m->setUser(user); QCOMPARE(m->dirty(), true); storage->d_func()->m_dirty = false; user.setCity("Town"); m->setUser(user); QCOMPARE(m->dirty(), true); storage->d_func()->m_dirty = false; user.setState("County"); m->setUser(user); QCOMPARE(m->dirty(), true); storage->d_func()->m_dirty = false; user.setPostcode("Postcode"); m->setUser(user); QCOMPARE(m->dirty(), true); storage->d_func()->m_dirty = false; user.setTelephone("Telephone"); m->setUser(user); QCOMPARE(m->dirty(), true); storage->d_func()->m_dirty = false; user.setEmail("Email"); m->setUser(user); QCOMPARE(m->dirty(), true); storage->d_func()->m_dirty = false; ft.commit(); user = m->user(); QCOMPARE(user.name(), QLatin1String("Name")); QCOMPARE(user.address(), QLatin1String("Street")); QCOMPARE(user.city(), QLatin1String("Town")); QCOMPARE(user.state(), QLatin1String("County")); QCOMPARE(user.postcode(), QLatin1String("Postcode")); QCOMPARE(user.telephone(), QLatin1String("Telephone")); QCOMPARE(user.email(), QLatin1String("Email")); } void MyMoneyFileTest::testAddAccounts() { testAddTwoInstitutions(); MyMoneyAccount a, b, c; a.setAccountType(eMyMoney::Account::Type::Checkings); b.setAccountType(eMyMoney::Account::Type::Checkings); MyMoneyInstitution institution; storage->d_func()->m_dirty = false; QCOMPARE(m->accountCount(), static_cast(5)); institution = m->institution("I000001"); QCOMPARE(institution.id(), QLatin1String("I000001")); a.setName("Account1"); a.setInstitutionId(institution.id()); a.setCurrencyId("EUR"); clearObjectLists(); MyMoneyFileTransaction ft; try { MyMoneyAccount parent = m->asset(); m->addAccount(a, parent); ft.commit(); QCOMPARE(m->accountCount(), static_cast(6)); QCOMPARE(a.parentAccountId(), QLatin1String("AStd::Asset")); QCOMPARE(a.id(), QLatin1String("A000001")); QCOMPARE(a.institutionId(), QLatin1String("I000001")); QCOMPARE(a.currencyId(), QLatin1String("EUR")); QCOMPARE(m->dirty(), true); QCOMPARE(m->asset().accountList().count(), 1); QCOMPARE(m->asset().accountList()[0], QLatin1String("A000001")); institution = m->institution("I000001"); QCOMPARE(institution.accountCount(), static_cast(1)); QCOMPARE(institution.accountList()[0], QLatin1String("A000001")); QCOMPARE(m_objectsRemoved.count(), 0); QCOMPARE(m_objectsAdded.count(), 1); QCOMPARE(m_objectsModified.count(), 2); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); QVERIFY(m_objectsAdded.contains(QLatin1String("A000001"))); QVERIFY(m_objectsModified.contains(QLatin1String("I000001"))); QVERIFY(m_objectsModified.contains(QLatin1String("AStd::Asset"))); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } // try to add this account again, should not work ft.restart(); try { MyMoneyAccount parent = m->asset(); m->addAccount(a, parent); QFAIL("Expecting exception!"); } catch (const MyMoneyException &) { ft.commit(); } // check that we can modify the local object and // reload it from the file a.setName("AccountX"); a = m->account("A000001"); QCOMPARE(a.name(), QLatin1String("Account1")); storage->d_func()->m_dirty = false; // check if we can get the same info to a different object c = m->account("A000001"); QCOMPARE(c.accountType(), eMyMoney::Account::Type::Checkings); QCOMPARE(c.id(), QLatin1String("A000001")); QCOMPARE(c.name(), QLatin1String("Account1")); QCOMPARE(c.institutionId(), QLatin1String("I000001")); QCOMPARE(m->dirty(), false); // add a second account institution = m->institution("I000002"); b.setName("Account2"); b.setInstitutionId(institution.id()); b.setCurrencyId("EUR"); clearObjectLists(); ft.restart(); try { MyMoneyAccount parent = m->asset(); m->addAccount(b, parent); ft.commit(); QCOMPARE(m->dirty(), true); QCOMPARE(b.id(), QLatin1String("A000002")); QCOMPARE(b.currencyId(), QLatin1String("EUR")); QCOMPARE(b.parentAccountId(), QLatin1String("AStd::Asset")); QCOMPARE(m->accountCount(), static_cast(7)); institution = m->institution("I000001"); QCOMPARE(institution.accountCount(), static_cast(1)); QCOMPARE(institution.accountList()[0], QLatin1String("A000001")); institution = m->institution("I000002"); QCOMPARE(institution.accountCount(), static_cast(1)); QCOMPARE(institution.accountList()[0], QLatin1String("A000002")); QCOMPARE(m->asset().accountList().count(), 2); QCOMPARE(m->asset().accountList()[0], QLatin1String("A000001")); QCOMPARE(m->asset().accountList()[1], QLatin1String("A000002")); QCOMPARE(m_objectsRemoved.count(), 0); QCOMPARE(m_objectsAdded.count(), 1); QCOMPARE(m_objectsModified.count(), 2); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); QVERIFY(m_objectsAdded.contains(QLatin1String("A000002"))); QVERIFY(m_objectsModified.contains(QLatin1String("I000002"))); QVERIFY(m_objectsModified.contains(QLatin1String("AStd::Asset"))); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } MyMoneyAccount p; p = m->account("A000002"); QCOMPARE(p.accountType(), eMyMoney::Account::Type::Checkings); QCOMPARE(p.id(), QLatin1String("A000002")); QCOMPARE(p.name(), QLatin1String("Account2")); QCOMPARE(p.institutionId(), QLatin1String("I000002")); QCOMPARE(p.currencyId(), QLatin1String("EUR")); } void MyMoneyFileTest::testAddCategories() { MyMoneyAccount a, b, c; a.setAccountType(eMyMoney::Account::Type::Income); a.setOpeningDate(QDate::currentDate()); b.setAccountType(eMyMoney::Account::Type::Expense); storage->d_func()->m_dirty = false; QCOMPARE(m->accountCount(), static_cast(5)); QCOMPARE(a.openingDate(), QDate::currentDate()); QVERIFY(!b.openingDate().isValid()); a.setName("Account1"); a.setCurrencyId("EUR"); clearObjectLists(); MyMoneyFileTransaction ft; try { MyMoneyAccount parent = m->income(); m->addAccount(a, parent); ft.commit(); QCOMPARE(m->accountCount(), static_cast(6)); QCOMPARE(a.parentAccountId(), QLatin1String("AStd::Income")); QCOMPARE(a.id(), QLatin1String("A000001")); QCOMPARE(a.institutionId(), QString()); QCOMPARE(a.currencyId(), QLatin1String("EUR")); QCOMPARE(a.openingDate(), QDate(1900, 1, 1)); QCOMPARE(m->dirty(), true); QCOMPARE(m->income().accountList().count(), 1); QCOMPARE(m->income().accountList()[0], QLatin1String("A000001")); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } // add a second category, expense this time b.setName("Account2"); b.setCurrencyId("EUR"); clearObjectLists(); ft.restart(); try { MyMoneyAccount parent = m->expense(); m->addAccount(b, parent); ft.commit(); QCOMPARE(m->dirty(), true); QCOMPARE(b.id(), QLatin1String("A000002")); QCOMPARE(a.institutionId(), QString()); QCOMPARE(b.currencyId(), QLatin1String("EUR")); QCOMPARE(b.openingDate(), QDate(1900, 1, 1)); QCOMPARE(b.parentAccountId(), QLatin1String("AStd::Expense")); QCOMPARE(m->accountCount(), static_cast(7)); QCOMPARE(m->income().accountList().count(), 1); QCOMPARE(m->expense().accountList().count(), 1); QCOMPARE(m->income().accountList()[0], QLatin1String("A000001")); QCOMPARE(m->expense().accountList()[0], QLatin1String("A000002")); QCOMPARE(m_objectsRemoved.count(), 0); QCOMPARE(m_objectsAdded.count(), 1); QCOMPARE(m_objectsModified.count(), 1); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); QVERIFY(m_objectsAdded.contains(QLatin1String("A000002"))); QVERIFY(m_objectsModified.contains(QLatin1String("AStd::Expense"))); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } } void MyMoneyFileTest::testModifyAccount() { testAddAccounts(); storage->d_func()->m_dirty = false; MyMoneyAccount p = m->account("A000001"); MyMoneyInstitution institution; QCOMPARE(p.accountType(), eMyMoney::Account::Type::Checkings); QCOMPARE(p.name(), QLatin1String("Account1")); p.setName("New account name"); MyMoneyFileTransaction ft; clearObjectLists(); try { m->modifyAccount(p); ft.commit(); QCOMPARE(m->dirty(), true); QCOMPARE(m->accountCount(), static_cast(7)); QCOMPARE(p.accountType(), eMyMoney::Account::Type::Checkings); QCOMPARE(p.name(), QLatin1String("New account name")); QCOMPARE(m_objectsRemoved.count(), 0); QCOMPARE(m_objectsAdded.count(), 0); QCOMPARE(m_objectsModified.count(), 1); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); QVERIFY(m_objectsModified.contains(QLatin1String("A000001"))); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } storage->d_func()->m_dirty = false; // try to move account to new institution p.setInstitutionId("I000002"); ft.restart(); clearObjectLists(); try { m->modifyAccount(p); ft.commit(); QCOMPARE(m->dirty(), true); QCOMPARE(m->accountCount(), static_cast(7)); QCOMPARE(p.accountType(), eMyMoney::Account::Type::Checkings); QCOMPARE(p.name(), QLatin1String("New account name")); QCOMPARE(p.institutionId(), QLatin1String("I000002")); institution = m->institution("I000001"); QCOMPARE(institution.accountCount(), static_cast(0)); institution = m->institution("I000002"); QCOMPARE(institution.accountCount(), static_cast(2)); QCOMPARE(institution.accountList()[0], QLatin1String("A000002")); QCOMPARE(institution.accountList()[1], QLatin1String("A000001")); QCOMPARE(m_objectsRemoved.count(), 0); QCOMPARE(m_objectsAdded.count(), 0); QCOMPARE(m_objectsModified.count(), 3); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); QVERIFY(m_objectsModified.contains(QLatin1String("A000001"))); QVERIFY(m_objectsModified.contains(QLatin1String("I000001"))); QVERIFY(m_objectsModified.contains(QLatin1String("I000002"))); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } storage->d_func()->m_dirty = false; // try to change to an account type that is allowed p.setAccountType(eMyMoney::Account::Type::Savings); ft.restart(); try { m->modifyAccount(p); ft.commit(); QCOMPARE(m->dirty(), true); QCOMPARE(m->accountCount(), static_cast(7)); QCOMPARE(p.accountType(), eMyMoney::Account::Type::Savings); QCOMPARE(p.name(), QLatin1String("New account name")); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } storage->d_func()->m_dirty = false; // try to change to an account type that is not allowed p.setAccountType(eMyMoney::Account::Type::CreditCard); ft.restart(); try { m->modifyAccount(p); QFAIL("Expecting exception!"); } catch (const MyMoneyException &) { ft.commit(); } storage->d_func()->m_dirty = false; // try to fool engine a bit p.setParentAccountId("A000001"); ft.restart(); try { m->modifyAccount(p); QFAIL("Expecting exception!"); } catch (const MyMoneyException &) { ft.commit(); } } void MyMoneyFileTest::testReparentAccount() { testAddAccounts(); storage->d_func()->m_dirty = false; MyMoneyAccount p = m->account("A000001"); MyMoneyAccount q = m->account("A000002"); MyMoneyAccount o = m->account(p.parentAccountId()); // make A000001 a child of A000002 clearObjectLists(); MyMoneyFileTransaction ft; try { QVERIFY(p.parentAccountId() != q.id()); QCOMPARE(o.accountCount(), 2); QCOMPARE(q.accountCount(), 0); m->reparentAccount(p, q); ft.commit(); QCOMPARE(m->dirty(), true); QCOMPARE(p.parentAccountId(), q.id()); QCOMPARE(q.accountCount(), 1); QCOMPARE(q.id(), QLatin1String("A000002")); QCOMPARE(p.id(), QLatin1String("A000001")); QCOMPARE(q.accountList()[0], p.id()); QCOMPARE(m_objectsRemoved.count(), 0); QCOMPARE(m_objectsAdded.count(), 0); QCOMPARE(m_objectsModified.count(), 3); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); QVERIFY(m_objectsModified.contains(QLatin1String("A000001"))); QVERIFY(m_objectsModified.contains(QLatin1String("A000002"))); QVERIFY(m_objectsModified.contains(QLatin1String("AStd::Asset"))); o = m->account(o.id()); QCOMPARE(o.accountCount(), 1); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } } void MyMoneyFileTest::testRemoveStdAccount(const MyMoneyAccount& acc) { QString txt("Exception expected while removing account "); txt += acc.id(); MyMoneyFileTransaction ft; try { m->removeAccount(acc); QFAIL(qPrintable(txt)); } catch (const MyMoneyException &) { ft.commit(); } } void MyMoneyFileTest::testRemoveAccount() { MyMoneyInstitution institution; testAddAccounts(); QCOMPARE(m->accountCount(), static_cast(7)); storage->d_func()->m_dirty = false; - QString id; MyMoneyAccount p = m->account("A000001"); clearObjectLists(); MyMoneyFileTransaction ft; try { MyMoneyAccount q("Ainvalid", p); m->removeAccount(q); QFAIL("Exception expected!"); } catch (const MyMoneyException &) { ft.commit(); } ft.restart(); try { QCOMPARE(m_objectsRemoved.count(), 0); QCOMPARE(m_objectsAdded.count(), 0); QCOMPARE(m_objectsModified.count(), 0); m->removeAccount(p); ft.commit(); QCOMPARE(m->dirty(), true); QCOMPARE(m->accountCount(), static_cast(6)); institution = m->institution("I000001"); QCOMPARE(institution.accountCount(), static_cast(0)); QCOMPARE(m->asset().accountList().count(), 1); QCOMPARE(m_objectsRemoved.count(), 1); QCOMPARE(m_objectsModified.count(), 2); QCOMPARE(m_objectsAdded.count(), 0); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); QVERIFY(m_objectsRemoved.contains(QLatin1String("A000001"))); QVERIFY(m_objectsModified.contains(QLatin1String("I000001"))); QVERIFY(m_objectsModified.contains(QLatin1String("AStd::Asset"))); institution = m->institution("I000002"); QCOMPARE(institution.accountCount(), static_cast(1)); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } // Check that the standard account-groups cannot be removed testRemoveStdAccount(m->liability()); testRemoveStdAccount(m->asset()); testRemoveStdAccount(m->expense()); testRemoveStdAccount(m->income()); } void MyMoneyFileTest::testRemoveAccountTree() { testReparentAccount(); MyMoneyAccount a = m->account("A000002"); clearObjectLists(); MyMoneyFileTransaction ft; // remove the account try { m->removeAccount(a); ft.commit(); QCOMPARE(m_objectsRemoved.count(), 1); QCOMPARE(m_objectsModified.count(), 3); QCOMPARE(m_objectsAdded.count(), 0); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); QVERIFY(m_objectsRemoved.contains(QLatin1String("A000002"))); QVERIFY(m_objectsModified.contains(QLatin1String("A000001"))); QVERIFY(m_objectsModified.contains(QLatin1String("I000002"))); QVERIFY(m_objectsModified.contains(QLatin1String("AStd::Asset"))); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } QCOMPARE(m->accountCount(), static_cast(6)); // make sure it's gone try { m->account("A000002"); QFAIL("Exception expected!"); } catch (const MyMoneyException &) { } // make sure that children are re-parented to parent account try { a = m->account("A000001"); QCOMPARE(a.parentAccountId(), m->asset().id()); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } } void MyMoneyFileTest::testAccountListRetrieval() { QList list; storage->d_func()->m_dirty = false; m->accountList(list); QCOMPARE(m->dirty(), false); QCOMPARE(list.count(), 0); testAddAccounts(); storage->d_func()->m_dirty = false; list.clear(); m->accountList(list); QCOMPARE(m->dirty(), false); QCOMPARE(list.count(), 2); QCOMPARE(list[0].accountType(), eMyMoney::Account::Type::Checkings); QCOMPARE(list[1].accountType(), eMyMoney::Account::Type::Checkings); } void MyMoneyFileTest::testAddTransaction() { testAddAccounts(); MyMoneyTransaction t, p; MyMoneyAccount exp1; exp1.setAccountType(eMyMoney::Account::Type::Expense); exp1.setName("Expense1"); MyMoneyAccount exp2; exp2.setAccountType(eMyMoney::Account::Type::Expense); exp2.setName("Expense2"); MyMoneyFileTransaction ft; try { MyMoneyAccount parent = m->expense(); m->addAccount(exp1, parent); m->addAccount(exp2, parent); ft.commit(); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } // fake the last modified flag to check that the // date is updated when we add the transaction MyMoneyAccount a = m->account("A000001"); a.setLastModified(QDate(1, 2, 3)); ft.restart(); try { m->modifyAccount(a); ft.commit(); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } ft.restart(); QCOMPARE(m->accountCount(), static_cast(9)); a = m->account("A000001"); QCOMPARE(a.lastModified(), QDate(1, 2, 3)); // construct a transaction and add it to the pool t.setPostDate(QDate(2002, 2, 1)); t.setMemo("Memotext"); MyMoneySplit split1; MyMoneySplit split2; split1.setAccountId("A000001"); split1.setShares(MyMoneyMoney(-1000, 100)); split1.setValue(MyMoneyMoney(-1000, 100)); split2.setAccountId("A000003"); split2.setValue(MyMoneyMoney(1000, 100)); split2.setShares(MyMoneyMoney(1000, 100)); try { t.addSplit(split1); t.addSplit(split2); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } /* // FIXME: we don't have a payee and a number field right now // guess we should have a number field per split, don't know // about the payee t.setMethod(MyMoneyCheckingTransaction::Withdrawal); t.setPayee("Thomas Baumgart"); t.setNumber("1234"); t.setState(MyMoneyCheckingTransaction::Cleared); */ storage->d_func()->m_dirty = false; ft.restart(); clearObjectLists(); try { m->addTransaction(t); ft.commit(); QCOMPARE(m_objectsRemoved.count(), 0); QCOMPARE(m_objectsModified.count(), 0); QCOMPARE(m_objectsAdded.count(), 1); QCOMPARE(m_balanceChanged.count(), 2); QCOMPARE(m_balanceChanged.count("A000001"), 1); QCOMPARE(m_balanceChanged.count("A000003"), 1); QCOMPARE(m_valueChanged.count(), 0); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } ft.restart(); clearObjectLists(); QCOMPARE(t.id(), QLatin1String("T000000000000000001")); QCOMPARE(t.postDate(), QDate(2002, 2, 1)); QCOMPARE(t.entryDate(), QDate::currentDate()); QCOMPARE(m->dirty(), true); // check the balance of the accounts a = m->account("A000001"); QCOMPARE(a.lastModified(), QDate::currentDate()); QCOMPARE(a.balance(), MyMoneyMoney(-1000, 100)); MyMoneyAccount b = m->account("A000003"); QCOMPARE(b.lastModified(), QDate::currentDate()); QCOMPARE(b.balance(), MyMoneyMoney(1000, 100)); storage->d_func()->m_dirty = false; // locate transaction in MyMoneyFile via id try { p = m->transaction("T000000000000000001"); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); QCOMPARE(p.splitCount(), static_cast(2)); QCOMPARE(p.memo(), QLatin1String("Memotext")); QCOMPARE(p.splits()[0].accountId(), QLatin1String("A000001")); QCOMPARE(p.splits()[1].accountId(), QLatin1String("A000003")); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } // check if it's in the account(s) as well try { p = m->transaction("A000001", 0); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); QCOMPARE(p.id(), QLatin1String("T000000000000000001")); QCOMPARE(p.splitCount(), static_cast(2)); QCOMPARE(p.memo(), QLatin1String("Memotext")); QCOMPARE(p.splits()[0].accountId(), QLatin1String("A000001")); QCOMPARE(p.splits()[1].accountId(), QLatin1String("A000003")); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } try { p = m->transaction("A000003", 0); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); QCOMPARE(p.id(), QLatin1String("T000000000000000001")); QCOMPARE(p.splitCount(), static_cast(2)); QCOMPARE(p.memo(), QLatin1String("Memotext")); QCOMPARE(p.splits()[0].accountId(), QLatin1String("A000001")); QCOMPARE(p.splits()[1].accountId(), QLatin1String("A000003")); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } } void MyMoneyFileTest::testIsStandardAccount() { QCOMPARE(m->isStandardAccount(m->liability().id()), true); QCOMPARE(m->isStandardAccount(m->asset().id()), true); QCOMPARE(m->isStandardAccount(m->expense().id()), true); QCOMPARE(m->isStandardAccount(m->income().id()), true); QCOMPARE(m->isStandardAccount("A00001"), false); } void MyMoneyFileTest::testHasActiveSplits() { testAddTransaction(); QCOMPARE(m->hasActiveSplits("A000001"), true); QCOMPARE(m->hasActiveSplits("A000002"), false); } void MyMoneyFileTest::testModifyTransactionSimple() { // this will test that we can modify the basic attributes // of a transaction testAddTransaction(); MyMoneyTransaction t = m->transaction("T000000000000000001"); t.setMemo("New Memotext"); storage->d_func()->m_dirty = false; MyMoneyFileTransaction ft; clearObjectLists(); try { m->modifyTransaction(t); ft.commit(); QCOMPARE(m_objectsRemoved.count(), 0); QCOMPARE(m_objectsModified.count(), 1); QCOMPARE(m_objectsAdded.count(), 0); QCOMPARE(m_balanceChanged.count(), 2); QCOMPARE(m_balanceChanged.count(QLatin1String("A000001")), 1); QCOMPARE(m_balanceChanged.count(QLatin1String("A000003")), 1); QCOMPARE(m_valueChanged.count(), 0); t = m->transaction("T000000000000000001"); QCOMPARE(t.memo(), QLatin1String("New Memotext")); QCOMPARE(m->dirty(), true); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } } void MyMoneyFileTest::testModifyTransactionNewPostDate() { // this will test that we can modify the basic attributes // of a transaction testAddTransaction(); MyMoneyTransaction t = m->transaction("T000000000000000001"); t.setPostDate(QDate(2004, 2, 1)); storage->d_func()->m_dirty = false; MyMoneyFileTransaction ft; clearObjectLists(); try { m->modifyTransaction(t); ft.commit(); t = m->transaction("T000000000000000001"); QCOMPARE(t.postDate(), QDate(2004, 2, 1)); t = m->transaction("A000001", 0); QCOMPARE(t.id(), QLatin1String("T000000000000000001")); QCOMPARE(m->dirty(), true); QCOMPARE(m_objectsRemoved.count(), 0); QCOMPARE(m_objectsModified.count(), 1); QCOMPARE(m_objectsAdded.count(), 0); QCOMPARE(m_balanceChanged.count(), 2); QCOMPARE(m_balanceChanged.count(QLatin1String("A000001")), 1); QCOMPARE(m_balanceChanged.count(QLatin1String("A000003")), 1); QCOMPARE(m_valueChanged.count(), 0); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } } void MyMoneyFileTest::testModifyTransactionNewAccount() { // this will test that we can modify the basic attributes // of a transaction testAddTransaction(); MyMoneyTransaction t = m->transaction("T000000000000000001"); MyMoneySplit s; s = t.splits()[0]; s.setAccountId("A000002"); t.modifySplit(s); storage->d_func()->m_dirty = false; MyMoneyFileTransaction ft; clearObjectLists(); try { MyMoneyTransactionFilter f1("A000001"); MyMoneyTransactionFilter f2("A000002"); MyMoneyTransactionFilter f3("A000003"); QCOMPARE(m->transactionList(f1).count(), 1); QCOMPARE(m->transactionList(f2).count(), 0); QCOMPARE(m->transactionList(f3).count(), 1); m->modifyTransaction(t); ft.commit(); t = m->transaction("T000000000000000001"); QCOMPARE(t.postDate(), QDate(2002, 2, 1)); t = m->transaction("A000002", 0); QCOMPARE(m->dirty(), true); QCOMPARE(m->transactionList(f1).count(), 0); QCOMPARE(m->transactionList(f2).count(), 1); QCOMPARE(m->transactionList(f3).count(), 1); QCOMPARE(m_objectsRemoved.count(), 0); QCOMPARE(m_objectsModified.count(), 1); QCOMPARE(m_objectsAdded.count(), 0); QCOMPARE(m_balanceChanged.count(), 3); QCOMPARE(m_balanceChanged.count(QLatin1String("A000001")), 1); QCOMPARE(m_balanceChanged.count(QLatin1String("A000002")), 1); QCOMPARE(m_balanceChanged.count(QLatin1String("A000003")), 1); QCOMPARE(m_valueChanged.count(), 0); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } } void MyMoneyFileTest::testRemoveTransaction() { testModifyTransactionNewPostDate(); MyMoneyTransaction t; t = m->transaction("T000000000000000001"); storage->d_func()->m_dirty = false; MyMoneyFileTransaction ft; clearObjectLists(); try { m->removeTransaction(t); ft.commit(); QCOMPARE(m->dirty(), true); QCOMPARE(m->transactionCount(), static_cast(0)); MyMoneyTransactionFilter f1("A000001"); MyMoneyTransactionFilter f2("A000002"); MyMoneyTransactionFilter f3("A000003"); QCOMPARE(m->transactionList(f1).count(), 0); QCOMPARE(m->transactionList(f2).count(), 0); QCOMPARE(m->transactionList(f3).count(), 0); QCOMPARE(m_objectsRemoved.count(), 1); QCOMPARE(m_objectsModified.count(), 0); QCOMPARE(m_objectsAdded.count(), 0); QCOMPARE(m_balanceChanged.count(), 2); QCOMPARE(m_balanceChanged.count(QLatin1String("A000001")), 1); QCOMPARE(m_balanceChanged.count(QLatin1String("A000003")), 1); QCOMPARE(m_valueChanged.count(), 0); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } } /* * This function is currently not implemented. It's kind of tricky * because it modifies a lot of objects in a single call. This might * be a problem for the undo/redo stuff. That's why I left it out in * the first run. We migh add it, if we need it. * / void testMoveSplits() { testModifyTransactionNewPostDate(); QCOMPARE(m->account("A000001").transactionCount(), 1); QCOMPARE(m->account("A000002").transactionCount(), 0); QCOMPARE(m->account("A000003").transactionCount(), 1); try { m->moveSplits("A000001", "A000002"); QCOMPARE(m->account("A000001").transactionCount(), 0); QCOMPARE(m->account("A000002").transactionCount(), 1); QCOMPARE(m->account("A000003").transactionCount(), 1); } catch (const MyMoneyException &e) { QFAIL("Unexpected exception!"); } } */ void MyMoneyFileTest::testBalanceTotal() { testAddTransaction(); MyMoneyTransaction t; // construct a transaction and add it to the pool t.setPostDate(QDate(2002, 2, 1)); t.setMemo("Memotext"); MyMoneySplit split1; MyMoneySplit split2; MyMoneyFileTransaction ft; try { split1.setAccountId("A000002"); split1.setShares(MyMoneyMoney(-1000, 100)); split1.setValue(MyMoneyMoney(-1000, 100)); split2.setAccountId("A000004"); split2.setValue(MyMoneyMoney(1000, 100)); split2.setShares(MyMoneyMoney(1000, 100)); t.addSplit(split1); t.addSplit(split2); m->addTransaction(t); ft.commit(); ft.restart(); QCOMPARE(t.id(), QLatin1String("T000000000000000002")); QCOMPARE(m->totalBalance("A000001"), MyMoneyMoney(-1000, 100)); QCOMPARE(m->totalBalance("A000002"), MyMoneyMoney(-1000, 100)); MyMoneyAccount p = m->account("A000001"); MyMoneyAccount q = m->account("A000002"); m->reparentAccount(p, q); ft.commit(); // check totalBalance() and balance() with combinations of parameters QCOMPARE(m->totalBalance("A000001"), MyMoneyMoney(-1000, 100)); QCOMPARE(m->totalBalance("A000002"), MyMoneyMoney(-2000, 100)); QVERIFY(m->totalBalance("A000002", QDate(2002, 1, 15)).isZero()); QCOMPARE(m->balance("A000001"), MyMoneyMoney(-1000, 100)); QCOMPARE(m->balance("A000002"), MyMoneyMoney(-1000, 100)); // Date of a transaction QCOMPARE(m->balance("A000001", QDate(2002, 2, 1)), MyMoneyMoney(-1000, 100)); QCOMPARE(m->balance("A000002", QDate(2002, 2, 1)), MyMoneyMoney(-1000, 100)); // Date after last transaction QCOMPARE(m->balance("A000001", QDate(2002, 2, 1)), MyMoneyMoney(-1000, 100)); QCOMPARE(m->balance("A000002", QDate(2002, 2, 1)), MyMoneyMoney(-1000, 100)); // Date before first transaction QVERIFY(m->balance("A000001", QDate(2002, 1, 15)).isZero()); QVERIFY(m->balance("A000002", QDate(2002, 1, 15)).isZero()); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } // Now check for exceptions try { // Account not found for balance() QVERIFY(m->balance("A000005").isZero()); QFAIL("Exception expected"); } catch (const MyMoneyException &) { } try { // Account not found for totalBalance() QVERIFY(m->totalBalance("A000005").isZero()); QFAIL("Exception expected"); } catch (const MyMoneyException &) { } } void MyMoneyFileTest::testSetAccountName() { MyMoneyFileTransaction ft; clearObjectLists(); try { m->setAccountName(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Liability), "Verbindlichkeiten"); ft.commit(); QCOMPARE(m_objectsRemoved.count(), 0); QCOMPARE(m_objectsModified.count(), 1); QCOMPARE(m_objectsAdded.count(), 0); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); QVERIFY(m_objectsModified.contains(QLatin1String("AStd::Liability"))); } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } ft.restart(); clearObjectLists(); try { m->setAccountName(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Asset), QString::fromUtf8("Vermögen")); ft.commit(); QCOMPARE(m_objectsRemoved.count(), 0); QCOMPARE(m_objectsModified.count(), 1); QCOMPARE(m_objectsAdded.count(), 0); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); QVERIFY(m_objectsModified.contains(QLatin1String("AStd::Asset"))); } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } ft.restart(); clearObjectLists(); try { m->setAccountName(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Expense), "Ausgaben"); ft.commit(); QCOMPARE(m_objectsRemoved.count(), 0); QCOMPARE(m_objectsModified.count(), 1); QCOMPARE(m_objectsAdded.count(), 0); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); QVERIFY(m_objectsModified.contains(QLatin1String("AStd::Expense"))); } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } ft.restart(); clearObjectLists(); try { m->setAccountName(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Income), "Einnahmen"); ft.commit(); QCOMPARE(m_objectsRemoved.count(), 0); QCOMPARE(m_objectsModified.count(), 1); QCOMPARE(m_objectsAdded.count(), 0); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); QVERIFY(m_objectsModified.contains(QLatin1String("AStd::Income"))); } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } ft.restart(); QCOMPARE(m->liability().name(), QLatin1String("Verbindlichkeiten")); QCOMPARE(m->asset().name(), QString::fromUtf8("Vermögen")); QCOMPARE(m->expense().name(), QLatin1String("Ausgaben")); QCOMPARE(m->income().name(), QLatin1String("Einnahmen")); try { m->setAccountName("A000001", "New account name"); ft.commit(); QFAIL("Exception expected"); } catch (const MyMoneyException &) { } } void MyMoneyFileTest::testAddPayee() { MyMoneyPayee p; p.setName("THB"); QCOMPARE(m->dirty(), false); MyMoneyFileTransaction ft; try { m->addPayee(p); ft.commit(); QCOMPARE(m->dirty(), true); QCOMPARE(p.id(), QLatin1String("P000001")); QCOMPARE(m_objectsRemoved.count(), 0); QCOMPARE(m_objectsModified.count(), 0); QCOMPARE(m_objectsAdded.count(), 1); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); QVERIFY(m_objectsAdded.contains(QLatin1String("P000001"))); } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } } void MyMoneyFileTest::testModifyPayee() { MyMoneyPayee p; testAddPayee(); clearObjectLists(); p = m->payee("P000001"); p.setName("New name"); MyMoneyFileTransaction ft; try { m->modifyPayee(p); ft.commit(); p = m->payee("P000001"); QCOMPARE(p.name(), QLatin1String("New name")); QCOMPARE(m_objectsRemoved.count(), 0); QCOMPARE(m_objectsModified.count(), 1); QCOMPARE(m_objectsAdded.count(), 0); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); QVERIFY(m_objectsModified.contains(QLatin1String("P000001"))); } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } } void MyMoneyFileTest::testRemovePayee() { MyMoneyPayee p; testAddPayee(); clearObjectLists(); QCOMPARE(m->payeeList().count(), 1); p = m->payee("P000001"); MyMoneyFileTransaction ft; try { m->removePayee(p); ft.commit(); QCOMPARE(m->payeeList().count(), 0); QCOMPARE(m_objectsRemoved.count(), 1); QCOMPARE(m_objectsModified.count(), 0); QCOMPARE(m_objectsAdded.count(), 0); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); QVERIFY(m_objectsRemoved.contains(QLatin1String("P000001"))); } catch (const MyMoneyException &) { QFAIL("Unexpected exception"); } } void MyMoneyFileTest::testPayeeWithIdentifier() { MyMoneyPayee p; try { MyMoneyFileTransaction ft; m->addPayee(p); ft.commit(); p = m->payee(p.id()); payeeIdentifier ident = payeeIdentifier(new payeeIdentifiers::ibanBic()); payeeIdentifierTyped iban(ident); iban->setIban(QLatin1String("DE82 2007 0024 0066 6446 00")); ft.restart(); p.addPayeeIdentifier(iban); m->modifyPayee(p); ft.commit(); p = m->payee(p.id()); QCOMPARE(p.payeeIdentifiers().count(), 1); ident = p.payeeIdentifiers().first(); try { iban = payeeIdentifierTyped(ident); } catch (...) { QFAIL("Unexpected exception"); } QCOMPARE(iban->electronicIban(), QLatin1String("DE82200700240066644600")); } catch (const MyMoneyException &e) { unexpectedException(e); } } void MyMoneyFileTest::testAddTransactionStd() { testAddAccounts(); MyMoneyTransaction t, p; MyMoneyAccount a; a = m->account("A000001"); // construct a transaction and add it to the pool t.setPostDate(QDate(2002, 2, 1)); t.setMemo("Memotext"); MyMoneySplit split1; MyMoneySplit split2; split1.setAccountId("A000001"); split1.setShares(MyMoneyMoney(-1000, 100)); split1.setValue(MyMoneyMoney(-1000, 100)); split2.setAccountId(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Expense)); split2.setValue(MyMoneyMoney(1000, 100)); split2.setShares(MyMoneyMoney(1000, 100)); try { t.addSplit(split1); t.addSplit(split2); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } /* // FIXME: we don't have a payee and a number field right now // guess we should have a number field per split, don't know // about the payee t.setMethod(MyMoneyCheckingTransaction::Withdrawal); t.setPayee("Thomas Baumgart"); t.setNumber("1234"); t.setState(MyMoneyCheckingTransaction::Cleared); */ storage->d_func()->m_dirty = false; MyMoneyFileTransaction ft; try { m->addTransaction(t); ft.commit(); QFAIL("Missing expected exception!"); } catch (const MyMoneyException &) { } QCOMPARE(m->dirty(), false); } void MyMoneyFileTest::testAttachStorage() { MyMoneyStorageMgr *store = new MyMoneyStorageMgr; MyMoneyFile *file = new MyMoneyFile; QCOMPARE(file->storageAttached(), false); try { file->attachStorage(store); QCOMPARE(file->storageAttached(), true); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } try { file->attachStorage(store); QFAIL("Exception expected!"); } catch (const MyMoneyException &) { } try { file->attachStorage(0); QFAIL("Exception expected!"); } catch (const MyMoneyException &) { } try { file->detachStorage(store); QCOMPARE(file->storageAttached(), false); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } delete store; delete file; } void MyMoneyFileTest::testAccount2Category() { testReparentAccount(); QCOMPARE(m->accountToCategory("A000001"), QLatin1String("Account2:Account1")); QCOMPARE(m->accountToCategory("A000002"), QLatin1String("Account2")); } void MyMoneyFileTest::testCategory2Account() { testAddTransaction(); MyMoneyAccount a = m->account("A000003"); MyMoneyAccount b = m->account("A000004"); MyMoneyFileTransaction ft; try { m->reparentAccount(b, a); ft.commit(); QCOMPARE(m->categoryToAccount("Expense1"), QLatin1String("A000003")); QCOMPARE(m->categoryToAccount("Expense1:Expense2"), QLatin1String("A000004")); QVERIFY(m->categoryToAccount("Acc2").isEmpty()); } catch (const MyMoneyException &e) { unexpectedException(e); } } void MyMoneyFileTest::testAttachedStorage() { QCOMPARE(m->storageAttached(), true); QVERIFY(m->storage() != 0); MyMoneyStorageMgr *p = m->storage(); m->detachStorage(p); QCOMPARE(m->storageAttached(), false); QCOMPARE(m->storage(), static_cast(0)); m->attachStorage(p); QCOMPARE(m->storageAttached(), true); QVERIFY(m->storage() != 0); } void MyMoneyFileTest::testHasAccount() { testAddAccounts(); MyMoneyAccount a, b; a.setAccountType(eMyMoney::Account::Type::Checkings); a.setName("Account3"); b = m->account("A000001"); MyMoneyFileTransaction ft; try { m->addAccount(a, b); ft.commit(); QCOMPARE(m->accountCount(), static_cast(8)); QCOMPARE(a.parentAccountId(), QLatin1String("A000001")); QCOMPARE(m->hasAccount("A000001", "Account3"), true); QCOMPARE(m->hasAccount("A000001", "Account2"), false); QCOMPARE(m->hasAccount("A000002", "Account3"), false); } catch (const MyMoneyException &e) { unexpectedException(e); } } void MyMoneyFileTest::testAddEquityAccount() { MyMoneyAccount i; i.setName("Investment"); i.setAccountType(eMyMoney::Account::Type::Investment); MyMoneyFileTransaction ft; try { MyMoneyAccount parent = m->asset(); m->addAccount(i, parent); ft.commit(); } catch (const MyMoneyException &e) { unexpectedException(e); } // keep a copy for later use m_inv = i; // make sure, that only equity accounts can be children to it MyMoneyAccount a; a.setName("Testaccount"); QList list; list << eMyMoney::Account::Type::Checkings; list << eMyMoney::Account::Type::Savings; list << eMyMoney::Account::Type::Cash; list << eMyMoney::Account::Type::CreditCard; list << eMyMoney::Account::Type::Loan; list << eMyMoney::Account::Type::CertificateDep; list << eMyMoney::Account::Type::Investment; list << eMyMoney::Account::Type::MoneyMarket; list << eMyMoney::Account::Type::Asset; list << eMyMoney::Account::Type::Liability; list << eMyMoney::Account::Type::Currency; list << eMyMoney::Account::Type::Income; list << eMyMoney::Account::Type::Expense; list << eMyMoney::Account::Type::AssetLoan; QList::Iterator it; for (it = list.begin(); it != list.end(); ++it) { a.setAccountType(*it); ft.restart(); try { char msg[100]; m->addAccount(a, i); sprintf(msg, "Can add non-equity type %d to investment", (int)*it); QFAIL(msg); } catch (const MyMoneyException &) { ft.commit(); } } ft.restart(); try { a.setName("Teststock"); a.setAccountType(eMyMoney::Account::Type::Stock); m->addAccount(a, i); ft.commit(); } catch (const MyMoneyException &e) { unexpectedException(e); } } void MyMoneyFileTest::testReparentEquity() { testAddEquityAccount(); testAddEquityAccount(); MyMoneyAccount parent; // check the bad cases QList list; list << eMyMoney::Account::Type::Checkings; list << eMyMoney::Account::Type::Savings; list << eMyMoney::Account::Type::Cash; list << eMyMoney::Account::Type::CertificateDep; list << eMyMoney::Account::Type::MoneyMarket; list << eMyMoney::Account::Type::Asset; list << eMyMoney::Account::Type::AssetLoan; list << eMyMoney::Account::Type::Currency; parent = m->asset(); testReparentEquity(list, parent); list.clear(); list << eMyMoney::Account::Type::CreditCard; list << eMyMoney::Account::Type::Loan; list << eMyMoney::Account::Type::Liability; parent = m->liability(); testReparentEquity(list, parent); list.clear(); list << eMyMoney::Account::Type::Income; parent = m->income(); testReparentEquity(list, parent); list.clear(); list << eMyMoney::Account::Type::Expense; parent = m->expense(); testReparentEquity(list, parent); // now check the good case MyMoneyAccount stock = m->account("A000002"); MyMoneyAccount inv = m->account(m_inv.id()); MyMoneyFileTransaction ft; try { m->reparentAccount(stock, inv); ft.commit(); } catch (const MyMoneyException &e) { unexpectedException(e); } } void MyMoneyFileTest::testReparentEquity(QList& list, MyMoneyAccount& parent) { MyMoneyAccount a; MyMoneyAccount stock = m->account("A000002"); QList::Iterator it; MyMoneyFileTransaction ft; for (it = list.begin(); it != list.end(); ++it) { a.setName(QString("Testaccount %1").arg((int)*it)); a.setAccountType(*it); try { m->addAccount(a, parent); char msg[100]; m->reparentAccount(stock, a); sprintf(msg, "Can reparent stock to non-investment type %d account", (int)*it); QFAIL(msg); } catch (const MyMoneyException &) { ft.commit(); } ft.restart(); } } void MyMoneyFileTest::testBaseCurrency() { MyMoneySecurity base("EUR", "Euro", QChar(0x20ac)); MyMoneySecurity ref; // make sure, no base currency is set try { ref = m->baseCurrency(); QVERIFY(ref.id().isEmpty()); } catch (const MyMoneyException &e) { unexpectedException(e); } // make sure, we cannot assign an unknown currency try { m->setBaseCurrency(base); QFAIL("Missing expected exception"); } catch (const MyMoneyException &) { } MyMoneyFileTransaction ft; // add the currency and try again try { m->addCurrency(base); m->setBaseCurrency(base); ft.commit(); } catch (const MyMoneyException &e) { unexpectedException(e); } ft.restart(); // make sure, the base currency is set try { ref = m->baseCurrency(); QCOMPARE(ref.id(), QLatin1String("EUR")); QCOMPARE(ref.name(), QLatin1String("Euro")); QVERIFY(ref.tradingSymbol() == QChar(0x20ac)); } catch (const MyMoneyException &e) { unexpectedException(e); } // check if it gets reset when attaching a new storage m->detachStorage(storage); MyMoneyStorageMgr* newStorage = new MyMoneyStorageMgr; m->attachStorage(newStorage); ref = m->baseCurrency(); QVERIFY(ref.id().isEmpty()); m->detachStorage(newStorage); delete newStorage; m->attachStorage(storage); ref = m->baseCurrency(); QCOMPARE(ref.id(), QLatin1String("EUR")); QCOMPARE(ref.name(), QLatin1String("Euro")); QVERIFY(ref.tradingSymbol() == QChar(0x20ac)); } void MyMoneyFileTest::testOpeningBalanceNoBase() { MyMoneyAccount openingAcc; MyMoneySecurity base; try { base = m->baseCurrency(); openingAcc = m->openingBalanceAccount(base); QFAIL("Missing expected exception"); } catch (const MyMoneyException &) { } } void MyMoneyFileTest::testOpeningBalance() { MyMoneyAccount openingAcc; MyMoneySecurity second("USD", "US Dollar", "$"); testBaseCurrency(); try { openingAcc = m->openingBalanceAccount(m->baseCurrency()); QCOMPARE(openingAcc.parentAccountId(), m->equity().id()); QCOMPARE(openingAcc.name(), MyMoneyFile::openingBalancesPrefix()); QCOMPARE(openingAcc.openingDate(), QDate::currentDate()); } catch (const MyMoneyException &e) { unexpectedException(e); } // add a second currency MyMoneyFileTransaction ft; try { m->addCurrency(second); ft.commit(); } catch (const MyMoneyException &e) { unexpectedException(e); } QString refName = QString("%1 (%2)").arg(MyMoneyFile::openingBalancesPrefix()).arg("USD"); try { openingAcc = m->openingBalanceAccount(second); QCOMPARE(openingAcc.parentAccountId(), m->equity().id()); QCOMPARE(openingAcc.name(), refName); QCOMPARE(openingAcc.openingDate(), QDate::currentDate()); } catch (const MyMoneyException &e) { unexpectedException(e); } } void MyMoneyFileTest::testModifyStdAccount() { QVERIFY(m->asset().currencyId().isEmpty()); QCOMPARE(m->asset().name(), QLatin1String("Asset")); testBaseCurrency(); QVERIFY(m->asset().currencyId().isEmpty()); QVERIFY(!m->baseCurrency().id().isEmpty()); MyMoneyFileTransaction ft; try { MyMoneyAccount acc = m->asset(); acc.setName("Anlagen"); acc.setCurrencyId(m->baseCurrency().id()); m->modifyAccount(acc); ft.commit(); QCOMPARE(m->asset().name(), QLatin1String("Anlagen")); QCOMPARE(m->asset().currencyId(), m->baseCurrency().id()); } catch (const MyMoneyException &e) { unexpectedException(e); } ft.restart(); try { MyMoneyAccount acc = m->asset(); acc.setNumber("Test"); m->modifyAccount(acc); QFAIL("Missing expected exception"); } catch (const MyMoneyException &) { ft.rollback(); } } void MyMoneyFileTest::testAddPrice() { testAddAccounts(); testBaseCurrency(); MyMoneyAccount p; MyMoneyFileTransaction ft; try { p = m->account("A000002"); p.setCurrencyId("RON"); m->modifyAccount(p); ft.commit(); QCOMPARE(m->account("A000002").currencyId(), QLatin1String("RON")); } catch (const MyMoneyException &e) { unexpectedException(e); } clearObjectLists(); ft.restart(); MyMoneyPrice price("EUR", "RON", QDate::currentDate(), MyMoneyMoney(4.1), "Test source"); m->addPrice(price); ft.commit(); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 1); QCOMPARE(m_valueChanged.count("A000002"), 1); clearObjectLists(); ft.restart(); MyMoneyPrice priceReciprocal("RON", "EUR", QDate::currentDate(), MyMoneyMoney(1 / 4.1), "Test source reciprocal price"); m->addPrice(priceReciprocal); ft.commit(); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 1); QCOMPARE(m_valueChanged.count("A000002"), 1); } void MyMoneyFileTest::testRemovePrice() { testAddPrice(); clearObjectLists(); MyMoneyFileTransaction ft; MyMoneyPrice price("EUR", "RON", QDate::currentDate(), MyMoneyMoney(4.1), "Test source"); m->removePrice(price); ft.commit(); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 1); QCOMPARE(m_valueChanged.count("A000002"), 1); } void MyMoneyFileTest::testGetPrice() { testAddPrice(); // the price for the current date is found QVERIFY(m->price("EUR", "RON", QDate::currentDate()).isValid()); // the price for the current date is returned when asking for the next day with exact date set to false { const MyMoneyPrice &price = m->price("EUR", "RON", QDate::currentDate().addDays(1), false); QVERIFY(price.isValid() && price.date() == QDate::currentDate()); } // no price is returned while asking for the next day with exact date set to true QVERIFY(!m->price("EUR", "RON", QDate::currentDate().addDays(1), true).isValid()); // no price is returned while asking for the previous day with exact date set to true/false because all prices are newer QVERIFY(!m->price("EUR", "RON", QDate::currentDate().addDays(-1), false).isValid()); QVERIFY(!m->price("EUR", "RON", QDate::currentDate().addDays(-1), true).isValid()); // add two more prices MyMoneyFileTransaction ft; m->addPrice(MyMoneyPrice("EUR", "RON", QDate::currentDate().addDays(3), MyMoneyMoney(4.1), "Test source")); m->addPrice(MyMoneyPrice("EUR", "RON", QDate::currentDate().addDays(5), MyMoneyMoney(4.1), "Test source")); ft.commit(); clearObjectLists(); // extra tests for the exactDate=false behavior { const MyMoneyPrice &price = m->price("EUR", "RON", QDate::currentDate().addDays(2), false); QVERIFY(price.isValid() && price.date() == QDate::currentDate()); } { const MyMoneyPrice &price = m->price("EUR", "RON", QDate::currentDate().addDays(3), false); QVERIFY(price.isValid() && price.date() == QDate::currentDate().addDays(3)); } { const MyMoneyPrice &price = m->price("EUR", "RON", QDate::currentDate().addDays(4), false); QVERIFY(price.isValid() && price.date() == QDate::currentDate().addDays(3)); } { const MyMoneyPrice &price = m->price("EUR", "RON", QDate::currentDate().addDays(5), false); QVERIFY(price.isValid() && price.date() == QDate::currentDate().addDays(5)); } { const MyMoneyPrice &price = m->price("EUR", "RON", QDate::currentDate().addDays(6), false); QVERIFY(price.isValid() && price.date() == QDate::currentDate().addDays(5)); } } void MyMoneyFileTest::testAddAccountMissingCurrency() { testAddTwoInstitutions(); MyMoneySecurity base("EUR", "Euro", QChar(0x20ac)); MyMoneyAccount a; a.setAccountType(eMyMoney::Account::Type::Checkings); MyMoneyInstitution institution; storage->d_func()->m_dirty = false; QCOMPARE(m->accountCount(), static_cast(5)); institution = m->institution("I000001"); QCOMPARE(institution.id(), QLatin1String("I000001")); a.setName("Account1"); a.setInstitutionId(institution.id()); clearObjectLists(); MyMoneyFileTransaction ft; try { m->addCurrency(base); m->setBaseCurrency(base); MyMoneyAccount parent = m->asset(); m->addAccount(a, parent); ft.commit(); QCOMPARE(m->account("A000001").currencyId(), QLatin1String("EUR")); } catch (const MyMoneyException &e) { unexpectedException(e); } } void MyMoneyFileTest::testAddTransactionToClosedAccount() { QSKIP("Test not implemented yet", SkipAll); } void MyMoneyFileTest::testRemoveTransactionFromClosedAccount() { QSKIP("Test not implemented yet", SkipAll); } void MyMoneyFileTest::testModifyTransactionInClosedAccount() { QSKIP("Test not implemented yet", SkipAll); } void MyMoneyFileTest::testStorageId() { QString id; // make sure id will be setup if it does not exist MyMoneyFileTransaction ft; try { m->setValue("kmm-id", ""); ft.commit(); } catch (const MyMoneyException &e) { unexpectedException(e); } try { // check for a new id id = m->storageId(); QVERIFY(!id.isEmpty()); // check that it is the same if we ask again QCOMPARE(id, m->storageId()); } catch (const MyMoneyException &e) { unexpectedException(e); } } void MyMoneyFileTest::testHasMatchingOnlineBalance_emptyAccountWithoutImportedBalance() { AddOneAccount(); MyMoneyAccount a = m->account("A000001"); QCOMPARE(m->hasMatchingOnlineBalance(a), false); } void MyMoneyFileTest::testHasMatchingOnlineBalance_emptyAccountWithEqualImportedBalance() { AddOneAccount(); MyMoneyAccount a = m->account("A000001"); a.setValue("lastImportedTransactionDate", QDate(2011, 12, 1).toString(Qt::ISODate)); a.setValue("lastStatementBalance", MyMoneyMoney().toString()); MyMoneyFileTransaction ft; m->modifyAccount(a); ft.commit(); QCOMPARE(m->hasMatchingOnlineBalance(a), true); } void MyMoneyFileTest::testHasMatchingOnlineBalance_emptyAccountWithUnequalImportedBalance() { AddOneAccount(); MyMoneyAccount a = m->account("A000001"); a.setValue("lastImportedTransactionDate", QDate(2011, 12, 1).toString(Qt::ISODate)); a.setValue("lastStatementBalance", MyMoneyMoney::ONE.toString()); MyMoneyFileTransaction ft; m->modifyAccount(a); ft.commit(); QCOMPARE(m->hasMatchingOnlineBalance(a), false); } void MyMoneyFileTest::testHasNewerTransaction_withoutAnyTransaction_afterLastImportedTransaction() { AddOneAccount(); MyMoneyAccount a = m->account("A000001"); QDate dateOfLastTransactionImport(2011, 12, 1); // There are no transactions at all: QCOMPARE(m->hasNewerTransaction(a.id(), dateOfLastTransactionImport), false); } void MyMoneyFileTest::testHasNewerTransaction_withoutNewerTransaction_afterLastImportedTransaction() { AddOneAccount(); QString accId("A000001"); QDate dateOfLastTransactionImport(2011, 12, 1); MyMoneyFileTransaction ft; MyMoneyTransaction t; // construct a transaction at the day of the last transaction import and add it to the pool t.setPostDate(dateOfLastTransactionImport); MyMoneySplit split1; split1.setAccountId(accId); split1.setShares(MyMoneyMoney(-1000, 100)); split1.setValue(MyMoneyMoney(-1000, 100)); t.addSplit(split1); ft.restart(); m->addTransaction(t); ft.commit(); QCOMPARE(m->hasNewerTransaction(accId, dateOfLastTransactionImport), false); } void MyMoneyFileTest::testHasNewerTransaction_withNewerTransaction_afterLastImportedTransaction() { AddOneAccount(); QString accId("A000001"); QDate dateOfLastTransactionImport(2011, 12, 1); QDate dateOfDayAfterLastTransactionImport(dateOfLastTransactionImport.addDays(1)); MyMoneyFileTransaction ft; MyMoneyTransaction t; // construct a transaction a day after the last transaction import and add it to the pool t.setPostDate(dateOfDayAfterLastTransactionImport); MyMoneySplit split1; split1.setAccountId(accId); split1.setShares(MyMoneyMoney(-1000, 100)); split1.setValue(MyMoneyMoney(-1000, 100)); t.addSplit(split1); ft.restart(); m->addTransaction(t); ft.commit(); QCOMPARE(m->hasNewerTransaction(accId, dateOfLastTransactionImport), true); } void MyMoneyFileTest::AddOneAccount() { QString accountId = "A000001"; MyMoneyAccount a; a.setAccountType(eMyMoney::Account::Type::Checkings); storage->d_func()->m_dirty = false; QCOMPARE(m->accountCount(), static_cast(5)); a.setName("Account1"); a.setCurrencyId("EUR"); clearObjectLists(); MyMoneyFileTransaction ft; try { MyMoneyAccount parent = m->asset(); m->addAccount(a, parent); ft.commit(); QCOMPARE(m->accountCount(), static_cast(6)); QCOMPARE(a.parentAccountId(), QLatin1String("AStd::Asset")); QCOMPARE(a.id(), accountId); QCOMPARE(a.currencyId(), QLatin1String("EUR")); QCOMPARE(m->dirty(), true); QCOMPARE(m->asset().accountList().count(), 1); QCOMPARE(m->asset().accountList()[0], accountId); QCOMPARE(m_objectsRemoved.count(), 0); QCOMPARE(m_objectsAdded.count(), 1); QCOMPARE(m_objectsModified.count(), 1); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); QVERIFY(m_objectsAdded.contains(accountId.toLatin1())); QVERIFY(m_objectsModified.contains(QLatin1String("AStd::Asset"))); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } } void MyMoneyFileTest::testCountTransactionsWithSpecificReconciliationState_noTransactions() { AddOneAccount(); QString accountId = "A000001"; QCOMPARE(m->countTransactionsWithSpecificReconciliationState(accountId, eMyMoney::TransactionFilter::State::NotReconciled), 0); } void MyMoneyFileTest::testCountTransactionsWithSpecificReconciliationState_transactionWithWantedReconcileState() { AddOneAccount(); QString accountId = "A000001"; // construct split & transaction MyMoneySplit split; split.setAccountId(accountId); split.setShares(MyMoneyMoney(-1000, 100)); split.setValue(MyMoneyMoney(-1000, 100)); MyMoneyTransaction transaction; transaction.setPostDate(QDate(2013, 1, 1)); transaction.addSplit(split); // add transaction MyMoneyFileTransaction ft; m->addTransaction(transaction); ft.commit(); QCOMPARE(m->countTransactionsWithSpecificReconciliationState(accountId, eMyMoney::TransactionFilter::State::NotReconciled), 1); } void MyMoneyFileTest::testCountTransactionsWithSpecificReconciliationState_transactionWithUnwantedReconcileState() { AddOneAccount(); QString accountId = "A000001"; // construct split & transaction MyMoneySplit split; split.setAccountId(accountId); split.setShares(MyMoneyMoney(-1000, 100)); split.setValue(MyMoneyMoney(-1000, 100)); split.setReconcileFlag(eMyMoney::Split::State::Reconciled); MyMoneyTransaction transaction; transaction.setPostDate(QDate(2013, 1, 1)); transaction.addSplit(split); // add transaction MyMoneyFileTransaction ft; m->addTransaction(transaction); ft.commit(); QCOMPARE(m->countTransactionsWithSpecificReconciliationState(accountId, eMyMoney::TransactionFilter::State::NotReconciled), 0); } void MyMoneyFileTest::testAddOnlineJob() { QSKIP("Need dummy task for this test", SkipAll); #if 0 // Add a onlineJob onlineJob job(new germanOnlineTransfer()); MyMoneyFileTransaction ft; m->addOnlineJob(job); QCOMPARE(job.id(), QString("O000001")); ft.commit(); QCOMPARE(m_objectsRemoved.count(), 0); QCOMPARE(m_objectsAdded.count(), 1); QCOMPARE(m_objectsModified.count(), 0); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); #endif } void MyMoneyFileTest::testGetOnlineJob() { QSKIP("Need dummy task for this test", SkipAll); #if 0 testAddOnlineJob(); const onlineJob requestedJob = m->getOnlineJob("O000001"); QVERIFY(!requestedJob.isNull()); QCOMPARE(requestedJob.id(), QString("O000001")); #endif } void MyMoneyFileTest::testRemoveOnlineJob() { QSKIP("Need dummy task for this test", SkipAll); #if 0 // Add a onlineJob onlineJob job(new germanOnlineTransfer()); onlineJob job2(new germanOnlineTransfer()); onlineJob job3(new germanOnlineTransfer()); MyMoneyFileTransaction ft; m->addOnlineJob(job); m->addOnlineJob(job2); m->addOnlineJob(job3); ft.commit(); clearObjectLists(); ft.restart(); m->removeOnlineJob(job); m->removeOnlineJob(job2); ft.commit(); QCOMPARE(m_objectsRemoved.count(), 2); QCOMPARE(m_objectsAdded.count(), 0); QCOMPARE(m_objectsModified.count(), 0); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); #endif } void MyMoneyFileTest::testOnlineJobRollback() { QSKIP("Need dummy task for this test", SkipAll); #if 0 // Add a onlineJob onlineJob job(new germanOnlineTransfer()); onlineJob job2(new germanOnlineTransfer()); onlineJob job3(new germanOnlineTransfer()); MyMoneyFileTransaction ft; m->addOnlineJob(job); m->addOnlineJob(job2); m->addOnlineJob(job3); ft.rollback(); QCOMPARE(m_objectsRemoved.count(), 0); QCOMPARE(m_objectsAdded.count(), 0); QCOMPARE(m_objectsModified.count(), 0); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); #endif } void MyMoneyFileTest::testRemoveLockedOnlineJob() { QSKIP("Need dummy task for this test", SkipAll); #if 0 // Add a onlineJob onlineJob job(new germanOnlineTransfer()); job.setLock(true); QVERIFY(job.isLocked()); MyMoneyFileTransaction ft; m->addOnlineJob(job); ft.commit(); clearObjectLists(); // Try removing locked transfer ft.restart(); m->removeOnlineJob(job); ft.commit(); QVERIFY2(m_objectsRemoved.count() == 0, "Online Job was locked, removing is not allowed"); QVERIFY(m_objectsAdded.count() == 0); QVERIFY(m_objectsModified.count() == 0); QVERIFY(m_balanceChanged.count() == 0); QVERIFY(m_valueChanged.count() == 0); #endif } /** @todo */ void MyMoneyFileTest::testModifyOnlineJob() { QSKIP("Need dummy task for this test", SkipAll); #if 0 // Add a onlineJob onlineJob job(new germanOnlineTransfer()); MyMoneyFileTransaction ft; m->addOnlineJob(job); ft.commit(); clearObjectLists(); // Modify online job job.setJobSend(); ft.restart(); m->modifyOnlineJob(job); ft.commit(); QCOMPARE(m_objectsRemoved.count(), 0); QCOMPARE(m_objectsAdded.count(), 0); QCOMPARE(m_objectsModified.count(), 1); QCOMPARE(m_balanceChanged.count(), 0); QCOMPARE(m_valueChanged.count(), 0); //onlineJob modifiedJob = m->getOnlineJob( job.id() ); //QCOMPARE(modifiedJob.responsibleAccount(), QString("Std::Assert")); #endif } void MyMoneyFileTest::testClearedBalance() { testAddTransaction(); MyMoneyTransaction t1; MyMoneyTransaction t2; // construct a transaction and add it to the pool t1.setPostDate(QDate(2002, 2, 1)); t1.setMemo("Memotext"); t2.setPostDate(QDate(2002, 2, 4)); t2.setMemo("Memotext"); MyMoneySplit split1; MyMoneySplit split2; MyMoneySplit split3; MyMoneySplit split4; MyMoneyFileTransaction ft; try { split1.setAccountId("A000002"); split1.setShares(MyMoneyMoney(-1000, 100)); split1.setValue(MyMoneyMoney(-1000, 100)); split1.setReconcileFlag(eMyMoney::Split::State::Cleared); split2.setAccountId("A000004"); split2.setValue(MyMoneyMoney(1000, 100)); split2.setShares(MyMoneyMoney(1000, 100)); split2.setReconcileFlag(eMyMoney::Split::State::Cleared); t1.addSplit(split1); t1.addSplit(split2); m->addTransaction(t1); ft.commit(); ft.restart(); QCOMPARE(t1.id(), QLatin1String("T000000000000000002")); split3.setAccountId("A000002"); split3.setShares(MyMoneyMoney(-2000, 100)); split3.setValue(MyMoneyMoney(-2000, 100)); split3.setReconcileFlag(eMyMoney::Split::State::Cleared); split4.setAccountId("A000004"); split4.setValue(MyMoneyMoney(2000, 100)); split4.setShares(MyMoneyMoney(2000, 100)); split4.setReconcileFlag(eMyMoney::Split::State::Cleared); t2.addSplit(split3); t2.addSplit(split4); m->addTransaction(t2); ft.commit(); ft.restart(); QCOMPARE(m->balance("A000001", QDate(2002, 2, 4)), MyMoneyMoney(-1000, 100)); QCOMPARE(m->balance("A000002", QDate(2002, 2, 4)), MyMoneyMoney(-3000, 100)); // Date of last cleared transaction QCOMPARE(m->clearedBalance("A000002", QDate(2002, 2, 1)), MyMoneyMoney(-1000, 100)); // Date of last transaction QCOMPARE(m->balance("A000002", QDate(2002, 2, 4)), MyMoneyMoney(-3000, 100)); // Date before first transaction QVERIFY(m->clearedBalance("A000002", QDate(2002, 1, 15)).isZero()); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } } void MyMoneyFileTest::testAdjustedValues() { // create a checking account, an expense, an investment account and a stock AddOneAccount(); MyMoneyAccount exp1; exp1.setAccountType(eMyMoney::Account::Type::Expense); exp1.setName("Expense1"); exp1.setCurrencyId("EUR"); MyMoneyFileTransaction ft; try { MyMoneyAccount parent = m->expense(); m->addAccount(exp1, parent); ft.commit(); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } testAddEquityAccount(); testBaseCurrency(); MyMoneySecurity stockSecurity(QLatin1String("Blubber"), QLatin1String("TestsockSecurity"), QLatin1String("BLUB"), 1000, 1000, 1000); stockSecurity.setTradingCurrency(QLatin1String("BLUB")); // add the security ft.restart(); try { m->addSecurity(stockSecurity); ft.commit(); } catch (const MyMoneyException &e) { unexpectedException(e); } MyMoneyAccount i = m->accountByName("Investment"); MyMoneyAccount stock; ft.restart(); try { stock.setName("Teststock"); stock.setCurrencyId(stockSecurity.id()); stock.setAccountType(eMyMoney::Account::Type::Stock); m->addAccount(stock, i); ft.commit(); } catch (const MyMoneyException &e) { unexpectedException(e); } // values taken from real example on https://bugs.kde.org/show_bug.cgi?id=345655 MyMoneySplit s1, s2, s3; s1.setAccountId(QLatin1String("A000001")); s1.setShares(MyMoneyMoney(QLatin1String("-99901/1000"))); s1.setValue(MyMoneyMoney(QLatin1String("-999/10"))); s2.setAccountId(exp1.id()); s2.setShares(MyMoneyMoney(QLatin1String("-611/250"))); s2.setValue(MyMoneyMoney(QLatin1String("-61/25"))); s3.setAccountId(stock.id()); s3.setAction(eMyMoney::Split::InvestmentTransactionType::BuyShares); s3.setShares(MyMoneyMoney(QLatin1String("64901/100000"))); s3.setPrice(MyMoneyMoney(QLatin1String("157689/1000"))); s3.setValue(MyMoneyMoney(QLatin1String("102340161/1000000"))); MyMoneyTransaction t; t.setCommodity(QLatin1String("EUR")); t.setPostDate(QDate::currentDate()); t.addSplit(s1); t.addSplit(s2); t.addSplit(s3); // make sure the split sum is not zero QVERIFY(!t.splitSum().isZero()); ft.restart(); try { m->addTransaction(t); ft.commit(); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } QCOMPARE(t.splitById(s1.id()).shares(), MyMoneyMoney(QLatin1String("-999/10"))); QCOMPARE(t.splitById(s1.id()).value(), MyMoneyMoney(QLatin1String("-999/10"))); QCOMPARE(t.splitById(s2.id()).shares(), MyMoneyMoney(QLatin1String("-61/25"))); QCOMPARE(t.splitById(s2.id()).value(), MyMoneyMoney(QLatin1String("-61/25"))); QCOMPARE(t.splitById(s3.id()).shares(), MyMoneyMoney(QLatin1String("649/1000"))); QCOMPARE(t.splitById(s3.id()).value(), MyMoneyMoney(QLatin1String("10234/100"))); QCOMPARE(t.splitById(s3.id()).price(), MyMoneyMoney(QLatin1String("157689/1000"))); QCOMPARE(t.splitSum(), MyMoneyMoney()); // now reset and check if modify also works s1.setShares(MyMoneyMoney(QLatin1String("-999/10"))); s1.setValue(MyMoneyMoney(QLatin1String("-999/10"))); s2.setShares(MyMoneyMoney(QLatin1String("-61/25"))); s2.setValue(MyMoneyMoney(QLatin1String("-61/25"))); s3.setShares(MyMoneyMoney(QLatin1String("649/1000"))); s3.setPrice(MyMoneyMoney(QLatin1String("157689/1000"))); s3.setValue(MyMoneyMoney(QLatin1String("102340161/1000000"))); t.modifySplit(s1); t.modifySplit(s2); t.modifySplit(s3); // make sure the split sum is not zero QVERIFY(!t.splitSum().isZero()); ft.restart(); try { m->modifyTransaction(t); ft.commit(); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } // we need to get the transaction from the engine, as modifyTransaction does // not return the modified values MyMoneyTransaction t2 = m->transaction(t.id()); QCOMPARE(t2.splitById(s3.id()).shares(), MyMoneyMoney(QLatin1String("649/1000"))); QCOMPARE(t2.splitById(s3.id()).value(), MyMoneyMoney(QLatin1String("10234/100"))); QCOMPARE(t2.splitById(s3.id()).price(), MyMoneyMoney(QLatin1String("157689/1000"))); QCOMPARE(t2.splitSum(), MyMoneyMoney()); } void MyMoneyFileTest::testVatAssignment() { MyMoneyAccount acc; MyMoneyAccount vat; MyMoneyAccount expense; testAddTransaction(); vat.setName("VAT"); vat.setCurrencyId("EUR"); vat.setAccountType(eMyMoney::Account::Type::Expense); // make it a VAT account vat.setValue(QLatin1String("VatRate"), QLatin1String("20/100")); MyMoneyFileTransaction ft; try { MyMoneyAccount parent = m->expense(); m->addAccount(vat, parent); QVERIFY(!vat.id().isEmpty()); acc = m->account(QLatin1String("A000001")); expense = m->account(QLatin1String("A000003")); QCOMPARE(acc.name(), QLatin1String("Account1")); QCOMPARE(expense.name(), QLatin1String("Expense1")); expense.setValue(QLatin1String("VatAccount"), vat.id()); m->modifyAccount(expense); ft.commit(); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } // the categories are setup now for gross value entry MyMoneyTransaction tr; MyMoneySplit sp; MyMoneyMoney amount(1707, 100); // setup the transaction sp.setShares(amount); sp.setValue(amount); sp.setAccountId(acc.id()); tr.addSplit(sp); sp.clearId(); sp.setShares(-amount); sp.setValue(-amount); sp.setAccountId(expense.id()); tr.addSplit(sp); QCOMPARE(m->addVATSplit(tr, acc, expense, amount), true); QCOMPARE(tr.splits().count(), 3); QCOMPARE(tr.splitByAccount(acc.id()).shares().toString(), MyMoneyMoney(1707, 100).toString()); QCOMPARE(tr.splitByAccount(expense.id()).shares().toString(), MyMoneyMoney(-1422, 100).toString()); QCOMPARE(tr.splitByAccount(vat.id()).shares().toString(), MyMoneyMoney(-285, 100).toString()); QCOMPARE(tr.splitSum().toString(), MyMoneyMoney().toString()); tr.removeSplits(); ft.restart(); try { expense.setValue(QLatin1String("VatAmount"), QLatin1String("net")); m->modifyAccount(expense); ft.commit(); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } // the categories are setup now for net value entry amount = MyMoneyMoney(1422, 100); sp.clearId(); sp.setShares(amount); sp.setValue(amount); sp.setAccountId(acc.id()); tr.addSplit(sp); sp.clearId(); sp.setShares(-amount); sp.setValue(-amount); sp.setAccountId(expense.id()); tr.addSplit(sp); QCOMPARE(m->addVATSplit(tr, acc, expense, amount), true); QCOMPARE(tr.splits().count(), 3); QCOMPARE(tr.splitByAccount(acc.id()).shares().toString(), MyMoneyMoney(1706, 100).toString()); QCOMPARE(tr.splitByAccount(expense.id()).shares().toString(), MyMoneyMoney(-1422, 100).toString()); QCOMPARE(tr.splitByAccount(vat.id()).shares().toString(), MyMoneyMoney(-284, 100).toString()); QCOMPARE(tr.splitSum().toString(), MyMoneyMoney().toString()); } void MyMoneyFileTest::testEmptyFilter() { testAddTransaction(); try { QList > tList; MyMoneyTransactionFilter filter; MyMoneyFile::instance()->transactionList(tList, filter); QCOMPARE(tList.count(), 2); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } } void MyMoneyFileTest::testAddSecurity() { // create a checking account, an expense, an investment account and a stock AddOneAccount(); MyMoneyAccount exp1; exp1.setAccountType(eMyMoney::Account::Type::Expense); exp1.setName("Expense1"); exp1.setCurrencyId("EUR"); MyMoneyFileTransaction ft; try { MyMoneyAccount parent = m->expense(); m->addAccount(exp1, parent); ft.commit(); } catch (const MyMoneyException &) { QFAIL("Unexpected exception!"); } testAddEquityAccount(); testBaseCurrency(); MyMoneySecurity stockSecurity(QLatin1String("Blubber"), QLatin1String("TestsockSecurity"), QLatin1String("BLUB"), 1000, 1000, 1000); stockSecurity.setTradingCurrency(QLatin1String("BLUB")); // add the security ft.restart(); try { m->addSecurity(stockSecurity); ft.commit(); } catch (const MyMoneyException &e) { unexpectedException(e); } // check that we can get it via the security method try { MyMoneySecurity sec = m->security(stockSecurity.id()); } catch (const MyMoneyException &e) { unexpectedException(e); } // and also via the currency method try { MyMoneySecurity sec = m->currency(stockSecurity.id()); } catch (const MyMoneyException &e) { unexpectedException(e); } } diff --git a/kmymoney/mymoney/tests/mymoneytransactionfilter-test.cpp b/kmymoney/mymoney/tests/mymoneytransactionfilter-test.cpp index 75f11cddc..228b6f1b6 100644 --- a/kmymoney/mymoney/tests/mymoneytransactionfilter-test.cpp +++ b/kmymoney/mymoney/tests/mymoneytransactionfilter-test.cpp @@ -1,787 +1,791 @@ /* * Copyright 2018 Ralf Habacker * Copyright 2018 Thomas Baumgart * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "mymoneytransactionfilter-test.h" #include #include "mymoneyenums.h" #include "mymoneytransactionfilter.h" #include "mymoneyfile.h" #include "mymoneystoragemgr.h" #include "mymoneyaccount.h" #include "mymoneypayee.h" #include "mymoneysecurity.h" #include "mymoneytag.h" #include "mymoneytransaction.h" #include "mymoneysplit.h" #include "mymoneyexception.h" // uses helper functions from reports tests #include "tests/testutilities.h" using namespace test; QTEST_GUILESS_MAIN(MyMoneyTransactionFilterTest) -// using namespace std; +MyMoneyTransactionFilterTest::MyMoneyTransactionFilterTest::MyMoneyTransactionFilterTest() + : storage(nullptr) + , file(nullptr) +{ +} void MyMoneyTransactionFilterTest::init() { storage = new MyMoneyStorageMgr; file = MyMoneyFile::instance(); file->attachStorage(storage); MyMoneyFileTransaction ft; file->addCurrency(MyMoneySecurity("USD", "US Dollar", "$")); file->setBaseCurrency(file->currency("USD")); MyMoneyPayee payeeTest("Payee 10.2"); file->addPayee(payeeTest); payeeId = payeeTest.id(); MyMoneyTag tag("Tag 10.2"); file->addTag(tag); tagIdList << tag.id(); QString acAsset = MyMoneyFile::instance()->asset().id(); QString acExpense = (MyMoneyFile::instance()->expense().id()); QString acIncome = (MyMoneyFile::instance()->income().id()); acCheckingId = makeAccount("Account 10.2", eMyMoney::Account::Type::Checkings, MyMoneyMoney(0.0), QDate(2004, 1, 1), acAsset); acExpenseId = makeAccount("Expense", eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 1, 11), acExpense); acIncomeId = makeAccount("Expense", eMyMoney::Account::Type::Expense, MyMoneyMoney(), QDate(2004, 1, 11), acIncome); ft.commit(); } void MyMoneyTransactionFilterTest::cleanup() { file->detachStorage(storage); delete storage; } void MyMoneyTransactionFilterTest::testMatchAmount() { MyMoneySplit split; split.setShares(MyMoneyMoney(123.20)); MyMoneyTransactionFilter filter; QCOMPARE(filter.matchAmount(split), true); filter.setAmountFilter(MyMoneyMoney("123.0"), MyMoneyMoney("124.0")); QCOMPARE(filter.matchAmount(split), true); filter.setAmountFilter(MyMoneyMoney("120.0"), MyMoneyMoney("123.0")); QCOMPARE(filter.matchAmount(split), false); } void MyMoneyTransactionFilterTest::testMatchText() { MyMoneySplit split; MyMoneyTransactionFilter filter; MyMoneyAccount account = file->account(acCheckingId); // no filter QCOMPARE(filter.matchText(split, account), true); filter.setTextFilter(QRegExp("10.2"), false); MyMoneyTransactionFilter filterInvert; filterInvert.setTextFilter(QRegExp("10.2"), true); MyMoneyTransactionFilter filterNotFound; filterNotFound.setTextFilter(QRegExp("10.5"), false); // memo split.setMemo("10.2"); QCOMPARE(filter.matchText(split, account), true); QCOMPARE(filterInvert.matchText(split, account), false); QCOMPARE(filterNotFound.matchText(split, account), false); split.setMemo(QString()); // payee split.setPayeeId(payeeId); QCOMPARE(filter.matchText(split, account), true); QCOMPARE(filterInvert.matchText(split, account), false); QCOMPARE(filterNotFound.matchText(split, account), false); split.setPayeeId(QString()); // tag split.setTagIdList(tagIdList); QCOMPARE(filter.matchText(split, account), true); QCOMPARE(filterInvert.matchText(split, account), false); QCOMPARE(filterNotFound.matchText(split, account), false); split.setTagIdList(QStringList()); // value split.setValue(MyMoneyMoney("10.2")); QCOMPARE(filter.matchText(split, account), true); QCOMPARE(filterInvert.matchText(split, account), false); QCOMPARE(filterNotFound.matchText(split, account), false); split.setValue(MyMoneyMoney()); // number split.setNumber("10.2"); QCOMPARE(filter.matchText(split, account), true); QCOMPARE(filterInvert.matchText(split, account), false); QCOMPARE(filterNotFound.matchText(split, account), false); split.setNumber("0.0"); // transaction id split.setTransactionId("10.2"); QCOMPARE(filter.matchText(split, account), true); QCOMPARE(filterInvert.matchText(split, account), false); QCOMPARE(filterNotFound.matchText(split, account), false); split.setTransactionId("0.0"); // account split.setAccountId(acCheckingId); QCOMPARE(filter.matchText(split, account), true); QCOMPARE(filterInvert.matchText(split, account), false); QCOMPARE(filterNotFound.matchText(split, account), false); } void MyMoneyTransactionFilterTest::testMatchSplit() { qDebug() << "returns matchText() || matchAmount(), which are already tested"; } void MyMoneyTransactionFilterTest::testMatchTransactionAll() { MyMoneySplit split; split.setAccountId(acCheckingId); split.setShares(MyMoneyMoney(123.00)); MyMoneySplit split2; split2.setAccountId(acExpenseId); split2.setShares(MyMoneyMoney(123.00)); MyMoneyTransaction transaction; transaction.setPostDate(QDate(2014, 1, 2)); transaction.addSplit(split); transaction.addSplit(split2); MyMoneyTransactionFilter filter; filter.setReportAllSplits(true); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 2); filter.setReportAllSplits(false); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 1); } void MyMoneyTransactionFilterTest::testMatchTransactionAccount() { MyMoneySplit split; split.setAccountId(acCheckingId); split.setShares(MyMoneyMoney(123.00)); MyMoneySplit split2; split2.setAccountId(acExpenseId); split2.setShares(MyMoneyMoney(123.00)); MyMoneyTransaction transaction; transaction.setPostDate(QDate(2014, 1, 2)); transaction.addSplit(split); transaction.addSplit(split2); MyMoneyTransactionFilter filter; filter.addAccount(acCheckingId); filter.setReportAllSplits(true); filter.setConsiderCategory(false); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 1); filter.setReportAllSplits(false); filter.setConsiderCategory(false); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 1); filter.setReportAllSplits(false); filter.setConsiderCategory(true); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 1); filter.setReportAllSplits(true); filter.setConsiderCategory(true); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 1); filter.clear(); } void MyMoneyTransactionFilterTest::testMatchTransactionCategory() { MyMoneySplit split; split.setAccountId(acCheckingId); split.setShares(MyMoneyMoney(123.00)); MyMoneySplit split2; split2.setAccountId(acExpenseId); split2.setShares(MyMoneyMoney(123.00)); MyMoneyTransaction transaction; transaction.setPostDate(QDate(2014, 1, 2)); transaction.addSplit(split); transaction.addSplit(split2); MyMoneyTransactionFilter filter; filter.addCategory(acExpenseId); filter.setReportAllSplits(true); filter.setConsiderCategory(true); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 2); filter.setConsiderCategory(false); QVERIFY(!filter.match(transaction)); } void MyMoneyTransactionFilterTest::testMatchTransactionDate() { MyMoneySplit split; split.setAccountId(acCheckingId); split.setShares(MyMoneyMoney(123.00)); MyMoneySplit split2; split2.setAccountId(acExpenseId); split2.setShares(MyMoneyMoney(123.00)); MyMoneyTransaction transaction; transaction.setPostDate(QDate(2014, 1, 2)); transaction.addSplit(split); transaction.addSplit(split2); MyMoneyTransactionFilter filter; filter.setReportAllSplits(true); filter.setDateFilter(QDate(2014, 1, 1), QDate(2014, 1, 3)); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 2); filter.setReportAllSplits(false); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 1); filter.setDateFilter(QDate(2014, 1, 3), QDate(2014, 1, 5)); QVERIFY(!filter.match(transaction)); } void setupTransactionForNumber(MyMoneyTransaction &transaction, const QString &accountId) { MyMoneySplit split; split.setAccountId(accountId); split.setShares(MyMoneyMoney(123.00)); split.setNumber("1"); split.setMemo("1"); MyMoneySplit split2; split2.setAccountId(accountId); split2.setShares(MyMoneyMoney(1.00)); split2.setNumber("2"); split2.setMemo("2"); MyMoneySplit split3; split3.setAccountId(accountId); split3.setShares(MyMoneyMoney(100.00)); split3.setNumber("3"); split3.setMemo("3"); MyMoneySplit split4; split4.setAccountId(accountId); split4.setShares(MyMoneyMoney(22.00)); split4.setNumber("4"); split4.setMemo("4"); transaction.setPostDate(QDate(2014, 1, 2)); transaction.addSplit(split); transaction.addSplit(split2); transaction.addSplit(split3); transaction.addSplit(split4); } void runtTestMatchTransactionNumber(MyMoneyTransaction &transaction, MyMoneyTransactionFilter &filter) { // return all matching splits filter.setReportAllSplits(true); filter.setNumberFilter("", ""); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 4); filter.setNumberFilter("1", ""); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 4); filter.setNumberFilter("", "4"); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 4); filter.setNumberFilter("1", "4"); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 4); filter.setNumberFilter("1", "2"); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 2); // do not return all matching splits filter.setReportAllSplits(false); filter.setNumberFilter("1", "4"); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 1); filter.setNumberFilter("1", "2"); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 1); } void MyMoneyTransactionFilterTest::testMatchTransactionNumber() { MyMoneyTransaction transaction; setupTransactionForNumber(transaction, acCheckingId); MyMoneyTransactionFilter filter; runtTestMatchTransactionNumber(transaction, filter); transaction.clear(); setupTransactionForNumber(transaction, acExpenseId); filter.clear(); runtTestMatchTransactionNumber(transaction, filter); } void MyMoneyTransactionFilterTest::testMatchTransactionPayee() { MyMoneySplit split; split.setAccountId(acCheckingId); split.setShares(MyMoneyMoney(123.00)); split.setPayeeId(payeeId); MyMoneySplit split2; split2.setAccountId(acCheckingId); split2.setShares(MyMoneyMoney(124.00)); MyMoneyTransaction transaction; transaction.setPostDate(QDate(2014, 1, 2)); transaction.addSplit(split); transaction.addSplit(split2); MyMoneyTransactionFilter filter; filter.addPayee(payeeId); filter.setReportAllSplits(true); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 1); filter.setReportAllSplits(false); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 1); // check no category support MyMoneySplit split3; split3.setAccountId(acExpenseId); split3.setShares(MyMoneyMoney(120.00)); split3.setPayeeId(payeeId); MyMoneyTransaction transaction2; transaction2.setPostDate(QDate(2014, 1, 2)); transaction2.addSplit(split3); filter.setReportAllSplits(true); QVERIFY(!filter.match(transaction2)); QCOMPARE(filter.matchingSplits(transaction2).size(), 0); qDebug() << "payee on categories could not be tested"; } void MyMoneyTransactionFilterTest::testMatchTransactionState() { MyMoneySplit split; split.setAccountId(acCheckingId); split.setShares(MyMoneyMoney(123.00)); split.setReconcileFlag(eMyMoney::Split::State::NotReconciled); MyMoneySplit split2; split2.setAccountId(acCheckingId); split2.setShares(MyMoneyMoney(1.00)); split2.setReconcileFlag(eMyMoney::Split::State::Cleared); MyMoneySplit split3; split3.setAccountId(acCheckingId); split3.setShares(MyMoneyMoney(100.00)); split3.setReconcileFlag(eMyMoney::Split::State::Reconciled); MyMoneySplit split4; split4.setAccountId(acCheckingId); split4.setShares(MyMoneyMoney(22.00)); split4.setReconcileFlag(eMyMoney::Split::State::Frozen); MyMoneyTransaction transaction; transaction.setPostDate(QDate(2014, 1, 2)); transaction.addSplit(split); transaction.addSplit(split2); transaction.addSplit(split3); transaction.addSplit(split4); MyMoneyTransactionFilter filter; filter.setReportAllSplits(true); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 4); // all states filter.addState((int)eMyMoney::TransactionFilter::State::NotReconciled); filter.addState((int)eMyMoney::TransactionFilter::State::Cleared); filter.addState((int)eMyMoney::TransactionFilter::State::Reconciled); filter.addState((int)eMyMoney::TransactionFilter::State::Frozen); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 4); // single state filter.clear(); filter.addState((int)eMyMoney::TransactionFilter::State::NotReconciled); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 1); filter.clear(); filter.addState((int)eMyMoney::TransactionFilter::State::Cleared); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 1); filter.clear(); filter.addState((int)eMyMoney::TransactionFilter::State::Reconciled); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 1); filter.clear(); filter.addState((int)eMyMoney::TransactionFilter::State::Frozen); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 1); // check no category support MyMoneySplit split5; split5.setAccountId(acCheckingId); split5.setShares(MyMoneyMoney(22.00)); split5.setReconcileFlag(eMyMoney::Split::State::Frozen); MyMoneyTransaction transaction2; transaction.setPostDate(QDate(2014, 1, 2)); transaction.addSplit(split5); filter.clear(); filter.setReportAllSplits(true); filter.addState((int)eMyMoney::TransactionFilter::State::Frozen); QVERIFY(!filter.match(transaction2)); QCOMPARE(filter.matchingSplits(transaction2).size(), 0); qDebug() << "states on categories could not be tested"; } void MyMoneyTransactionFilterTest::testMatchTransactionTag() { MyMoneySplit split; split.setAccountId(acCheckingId); split.setShares(MyMoneyMoney(123.00)); split.setTagIdList(tagIdList); MyMoneySplit split2; split2.setAccountId(acExpenseId); split2.setShares(MyMoneyMoney(123.00)); split2.setTagIdList(tagIdList); MyMoneySplit split3; split3.setAccountId(acCheckingId); split3.setShares(MyMoneyMoney(10.00)); split3.setTagIdList(tagIdList); MyMoneyTransaction transaction; transaction.setPostDate(QDate(2014, 1, 2)); transaction.addSplit(split); transaction.addSplit(split2); transaction.addSplit(split3); MyMoneyTransactionFilter filter; filter.addTag(tagIdList.first()); filter.setReportAllSplits(true); QVERIFY(filter.match(transaction)); // -1 because categories are not supported yet QCOMPARE(filter.matchingSplits(transaction).size(), 2); filter.setReportAllSplits(false); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 1); // check no category support MyMoneySplit split4; split4.setAccountId(acExpenseId); split4.setShares(MyMoneyMoney(123.00)); split4.setTagIdList(tagIdList); MyMoneyTransaction transaction2; transaction2.setPostDate(QDate(2014, 1, 2)); transaction2.addSplit(split4); filter.setReportAllSplits(true); QVERIFY(!filter.match(transaction2)); QCOMPARE(filter.matchingSplits(transaction2).size(), 0); qDebug() << "tags on categories could not be tested"; } void MyMoneyTransactionFilterTest::testMatchTransactionTypeAllTypes() { /* alltypes - account group == MyMoneyAccount::Income || - account group == MyMoneyAccount::Expense */ MyMoneySplit split; split.setAccountId(acExpenseId); split.setValue(MyMoneyMoney(123.00)); MyMoneySplit split2; split2.setAccountId(acIncomeId); split2.setValue(MyMoneyMoney(-123.00)); MyMoneyTransaction transaction; transaction.setPostDate(QDate(2014, 1, 2)); transaction.addSplit(split); transaction.addSplit(split2); MyMoneyTransactionFilter filter; filter.setReportAllSplits(true); // all splits QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 2); filter.addType((int)eMyMoney::TransactionFilter::State::All); qDebug() << "MyMoneyTransactionFilter::allTypes could not be tested"; qDebug() << "because type filter does not work with categories"; QVERIFY(!filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 0); // ! alltypes MyMoneySplit split3; split3.setAccountId(acCheckingId); split3.setValue(MyMoneyMoney(-123.00)); MyMoneyTransaction transaction2; transaction2.addSplit(split3); QVERIFY(!filter.match(transaction2)); QCOMPARE(filter.matchingSplits(transaction2).size(), 0); } void MyMoneyTransactionFilterTest::testMatchTransactionTypeDeposits() { // deposits - split value is positive MyMoneySplit split; split.setAccountId(acCheckingId); split.setValue(MyMoneyMoney(123.00)); MyMoneyTransaction transaction; transaction.setPostDate(QDate(2014, 1, 2)); transaction.addSplit(split); MyMoneyTransactionFilter filter; filter.setReportAllSplits(true); // all splits QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 1); // deposits filter.addType((int)eMyMoney::TransactionFilter::Type::Deposits); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 1); // no deposits MyMoneySplit split2; split2.setAccountId(acCheckingId); split2.setValue(MyMoneyMoney(-123.00)); MyMoneyTransaction transaction2; transaction2.setPostDate(QDate(2014, 1, 2)); transaction2.addSplit(split2); QVERIFY(!filter.match(transaction2)); QCOMPARE(filter.matchingSplits(transaction2).size(), 0); } void MyMoneyTransactionFilterTest::testMatchTransactionTypePayments() { /* payments - account group != MyMoneyAccount::Income - account group != MyMoneyAccount::Expense - split value is not positive - number of splits != 2 */ MyMoneySplit split; split.setAccountId(acCheckingId); split.setValue(MyMoneyMoney(-123.00)); MyMoneyTransaction transaction; transaction.setPostDate(QDate(2014, 1, 2)); transaction.addSplit(split); MyMoneyTransactionFilter filter; filter.setReportAllSplits(true); // all splits QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 1); // valid payments filter.addType((int)eMyMoney::TransactionFilter::Type::Payments); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 1); // no payments // check number of splits != 2 MyMoneySplit split2; split2.setAccountId(acCheckingId); split2.setValue(MyMoneyMoney(-123.00)); transaction.addSplit(split2); QVERIFY(!filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 0); // split value is not positive MyMoneySplit split3; split3.setAccountId(acCheckingId); split3.setValue(MyMoneyMoney(123.00)); transaction.addSplit(split3); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 2); // account group != MyMoneyAccount::Income && account group != MyMoneyAccount::Expense MyMoneySplit split4; split4.setAccountId(acExpenseId); split4.setValue(MyMoneyMoney(-124.00)); transaction.addSplit(split4); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 2); } void MyMoneyTransactionFilterTest::testMatchTransactionTypeTransfers() { /* check transfers - number of splits == 2 - account group != MyMoneyAccount::Income - account group != MyMoneyAccount::Expense */ MyMoneySplit split; split.setAccountId(acCheckingId); split.setValue(MyMoneyMoney(123.00)); MyMoneySplit split2; split2.setAccountId(acCheckingId); split2.setValue(MyMoneyMoney(-123.00)); MyMoneySplit split3; split3.setAccountId(acCheckingId); split3.setValue(MyMoneyMoney(-123.00)); MyMoneyTransaction transaction; transaction.setPostDate(QDate(2014, 1, 2)); transaction.addSplit(split); transaction.addSplit(split2); MyMoneyTransactionFilter filter; filter.setReportAllSplits(true); // all splits QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 2); filter.addType((int)eMyMoney::TransactionFilter::Type::Transfers); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 2); // transfers - invalid number of counts transaction.addSplit(split3); QVERIFY(!filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 0); // transfers - invalid account MyMoneySplit split4; split4.setAccountId(acIncomeId); split4.setValue(MyMoneyMoney(-123.00)); MyMoneySplit split5; split5.setAccountId(acCheckingId); split5.setValue(MyMoneyMoney(123.00)); MyMoneyTransaction transaction2; transaction2.setPostDate(QDate(2014, 1, 2)); transaction2.addSplit(split4); transaction2.addSplit(split5); QVERIFY(!filter.match(transaction2)); QCOMPARE(filter.matchingSplits(transaction2).size(), 0); } void MyMoneyTransactionFilterTest::testMatchTransactionValidity() { MyMoneySplit split; split.setAccountId(acCheckingId); split.setValue(MyMoneyMoney(123.00)); MyMoneySplit split2; split2.setAccountId(acExpenseId); split2.setValue(MyMoneyMoney(-123.00)); MyMoneyTransaction transaction; transaction.setPostDate(QDate(2014, 1, 2)); transaction.addSplit(split); transaction.addSplit(split2); // check valid transaction MyMoneyTransactionFilter filter; filter.addValidity((int)eMyMoney::TransactionFilter::Validity::Valid); filter.setReportAllSplits(true); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 2); filter.setReportAllSplits(false); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 1); // check invalid transaction filter.clear(); filter.addValidity((int)eMyMoney::TransactionFilter::Validity::Invalid); filter.setReportAllSplits(true); QVERIFY(!filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 0); // add split to make transaction invalid MyMoneySplit split3; split3.setAccountId(acExpenseId); split3.setValue(MyMoneyMoney(-10.00)); transaction.addSplit(split3); filter.setReportAllSplits(true); QVERIFY(filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 3); filter.clear(); filter.addValidity((int)eMyMoney::TransactionFilter::Validity::Valid); QVERIFY(!filter.match(transaction)); QCOMPARE(filter.matchingSplits(transaction).size(), 0); } diff --git a/kmymoney/mymoney/tests/mymoneytransactionfilter-test.h b/kmymoney/mymoney/tests/mymoneytransactionfilter-test.h index 96c3e87cf..aaaed562f 100644 --- a/kmymoney/mymoney/tests/mymoneytransactionfilter-test.h +++ b/kmymoney/mymoney/tests/mymoneytransactionfilter-test.h @@ -1,58 +1,61 @@ /* * Copyright 2018 Ralf Habacker * Copyright 2018 Thomas Baumgart * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef MYMONEYTRANSACTIONFILTERTEST_H #define MYMONEYTRANSACTIONFILTERTEST_H #include class MyMoneyStorageMgr; class MyMoneyFile; class MyMoneyTransactionFilterTest : public QObject { - Q_OBJECT + Q_OBJECT +public: + MyMoneyTransactionFilterTest(); + private slots: void init(); void cleanup(); void testMatchAmount(); void testMatchText(); void testMatchSplit(); void testMatchTransactionAll(); void testMatchTransactionAccount(); void testMatchTransactionCategory(); void testMatchTransactionDate(); void testMatchTransactionNumber(); void testMatchTransactionPayee(); void testMatchTransactionState(); void testMatchTransactionTag(); void testMatchTransactionTypeAllTypes(); void testMatchTransactionTypeDeposits(); void testMatchTransactionTypePayments(); void testMatchTransactionTypeTransfers(); void testMatchTransactionValidity(); private: QString payeeId; QList tagIdList; QString acCheckingId; QString acExpenseId; QString acIncomeId; MyMoneyStorageMgr* storage; MyMoneyFile* file; }; #endif diff --git a/kmymoney/plugins/csv/export/csvexportdlg.cpp b/kmymoney/plugins/csv/export/csvexportdlg.cpp index 660f36ca1..d7ddb6286 100644 --- a/kmymoney/plugins/csv/export/csvexportdlg.cpp +++ b/kmymoney/plugins/csv/export/csvexportdlg.cpp @@ -1,255 +1,254 @@ /* * Copyright 2013-2014 Allan Anderson * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "csvexportdlg.h" #include "ui_csvexportdlg.h" // ---------------------------------------------------------------------------- // QT Headers #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Headers #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Headers #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneytransaction.h" #include "mymoneytransactionfilter.h" #include "icons/icons.h" #include "mymoneyenums.h" using namespace Icons; CsvExportDlg::CsvExportDlg(QWidget *parent) : QDialog(parent), ui(new Ui::CsvExportDlg) { ui->setupUi(this); m_fieldDelimiterCharList << "," << ";" << "\t"; ui->m_separatorComboBox->setCurrentIndex(-1); // Set (almost) all the last used options readConfig(); loadAccounts(); // load button icons KGuiItem::assign(ui->m_qbuttonCancel, KStandardGuiItem::cancel()); KGuiItem okButtonItem(i18n("&Export"), Icons::get(Icon::DocumentExport), i18n("Start operation"), i18n("Use this to start the export operation")); KGuiItem::assign(ui->m_qbuttonOk, okButtonItem); KGuiItem browseButtonItem(i18n("&Browse..."), Icons::get(Icon::DocumentOpen), i18n("Select filename"), i18n("Use this to select a filename to export to")); KGuiItem::assign(ui->m_qbuttonBrowse, browseButtonItem); // connect the buttons to their functionality connect(ui->m_qbuttonBrowse, SIGNAL(clicked()), this, SLOT(slotBrowse())); connect(ui->m_qbuttonOk, SIGNAL(clicked()), this, SLOT(slotOkClicked())); connect(ui->m_qbuttonCancel, SIGNAL(clicked()), this, SLOT(reject())); // connect the change signals to the check slot and perform initial check connect(ui->m_qlineeditFile, SIGNAL(editingFinished()), this, SLOT(checkData())); connect(ui->m_radioButtonAccount, SIGNAL(toggled(bool)), this, SLOT(checkData())); connect(ui->m_radioButtonCategories, SIGNAL(toggled(bool)), this, SLOT(checkData())); connect(ui->m_accountComboBox, SIGNAL(currentIndexChanged(QString)), this, SLOT(checkData(QString))); connect(ui->m_separatorComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(separator(int))); connect(ui->m_separatorComboBox, SIGNAL(currentIndexChanged(QString)), this, SLOT(checkData())); checkData(QString()); } CsvExportDlg::~CsvExportDlg() { } void CsvExportDlg::slotBrowse() { QString newName(QFileDialog::getSaveFileName(this, QString(), QString(), QLatin1String("*.CSV"))); if (newName.indexOf('.') == -1) newName += QLatin1String(".csv"); if (!newName.isEmpty()) ui->m_qlineeditFile->setText(newName); } void CsvExportDlg::slotOkClicked() { // Make sure we save the last used settings for use next time, writeConfig(); accept(); } void CsvExportDlg::separator(int separatorIndex) { m_separator = m_fieldDelimiterCharList[separatorIndex]; } void CsvExportDlg::readConfig() { KSharedConfig::Ptr config = KSharedConfig::openConfig(QStandardPaths::locate(QStandardPaths::ConfigLocation, QLatin1String("csvexporterrc"))); KConfigGroup conf = config->group("Last Use Settings"); ui->m_qlineeditFile->setText(conf.readEntry("CsvExportDlg_LastFile")); ui->m_radioButtonAccount->setChecked(conf.readEntry("CsvExportDlg_AccountOpt", true)); ui->m_radioButtonCategories->setChecked(conf.readEntry("CsvExportDlg_CatOpt", true)); ui->m_kmymoneydateStart->setDate(conf.readEntry("CsvExportDlg_StartDate", QDate())); ui->m_kmymoneydateEnd->setDate(conf.readEntry("CsvExportDlg_EndDate", QDate())); } void CsvExportDlg::writeConfig() { KSharedConfigPtr config = KSharedConfig::openConfig(QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + QLatin1String("/csvexporterrc")); KConfigGroup grp = config->group("Last Use Settings"); grp.writeEntry("CsvExportDlg_LastFile", ui->m_qlineeditFile->text()); grp.writeEntry("CsvExportDlg_AccountOpt", ui->m_radioButtonAccount->isChecked()); grp.writeEntry("CsvExportDlg_CatOpt", ui->m_radioButtonCategories->isChecked()); grp.writeEntry("CsvExportDlg_StartDate", QDateTime(ui->m_kmymoneydateStart->date())); grp.writeEntry("CsvExportDlg_EndDate", QDateTime(ui->m_kmymoneydateEnd->date())); grp.writeEntry("CsvExportDlg_separatorIndex", ui->m_separatorComboBox->currentIndex()); config->sync(); } void CsvExportDlg::checkData(const QString& accountName) { bool okEnabled = false; if (!ui->m_qlineeditFile->text().isEmpty()) { auto strFile(ui->m_qlineeditFile->text()); if (!strFile.endsWith(QLatin1String(".csv"), Qt::CaseInsensitive)) strFile.append(QLatin1String(".csv")); ui->m_qlineeditFile->setText(strFile); } QDate earliestDate(QDate(2500, 01, 01)); QDate latestDate(QDate(1900, 01, 01)); QList listTrans; QList::Iterator itTrans; MyMoneyAccount account; MyMoneyFile* file = MyMoneyFile::instance(); if (!accountName.isEmpty()) { account = file->accountByName(accountName); m_accountId = account.id(); MyMoneyAccount accnt; if (account.accountType() == eMyMoney::Account::Type::Investment) { // If this is Investment account, we need child account. foreach (const auto sAccount, account.accountList()) { accnt = file->account(sAccount); MyMoneyTransactionFilter filter(accnt.id()); listTrans = file->transactionList(filter); if (!listTrans.isEmpty()) { if (listTrans[0].postDate() < earliestDate) { earliestDate = listTrans[0].postDate(); } latestDate = listTrans[listTrans.count() - 1].postDate(); } } } else { //Checking, etc. MyMoneyTransactionFilter filter(account.id()); listTrans = file->transactionList(filter); if (listTrans.isEmpty()) { KMessageBox::sorry(0, i18n("There are no entries in this account.\n"), i18n("Invalid account")); return; } earliestDate = listTrans[0].postDate(); latestDate = listTrans[listTrans.count() - 1].postDate(); } ui->m_kmymoneydateStart->setDate(earliestDate); ui->m_kmymoneydateEnd->setDate(latestDate); ui->m_accountComboBox->setCompletedText(accnt.id()); } if (!ui->m_qlineeditFile->text().isEmpty() && !ui->m_accountComboBox->currentText().isEmpty() && ui->m_kmymoneydateStart->date() <= ui->m_kmymoneydateEnd->date() && (ui->m_radioButtonAccount->isChecked() || ui->m_radioButtonCategories->isChecked()) && (ui->m_separatorComboBox->currentIndex() >= 0)) { okEnabled = true; } ui->m_qbuttonOk->setEnabled(okEnabled); } void CsvExportDlg::loadAccounts() { QStringList lst = getAccounts(); for (int i = 0; i < lst.count(); i++) { ui->m_accountComboBox->addItem(lst[i]); } ui->m_accountComboBox->setCurrentIndex(-1); } QStringList CsvExportDlg::getAccounts() { QStringList list; MyMoneyFile* file = MyMoneyFile::instance(); - QString accountId; // Get a list of all accounts QList accounts; file->accountList(accounts); QList::const_iterator it_account = accounts.constBegin(); m_idList.clear(); while (it_account != accounts.constEnd()) { MyMoneyAccount account((*it_account).id(), (*it_account)); if (!account.isClosed()) { eMyMoney::Account::Type accntType = account.accountType(); eMyMoney::Account::Type accntGroup = account.accountGroup(); if ((accntGroup == eMyMoney::Account::Type::Liability) || ((accntGroup == eMyMoney::Account::Type::Asset) && (accntType != eMyMoney::Account::Type::Stock))) { // ie Asset or Liability types list << account.name(); m_idList << account.id(); } } ++it_account; } qSort(list.begin(), list.end(), caseInsensitiveLessThan); return list; } void CsvExportDlg::slotStatusProgressBar(int current, int total) { if (total == -1 && current == -1) { // reset ui->progressBar->setValue(ui->progressBar->maximum()); } else if (total != 0) { // init ui->progressBar->setMaximum(total); ui->progressBar->setValue(0); ui->progressBar->show(); } else { // update ui->progressBar->setValue(current); } update(); } bool caseInsensitiveLessThan(const QString &s1, const QString &s2) { return s1.toLower() < s2.toLower(); } diff --git a/kmymoney/plugins/csv/export/csvwriter.cpp b/kmymoney/plugins/csv/export/csvwriter.cpp index 9a99fb3b9..7816ad5ee 100644 --- a/kmymoney/plugins/csv/export/csvwriter.cpp +++ b/kmymoney/plugins/csv/export/csvwriter.cpp @@ -1,475 +1,474 @@ /* * Copyright 2013-2014 Allan Anderson * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "csvwriter.h" // ---------------------------------------------------------------------------- // QT Headers #include #include #include #include // ---------------------------------------------------------------------------- // KDE Headers #include #include // ---------------------------------------------------------------------------- // Project Headers #include "mymoneymoney.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneytransaction.h" #include "mymoneytransactionfilter.h" #include "mymoneysplit.h" #include "mymoneypayee.h" #include "mymoneyexception.h" #include "csvexportdlg.h" #include "csvexporter.h" #include "mymoneyenums.h" CsvWriter::CsvWriter() : m_plugin(0), m_firstSplit(false), m_highestSplitCount(0), m_noError(true) { } CsvWriter::~CsvWriter() { } void CsvWriter::write(const QString& filename, const QString& accountId, const bool accountData, const bool categoryData, const QDate& startDate, const QDate& endDate, const QString& separator) { m_separator = separator; QFile csvFile(filename); if (csvFile.open(QIODevice::WriteOnly)) { QTextStream s(&csvFile); s.setCodec("UTF-8"); m_plugin->exporterDialog()->show(); try { if (categoryData) { writeCategoryEntries(s); } if (accountData) { writeAccountEntry(s, accountId, startDate, endDate); } emit signalProgress(-1, -1); } catch (const MyMoneyException &e) { KMessageBox::error(nullptr, i18n("Unexpected exception '%1'", QString::fromLatin1(e.what()))); } csvFile.close(); qDebug() << i18n("Export completed.\n"); delete m_plugin->exporterDialog(); // Can now delete as export finished } else { KMessageBox::error(0, i18n("Unable to open file '%1' for writing", filename)); } } void CsvWriter::writeAccountEntry(QTextStream& stream, const QString& accountId, const QDate& startDate, const QDate& endDate) { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyAccount account; QString data; account = file->account(accountId); MyMoneyTransactionFilter filter(accountId); QString type = account.accountTypeToString(account.accountType()); data = QString(i18n("Account Type:")); if (account.accountType() == eMyMoney::Account::Type::Investment) { data += QString("%1\n\n").arg(type); m_headerLine << QString(i18n("Date")) << QString(i18n("Security")) << QString(i18n("Action/Type")) << QString(i18n("Amount")) << QString(i18n("Quantity")) << QString(i18n("Price")) << QString(i18n("Interest")) << QString(i18n("Fees")) << QString(i18n("Account")) << QString(i18n("Memo")) << QString(i18n("Status")); data += m_headerLine.join(m_separator); extractInvestmentEntries(accountId, startDate, endDate); } else { data += QString("%1\n\n").arg(type); m_headerLine << QString(i18n("Date")) << QString(i18n("Payee")) << QString(i18n("Amount")) << QString(i18n("Account/Cat")) << QString(i18n("Memo")) << QString(i18n("Status")) << QString(i18n("Number")); filter.setDateFilter(startDate, endDate); QList trList = file->transactionList(filter); QList::ConstIterator it; signalProgress(0, trList.count()); int count = 0; m_highestSplitCount = 0; for (it = trList.constBegin(); it != trList.constEnd(); ++it) { writeTransactionEntry(*it, accountId, ++count); if (m_noError) signalProgress(count, 0); } data += m_headerLine.join(m_separator); } QString result; QMap::const_iterator it_map = m_map.constBegin(); while (it_map != m_map.constEnd()) { result += it_map.value(); ++it_map; } stream << data << result << QLatin1Char('\n'); } void CsvWriter::writeCategoryEntries(QTextStream &s) { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyAccount income; MyMoneyAccount expense; income = file->income(); expense = file->expense(); QStringList list = income.accountList() + expense.accountList(); emit signalProgress(0, list.count()); QStringList::Iterator it_catList; int count = 0; for (it_catList = list.begin(); it_catList != list.end(); ++it_catList) { writeCategoryEntry(s, *it_catList, ""); emit signalProgress(++count, 0); } } void CsvWriter::writeCategoryEntry(QTextStream &s, const QString& accountId, const QString& leadIn) { MyMoneyAccount acc = MyMoneyFile::instance()->account(accountId); QString name = format(acc.name()); s << leadIn << name; s << (acc.accountGroup() == eMyMoney::Account::Type::Expense ? QLatin1Char('E') : QLatin1Char('I')); s << endl; foreach (const auto sAccount, acc.accountList()) writeCategoryEntry(s, sAccount, name); } void CsvWriter::writeTransactionEntry(const MyMoneyTransaction& t, const QString& accountId, const int count) { m_firstSplit = true; m_noError = true; MyMoneyFile* file = MyMoneyFile::instance(); MyMoneySplit split = t.splitByAccount(accountId); QList splits = t.splits(); if (splits.count() < 2) { KMessageBox::sorry(0, i18n("Transaction number '%1' is missing an account assignment.\n" "Date '%2', Payee '%3'.\nTransaction dropped.\n", count, t.postDate().toString(Qt::ISODate), file->payee(split.payeeId()).name()), i18n("Invalid transaction")); m_noError = false; return; } QString str; str += QLatin1Char('\n'); str += QString("%1" + m_separator).arg(t.postDate().toString(Qt::ISODate)); MyMoneyPayee payee = file->payee(split.payeeId()); str += format(payee.name()); str += format(split.value()); if (splits.count() > 1) { MyMoneySplit sp = t.splitByAccount(accountId, false); str += format(file->accountToCategory(sp.accountId())); } str += format(split.memo()); switch (split.reconcileFlag()) { case eMyMoney::Split::State::Cleared: str += QLatin1String("C") + m_separator; break; case eMyMoney::Split::State::Reconciled: case eMyMoney::Split::State::Frozen: str += QLatin1String("R") + m_separator; break; default: str += m_separator; break; } str += format(split.number(), false); if (splits.count() > 2) { QList::ConstIterator it; for (it = splits.constBegin(); it != splits.constEnd(); ++it) { if (!((*it) == split)) { writeSplitEntry(str, *it, splits.count() - 1, it+1 == splits.constEnd()); } } } QString date = t.postDate().toString(Qt::ISODate); m_map.insertMulti(date, str); } void CsvWriter::writeSplitEntry(QString &str, const MyMoneySplit& split, const int splitCount, const int lastEntry) { if (m_firstSplit) { m_firstSplit = false; str += m_separator; } MyMoneyFile* file = MyMoneyFile::instance(); str += format(file->accountToCategory(split.accountId())); if (splitCount > m_highestSplitCount) { m_highestSplitCount++; m_headerLine << QString(i18n("splitCategory")) << QString(i18n("splitMemo")) << QString(i18n("splitAmount")); m_headerLine.join(m_separator); } str += format(split.memo()); str += format(split.value(), 2, !lastEntry); } void CsvWriter::extractInvestmentEntries(const QString& accountId, const QDate& startDate, const QDate& endDate) { MyMoneyFile* file = MyMoneyFile::instance(); foreach (const auto sAccount, file->account(accountId).accountList()) { MyMoneyTransactionFilter filter(sAccount); filter.setDateFilter(startDate, endDate); QList list = file->transactionList(filter); QList::ConstIterator itList; signalProgress(0, list.count()); int count = 0; for (itList = list.constBegin(); itList != list.constEnd(); ++itList) { writeInvestmentEntry(*itList, ++count); signalProgress(count, 0); } } } void CsvWriter::writeInvestmentEntry(const MyMoneyTransaction& t, const int count) { QString strQuantity; QString strAmount; QString strPrice; QString strAccName; QString strCheckingAccountName; QString strMemo; QString strAction; QString strStatus; QString strInterest; QString strFees; MyMoneyFile* file = MyMoneyFile::instance(); QString chkAccnt; QList lst = t.splits(); QList::Iterator itSplit; eMyMoney::Account::Type typ; QString chkAccntId; MyMoneyMoney qty; MyMoneyMoney value; QMap map; for (int i = 0; i < lst.count(); i++) { MyMoneyAccount acc = file->account(lst[i].accountId()); - QString accName = acc.name(); typ = acc.accountType(); map.insert(typ, lst[i].accountId()); if (typ == eMyMoney::Account::Type::Stock) { switch (lst[i].reconcileFlag()) { case eMyMoney::Split::State::Cleared: strStatus = QLatin1Char('C'); break; case eMyMoney::Split::State::Reconciled: case eMyMoney::Split::State::Frozen: strStatus = QLatin1Char('R'); break; default: strStatus.clear(); break; } } } // // Add date. // QString str = QString("\n%1" + m_separator).arg(t.postDate().toString(Qt::ISODate)); for (itSplit = lst.begin(); itSplit != lst.end(); ++itSplit) { MyMoneyAccount acc = file->account((*itSplit).accountId()); // // eMyMoney::Account::Type::Checkings. // if ((acc.accountType() == eMyMoney::Account::Type::Checkings) || (acc.accountType() == eMyMoney::Account::Type::Cash) || (acc.accountType() == eMyMoney::Account::Type::Savings)) { chkAccntId = (*itSplit).accountId(); chkAccnt = file->account(chkAccntId).name(); strCheckingAccountName = format(file->accountToCategory(chkAccntId)); strAmount = format((*itSplit).value()); } else if (acc.accountType() == eMyMoney::Account::Type::Income) { // // eMyMoney::Account::Type::Income. // qty = (*itSplit).shares(); value = (*itSplit).value(); strInterest = format(value); } else if (acc.accountType() == eMyMoney::Account::Type::Expense) { // // eMyMoney::Account::Type::Expense. // qty = (*itSplit).shares(); value = (*itSplit).value(); strFees = format(value); } else if (acc.accountType() == eMyMoney::Account::Type::Stock) { // // eMyMoney::Account::Type::Stock. // strMemo = format((*itSplit).memo()); // // Actions. // if ((*itSplit).action() == QLatin1String("Dividend")) { strAction = QLatin1String("DivX"); } else if ((*itSplit).action() == QLatin1String("IntIncome")) { strAction = QLatin1String("IntIncX"); } if ((strAction == QLatin1String("DivX")) || (strAction == QLatin1String("IntIncX"))) { if ((map.value(eMyMoney::Account::Type::Checkings).isEmpty()) && (map.value(eMyMoney::Account::Type::Cash).isEmpty())) { KMessageBox::sorry(0, i18n("Transaction number '%1' is missing an account assignment.\n" "Date '%2', Amount '%3'.\nTransaction dropped.\n", count, t.postDate().toString(Qt::ISODate), strAmount), i18n("Invalid transaction")); return; } } else if ((*itSplit).action() == QLatin1String("Buy")) { qty = (*itSplit).shares(); if (qty.isNegative()) { strAction = QLatin1String("Sell"); } else { strAction = QLatin1String("Buy"); } } else if ((*itSplit).action() == QLatin1String("Add")) { qty = (*itSplit).shares(); if (qty.isNegative()) { strAction = QLatin1String("Shrsout"); } else { strAction = QLatin1String("Shrsin"); } } else if ((*itSplit).action() == QLatin1String("Reinvest")) { qty = (*itSplit).shares(); strAmount = format((*itSplit).value()); strAction = QLatin1String("ReinvDiv"); } else { strAction = (*itSplit).action(); } // // Add action. // if ((strAction == QLatin1String("Buy")) || (strAction == QLatin1String("Sell")) || (strAction == QLatin1String("ReinvDiv"))) { // // Add total. // TODO: value is not used below if (strAction == QLatin1String("Sell")) { value = -value; } // // Add price. // strPrice = format((*itSplit).price(), 6); if (!qty.isZero()) { // // Add quantity. // if (strAction == QLatin1String("Sell")) { qty = -qty; } strQuantity = format(qty); } } else if ((strAction == QLatin1String("Shrsin")) || (strAction == QLatin1String("Shrsout"))) { // // Add quantity for "Shrsin" || "Shrsout". // if (strAction == QLatin1String("Shrsout")) { qty = -qty; } strQuantity = format(qty); } strAccName = format(acc.name()); strAction += m_separator; } if (strAmount.isEmpty()) strAmount = m_separator; if (strQuantity.isEmpty()) strQuantity = m_separator; if (strPrice.isEmpty()) strPrice = m_separator; if (strCheckingAccountName.isEmpty()) { strCheckingAccountName = m_separator; } if (strInterest.isEmpty()) { strInterest = m_separator; } if (strFees.isEmpty()) { strFees = m_separator; } } // end of itSplit loop str += strAccName + strAction + strAmount + strQuantity + strPrice + strInterest + strFees + strCheckingAccountName + strMemo + strStatus; QString date = t.postDate().toString(Qt::ISODate); m_map.insertMulti(date, str); } /** * Format string field according to csv rules * @param s string to format * @param withSeparator append field separator to string (default = true) * @return csv formatted string */ QString CsvWriter::format(const QString &s, bool withSeparator) { if (s.isEmpty()) return withSeparator ? m_separator : QString(); QString m = s; m.remove('\''); -#if QT_VERSION >= 0x050000 +#if QT_VERSION >= QT_VERSION_CHECK(5,0,0) m.replace(QLatin1Char('"'), QStringLiteral("\"\"")); #else m.replace(QLatin1Char('"'), QLatin1String("\"\"")); #endif return QString("\"%1\"%2").arg(m, withSeparator ? m_separator : QString()); } /** * format money value according to csv rules * @param value value to format * @param prec precision used for formatting (default = 2) * @param withSeparator append field separator to string (default = true) * @return formatted value as string */ QString CsvWriter::format(const MyMoneyMoney &value, int prec, bool withSeparator) { return QString("\"%1\"%2").arg(value.formatMoney("", prec, false), withSeparator ? m_separator : QString()); } diff --git a/kmymoney/plugins/csv/import/bankingwizardpage.cpp b/kmymoney/plugins/csv/import/bankingwizardpage.cpp index 26f7a6982..236cb13a1 100644 --- a/kmymoney/plugins/csv/import/bankingwizardpage.cpp +++ b/kmymoney/plugins/csv/import/bankingwizardpage.cpp @@ -1,396 +1,430 @@ /* * Copyright 2011-2017 Allan Anderson * Copyright 2016-2018 Łukasz Wojniłowicz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "bankingwizardpage.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "csvwizard.h" #include "core/csvimportercore.h" #include "ui_bankingwizardpage.h" // ---------------------------------------------------------------------------- BankingPage::BankingPage(CSVWizard *dlg, CSVImporterCore *imp) : CSVWizardPage(dlg, imp), ui(new Ui::BankingPage) { ui->setupUi(this); connect(ui->m_clear, &QAbstractButton::clicked, this, &BankingPage::clearColumns); connect(ui->m_radioAmount, &QAbstractButton::toggled, this, &BankingPage::amountToggled); connect(ui->m_radioDebitCredit, &QAbstractButton::toggled, this, &BankingPage::debitCreditToggled); connect(ui->m_oppositeSigns, &QAbstractButton::clicked, this, &BankingPage::oppositeSignsClicked); // initialize column names m_dlg->m_colTypeName.insert(Column::Payee,i18n("Payee")); m_dlg->m_colTypeName.insert(Column::Number,i18n("Number")); m_dlg->m_colTypeName.insert(Column::Debit,i18n("Debit")); m_dlg->m_colTypeName.insert(Column::Credit,i18n("Credit")); m_dlg->m_colTypeName.insert(Column::Date,i18n("Date")); m_dlg->m_colTypeName.insert(Column::Amount,i18n("Amount")); m_dlg->m_colTypeName.insert(Column::Category,i18n("Category")); m_dlg->m_colTypeName.insert(Column::Memo,i18n("Memo")); m_profile = dynamic_cast(m_imp->m_profile); - connect(ui->m_amountCol, SIGNAL(currentIndexChanged(int)), this, SLOT(amountColSelected(int))); - connect(ui->m_debitCol, SIGNAL(currentIndexChanged(int)), this, SLOT(debitColSelected(int))); - connect(ui->m_creditCol, SIGNAL(currentIndexChanged(int)), this, SLOT(creditColSelected(int))); - connect(ui->m_memoCol, SIGNAL(currentIndexChanged(int)), this, SLOT(memoColSelected(int))); - connect(ui->m_numberCol, SIGNAL(currentIndexChanged(int)), this, SLOT(numberColSelected(int))); - connect(ui->m_dateCol, SIGNAL(currentIndexChanged(int)), this, SLOT(dateColSelected(int))); - connect(ui->m_payeeCol, SIGNAL(currentIndexChanged(int)), this, SLOT(payeeColSelected(int))); - connect(ui->m_categoryCol, SIGNAL(currentIndexChanged(int)), this, SLOT(categoryColSelected(int))); + void (QComboBox::* signal)(int) = &QComboBox::currentIndexChanged; + connect(ui->m_amountCol, signal, this, &BankingPage::amountColSelected); + connect(ui->m_debitCol, signal, this, &BankingPage::debitColSelected); + connect(ui->m_creditCol, signal, this, &BankingPage::creditColSelected); + connect(ui->m_memoCol, signal, this, &BankingPage::memoColSelected); + connect(ui->m_numberCol, signal, this, &BankingPage::numberColSelected); + connect(ui->m_dateCol, signal, this, &BankingPage::dateColSelected); + connect(ui->m_payeeCol, signal, this, &BankingPage::payeeColSelected); + connect(ui->m_categoryCol, signal, this, &BankingPage::categoryColSelected); + + connect(ui->m_clearMemoColumns, &QToolButton::clicked, this, &BankingPage::clearMemoColumns); + + updateCurrentMemoSelection(); } BankingPage::~BankingPage() { delete ui; } void BankingPage::initializePage() { QHash columns {{Column::Amount, ui->m_amountCol}, {Column::Debit, ui->m_debitCol}, {Column::Credit, ui->m_creditCol}, {Column::Memo, ui->m_memoCol}, {Column::Number, ui->m_numberCol}, {Column::Date, ui->m_dateCol}, {Column::Payee, ui->m_payeeCol}, {Column::Category, ui->m_categoryCol}}; if (ui->m_dateCol->count() != m_imp->m_file->m_columnCount) m_dlg->initializeComboBoxes(columns); columns.remove(Column::Memo); - for (auto it = columns.cbegin(); it != columns.cend(); ++it) + for (auto it = columns.cbegin(); it != columns.cend(); ++it) { it.value()->setCurrentIndex(m_profile->m_colTypeNum.value(it.key())); + } ui->m_oppositeSigns->setChecked(m_profile->m_oppositeSigns); if (m_profile->m_memoColList.count() > 0) { for (int i = 0; i < m_profile->m_memoColList.count(); ++i) ui->m_memoCol->setCurrentIndex(m_profile->m_memoColList.value(i)); } else ui->m_memoCol->setCurrentIndex(-1); if (this->m_profile->m_colTypeNum.value(Column::Debit) == -1) // If amount previously selected, set check radio_amount ui->m_radioAmount->setChecked(true); else // ...else set check radio_debCred to clear amount col ui->m_radioDebitCredit->setChecked(true); } int BankingPage::nextId() const { return CSVWizard::PageFormats; } bool BankingPage::isComplete() const { return ui->m_dateCol->currentIndex() > -1 && ui->m_payeeCol->currentIndex() > -1 && (ui->m_amountCol->currentIndex() > -1 || (ui->m_debitCol->currentIndex() > -1 && ui->m_creditCol->currentIndex() > -1)); } bool BankingPage::validateMemoComboBox() { - if (m_profile->m_memoColList.count() == 0) + if (m_profile->m_memoColList.isEmpty()) return true; + for (int i = 0; i < ui->m_memoCol->count(); ++i) { - QString txt = ui->m_memoCol->itemText(i); + const QString txt = ui->m_memoCol->itemText(i); if (txt.contains(QLatin1Char('*'))) // check if text containing '*' belongs to valid column types if (m_profile->m_colNumType.value(i) != Column::Payee) { ui->m_memoCol->setItemText(i, QString::number(i + 1)); m_profile->m_memoColList.removeOne(i); return false; } } return true; } void BankingPage::memoColSelected(int col) { if (m_profile->m_colNumType.value(col) == Column::Payee ) { int rc = KMessageBox::Yes; if (isVisible()) rc = KMessageBox::questionYesNo(m_dlg, i18n("
The '%1' field already has this column selected.
" "
If you wish to copy the Payee data to the memo field, click 'Yes'.
", m_dlg->m_colTypeName.value(m_profile->m_colNumType.value(col)))); if (rc == KMessageBox::Yes) { ui->m_memoCol->setItemText(col, QString::number(col + 1) + QLatin1Char('*')); if (!m_profile->m_memoColList.contains(col)) m_profile->m_memoColList.append(col); } else { ui->m_memoCol->setItemText(col, QString::number(col + 1)); m_profile->m_memoColList.removeOne(col); } //allow only separate memo field occupy combobox ui->m_memoCol->blockSignals(true); if (m_profile->m_colTypeNum.value(Column::Memo) != -1) ui->m_memoCol->setCurrentIndex(m_profile->m_colTypeNum.value(Column::Memo)); else ui->m_memoCol->setCurrentIndex(-1); ui->m_memoCol->blockSignals(false); - return; + + } else { + if (m_profile->m_colTypeNum.value(Column::Memo) != -1) // check if this memo has any column 'number' assigned... + m_profile->m_memoColList.removeOne(col); // ...if true remove it from memo list + + if(validateSelectedColumn(col, Column::Memo)) { + if (col != - 1 && !m_profile->m_memoColList.contains(col)) { + m_profile->m_memoColList.append(col); + qSort(m_profile->m_memoColList); + } + } } + updateCurrentMemoSelection(); +} - if (m_profile->m_colTypeNum.value(Column::Memo) != -1) // check if this memo has any column 'number' assigned... - m_profile->m_memoColList.removeOne(col); // ...if true remove it from memo list +void BankingPage::updateCurrentMemoSelection() +{ + const auto& list = m_profile->m_memoColList; + const bool haveSelection = !list.isEmpty(); + QString txt; + if (haveSelection) { + for (const auto& entry : list) { + txt += QString("%1, ").arg(entry+1); + } + txt = txt.left(txt.length()-2); + } + ui->m_currentMemoColums->setText(QString("%1").arg(txt, -30, QChar(' '))); - if(validateSelectedColumn(col, Column::Memo)) - if (col != - 1 && !m_profile->m_memoColList.contains(col)) - m_profile->m_memoColList.append(col); + ui->m_clearMemoColumns->setEnabled(haveSelection); } + void BankingPage::categoryColSelected(int col) { validateSelectedColumn(col, Column::Category); } void BankingPage::numberColSelected(int col) { validateSelectedColumn(col, Column::Number); } void BankingPage::payeeColSelected(int col) { if (validateSelectedColumn(col, Column::Payee)) - if (!validateMemoComboBox()) // user could have it already in memo so... + if (!validateMemoComboBox() && col != -1) // user could have it already in memo so... memoColSelected(col); // ...if true set memo field again } void BankingPage::dateColSelected(int col) { validateSelectedColumn(col, Column::Date); } void BankingPage::debitColSelected(int col) { validateSelectedColumn(col, Column::Debit); } void BankingPage::creditColSelected(int col) { validateSelectedColumn(col, Column::Credit); } void BankingPage::amountColSelected(int col) { validateSelectedColumn(col, Column::Amount); } void BankingPage::amountToggled(bool checked) { if (checked) { ui->m_amountCol->setEnabled(true); // disable credit & debit ui choices ui->labelBnk_amount->setEnabled(true); ui->labelBnk_credits->setEnabled(false); ui->labelBnk_debits->setEnabled(false); ui->m_debitCol->setEnabled(false); ui->m_debitCol->setCurrentIndex(-1); ui->m_creditCol->setEnabled(false); ui->m_creditCol->setCurrentIndex(-1); } } void BankingPage::debitCreditToggled(bool checked) { if (checked) { ui->m_debitCol->setEnabled(true); // if 'debit/credit' selected ui->labelBnk_debits->setEnabled(true); ui->m_creditCol->setEnabled(true); ui->labelBnk_credits->setEnabled(true); ui->m_amountCol->setEnabled(false); // disable 'amount' ui choices ui->m_amountCol->setCurrentIndex(-1); // as credit/debit chosen ui->labelBnk_amount->setEnabled(false); } } void BankingPage::oppositeSignsClicked(bool checked) { m_profile->m_oppositeSigns = checked; } void BankingPage::clearColumns() { ui->m_dateCol->setCurrentIndex(-1); ui->m_payeeCol->setCurrentIndex(-1); - ui->m_memoCol->setCurrentIndex(-1); ui->m_numberCol->setCurrentIndex(-1); ui->m_amountCol->setCurrentIndex(-1); ui->m_debitCol->setCurrentIndex(-1); ui->m_creditCol->setCurrentIndex(-1); ui->m_categoryCol->setCurrentIndex(-1); + clearMemoColumns(); +} + +void BankingPage::clearMemoColumns() +{ + m_profile->m_memoColList.clear(); + ui->m_memoCol->setCurrentIndex(-1); } void BankingPage::resetComboBox(const Column comboBox) { switch (comboBox) { case Column::Amount: ui->m_amountCol->setCurrentIndex(-1); break; case Column::Credit: ui->m_creditCol->setCurrentIndex(-1); break; case Column::Date: ui->m_dateCol->setCurrentIndex(-1); break; case Column::Debit: ui->m_debitCol->setCurrentIndex(-1); break; case Column::Memo: ui->m_memoCol->setCurrentIndex(-1); break; case Column::Number: ui->m_numberCol->setCurrentIndex(-1); break; case Column::Payee: ui->m_payeeCol->setCurrentIndex(-1); break; case Column::Category: ui->m_categoryCol->setCurrentIndex(-1); break; default: KMessageBox::sorry(m_dlg, i18n("
Field name not recognised.
'%1'
Please re-enter your column selections." , (int)comboBox), i18n("CSV import")); } } bool BankingPage::validateSelectedColumn(const int col, const Column type) { if (m_profile->m_colTypeNum.value(type) != -1) // check if this 'type' has any column 'number' assigned... m_profile->m_colNumType.remove(m_profile->m_colTypeNum[type]); // ...if true remove 'type' assigned to this column 'number' bool ret = true; if (col == -1) { // user only wanted to reset his column so allow him m_profile->m_colTypeNum[type] = col; // assign new column 'number' to this 'type' } else if (m_profile->m_colNumType.contains(col)) { // if this column 'number' has already 'type' assigned KMessageBox::information(m_dlg, i18n("The '%1' field already has this column selected.
Please reselect both entries as necessary.
", m_dlg->m_colTypeName.value(m_profile->m_colNumType.value(col)))); resetComboBox(m_profile->m_colNumType.value(col)); resetComboBox(type); ret = false; } else { m_profile->m_colTypeNum[type] = col; // assign new column 'number' to this 'type' m_profile->m_colNumType[col] = type; // assign new 'type' to this column 'number' } emit completeChanged(); return ret; } bool BankingPage::validateCreditDebit() { for (int row = m_profile->m_startLine; row <= m_profile->m_endLine; ++row) { // process credit/debit field if (m_profile->m_colTypeNum.value(Column::Credit) != -1 && m_profile->m_colTypeNum.value(Column::Debit) != -1) { QString credit = m_imp->m_file->m_model->item(row, m_profile->m_colTypeNum.value(Column::Credit))->text(); QString debit = m_imp->m_file->m_model->item(row, m_profile->m_colTypeNum.value(Column::Debit))->text(); m_imp->processCreditDebit(credit, debit); if (!credit.isEmpty() && !debit.isEmpty()) { int ret = KMessageBox::questionYesNoCancel(m_dlg, i18n("
The %1 field contains '%2'
" "
and the %3 field contains '%4'.
" "
Please choose which you wish to accept.
", m_dlg->m_colTypeName.value(Column::Debit), debit, m_dlg->m_colTypeName.value(Column::Credit), credit), i18n("CSV invalid field values"), KGuiItem(i18n("Accept %1", m_dlg->m_colTypeName.value(Column::Debit))), KGuiItem(i18n("Accept %1", m_dlg->m_colTypeName.value(Column::Credit))), KGuiItem(i18n("Cancel"))); switch(ret) { case KMessageBox::Cancel: return false; case KMessageBox::Yes: m_imp->m_file->m_model->item(row, m_profile->m_colTypeNum.value(Column::Credit))->setText(QString()); break; case KMessageBox::No: m_imp->m_file->m_model->item(row, m_profile->m_colTypeNum.value(Column::Debit))->setText(QString()); break; } } } } return true; } void BankingPage::makeQIF(const MyMoneyStatement& st, const QString& outFileName) { QFile oFile(outFileName); oFile.open(QIODevice::WriteOnly); QTextStream out(&oFile); QString buffer; QString strEType; switch (st.m_eType) { case eMyMoney::Statement::Type::CreditCard: strEType = QStringLiteral("CCard"); break; case eMyMoney::Statement::Type::Savings: case eMyMoney::Statement::Type::Checkings: default: strEType = QStringLiteral("Bank"); } if (!st.m_strAccountName.isEmpty()) { buffer.append(QStringLiteral("!Account\n")); buffer.append(QChar(QLatin1Char('N')) + st.m_strAccountName + QChar(QLatin1Char('\n'))); buffer.append(QChar(QLatin1Char('T')) + strEType + QChar(QLatin1Char('\n'))); buffer.append(QStringLiteral("^\n")); } buffer.append(QStringLiteral("!Type:") + strEType + QChar(QLatin1Char('\n'))); for (QList::const_iterator it = st.m_listTransactions.constBegin(); it != st.m_listTransactions.constEnd(); ++it) { buffer.append(QChar(QLatin1Char('D')) + it->m_datePosted.toString(QStringLiteral("MM/dd/yyyy")) + QChar(QLatin1Char('\n'))); QString txt; txt.setNum(it->m_amount.toDouble(), 'f', 4); buffer.append(QChar(QLatin1Char('T')) + txt + QChar(QLatin1Char('\n'))); buffer.append(QChar(QLatin1Char('P')) + it->m_strPayee + QChar(QLatin1Char('\n'))); if (!it->m_listSplits.isEmpty()) buffer.append(QChar(QLatin1Char('L')) + it->m_listSplits.first().m_strCategoryName + QChar(QLatin1Char('\n'))); if (!it->m_strNumber.isEmpty()) buffer.append(QChar(QLatin1Char('N')) + it->m_strNumber + QChar(QLatin1Char('\n'))); if (!it->m_strMemo.isEmpty()) buffer.append(QChar(QLatin1Char('M')) + it->m_strMemo + QChar(QLatin1Char('\n'))); buffer.append(QStringLiteral("^\n")); out << buffer;// output qif file buffer.clear(); } oFile.close(); } diff --git a/kmymoney/plugins/csv/import/bankingwizardpage.h b/kmymoney/plugins/csv/import/bankingwizardpage.h index 43f664349..7297e0e96 100644 --- a/kmymoney/plugins/csv/import/bankingwizardpage.h +++ b/kmymoney/plugins/csv/import/bankingwizardpage.h @@ -1,84 +1,86 @@ /* * Copyright 2011-2017 Allan Anderson * Copyright 2016-2018 Łukasz Wojniłowicz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef BANKINGWIZARDPAGE_H #define BANKINGWIZARDPAGE_H // ---------------------------------------------------------------------------- // QT Includes // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "csvwizardpage.h" // ---------------------------------------------------------------------------- class BankingProfile; class MyMoneyStatement; namespace Ui { class BankingPage; } class BankingPage : public CSVWizardPage { Q_OBJECT public: explicit BankingPage(CSVWizard *dlg, CSVImporterCore *imp); ~BankingPage(); bool validateCreditDebit(); /** * This method fills QIF file with bank/credit card data */ void makeQIF(const MyMoneyStatement &st, const QString &outFileName); private: void initializePage() final override; bool isComplete() const final override; int nextId() const final override; bool validateMemoComboBox(); void resetComboBox(const Column comboBox); bool validateSelectedColumn(const int col, const Column type); BankingProfile *m_profile; Ui::BankingPage *ui; -private Q_SLOTS: void memoColSelected(int col); void categoryColSelected(int col); void numberColSelected(int col); void payeeColSelected(int col); void dateColSelected(int col); void debitColSelected(int col); void creditColSelected(int col); void amountColSelected(int col); void amountToggled(bool checked); void debitCreditToggled(bool checked); void oppositeSignsClicked(bool checked); void clearColumns(); + void updateCurrentMemoSelection(); + void clearMemoColumns(); + }; #endif // BANKINGWIZARDPAGE_H diff --git a/kmymoney/plugins/csv/import/bankingwizardpage.ui b/kmymoney/plugins/csv/import/bankingwizardpage.ui index 8e01e25a3..4a781dc77 100644 --- a/kmymoney/plugins/csv/import/bankingwizardpage.ui +++ b/kmymoney/plugins/csv/import/bankingwizardpage.ui @@ -1,458 +1,480 @@ BankingPage 0 0 681 253 0 0 Banking Wizard Page - + Qt::Horizontal 40 20 0 0 Please select the appropriate columns to use, corresponding to your data. Qt::AlignCenter true - + false Debit Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 16777215 16777215 Category Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 0 0 Select column containing date field. 12 0 0 Select column containing category field. 12 false Credit Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 0 0 Select column containing credit field. 12 Qt::Horizontal QSizePolicy::Minimum 6 0 0 0 false Select 'Debit/credit' if both columns exist, otherwise select 'Amount'. De&bit/credit false Date Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 0 0 Select column containing amount field. 12 0 0 Select column containing debit field. 12 - - - - - 0 - 0 - - - - Clear selected column entries - - - Clear - - - Payee/Description Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - 0 - 0 - - - - Select column containing memo field. - - - 12 - - + + + + + + 0 + 0 + + + + Select column containing memo field. + + + 12 + + + + + + + Selection + + + + + + + Clear selected memo column entries + + + Clear + + + + 0 0 Select column containing payee or description field. 12 Memo Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - 0 - 0 - - - - Select if your amount column has signs improperly set. - - - Qt::LeftToRight - - - Opposite signs - - - 0 0 Select column containing number field. false 12 Number Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Amount Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 0 0 Select 'Amount' if only one value column, otherwise select 'Debit/credit'. A&mount false 0 0 Qt::Horizontal + + + + + 0 + 0 + + + + Select if your amount column has signs improperly set. + + + Qt::LeftToRight + + + Opposite signs + + + + + + + + 0 + 0 + + + + Clear all selected column entries + + + Clear all + + + Qt::Horizontal 40 20 m_numberCol m_dateCol m_payeeCol m_categoryCol m_memoCol + m_clearMemoColumns m_radioAmount m_radioDebitCredit m_amountCol m_debitCol m_creditCol m_oppositeSigns m_clear diff --git a/kmymoney/plugins/csv/import/core/tests/symbol-test.cpp b/kmymoney/plugins/csv/import/core/tests/symbol-test.cpp index a9e89782f..954a841bc 100644 --- a/kmymoney/plugins/csv/import/core/tests/symbol-test.cpp +++ b/kmymoney/plugins/csv/import/core/tests/symbol-test.cpp @@ -1,168 +1,168 @@ /* * Copyright 2011-2012 Allan Anderson * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "symbol-test.h" #include "../csvutil.h" #include #include #include QTEST_GUILESS_MAIN(SymbolTest); Parse* m_parse; SymbolTest::SymbolTest() : m_parse(nullptr) { } void SymbolTest::init() { m_parse = new Parse; m_parse->setDecimalSymbol(DecimalSymbol::Dot); m_localeDecimal = QLocale().decimalPoint(); m_localeThousands = QLocale().groupSeparator(); } void SymbolTest::cleanup() { delete m_parse; } void SymbolTest::testDecimalSymbolDot() { // Detect '.' as decimal and replace from locale m_parse->setDecimalSymbol(DecimalSymbol::Dot); // "." QFETCH(QString, input); QFETCH(QString, result); QVERIFY(m_parse->possiblyReplaceSymbol(input) == result); } void SymbolTest::testDecimalSymbolComma() { // Detect ',' as decimal and replace from locale m_parse->setDecimalSymbol(DecimalSymbol::Comma); // "," QFETCH(QString, input); QFETCH(QString, result); QCOMPARE(m_parse->possiblyReplaceSymbol(input), result); } void SymbolTest::testDecimalSymbolInvalid() { // Check for ',' as decimal, and none present m_parse->setDecimalSymbol(DecimalSymbol::Comma); // "," m_testDecimal = m_parse->decimalSymbol(DecimalSymbol::Comma); QFETCH(QString, input); - QFETCH(QString, result); - QString res = m_parse->possiblyReplaceSymbol(input); + // QFETCH(QString, result); + m_parse->possiblyReplaceSymbol(input); QVERIFY(m_parse->invalidConversion() == true); } void SymbolTest::testDecimalSymbolDot_data() { QTest::addColumn ("input"); QTest::addColumn ("result"); // Detect '.' as decimal and replace from locale QTest::newRow("test 1") << "1234.56" << QString("1234" + m_localeDecimal + "56"); // Check for '.' as decimal, and none present QTest::newRow("test 2") << "145" << QString("145" + m_localeDecimal + "00"); // Detect '.' as decimal and replace from locale, // with thousands separator present QTest::newRow("test 3") << "-123,456.78" << QString("-123456" + m_localeDecimal + "78"); // Detect '.' as decimal and replace from locale // and thousands separator present QTest::newRow("test 4") << "123,456.78" << QString("123456" + m_localeDecimal + "78"); // Detect '.' as decimal and replace from locale // and thousands separator present QTest::newRow("test 5") << "987,654.32" << QString("987654" + m_localeDecimal + "32"); } void SymbolTest::testDecimalSymbolComma_data() { QTest::addColumn ("input"); QTest::addColumn ("result"); // Detect ',' as decimal and replace from locale QTest::newRow("test 1") << "$987,654" << QString("$987" + m_localeDecimal + "654"); // Detect ',' as decimal and replace from locale // with thousands separator present QTest::newRow("test 2") << "-123.456,78" << QString("-123456" + m_localeDecimal + "78"); QTest::newRow("test 3") << "145" << QString("145" + m_localeDecimal + "00"); // Check for ',' as decimal QTest::newRow("test 4") << "123.456" << QString("123456" + m_localeDecimal + "00"); } void SymbolTest::testDecimalSymbolInvalid_data() { QTest::addColumn ("input"); QTest::addColumn ("result"); // Check for ',' as decimal, and none present QTest::newRow("test 1") << "1234.56" << "invalid"; // Detect ',' as decimal and replace from locale // with thousands separator present QTest::newRow("test 2") << "987,654.32" << "invalid"; } void SymbolTest::cleanupTestCase() { } void SymbolTest::testConstructor() { } void SymbolTest::testConstructor_data() { } void SymbolTest::testDefaultConstructor() { } void SymbolTest::testDefaultConstructor_data() { } void SymbolTest::initTestCase() { } void SymbolTest::initTestCase_data() { } diff --git a/kmymoney/plugins/gnc/import/mymoneygncreader.cpp b/kmymoney/plugins/gnc/import/mymoneygncreader.cpp index e8407c48e..d88b104f1 100644 --- a/kmymoney/plugins/gnc/import/mymoneygncreader.cpp +++ b/kmymoney/plugins/gnc/import/mymoneygncreader.cpp @@ -1,2693 +1,2693 @@ /*************************************************************************** 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 +#if QT_VERSION >= QT_VERSION_CHECK(5,10,0) #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"))) throw MYMONEYEXCEPTION(QString::fromLatin1("%1 : Sorry. This importer cannot handle version %2 of element %3").arg(Q_FUNC_INFO, elAttrs.value("version"), elName)); } 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 +#if QT_VERSION >= QT_VERSION_CHECK(5,10,0) 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) MYMONEYEXCEPTION(QString::fromLatin1("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_CSTRING("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_CSTRING("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_CSTRING("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_CSTRING("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_CSTRING("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_CSTRING("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_CSTRING("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_CSTRING("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_CSTRING("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_CSTRING("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_CSTRING("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(QString::fromLatin1("Input file cannot be parsed; may be corrupt\n%1").arg(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(QString::fromLatin1("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", QString::fromLatin1(e.what())), PACKAGE); qWarning("%s", 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", QString::fromLatin1(e.what())), PACKAGE); qWarning("%s", 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", QString::fromLatin1(e.what())), PACKAGE); qWarning("%s", 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 +#if QT_VERSION >= QT_VERSION_CHECK(5,10,0) 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 throw MYMONEYEXCEPTION(QString::fromLatin1("Current importer does not recognize GnuCash account type %1").arg(gac->type())); } // 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 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(QString::fromLatin1("Cannot find template transaction for schedule %1").arg(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_CSTRING("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_CSTRING("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 initially 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", 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/kbanking/kbanking.cpp b/kmymoney/plugins/kbanking/kbanking.cpp index 83892059d..8c54215d8 100644 --- a/kmymoney/plugins/kbanking/kbanking.cpp +++ b/kmymoney/plugins/kbanking/kbanking.cpp @@ -1,1546 +1,1547 @@ /* * Copyright 2004 Martin Preuss aquamaniac@users.sourceforge.net * Copyright 2009 Cristian Onet onet.cristian@gmail.com * Copyright 2010-2018 Thomas Baumgart tbaumgart@kde.org * Copyright 2015 Christian David christian-david@web.de * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include "kbanking.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include //! @todo remove @c #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Library Includes #include #include #include #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoney/onlinejob.h" #include "kbaccountsettings.h" #include "kbmapaccount.h" #include "mymoneyfile.h" #include "onlinejobadministration.h" #include "kmymoneyview.h" #include "kbpickstartdate.h" #include "mymoneyinstitution.h" #include "mymoneyexception.h" #include "gwenkdegui.h" #include "gwenhywfarqtoperators.h" #include "aqbankingkmmoperators.h" #include "mymoneystatement.h" #include "statementinterface.h" #include "viewinterface.h" #ifdef KMM_DEBUG // Added an option to open the chipTanDialog from the menu for debugging purposes #include "chiptandialog.h" #endif #ifdef IS_APPIMAGE #include #include #endif class KBanking::Private { public: Private() : passwordCacheTimer(nullptr), jobList(), fileId() { QString gwenProxy = QString::fromLocal8Bit(qgetenv("GWEN_PROXY")); if (gwenProxy.isEmpty()) { std::unique_ptr cfg = std::unique_ptr(new KConfig("kioslaverc")); QRegExp exp("(\\w+://)?([^/]{2}.+:\\d+)"); QString proxy; KConfigGroup grp = cfg->group("Proxy Settings"); int type = grp.readEntry("ProxyType", 0); switch (type) { case 0: // no proxy break; case 1: // manual specified proxy = grp.readEntry("httpsProxy"); qDebug("KDE https proxy setting is '%s'", qPrintable(proxy)); if (exp.exactMatch(proxy)) { proxy = exp.cap(2); qDebug("Setting GWEN_PROXY to '%s'", qPrintable(proxy)); if (!qputenv("GWEN_PROXY", qPrintable(proxy))) { qDebug("Unable to setup GWEN_PROXY"); } } break; default: // other currently not supported qDebug("KDE proxy setting of type %d not supported", type); break; } } } /** * KMyMoney asks for accounts over and over again which causes a lot of "Job not supported with this account" error messages. * This function filters messages with that string. */ static int gwenLogHook(GWEN_GUI* gui, const char* domain, GWEN_LOGGER_LEVEL level, const char* message) { Q_UNUSED(gui); Q_UNUSED(domain); Q_UNUSED(level); const char* messageToFilter = "Job not supported with this account"; if (strstr(message, messageToFilter) != 0) return 1; return 0; } QTimer *passwordCacheTimer; QMap jobList; QString fileId; }; KBanking::KBanking(QObject *parent, const QVariantList &args) : OnlinePluginExtended(parent, "kbanking") , d(new Private) , m_configAction(nullptr) , m_importAction(nullptr) , m_kbanking(nullptr) , m_accountSettings(nullptr) + , m_statementCount(0) { Q_UNUSED(args) qDebug("Plugins: kbanking loaded"); } KBanking::~KBanking() { delete d; qDebug("Plugins: kbanking unloaded"); } void KBanking::plug() { const auto componentName = QLatin1String("kbanking"); const auto rcFileName = QLatin1String("kbanking.rc"); m_kbanking = new KBankingExt(this, "KMyMoney"); d->passwordCacheTimer = new QTimer(this); d->passwordCacheTimer->setSingleShot(true); d->passwordCacheTimer->setInterval(60000); connect(d->passwordCacheTimer, &QTimer::timeout, this, &KBanking::slotClearPasswordCache); if (m_kbanking) { if (AB_Banking_HasConf4(m_kbanking->getCInterface())) { qDebug("KBankingPlugin: No AqB4 config found."); if (AB_Banking_HasConf3(m_kbanking->getCInterface())) { qDebug("KBankingPlugin: No AqB3 config found."); if (!AB_Banking_HasConf2(m_kbanking->getCInterface())) { qDebug("KBankingPlugin: AqB2 config found - converting."); AB_Banking_ImportConf2(m_kbanking->getCInterface()); } } else { qDebug("KBankingPlugin: AqB3 config found - converting."); AB_Banking_ImportConf3(m_kbanking->getCInterface()); } } //! @todo when is gwenKdeGui deleted? gwenKdeGui *gui = new gwenKdeGui(); GWEN_Gui_SetGui(gui->getCInterface()); GWEN_Logger_SetLevel(0, GWEN_LoggerLevel_Warning); if (m_kbanking->init() == 0) { // Tell the host application to load my GUI component setComponentName(componentName, "KBanking"); #ifdef IS_APPIMAGE const QString rcFilePath = QCoreApplication::applicationDirPath() + QLatin1String("/../share/kxmlgui5/") + componentName + QLatin1Char('/') + rcFileName; setXMLFile(rcFilePath); const QString localRcFilePath = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation).first() + QLatin1Char('/') + componentName + QLatin1Char('/') + rcFileName; setLocalXMLFile(localRcFilePath); #else setXMLFile(rcFileName); #endif // get certificate handling and dialog settings management AB_Gui_Extend(gui->getCInterface(), m_kbanking->getCInterface()); // create actions createActions(); // load protocol conversion list loadProtocolConversion(); GWEN_Logger_SetLevel(AQBANKING_LOGDOMAIN, GWEN_LoggerLevel_Warning); GWEN_Gui_SetLogHookFn(GWEN_Gui_GetGui(), &KBanking::Private::gwenLogHook); } else { qWarning("Could not initialize KBanking online banking interface"); delete m_kbanking; m_kbanking = 0; } } } void KBanking::unplug() { d->passwordCacheTimer->deleteLater(); if (m_kbanking) { m_kbanking->fini(); delete m_kbanking; qDebug("Plugins: kbanking unplugged"); } } void KBanking::loadProtocolConversion() { if (m_kbanking) { m_protocolConversionMap = { {"aqhbci", "HBCI"}, {"aqofxconnect", "OFX"}, {"aqyellownet", "YellowNet"}, {"aqgeldkarte", "Geldkarte"}, {"aqdtaus", "DTAUS"} }; } } void KBanking::protocols(QStringList& protocolList) const { if (m_kbanking) { std::list list = m_kbanking->getActiveProviders(); std::list::iterator it; for (it = list.begin(); it != list.end(); ++it) { // skip the dummy if (*it == "aqnone") continue; QMap::const_iterator it_m; it_m = m_protocolConversionMap.find((*it).c_str()); if (it_m != m_protocolConversionMap.end()) protocolList << (*it_m); else protocolList << (*it).c_str(); } } } QWidget* KBanking::accountConfigTab(const MyMoneyAccount& acc, QString& name) { const MyMoneyKeyValueContainer& kvp = acc.onlineBankingSettings(); name = i18n("Online settings"); if (m_kbanking) { m_accountSettings = new KBAccountSettings(acc, 0); m_accountSettings->loadUi(kvp); return m_accountSettings; } QLabel* label = new QLabel(i18n("KBanking module not correctly initialized"), 0); label->setAlignment(Qt::AlignVCenter | Qt::AlignHCenter); return label; } MyMoneyKeyValueContainer KBanking::onlineBankingSettings(const MyMoneyKeyValueContainer& current) { MyMoneyKeyValueContainer kvp(current); kvp["provider"] = objectName().toLower(); if (m_accountSettings) { m_accountSettings->loadKvp(kvp); } return kvp; } void KBanking::createActions() { QAction *settings_aqbanking = actionCollection()->addAction("settings_aqbanking"); settings_aqbanking->setText(i18n("Configure Aq&Banking...")); connect(settings_aqbanking, &QAction::triggered, this, &KBanking::slotSettings); QAction *file_import_aqbanking = actionCollection()->addAction("file_import_aqbanking"); file_import_aqbanking->setText(i18n("AqBanking importer...")); connect(file_import_aqbanking, &QAction::triggered, this, &KBanking::slotImport); Q_CHECK_PTR(viewInterface()); connect(viewInterface(), &KMyMoneyPlugin::ViewInterface::viewStateChanged, action("file_import_aqbanking"), &QAction::setEnabled); #ifdef KMM_DEBUG QAction *openChipTanDialog = actionCollection()->addAction("open_chiptan_dialog"); openChipTanDialog->setText("Open ChipTan Dialog"); connect(openChipTanDialog, &QAction::triggered, [&](){ auto dlg = new chipTanDialog(); dlg->setHhdCode("0F04871100030333555414312C32331D"); dlg->setInfoText("

Test Graphic for debugging

The encoded data is

Account Number: 335554
Amount: 1,23

"); connect(dlg, &QDialog::accepted, dlg, &chipTanDialog::deleteLater); connect(dlg, &QDialog::rejected, dlg, &chipTanDialog::deleteLater); dlg->show(); }); #endif } void KBanking::slotSettings() { if (m_kbanking) { GWEN_DIALOG* dlg = AB_SetupDialog_new(m_kbanking->getCInterface()); if (dlg == NULL) { DBG_ERROR(0, "Could not create setup dialog."); return; } if (GWEN_Gui_ExecDialog(dlg, 0) == 0) { DBG_ERROR(0, "Aborted by user"); GWEN_Dialog_free(dlg); return; } GWEN_Dialog_free(dlg); } } bool KBanking::mapAccount(const MyMoneyAccount& acc, MyMoneyKeyValueContainer& settings) { bool rc = false; if (m_kbanking && !acc.id().isEmpty()) { m_kbanking->askMapAccount(acc); // at this point, the account should be mapped // so we search it and setup the account reference in the KMyMoney object AB_ACCOUNT* ab_acc; ab_acc = aqbAccount(acc); if (ab_acc) { MyMoneyAccount a(acc); setupAccountReference(a, ab_acc); settings = a.onlineBankingSettings(); rc = true; } } return rc; } AB_ACCOUNT* KBanking::aqbAccount(const MyMoneyAccount& acc) const { if (m_kbanking == 0) { return 0; } // certainly looking for an expense or income account does not make sense at this point // so we better get out right away if (acc.isIncomeExpense()) { return 0; } AB_ACCOUNT *ab_acc = AB_Banking_GetAccountByAlias(m_kbanking->getCInterface(), m_kbanking->mappingId(acc).toUtf8().data()); // if the account is not found, we temporarily scan for the 'old' mapping (the one w/o the file id) // in case we find it, we setup the new mapping in addition on the fly. if (!ab_acc && acc.isAssetLiability()) { ab_acc = AB_Banking_GetAccountByAlias(m_kbanking->getCInterface(), acc.id().toUtf8().data()); if (ab_acc) { qDebug("Found old mapping for '%s' but not new. Setup new mapping", qPrintable(acc.name())); m_kbanking->setAccountAlias(ab_acc, m_kbanking->mappingId(acc).toUtf8().constData()); // TODO at some point in time, we should remove the old mapping } } return ab_acc; } AB_ACCOUNT* KBanking::aqbAccount(const QString& accountId) const { MyMoneyAccount account = MyMoneyFile::instance()->account(accountId); return aqbAccount(account); } QString KBanking::stripLeadingZeroes(const QString& s) const { QString rc(s); QRegExp exp("^(0*)([^0].*)"); if (exp.exactMatch(s)) { rc = exp.cap(2); } return rc; } void KBanking::setupAccountReference(const MyMoneyAccount& acc, AB_ACCOUNT* ab_acc) { MyMoneyKeyValueContainer kvp; if (ab_acc) { QString accountNumber = stripLeadingZeroes(AB_Account_GetAccountNumber(ab_acc)); QString routingNumber = stripLeadingZeroes(AB_Account_GetBankCode(ab_acc)); QString val = QString("%1-%2").arg(routingNumber, accountNumber); if (val != acc.onlineBankingSettings().value("kbanking-acc-ref")) { kvp.clear(); // make sure to keep our own previous settings const QMap& vals = acc.onlineBankingSettings().pairs(); QMap::const_iterator it_p; for (it_p = vals.begin(); it_p != vals.end(); ++it_p) { if (QString(it_p.key()).startsWith("kbanking-")) { kvp.setValue(it_p.key(), *it_p); } } kvp.setValue("kbanking-acc-ref", val); kvp.setValue("provider", objectName().toLower()); setAccountOnlineParameters(acc, kvp); } } else { // clear the connection setAccountOnlineParameters(acc, kvp); } } bool KBanking::accountIsMapped(const MyMoneyAccount& acc) { return aqbAccount(acc) != 0; } bool KBanking::updateAccount(const MyMoneyAccount& acc) { return updateAccount(acc, false); } bool KBanking::updateAccount(const MyMoneyAccount& acc, bool moreAccounts) { if (!m_kbanking) return false; bool rc = false; if (!acc.id().isEmpty()) { AB_JOB *job = 0; int rv; /* get AqBanking account */ AB_ACCOUNT *ba = aqbAccount(acc); // Update the connection between the KMyMoney account and the AqBanking equivalent. // If the account is not found anymore ba == 0 and the connection is removed. setupAccountReference(acc, ba); if (!ba) { KMessageBox::error(0, i18n("" "The given application account %1 " "has not been mapped to an online " "account." "", acc.name()), i18n("Account Not Mapped")); } else { bool enqueJob = true; if (acc.onlineBankingSettings().value("kbanking-txn-download") != "no") { /* create getTransactions job */ job = AB_JobGetTransactions_new(ba); rv = AB_Job_CheckAvailability(job); if (rv) { DBG_ERROR(0, "Job \"GetTransactions\" is not available (%d)", rv); KMessageBox::error(0, i18n("" "The update job is not supported by the " "bank/account/backend.\n" ""), i18n("Job not Available")); AB_Job_free(job); job = 0; } if (job) { int days = AB_JobGetTransactions_GetMaxStoreDays(job); QDate qd; if (days > 0) { GWEN_TIME *ti1; GWEN_TIME *ti2; ti1 = GWEN_CurrentTime(); ti2 = GWEN_Time_fromSeconds(GWEN_Time_Seconds(ti1) - (60 * 60 * 24 * days)); GWEN_Time_free(ti1); ti1 = ti2; int year, month, day; if (GWEN_Time_GetBrokenDownDate(ti1, &day, &month, &year)) { DBG_ERROR(0, "Bad date"); qd = QDate(); } else qd = QDate(year, month + 1, day); GWEN_Time_free(ti1); } // get last statement request date from application account object // and start from a few days before if the date is valid QDate lastUpdate = QDate::fromString(acc.value("lastImportedTransactionDate"), Qt::ISODate); if (lastUpdate.isValid()) lastUpdate = lastUpdate.addDays(-3); int dateOption = acc.onlineBankingSettings().value("kbanking-statementDate").toInt(); switch (dateOption) { case 0: // Ask user break; case 1: // No date qd = QDate(); break; case 2: // Last download qd = lastUpdate; break; case 3: // First possible // qd is already setup break; } // the pick start date option dialog is needed in // case the dateOption is 0 or the date option is > 1 // and the qd is invalid if (dateOption == 0 || (dateOption > 1 && !qd.isValid())) { QPointer psd = new KBPickStartDate(m_kbanking, qd, lastUpdate, acc.name(), lastUpdate.isValid() ? 2 : 3, 0, true); if (psd->exec() == QDialog::Accepted) { qd = psd->date(); } else { enqueJob = false; } delete psd; } if (enqueJob) { if (qd.isValid()) { GWEN_TIME *ti1; ti1 = GWEN_Time_new(qd.year(), qd.month() - 1, qd.day(), 0, 0, 0, 0); AB_JobGetTransactions_SetFromTime(job, ti1); GWEN_Time_free(ti1); } rv = m_kbanking->enqueueJob(job); if (rv) { DBG_ERROR(0, "Error %d", rv); KMessageBox::error(0, i18n("" "Could not enqueue the job.\n" ""), i18n("Error")); } } AB_Job_free(job); } } if (enqueJob) { /* create getBalance job */ job = AB_JobGetBalance_new(ba); rv = AB_Job_CheckAvailability(job); if (!rv) rv = m_kbanking->enqueueJob(job); else rv = 0; AB_Job_free(job); if (rv) { DBG_ERROR(0, "Error %d", rv); KMessageBox::error(0, i18n("" "Could not enqueue the job.\n" ""), i18n("Error")); } else { rc = true; emit queueChanged(); } } } } // make sure we have at least one job in the queue before sending it if (!moreAccounts && m_kbanking->getEnqueuedJobs().size() > 0) executeQueue(); return rc; } void KBanking::executeQueue() { if (m_kbanking && m_kbanking->getEnqueuedJobs().size() > 0) { AB_IMEXPORTER_CONTEXT *ctx; ctx = AB_ImExporterContext_new(); int rv = m_kbanking->executeQueue(ctx); if (!rv) { m_kbanking->importContext(ctx, 0); } else { DBG_ERROR(0, "Error: %d", rv); } AB_ImExporterContext_free(ctx); } } /** @todo improve error handling, e.g. by adding a .isValid to nationalTransfer * @todo use new onlineJob system */ void KBanking::sendOnlineJob(QList& jobs) { Q_CHECK_PTR(m_kbanking); m_onlineJobQueue.clear(); QList unhandledJobs; if (!jobs.isEmpty()) { foreach (onlineJob job, jobs) { if (sepaOnlineTransfer::name() == job.task()->taskName()) { onlineJobTyped typedJob(job); enqueTransaction(typedJob); job = typedJob; } else { job.addJobMessage(onlineJobMessage(eMyMoney::OnlineJob::MessageType::Error, "KBanking", "Cannot handle this request")); unhandledJobs.append(job); } m_onlineJobQueue.insert(m_kbanking->mappingId(job), job); } executeQueue(); } jobs = m_onlineJobQueue.values() + unhandledJobs; m_onlineJobQueue.clear(); } QStringList KBanking::availableJobs(QString accountId) { try { MyMoneyAccount acc = MyMoneyFile::instance()->account(accountId); QString id = MyMoneyFile::instance()->value("kmm-id"); if(id != d->fileId) { d->jobList.clear(); d->fileId = id; } } catch (const MyMoneyException &) { // Exception usually means account was not found return QStringList(); } if(d->jobList.contains(accountId)) { return d->jobList[accountId]; } QStringList list; AB_ACCOUNT* abAccount = aqbAccount(accountId); if (!abAccount) { return list; } // Check availableJobs // sepa transfer AB_JOB* abJob = AB_JobSepaTransfer_new(abAccount); if (AB_Job_CheckAvailability(abJob) == 0) list.append(sepaOnlineTransfer::name()); AB_Job_free(abJob); d->jobList[accountId] = list; return list; } /** @brief experimenting with QScopedPointer and aqBanking pointers */ class QScopedPointerAbJobDeleter { public: static void cleanup(AB_JOB* job) { AB_Job_free(job); } }; /** @brief experimenting with QScopedPointer and aqBanking pointers */ class QScopedPointerAbAccountDeleter { public: static void cleanup(AB_ACCOUNT* account) { AB_Account_free(account); } }; IonlineTaskSettings::ptr KBanking::settings(QString accountId, QString taskName) { AB_ACCOUNT* abAcc = aqbAccount(accountId); if (abAcc == 0) return IonlineTaskSettings::ptr(); if (sepaOnlineTransfer::name() == taskName) { // Get limits for sepaonlinetransfer QScopedPointer abJob(AB_JobSepaTransfer_new(abAcc)); if (AB_Job_CheckAvailability(abJob.data()) != 0) return IonlineTaskSettings::ptr(); const AB_TRANSACTION_LIMITS* limits = AB_Job_GetFieldLimits(abJob.data()); return AB_TransactionLimits_toSepaOnlineTaskSettings(limits).dynamicCast(); } return IonlineTaskSettings::ptr(); } bool KBanking::enqueTransaction(onlineJobTyped& job) { /* get AqBanking account */ const QString accId = job.constTask()->responsibleAccount(); AB_ACCOUNT *abAccount = aqbAccount(accId); if (!abAccount) { job.addJobMessage(onlineJobMessage(eMyMoney::OnlineJob::MessageType::Warning, "KBanking", i18n("" "The given application account %1 " "has not been mapped to an online " "account." "", MyMoneyFile::instance()->account(accId).name()))); return false; } //setupAccountReference(acc, ba); // needed? AB_JOB *abJob = AB_JobSepaTransfer_new(abAccount); int rv = AB_Job_CheckAvailability(abJob); if (rv) { qDebug("AB_ERROR_OFFSET is %i", AB_ERROR_OFFSET); job.addJobMessage(onlineJobMessage(eMyMoney::OnlineJob::MessageType::Error, "AqBanking", QString("Sepa credit transfers for account \"%1\" are not available, error code %2.").arg(MyMoneyFile::instance()->account(accId).name(), rv) ) ); return false; } AB_TRANSACTION *AbTransaction = AB_Transaction_new(); // Recipient payeeIdentifiers::ibanBic beneficiaryAcc = job.constTask()->beneficiaryTyped(); AB_Transaction_SetRemoteName(AbTransaction, GWEN_StringList_fromQString(beneficiaryAcc.ownerName())); AB_Transaction_SetRemoteIban(AbTransaction, beneficiaryAcc.electronicIban().toUtf8().constData()); AB_Transaction_SetRemoteBic(AbTransaction, beneficiaryAcc.fullStoredBic().toUtf8().constData()); // Origin Account AB_Transaction_SetLocalAccount(AbTransaction, abAccount); // Purpose QStringList qPurpose = job.constTask()->purpose().split('\n'); GWEN_STRINGLIST *purpose = GWEN_StringList_fromQStringList(qPurpose); AB_Transaction_SetPurpose(AbTransaction, purpose); GWEN_StringList_free(purpose); // Reference // AqBanking duplicates the string. This should be safe. AB_Transaction_SetEndToEndReference(AbTransaction, job.constTask()->endToEndReference().toUtf8().constData()); // Other Fields AB_Transaction_SetTextKey(AbTransaction, job.constTask()->textKey()); AB_Transaction_SetValue(AbTransaction, AB_Value_fromMyMoneyMoney(job.constTask()->value())); /** @todo LOW remove Debug info */ qDebug() << "SetTransaction: " << AB_Job_SetTransaction(abJob, AbTransaction); GWEN_DB_NODE *gwenNode = AB_Job_GetAppData(abJob); GWEN_DB_SetCharValue(gwenNode, GWEN_DB_FLAGS_DEFAULT, "kmmOnlineJobId", m_kbanking->mappingId(job).toLatin1().constData()); qDebug() << "Enqueue: " << m_kbanking->enqueueJob(abJob); //delete localAcc; return true; } void KBanking::startPasswordTimer() { if (d->passwordCacheTimer->isActive()) d->passwordCacheTimer->stop(); d->passwordCacheTimer->start(); } void KBanking::slotClearPasswordCache() { m_kbanking->clearPasswordCache(); } void KBanking::slotImport() { m_statementCount = 0; statementInterface()->resetMessages(); if (!m_kbanking->interactiveImport()) qWarning("Error on import dialog"); else statementInterface()->showMessages(m_statementCount); } bool KBanking::importStatement(const MyMoneyStatement& s) { m_statementCount++; return !statementInterface()->import(s).isEmpty(); } MyMoneyAccount KBanking::account(const QString& key, const QString& value) const { return statementInterface()->account(key, value); } void KBanking::setAccountOnlineParameters(const MyMoneyAccount& acc, const MyMoneyKeyValueContainer& kvps) const { return statementInterface()->setAccountOnlineParameters(acc, kvps); } KBankingExt::KBankingExt(KBanking* parent, const char* appname, const char* fname) : AB_Banking(appname, fname) , m_parent(parent) , _jobQueue(0) { m_sepaKeywords = {QString::fromUtf8("SEPA-BASISLASTSCHRIFT"), QString::fromUtf8("SEPA-ÜBERWEISUNG")}; } int KBankingExt::init() { int rv = AB_Banking::init(); if (rv < 0) return rv; rv = onlineInit(); if (rv) { fprintf(stderr, "Error on online init (%d).\n", rv); AB_Banking::fini(); return rv; } _jobQueue = AB_Job_List2_new(); return 0; } int KBankingExt::fini() { if (_jobQueue) { AB_Job_List2_FreeAll(_jobQueue); _jobQueue = 0; } const int rv = onlineFini(); if (rv) { AB_Banking::fini(); return rv; } return AB_Banking::fini(); } int KBankingExt::executeQueue(AB_IMEXPORTER_CONTEXT *ctx) { m_parent->startPasswordTimer(); int rv = AB_Banking::executeJobs(_jobQueue, ctx); if (rv != 0) { qDebug() << "Sending queue by aqbanking got error no " << rv; } /** check result of each job */ AB_JOB_LIST2_ITERATOR* jobIter = AB_Job_List2_First(_jobQueue); if (jobIter) { AB_JOB* abJob = AB_Job_List2Iterator_Data(jobIter); while (abJob) { GWEN_DB_NODE* gwenNode = AB_Job_GetAppData(abJob); if (gwenNode == 0) { qWarning("Executed AB_Job without KMyMoney id"); abJob = AB_Job_List2Iterator_Next(jobIter); break; } QString jobIdent = QString::fromUtf8(GWEN_DB_GetCharValue(gwenNode, "kmmOnlineJobId", 0, "")); onlineJob job = m_parent->m_onlineJobQueue.value(jobIdent); if (job.isNull()) { // It should not be possible that this will happen (only if AqBanking fails heavily). //! @todo correct exception text qWarning("Executed a job which was not in queue. Please inform the KMyMoney developers."); abJob = AB_Job_List2Iterator_Next(jobIter); continue; } AB_JOB_STATUS abStatus = AB_Job_GetStatus(abJob); if (abStatus == AB_Job_StatusSent || abStatus == AB_Job_StatusPending || abStatus == AB_Job_StatusFinished || abStatus == AB_Job_StatusError || abStatus == AB_Job_StatusUnknown) job.setJobSend(); if (abStatus == AB_Job_StatusFinished) job.setBankAnswer(eMyMoney::OnlineJob::sendingState::acceptedByBank); else if (abStatus == AB_Job_StatusError || abStatus == AB_Job_StatusUnknown) job.setBankAnswer(eMyMoney::OnlineJob::sendingState::sendingError); job.addJobMessage(onlineJobMessage(eMyMoney::OnlineJob::MessageType::Debug, "KBanking", "Job was processed")); m_parent->m_onlineJobQueue.insert(jobIdent, job); abJob = AB_Job_List2Iterator_Next(jobIter); } AB_Job_List2Iterator_free(jobIter); } AB_JOB_LIST2 *oldQ = _jobQueue; _jobQueue = AB_Job_List2_new(); AB_Job_List2_FreeAll(oldQ); emit m_parent->queueChanged(); m_parent->startPasswordTimer(); return rv; } void KBankingExt::clearPasswordCache() { /* clear password DB */ GWEN_Gui_SetPasswordStatus(NULL, NULL, GWEN_Gui_PasswordStatus_Remove, 0); } std::list KBankingExt::getEnqueuedJobs() { AB_JOB_LIST2 *ll; std::list rl; ll = _jobQueue; if (ll && AB_Job_List2_GetSize(ll)) { AB_JOB *j; AB_JOB_LIST2_ITERATOR *it; it = AB_Job_List2_First(ll); assert(it); j = AB_Job_List2Iterator_Data(it); assert(j); while (j) { rl.push_back(j); j = AB_Job_List2Iterator_Next(it); } AB_Job_List2Iterator_free(it); } return rl; } int KBankingExt::enqueueJob(AB_JOB *j) { assert(_jobQueue); assert(j); AB_Job_Attach(j); AB_Job_List2_PushBack(_jobQueue, j); return 0; } int KBankingExt::dequeueJob(AB_JOB *j) { assert(_jobQueue); AB_Job_List2_Remove(_jobQueue, j); AB_Job_free(j); emit m_parent->queueChanged(); return 0; } void KBankingExt::transfer() { //m_parent->transfer(); } bool KBankingExt::askMapAccount(const MyMoneyAccount& acc) { MyMoneyFile* file = MyMoneyFile::instance(); QString bankId; QString accountId; // extract some information about the bank. if we have a sortcode // (BLZ) we display it, otherwise the name is enough. try { const MyMoneyInstitution &bank = file->institution(acc.institutionId()); bankId = bank.name(); if (!bank.sortcode().isEmpty()) bankId = bank.sortcode(); } catch (const MyMoneyException &e) { // no bank assigned, we just leave the field empty } // extract account information. if we have an account number // we show it, otherwise the name will be displayed accountId = acc.number(); if (accountId.isEmpty()) accountId = acc.name(); // do the mapping. the return value of this method is either // true, when the user mapped the account or false, if he // decided to quit the dialog. So not really a great thing // to present some more information. KBMapAccount *w; w = new KBMapAccount(this, bankId.toUtf8().constData(), accountId.toUtf8().constData()); if (w->exec() == QDialog::Accepted) { AB_ACCOUNT *a; a = w->getAccount(); assert(a); DBG_NOTICE(0, "Mapping application account \"%s\" to " "online account \"%s/%s\"", qPrintable(acc.name()), AB_Account_GetBankCode(a), AB_Account_GetAccountNumber(a)); // TODO remove the following line once we don't need backward compatibility setAccountAlias(a, acc.id().toUtf8().constData()); qDebug("Setup mapping to '%s'", acc.id().toUtf8().constData()); setAccountAlias(a, mappingId(acc).toUtf8().constData()); qDebug("Setup mapping to '%s'", mappingId(acc).toUtf8().constData()); delete w; return true; } delete w; return false; } QString KBankingExt::mappingId(const MyMoneyObject& object) const { QString id = MyMoneyFile::instance()->storageId() + QLatin1Char('-') + object.id(); // AqBanking does not handle the enclosing parens, so we remove it id.remove('{'); id.remove('}'); return id; } bool KBankingExt::interactiveImport() { AB_IMEXPORTER_CONTEXT *ctx; GWEN_DIALOG *dlg; int rv; ctx = AB_ImExporterContext_new(); dlg = AB_ImporterDialog_new(getCInterface(), ctx, NULL); if (dlg == NULL) { DBG_ERROR(0, "Could not create importer dialog."); AB_ImExporterContext_free(ctx); return false; } rv = GWEN_Gui_ExecDialog(dlg, 0); if (rv == 0) { DBG_ERROR(0, "Aborted by user"); GWEN_Dialog_free(dlg); AB_ImExporterContext_free(ctx); return false; } if (!importContext(ctx, 0)) { DBG_ERROR(0, "Error on importContext"); GWEN_Dialog_free(dlg); AB_ImExporterContext_free(ctx); return false; } GWEN_Dialog_free(dlg); AB_ImExporterContext_free(ctx); return true; } const AB_ACCOUNT_STATUS* KBankingExt::_getAccountStatus(AB_IMEXPORTER_ACCOUNTINFO *ai) { const AB_ACCOUNT_STATUS *ast; const AB_ACCOUNT_STATUS *best; best = 0; ast = AB_ImExporterAccountInfo_GetFirstAccountStatus(ai); while (ast) { if (!best) best = ast; else { const GWEN_TIME *tiBest; const GWEN_TIME *ti; tiBest = AB_AccountStatus_GetTime(best); ti = AB_AccountStatus_GetTime(ast); if (!tiBest) { best = ast; } else { if (ti) { double d; /* we have two times, compare them */ d = GWEN_Time_Diff(ti, tiBest); if (d > 0) /* newer */ best = ast; } } } ast = AB_ImExporterAccountInfo_GetNextAccountStatus(ai); } /* while */ return best; } void KBankingExt::_xaToStatement(MyMoneyStatement &ks, const MyMoneyAccount& acc, const AB_TRANSACTION *t) { const GWEN_STRINGLIST *sl; QString s; QString memo; const char *p; const AB_VALUE *val; const GWEN_TIME *ti; const GWEN_TIME *startTime = 0; MyMoneyStatement::Transaction kt; unsigned long h; kt.m_fees = MyMoneyMoney(); // bank's transaction id p = AB_Transaction_GetFiId(t); if (p) kt.m_strBankID = QString("ID ") + QString::fromUtf8(p); // payee s.truncate(0); sl = AB_Transaction_GetRemoteName(t); if (sl) { GWEN_STRINGLISTENTRY *se; se = GWEN_StringList_FirstEntry(sl); while (se) { p = GWEN_StringListEntry_Data(se); assert(p); s += QString::fromUtf8(p); se = GWEN_StringListEntry_Next(se); } // while } kt.m_strPayee = s; // memo // The variable 's' contains the old method of extracting // the memo which added a linefeed after each part received // from AqBanking. The new variable 'memo' does not have // this inserted linefeed. We keep the variable 's' to // construct the hash-value to retrieve the reference s.truncate(0); sl = AB_Transaction_GetPurpose(t); if (sl) { GWEN_STRINGLISTENTRY *se; bool insertLineSep = false; se = GWEN_StringList_FirstEntry(sl); while (se) { p = GWEN_StringListEntry_Data(se); assert(p); if (insertLineSep) s += '\n'; insertLineSep = true; s += QString::fromUtf8(p).trimmed(); memo += QString::fromUtf8(p).trimmed(); se = GWEN_StringListEntry_Next(se); } // while // Sparda / Netbank hack: the software these banks use stores // parts of the payee name in the beginning of the purpose field // in case the payee name exceeds the 27 character limit. This is // the case, when one of the strings listed in m_sepaKeywords is part // of the purpose fields but does not start at the beginning. In this // case, the part leading up to the keyword is to be treated as the // tail of the payee. Also, a blank is inserted after the keyword. QSet::const_iterator itk; for (itk = m_sepaKeywords.constBegin(); itk != m_sepaKeywords.constEnd(); ++itk) { int idx = s.indexOf(*itk); if (idx >= 0) { if (idx > 0) { // re-add a possibly removed blank to name if (kt.m_strPayee.length() < 27) kt.m_strPayee += ' '; kt.m_strPayee += s.left(idx); s = s.mid(idx); } s = QString("%1 %2").arg(*itk).arg(s.mid((*itk).length())); // now do the same for 'memo' except for updating the payee idx = memo.indexOf(*itk); if (idx >= 0) { if (idx > 0) { memo = memo.mid(idx); } } memo = QString("%1 %2").arg(*itk).arg(memo.mid((*itk).length())); break; } } // in case we have some SEPA fields filled with information // we add them to the memo field p = AB_Transaction_GetEndToEndReference(t); if (p) { s += QString(", EREF: %1").arg(p); if(memo.length()) memo.append('\n'); memo.append(QString("EREF: %1").arg(p)); } p = AB_Transaction_GetCustomerReference(t); if (p) { s += QString(", CREF: %1").arg(p); if(memo.length()) memo.append('\n'); memo.append(QString("CREF: %1").arg(p)); } p = AB_Transaction_GetMandateId(t); if (p) { s += QString(", MREF: %1").arg(p); if(memo.length()) memo.append('\n'); memo.append(QString("MREF: %1").arg(p)); } p = AB_Transaction_GetCreditorSchemeId(t); if (p) { s += QString(", CRED: %1").arg(p); if(memo.length()) memo.append('\n'); memo.append(QString("CRED: %1").arg(p)); } p = AB_Transaction_GetOriginatorIdentifier(t); if (p) { s += QString(", DEBT: %1").arg(p); if(memo.length()) memo.append('\n'); memo.append(QString("DEBT: %1").arg(p)); } } const MyMoneyKeyValueContainer& kvp = acc.onlineBankingSettings(); // check if we need the version with or without linebreaks if (kvp.value("kbanking-memo-removelinebreaks").compare(QLatin1String("no"))) { kt.m_strMemo = memo; } else { kt.m_strMemo = s; } // calculate the hash code and start with the payee info // and append the memo field h = MyMoneyTransaction::hash(kt.m_strPayee.trimmed()); h = MyMoneyTransaction::hash(s, h); // see, if we need to extract the payee from the memo field QString rePayee = kvp.value("kbanking-payee-regexp"); if (!rePayee.isEmpty() && kt.m_strPayee.isEmpty()) { QString reMemo = kvp.value("kbanking-memo-regexp"); QStringList exceptions = kvp.value("kbanking-payee-exceptions").split(';', QString::SkipEmptyParts); bool needExtract = true; QStringList::const_iterator it_s; for (it_s = exceptions.constBegin(); needExtract && it_s != exceptions.constEnd(); ++it_s) { QRegExp exp(*it_s, Qt::CaseInsensitive); if (exp.indexIn(kt.m_strMemo) != -1) { needExtract = false; } } if (needExtract) { QRegExp expPayee(rePayee, Qt::CaseInsensitive); QRegExp expMemo(reMemo, Qt::CaseInsensitive); if (expPayee.indexIn(kt.m_strMemo) != -1) { kt.m_strPayee = expPayee.cap(1); if (expMemo.indexIn(kt.m_strMemo) != -1) { kt.m_strMemo = expMemo.cap(1); } } } } kt.m_strPayee = kt.m_strPayee.trimmed(); // date ti = AB_Transaction_GetDate(t); if (!ti) ti = AB_Transaction_GetValutaDate(t); if (ti) { int year, month, day; if (!startTime) startTime = ti; /*else { dead code if (GWEN_Time_Diff(ti, startTime) < 0) startTime = ti; }*/ if (!GWEN_Time_GetBrokenDownDate(ti, &day, &month, &year)) { kt.m_datePosted = QDate(year, month + 1, day); } } else { DBG_WARN(0, "No date for transaction"); } // value val = AB_Transaction_GetValue(t); if (val) { if (ks.m_strCurrency.isEmpty()) { p = AB_Value_GetCurrency(val); if (p) ks.m_strCurrency = p; } else { p = AB_Value_GetCurrency(val); if (p) s = p; if (ks.m_strCurrency.toLower() != s.toLower()) { // TODO: handle currency difference DBG_ERROR(0, "Mixed currencies currently not allowed"); } } kt.m_amount = MyMoneyMoney(AB_Value_GetValueAsDouble(val)); // The initial implementation of this feature was based on // a denominator of 100. Since the denominator might be // different nowadays, we make sure to use 100 for the // duplicate detection QString tmpVal = kt.m_amount.formatMoney(100, false); tmpVal.remove(QRegExp("[,\\.]")); tmpVal += QLatin1String("/100"); h = MyMoneyTransaction::hash(tmpVal, h); } else { DBG_WARN(0, "No value for transaction"); } if (startTime) { int year, month, day; if (!GWEN_Time_GetBrokenDownDate(startTime, &day, &month, &year)) { QDate d(year, month + 1, day); if (!ks.m_dateBegin.isValid()) ks.m_dateBegin = d; else if (d < ks.m_dateBegin) ks.m_dateBegin = d; if (!ks.m_dateEnd.isValid()) ks.m_dateEnd = d; else if (d > ks.m_dateEnd) ks.m_dateEnd = d; } } else { DBG_WARN(0, "No date in current transaction"); } // add information about remote account to memo in case we have something const char *remoteAcc = AB_Transaction_GetRemoteAccountNumber(t); const char *remoteBankCode = AB_Transaction_GetRemoteBankCode(t); if (remoteAcc && remoteBankCode) { kt.m_strMemo += QString("\n%1/%2").arg(remoteBankCode, remoteAcc); } // make hash value unique in case we don't have one already if (kt.m_strBankID.isEmpty()) { QString hashBase; hashBase.sprintf("%s-%07lx", qPrintable(kt.m_datePosted.toString(Qt::ISODate)), h); int idx = 1; QString hash; for (;;) { hash = QString("%1-%2").arg(hashBase).arg(idx); QMap::const_iterator it; it = m_hashMap.constFind(hash); if (it == m_hashMap.constEnd()) { m_hashMap[hash] = true; break; } ++idx; } kt.m_strBankID = QString("%1-%2").arg(acc.id()).arg(hash); } // store transaction ks.m_listTransactions += kt; } bool KBankingExt::importAccountInfo(AB_IMEXPORTER_ACCOUNTINFO *ai, uint32_t /*flags*/) { const char *p; DBG_INFO(0, "Importing account..."); // account number MyMoneyStatement ks; p = AB_ImExporterAccountInfo_GetAccountNumber(ai); if (p) { ks.m_strAccountNumber = m_parent->stripLeadingZeroes(p); } p = AB_ImExporterAccountInfo_GetBankCode(ai); if (p) { ks.m_strRoutingNumber = m_parent->stripLeadingZeroes(p); } MyMoneyAccount kacc = m_parent->account("kbanking-acc-ref", QString("%1-%2").arg(ks.m_strRoutingNumber, ks.m_strAccountNumber)); ks.m_accountId = kacc.id(); // account name p = AB_ImExporterAccountInfo_GetAccountName(ai); if (p) ks.m_strAccountName = p; // account type switch (AB_ImExporterAccountInfo_GetType(ai)) { case AB_AccountType_Bank: ks.m_eType = eMyMoney::Statement::Type::Savings; break; case AB_AccountType_CreditCard: ks.m_eType = eMyMoney::Statement::Type::CreditCard; break; case AB_AccountType_Checking: ks.m_eType = eMyMoney::Statement::Type::Checkings; break; case AB_AccountType_Savings: ks.m_eType = eMyMoney::Statement::Type::Savings; break; case AB_AccountType_Investment: ks.m_eType = eMyMoney::Statement::Type::Investment; break; case AB_AccountType_Cash: default: ks.m_eType = eMyMoney::Statement::Type::None; } // account status const AB_ACCOUNT_STATUS* ast = _getAccountStatus(ai); if (ast) { const AB_BALANCE *bal; bal = AB_AccountStatus_GetBookedBalance(ast); if (!bal) bal = AB_AccountStatus_GetNotedBalance(ast); if (bal) { const AB_VALUE* val = AB_Balance_GetValue(bal); if (val) { DBG_INFO(0, "Importing balance"); ks.m_closingBalance = AB_Value_toMyMoneyMoney(val); p = AB_Value_GetCurrency(val); if (p) ks.m_strCurrency = p; } const GWEN_TIME* ti = AB_Balance_GetTime(bal); if (ti) { int year, month, day; if (!GWEN_Time_GetBrokenDownDate(ti, &day, &month, &year)) ks.m_dateEnd = QDate(year, month + 1, day); } else { DBG_WARN(0, "No time for balance"); } } else { DBG_WARN(0, "No account balance"); } } else { DBG_WARN(0, "No account status"); } // clear hash map m_hashMap.clear(); // get all transactions const AB_TRANSACTION* t = AB_ImExporterAccountInfo_GetFirstTransaction(ai); while (t) { _xaToStatement(ks, kacc, t); t = AB_ImExporterAccountInfo_GetNextTransaction(ai); } // import them if (!m_parent->importStatement(ks)) { if (KMessageBox::warningYesNo(0, i18n("Error importing statement. Do you want to continue?"), i18n("Critical Error")) == KMessageBox::No) { DBG_ERROR(0, "User aborted"); return false; } } return true; } K_PLUGIN_FACTORY_WITH_JSON(KBankingFactory, "kbanking.json", registerPlugin();) #include "kbanking.moc" diff --git a/kmymoney/plugins/ofx/import/dialogs/kofxdirectconnectdlg.cpp b/kmymoney/plugins/ofx/import/dialogs/kofxdirectconnectdlg.cpp index afc459994..ea5d29e9e 100644 --- a/kmymoney/plugins/ofx/import/dialogs/kofxdirectconnectdlg.cpp +++ b/kmymoney/plugins/ofx/import/dialogs/kofxdirectconnectdlg.cpp @@ -1,225 +1,224 @@ /*************************************************************************** kofxdirectconnectdlg.cpp ------------------- begin : Sat Nov 13 2004 copyright : (C) 2002 by Ace Jones email : acejones@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 "kofxdirectconnectdlg.h" #include "kmymoneysettings.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyofxconnector.h" class KOfxDirectConnectDlg::Private { public: Private() : m_firstData(true) {} QFile m_fpTrace; bool m_firstData; }; KOfxDirectConnectDlg::KOfxDirectConnectDlg(const MyMoneyAccount& account, QWidget *parent) : KOfxDirectConnectDlgDecl(parent), d(new Private), m_tmpfile(0), m_connector(account), m_job(0) { } KOfxDirectConnectDlg::~KOfxDirectConnectDlg() { if (d->m_fpTrace.isOpen()) { d->m_fpTrace.close(); } delete m_tmpfile; delete d; } bool KOfxDirectConnectDlg::init() { show(); QByteArray request = m_connector.statementRequest(); if (request.isEmpty()) { hide(); return false; } // For debugging, dump out the request #if 0 QFile g("request.ofx"); g.open(QIODevice::WriteOnly); QTextStream(&g) << m_connector.url() << "\n" << QString(request); g.close(); #endif if (KMyMoneySettings::logOfxTransactions()) { QString logPath = KMyMoneySettings::logPath(); d->m_fpTrace.setFileName(QString("%1/ofxlog.txt").arg(logPath)); d->m_fpTrace.open(QIODevice::WriteOnly | QIODevice::Append); } if (d->m_fpTrace.isOpen()) { QByteArray connectorData = m_connector.url().toUtf8(); d->m_fpTrace.write("url: ", 5); d->m_fpTrace.write(connectorData, strlen(connectorData)); d->m_fpTrace.write("\n", 1); d->m_fpTrace.write("request:\n", 9); QByteArray trcData(request); // make local copy trcData.replace('\r', ""); // krazy:exclude=doublequote_chars d->m_fpTrace.write(trcData, trcData.size()); d->m_fpTrace.write("\n", 1); d->m_fpTrace.write("response:\n", 10); } qDebug("creating job"); m_job = KIO::http_post(QUrl(m_connector.url()), request, KIO::HideProgressInfo); // open the temp file. We come around here twice if init() is called twice if (m_tmpfile) { qDebug() << "Already connected, using " << m_tmpfile->fileName(); delete m_tmpfile; //delete otherwise we mem leak } m_tmpfile = new QTemporaryFile(); // for debugging purposes one might want to leave the temp file around // in order to achieve this, please uncomment the next line // m_tmpfile->setAutoRemove(false); if (!m_tmpfile->open()) { qWarning("Unable to open tempfile '%s' for download.", qPrintable(m_tmpfile->fileName())); return false; } m_job->addMetaData("content-type", "Content-type: application/x-ofx"); connect(m_job, SIGNAL(result(KJob*)), this, SLOT(slotOfxFinished(KJob*))); connect(m_job, SIGNAL(data(KIO::Job*,QByteArray)), this, SLOT(slotOfxData(KIO::Job*,QByteArray))); setStatus(QString("Contacting %1...").arg(m_connector.url())); kProgress1->setMaximum(3); kProgress1->setValue(1); return true; } void KOfxDirectConnectDlg::setStatus(const QString& _status) { textLabel1->setText(_status); qDebug() << "STATUS:" << _status; } void KOfxDirectConnectDlg::setDetails(const QString& _details) { qDebug() << "DETAILS: " << _details; } void KOfxDirectConnectDlg::slotOfxData(KIO::Job*, const QByteArray& _ba) { qDebug("Got %d bytes of data", _ba.size()); if (d->m_firstData) { setStatus("Connection established, retrieving data..."); setDetails(QString("Downloading data to %1...").arg(m_tmpfile->fileName())); kProgress1->setValue(kProgress1->value() + 1); d->m_firstData = false; } m_tmpfile->write(_ba); setDetails(QString("Got %1 bytes").arg(_ba.size())); if (d->m_fpTrace.isOpen()) { QByteArray trcData(_ba); trcData.replace('\r', ""); // krazy:exclude=doublequote_chars d->m_fpTrace.write(trcData, trcData.size()); } } void KOfxDirectConnectDlg::slotOfxFinished(KJob* /* e */) { qDebug("Job finished"); kProgress1->setValue(kProgress1->value() + 1); setStatus("Completed."); if (d->m_fpTrace.isOpen()) { d->m_fpTrace.write("\nCompleted\n\n\n\n", 14); } int error = m_job->error(); if (m_tmpfile) { qDebug("Closing tempfile"); m_tmpfile->close(); } qDebug("Tempfile closed"); if (error) { qDebug("Show error message"); m_job->uiDelegate()->showErrorMessage(); } else if (m_job->isErrorPage()) { qDebug("Process error page"); QString details; if (m_tmpfile) { QFile f(m_tmpfile->fileName()); if (f.open(QIODevice::ReadOnly)) { QTextStream stream(&f); - QString line; while (!stream.atEnd()) { details += stream.readLine(); // line of text excluding '\n' } f.close(); qDebug() << "The HTTP request failed: " << details; } } KMessageBox::detailedSorry(this, i18n("The HTTP request failed."), details, i18nc("The HTTP request failed", "Failed")); } else if (m_tmpfile) { qDebug("Emit statementReady signal with '%s'", qPrintable(m_tmpfile->fileName())); emit statementReady(m_tmpfile->fileName()); qDebug("Return from signal statementReady() processing"); } delete m_tmpfile; m_tmpfile = 0; hide(); qDebug("Finishing slotOfxFinished"); } void KOfxDirectConnectDlg::reject() { if (m_job) m_job->kill(); if (m_tmpfile) { m_tmpfile->close(); delete m_tmpfile; m_tmpfile = 0; } QDialog::reject(); } diff --git a/kmymoney/plugins/ofx/import/ofxpartner.cpp b/kmymoney/plugins/ofx/import/ofxpartner.cpp index f402a3bad..f767c72cb 100644 --- a/kmymoney/plugins/ofx/import/ofxpartner.cpp +++ b/kmymoney/plugins/ofx/import/ofxpartner.cpp @@ -1,409 +1,408 @@ /*************************************************************************** ofxpartner.cpp ---------- begin : Fri Jan 23 2009 copyright : (C) 2009 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 #include "ofxpartner.h" #include "kmymoneysettings.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Some standard defined stuff collides with libofx.h #ifdef Q_CC_MSVC #undef ERROR #undef DELETE #endif // ---------------------------------------------------------------------------- // Project Includes namespace OfxPartner { bool post(const QString& request, const QMap& attr, const QUrl &url, const QUrl& filename); bool get(const QString& request, const QMap& attr, const QUrl &url, const QUrl& filename); const QString kBankFilename = "ofx-bank-index.xml"; const QString kCcFilename = "ofx-cc-index.xml"; const QString kInvFilename = "ofx-inv-index.xml"; #define VER "9" static QString directory; void setDirectory(const QString& dir) { directory = dir; } bool needReload(const QFileInfo& i) { return ((!i.isReadable()) || (i.lastModified().addDays(7) < QDateTime::currentDateTime()) || (i.size() < 1024)); } void ValidateIndexCache() { // TODO (Ace) Check whether these files exist and are recent enough before getting them again QUrl fname; QMap attr; fname = QUrl("file://" + directory + kBankFilename); QDir dir; dir.mkpath(directory); QFileInfo i(fname.toLocalFile()); if (needReload(i)) get("", attr, QUrl(QStringLiteral("http://www.ofxhome.com/api.php?all=yes")), fname); } static void ParseFile(QMap& result, const QString& fileName, const QString& bankName) { QFile f(fileName); if (f.open(QIODevice::ReadOnly)) { QTextStream stream(&f); stream.setCodec("UTF-8"); QString msg; int errl, errc; QDomDocument doc; if (doc.setContent(stream.readAll(), &msg, &errl, &errc)) { QDomNodeList olist = doc.elementsByTagName("institutionid"); for (int i = 0; i < olist.count(); ++i) { QDomNode onode = olist.item(i); if (onode.isElement()) { QDomElement elo = onode.toElement(); QString name = elo.attribute("name"); if (bankName.isEmpty()) result[name].clear(); else if (name == bankName) { result[elo.attribute("id")].clear(); } } } } f.close(); } } QStringList BankNames() { QMap result; // Make sure the index files are up to date ValidateIndexCache(); ParseFile(result, directory + kBankFilename, QString()); // Add Innovision result["Innovision"].clear(); return QStringList() << result.keys(); } QStringList FipidForBank(const QString& bank) { QMap result; ParseFile(result, directory + kBankFilename, bank); // the fipid for Innovision is 1. if (bank == "Innovision") result["1"].clear(); return QStringList() << result.keys(); } QString extractNodeText(QDomElement& node, const QString& name) { QString res; QRegExp exp("([^/]+)/?([^/].*)?"); if (exp.indexIn(name) != -1) { QDomNodeList olist = node.elementsByTagName(exp.cap(1)); if (olist.count()) { QDomNode onode = olist.item(0); if (onode.isElement()) { QDomElement elo = onode.toElement(); if (exp.cap(2).isEmpty()) { res = elo.text(); } else { res = extractNodeText(elo, exp.cap(2)); } } } } return res; } QString extractNodeText(QDomDocument& doc, const QString& name) { QString res; QRegExp exp("([^/]+)/?([^/].*)?"); if (exp.indexIn(name) != -1) { QDomNodeList olist = doc.elementsByTagName(exp.cap(1)); if (olist.count()) { QDomNode onode = olist.item(0); if (onode.isElement()) { QDomElement elo = onode.toElement(); if (exp.cap(2).isEmpty()) { res = elo.text(); } else { res = extractNodeText(elo, exp.cap(2)); } } } } return res; } OfxHomeServiceInfo ServiceInfo(const QString& fipid) { OfxHomeServiceInfo result; memset(&result.ofxInfo, 0, sizeof(result.ofxInfo)); result.ofxValidated = true; result.sslValidated = true; result.lastOfxValidated = QDate::currentDate().toString(); result.lastSslValidated = result.lastOfxValidated; // Hard-coded values for Innovision test server if (fipid == "1") { strncpy(result.ofxInfo.fid, "00000", OFX_FID_LENGTH - 1); strncpy(result.ofxInfo.org, "ReferenceFI", OFX_ORG_LENGTH - 1); strncpy(result.ofxInfo.url, "http://ofx.innovision.com", OFX_URL_LENGTH - 1); result.ofxInfo.accountlist = 1; result.ofxInfo.statements = 1; result.ofxInfo.billpay = 1; result.ofxInfo.investments = 1; return result; } QMap attr; QUrl guidFile(QString("file://%1fipid-%2.xml").arg(directory).arg(fipid)); QFileInfo i(guidFile.toLocalFile()); if (!i.isReadable() || i.lastModified().addDays(7) < QDateTime::currentDateTime()) get("", attr, QUrl(QString("http://www.ofxhome.com/api.php?lookup=%1").arg(fipid)), guidFile); QFile f(guidFile.toLocalFile()); if (f.open(QIODevice::ReadOnly)) { QTextStream stream(&f); stream.setCodec("UTF-8"); QString msg; int errl, errc; QDomDocument doc; if (doc.setContent(stream.readAll(), &msg, &errl, &errc)) { const auto fid = extractNodeText(doc, "institution/fid"); const auto org = extractNodeText(doc, "institution/org"); const auto url = extractNodeText(doc, "institution/url"); result.ofxValidated = (extractNodeText(doc, "institution/ofxfail").toUInt() == 0); result.sslValidated = (extractNodeText(doc, "institution/sslfail").toUInt() == 0); result.lastOfxValidated = extractNodeText(doc, "institution/lastofxvalidation"); result.lastSslValidated = extractNodeText(doc, "institution/lastsslvalidation"); strncpy(result.ofxInfo.fid, fid.toLatin1(), OFX_FID_LENGTH - 1); strncpy(result.ofxInfo.org, org.toLatin1(), OFX_ORG_LENGTH - 1); strncpy(result.ofxInfo.url, url.toLatin1(), OFX_URL_LENGTH - 1); result.ofxInfo.accountlist = true; result.ofxInfo.statements = true; result.ofxInfo.billpay = false; result.ofxInfo.investments = true; } } else { memset(&result.ofxInfo, 0, sizeof(result.ofxInfo)); result.ofxValidated = false; result.sslValidated = false; result.lastOfxValidated.clear(); result.lastSslValidated.clear(); qDebug() << "OFX ServiceInfo:" << f.errorString(); } return result; } bool get(const QString& request, const QMap& attr, const QUrl &url, const QUrl& filename) { Q_UNUSED(request); QByteArray req; OfxHttpRequest job("GET", url, req, attr, filename, false); return job.error() == 0; } bool post(const QString& request, const QMap& attr, const QUrl &url, const QUrl& filename) { QByteArray req(request.toUtf8()); OfxHttpRequest job("POST", url, req, attr, filename, false); return job.error() == 0; } } // namespace OfxPartner class OfxHttpRequest::Private { public: QFile m_fpTrace; }; OfxHttpRequest::OfxHttpRequest(const QString& type, const QUrl &url, const QByteArray &postData, const QMap& metaData, const QUrl& dst, bool showProgressInfo) : d(new Private) , m_dst(dst) , m_error(-1) , m_postJob(0) , m_getJob(0) { m_eventLoop = new QEventLoop(qApp->activeWindow()); if (KMyMoneySettings::logOfxTransactions()) { QString logPath = KMyMoneySettings::logPath(); d->m_fpTrace.setFileName(QString("%1/ofxlog.txt").arg(logPath)); d->m_fpTrace.open(QIODevice::WriteOnly | QIODevice::Append); } KIO::JobFlag jobFlags = KIO::DefaultFlags; if (!showProgressInfo) jobFlags = KIO::HideProgressInfo; KIO::Job* job; if(type.toLower() == QStringLiteral("get")) { job = m_getJob = KIO::copy(url, dst, jobFlags); } else { job = m_postJob = KIO::http_post(url, postData, jobFlags); m_postJob->addMetaData("content-type", "Content-type: application/x-ofx"); m_postJob->addMetaData(metaData); connect(job, SIGNAL(data(KIO::Job*,QByteArray)), this, SLOT(slotOfxData(KIO::Job*,QByteArray))); connect(job, SIGNAL(connected(KIO::Job*)), this, SLOT(slotOfxConnected(KIO::Job*))); } if (d->m_fpTrace.isOpen()) { QTextStream ts(&d->m_fpTrace); ts << "url: " << url.toDisplayString() << "\n"; ts << "request:\n" << QString(postData) << "\n" << "response:\n"; } connect(job, SIGNAL(result(KJob*)), this, SLOT(slotOfxFinished(KJob*))); job->start(); qDebug("Starting eventloop"); if (m_eventLoop) m_eventLoop->exec(); qDebug("Ending eventloop"); } OfxHttpRequest::~OfxHttpRequest() { delete m_eventLoop; if (d->m_fpTrace.isOpen()) { d->m_fpTrace.close(); } delete d; } void OfxHttpRequest::slotOfxConnected(KIO::Job*) { qDebug() << "OfxHttpRequest::slotOfxConnected" << m_dst.toLocalFile(); m_file.setFileName(m_dst.toLocalFile()); m_file.open(QIODevice::WriteOnly); } void OfxHttpRequest::slotOfxData(KIO::Job*, const QByteArray& _ba) { if (m_file.isOpen()) { m_file.write(_ba); if (d->m_fpTrace.isOpen()) { d->m_fpTrace.write(_ba); } } } void OfxHttpRequest::slotOfxFinished(KJob* /* e */) { if (m_file.isOpen()) { m_file.close(); if (d->m_fpTrace.isOpen()) { d->m_fpTrace.write("\nCompleted\n\n\n\n", 14); } } if(m_postJob) { m_error = m_postJob->error(); if (m_error) { m_postJob->uiDelegate()->showErrorMessage(); QFile::remove(m_dst.toLocalFile()); } else if (m_postJob->isErrorPage()) { QString details; QFile f(m_dst.toLocalFile()); if (f.open(QIODevice::ReadOnly)) { QTextStream stream(&f); - QString line; while (!stream.atEnd()) { details += stream.readLine(); // line of text excluding '\n' } f.close(); } KMessageBox::detailedSorry(0, i18n("The HTTP request failed."), details, i18nc("The HTTP request failed", "Failed")); QFile::remove(m_dst.toLocalFile()); } } else if(m_getJob) { m_error = m_getJob->error(); if (m_error) { m_getJob->uiDelegate()->showErrorMessage(); QFile::remove(m_dst.toLocalFile()); } } qDebug("Finishing eventloop"); if (m_eventLoop) m_eventLoop->exit(); } diff --git a/kmymoney/plugins/qif/export/mymoneyqifwriter.cpp b/kmymoney/plugins/qif/export/mymoneyqifwriter.cpp index f4aaefc29..bac385f52 100644 --- a/kmymoney/plugins/qif/export/mymoneyqifwriter.cpp +++ b/kmymoney/plugins/qif/export/mymoneyqifwriter.cpp @@ -1,468 +1,466 @@ /*************************************************************************** mymoneyqifwriter.cpp - description ------------------- begin : Sun Jan 5 2003 copyright : (C) 2000-2003 by Michael Edwardes email : mte@users.sourceforge.net Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio Allan Anderson agander93@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 "mymoneyqifwriter.h" // ---------------------------------------------------------------------------- // QT Headers #include #include #include // ---------------------------------------------------------------------------- // KDE Headers #include #include // ---------------------------------------------------------------------------- // Project Headers #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneytransaction.h" #include "mymoneytransactionfilter.h" #include "mymoneysplit.h" #include "mymoneymoney.h" #include "mymoneypayee.h" #include "mymoneyexception.h" #include "mymoneyenums.h" MyMoneyQifWriter::MyMoneyQifWriter() { } MyMoneyQifWriter::~MyMoneyQifWriter() { } void MyMoneyQifWriter::write(const QString& filename, const QString& profile, const QString& accountId, const bool accountData, const bool categoryData, const QDate& startDate, const QDate& endDate) { m_qifProfile.loadProfile("Profile-" + profile); QFile qifFile(filename); if (qifFile.open(QIODevice::WriteOnly)) { QTextStream s(&qifFile); s.setCodec("UTF-8"); try { if (categoryData) { writeCategoryEntries(s); } if (accountData) { writeAccountEntry(s, accountId, startDate, endDate); } emit signalProgress(-1, -1); } catch (const MyMoneyException &e) { KMessageBox::error(nullptr, i18n("Unexpected exception '%1'", QString::fromLatin1(e.what()))); } qifFile.close(); qDebug() << "Export completed.\n"; } else { KMessageBox::error(0, i18n("Unable to open file '%1' for writing", filename)); } } void MyMoneyQifWriter::writeAccountEntry(QTextStream& s, const QString& accountId, const QDate& startDate, const QDate& endDate) { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyAccount account; account = file->account(accountId); MyMoneyTransactionFilter filter(accountId); QString openingBalanceTransactionId; QString type = m_qifProfile.profileType(); s << "!Type:" << type << endl; if (type == "Invst") { extractInvestmentEntries(s, accountId, startDate, endDate); } else { filter.setDateFilter(startDate, endDate); QList list = file->transactionList(filter); if (!startDate.isValid() || startDate <= account.openingDate()) { s << "D" << m_qifProfile.date(account.openingDate()) << endl; openingBalanceTransactionId = file->openingBalanceTransaction(account); MyMoneySplit split; if (!openingBalanceTransactionId.isEmpty()) { MyMoneyTransaction openingBalanceTransaction = file->transaction(openingBalanceTransactionId); split = openingBalanceTransaction.splitByAccount(account.id(), true /* match */); } s << "T" << m_qifProfile.value('T', split.value()) << endl; } else { s << "D" << m_qifProfile.date(startDate) << endl; s << "T" << m_qifProfile.value('T', file->balance(accountId, startDate.addDays(-1))) << endl; } s << "CX" << endl; s << "P" << m_qifProfile.openingBalanceText() << endl; s << "L"; if (m_qifProfile.accountDelimiter().length()) s << m_qifProfile.accountDelimiter()[0]; s << account.name(); if (m_qifProfile.accountDelimiter().length() > 1) s << m_qifProfile.accountDelimiter()[1]; s << endl; s << "^" << endl; QList::ConstIterator it; signalProgress(0, list.count()); int count = 0; for (it = list.constBegin(); it != list.constEnd(); ++it) { // don't include the openingBalanceTransaction again if ((*it).id() != openingBalanceTransactionId) writeTransactionEntry(s, *it, accountId); signalProgress(++count, 0); } } } void MyMoneyQifWriter::writeCategoryEntries(QTextStream &s) { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneyAccount income; MyMoneyAccount expense; income = file->income(); expense = file->expense(); s << "!Type:Cat" << endl; QStringList list = income.accountList() + expense.accountList(); emit signalProgress(0, list.count()); QStringList::Iterator it; int count = 0; for (it = list.begin(); it != list.end(); ++it) { writeCategoryEntry(s, *it, ""); emit signalProgress(++count, 0); } } void MyMoneyQifWriter::writeCategoryEntry(QTextStream &s, const QString& accountId, const QString& leadIn) { MyMoneyAccount acc = MyMoneyFile::instance()->account(accountId); QString name = acc.name(); s << "N" << leadIn << name << endl; s << (acc.accountGroup() == eMyMoney::Account::Type::Expense ? "E" : "I") << endl; s << "^" << endl; QStringList list = acc.accountList(); QStringList::Iterator it; name += ':'; for (it = list.begin(); it != list.end(); ++it) { writeCategoryEntry(s, *it, name); } } void MyMoneyQifWriter::writeTransactionEntry(QTextStream &s, const MyMoneyTransaction& t, const QString& accountId) { MyMoneyFile* file = MyMoneyFile::instance(); MyMoneySplit split = t.splitByAccount(accountId); s << "D" << m_qifProfile.date(t.postDate()) << endl; switch (split.reconcileFlag()) { case eMyMoney::Split::State::Cleared: s << "C*" << endl; break; case eMyMoney::Split::State::Reconciled: case eMyMoney::Split::State::Frozen: s << "CX" << endl; break; default: break; } if (split.memo().length() > 0) { QString m = split.memo(); m.replace('\n', "\\n"); s << "M" << m << endl; } s << "T" << m_qifProfile.value('T', split.value()) << endl; if (split.number().length() > 0) s << "N" << split.number() << endl; if (!split.payeeId().isEmpty()) { MyMoneyPayee payee = file->payee(split.payeeId()); s << "P" << payee.name() << endl; } QList list = t.splits(); if (list.count() > 1) { MyMoneySplit sp = t.splitByAccount(accountId, false); MyMoneyAccount acc = file->account(sp.accountId()); if (acc.accountGroup() != eMyMoney::Account::Type::Income && acc.accountGroup() != eMyMoney::Account::Type::Expense) { s << "L" << m_qifProfile.accountDelimiter()[0] << MyMoneyFile::instance()->accountToCategory(sp.accountId()) << m_qifProfile.accountDelimiter()[1] << endl; } else { s << "L" << file->accountToCategory(sp.accountId()) << endl; } if (list.count() > 2) { QList::ConstIterator it; for (it = list.constBegin(); it != list.constEnd(); ++it) { if (!((*it) == split)) { writeSplitEntry(s, *it); } } } } s << "^" << endl; } void MyMoneyQifWriter::writeSplitEntry(QTextStream& s, const MyMoneySplit& split) { MyMoneyFile* file = MyMoneyFile::instance(); s << "S"; MyMoneyAccount acc = file->account(split.accountId()); if (acc.accountGroup() != eMyMoney::Account::Type::Income && acc.accountGroup() != eMyMoney::Account::Type::Expense) { s << m_qifProfile.accountDelimiter()[0] << file->accountToCategory(split.accountId()) << m_qifProfile.accountDelimiter()[1]; } else { s << file->accountToCategory(split.accountId()); } s << endl; if (split.memo().length() > 0) { QString m = split.memo(); m.replace('\n', "\\n"); s << "E" << m << endl; } s << "$" << m_qifProfile.value('$', -split.value()) << endl; } void MyMoneyQifWriter::extractInvestmentEntries(QTextStream &s, const QString& accountId, const QDate& startDate, const QDate& endDate) { MyMoneyFile* file = MyMoneyFile::instance(); QList accList = file->account(accountId).accountList(); QList::ConstIterator itAcc; for (itAcc = accList.constBegin(); itAcc != accList.constEnd(); ++itAcc) { MyMoneyTransactionFilter filter((*itAcc)); filter.setDateFilter(startDate, endDate); QList list = file->transactionList(filter); QList::ConstIterator it; signalProgress(0, list.count()); int count = 0; for (it = list.constBegin(); it != list.constEnd(); ++it) { writeInvestmentEntry(s, *it, ++count); signalProgress(count, 0); } } } void MyMoneyQifWriter::writeInvestmentEntry(QTextStream& stream, const MyMoneyTransaction& t, const int count) { QString s; QString memo; MyMoneyFile* file = MyMoneyFile::instance(); QString chkAccnt; bool isXfer = false; bool noError = true; QList lst = t.splits(); QList::Iterator it; eMyMoney::Account::Type typ; QString chkAccntId; MyMoneyMoney qty; MyMoneyMoney value; QMap map; for (int i = 0; i < lst.count(); i++) { - QString actionType = lst[i].action(); MyMoneyAccount acc = file->account(lst[i].accountId()); - QString accName = acc.name(); typ = acc.accountType(); map.insert(typ, lst[i].accountId()); if (typ == eMyMoney::Account::Type::Stock) { memo = lst[i].memo(); } } // // Add date. // if (noError) { s += 'D' + m_qifProfile.date(t.postDate()) + '\n'; } for (it = lst.begin(); it != lst.end(); ++it) { QString accName; QString actionType = (*it).action(); MyMoneyAccount acc = file->account((*it).accountId()); typ = acc.accountType(); // // eMyMoney::Account::Type::Checkings. // if ((acc.accountType() == eMyMoney::Account::Type::Checkings) || (acc.accountType() == eMyMoney::Account::Type::Cash)) { chkAccntId = (*it).accountId(); chkAccnt = file->account(chkAccntId).name(); } else if (acc.accountType() == eMyMoney::Account::Type::Income) { // // eMyMoney::Account::Type::Income. // } else if (acc.accountType() == eMyMoney::Account::Type::Expense) { // // eMyMoney::Account::Type::Expense. // } else if (acc.accountType() == eMyMoney::Account::Type::Stock) { // // eMyMoney::Account::Type::Stock. // qty = (*it).shares(); value = (*it).value(); accName = acc.name(); if ((actionType == "Dividend") || (actionType == "Buy") || (actionType == "IntIncome")) { isXfer = true; } // // Actions. // QString action; if ((*it).action() == "Dividend") { action = "DivX"; } else if ((*it).action() == "IntIncome") { action = "IntIncX"; } if ((action == "DivX") || (action == "IntIncX")) { if (map.value(eMyMoney::Account::Type::Checkings).isEmpty()) { KMessageBox::sorry(0, QString("%1").arg(i18n("Transaction number %1 is missing an account assignment.\nTransaction dropped.", count)), i18n("Invalid transaction")); noError = false; return; } MyMoneySplit sp = t.splitByAccount(map.value(eMyMoney::Account::Type::Checkings), true); QString txt = sp.value().formatMoney("", 2); if (noError) { s += 'T' + txt + '\n'; } } else if ((*it).action() == "Buy") { if (qty.isNegative()) { action = "Sell"; } else { action = "Buy"; } } else if ((*it).action() == "Add") { qty = (*it).shares(); if (qty.isNegative()) { action = "Shrsout"; } else { action = "Shrsin"; } } else if ((*it).action() == "Reinvest") { action = "ReinvDiv"; } else { action = (*it).action(); } // // Add action. // if (noError) { s += 'N' + action + '\n'; } QString txt; if ((action == "Buy") || (action == "Sell") || (action == "ReinvDiv")) { // // Add total. // txt = value.formatMoney("", 2); if (action == "Sell") { value = -value; txt = value.formatMoney("", 2); } if (noError) { s += 'T' + txt + '\n'; } // // Add price. // txt = (*it).price().formatMoney("", 6); if (noError) { s += 'I' + txt + '\n'; } if (!qty.isZero()) { // // Add quantity. // if (noError) { if (action == "Sell") { qty = -qty; } s += 'Q' + m_qifProfile.value('Q', qty) + '\n'; } } } else if ((action == "Shrsin") || (action == "Shrsout")) { // // Add quantity for "Shrsin" || "Shrsout". // if (noError) { if (action == "Shrsout") { qty = -qty; } s += 'Q' + m_qifProfile.value('Q', qty) + '\n'; } } } if (!accName.isEmpty()) { if (noError) { s += 'Y' + accName + '\n'; } } } if (!memo.isEmpty()) { if (noError) { memo.replace('\n', "\\n"); s += 'M' + memo + '\n'; } } if ((!chkAccnt.isEmpty()) && isXfer) { // // Add account - including its hierarchy. // if (noError) { s += 'L' + m_qifProfile.accountDelimiter()[0] + file->accountToCategory(chkAccntId) + m_qifProfile.accountDelimiter()[1] + '\n'; stream << s; } else { // Don't output the transaction } } else { stream << s; } stream << '^' << '\n'; } diff --git a/kmymoney/plugins/qif/import/kimportdlg.cpp b/kmymoney/plugins/qif/import/kimportdlg.cpp index 5e7429190..5f26576bd 100644 --- a/kmymoney/plugins/qif/import/kimportdlg.cpp +++ b/kmymoney/plugins/qif/import/kimportdlg.cpp @@ -1,204 +1,203 @@ /*************************************************************************** kimportdlg.cpp - description ------------------- begin : Wed May 16 2001 copyright : (C) 2001 by Michael Edwardes email : mte@users.sourceforge.net Javier Campos Morales Felix Rodriguez 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 "kimportdlg.h" // ---------------------------------------------------------------------------- // QT Headers #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Headers #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Headers #include "kmymoneyutils.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "../config/mymoneyqifprofile.h" #include using namespace Icons; KImportDlg::KImportDlg(QWidget *parent) : KImportDlgDecl(parent) { // Set all the last used options readConfig(); loadProfiles(true); // load button icons KGuiItem okButtenItem(i18n("&Import"), Icons::get(Icon::DocumentImport), i18n("Start operation"), i18n("Use this to start the import operation")); KGuiItem::assign(m_buttonBox->button(QDialogButtonBox::Ok), okButtenItem); KGuiItem browseButtenItem(i18n("&Browse..."), Icons::get(Icon::DocumentOpen), i18n("Select filename"), i18n("Use this to select a filename to export to")); KGuiItem::assign(m_qbuttonBrowse, browseButtenItem); KGuiItem newButtenItem(i18nc("New profile", "&New..."), Icons::get(Icon::DocumentNew), i18n("Create a new profile"), i18n("Use this to open the profile editor")); // connect the buttons to their functionality connect(m_qbuttonBrowse, &QPushButton::clicked, this, &KImportDlg::slotBrowse); connect(m_buttonBox, &QDialogButtonBox::accepted, this, &KImportDlg::slotOkClicked); connect(m_buttonBox, &QDialogButtonBox::rejected, this, &KImportDlg::reject); // connect the change signals to the check slot and perform initial check connect(m_qlineeditFile, &KLineEdit::textChanged, this, &KImportDlg::slotFileTextChanged); // setup button enable status slotFileTextChanged(m_qlineeditFile->text()); } KImportDlg::~KImportDlg() { } void KImportDlg::slotBrowse() { // determine what the browse prefix should be from the current profile MyMoneyQifProfile tmpprofile; tmpprofile.loadProfile("Profile-" + profile()); QUrl file = QFileDialog::getOpenFileUrl(this, i18n("Import File..."), QUrl::fromLocalFile(m_qlineeditFile->text()), i18n("Import files (%1);;All files (%2)", tmpprofile.filterFileType(), "*")); if (!file.isEmpty()) { m_qlineeditFile->setText(file.toDisplayString(QUrl::PreferLocalFile)); } } void KImportDlg::slotOkClicked() { // Save the used options. writeConfig(); // leave dialog directly accept(); } void KImportDlg::readConfig() { KSharedConfigPtr kconfig = KSharedConfig::openConfig(); KConfigGroup kgrp = kconfig->group("Last Use Settings"); m_qlineeditFile->setText(kgrp.readEntry("KImportDlg_LastFile")); } void KImportDlg::writeConfig() { KSharedConfigPtr kconfig = KSharedConfig::openConfig(); KConfigGroup grp = kconfig->group("Last Use Settings"); grp.writeEntry("KImportDlg_LastFile", m_qlineeditFile->text()); grp.writeEntry("KImportDlg_LastProfile", m_profileComboBox->currentText()); kconfig->sync(); } /** Make sure the text input is ok */ void KImportDlg::slotFileTextChanged(const QString& text) { bool fileExists = false; if (file().isValid()) { Q_CONSTEXPR short int detailLevel = 0; // it's a file or a directory or a symlink, or it doesn't exist KIO::StatJob* statjob = KIO::stat(file(), KIO::StatJob::SourceSide, detailLevel); bool noerror = statjob->exec(); if (noerror) { // We want a file fileExists = !statjob->statResult().isDir(); } } if (!text.isEmpty() && fileExists) { // m_qcomboboxDateFormat->setEnabled(true); m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); m_qlineeditFile->setText(text); } else { // m_qcomboboxDateFormat->setEnabled(false); m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); } } void KImportDlg::loadProfiles(const bool selectLast) { QString current = m_profileComboBox->currentText(); m_profileComboBox->clear(); QStringList list; KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup grp = config->group("Profiles"); list = grp.readEntry("profiles", QStringList()); list.sort(); m_profileComboBox->addItems(list); if (selectLast == true) { config->group("Last Use Settings"); current = grp.readEntry("KImportDlg_LastProfile"); } int index = m_profileComboBox->findText(current, Qt::MatchExactly); if (index > -1) { m_profileComboBox->setCurrentIndex(index); } else { m_profileComboBox->setCurrentIndex(0); } } void KImportDlg::addCategories(QStringList& strList, const QString& id, const QString& leadIn) const { MyMoneyFile *file = MyMoneyFile::instance(); - QString name; MyMoneyAccount account = file->account(id); QStringList accList = account.accountList(); QStringList::ConstIterator it_a; for (it_a = accList.constBegin(); it_a != accList.constEnd(); ++it_a) { account = file->account(*it_a); strList << leadIn + account.name(); addCategories(strList, *it_a, leadIn + account.name() + MyMoneyFile::AccountSeparator); } } diff --git a/kmymoney/plugins/sql/mymoneystoragesql.cpp b/kmymoney/plugins/sql/mymoneystoragesql.cpp index 9e45abc07..d20b68188 100644 --- a/kmymoney/plugins/sql/mymoneystoragesql.cpp +++ b/kmymoney/plugins/sql/mymoneystoragesql.cpp @@ -1,2906 +1,2903 @@ /*************************************************************************** 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. * * * ***************************************************************************/ #include "mymoneystoragesql_p.h" // ---------------------------------------------------------------------------- // System Includes // ---------------------------------------------------------------------------- // QT Includes #include // ---------------------------------------------------------------------------- // KDE Includes #include "KMessageBox" // ---------------------------------------------------------------------------- // Project Includes //************************ Constructor/Destructor ***************************** MyMoneyStorageSql::MyMoneyStorageSql(MyMoneyStorageMgr *storage, const QUrl &url) : QSqlDatabase(QUrlQuery(url).queryItemValue("driver")), d_ptr(new MyMoneyStorageSqlPrivate(this)) { Q_D(MyMoneyStorageSql); d->m_storage = storage; } MyMoneyStorageSql::~MyMoneyStorageSql() { try { close(true); } catch (const MyMoneyException &e) { qDebug() << "Caught Exception in MMStorageSql dtor: " << e.what(); } Q_D(MyMoneyStorageSql); delete d; } uint MyMoneyStorageSql::currentVersion() const { Q_D(const MyMoneyStorageSql); return (d->m_db.currentVersion()); } int MyMoneyStorageSql::open(const QUrl &url, int openMode, bool clear) { Q_D(MyMoneyStorageSql); try { int rc = 0; d->m_driver = MyMoneyDbDriver::create(QUrlQuery(url).queryItemValue("driver")); //get the input options QStringList options = QUrlQuery(url).queryItemValue("options").split(','); d->m_loadAll = true; // force loading whole database into memory since unification of storages // options.contains("loadAll")/*|| m_mode == 0*/; d->m_override = options.contains("override"); // create the database connection // regarding the construction of the database name see the discussion on // https://phabricator.kde.org/D12681. In case of a local file based DB // driver we cut off the leading slash only in those cases, where we // a) have a file based DB on Windows systems and // b) have a server based DB. // so that we do not modify the absolute path on *nix based systems // in case of a DB based driver QString dbName = url.path(); if(d->m_driver->requiresExternalFile()) { #ifdef Q_OS_WIN dbName = url.path().remove(0, 1); // remove separator slash for files on Windows #endif } else { dbName = url.path().remove(0, 1); // remove separator slash for server based databases } setDatabaseName(dbName); setHostName(url.host()); setUserName(url.userName()); setPassword(url.password()); if (QUrlQuery(url).queryItemValue("driver").contains("QMYSQL")) { setConnectOptions("MYSQL_OPT_RECONNECT=1"); } QSqlQuery query(*this); switch (openMode) { case QIODevice::ReadOnly: // OpenDatabase menu entry (or open last file) case QIODevice::ReadWrite: // Save menu entry with database open // this may be a sqlite file opened from the recently used list // but which no longer exists. In that case, open will work but create an empty file. // This is not what the user's after; he may accuse KMM of deleting all his data! if (d->m_driver->requiresExternalFile()) { if (!d->fileExists(dbName)) { rc = 1; break; } } if (!QSqlDatabase::open()) { d->buildError(QSqlQuery(*this), Q_FUNC_INFO, "opening database"); rc = 1; } else { if (driverName().compare(QLatin1String("QSQLCIPHER")) == 0) { auto passphrase = password(); while (true) { if (!passphrase.isEmpty()) { query.exec(QString::fromLatin1("PRAGMA cipher_version")); if(!query.next()) throw MYMONEYEXCEPTION_CSTRING("Based on empty cipher_version, libsqlcipher is not in use."); query.exec(QString::fromLatin1("PRAGMA key = '%1'").arg(passphrase)); // SQLCipher feature to decrypt a database } query.exec(QStringLiteral("SELECT count(*) FROM sqlite_master")); // SQLCipher recommended way to check if password is correct if (query.next()) { rc = d->createTables(); // check all tables are present, create if not break; } auto ok = false; passphrase = QInputDialog::getText(nullptr, i18n("Password"), i18n("You're trying to open an encrypted database.\n" "Please provide a password in order to open it."), QLineEdit::Password, QString(), &ok); if (!ok) { QSqlDatabase::close(); throw MYMONEYEXCEPTION_CSTRING("Bad password."); } } } else { rc = d->createTables(); // check all tables are present, create if not } } break; case QIODevice::WriteOnly: // SaveAs Database - if exists, must be empty, if not will create { // Try to open the database. // If that fails, try to create the database, then try to open it again. d->m_newDatabase = true; // QSqlDatabase::open() always returns true on MS Windows // even if SQLite database doesn't exist auto isSQLiteAutocreated = false; if (driverName().compare(QLatin1String("QSQLITE")) == 0 || driverName().compare(QLatin1String("QSQLCIPHER")) == 0) { if (!QFile::exists(dbName)) isSQLiteAutocreated = true; } const auto isSuccessfullyOpened = QSqlDatabase::open(); if (!isSuccessfullyOpened || (isSQLiteAutocreated && isSuccessfullyOpened)) { if (!d->createDatabase(url)) { rc = 1; } else { if (!QSqlDatabase::open()) { d->buildError(QSqlQuery(*this), Q_FUNC_INFO, "opening new database"); rc = 1; } else { query.exec(QString::fromLatin1("PRAGMA key = '%1'").arg(password())); rc = d->createTables(); } } } else { if (driverName().compare(QLatin1String("QSQLCIPHER")) == 0 && !password().isEmpty()) { KMessageBox::information(nullptr, i18n("Overwriting an existing database with an encrypted database is not yet supported.\n" "Please save your database under a new name.")); QSqlDatabase::close(); rc = 3; return rc; } rc = d->createTables(); if (rc == 0) { if (clear) { d->clean(); } else { rc = d->isEmpty(); } } } break; } default: qWarning("%s", qPrintable(QString("%1 - unknown open mode %2").arg(Q_FUNC_INFO).arg(openMode))); } if (rc != 0) return (rc); // bypass logon check if we are creating a database if (d->m_newDatabase) return(0); // check if the database is locked, if not lock it d->readFileInfo(); if (!d->m_logonUser.isEmpty() && (!d->m_override)) { d->m_error = i18n("Database apparently in use\nOpened by %1 on %2 at %3.\nOpen anyway?", d->m_logonUser, d->m_logonAt.date().toString(Qt::ISODate), d->m_logonAt.time().toString("hh.mm.ss")); qDebug("%s", qPrintable(d->m_error)); close(false); rc = -1; // retryable error } else { d->m_logonUser = url.userName() + '@' + url.host(); d->m_logonAt = QDateTime::currentDateTime(); d->writeFileInfo(); } return(rc); } catch (const QString& s) { qDebug("%s", qPrintable(s)); return (1); } } void MyMoneyStorageSql::close(bool logoff) { Q_D(MyMoneyStorageSql); if (QSqlDatabase::isOpen()) { if (logoff) { MyMoneyDbTransaction t(*this, Q_FUNC_INFO); d->m_logonUser.clear(); d->writeFileInfo(); } QSqlDatabase::close(); QSqlDatabase::removeDatabase(connectionName()); } } ulong MyMoneyStorageSql::getRecCount(const QString& table) const { Q_D(const MyMoneyStorageSql); QSqlQuery q(*const_cast (this)); q.prepare(QString("SELECT COUNT(*) FROM %1;").arg(table)); if ((!q.exec()) || (!q.next())) { // krazy:exclude=crashy d->buildError(q, Q_FUNC_INFO, "error retrieving record count"); qFatal("Error retrieving record count"); // definitely shouldn't happen } return ((ulong) q.value(0).toULongLong()); } ////////////////////////////////////////////////////////////////// bool MyMoneyStorageSql::readFile() { Q_D(MyMoneyStorageSql); d->m_displayStatus = true; try { d->readFileInfo(); d->readInstitutions(); if (d->m_loadAll) { readPayees(); } else { QList user; user.append(QString("USER")); readPayees(user); } readTags(); d->readCurrencies(); d->readSecurities(); d->readAccounts(); if (d->m_loadAll) { d->readTransactions(); } else { if (d->m_preferred.filterSet().singleFilter.accountFilter) readTransactions(d->m_preferred); } d->readSchedules(); d->readPrices(); d->readReports(); d->readBudgets(); d->readOnlineJobs(); //FIXME - ?? if (m_mode == 0) //m_storage->rebuildAccountBalances(); // this seems to be nonsense, but it clears the dirty flag // as a side-effect. d->m_storage->setLastModificationDate(d->m_storage->lastModificationDate()); // FIXME?? if (m_mode == 0) m_storage = NULL; // make sure the progress bar is not shown any longer d->signalProgress(-1, -1); d->m_displayStatus = false; //MyMoneySqlQuery::traceOn(); return true; } catch (const QString &) { return false; } // this seems to be nonsense, but it clears the dirty flag // as a side-effect. } // The following is called from 'SaveAsDatabase' bool MyMoneyStorageSql::writeFile() { Q_D(MyMoneyStorageSql); // initialize record counts and hi ids d->m_institutions = d->m_accounts = d->m_payees = d->m_tags = d->m_transactions = d->m_splits = d->m_securities = d->m_prices = d->m_currencies = d->m_schedules = d->m_reports = d->m_kvps = d->m_budgets = 0; d->m_hiIdInstitutions = d->m_hiIdPayees = d->m_hiIdTags = d->m_hiIdAccounts = d->m_hiIdTransactions = d->m_hiIdSchedules = d->m_hiIdSecurities = d->m_hiIdReports = d->m_hiIdBudgets = 0; d->m_onlineJobs = d->m_payeeIdentifier = 0; d->m_displayStatus = true; try { const auto driverName = this->driverName(); if (driverName.compare(QLatin1String("QSQLITE")) == 0 || driverName.compare(QLatin1String("QSQLCIPHER")) == 0) { QSqlQuery query(*this); query.exec("PRAGMA foreign_keys = ON"); // this is needed for "ON UPDATE" and "ON DELETE" to work } MyMoneyDbTransaction t(*this, Q_FUNC_INFO); d->writeInstitutions(); d->writePayees(); d->writeTags(); d->writeAccounts(); d->writeTransactions(); d->writeSchedules(); d->writeSecurities(); d->writePrices(); d->writeCurrencies(); d->writeReports(); d->writeBudgets(); d->writeOnlineJobs(); d->writeFileInfo(); // this seems to be nonsense, but it clears the dirty flag // as a side-effect. //m_storage->setLastModificationDate(m_storage->lastModificationDate()); // FIXME?? if (m_mode == 0) m_storage = NULL; // make sure the progress bar is not shown any longer d->signalProgress(-1, -1); d->m_displayStatus = false; // this seems to be nonsense, but it clears the dirty flag // as a side-effect. d->m_storage->setLastModificationDate(d->m_storage->lastModificationDate()); return true; } catch (const QString &) { return false; } } QString MyMoneyStorageSql::lastError() const { Q_D(const MyMoneyStorageSql); return d->m_error; } // --------------- SQL Transaction (commit unit) handling ----------------------------------- void MyMoneyStorageSql::startCommitUnit(const QString& callingFunction) { Q_D(MyMoneyStorageSql); if (d->m_commitUnitStack.isEmpty()) { if (!transaction()) throw MYMONEYEXCEPTION(d->buildError(QSqlQuery(), callingFunction, "starting commit unit")); } d->m_commitUnitStack.push(callingFunction); } bool MyMoneyStorageSql::endCommitUnit(const QString& callingFunction) { Q_D(MyMoneyStorageSql); // for now, we don't know if there were any changes made to the data so // we expect the data to have changed. This assumption causes some unnecessary // repaints of the UI here and there, but for now it's ok. If we can determine // that the commit() really changes the data, we can return that information // as value of this method. bool rc = true; if (d->m_commitUnitStack.isEmpty()) { throw MYMONEYEXCEPTION_CSTRING("Empty commit unit stack while trying to commit"); } if (callingFunction != d->m_commitUnitStack.top()) qDebug("%s", qPrintable(QString("%1 - %2 s/be %3").arg(Q_FUNC_INFO).arg(callingFunction).arg(d->m_commitUnitStack.top()))); d->m_commitUnitStack.pop(); if (d->m_commitUnitStack.isEmpty()) { //qDebug() << "Committing with " << QSqlQuery::refCount() << " queries"; if (!commit()) throw MYMONEYEXCEPTION(d->buildError(QSqlQuery(), callingFunction, "ending commit unit")); } return rc; } void MyMoneyStorageSql::cancelCommitUnit(const QString& callingFunction) { Q_D(MyMoneyStorageSql); if (d->m_commitUnitStack.isEmpty()) return; if (callingFunction != d->m_commitUnitStack.top()) qDebug("%s", qPrintable(QString("%1 - %2 s/be %3").arg(Q_FUNC_INFO).arg(callingFunction).arg(d->m_commitUnitStack.top()))); d->m_commitUnitStack.clear(); if (!rollback()) throw MYMONEYEXCEPTION(d->buildError(QSqlQuery(), callingFunction, "cancelling commit unit") + ' ' + callingFunction); } ///////////////////////////////////////////////////////////////////// void MyMoneyStorageSql::fillStorage() { Q_D(MyMoneyStorageSql); // if (!m_transactionListRead) // make sure we have loaded everything d->readTransactions(); // if (!m_payeeListRead) readPayees(); } //------------------------------ Write SQL routines ---------------------------------------- // **** Institutions **** void MyMoneyStorageSql::addInstitution(const MyMoneyInstitution& inst) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmInstitutions"].insertString()); QList iList; iList << inst; d->writeInstitutionList(iList , q); ++d->m_institutions; d->writeFileInfo(); } void MyMoneyStorageSql::modifyInstitution(const MyMoneyInstitution& inst) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmInstitutions"].updateString()); QVariantList kvpList; kvpList << inst.id(); d->deleteKeyValuePairs("OFXSETTINGS", kvpList); QList iList; iList << inst; d->writeInstitutionList(iList , q); d->writeFileInfo(); } void MyMoneyStorageSql::removeInstitution(const MyMoneyInstitution& inst) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QVariantList kvpList; kvpList << inst.id(); d->deleteKeyValuePairs("OFXSETTINGS", kvpList); QSqlQuery query(*this); query.prepare(d->m_db.m_tables["kmmInstitutions"].deleteString()); query.bindValue(":id", inst.id()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("deleting Institution")); // krazy:exclude=crashy --d->m_institutions; d->writeFileInfo(); } void MyMoneyStorageSql::addPayee(const MyMoneyPayee& payee) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery query(*this); query.prepare(d->m_db.m_tables["kmmPayees"].insertString()); d->writePayee(payee, query); ++d->m_payees; QVariantList identIds; QList idents = payee.payeeIdentifiers(); // Store ids which have to be stored in the map table identIds.reserve(idents.count()); foreach (payeeIdentifier ident, idents) { try { // note: this changes ident addPayeeIdentifier(ident); identIds.append(ident.idString()); } catch (const payeeIdentifier::empty &) { } } if (!identIds.isEmpty()) { // Create lists for batch processing QVariantList order; QVariantList payeeIdList; order.reserve(identIds.size()); payeeIdList.reserve(identIds.size()); for (int i = 0; i < identIds.size(); ++i) { order << i; payeeIdList << payee.id(); } query.prepare("INSERT INTO kmmPayeesPayeeIdentifier (payeeId, identifierId, userOrder) VALUES(?, ?, ?)"); query.bindValue(0, payeeIdList); query.bindValue(1, identIds); query.bindValue(2, order); if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("writing payee's identifiers")); // krazy:exclude=crashy } d->writeFileInfo(); } void MyMoneyStorageSql::modifyPayee(MyMoneyPayee payee) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery query(*this); query.prepare(d->m_db.m_tables["kmmPayees"].updateString()); d->writePayee(payee, query); // Get a list of old identifiers first query.prepare("SELECT identifierId FROM kmmPayeesPayeeIdentifier WHERE payeeId = ?"); query.bindValue(0, payee.id()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("modifying payee's identifiers (getting old values failed)")); // krazy:exclude=crashy QStringList oldIdentIds; oldIdentIds.reserve(query.numRowsAffected()); while (query.next()) oldIdentIds << query.value(0).toString(); // Add new and modify old payeeIdentifiers foreach (payeeIdentifier ident, payee.payeeIdentifiers()) { if (ident.idString().isEmpty()) { payeeIdentifier oldIdent(ident); addPayeeIdentifier(ident); // addPayeeIdentifier could fail (throws an exception then) only remove old // identifier if new one is stored correctly payee.removePayeeIdentifier(oldIdent); payee.addPayeeIdentifier(ident); } else { modifyPayeeIdentifier(ident); payee.modifyPayeeIdentifier(ident); oldIdentIds.removeAll(ident.idString()); } } // Remove identifiers which are not used anymore foreach (QString idToRemove, oldIdentIds) { payeeIdentifier ident(fetchPayeeIdentifier(idToRemove)); removePayeeIdentifier(ident); } // Update relation table query.prepare("DELETE FROM kmmPayeesPayeeIdentifier WHERE payeeId = ?"); query.bindValue(0, payee.id()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("modifying payee's identifiers (delete from mapping table)")); // krazy:exclude=crashy // Get list again because modifiyPayeeIdentifier which is used above may change the id QList idents(payee.payeeIdentifiers()); QVariantList order; QVariantList payeeIdList; QVariantList identIdList; order.reserve(idents.size()); payeeIdList.reserve(idents.size()); identIdList.reserve(idents.size()); { QList::const_iterator end = idents.constEnd(); int i = 0; for (QList::const_iterator iter = idents.constBegin(); iter != end; ++iter, ++i) { order << i; payeeIdList << payee.id(); identIdList << iter->idString(); } } query.prepare("INSERT INTO kmmPayeesPayeeIdentifier (payeeId, userOrder, identifierId) VALUES(?, ?, ?)"); query.bindValue(0, payeeIdList); query.bindValue(1, order); query.bindValue(2, identIdList); if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("writing payee's identifiers during modify")); // krazy:exclude=crashy d->writeFileInfo(); } void MyMoneyStorageSql::modifyUserInfo(const MyMoneyPayee& payee) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmPayees"].updateString()); d->writePayee(payee, q, true); d->writeFileInfo(); } void MyMoneyStorageSql::removePayee(const MyMoneyPayee& payee) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery query(*this); // Get identifiers first so we know which to delete query.prepare("SELECT identifierId FROM kmmPayeesPayeeIdentifier WHERE payeeId = ?"); query.bindValue(0, payee.id()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("removing payee's identifiers (getting old values failed)")); // krazy:exclude=crashy QStringList identIds; while (query.next()) identIds << query.value(0).toString(); QMap idents = fetchPayeeIdentifiers(identIds); foreach (payeeIdentifier ident, idents) { removePayeeIdentifier(ident); } // Delete entries from mapping table query.prepare("DELETE FROM kmmPayeesPayeeIdentifier WHERE payeeId = ?"); query.bindValue(0, payee.id()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("removing payee's identifiers (delete from mapping table)")); // krazy:exclude=crashy // Delete payee query.prepare(d->m_db.m_tables["kmmPayees"].deleteString()); query.bindValue(":id", payee.id()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("deleting Payee")); // krazy:exclude=crashy --d->m_payees; d->writeFileInfo(); } // **** Tags **** void MyMoneyStorageSql::addTag(const MyMoneyTag& tag) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmTags"].insertString()); d->writeTag(tag, q); ++d->m_tags; d->writeFileInfo(); } void MyMoneyStorageSql::modifyTag(const MyMoneyTag& tag) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmTags"].updateString()); d->writeTag(tag, q); d->writeFileInfo(); } void MyMoneyStorageSql::removeTag(const MyMoneyTag& tag) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery query(*this); query.prepare(d->m_db.m_tables["kmmTags"].deleteString()); query.bindValue(":id", tag.id()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("deleting Tag")); // krazy:exclude=crashy --d->m_tags; d->writeFileInfo(); } // **** Accounts **** void MyMoneyStorageSql::addAccount(const MyMoneyAccount& acc) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmAccounts"].insertString()); QList aList; aList << acc; d->writeAccountList(aList, q); ++d->m_accounts; d->writeFileInfo(); } void MyMoneyStorageSql::modifyAccount(const MyMoneyAccount& acc) { QList aList; aList << acc; modifyAccountList(aList); } void MyMoneyStorageSql::modifyAccountList(const QList& acc) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmAccounts"].updateString()); QVariantList kvpList; foreach (const MyMoneyAccount& a, acc) { kvpList << a.id(); } d->deleteKeyValuePairs("ACCOUNT", kvpList); d->deleteKeyValuePairs("ONLINEBANKING", kvpList); d->writeAccountList(acc, q); d->writeFileInfo(); } void MyMoneyStorageSql::removeAccount(const MyMoneyAccount& acc) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QVariantList kvpList; kvpList << acc.id(); d->deleteKeyValuePairs("ACCOUNT", kvpList); d->deleteKeyValuePairs("ONLINEBANKING", kvpList); QSqlQuery query(*this); query.prepare(d->m_db.m_tables["kmmAccounts"].deleteString()); query.bindValue(":id", acc.id()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("deleting Account")); // krazy:exclude=crashy --d->m_accounts; d->writeFileInfo(); } // **** Transactions and Splits **** void MyMoneyStorageSql::addTransaction(const MyMoneyTransaction& tx) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); // add the transaction and splits QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmTransactions"].insertString()); d->writeTransaction(tx.id(), tx, q, "N"); ++d->m_transactions; QList aList; // for each split account, update lastMod date, balance, txCount foreach (const MyMoneySplit& it_s, tx.splits()) { MyMoneyAccount acc = d->m_storage->account(it_s.accountId()); ++d->m_transactionCountMap[acc.id()]; aList << acc; } modifyAccountList(aList); // in the fileinfo record, update lastMod, txCount, next TxId d->writeFileInfo(); } void MyMoneyStorageSql::modifyTransaction(const MyMoneyTransaction& tx) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); // remove the splits of the old tx from the count table QSqlQuery query(*this); query.prepare("SELECT accountId FROM kmmSplits WHERE transactionId = :txId;"); query.bindValue(":txId", tx.id()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("retrieving old splits")); while (query.next()) { QString id = query.value(0).toString(); --d->m_transactionCountMap[id]; } // add the transaction and splits query.prepare(d->m_db.m_tables["kmmTransactions"].updateString()); d->writeTransaction(tx.id(), tx, query, "N"); QList aList; // for each split account, update lastMod date, balance, txCount foreach (const MyMoneySplit& it_s, tx.splits()) { MyMoneyAccount acc = d->m_storage->account(it_s.accountId()); ++d->m_transactionCountMap[acc.id()]; aList << acc; } modifyAccountList(aList); //writeSplits(tx.id(), "N", tx.splits()); // in the fileinfo record, update lastMod d->writeFileInfo(); } void MyMoneyStorageSql::removeTransaction(const MyMoneyTransaction& tx) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); d->deleteTransaction(tx.id()); --d->m_transactions; QList aList; // for each split account, update lastMod date, balance, txCount foreach (const MyMoneySplit& it_s, tx.splits()) { MyMoneyAccount acc = d->m_storage->account(it_s.accountId()); --d->m_transactionCountMap[acc.id()]; aList << acc; } modifyAccountList(aList); // in the fileinfo record, update lastModDate, txCount d->writeFileInfo(); } // **** Schedules **** void MyMoneyStorageSql::addSchedule(const MyMoneySchedule& sched) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmSchedules"].insertString()); d->writeSchedule(sched, q, true); ++d->m_schedules; d->writeFileInfo(); } void MyMoneyStorageSql::modifySchedule(const MyMoneySchedule& sched) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmSchedules"].updateString()); d->writeSchedule(sched, q, false); d->writeFileInfo(); } void MyMoneyStorageSql::removeSchedule(const MyMoneySchedule& sched) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); d->deleteSchedule(sched.id()); --d->m_schedules; d->writeFileInfo(); } // **** Securities **** void MyMoneyStorageSql::addSecurity(const MyMoneySecurity& sec) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmSecurities"].insertString()); d->writeSecurity(sec, q); ++d->m_securities; d->writeFileInfo(); } void MyMoneyStorageSql::modifySecurity(const MyMoneySecurity& sec) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QVariantList kvpList; kvpList << sec.id(); d->deleteKeyValuePairs("SECURITY", kvpList); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmSecurities"].updateString()); d->writeSecurity(sec, q); d->writeFileInfo(); } void MyMoneyStorageSql::removeSecurity(const MyMoneySecurity& sec) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QVariantList kvpList; kvpList << sec.id(); d->deleteKeyValuePairs("SECURITY", kvpList); QSqlQuery query(*this); query.prepare(d->m_db.m_tables["kmmSecurities"].deleteString()); query.bindValue(":id", kvpList); if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("deleting Security")); --d->m_securities; d->writeFileInfo(); } // **** Prices **** void MyMoneyStorageSql::addPrice(const MyMoneyPrice& p) { Q_D(MyMoneyStorageSql); if (d->m_readingPrices) return; // the app always calls addPrice, whether or not there is already one there MyMoneyDbTransaction t(*this, Q_FUNC_INFO); bool newRecord = false; QSqlQuery query(*this); QString s = d->m_db.m_tables["kmmPrices"].selectAllString(false); s += " WHERE fromId = :fromId AND toId = :toId AND priceDate = :priceDate;"; query.prepare(s); query.bindValue(":fromId", p.from()); query.bindValue(":toId", p.to()); query.bindValue(":priceDate", p.date().toString(Qt::ISODate)); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("finding Price")); // krazy:exclude=crashy if (query.next()) { query.prepare(d->m_db.m_tables["kmmPrices"].updateString()); } else { query.prepare(d->m_db.m_tables["kmmPrices"].insertString()); ++d->m_prices; newRecord = true; } 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()); const MyMoneySecurity sec = d->m_storage->security(p.to()); query.bindValue(":priceFormatted", p.rate(QString()).formatMoney("", sec.pricePrecision())); query.bindValue(":priceSource", p.source()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("writing Price")); // krazy:exclude=crashy if (newRecord) d->writeFileInfo(); } void MyMoneyStorageSql::removePrice(const MyMoneyPrice& p) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery query(*this); query.prepare(d->m_db.m_tables["kmmPrices"].deleteString()); query.bindValue(":fromId", p.from()); query.bindValue(":toId", p.to()); query.bindValue(":priceDate", p.date().toString(Qt::ISODate)); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("deleting Price")); // krazy:exclude=crashy --d->m_prices; d->writeFileInfo(); } // **** Currencies **** void MyMoneyStorageSql::addCurrency(const MyMoneySecurity& sec) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmCurrencies"].insertString()); d->writeCurrency(sec, q); ++d->m_currencies; d->writeFileInfo(); } void MyMoneyStorageSql::modifyCurrency(const MyMoneySecurity& sec) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmCurrencies"].updateString()); d->writeCurrency(sec, q); d->writeFileInfo(); } void MyMoneyStorageSql::removeCurrency(const MyMoneySecurity& sec) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery query(*this); query.prepare(d->m_db.m_tables["kmmCurrencies"].deleteString()); query.bindValue(":ISOcode", sec.id()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("deleting Currency")); // krazy:exclude=crashy --d->m_currencies; d->writeFileInfo(); } void MyMoneyStorageSql::addReport(const MyMoneyReport& rep) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmReportConfig"].insertString()); d->writeReport(rep, q); ++d->m_reports; d->writeFileInfo(); } void MyMoneyStorageSql::modifyReport(const MyMoneyReport& rep) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmReportConfig"].updateString()); d->writeReport(rep, q); d->writeFileInfo(); } void MyMoneyStorageSql::removeReport(const MyMoneyReport& rep) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery query(*this); query.prepare("DELETE FROM kmmReportConfig WHERE id = :id"); query.bindValue(":id", rep.id()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("deleting Report")); // krazy:exclude=crashy --d->m_reports; d->writeFileInfo(); } void MyMoneyStorageSql::addBudget(const MyMoneyBudget& bud) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmBudgetConfig"].insertString()); d->writeBudget(bud, q); ++d->m_budgets; d->writeFileInfo(); } void MyMoneyStorageSql::modifyBudget(const MyMoneyBudget& bud) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery q(*this); q.prepare(d->m_db.m_tables["kmmBudgetConfig"].updateString()); d->writeBudget(bud, q); d->writeFileInfo(); } void MyMoneyStorageSql::removeBudget(const MyMoneyBudget& bud) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery query(*this); query.prepare(d->m_db.m_tables["kmmBudgetConfig"].deleteString()); query.bindValue(":id", bud.id()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("deleting Budget")); // krazy:exclude=crashy --d->m_budgets; d->writeFileInfo(); } void MyMoneyStorageSql::addOnlineJob(const onlineJob& job) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery query(*this); query.prepare("INSERT INTO kmmOnlineJobs (id, type, jobSend, bankAnswerDate, state, locked) VALUES(:id, :type, :jobSend, :bankAnswerDate, :state, :locked);"); d->writeOnlineJob(job, query); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("writing onlineJob")); // krazy:exclude=crashy ++d->m_onlineJobs; try { // Save online task d->actOnOnlineJobInSQL(MyMoneyStorageSqlPrivate::SQLAction::Save, *job.constTask(), job.id()); } catch (onlineJob::emptyTask&) { } } void MyMoneyStorageSql::modifyOnlineJob(const onlineJob& job) { Q_D(MyMoneyStorageSql); Q_ASSERT(!job.id().isEmpty()); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery query(*this); query.prepare(QLatin1String( "UPDATE kmmOnlineJobs SET " " type = :type, " " jobSend = :jobSend, " " bankAnswerDate = :bankAnswerDate, " " state = :state, " " locked = :locked " " WHERE id = :id" )); d->writeOnlineJob(job, query); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("writing onlineJob")); // krazy:exclude=crashy try { // Modify online task d->actOnOnlineJobInSQL(MyMoneyStorageSqlPrivate::SQLAction::Modify, *job.constTask(), job.id()); } catch (onlineJob::emptyTask&) { // If there is no task attached this is fine as well } } void MyMoneyStorageSql::removeOnlineJob(const onlineJob& job) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); // Remove onlineTask first, because it could have a constraint // which could block the removal of the onlineJob try { // Remove task d->actOnOnlineJobInSQL(MyMoneyStorageSqlPrivate::SQLAction::Remove, *job.constTask(), job.id()); } catch (onlineJob::emptyTask&) { } QSqlQuery query(*this); query.prepare(d->m_db.m_tables["kmmOnlineJobs"].deleteString()); query.bindValue(":id", job.id()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("deleting onlineJob")); // krazy:exclude=crashy --d->m_onlineJobs; } void MyMoneyStorageSql::addPayeeIdentifier(payeeIdentifier& ident) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); ident = payeeIdentifier(incrementPayeeIdentfierId(), ident); QSqlQuery q(*this); q.prepare("INSERT INTO kmmPayeeIdentifier (id, type) VALUES(:id, :type)"); d->writePayeeIdentifier(ident, q); ++d->m_payeeIdentifier; try { d->actOnPayeeIdentifierObjectInSQL(MyMoneyStorageSqlPrivate::SQLAction::Save, ident); } catch (const payeeIdentifier::empty &) { } } void MyMoneyStorageSql::modifyPayeeIdentifier(const payeeIdentifier& ident) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); QSqlQuery query(*this); query.prepare("SELECT type FROM kmmPayeeIdentifier WHERE id = ?"); query.bindValue(0, ident.idString()); if (!query.exec() || !query.next()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("modifying payeeIdentifier")); // krazy:exclude=crashy bool typeChanged = (query.value(0).toString() != ident.iid()); if (typeChanged) { // Delete old identifier if type changed const payeeIdentifier oldIdent(fetchPayeeIdentifier(ident.idString())); try { d->actOnPayeeIdentifierObjectInSQL(MyMoneyStorageSqlPrivate::SQLAction::Modify, oldIdent); } catch (const payeeIdentifier::empty &) { // Note: this should not happen because the ui does not offer a way to change // the type of an payeeIdentifier if it was not correctly loaded. throw MYMONEYEXCEPTION((QString::fromLatin1("Could not modify payeeIdentifier '") + ident.idString() + QLatin1String("' because type changed and could not remove identifier of old type. Maybe a plugin is missing?")) ); // krazy:exclude=crashy } } query.prepare("UPDATE kmmPayeeIdentifier SET type = :type WHERE id = :id"); d->writePayeeIdentifier(ident, query); try { if (typeChanged) d->actOnPayeeIdentifierObjectInSQL(MyMoneyStorageSqlPrivate::SQLAction::Save, ident); else d->actOnPayeeIdentifierObjectInSQL(MyMoneyStorageSqlPrivate::SQLAction::Modify, ident); } catch (const payeeIdentifier::empty &) { } } void MyMoneyStorageSql::removePayeeIdentifier(const payeeIdentifier& ident) { Q_D(MyMoneyStorageSql); MyMoneyDbTransaction t(*this, Q_FUNC_INFO); // Remove first, the table could have a contraint which prevents removal // of row in kmmPayeeIdentifier try { d->actOnPayeeIdentifierObjectInSQL(MyMoneyStorageSqlPrivate::SQLAction::Remove, ident); } catch (const payeeIdentifier::empty &) { } QSqlQuery query(*this); query.prepare(d->m_db.m_tables["kmmPayeeIdentifier"].deleteString()); query.bindValue(":id", ident.idString()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("deleting payeeIdentifier")); // krazy:exclude=crashy --d->m_payeeIdentifier; } // **** Key/value pairs **** //******************************** read SQL routines ************************************** /*void MyMoneyStorageSql::setVersion (const QString& version) { m_dbVersion = version.section('.', 0, 0).toUInt(); m_minorVersion = version.section('.', 1, 1).toUInt(); // Okay, I made a cockup by forgetting to include a fixversion in the database // design, so we'll use the minor version as fix level (similar to VERSION // and FIXVERSION in XML file format). A second mistake was setting minor version to 1 // in the first place, so we need to subtract one on reading and add one on writing (sigh)!! m_storage->setFileFixVersion( m_minorVersion - 1); }*/ QMap MyMoneyStorageSql::fetchInstitutions(const QStringList& idList, bool forUpdate) const { Q_D(const MyMoneyStorageSql); int institutionsNb = (idList.isEmpty() ? d->m_institutions : idList.size()); d->signalProgress(0, institutionsNb, QObject::tr("Loading institutions...")); int progress = 0; QMap iList; ulong lastId = 0; const MyMoneyDbTable& t = d->m_db.m_tables["kmmInstitutions"]; QSqlQuery sq(*const_cast (this)); sq.prepare("SELECT id from kmmAccounts where institutionId = :id"); QSqlQuery query(*const_cast (this)); QString queryString(t.selectAllString(false)); // Use bind variables, instead of just inserting the values in the queryString, // so that values containing a ':' will work. if (! idList.empty()) { queryString += " WHERE"; for (int i = 0; i < idList.count(); ++i) queryString += QString(" id = :id%1 OR").arg(i); queryString = queryString.left(queryString.length() - 2); } if (forUpdate) queryString += d->m_driver->forUpdateString(); queryString += ';'; query.prepare(queryString); if (! idList.empty()) { QStringList::ConstIterator bindVal = idList.constBegin(); for (int i = 0; bindVal != idList.constEnd(); ++i, ++bindVal) { query.bindValue(QString(":id%1").arg(i), *bindVal); } } if (!query.exec()) throw MYMONEYEXCEPTION(d->buildError(query, Q_FUNC_INFO, QString::fromLatin1("reading Institution"))); // krazy:exclude=crashy int idCol = t.fieldNumber("id"); int nameCol = t.fieldNumber("name"); int managerCol = t.fieldNumber("manager"); int routingCodeCol = t.fieldNumber("routingCode"); int addressStreetCol = t.fieldNumber("addressStreet"); int addressCityCol = t.fieldNumber("addressCity"); int addressZipcodeCol = t.fieldNumber("addressZipcode"); int telephoneCol = t.fieldNumber("telephone"); while (query.next()) { MyMoneyInstitution inst; QString iid = GETSTRING(idCol); inst.setName(GETSTRING(nameCol)); inst.setManager(GETSTRING(managerCol)); inst.setSortcode(GETSTRING(routingCodeCol)); inst.setStreet(GETSTRING(addressStreetCol)); inst.setCity(GETSTRING(addressCityCol)); inst.setPostcode(GETSTRING(addressZipcodeCol)); inst.setTelephone(GETSTRING(telephoneCol)); // get list of subaccounts sq.bindValue(":id", iid); if (!sq.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("reading Institution AccountList")); // krazy:exclude=crashy QStringList aList; while (sq.next()) aList.append(sq.value(0).toString()); foreach (const QString& it, aList) inst.addAccountId(it); iList[iid] = MyMoneyInstitution(iid, inst); ulong id = MyMoneyUtils::extractId(iid); if (id > lastId) lastId = id; d->signalProgress(++progress, 0); } return iList; } QMap MyMoneyStorageSql::fetchInstitutions() const { return fetchInstitutions(QStringList(), false); } void MyMoneyStorageSql::readPayees(const QString& id) { QList list; list.append(id); readPayees(list); } void MyMoneyStorageSql::readPayees(const QList& pid) { Q_D(MyMoneyStorageSql); try { d->m_storage->loadPayees(fetchPayees(pid)); } catch (const MyMoneyException &) { } // if (pid.isEmpty()) m_payeeListRead = true; } void MyMoneyStorageSql::readPayees() { readPayees(QList()); } QMap MyMoneyStorageSql::fetchPayees(const QStringList& idList, bool /*forUpdate*/) const { Q_D(const MyMoneyStorageSql); MyMoneyDbTransaction trans(const_cast (*this), Q_FUNC_INFO); if (d->m_displayStatus) { int payeesNb = (idList.isEmpty() ? d->m_payees : idList.size()); d->signalProgress(0, payeesNb, QObject::tr("Loading payees...")); } int progress = 0; QMap pList; QSqlQuery query(*const_cast (this)); QString queryString = QLatin1String("SELECT kmmPayees.id AS id, kmmPayees.name AS name, kmmPayees.reference AS reference, " " kmmPayees.email AS email, kmmPayees.addressStreet AS addressStreet, kmmPayees.addressCity AS addressCity, kmmPayees.addressZipcode AS addressZipcode, " " kmmPayees.addressState AS addressState, kmmPayees.telephone AS telephone, kmmPayees.notes AS notes, " " kmmPayees.defaultAccountId AS defaultAccountId, kmmPayees.matchData AS matchData, kmmPayees.matchIgnoreCase AS matchIgnoreCase, " " kmmPayees.matchKeys AS matchKeys, " " kmmPayeesPayeeIdentifier.identifierId AS identId " " FROM ( SELECT * FROM kmmPayees "); if (!idList.isEmpty()) { // Create WHERE clause if needed queryString += QLatin1String(" WHERE id IN ("); queryString += QString("?, ").repeated(idList.length()); queryString.chop(2); // remove ", " from end queryString += QLatin1Char(')'); } queryString += QLatin1String( " ) kmmPayees " " LEFT OUTER JOIN kmmPayeesPayeeIdentifier ON kmmPayees.Id = kmmPayeesPayeeIdentifier.payeeId " // The order is used below " ORDER BY kmmPayees.id, kmmPayeesPayeeIdentifier.userOrder;"); query.prepare(queryString); if (!idList.isEmpty()) { // Bind values QStringList::const_iterator end = idList.constEnd(); for (QStringList::const_iterator iter = idList.constBegin(); iter != end; ++iter) { query.addBindValue(*iter); } } if (!query.exec()) throw MYMONEYEXCEPTION(d->buildError(query, Q_FUNC_INFO, QString::fromLatin1("reading Payee"))); // krazy:exclude=crashy const QSqlRecord record = query.record(); const int idCol = record.indexOf("id"); const int nameCol = record.indexOf("name"); const int referenceCol = record.indexOf("reference"); const int emailCol = record.indexOf("email"); const int addressStreetCol = record.indexOf("addressStreet"); const int addressCityCol = record.indexOf("addressCity"); const int addressZipcodeCol = record.indexOf("addressZipcode"); const int addressStateCol = record.indexOf("addressState"); const int telephoneCol = record.indexOf("telephone"); const int notesCol = record.indexOf("notes"); const int defaultAccountIdCol = record.indexOf("defaultAccountId"); const int matchDataCol = record.indexOf("matchData"); const int matchIgnoreCaseCol = record.indexOf("matchIgnoreCase"); const int matchKeysCol = record.indexOf("matchKeys"); const int identIdCol = record.indexOf("identId"); if (query.next()) { while (query.isValid()) { QString pid; - QString boolChar; MyMoneyPayee payee; uint type; bool ignoreCase; QString matchKeys; pid = GETSTRING(idCol); payee.setName(GETSTRING(nameCol)); payee.setReference(GETSTRING(referenceCol)); payee.setEmail(GETSTRING(emailCol)); payee.setAddress(GETSTRING(addressStreetCol)); payee.setCity(GETSTRING(addressCityCol)); payee.setPostcode(GETSTRING(addressZipcodeCol)); payee.setState(GETSTRING(addressStateCol)); payee.setTelephone(GETSTRING(telephoneCol)); payee.setNotes(GETSTRING(notesCol)); payee.setDefaultAccountId(GETSTRING(defaultAccountIdCol)); type = GETINT(matchDataCol); ignoreCase = (GETSTRING(matchIgnoreCaseCol) == "Y"); matchKeys = GETSTRING(matchKeysCol); payee.setMatchData(static_cast(type), ignoreCase, matchKeys); // Get payeeIdentifier ids QStringList identifierIds; do { identifierIds.append(GETSTRING(identIdCol)); } while (query.next() && GETSTRING(idCol) == pid); // as long as the payeeId is unchanged // Fetch and save payeeIdentifier if (!identifierIds.isEmpty()) { QList< ::payeeIdentifier > identifier = fetchPayeeIdentifiers(identifierIds).values(); payee.resetPayeeIdentifiers(identifier); } if (pid == "USER") d->m_storage->setUser(payee); else pList[pid] = MyMoneyPayee(pid, payee); if (d->m_displayStatus) d->signalProgress(++progress, 0); } } return pList; } QMap MyMoneyStorageSql::fetchPayees() const { return fetchPayees(QStringList(), false); } void MyMoneyStorageSql::readTags(const QString& id) { QList list; list.append(id); readTags(list); } void MyMoneyStorageSql::readTags(const QList& pid) { Q_D(MyMoneyStorageSql); try { d->m_storage->loadTags(fetchTags(pid)); } catch (const MyMoneyException &) { } } void MyMoneyStorageSql::readTags() { readTags(QList()); } QMap MyMoneyStorageSql::fetchOnlineJobs(const QStringList& idList, bool forUpdate) const { Q_D(const MyMoneyStorageSql); Q_UNUSED(forUpdate); MyMoneyDbTransaction trans(const_cast (*this), Q_FUNC_INFO); if (d->m_displayStatus) d->signalProgress(0, idList.isEmpty() ? d->m_onlineJobs : idList.size(), QObject::tr("Loading online banking data...")); // Create query QSqlQuery query(*const_cast (this)); if (idList.isEmpty()) { query.prepare("SELECT id, type, jobSend, bankAnswerDate, state, locked FROM kmmOnlineJobs;"); } else { QString queryIdSet = QString("?, ").repeated(idList.length()); queryIdSet.chop(2); query.prepare(QLatin1String("SELECT id, type, jobSend, bankAnswerDate, state, locked FROM kmmOnlineJobs WHERE id IN (") + queryIdSet + QLatin1String(");")); QStringList::const_iterator end = idList.constEnd(); for (QStringList::const_iterator iter = idList.constBegin(); iter != end; ++iter) { query.addBindValue(*iter); } } if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("reading onlineJobs")); // krazy:exclude=crashy // Create onlineJobs int progress = 0; QMap jobList; while (query.next()) { const QString& id = query.value(0).toString(); onlineTask *const task = d->createOnlineTaskObject(query.value(1).toString(), id, *this); onlineJob job = onlineJob(task, id); job.setJobSend(query.value(2).toDateTime()); eMyMoney::OnlineJob::sendingState state; const QString stateString = query.value(4).toString(); if (stateString == "acceptedByBank") state = eMyMoney::OnlineJob::sendingState::acceptedByBank; else if (stateString == "rejectedByBank") state = eMyMoney::OnlineJob::sendingState::rejectedByBank; else if (stateString == "abortedByUser") state = eMyMoney::OnlineJob::sendingState::abortedByUser; else if (stateString == "sendingError") state = eMyMoney::OnlineJob::sendingState::sendingError; else // includes: stateString == "noBankAnswer" state = eMyMoney::OnlineJob::sendingState::noBankAnswer; job.setBankAnswer(state, query.value(4).toDateTime()); job.setLock(query.value(5).toString() == QLatin1String("Y") ? true : false); jobList.insert(job.id(), job); if (d->m_displayStatus) d->signalProgress(++progress, 0); } return jobList; } QMap MyMoneyStorageSql::fetchOnlineJobs() const { return fetchOnlineJobs(QStringList(), false); } payeeIdentifier MyMoneyStorageSql::fetchPayeeIdentifier(const QString& id) const { QMap list = fetchPayeeIdentifiers(QStringList(id)); QMap::const_iterator iter = list.constFind(id); if (iter == list.constEnd()) throw MYMONEYEXCEPTION(QString::fromLatin1("payeeIdentifier with id '%1' not found").arg(id)); // krazy:exclude=crashy return *iter; } QMap< QString, payeeIdentifier > MyMoneyStorageSql::fetchPayeeIdentifiers(const QStringList& idList) const { Q_D(const MyMoneyStorageSql); MyMoneyDbTransaction trans(const_cast (*this), Q_FUNC_INFO); // Create query QSqlQuery query(*const_cast (this)); if (idList.isEmpty()) { query.prepare("SELECT id, type FROM kmmPayeeIdentifier;"); } else { QString queryIdSet = QString("?, ").repeated(idList.length()); queryIdSet.chop(2); // remove ", " from end query.prepare(QLatin1String("SELECT id, type FROM kmmPayeeIdentifier WHERE id IN (") + queryIdSet + QLatin1String(");")); QStringList::const_iterator end = idList.constEnd(); for (QStringList::const_iterator iter = idList.constBegin(); iter != end; ++iter) { query.addBindValue(*iter); } } if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("reading payee identifiers")); // krazy:exclude=crashy QMap identList; while (query.next()) { const auto id = query.value(0).toString(); identList.insert(id, d->createPayeeIdentifierObject(*this, query.value(1).toString(), id)); } return identList; } QMap< QString, payeeIdentifier > MyMoneyStorageSql::fetchPayeeIdentifiers() const { return fetchPayeeIdentifiers(QStringList()); } QMap MyMoneyStorageSql::fetchTags(const QStringList& idList, bool /*forUpdate*/) const { Q_D(const MyMoneyStorageSql); MyMoneyDbTransaction trans(const_cast (*this), Q_FUNC_INFO); if (d->m_displayStatus) { int tagsNb = (idList.isEmpty() ? d->m_tags : idList.size()); d->signalProgress(0, tagsNb, QObject::tr("Loading tags...")); } else { // if (m_tagListRead) return; } int progress = 0; QMap taList; //ulong lastId; const MyMoneyDbTable& t = d->m_db.m_tables["kmmTags"]; QSqlQuery query(*const_cast (this)); if (idList.isEmpty()) { query.prepare(t.selectAllString()); } else { QString whereClause = " where ("; QString itemConnector = ""; foreach (const QString& it, idList) { whereClause.append(QString("%1id = '%2'").arg(itemConnector).arg(it)); itemConnector = " or "; } whereClause += ')'; query.prepare(t.selectAllString(false) + whereClause); } if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("reading Tag")); // krazy:exclude=crashy int idCol = t.fieldNumber("id"); int nameCol = t.fieldNumber("name"); int notesCol = t.fieldNumber("notes"); int tagColorCol = t.fieldNumber("tagColor"); int closedCol = t.fieldNumber("closed"); while (query.next()) { QString pid; - QString boolChar; MyMoneyTag tag; pid = GETSTRING(idCol); tag.setName(GETSTRING(nameCol)); tag.setNotes(GETSTRING(notesCol)); tag.setClosed((GETSTRING(closedCol) == "Y")); tag.setTagColor(QColor(GETSTRING(tagColorCol))); taList[pid] = MyMoneyTag(pid, tag); if (d->m_displayStatus) d->signalProgress(++progress, 0); } return taList; } QMap MyMoneyStorageSql::fetchTags() const { return fetchTags(QStringList(), false); } QMap MyMoneyStorageSql::fetchAccounts(const QStringList& idList, bool forUpdate) const { Q_D(const MyMoneyStorageSql); int accountsNb = (idList.isEmpty() ? d->m_accounts : idList.size()); d->signalProgress(0, accountsNb, QObject::tr("Loading accounts...")); int progress = 0; QMap accList; QStringList kvpAccountList(idList); const MyMoneyDbTable& t = d->m_db.m_tables["kmmAccounts"]; QSqlQuery query(*const_cast (this)); QSqlQuery sq(*const_cast (this)); QString childQueryString = "SELECT id, parentId FROM kmmAccounts WHERE "; QString queryString(t.selectAllString(false)); // Use bind variables, instead of just inserting the values in the queryString, // so that values containing a ':' will work. if (! idList.empty()) { queryString += " WHERE id IN ("; childQueryString += " parentId IN ("; QString inString; for (int i = 0; i < idList.count(); ++i) { inString += QString(":id%1, ").arg(i); } inString = inString.left(inString.length() - 2) + ')'; queryString += inString; childQueryString += inString; } else { childQueryString += " NOT parentId IS NULL"; } queryString += " ORDER BY id"; childQueryString += " ORDER BY parentid, id"; if (forUpdate) { queryString += d->m_driver->forUpdateString(); childQueryString += d->m_driver->forUpdateString(); } query.prepare(queryString); sq.prepare(childQueryString); if (! idList.empty()) { QStringList::ConstIterator bindVal = idList.constBegin(); for (int i = 0; bindVal != idList.constEnd(); ++i, ++bindVal) { query.bindValue(QString(":id%1").arg(i), *bindVal); sq.bindValue(QString(":id%1").arg(i), *bindVal); } } if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("reading Account")); // krazy:exclude=crashy if (!sq.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("reading subAccountList")); // krazy:exclude=crashy // Reserve enough space for all values. Approximate it with the size of the // idList in case the db doesn't support reporting the size of the // resultset to the caller. //FIXME: this is for if/when there is a QHash conversion //accList.reserve(q.size() > 0 ? q.size() : idList.size()); static const int idCol = t.fieldNumber("id"); static const int institutionIdCol = t.fieldNumber("institutionId"); static const int parentIdCol = t.fieldNumber("parentId"); static const int lastReconciledCol = t.fieldNumber("lastReconciled"); static const int lastModifiedCol = t.fieldNumber("lastModified"); static const int openingDateCol = t.fieldNumber("openingDate"); static const int accountNumberCol = t.fieldNumber("accountNumber"); static const int accountTypeCol = t.fieldNumber("accountType"); static const int accountNameCol = t.fieldNumber("accountName"); static const int descriptionCol = t.fieldNumber("description"); static const int currencyIdCol = t.fieldNumber("currencyId"); static const int balanceCol = t.fieldNumber("balance"); static const int transactionCountCol = t.fieldNumber("transactionCount"); while (query.next()) { QString aid; - QString balance; MyMoneyAccount acc; aid = GETSTRING(idCol); acc.setInstitutionId(GETSTRING(institutionIdCol)); acc.setParentAccountId(GETSTRING(parentIdCol)); acc.setLastReconciliationDate(GETDATE_D(lastReconciledCol)); acc.setLastModified(GETDATE_D(lastModifiedCol)); acc.setOpeningDate(GETDATE_D(openingDateCol)); acc.setNumber(GETSTRING(accountNumberCol)); acc.setAccountType(static_cast(GETINT(accountTypeCol))); acc.setName(GETSTRING(accountNameCol)); acc.setDescription(GETSTRING(descriptionCol)); acc.setCurrencyId(GETSTRING(currencyIdCol)); acc.setBalance(MyMoneyMoney(GETSTRING(balanceCol))); const_cast (this)->d_func()->m_transactionCountMap[aid] = (ulong) GETULL(transactionCountCol); // Process any key value pair if (idList.empty()) kvpAccountList.append(aid); accList.insert(aid, MyMoneyAccount(aid, acc)); if (acc.value("PreferredAccount") == "Yes") { const_cast (this)->d_func()->m_preferred.addAccount(aid); } d->signalProgress(++progress, 0); } QMap::Iterator it_acc; QMap::Iterator accListEnd = accList.end(); while (sq.next()) { it_acc = accList.find(sq.value(1).toString()); if (it_acc != accListEnd && it_acc.value().id() == sq.value(1).toString()) { while (sq.isValid() && it_acc != accListEnd && it_acc.value().id() == sq.value(1).toString()) { it_acc.value().addAccountId(sq.value(0).toString()); if (!sq.next()) break; } sq.previous(); } } //TODO: There should be a better way than this. What's below is O(n log n) or more, // where it may be able to be done in O(n), if things are just right. // The operator[] call in the loop is the most expensive call in this function, according // to several profile runs. QHash kvpResult = d->readKeyValuePairs("ACCOUNT", kvpAccountList); QHash ::const_iterator kvp_end = kvpResult.constEnd(); for (QHash ::const_iterator it_kvp = kvpResult.constBegin(); it_kvp != kvp_end; ++it_kvp) { accList[it_kvp.key()].setPairs(it_kvp.value().pairs()); } kvpResult = d->readKeyValuePairs("ONLINEBANKING", kvpAccountList); kvp_end = kvpResult.constEnd(); for (QHash ::const_iterator it_kvp = kvpResult.constBegin(); it_kvp != kvp_end; ++it_kvp) { accList[it_kvp.key()].setOnlineBankingSettings(it_kvp.value()); } return accList; } QMap MyMoneyStorageSql::fetchAccounts() const { return fetchAccounts(QStringList(), false); } QMap MyMoneyStorageSql::fetchBalance(const QStringList& idList, const QDate& date) const { Q_D(const MyMoneyStorageSql); QMap returnValue; QSqlQuery query(*const_cast (this)); QString queryString = "SELECT action, shares, accountId, postDate " "FROM kmmSplits WHERE txType = 'N'"; if (idList.count() > 0) { queryString += "AND accountId in ("; for (int i = 0; i < idList.count(); ++i) { queryString += QString(":id%1, ").arg(i); } queryString = queryString.left(queryString.length() - 2) + ')'; } // SQLite stores dates as YYYY-MM-DDTHH:mm:ss with 0s for the time part. This makes // the <= operator misbehave when the date matches. To avoid this, add a day to the // requested date and use the < operator. if (date.isValid() && !date.isNull()) queryString += QString(" AND postDate < '%1'").arg(date.addDays(1).toString(Qt::ISODate)); queryString += " ORDER BY accountId, postDate;"; //DBG(queryString); query.prepare(queryString); int i = 0; foreach (const QString& bindVal, idList) { query.bindValue(QString(":id%1").arg(i), bindVal); ++i; } if (!query.exec()) // krazy:exclude=crashy throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("fetching balance")); QString id; QString oldId; MyMoneyMoney temp; while (query.next()) { id = query.value(2).toString(); // If the old ID does not match the new ID, then the account being summed has changed. // Write the balance into the returnValue map and update the oldId to the current one. if (id != oldId) { if (!oldId.isEmpty()) { returnValue.insert(oldId, temp); temp = 0; } oldId = id; } if (MyMoneySplit::actionName(eMyMoney::Split::Action::SplitShares) == query.value(0).toString()) temp *= MyMoneyMoney(query.value(1).toString()); else temp += MyMoneyMoney(query.value(1).toString()); } // Do not forget the last id in the list. returnValue.insert(id, temp); // Return the map. return returnValue; } void MyMoneyStorageSql::readTransactions(const MyMoneyTransactionFilter& filter) { Q_D(MyMoneyStorageSql); try { d->m_storage->loadTransactions(fetchTransactions(filter)); } catch (const MyMoneyException &) { throw; } } QMap MyMoneyStorageSql::fetchTransactions(const QString& tidList, const QString& dateClause, bool /*forUpdate*/) const { Q_D(const MyMoneyStorageSql); // if (m_transactionListRead) return; // all list already in memory if (d->m_displayStatus) { int transactionsNb = (tidList.isEmpty() ? d->m_transactions : tidList.size()); d->signalProgress(0, transactionsNb, QObject::tr("Loading transactions...")); } int progress = 0; // m_payeeList.clear(); QString whereClause = " WHERE txType = 'N' "; if (! tidList.isEmpty()) { whereClause += " AND id IN " + tidList; } if (!dateClause.isEmpty()) whereClause += " and " + dateClause; const MyMoneyDbTable& t = d->m_db.m_tables["kmmTransactions"]; QSqlQuery query(*const_cast (this)); query.prepare(t.selectAllString(false) + whereClause + " ORDER BY id;"); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("reading Transaction")); // krazy:exclude=crashy const MyMoneyDbTable& ts = d->m_db.m_tables["kmmSplits"]; whereClause = " WHERE txType = 'N' "; if (! tidList.isEmpty()) { whereClause += " AND transactionId IN " + tidList; } if (!dateClause.isEmpty()) whereClause += " and " + dateClause; QSqlQuery qs(*const_cast (this)); QString splitQuery = ts.selectAllString(false) + whereClause + " ORDER BY transactionId, splitId;"; qs.prepare(splitQuery); if (!qs.exec()) throw MYMONEYEXCEPTION(d->buildError(qs, Q_FUNC_INFO, "reading Splits")); // krazy:exclude=crashy QString splitTxId = "ZZZ"; MyMoneySplit s; if (qs.next()) { splitTxId = qs.value(0).toString(); s = d->readSplit(qs); } else { splitTxId = "ZZZ"; } QMap txMap; QStringList txList; int idCol = t.fieldNumber("id"); int postDateCol = t.fieldNumber("postDate"); int memoCol = t.fieldNumber("memo"); int entryDateCol = t.fieldNumber("entryDate"); int currencyIdCol = t.fieldNumber("currencyId"); int bankIdCol = t.fieldNumber("bankId"); while (query.next()) { MyMoneyTransaction tx; QString txId = GETSTRING(idCol); tx.setPostDate(GETDATE_D(postDateCol)); tx.setMemo(GETSTRING(memoCol)); tx.setEntryDate(GETDATE_D(entryDateCol)); tx.setCommodity(GETSTRING(currencyIdCol)); tx.setBankID(GETSTRING(bankIdCol)); // skip all splits while the transaction id of the split is less than // the transaction id of the current transaction. Don't forget to check // for the ZZZ flag for the end of the list. while (txId < splitTxId && splitTxId != "ZZZ") { if (qs.next()) { splitTxId = qs.value(0).toString(); s = d->readSplit(qs); } else { splitTxId = "ZZZ"; } } // while the split transaction id matches the current transaction id, // add the split to the current transaction. Set the ZZZ flag if // all splits for this transaction have been read. while (txId == splitTxId) { tx.addSplit(s); if (qs.next()) { splitTxId = qs.value(0).toString(); s = d->readSplit(qs); } else { splitTxId = "ZZZ"; } } // Process any key value pair if (! txId.isEmpty()) { txList.append(txId); tx = MyMoneyTransaction(txId, tx); txMap.insert(tx.uniqueSortKey(), tx); } } // get the kvps QHash kvpMap = d->readKeyValuePairs("TRANSACTION", txList); QMap::Iterator txMapEnd = txMap.end(); for (QMap::Iterator i = txMap.begin(); i != txMapEnd; ++i) { i.value().setPairs(kvpMap[i.value().id()].pairs()); if (d->m_displayStatus) d->signalProgress(++progress, 0); } if ((tidList.isEmpty()) && (dateClause.isEmpty())) { //qDebug("setting full list read"); } return txMap; } QMap MyMoneyStorageSql::fetchTransactions(const QString& tidList) const { return fetchTransactions(tidList, QString(), false); } QMap MyMoneyStorageSql::fetchTransactions() const { return fetchTransactions(QString(), QString(), false); } QMap MyMoneyStorageSql::fetchTransactions(const MyMoneyTransactionFilter& filter) const { Q_D(const MyMoneyStorageSql); // analyze the filter // if (m_transactionListRead) return; // all list already in memory // if the filter is restricted to certain accounts/categories // check if we already have them all in memory QStringList accounts; QString inQuery; filter.accounts(accounts); filter.categories(accounts); // QStringList::iterator it; // bool allAccountsLoaded = true; // for (it = accounts.begin(); it != accounts.end(); ++it) { // if (m_accountsLoaded.find(*it) == m_accountsLoaded.end()) { // allAccountsLoaded = false; // break; // } // } // if (allAccountsLoaded) return; /* Some filter combinations do not lend themselves to implementation * in SQL, or are likely to require such extensive reading of the database * as to make it easier to just read everything into memory. */ bool canImplementFilter = true; MyMoneyMoney m1, m2; if (filter.amountFilter(m1, m2)) { d->alert("Amount Filter Set"); canImplementFilter = false; } QString n1, n2; if (filter.numberFilter(n1, n2)) { d->alert("Number filter set"); canImplementFilter = false; } int t1; if (filter.firstType(t1)) { d->alert("Type filter set"); canImplementFilter = false; } // int s1; // if (filter.firstState(s1)) { // alert("State filter set"); // canImplementFilter = false; // } QRegExp t2; if (filter.textFilter(t2)) { d->alert("text filter set"); canImplementFilter = false; } MyMoneyTransactionFilter::FilterSet s = filter.filterSet(); if (s.singleFilter.validityFilter) { d->alert("Validity filter set"); canImplementFilter = false; } if (!canImplementFilter) { QMap transactionList = fetchTransactions(); std::remove_if(transactionList.begin(), transactionList.end(), FilterFail(filter)); return transactionList; } bool splitFilterActive = false; // the split filter is active if we are selecting on fields in the split table // get start and end dates QDate start = filter.fromDate(); QDate end = filter.toDate(); // not entirely sure if the following is correct, but at best, saves a lot of reads, at worst // it only causes us to read a few more transactions that strictly necessary (I think...) if (start == MyMoneyStorageSqlPrivate::m_startDate) start = QDate(); bool txFilterActive = ((start != QDate()) || (end != QDate())); // and this for fields in the transaction table QString whereClause = ""; QString subClauseconnector = " where txType = 'N' and "; // payees QStringList payees; if (filter.payees(payees)) { QString itemConnector = "payeeId in ("; QString payeesClause = ""; foreach (const QString& it, payees) { payeesClause.append(QString("%1'%2'") .arg(itemConnector).arg(it)); itemConnector = ", "; } if (!payeesClause.isEmpty()) { whereClause += subClauseconnector + payeesClause + ')'; subClauseconnector = " and "; } splitFilterActive = true; } //tags QStringList tags; if (filter.tags(tags)) { QString itemConnector = "splitId in ( SELECT splitId from kmmTagSplits where kmmTagSplits.transactionId = kmmSplits.transactionId and tagId in ("; QString tagsClause = ""; foreach (const QString& it, tags) { tagsClause.append(QString("%1'%2'") .arg(itemConnector).arg(it)); itemConnector = ", "; } if (!tagsClause.isEmpty()) { whereClause += subClauseconnector + tagsClause + ')'; subClauseconnector = " and "; } splitFilterActive = true; } // accounts and categories if (!accounts.isEmpty()) { splitFilterActive = true; QString itemConnector = "accountId in ("; QString accountsClause = ""; foreach (const QString& it, accounts) { accountsClause.append(QString("%1 '%2'") .arg(itemConnector).arg(it)); itemConnector = ", "; } if (!accountsClause.isEmpty()) { whereClause += subClauseconnector + accountsClause + ')'; subClauseconnector = " and ("; } } // split states QList splitStates; if (filter.states(splitStates)) { splitFilterActive = true; QString itemConnector = " reconcileFlag IN ("; QString statesClause = ""; foreach (int it, splitStates) { statesClause.append(QString(" %1 '%2'").arg(itemConnector) .arg(d->splitState(TransactionFilter::State(it)))); itemConnector = ','; } if (!statesClause.isEmpty()) { whereClause += subClauseconnector + statesClause + ')'; subClauseconnector = " and ("; } } // I've given up trying to work out the logic. we keep getting the wrong number of close brackets int obc = whereClause.count('('); int cbc = whereClause.count(')'); if (cbc > obc) { qDebug() << "invalid where clause " << whereClause; qFatal("aborting"); } while (cbc < obc) { whereClause.append(')'); cbc++; } // if the split filter is active, but the where clause and the date filter is empty // it means we already have all the transactions for the specified filter // in memory, so just exit if ((splitFilterActive) && (whereClause.isEmpty()) && (!txFilterActive)) { qDebug("all transactions already in storage"); return fetchTransactions(); } // if we have neither a split filter, nor a tx (date) filter // it's effectively a read all if ((!splitFilterActive) && (!txFilterActive)) { //qDebug("reading all transactions"); return fetchTransactions(); } // build a date clause for the transaction table QString dateClause; QString connector = ""; if (end != QDate()) { dateClause = QString("(postDate < '%1')").arg(end.addDays(1).toString(Qt::ISODate)); connector = " and "; } if (start != QDate()) { dateClause += QString("%1 (postDate >= '%2')").arg(connector).arg(start.toString(Qt::ISODate)); } // now get a list of transaction ids // if we have only a date filter, we need to build the list from the tx table // otherwise we need to build from the split table if (splitFilterActive) { inQuery = QString("(select distinct transactionId from kmmSplits %1)").arg(whereClause); } else { inQuery = QString("(select distinct id from kmmTransactions where %1)").arg(dateClause); txFilterActive = false; // kill off the date filter now } return fetchTransactions(inQuery, dateClause); //FIXME: if we have an accounts-only filter, recalc balances on loaded accounts } ulong MyMoneyStorageSql::transactionCount(const QString& aid) const { Q_D(const MyMoneyStorageSql); if (aid.isEmpty()) return d->m_transactions; else return d->m_transactionCountMap[aid]; } QHash MyMoneyStorageSql::transactionCountMap() const { Q_D(const MyMoneyStorageSql); return d->m_transactionCountMap; } bool MyMoneyStorageSql::isReferencedByTransaction(const QString& id) const { Q_D(const MyMoneyStorageSql); //FIXME-ALEX should I add sub query for kmmTagSplits here? QSqlQuery q(*const_cast (this)); q.prepare("SELECT COUNT(*) FROM kmmTransactions " "INNER JOIN kmmSplits ON kmmTransactions.id = kmmSplits.transactionId " "WHERE kmmTransactions.currencyId = :ID OR kmmSplits.payeeId = :ID " "OR kmmSplits.accountId = :ID OR kmmSplits.costCenterId = :ID"); q.bindValue(":ID", id); if ((!q.exec()) || (!q.next())) { // krazy:exclude=crashy d->buildError(q, Q_FUNC_INFO, "error retrieving reference count"); qFatal("Error retrieving reference count"); // definitely shouldn't happen } return (0 != q.value(0).toULongLong()); } QMap MyMoneyStorageSql::fetchSchedules(const QStringList& idList, bool forUpdate) const { Q_D(const MyMoneyStorageSql); int schedulesNb = (idList.isEmpty() ? d->m_schedules : idList.size()); d->signalProgress(0, schedulesNb, QObject::tr("Loading schedules...")); int progress = 0; const MyMoneyDbTable& t = d->m_db.m_tables["kmmSchedules"]; QSqlQuery query(*const_cast (this)); QMap sList; //ulong lastId = 0; const MyMoneyDbTable& ts = d->m_db.m_tables["kmmSplits"]; QSqlQuery qs(*const_cast (this)); qs.prepare(ts.selectAllString(false) + " WHERE transactionId = :id ORDER BY splitId;"); QSqlQuery sq(*const_cast (this)); sq.prepare("SELECT payDate from kmmSchedulePaymentHistory where schedId = :id"); QString queryString(t.selectAllString(false)); // Use bind variables, instead of just inserting the values in the queryString, // so that values containing a ':' will work. if (! idList.empty()) { queryString += " WHERE"; for (int i = 0; i < idList.count(); ++i) queryString += QString(" id = :id%1 OR").arg(i); queryString = queryString.left(queryString.length() - 2); } queryString += " ORDER BY id"; if (forUpdate) queryString += d->m_driver->forUpdateString(); query.prepare(queryString); if (! idList.empty()) { QStringList::ConstIterator bindVal = idList.constBegin(); for (int i = 0; bindVal != idList.constEnd(); ++i, ++bindVal) { query.bindValue(QString(":id%1").arg(i), *bindVal); } } if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("reading Schedules")); // krazy:exclude=crashy int idCol = t.fieldNumber("id"); int nameCol = t.fieldNumber("name"); int typeCol = t.fieldNumber("type"); int occurrenceCol = t.fieldNumber("occurence"); // krazy:exclude=spelling int occurrenceMultiplierCol = t.fieldNumber("occurenceMultiplier"); // krazy:exclude=spelling int paymentTypeCol = t.fieldNumber("paymentType"); int startDateCol = t.fieldNumber("startDate"); int endDateCol = t.fieldNumber("endDate"); int fixedCol = t.fieldNumber("fixed"); int lastDayInMonthCol = t.fieldNumber("lastDayInMonth"); int autoEnterCol = t.fieldNumber("autoEnter"); int lastPaymentCol = t.fieldNumber("lastPayment"); int weekendOptionCol = t.fieldNumber("weekendOption"); int nextPaymentDueCol = t.fieldNumber("nextPaymentDue"); while (query.next()) { MyMoneySchedule s; QString boolChar; QString sId = GETSTRING(idCol); s.setName(GETSTRING(nameCol)); s.setType(static_cast(GETINT(typeCol))); s.setOccurrencePeriod(static_cast(GETINT(occurrenceCol))); s.setOccurrenceMultiplier(GETINT(occurrenceMultiplierCol)); s.setPaymentType(static_cast(GETINT(paymentTypeCol))); s.setStartDate(GETDATE_D(startDateCol)); s.setEndDate(GETDATE_D(endDateCol)); boolChar = GETSTRING(fixedCol); s.setFixed(boolChar == "Y"); boolChar = GETSTRING(lastDayInMonthCol); s.setLastDayInMonth(boolChar == "Y"); boolChar = GETSTRING(autoEnterCol); s.setAutoEnter(boolChar == "Y"); s.setLastPayment(GETDATE_D(lastPaymentCol)); s.setWeekendOption(static_cast(GETINT(weekendOptionCol))); QDate nextPaymentDue = GETDATE_D(nextPaymentDueCol); // convert simple occurrence to compound occurrence int mult = s.occurrenceMultiplier(); Schedule::Occurrence occ = s.occurrencePeriod(); MyMoneySchedule::simpleToCompoundOccurrence(mult, occ); s.setOccurrencePeriod(occ); s.setOccurrenceMultiplier(mult); // now assign the id to the schedule MyMoneySchedule _s(sId, s); s = _s; // read the associated transaction // m_payeeList.clear(); const MyMoneyDbTable& transactionTable = d->m_db.m_tables["kmmTransactions"]; QSqlQuery q(*const_cast (this)); q.prepare(transactionTable.selectAllString(false) + " WHERE id = :id;"); q.bindValue(":id", s.id()); if (!q.exec()) throw MYMONEYEXCEPTION(d->buildError(q, Q_FUNC_INFO, QString("reading Scheduled Transaction"))); // krazy:exclude=crashy QSqlRecord rec = q.record(); if (!q.next()) throw MYMONEYEXCEPTION(d->buildError(q, Q_FUNC_INFO, QString("retrieving scheduled transaction"))); MyMoneyTransaction tx(s.id(), MyMoneyTransaction()); // we cannot use the GET.... macros here as they are bound to the query variable tx.setPostDate(d->getDate(q.value(transactionTable.fieldNumber("postDate")).toString())); tx.setMemo(q.value(transactionTable.fieldNumber("memo")).toString()); tx.setEntryDate(d->getDate(q.value(transactionTable.fieldNumber("entryDate")).toString())); tx.setCommodity(q.value(transactionTable.fieldNumber("currencyId")).toString()); tx.setBankID(q.value(transactionTable.fieldNumber("bankId")).toString()); qs.bindValue(":id", s.id()); if (!qs.exec()) throw MYMONEYEXCEPTION(d->buildError(qs, Q_FUNC_INFO, "reading Scheduled Splits")); // krazy:exclude=crashy while (qs.next()) { MyMoneySplit sp(d->readSplit(qs)); tx.addSplit(sp); } // if (!m_payeeList.isEmpty()) // readPayees(m_payeeList); // Process any key value pair tx.setPairs(d->readKeyValuePairs("TRANSACTION", s.id()).pairs()); // If the transaction doesn't have a post date, setTransaction will reject it. // The old way of handling things was to store the next post date in the schedule object // and set the transaction post date to QDate(). // For compatibility, if this is the case, copy the next post date from the schedule object // to the transaction object post date. if (!tx.postDate().isValid()) { tx.setPostDate(nextPaymentDue); } s.setTransaction(tx); // read in the recorded payments sq.bindValue(":id", s.id()); if (!sq.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("reading schedule payment history")); // krazy:exclude=crashy while (sq.next()) s.recordPayment(sq.value(0).toDate()); sList[s.id()] = s; //FIXME: enable when schedules have KVPs. // s.setPairs(readKeyValuePairs("SCHEDULE", s.id()).pairs()); //ulong id = MyMoneyUtils::extractId(s.id().data()); //if(id > lastId) // lastId = id; d->signalProgress(++progress, 0); } return sList; } QMap MyMoneyStorageSql::fetchSchedules() const { return fetchSchedules(QStringList(), false); } QMap MyMoneyStorageSql::fetchSecurities(const QStringList& /*idList*/, bool /*forUpdate*/) const { Q_D(const MyMoneyStorageSql); d->signalProgress(0, d->m_securities, QObject::tr("Loading securities...")); int progress = 0; QMap sList; ulong lastId = 0; const MyMoneyDbTable& t = d->m_db.m_tables["kmmSecurities"]; QSqlQuery query(*const_cast (this)); query.prepare(t.selectAllString(false) + " ORDER BY id;"); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("reading Securities")); // krazy:exclude=crashy int idCol = t.fieldNumber("id"); int nameCol = t.fieldNumber("name"); int symbolCol = t.fieldNumber("symbol"); int typeCol = t.fieldNumber("type"); int roundingMethodCol = t.fieldNumber("roundingMethod"); int smallestAccountFractionCol = t.fieldNumber("smallestAccountFraction"); int pricePrecisionCol = t.fieldNumber("pricePrecision"); int tradingCurrencyCol = t.fieldNumber("tradingCurrency"); int tradingMarketCol = t.fieldNumber("tradingMarket"); while (query.next()) { MyMoneySecurity e; QString eid; eid = GETSTRING(idCol); e.setName(GETSTRING(nameCol)); e.setTradingSymbol(GETSTRING(symbolCol)); e.setSecurityType(static_cast(GETINT(typeCol))); e.setRoundingMethod(static_cast(GETINT(roundingMethodCol))); int saf = GETINT(smallestAccountFractionCol); int pp = GETINT(pricePrecisionCol); e.setTradingCurrency(GETSTRING(tradingCurrencyCol)); e.setTradingMarket(GETSTRING(tradingMarketCol)); if (e.tradingCurrency().isEmpty()) e.setTradingCurrency(d->m_storage->pairs()["kmm-baseCurrency"]); if (saf == 0) saf = 100; if (pp == 0 || pp > 10) pp = 4; e.setSmallestAccountFraction(saf); e.setPricePrecision(pp); // Process any key value pairs e.setPairs(d->readKeyValuePairs("SECURITY", eid).pairs()); //tell the storage objects we have a new security object. // FIXME: Adapt to new interface make sure, to take care of the currencies as well // see MyMoneyStorageXML::readSecurites() MyMoneySecurity security(eid, e); sList[security.id()] = security; ulong id = MyMoneyUtils::extractId(security.id()); if (id > lastId) lastId = id; d->signalProgress(++progress, 0); } return sList; } QMap MyMoneyStorageSql::fetchSecurities() const { return fetchSecurities(QStringList(), false); } MyMoneyPrice MyMoneyStorageSql::fetchSinglePrice(const QString& fromId, const QString& toId, const QDate& date_, bool exactDate, bool /*forUpdate*/) const { Q_D(const MyMoneyStorageSql); const MyMoneyDbTable& t = d->m_db.m_tables["kmmPrices"]; static const int priceDateCol = t.fieldNumber("priceDate"); static const int priceCol = t.fieldNumber("price"); static const int priceSourceCol = t.fieldNumber("priceSource"); QSqlQuery query(*const_cast (this)); // Use bind variables, instead of just inserting the values in the queryString, // so that values containing a ':' will work. // See balance query for why the date logic seems odd. QString queryString = t.selectAllString(false) + " WHERE fromId = :fromId AND toId = :toId AND priceDate < :priceDate "; if (exactDate) queryString += "AND priceDate > :exactDate "; queryString += "ORDER BY priceDate DESC;"; query.prepare(queryString); QDate date(date_); if (!date.isValid()) date = QDate::currentDate(); query.bindValue(":fromId", fromId); query.bindValue(":toId", toId); query.bindValue(":priceDate", date.addDays(1).toString(Qt::ISODate)); if (exactDate) query.bindValue(":exactDate", date.toString(Qt::ISODate)); if (! query.exec()) return MyMoneyPrice(); // krazy:exclude=crashy if (query.next()) { return MyMoneyPrice(fromId, toId, GETDATE_D(priceDateCol), MyMoneyMoney(GETSTRING(priceCol)), GETSTRING(priceSourceCol)); } return MyMoneyPrice(); } MyMoneyPriceList MyMoneyStorageSql::fetchPrices(const QStringList& fromIdList, const QStringList& toIdList, bool forUpdate) const { Q_D(const MyMoneyStorageSql); int pricesNb = (fromIdList.isEmpty() ? d->m_prices : fromIdList.size()); d->signalProgress(0, pricesNb, QObject::tr("Loading prices...")); int progress = 0; const_cast (this)->d_func()->m_readingPrices = true; MyMoneyPriceList pList; const MyMoneyDbTable& t = d->m_db.m_tables["kmmPrices"]; QSqlQuery query(*const_cast (this)); QString queryString = t.selectAllString(false); // Use bind variables, instead of just inserting the values in the queryString, // so that values containing a ':' will work. if (! fromIdList.empty()) { queryString += " WHERE ("; for (int i = 0; i < fromIdList.count(); ++i) { queryString += QString(" fromId = :fromId%1 OR").arg(i); } queryString = queryString.left(queryString.length() - 2) + ')'; } if (! toIdList.empty()) { queryString += " AND ("; for (int i = 0; i < toIdList.count(); ++i) { queryString += QString(" toId = :toId%1 OR").arg(i); } queryString = queryString.left(queryString.length() - 2) + ')'; } if (forUpdate) queryString += d->m_driver->forUpdateString(); queryString += ';'; query.prepare(queryString); if (! fromIdList.empty()) { QStringList::ConstIterator bindVal = fromIdList.constBegin(); for (int i = 0; bindVal != fromIdList.constEnd(); ++i, ++bindVal) { query.bindValue(QString(":fromId%1").arg(i), *bindVal); } } if (! toIdList.empty()) { QStringList::ConstIterator bindVal = toIdList.constBegin(); for (int i = 0; bindVal != toIdList.constEnd(); ++i, ++bindVal) { query.bindValue(QString(":toId%1").arg(i), *bindVal); } } if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("reading Prices")); // krazy:exclude=crashy static const int fromIdCol = t.fieldNumber("fromId"); static const int toIdCol = t.fieldNumber("toId"); static const int priceDateCol = t.fieldNumber("priceDate"); static const int priceCol = t.fieldNumber("price"); static const int priceSourceCol = t.fieldNumber("priceSource"); while (query.next()) { QString from = GETSTRING(fromIdCol); QString to = GETSTRING(toIdCol); QDate date = GETDATE_D(priceDateCol); pList [MyMoneySecurityPair(from, to)].insert(date, MyMoneyPrice(from, to, date, MyMoneyMoney(GETSTRING(priceCol)), GETSTRING(priceSourceCol))); d->signalProgress(++progress, 0); } const_cast (this)->d_func()->m_readingPrices = false; return pList; } MyMoneyPriceList MyMoneyStorageSql::fetchPrices() const { return fetchPrices(QStringList(), QStringList(), false); } QMap MyMoneyStorageSql::fetchCurrencies(const QStringList& idList, bool forUpdate) const { Q_D(const MyMoneyStorageSql); int currenciesNb = (idList.isEmpty() ? d->m_currencies : idList.size()); d->signalProgress(0, currenciesNb, QObject::tr("Loading currencies...")); int progress = 0; QMap cList; const MyMoneyDbTable& t = d->m_db.m_tables["kmmCurrencies"]; QSqlQuery query(*const_cast (this)); QString queryString(t.selectAllString(false)); // Use bind variables, instead of just inserting the values in the queryString, // so that values containing a ':' will work. if (! idList.empty()) { queryString += " WHERE"; for (int i = 0; i < idList.count(); ++i) queryString += QString(" isocode = :id%1 OR").arg(i); queryString = queryString.left(queryString.length() - 2); } queryString += " ORDER BY ISOcode"; if (forUpdate) queryString += d->m_driver->forUpdateString(); queryString += ';'; query.prepare(queryString); if (! idList.empty()) { QStringList::ConstIterator bindVal = idList.constBegin(); for (int i = 0; bindVal != idList.end(); ++i, ++bindVal) { query.bindValue(QString(":id%1").arg(i), *bindVal); } } if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("reading Currencies")); // krazy:exclude=crashy int ISOcodeCol = t.fieldNumber("ISOcode"); int nameCol = t.fieldNumber("name"); int typeCol = t.fieldNumber("type"); int symbol1Col = t.fieldNumber("symbol1"); int symbol2Col = t.fieldNumber("symbol2"); int symbol3Col = t.fieldNumber("symbol3"); int smallestCashFractionCol = t.fieldNumber("smallestCashFraction"); int smallestAccountFractionCol = t.fieldNumber("smallestAccountFraction"); int pricePrecisionCol = t.fieldNumber("pricePrecision"); while (query.next()) { QString id; MyMoneySecurity c; QChar symbol[3]; id = GETSTRING(ISOcodeCol); c.setName(GETSTRING(nameCol)); c.setSecurityType(static_cast(GETINT(typeCol))); symbol[0] = QChar(GETINT(symbol1Col)); symbol[1] = QChar(GETINT(symbol2Col)); symbol[2] = QChar(GETINT(symbol3Col)); c.setSmallestCashFraction(GETINT(smallestCashFractionCol)); c.setSmallestAccountFraction(GETINT(smallestAccountFractionCol)); c.setPricePrecision(GETINT(pricePrecisionCol)); c.setTradingSymbol(QString(symbol, 3).trimmed()); cList[id] = MyMoneySecurity(id, c); d->signalProgress(++progress, 0); } return cList; } QMap MyMoneyStorageSql::fetchCurrencies() const { return fetchCurrencies(QStringList(), false); } QMap MyMoneyStorageSql::fetchReports(const QStringList& /*idList*/, bool /*forUpdate*/) const { Q_D(const MyMoneyStorageSql); d->signalProgress(0, d->m_reports, QObject::tr("Loading reports...")); int progress = 0; const MyMoneyDbTable& t = d->m_db.m_tables["kmmReportConfig"]; QSqlQuery query(*const_cast (this)); query.prepare(t.selectAllString(true)); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("reading reports")); // krazy:exclude=crashy int xmlCol = t.fieldNumber("XML"); QMap rList; while (query.next()) { QDomDocument dom; dom.setContent(GETSTRING(xmlCol), false); QDomNode child = dom.firstChild(); child = child.firstChild(); auto report = MyMoneyXmlContentHandler2::readReport(child.toElement()); rList[report.id()] = report; d->signalProgress(++progress, 0); } return rList; } QMap MyMoneyStorageSql::fetchReports() const { return fetchReports(QStringList(), false); } QMap MyMoneyStorageSql::fetchBudgets(const QStringList& idList, bool forUpdate) const { Q_D(const MyMoneyStorageSql); int budgetsNb = (idList.isEmpty() ? d->m_budgets : idList.size()); d->signalProgress(0, budgetsNb, QObject::tr("Loading budgets...")); int progress = 0; const MyMoneyDbTable& t = d->m_db.m_tables["kmmBudgetConfig"]; QSqlQuery query(*const_cast (this)); QString queryString(t.selectAllString(false)); if (! idList.empty()) { queryString += " WHERE id = '" + idList.join("' OR id = '") + '\''; } if (forUpdate) queryString += d->m_driver->forUpdateString(); queryString += ';'; query.prepare(queryString); if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("reading budgets")); // krazy:exclude=crashy QMap budgets; int xmlCol = t.fieldNumber("XML"); while (query.next()) { QDomDocument dom; dom.setContent(GETSTRING(xmlCol), false); QDomNode child = dom.firstChild(); child = child.firstChild(); auto budget = MyMoneyXmlContentHandler2::readBudget(child.toElement()); budgets.insert(budget.id(), budget); d->signalProgress(++progress, 0); } return budgets; } QMap MyMoneyStorageSql::fetchBudgets() const { return fetchBudgets(QStringList(), false); } ulong MyMoneyStorageSql::getNextBudgetId() const { Q_D(const MyMoneyStorageSql); return d->getNextId<&MyMoneyStorageSqlPrivate::m_hiIdBudgets>(QLatin1String("kmmBudgetConfig"), QLatin1String("id"), 1); } ulong MyMoneyStorageSql::getNextAccountId() const { Q_D(const MyMoneyStorageSql); return d->getNextId<&MyMoneyStorageSqlPrivate::m_hiIdAccounts>(QLatin1String("kmmAccounts"), QLatin1String("id"), 1); } ulong MyMoneyStorageSql::getNextInstitutionId() const { Q_D(const MyMoneyStorageSql); return d->getNextId<&MyMoneyStorageSqlPrivate::m_hiIdInstitutions>(QLatin1String("kmmInstitutions"), QLatin1String("id"), 1); } ulong MyMoneyStorageSql::getNextPayeeId() const { Q_D(const MyMoneyStorageSql); return d->getNextId<&MyMoneyStorageSqlPrivate::m_hiIdPayees>(QLatin1String("kmmPayees"), QLatin1String("id"), 1); } ulong MyMoneyStorageSql::getNextTagId() const { Q_D(const MyMoneyStorageSql); return d->getNextId<&MyMoneyStorageSqlPrivate::m_hiIdTags>(QLatin1String("kmmTags"), QLatin1String("id"), 1); } ulong MyMoneyStorageSql::getNextReportId() const { Q_D(const MyMoneyStorageSql); return d->getNextId<&MyMoneyStorageSqlPrivate::m_hiIdReports>(QLatin1String("kmmReportConfig"), QLatin1String("id"), 1); } ulong MyMoneyStorageSql::getNextScheduleId() const { Q_D(const MyMoneyStorageSql); return d->getNextId<&MyMoneyStorageSqlPrivate::m_hiIdSchedules>(QLatin1String("kmmSchedules"), QLatin1String("id"), 3); } ulong MyMoneyStorageSql::getNextSecurityId() const { Q_D(const MyMoneyStorageSql); return d->getNextId<&MyMoneyStorageSqlPrivate::m_hiIdSecurities>(QLatin1String("kmmSecurities"), QLatin1String("id"), 1); } ulong MyMoneyStorageSql::getNextTransactionId() const { Q_D(const MyMoneyStorageSql); return d->getNextId<&MyMoneyStorageSqlPrivate::m_hiIdTransactions>(QLatin1String("kmmTransactions"), QLatin1String("id"), 1); } ulong MyMoneyStorageSql::getNextOnlineJobId() const { Q_D(const MyMoneyStorageSql); return d->getNextId<&MyMoneyStorageSqlPrivate::m_hiIdOnlineJobs>(QLatin1String("kmmOnlineJobs"), QLatin1String("id"), 1); } ulong MyMoneyStorageSql::getNextPayeeIdentifierId() const { Q_D(const MyMoneyStorageSql); return d->getNextId<&MyMoneyStorageSqlPrivate::m_hiIdPayeeIdentifier>(QLatin1String("kmmPayeeIdentifier"), QLatin1String("id"), 5); } ulong MyMoneyStorageSql::getNextCostCenterId() const { Q_D(const MyMoneyStorageSql); return d->getNextId<&MyMoneyStorageSqlPrivate::m_hiIdCostCenter>(QLatin1String("kmmCostCenterIdentifier"), QLatin1String("id"), 5); } ulong MyMoneyStorageSql::incrementBudgetId() { Q_D(MyMoneyStorageSql); d->m_hiIdBudgets = getNextBudgetId() + 1; return (d->m_hiIdBudgets - 1); } /** * @warning This method uses getNextAccountId() internally. The database is not informed which can cause issues * when the database is accessed concurrently. Then maybe a single id is used twice but the RDBMS will detect the * issue and KMyMoney crashes. This issue can only occur when two instances of KMyMoney access the same database. * But in this unlikly case MyMoneyStorageSql will have a lot more issues, I think. */ ulong MyMoneyStorageSql::incrementAccountId() { Q_D(MyMoneyStorageSql); d->m_hiIdAccounts = getNextAccountId() + 1; return (d->m_hiIdAccounts - 1); } ulong MyMoneyStorageSql::incrementInstitutionId() { Q_D(MyMoneyStorageSql); d->m_hiIdInstitutions = getNextInstitutionId() + 1; return (d->m_hiIdInstitutions - 1); } ulong MyMoneyStorageSql::incrementPayeeId() { Q_D(MyMoneyStorageSql); d->m_hiIdPayees = getNextPayeeId() + 1; return (d->m_hiIdPayees - 1); } ulong MyMoneyStorageSql::incrementTagId() { Q_D(MyMoneyStorageSql); d->m_hiIdTags = getNextTagId() + 1; return (d->m_hiIdTags - 1); } ulong MyMoneyStorageSql::incrementReportId() { Q_D(MyMoneyStorageSql); d->m_hiIdReports = getNextReportId() + 1; return (d->m_hiIdReports - 1); } ulong MyMoneyStorageSql::incrementScheduleId() { Q_D(MyMoneyStorageSql); d->m_hiIdSchedules = getNextScheduleId() + 1; return (d->m_hiIdSchedules - 1); } ulong MyMoneyStorageSql::incrementSecurityId() { Q_D(MyMoneyStorageSql); d->m_hiIdSecurities = getNextSecurityId() + 1; return (d->m_hiIdSecurities - 1); } ulong MyMoneyStorageSql::incrementTransactionId() { Q_D(MyMoneyStorageSql); d->m_hiIdTransactions = getNextTransactionId() + 1; return (d->m_hiIdTransactions - 1); } ulong MyMoneyStorageSql::incrementOnlineJobId() { Q_D(MyMoneyStorageSql); d->m_hiIdOnlineJobs = getNextOnlineJobId() + 1; return (d->m_hiIdOnlineJobs - 1); } ulong MyMoneyStorageSql::incrementPayeeIdentfierId() { Q_D(MyMoneyStorageSql); d->m_hiIdPayeeIdentifier = getNextPayeeIdentifierId() + 1; return (d->m_hiIdPayeeIdentifier - 1); } ulong MyMoneyStorageSql::incrementCostCenterId() { Q_D(MyMoneyStorageSql); d->m_hiIdCostCenter = getNextCostCenterId() + 1; return (d->m_hiIdCostCenter - 1); } void MyMoneyStorageSql::loadAccountId(ulong id) { Q_D(MyMoneyStorageSql); d->m_hiIdAccounts = id; d->writeFileInfo(); } void MyMoneyStorageSql::loadTransactionId(ulong id) { Q_D(MyMoneyStorageSql); d->m_hiIdTransactions = id; d->writeFileInfo(); } void MyMoneyStorageSql::loadPayeeId(ulong id) { Q_D(MyMoneyStorageSql); d->m_hiIdPayees = id; d->writeFileInfo(); } void MyMoneyStorageSql::loadTagId(ulong id) { Q_D(MyMoneyStorageSql); d->m_hiIdTags = id; d->writeFileInfo(); } void MyMoneyStorageSql::loadInstitutionId(ulong id) { Q_D(MyMoneyStorageSql); d->m_hiIdInstitutions = id; d->writeFileInfo(); } void MyMoneyStorageSql::loadScheduleId(ulong id) { Q_D(MyMoneyStorageSql); d->m_hiIdSchedules = id; d->writeFileInfo(); } void MyMoneyStorageSql::loadSecurityId(ulong id) { Q_D(MyMoneyStorageSql); d->m_hiIdSecurities = id; d->writeFileInfo(); } void MyMoneyStorageSql::loadReportId(ulong id) { Q_D(MyMoneyStorageSql); d->m_hiIdReports = id; d->writeFileInfo(); } void MyMoneyStorageSql::loadBudgetId(ulong id) { Q_D(MyMoneyStorageSql); d->m_hiIdBudgets = id; d->writeFileInfo(); } void MyMoneyStorageSql::loadOnlineJobId(ulong id) { Q_D(MyMoneyStorageSql); d->m_hiIdOnlineJobs = id; d->writeFileInfo(); } void MyMoneyStorageSql::loadPayeeIdentifierId(ulong id) { Q_D(MyMoneyStorageSql); d->m_hiIdPayeeIdentifier = id; d->writeFileInfo(); } //**************************************************** void MyMoneyStorageSql::setProgressCallback(void(*callback)(int, int, const QString&)) { Q_D(MyMoneyStorageSql); d->m_progressCallback = callback; } void MyMoneyStorageSql::readFile(QIODevice* s, MyMoneyStorageMgr* storage) { Q_UNUSED(s); Q_UNUSED(storage) } void MyMoneyStorageSql::writeFile(QIODevice* s, MyMoneyStorageMgr* storage) { Q_UNUSED(s); Q_UNUSED(storage) } // **************************** Error display routine ******************************* QDate MyMoneyStorageSqlPrivate::m_startDate = QDate(1900, 1, 1); void MyMoneyStorageSql::setStartDate(const QDate& startDate) { MyMoneyStorageSqlPrivate::m_startDate = startDate; } QMap< QString, MyMoneyCostCenter > MyMoneyStorageSql::fetchCostCenters(const QStringList& idList, bool forUpdate) const { Q_D(const MyMoneyStorageSql); Q_UNUSED(forUpdate); MyMoneyDbTransaction trans(const_cast (*this), Q_FUNC_INFO); if (d->m_displayStatus) { int costCenterNb = (idList.isEmpty() ? 100 : idList.size()); d->signalProgress(0, costCenterNb, QObject::tr("Loading cost center...")); } int progress = 0; QMap costCenterList; //ulong lastId; const MyMoneyDbTable& t = d->m_db.m_tables["kmmCostCenter"]; QSqlQuery query(*const_cast (this)); if (idList.isEmpty()) { query.prepare(t.selectAllString()); } else { QString whereClause = " where ("; QString itemConnector = ""; foreach (const QString& it, idList) { whereClause.append(QString("%1id = '%2'").arg(itemConnector).arg(it)); itemConnector = " or "; } whereClause += ')'; query.prepare(t.selectAllString(false) + whereClause); } if (!query.exec()) throw MYMONEYEXCEPTIONSQL_D(QString::fromLatin1("reading CostCenter")); // krazy:exclude=crashy const int idCol = t.fieldNumber("id"); const int nameCol = t.fieldNumber("name"); while (query.next()) { MyMoneyCostCenter costCenter; QString pid = GETSTRING(idCol); costCenter.setName(GETSTRING(nameCol)); costCenterList[pid] = MyMoneyCostCenter(pid, costCenter); if (d->m_displayStatus) d->signalProgress(++progress, 0); } return costCenterList; } QMap< QString, MyMoneyCostCenter > MyMoneyStorageSql::fetchCostCenters() const { return fetchCostCenters(QStringList(), false); } diff --git a/kmymoney/plugins/sql/mymoneystoragesql_p.h b/kmymoney/plugins/sql/mymoneystoragesql_p.h index bb64ee132..57f1e8574 100644 --- a/kmymoney/plugins/sql/mymoneystoragesql_p.h +++ b/kmymoney/plugins/sql/mymoneystoragesql_p.h @@ -1,3325 +1,3316 @@ /*************************************************************************** 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 // ---------------------------------------------------------------------------- // Project Includes #include "mymoneystoragemgr.h" #include "onlinejobadministration.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 "payeeidentifierdata.h" #include "payeeidentifier.h" #include "payeeidentifiertyped.h" #include "payeeidentifier/ibanbic/ibanbic.h" #include "payeeidentifier/nationalaccount/nationalaccount.h" #include "onlinetasks/sepa/sepaonlinetransferimpl.h" #include "xmlstoragehelper.h" #include "mymoneyenums.h" using namespace eMyMoney; 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 (const MyMoneyException &) { 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() #define MYMONEYEXCEPTIONSQL(exceptionMessage) MYMONEYEXCEPTION(buildError(query, Q_FUNC_INFO, exceptionMessage)) #define MYMONEYEXCEPTIONSQL_D(exceptionMessage) MYMONEYEXCEPTION(d->buildError(query, Q_FUNC_INFO, exceptionMessage)) 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() { } enum class SQLAction { Save, Modify, Remove }; /** * 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 MYMONEYEXCEPTIONSQL("retrieving highest ID number"); 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 MYMONEYEXCEPTIONSQL("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 MYMONEYEXCEPTIONSQL("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 MYMONEYEXCEPTIONSQL("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 MYMONEYEXCEPTIONSQL("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 MYMONEYEXCEPTIONSQL("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 MYMONEYEXCEPTIONSQL("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(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Liability), acc_l); MyMoneyAccount acc_a; acc_a.setAccountType(Account::Type::Asset); acc_a.setName("Asset"); MyMoneyAccount asset(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Asset), acc_a); MyMoneyAccount acc_e; acc_e.setAccountType(Account::Type::Expense); acc_e.setName("Expense"); MyMoneyAccount expense(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Expense), acc_e); MyMoneyAccount acc_i; acc_i.setAccountType(Account::Type::Income); acc_i.setName("Income"); MyMoneyAccount income(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Income), acc_i); MyMoneyAccount acc_q; acc_q.setAccountType(Account::Type::Equity); acc_q.setName("Equity"); MyMoneyAccount equity(MyMoneyAccount::stdAccName(eMyMoney::Account::Standard::Equity), 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 MYMONEYEXCEPTIONSQL("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 MYMONEYEXCEPTIONSQL("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..."); 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 MYMONEYEXCEPTIONSQL("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 MYMONEYEXCEPTIONSQL("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 MYMONEYEXCEPTIONSQL("deleting Security"); query2.bindValue(":fromId", idList); query2.bindValue(":toId", idList); if (!query2.execBatch()) throw MYMONEYEXCEPTIONSQL("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 MYMONEYEXCEPTIONSQL("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 MYMONEYEXCEPTIONSQL("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 MYMONEYEXCEPTIONSQL("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 MYMONEYEXCEPTIONSQL("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 MYMONEYEXCEPTIONSQL("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 MYMONEYEXCEPTIONSQL("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 MYMONEYEXCEPTIONSQL("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 MYMONEYEXCEPTIONSQL("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 MYMONEYEXCEPTIONSQL("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 MYMONEYEXCEPTIONSQL("deleting Budget"); } } bool clearTable(const QString& tableName, QSqlQuery& query) { if (query.exec(QString("SELECT count(*) FROM %1").arg(tableName))) { if (query.next()) { if (query.value(0).toUInt() > 0) { if (!query.exec(QString("DELETE FROM %1").arg(tableName))) return false; } } } return true; } void writeOnlineJobs() { Q_Q(MyMoneyStorageSql); QSqlQuery query(*q); if (!clearTable(QStringLiteral("kmmOnlineJobs"), query)) throw MYMONEYEXCEPTIONSQL("Clean kmmOnlineJobs table"); if (!clearTable(QStringLiteral("kmmSepaOrders"), query)) throw MYMONEYEXCEPTIONSQL("Clean kmmSepaOrders table"); if (!clearTable(QStringLiteral("kmmNationalAccountNumber"), query)) throw MYMONEYEXCEPTIONSQL("Clean kmmNationalAccountNumber 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 (const 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() << "Reason:" << e.what(); } signalProgress(++jobCount, 0); } if (!failedJobs.isEmpty()) { /** @todo Improve error message */ throw MYMONEYEXCEPTION_CSTRING("Could not save onlineJob."); } } /** @} */ /** * @name writeMethods * @{ * These methods bind the data fields of MyMoneyObjects to a given query and execute the query. * This is helpful 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 MYMONEYEXCEPTIONSQL("writing Institution"); deleteKeyValuePairs("OFXSETTINGS", idList); 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; auto 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 MYMONEYEXCEPTIONSQL("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 MYMONEYEXCEPTIONSQL("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 MYMONEYEXCEPTIONSQL("writing Account"); //Add in Key-Value Pairs for accounts. deleteKeyValuePairs("ACCOUNT", idList); deleteKeyValuePairs("ONLINEBANKING", idList); 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 MYMONEYEXCEPTIONSQL("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 MYMONEYEXCEPTIONSQL("building Split list"); // krazy:exclude=crashy while (query.next()) dbList.append(query.value(0).toUInt()); QSqlQuery query2(*q); query.prepare(m_db.m_tables["kmmSplits"].updateString()); query2.prepare(m_db.m_tables["kmmSplits"].insertString()); auto i = 0; for (auto it = splitList.constBegin(); it != splitList.constEnd(); ++it) { if (dbList.contains(i)) { dbList.removeAll(i); updateList << *it; updateIdList << i; } else { ++m_splits; insertList << *it; insertIdList << i; } ++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 MYMONEYEXCEPTIONSQL("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 MYMONEYEXCEPTIONSQL("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 MYMONEYEXCEPTIONSQL("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 MYMONEYEXCEPTIONSQL("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 MYMONEYEXCEPTIONSQL("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 MYMONEYEXCEPTIONSQL("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 MYMONEYEXCEPTIONSQL("writing Securities"); // krazy:exclude=crashy //Add in Key-Value Pairs for security QVariantList idList; idList << security.id(); QList > pairs; pairs << security.pairs(); deleteKeyValuePairs("SECURITY", idList); 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 MYMONEYEXCEPTIONSQL("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 MYMONEYEXCEPTIONSQL("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); MyMoneyXmlContentHandler2::writeReport(rep, 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 MYMONEYEXCEPTIONSQL("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); MyMoneyXmlContentHandler2::writeBudget(bud, 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 MYMONEYEXCEPTIONSQL("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 MYMONEYEXCEPTIONSQL("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 eMyMoney::OnlineJob::sendingState::acceptedByBank: query.bindValue(":state", QLatin1String("acceptedByBank")); break; case eMyMoney::OnlineJob::sendingState::rejectedByBank: query.bindValue(":state", QLatin1String("rejectedByBank")); break; case eMyMoney::OnlineJob::sendingState::abortedByUser: query.bindValue(":state", QLatin1String("abortedByUser")); break; case eMyMoney::OnlineJob::sendingState::sendingError: query.bindValue(":state", QLatin1String("sendingError")); break; case eMyMoney::OnlineJob::sendingState::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 MYMONEYEXCEPTIONSQL("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 MYMONEYEXCEPTIONSQL("reading FileInfo"); // krazy:exclude=crashy if (!query.next()) throw MYMONEYEXCEPTIONSQL("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()); } MyMoneySplit readSplit(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"); MyMoneySplit s; 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 MYMONEYEXCEPTIONSQL("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 s; } 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 MYMONEYEXCEPTIONSQL(QString::fromLatin1("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 MYMONEYEXCEPTIONSQL(QString::fromLatin1("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()); } } */ const bool isOnlineBanking = kvpType.toLower().compare(QLatin1String("onlinebanking")) == 0; while (query.next()) { QString kvpId = query.value(0).toString(); QString kvpKey = query.value(1).toString(); QString kvpData = query.value(2).toString(); if (isOnlineBanking) { if ((kvpKey.toLower().compare(QLatin1String("provider")) == 0) && (kvpData.toLower().compare(QLatin1String("kmymoney ofx")) == 0)) { kvpData = QStringLiteral("ofximporter"); } } retval[kvpId].setValue(kvpKey, kvpData); } 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() { Q_Q(MyMoneyStorageSql); try { m_storage->loadPrices(q->fetchPrices()); } 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 readOnlineJobs() { Q_Q(MyMoneyStorageSql); m_storage->loadOnlineJobs(q->fetchOnlineJobs()); } /** @} */ 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 MYMONEYEXCEPTIONSQL("deleting Splits"); query.prepare("DELETE FROM kmmKeyValuePairs WHERE kvpType = 'SPLIT' " "AND kvpId LIKE '?%'"); query.bindValue(1, idList); if (!query.execBatch()) throw MYMONEYEXCEPTIONSQL("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 MYMONEYEXCEPTIONSQL("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 MYMONEYEXCEPTIONSQL("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 MYMONEYEXCEPTIONSQL("deleting Schedule Payment History"); // krazy:exclude=crashy query.prepare(m_db.m_tables["kmmSchedules"].deleteString()); query.bindValue(":id", id); if (!query.exec()) throw MYMONEYEXCEPTIONSQL("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 MYMONEYEXCEPTIONSQL(QString::fromLatin1("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::fromLatin1("opening database %1 in function %2") .arg(maindb.databaseName()).arg(Q_FUNC_INFO)); } else { QSqlQuery qm(maindb); qm.exec(QString::fromLatin1("PRAGMA key = '%1'").arg(q->password())); 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 MYMONEYEXCEPTIONSQL(QString::fromLatin1("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 MYMONEYEXCEPTIONSQL(QString::fromLatin1("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 MYMONEYEXCEPTIONSQL(QString::fromLatin1("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) { // create the fileinfo stuff if it does not exist query.prepare("SELECT count(*) FROM kmmFileInfo;"); if (!query.exec() || !query.next()) throw MYMONEYEXCEPTIONSQL("checking fileinfo"); // krazy:exclude=crashy if (query.value(0).toInt() == 0) { query.prepare(QLatin1String("INSERT INTO kmmFileInfo (version, fixLevel) VALUES(?,?);")); query.bindValue(0, m_dbVersion); query.bindValue(1, m_storage->fileFixVersion()); if (!query.exec()) throw MYMONEYEXCEPTIONSQL(QString::fromLatin1("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 MYMONEYEXCEPTIONSQL(QString::fromLatin1("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, true) + ") SELECT " + t.columnList(fromVersion, false) + " 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 MYMONEYEXCEPTIONSQL("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 MYMONEYEXCEPTIONSQL("getting record count"); // krazy:exclude=crashy if (!query.next()) throw MYMONEYEXCEPTIONSQL("retrieving record count"); recordCount += query.value(0).toInt(); ++tt; } // a fresh created database contains at least one record (see createTables()) in // the kmmFileInfo table providing file and fix version. So we report empty // even if there is a recordCount of 1 if (recordCount > 1) { 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_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 sqlIID; MyMoneyDbTransaction t(*q, Q_FUNC_INFO); auto rc = false; if (iid == payeeIdentifiers::ibanBic::staticPayeeIdentifierIid()) rc = setupIBANBIC(*q); else if (iid == payeeIdentifiers::nationalAccount::staticPayeeIdentifierIid()) rc = setupNationalAccount(*q); else if (iid == sepaOnlineTransferImpl::name()) rc = setupSepaOnlineTransfer(*q); else rc = false; return rc; } bool setupIBANBIC(QSqlDatabase connection) { auto iid = QLatin1String("org.kmymoney.payeeIdentifier.ibanbic.sqlStoragePlugin"); // Get current version QSqlQuery query = QSqlQuery(connection); query.prepare("SELECT versionMajor FROM kmmPluginInfo WHERE iid = ?"); query.bindValue(0, iid); if (!query.exec()) { qWarning("Could not execute query for ibanBicStoragePlugin: %s", qPrintable(query.lastError().text())); return false; } int currentVersion = 0; if (query.next()) currentVersion = query.value(0).toInt(); // Create database in it's most recent version if version is 0 // (version 0 means the database was not installed) if (currentVersion == 0) { // If the database is recreated the table may be still there. So drop it if needed. No error handling needed // as this step is not necessary - only the creation is important. if (!query.exec("DROP TABLE IF EXISTS kmmIbanBic;")) return false; if (!query.exec( "CREATE TABLE kmmIbanBic (" " id varchar(32) NOT NULL PRIMARY KEY REFERENCES kmmPayeeIdentifier( id ) ON DELETE CASCADE ON UPDATE CASCADE," " iban varchar(32)," " bic char(11) CHECK(length(bic) = 11 OR bic IS NULL)," " name text" " );" )) { qWarning("Could not create table for ibanBicStoragePlugin: %s", qPrintable(query.lastError().text())); return false; } query.prepare("INSERT INTO kmmPluginInfo (iid, versionMajor, versionMinor, uninstallQuery) VALUES(?, ?, ?, ?)"); query.bindValue(0, iid); query.bindValue(1, 1); query.bindValue(2, 0); query.bindValue(3, "DROP TABLE kmmIbanBic;"); if (query.exec()) return true; qWarning("Could not save plugin info for ibanBicStoragePlugin (%s): %s", qPrintable(iid), qPrintable(query.lastError().text())); return false; } // Check if version is valid with this plugin switch (currentVersion) { case 1: return true; } return false; } bool setupNationalAccount(QSqlDatabase connection) { auto iid = QLatin1String("org.kmymoney.payeeIdentifier.nationalAccount.sqlStoragePlugin"); // Get current version QSqlQuery query = QSqlQuery(connection); query.prepare("SELECT versionMajor FROM kmmPluginInfo WHERE iid = ?"); query.bindValue(0, iid); if (!query.exec()) { qWarning("Could not execute query for nationalAccountStoragePlugin: %s", qPrintable(query.lastError().text())); return false; } int currentVersion = 0; if (query.next()) currentVersion = query.value(0).toInt(); // Create database in it's most recent version if version is 0 // (version 0 means the database was not installed) if (currentVersion == 0) { // If the database is recreated the table may be still there. So drop it if needed. No error handling needed // as this step is not necessary - only the creation is important. if (!query.exec("DROP TABLE IF EXISTS kmmNationalAccountNumber;")) return false; if (!query.exec( "CREATE TABLE kmmNationalAccountNumber (" " id varchar(32) NOT NULL PRIMARY KEY REFERENCES kmmPayeeIdentifier( id ) ON DELETE CASCADE ON UPDATE CASCADE," " countryCode varchar(3)," " accountNumber TEXT," " bankCode TEXT," " name TEXT" " );" )) { qWarning("Could not create table for nationalAccountStoragePlugin: %s", qPrintable(query.lastError().text())); return false; } query.prepare("INSERT INTO kmmPluginInfo (iid, versionMajor, versionMinor, uninstallQuery) VALUES(?, ?, ?, ?)"); query.bindValue(0, iid); query.bindValue(1, 1); query.bindValue(2, 0); query.bindValue(3, "DROP TABLE kmmNationalAccountNumber;"); if (query.exec()) return true; qWarning("Could not save plugin info for nationalAccountStoragePlugin (%s): %s", qPrintable(iid), qPrintable(query.lastError().text())); return false; } // Check if version is valid with this plugin switch (currentVersion) { case 1: return true; } return false; } bool setupSepaOnlineTransfer(QSqlDatabase connection) { auto iid = QLatin1String("org.kmymoney.creditTransfer.sepa.sqlStoragePlugin"); // Get current version QSqlQuery query = QSqlQuery(connection); query.prepare("SELECT versionMajor FROM kmmPluginInfo WHERE iid = ?"); query.bindValue(0, iid); if (!query.exec()) { qWarning("Could not execute query for sepaStoragePlugin: %s", qPrintable(query.lastError().text())); return false; } int currentVersion = 0; if (query.next()) currentVersion = query.value(0).toInt(); // Create database in it's most recent version if version is 0 // (version 0 means the database was not installed) if (currentVersion <= 1) { // If the database is recreated the table may be still there. So drop it if needed. No error handling needed // as this step is not necessary - only the creation is important. if (!query.exec("DROP TABLE IF EXISTS kmmSepaOrders;")) return false; if (!query.exec( "CREATE TABLE kmmSepaOrders (" " id varchar(32) NOT NULL PRIMARY KEY REFERENCES kmmOnlineJobs( id ) ON UPDATE CASCADE ON DELETE CASCADE," " originAccount varchar(32) REFERENCES kmmAccounts( id ) ON UPDATE CASCADE ON DELETE SET NULL," " value text," " purpose text," " endToEndReference varchar(35)," " beneficiaryName varchar(27)," " beneficiaryIban varchar(32)," " beneficiaryBic char(11)," " textKey int," " subTextKey int" " );" )) { qWarning("Error while creating table kmmSepaOrders: %s", qPrintable(query.lastError().text())); return false; } query.prepare("DELETE FROM kmmPluginInfo WHERE iid = ?;"); query.bindValue(0, iid); query.exec(); query.prepare("INSERT INTO kmmPluginInfo (iid, versionMajor, versionMinor, uninstallQuery) VALUES(?, ?, ?, ?)"); query.bindValue(0, iid); query.bindValue(1, 2); query.bindValue(2, 0); query.bindValue(3, "DROP TABLE kmmSepaOrders;"); if (query.exec()) return true; qWarning("Error while inserting kmmPluginInfo for '%s': %s", qPrintable(iid), qPrintable(query.lastError().text())); return false; } // Check if version is valid with this plugin switch (currentVersion) { case 2: return true; } return false; } bool actOnIBANBICObjectInSQL(SQLAction action, const payeeIdentifier &obj) { payeeIdentifierTyped payeeIdentifier = payeeIdentifierTyped(obj); Q_Q(MyMoneyStorageSql); QSqlQuery query(*q); auto writeQuery = [&]() { query.bindValue(":id", obj.idString()); query.bindValue(":iban", payeeIdentifier->electronicIban()); const auto bic = payeeIdentifier->fullStoredBic(); query.bindValue(":bic", (bic.isEmpty()) ? QVariant(QVariant::String) : bic); query.bindValue(":name", payeeIdentifier->ownerName()); if (!query.exec()) { // krazy:exclude=crashy qWarning("Error while saving ibanbic data for '%s': %s", qPrintable(obj.idString()), qPrintable(query.lastError().text())); return false; } return true; }; switch(action) { case SQLAction::Save: query.prepare("INSERT INTO kmmIbanBic " " ( id, iban, bic, name )" " VALUES( :id, :iban, :bic, :name ) " ); return writeQuery(); case SQLAction::Modify: query.prepare("UPDATE kmmIbanBic SET iban = :iban, bic = :bic, name = :name WHERE id = :id;"); return writeQuery(); case SQLAction::Remove: query.prepare("DELETE FROM kmmIbanBic WHERE id = ?;"); query.bindValue(0, obj.idString()); if (!query.exec()) { qWarning("Error while deleting ibanbic data '%s': %s", qPrintable(obj.idString()), qPrintable(query.lastError().text())); return false; } return true; } return false; } bool actOnNationalAccountObjectInSQL(SQLAction action, const payeeIdentifier &obj) { payeeIdentifierTyped payeeIdentifier = payeeIdentifierTyped(obj); Q_Q(MyMoneyStorageSql); QSqlQuery query(*q); auto writeQuery = [&]() { query.bindValue(":id", obj.idString()); query.bindValue(":countryCode", payeeIdentifier->country()); query.bindValue(":accountNumber", payeeIdentifier->accountNumber()); query.bindValue(":bankCode", (payeeIdentifier->bankCode().isEmpty()) ? QVariant(QVariant::String) : payeeIdentifier->bankCode()); query.bindValue(":name", payeeIdentifier->ownerName()); if (!query.exec()) { // krazy:exclude=crashy qWarning("Error while saving national account number for '%s': %s", qPrintable(obj.idString()), qPrintable(query.lastError().text())); return false; } return true; }; switch(action) { case SQLAction::Save: query.prepare("INSERT INTO kmmNationalAccountNumber " " ( id, countryCode, accountNumber, bankCode, name )" " VALUES( :id, :countryCode, :accountNumber, :bankCode, :name ) " ); return writeQuery(); case SQLAction::Modify: query.prepare("UPDATE kmmNationalAccountNumber SET countryCode = :countryCode, accountNumber = :accountNumber, bankCode = :bankCode, name = :name WHERE id = :id;"); return writeQuery(); case SQLAction::Remove: query.prepare("DELETE FROM kmmNationalAccountNumber WHERE id = ?;"); query.bindValue(0, obj.idString()); if (!query.exec()) { qWarning("Error while deleting national account number '%s': %s", qPrintable(obj.idString()), qPrintable(query.lastError().text())); return false; } return true; } return false; } bool actOnSepaOnlineTransferObjectInSQL(SQLAction action, const onlineTask &obj, const QString& id) { Q_Q(MyMoneyStorageSql); QSqlQuery query(*q); const auto& task = dynamic_cast(obj); auto bindValuesToQuery = [&]() { auto value = task.value().toString(); if (value.isEmpty()) { value = QStringLiteral("0"); } query.bindValue(":id", id); query.bindValue(":originAccount", task.responsibleAccount()); query.bindValue(":value", value); query.bindValue(":purpose", task.purpose()); query.bindValue(":endToEndReference", (task.endToEndReference().isEmpty()) ? QVariant() : QVariant::fromValue(task.endToEndReference())); query.bindValue(":beneficiaryName", task.beneficiaryTyped().ownerName()); query.bindValue(":beneficiaryIban", task.beneficiaryTyped().electronicIban()); query.bindValue(":beneficiaryBic", (task.beneficiaryTyped().storedBic().isEmpty()) ? QVariant() : QVariant::fromValue(task.beneficiaryTyped().storedBic())); query.bindValue(":textKey", task.textKey()); query.bindValue(":subTextKey", task.subTextKey()); }; switch(action) { case SQLAction::Save: query.prepare("INSERT INTO kmmSepaOrders (" " id, originAccount, value, purpose, endToEndReference, beneficiaryName, beneficiaryIban, " " beneficiaryBic, textKey, subTextKey) " " VALUES( :id, :originAccount, :value, :purpose, :endToEndReference, :beneficiaryName, :beneficiaryIban, " " :beneficiaryBic, :textKey, :subTextKey ) " ); bindValuesToQuery(); if (!query.exec()) { qWarning("Error while saving sepa order '%s': %s", qPrintable(id), qPrintable(query.lastError().text())); return false; } return true; case SQLAction::Modify: query.prepare( "UPDATE kmmSepaOrders SET" " originAccount = :originAccount," " value = :value," " purpose = :purpose," " endToEndReference = :endToEndReference," " beneficiaryName = :beneficiaryName," " beneficiaryIban = :beneficiaryIban," " beneficiaryBic = :beneficiaryBic," " textKey = :textKey," " subTextKey = :subTextKey " " WHERE id = :id"); bindValuesToQuery(); if (!query.exec()) { qWarning("Could not modify sepaOnlineTransfer '%s': %s", qPrintable(id), qPrintable(query.lastError().text())); return false; } return true; case SQLAction::Remove: query.prepare("DELETE FROM kmmSepaOrders WHERE id = ?"); query.bindValue(0, id); return query.exec(); } return false; } void actOnPayeeIdentifierObjectInSQL(SQLAction action, const payeeIdentifier& obj) { setupStoragePlugin(obj->payeeIdentifierId()); auto isSuccessfull = false; if (obj->payeeIdentifierId() == payeeIdentifiers::ibanBic::staticPayeeIdentifierIid()) isSuccessfull = actOnIBANBICObjectInSQL(action, obj); else if (obj->payeeIdentifierId() == payeeIdentifiers::nationalAccount::staticPayeeIdentifierIid()) isSuccessfull = actOnNationalAccountObjectInSQL(action, obj); if (!isSuccessfull) { switch (action) { case SQLAction::Save: throw MYMONEYEXCEPTION(QString::fromLatin1("Could not save object with id '%1' in database (plugin failed).").arg(obj.idString())); case SQLAction::Modify: throw MYMONEYEXCEPTION(QString::fromLatin1("Could not modify object with id '%1' in database (plugin failed).").arg(obj.idString())); case SQLAction::Remove: throw MYMONEYEXCEPTION(QString::fromLatin1("Could not remove object with id '%1' from database (plugin failed).").arg(obj.idString())); } } } void actOnOnlineJobInSQL(SQLAction action, const onlineTask& obj, const QString& id) { setupStoragePlugin(obj.taskName()); auto isSuccessfull = false; if (obj.taskName() == sepaOnlineTransferImpl::name()) isSuccessfull = actOnSepaOnlineTransferObjectInSQL(action, obj, id); if (!isSuccessfull) { switch (action) { case SQLAction::Save: throw MYMONEYEXCEPTION(QString::fromLatin1("Could not save object with id '%1' in database (plugin failed).").arg(id)); case SQLAction::Modify: throw MYMONEYEXCEPTION(QString::fromLatin1("Could not modify object with id '%1' in database (plugin failed).").arg(id)); case SQLAction::Remove: throw MYMONEYEXCEPTION(QString::fromLatin1("Could not remove object with id '%1' from database (plugin failed).").arg(id)); } } } payeeIdentifierData* createIBANBICObject(QSqlDatabase db, const QString& identId) const { QSqlQuery query(db); query.prepare("SELECT iban, bic, name FROM kmmIbanBic WHERE id = ?;"); query.bindValue(0, identId); if (!query.exec() || !query.next()) { qWarning("Could load iban bic identifier from database"); return nullptr; } payeeIdentifiers::ibanBic *const ident = new payeeIdentifiers::ibanBic; ident->setIban(query.value(0).toString()); ident->setBic(query.value(1).toString()); ident->setOwnerName(query.value(2).toString()); return ident; } payeeIdentifierData* createNationalAccountObject(QSqlDatabase db, const QString& identId) const { QSqlQuery query(db); query.prepare("SELECT countryCode, accountNumber, bankCode, name FROM kmmNationalAccountNumber WHERE id = ?;"); query.bindValue(0, identId); if (!query.exec() || !query.next()) { qWarning("Could load national account number from database"); return nullptr; } payeeIdentifiers::nationalAccount *const ident = new payeeIdentifiers::nationalAccount; ident->setCountry(query.value(0).toString()); ident->setAccountNumber(query.value(1).toString()); ident->setBankCode(query.value(2).toString()); ident->setOwnerName(query.value(3).toString()); return ident; } payeeIdentifier createPayeeIdentifierObject(QSqlDatabase db, const QString& identifierType, const QString& identifierId) const { payeeIdentifierData* identData = nullptr; if (identifierType == payeeIdentifiers::ibanBic::staticPayeeIdentifierIid()) identData = createIBANBICObject(db, identifierId); else if (identifierType == payeeIdentifiers::nationalAccount::staticPayeeIdentifierIid()) identData = createNationalAccountObject(db, identifierId); return payeeIdentifier(identifierId, identData); } onlineTask* createSepaOnlineTransferObject(QSqlDatabase connection, const QString& onlineJobId) const { Q_ASSERT(!onlineJobId.isEmpty()); Q_ASSERT(connection.isOpen()); QSqlQuery query = QSqlQuery( "SELECT originAccount, value, purpose, endToEndReference, beneficiaryName, beneficiaryIban, " " beneficiaryBic, textKey, subTextKey FROM kmmSepaOrders WHERE id = ?", connection ); query.bindValue(0, onlineJobId); if (query.exec() && query.next()) { sepaOnlineTransferImpl* task = new sepaOnlineTransferImpl(); task->setOriginAccount(query.value(0).toString()); task->setValue(MyMoneyMoney(query.value(1).toString())); task->setPurpose(query.value(2).toString()); task->setEndToEndReference(query.value(3).toString()); task->setTextKey(query.value(7).toUInt()); task->setSubTextKey(query.value(8).toUInt()); payeeIdentifiers::ibanBic beneficiary; beneficiary.setOwnerName(query.value(4).toString()); beneficiary.setIban(query.value(5).toString()); beneficiary.setBic(query.value(6).toString()); task->setBeneficiary(beneficiary); return task; } return nullptr; } onlineTask* createOnlineTaskObject(const QString& iid, const QString& onlineTaskId, QSqlDatabase connection) const { onlineTask* taskOnline = nullptr; if (iid == sepaOnlineTransferImpl::name()) { // @todo This is probably memory leak but for now it works alike to original code onlineJobAdministration::instance()->registerOnlineTask(new sepaOnlineTransferImpl); taskOnline = createSepaOnlineTransferObject(connection, onlineTaskId); } if (!taskOnline) qWarning("In the file is a onlineTask for which I could not find the plugin ('%s')", qPrintable(iid)); return taskOnline; } void alert(QString s) const // FIXME: remove... { qDebug() << s; } void signalProgress(qint64 current, qint64 total, const QString& msg) const { if (m_progressCallback != 0) (*m_progressCallback)(current, total, msg); } void signalProgress(qint64 current, qint64 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/views/forecast/kforecastview_p.h b/kmymoney/plugins/views/forecast/kforecastview_p.h index 4767fae72..ae3172055 100644 --- a/kmymoney/plugins/views/forecast/kforecastview_p.h +++ b/kmymoney/plugins/views/forecast/kforecastview_p.h @@ -1,1027 +1,1027 @@ /*************************************************************************** kforecastview.cpp ------------------- copyright : (C) 2007 by Alvaro Soliverez email : asoliverez@gmail.com (C) 2017 Ł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 KFORECASTVIEW_P_H #define KFORECASTVIEW_P_H #include "kforecastview.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "ui_kforecastview.h" #include "forecastviewsettings.h" #include "kmymoneyviewbase_p.h" #include "mymoneymoney.h" #include "mymoneyforecast.h" #include "mymoneyprice.h" #include "mymoneyutils.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneyexception.h" #include "mymoneysecurity.h" #include "kmymoneysettings.h" #include "mymoneybudget.h" #include "fixedcolumntreeview.h" #include "icons.h" #include "mymoneyenums.h" #include "kmymoneyutils.h" #include "kmymoneyplugin.h" #include "plugins/views/reports/reportsviewenums.h" using namespace Icons; typedef enum { SummaryView = 0, ListView, AdvancedView, BudgetView, ChartView, // insert new values above this line MaxViewTabs } ForecastViewTab; enum ForecastViewRoles { ForecastRole = Qt::UserRole, /**< The forecast is held in this role.*/ AccountRole = Qt::UserRole + 1, /**< The MyMoneyAccount is stored in this role in column 0.*/ AmountRole = Qt::UserRole + 2, /**< The amount.*/ ValueRole = Qt::UserRole + 3, /**< The value.*/ }; enum EForecastViewType { eSummary = 0, eDetailed, eAdvanced, eBudget, eUndefined }; class KForecastViewPrivate : public KMyMoneyViewBasePrivate { Q_DECLARE_PUBLIC(KForecastView) public: explicit KForecastViewPrivate(KForecastView *qq) : KMyMoneyViewBasePrivate(), q_ptr(qq), ui(new Ui::KForecastView), m_needLoad(true), m_totalItem(0), m_assetItem(0), m_liabilityItem(0), m_incomeItem(0), m_expenseItem(0), m_chartLayout(0), m_forecastChart(nullptr) { } ~KForecastViewPrivate() { delete ui; } void init() { Q_Q(KForecastView); m_needLoad = false; ui->setupUi(q); for (int i = 0; i < MaxViewTabs; ++i) m_needReload[i] = false; KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup grp = config->group("Last Use Settings"); ui->m_tab->setCurrentIndex(grp.readEntry("KForecastView_LastType", 0)); ui->m_forecastButton->setIcon(Icons::get(Icon::ViewForecast)); q->connect(ui->m_tab, &QTabWidget::currentChanged, q, &KForecastView::slotTabChanged); q->connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KForecastView::refresh); q->connect(ui->m_forecastButton, &QAbstractButton::clicked, q, &KForecastView::slotManualForecast); ui->m_forecastList->setUniformRowHeights(true); ui->m_forecastList->setAllColumnsShowFocus(true); ui->m_summaryList->setAllColumnsShowFocus(true); ui->m_budgetList->setAllColumnsShowFocus(true); ui->m_advancedList->setAlternatingRowColors(true); q->connect(ui->m_forecastList, &QTreeWidget::itemExpanded, q, &KForecastView::itemExpanded); q->connect(ui->m_forecastList, &QTreeWidget::itemCollapsed, q, &KForecastView::itemCollapsed); q->connect(ui->m_summaryList, &QTreeWidget::itemExpanded, q, &KForecastView::itemExpanded); q->connect(ui->m_summaryList, &QTreeWidget::itemCollapsed, q, &KForecastView::itemCollapsed); q->connect(ui->m_budgetList, &QTreeWidget::itemExpanded, q, &KForecastView::itemExpanded); q->connect(ui->m_budgetList, &QTreeWidget::itemCollapsed, q, &KForecastView::itemCollapsed); m_chartLayout = ui->m_tabChart->layout(); m_chartLayout->setSpacing(6); loadForecastSettings(); } void loadForecast(ForecastViewTab tab) { if (m_needReload[tab]) { switch (tab) { case ListView: loadListView(); break; case SummaryView: loadSummaryView(); break; case AdvancedView: loadAdvancedView(); break; case BudgetView: loadBudgetView(); break; case ChartView: loadChartView(); break; default: break; } m_needReload[tab] = false; } } void loadListView() { MyMoneyForecast forecast = KMyMoneyUtils::forecast(); const auto file = MyMoneyFile::instance(); //get the settings from current page forecast.setForecastDays(ui->m_forecastDays->value()); forecast.setAccountsCycle(ui->m_accountsCycle->value()); forecast.setBeginForecastDay(ui->m_beginDay->value()); forecast.setForecastCycles(ui->m_forecastCycles->value()); forecast.setHistoryMethod(ui->m_historyMethod->checkedId()); forecast.doForecast(); ui->m_forecastList->clear(); ui->m_forecastList->setColumnCount(0); ui->m_forecastList->setIconSize(QSize(22, 22)); ui->m_forecastList->setSortingEnabled(true); ui->m_forecastList->sortByColumn(0, Qt::AscendingOrder); //add columns QStringList headerLabels; headerLabels << i18n("Account"); //add cycle interval columns headerLabels << i18nc("Today's forecast", "Current"); for (int i = 1; i <= forecast.forecastDays(); ++i) { QDate forecastDate = QDate::currentDate().addDays(i); headerLabels << QLocale().toString(forecastDate, QLocale::LongFormat); } //add variation columns headerLabels << i18n("Total variation"); //set the columns ui->m_forecastList->setHeaderLabels(headerLabels); //add default rows addTotalRow(ui->m_forecastList, forecast); addAssetLiabilityRows(forecast); //load asset and liability forecast accounts loadAccounts(forecast, file->asset(), m_assetItem, eDetailed); loadAccounts(forecast, file->liability(), m_liabilityItem, eDetailed); adjustHeadersAndResizeToContents(ui->m_forecastList); // add the fixed column only if the horizontal scroll bar is visible m_fixedColumnView.reset(ui->m_forecastList->horizontalScrollBar()->isVisible() ? new FixedColumnTreeView(ui->m_forecastList) : 0); } void loadAccounts(MyMoneyForecast& forecast, const MyMoneyAccount& account, QTreeWidgetItem* parentItem, int forecastType) { QMap nameIdx; const auto file = MyMoneyFile::instance(); QTreeWidgetItem *forecastItem = 0; //Get all accounts of the right type to calculate forecast const auto accList = account.accountList(); if (accList.isEmpty()) return; foreach (const auto sAccount, accList) { auto subAccount = file->account(sAccount); //only add the account if it is a forecast account or the parent of a forecast account if (includeAccount(forecast, subAccount)) { nameIdx[subAccount.id()] = subAccount.id(); } } QMap::ConstIterator it_nc; for (it_nc = nameIdx.constBegin(); it_nc != nameIdx.constEnd(); ++it_nc) { const MyMoneyAccount subAccount = file->account(*it_nc); MyMoneySecurity currency; if (subAccount.isInvest()) { MyMoneySecurity underSecurity = file->security(subAccount.currencyId()); currency = file->security(underSecurity.tradingCurrency()); } else { currency = file->security(subAccount.currencyId()); } forecastItem = new QTreeWidgetItem(parentItem); forecastItem->setText(0, subAccount.name()); forecastItem->setIcon(0, subAccount.accountPixmap()); forecastItem->setData(0, ForecastRole, QVariant::fromValue(forecast)); forecastItem->setData(0, AccountRole, QVariant::fromValue(subAccount)); forecastItem->setExpanded(true); switch (forecastType) { case eSummary: updateSummary(forecastItem); break; case eDetailed: updateDetailed(forecastItem); break; case EForecastViewType::eBudget: updateBudget(forecastItem); break; default: break; } loadAccounts(forecast, subAccount, forecastItem, forecastType); } } void loadSummaryView() { MyMoneyForecast forecast = KMyMoneyUtils::forecast(); QList accList; const auto file = MyMoneyFile::instance(); //get the settings from current page forecast.setForecastDays(ui->m_forecastDays->value()); forecast.setAccountsCycle(ui->m_accountsCycle->value()); forecast.setBeginForecastDay(ui->m_beginDay->value()); forecast.setForecastCycles(ui->m_forecastCycles->value()); forecast.setHistoryMethod(ui->m_historyMethod->checkedId()); forecast.doForecast(); //add columns QStringList headerLabels; headerLabels << i18n("Account"); headerLabels << i18nc("Today's forecast", "Current"); //if beginning of forecast is today, set the begin day to next cycle to avoid repeating the first cycle qint64 daysToBeginDay; if (QDate::currentDate() < forecast.beginForecastDate()) { daysToBeginDay = QDate::currentDate().daysTo(forecast.beginForecastDate()); } else { daysToBeginDay = forecast.accountsCycle(); } for (auto i = 0; ((i*forecast.accountsCycle()) + daysToBeginDay) <= forecast.forecastDays(); ++i) { auto intervalDays = ((i * forecast.accountsCycle()) + daysToBeginDay); headerLabels << i18np("1 day", "%1 days", intervalDays); } //add variation columns headerLabels << i18n("Total variation"); ui->m_summaryList->clear(); //set the columns ui->m_summaryList->setHeaderLabels(headerLabels); ui->m_summaryList->setIconSize(QSize(22, 22)); ui->m_summaryList->setSortingEnabled(true); ui->m_summaryList->sortByColumn(0, Qt::AscendingOrder); //add default rows addTotalRow(ui->m_summaryList, forecast); addAssetLiabilityRows(forecast); loadAccounts(forecast, file->asset(), m_assetItem, eSummary); loadAccounts(forecast, file->liability(), m_liabilityItem, eSummary); adjustHeadersAndResizeToContents(ui->m_summaryList); //Add comments to the advice list ui->m_adviceText->clear(); //Get all accounts of the right type to calculate forecast m_nameIdx.clear(); accList = forecast.accountList(); QList::const_iterator accList_t = accList.constBegin(); for (; accList_t != accList.constEnd(); ++accList_t) { MyMoneyAccount acc = *accList_t; if (m_nameIdx[acc.id()] != acc.id()) { //Check if the account is there m_nameIdx[acc.id()] = acc.id(); } } QMap::ConstIterator it_nc; for (it_nc = m_nameIdx.constBegin(); it_nc != m_nameIdx.constEnd(); ++it_nc) { const MyMoneyAccount& acc = file->account(*it_nc); MyMoneySecurity currency; //change currency to deep currency if account is an investment if (acc.isInvest()) { MyMoneySecurity underSecurity = file->security(acc.currencyId()); currency = file->security(underSecurity.tradingCurrency()); } else { currency = file->security(acc.currencyId()); } //Check if the account is going to be below zero or below the minimal balance in the forecast period QString minimumBalance = acc.value("minimumBalance"); MyMoneyMoney minBalance = MyMoneyMoney(minimumBalance); //Check if the account is going to be below minimal balance auto dropMinimum = forecast.daysToMinimumBalance(acc); //Check if the account is going to be below zero in the future auto dropZero = forecast.daysToZeroBalance(acc); // 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 = QString("").arg(KMyMoneySettings::schemeColor(SchemeColor::Negative).name()); msg += i18n("The balance of %1 is below the minimum balance %2 today.", acc.name(), MyMoneyUtils::formatMoney(minBalance, acc, currency)); msg += QString(""); break; default: msg = QString("").arg(KMyMoneySettings::schemeColor(SchemeColor::Negative).name()); 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, acc.name(), MyMoneyUtils::formatMoney(minBalance, acc, currency)); msg += QString(""); } if (!msg.isEmpty()) { ui->m_adviceText->append(msg); } } // a drop below zero is always shown msg.clear(); switch (dropZero) { case -1: break; case 0: if (acc.accountGroup() == eMyMoney::Account::Type::Asset) { msg = QString("").arg(KMyMoneySettings::schemeColor(SchemeColor::Negative).name()); msg += i18n("The balance of %1 is below %2 today.", acc.name(), MyMoneyUtils::formatMoney(MyMoneyMoney(), acc, currency)); msg += QString(""); break; } if (acc.accountGroup() == eMyMoney::Account::Type::Liability) { msg = i18n("The balance of %1 is above %2 today.", acc.name(), MyMoneyUtils::formatMoney(MyMoneyMoney(), acc, currency)); break; } break; default: if (acc.accountGroup() == eMyMoney::Account::Type::Asset) { msg = QString("").arg(KMyMoneySettings::schemeColor(SchemeColor::Negative).name()); 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, acc.name(), MyMoneyUtils::formatMoney(MyMoneyMoney(), acc, currency)); msg += QString(""); break; } if (acc.accountGroup() == eMyMoney::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, acc.name(), MyMoneyUtils::formatMoney(MyMoneyMoney(), acc, currency)); break; } } if (!msg.isEmpty()) { ui->m_adviceText->append(msg); } //advice about trends msg.clear(); MyMoneyMoney accCycleVariation = forecast.accountCycleVariation(acc); if (accCycleVariation < MyMoneyMoney()) { msg = QString("").arg(KMyMoneySettings::schemeColor(SchemeColor::Negative).name()); msg += i18n("The account %1 is decreasing %2 per cycle.", acc.name(), MyMoneyUtils::formatMoney(accCycleVariation, acc, currency)); msg += QString(""); } if (!msg.isEmpty()) { ui->m_adviceText->append(msg); } } ui->m_adviceText->show(); } void loadAdvancedView() { const auto file = MyMoneyFile::instance(); QList accList; MyMoneySecurity baseCurrency = file->baseCurrency(); MyMoneyForecast forecast = KMyMoneyUtils::forecast(); qint64 daysToBeginDay; //get the settings from current page forecast.setForecastDays(ui->m_forecastDays->value()); forecast.setAccountsCycle(ui->m_accountsCycle->value()); forecast.setBeginForecastDay(ui->m_beginDay->value()); forecast.setForecastCycles(ui->m_forecastCycles->value()); forecast.setHistoryMethod(ui->m_historyMethod->checkedId()); forecast.doForecast(); //Get all accounts of the right type to calculate forecast m_nameIdx.clear(); accList = forecast.accountList(); QList::const_iterator accList_t = accList.constBegin(); for (; accList_t != accList.constEnd(); ++accList_t) { MyMoneyAccount acc = *accList_t; if (m_nameIdx[acc.id()] != acc.id()) { //Check if the account is there m_nameIdx[acc.id()] = acc.id(); } } //clear the list, including columns ui->m_advancedList->clear(); ui->m_advancedList->setColumnCount(0); ui->m_advancedList->setIconSize(QSize(22, 22)); QStringList headerLabels; //add first column of both lists headerLabels << i18n("Account"); //if beginning of forecast is today, set the begin day to next cycle to avoid repeating the first cycle if (QDate::currentDate() < forecast.beginForecastDate()) { daysToBeginDay = QDate::currentDate().daysTo(forecast.beginForecastDate()); } else { daysToBeginDay = forecast.accountsCycle(); } //add columns for (auto i = 1; ((i * forecast.accountsCycle()) + daysToBeginDay) <= forecast.forecastDays(); ++i) { headerLabels << i18n("Min Bal %1", i); headerLabels << i18n("Min Date %1", i); } for (auto i = 1; ((i * forecast.accountsCycle()) + daysToBeginDay) <= forecast.forecastDays(); ++i) { headerLabels << i18n("Max Bal %1", i); headerLabels << i18n("Max Date %1", i); } headerLabels << i18nc("Average balance", "Average"); ui->m_advancedList->setHeaderLabels(headerLabels); QTreeWidgetItem *advancedItem = 0; QMap::ConstIterator it_nc; for (it_nc = m_nameIdx.constBegin(); it_nc != m_nameIdx.constEnd(); ++it_nc) { const MyMoneyAccount& acc = file->account(*it_nc); QString amount; MyMoneyMoney amountMM; MyMoneySecurity currency; //change currency to deep currency if account is an investment if (acc.isInvest()) { MyMoneySecurity underSecurity = file->security(acc.currencyId()); currency = file->security(underSecurity.tradingCurrency()); } else { currency = file->security(acc.currencyId()); } advancedItem = new QTreeWidgetItem(ui->m_advancedList, advancedItem, false); advancedItem->setText(0, acc.name()); advancedItem->setIcon(0, acc.accountPixmap()); auto it_c = 1; // iterator for the columns of the listview //get minimum balance list QList minBalanceList = forecast.accountMinimumBalanceDateList(acc); QList::Iterator t_min; for (t_min = minBalanceList.begin(); t_min != minBalanceList.end() ; ++t_min) { QDate minDate = *t_min; amountMM = forecast.forecastBalance(acc, minDate); amount = MyMoneyUtils::formatMoney(amountMM, acc, currency); advancedItem->setText(it_c, amount); advancedItem->setTextAlignment(it_c, Qt::AlignRight | Qt::AlignVCenter); if (amountMM.isNegative()) { advancedItem->setForeground(it_c, KMyMoneySettings::schemeColor(SchemeColor::Negative)); } it_c++; QString dateString = QLocale().toString(minDate, QLocale::ShortFormat); advancedItem->setText(it_c, dateString); advancedItem->setTextAlignment(it_c, Qt::AlignRight | Qt::AlignVCenter); if (amountMM.isNegative()) { advancedItem->setForeground(it_c, KMyMoneySettings::schemeColor(SchemeColor::Negative)); } it_c++; } //get maximum balance list QList maxBalanceList = forecast.accountMaximumBalanceDateList(acc); QList::Iterator t_max; for (t_max = maxBalanceList.begin(); t_max != maxBalanceList.end() ; ++t_max) { QDate maxDate = *t_max; amountMM = forecast.forecastBalance(acc, maxDate); amount = MyMoneyUtils::formatMoney(amountMM, acc, currency); advancedItem->setText(it_c, amount); advancedItem->setTextAlignment(it_c, Qt::AlignRight | Qt::AlignVCenter); if (amountMM.isNegative()) { advancedItem->setForeground(it_c, KMyMoneySettings::schemeColor(SchemeColor::Negative)); } it_c++; QString dateString = QLocale().toString(maxDate, QLocale::ShortFormat); advancedItem->setText(it_c, dateString); advancedItem->setTextAlignment(it_c, Qt::AlignRight | Qt::AlignVCenter); if (amountMM.isNegative()) { advancedItem->setForeground(it_c, KMyMoneySettings::schemeColor(SchemeColor::Negative)); } it_c++; } //get average balance amountMM = forecast.accountAverageBalance(acc); amount = MyMoneyUtils::formatMoney(amountMM, acc, currency); advancedItem->setText(it_c, amount); advancedItem->setTextAlignment(it_c, Qt::AlignRight | Qt::AlignVCenter); if (amountMM.isNegative()) { advancedItem->setForeground(it_c, KMyMoneySettings::schemeColor(SchemeColor::Negative)); } it_c++; } // make sure all data is shown adjustHeadersAndResizeToContents(ui->m_advancedList); ui->m_advancedList->show(); } void loadBudgetView() { const auto file = MyMoneyFile::instance(); MyMoneyForecast forecast = KMyMoneyUtils::forecast(); //get the settings from current page and calculate this year based on last year QDate historyEndDate = QDate(QDate::currentDate().year() - 1, 12, 31); QDate historyStartDate = historyEndDate.addDays(-ui->m_accountsCycle->value() * ui->m_forecastCycles->value()); QDate forecastStartDate = QDate(QDate::currentDate().year(), 1, 1); QDate forecastEndDate = QDate::currentDate().addDays(ui->m_forecastDays->value()); forecast.setHistoryMethod(ui->m_historyMethod->checkedId()); MyMoneyBudget budget; forecast.createBudget(budget, historyStartDate, historyEndDate, forecastStartDate, forecastEndDate, false); ui->m_budgetList->clear(); ui->m_budgetList->setIconSize(QSize(22, 22)); ui->m_budgetList->setSortingEnabled(true); ui->m_budgetList->sortByColumn(0, Qt::AscendingOrder); //add columns QStringList headerLabels; headerLabels << i18n("Account"); { forecastStartDate = forecast.forecastStartDate(); forecastEndDate = forecast.forecastEndDate(); //add cycle interval columns QDate f_date = forecastStartDate; for (; f_date <= forecastEndDate; f_date = f_date.addMonths(1)) { headerLabels << QDate::longMonthName(f_date.month()); } } //add total column headerLabels << i18nc("Total balance", "Total"); //set the columns ui->m_budgetList->setHeaderLabels(headerLabels); //add default rows addTotalRow(ui->m_budgetList, forecast); addIncomeExpenseRows(forecast); //load income and expense budget accounts loadAccounts(forecast, file->income(), m_incomeItem, EForecastViewType::eBudget); loadAccounts(forecast, file->expense(), m_expenseItem, EForecastViewType::eBudget); adjustHeadersAndResizeToContents(ui->m_budgetList); } void loadChartView() { if (m_forecastChart) delete m_forecastChart; if (const auto reportsPlugin = pPlugins.data.value("reportsview", nullptr)) { const QString args = QString::number(ui->m_comboDetail->currentIndex()) + ';' + QString::number(ui->m_forecastDays->value()) + ';' + QString::number(ui->m_tab->width()) + ';' + QString::number(ui->m_tab->height()); - const auto variantReport = reportsPlugin->requestData(args, eWidgetPlugin::WidgetType::NetWorthForecastWithArgs); + const auto variantReport = reportsPlugin->requestData(args, eWidgetPlugin::WidgetType::NetWorthForecastWithArgs); if (!variantReport.isNull()) m_forecastChart = variantReport.value(); + else + m_forecastChart = new QLabel(i18n("No data provided by reports plugin for this chart.")); } else { m_forecastChart = new QLabel(i18n("Enable reports plugin to see this chart.")); } m_chartLayout->addWidget(m_forecastChart); } void loadForecastSettings() { //fill the settings controls ui->m_forecastDays->setValue(KMyMoneySettings::forecastDays()); ui->m_accountsCycle->setValue(KMyMoneySettings::forecastAccountCycle()); ui->m_beginDay->setValue(KMyMoneySettings::beginForecastDay()); ui->m_forecastCycles->setValue(KMyMoneySettings::forecastCycles()); ui->m_historyMethod->setId(ui->radioButton11, 0); // simple moving avg ui->m_historyMethod->setId(ui->radioButton12, 1); // weighted moving avg ui->m_historyMethod->setId(ui->radioButton13, 2); // linear regression ui->m_historyMethod->button(KMyMoneySettings::historyMethod())->setChecked(true); switch (KMyMoneySettings::forecastMethod()) { case 0: ui->m_forecastMethod->setText(i18nc("Scheduled method", "Scheduled")); ui->m_forecastCycles->setDisabled(true); ui->m_historyMethodGroupBox->setDisabled(true); break; case 1: ui->m_forecastMethod->setText(i18nc("History-based method", "History")); ui->m_forecastCycles->setEnabled(true); ui->m_historyMethodGroupBox->setEnabled(true); break; default: ui->m_forecastMethod->setText(i18nc("Unknown forecast method", "Unknown")); break; } } void addAssetLiabilityRows(const MyMoneyForecast& forecast) { const auto file = MyMoneyFile::instance(); m_assetItem = new QTreeWidgetItem(m_totalItem); m_assetItem->setText(0, file->asset().name()); m_assetItem->setIcon(0, file->asset().accountPixmap()); m_assetItem->setData(0, ForecastRole, QVariant::fromValue(forecast)); m_assetItem->setData(0, AccountRole, QVariant::fromValue(file->asset())); m_assetItem->setExpanded(true); m_liabilityItem = new QTreeWidgetItem(m_totalItem); m_liabilityItem->setText(0, file->liability().name()); m_liabilityItem->setIcon(0, file->liability().accountPixmap()); m_liabilityItem->setData(0, ForecastRole, QVariant::fromValue(forecast)); m_liabilityItem->setData(0, AccountRole, QVariant::fromValue(file->liability())); m_liabilityItem->setExpanded(true); } void addIncomeExpenseRows(const MyMoneyForecast& forecast) { const auto file = MyMoneyFile::instance(); m_incomeItem = new QTreeWidgetItem(m_totalItem); m_incomeItem->setText(0, file->income().name()); m_incomeItem->setIcon(0, file->income().accountPixmap()); m_incomeItem->setData(0, ForecastRole, QVariant::fromValue(forecast)); m_incomeItem->setData(0, AccountRole, QVariant::fromValue(file->income())); m_incomeItem->setExpanded(true); m_expenseItem = new QTreeWidgetItem(m_totalItem); m_expenseItem->setText(0, file->expense().name()); m_expenseItem->setIcon(0, file->expense().accountPixmap()); m_expenseItem->setData(0, ForecastRole, QVariant::fromValue(forecast)); m_expenseItem->setData(0, AccountRole, QVariant::fromValue(file->expense())); m_expenseItem->setExpanded(true); } void addTotalRow(QTreeWidget* forecastList, const MyMoneyForecast& forecast) { const auto file = MyMoneyFile::instance(); m_totalItem = new QTreeWidgetItem(forecastList); QFont font; font.setBold(true); m_totalItem->setFont(0, font); m_totalItem->setText(0, i18nc("Total balance", "Total")); m_totalItem->setIcon(0, file->asset().accountPixmap()); m_totalItem->setData(0, ForecastRole, QVariant::fromValue(forecast)); m_totalItem->setData(0, AccountRole, QVariant::fromValue(file->asset())); m_totalItem->setExpanded(true); } bool includeAccount(MyMoneyForecast& forecast, const MyMoneyAccount& acc) { const auto file = MyMoneyFile::instance(); if (forecast.isForecastAccount(acc)) return true; foreach (const auto sAccount, acc.accountList()) { auto account = file->account(sAccount); if (includeAccount(forecast, account)) return true; } return false; } void adjustHeadersAndResizeToContents(QTreeWidget *widget) { QSize sizeHint(0, widget->sizeHintForRow(0)); QTreeWidgetItem *header = widget->headerItem(); for (int i = 0; i < header->columnCount(); ++i) { if (i > 0) { header->setData(i, Qt::TextAlignmentRole, Qt::AlignRight); // make sure that the row height stays the same even when the column that has icons is not visible if (m_totalItem) { m_totalItem->setSizeHint(i, sizeHint); } } widget->resizeColumnToContents(i); } } void setNegative(QTreeWidgetItem *item, bool isNegative) { if (isNegative) { for (int i = 0; i < item->columnCount(); ++i) { item->setForeground(i, KMyMoneySettings::schemeColor(SchemeColor::Negative)); } } } void showAmount(QTreeWidgetItem* item, int column, const MyMoneyMoney& amount, const MyMoneySecurity& security) { item->setText(column, MyMoneyUtils::formatMoney(amount, security)); item->setTextAlignment(column, Qt::AlignRight | Qt::AlignVCenter); item->setFont(column, item->font(0)); if (amount.isNegative()) { item->setForeground(column, KMyMoneySettings::schemeColor(SchemeColor::Negative)); } } void adjustParentValue(QTreeWidgetItem *item, int column, const MyMoneyMoney& value) { if (!item) return; item->setData(column, ValueRole, QVariant::fromValue(item->data(column, ValueRole).value() + value)); item->setData(column, ValueRole, QVariant::fromValue(item->data(column, ValueRole).value().convert(MyMoneyFile::instance()->baseCurrency().smallestAccountFraction()))); // if the entry has no children, // or it is the top entry // or it is currently not open // we need to display the value of it if (item->childCount() == 0 || !item->parent() || (!item->isExpanded() && item->childCount() > 0) || (item->parent() && !item->parent()->parent())) { if (item->childCount() > 0) item->setText(column, " "); MyMoneyMoney amount = item->data(column, ValueRole).value(); showAmount(item, column, amount, MyMoneyFile::instance()->baseCurrency()); } // now make sure, the upstream accounts also get notified about the value change adjustParentValue(item->parent(), column, value); } void setValue(QTreeWidgetItem* item, int column, const MyMoneyMoney& amount, const QDate& forecastDate) { MyMoneyAccount account = item->data(0, AccountRole).value(); //calculate the balance in base currency for the total row if (account.currencyId() != MyMoneyFile::instance()->baseCurrency().id()) { const auto file = MyMoneyFile::instance(); const auto curPrice = file->price(account.tradingCurrencyId(), file->baseCurrency().id(), forecastDate); const auto curRate = curPrice.rate(file->baseCurrency().id()); auto baseAmountMM = amount * curRate; auto value = baseAmountMM.convert(file->baseCurrency().smallestAccountFraction()); item->setData(column, ValueRole, QVariant::fromValue(value)); adjustParentValue(item->parent(), column, value); } else { item->setData(column, ValueRole, QVariant::fromValue(item->data(column, ValueRole).value() + amount)); adjustParentValue(item->parent(), column, amount); } } void setAmount(QTreeWidgetItem* item, int column, const MyMoneyMoney& amount) { item->setData(column, AmountRole, QVariant::fromValue(amount)); item->setTextAlignment(column, Qt::AlignRight | Qt::AlignVCenter); } void updateSummary(QTreeWidgetItem *item) { MyMoneyMoney amountMM; auto it_c = 1; // iterator for the columns of the listview const auto file = MyMoneyFile::instance(); qint64 daysToBeginDay; MyMoneyForecast forecast = item->data(0, ForecastRole).value(); if (QDate::currentDate() < forecast.beginForecastDate()) { daysToBeginDay = QDate::currentDate().daysTo(forecast.beginForecastDate()); } else { daysToBeginDay = forecast.accountsCycle(); } MyMoneyAccount account = item->data(0, AccountRole).value(); MyMoneySecurity currency; if (account.isInvest()) { MyMoneySecurity underSecurity = file->security(account.currencyId()); currency = file->security(underSecurity.tradingCurrency()); } else { currency = file->security(account.currencyId()); } //add current balance column QDate summaryDate = QDate::currentDate(); amountMM = forecast.forecastBalance(account, summaryDate); //calculate the balance in base currency for the total row setAmount(item, it_c, amountMM); setValue(item, it_c, amountMM, summaryDate); showAmount(item, it_c, amountMM, currency); it_c++; //iterate through all other columns for (summaryDate = QDate::currentDate().addDays(daysToBeginDay); summaryDate <= forecast.forecastEndDate(); summaryDate = summaryDate.addDays(forecast.accountsCycle()), ++it_c) { amountMM = forecast.forecastBalance(account, summaryDate); //calculate the balance in base currency for the total row setAmount(item, it_c, amountMM); setValue(item, it_c, amountMM, summaryDate); showAmount(item, it_c, amountMM, currency); } //calculate and add variation per cycle setNegative(item, forecast.accountTotalVariation(account).isNegative()); setAmount(item, it_c, forecast.accountTotalVariation(account)); setValue(item, it_c, forecast.accountTotalVariation(account), forecast.forecastEndDate()); showAmount(item, it_c, forecast.accountTotalVariation(account), currency); } void updateDetailed(QTreeWidgetItem *item) { - QString amount; - QString vAmount; MyMoneyMoney vAmountMM; const auto file = MyMoneyFile::instance(); MyMoneyAccount account = item->data(0, AccountRole).value(); MyMoneySecurity currency; if (account.isInvest()) { MyMoneySecurity underSecurity = file->security(account.currencyId()); currency = file->security(underSecurity.tradingCurrency()); } else { currency = file->security(account.currencyId()); } int it_c = 1; // iterator for the columns of the listview MyMoneyForecast forecast = item->data(0, ForecastRole).value(); for (QDate forecastDate = QDate::currentDate(); forecastDate <= forecast.forecastEndDate(); ++it_c, forecastDate = forecastDate.addDays(1)) { MyMoneyMoney amountMM = forecast.forecastBalance(account, forecastDate); //calculate the balance in base currency for the total row setAmount(item, it_c, amountMM); setValue(item, it_c, amountMM, forecastDate); showAmount(item, it_c, amountMM, currency); } //calculate and add variation per cycle vAmountMM = forecast.accountTotalVariation(account); setAmount(item, it_c, vAmountMM); setValue(item, it_c, vAmountMM, forecast.forecastEndDate()); showAmount(item, it_c, vAmountMM, currency); } void updateBudget(QTreeWidgetItem *item) { MyMoneySecurity currency; MyMoneyMoney tAmountMM; MyMoneyForecast forecast = item->data(0, ForecastRole).value(); const auto file = MyMoneyFile::instance(); int it_c = 1; // iterator for the columns of the listview QDate forecastDate = forecast.forecastStartDate(); MyMoneyAccount account = item->data(0, AccountRole).value(); if (account.isInvest()) { MyMoneySecurity underSecurity = file->security(account.currencyId()); currency = file->security(underSecurity.tradingCurrency()); } else { currency = file->security(account.currencyId()); } //iterate columns for (; forecastDate <= forecast.forecastEndDate(); forecastDate = forecastDate.addMonths(1), ++it_c) { MyMoneyMoney amountMM; amountMM = forecast.forecastBalance(account, forecastDate); if (account.accountType() == eMyMoney::Account::Type::Expense) amountMM = -amountMM; tAmountMM += amountMM; setAmount(item, it_c, amountMM); setValue(item, it_c, amountMM, forecastDate); showAmount(item, it_c, amountMM, currency); } //set total column setAmount(item, it_c, tAmountMM); setValue(item, it_c, tAmountMM, forecast.forecastEndDate()); showAmount(item, it_c, tAmountMM, currency); } /** * Get the list of prices for an account * This is used later to create an instance of KMyMoneyAccountTreeForecastItem * */ // QList getAccountPrices(const MyMoneyAccount& acc) // { // const auto file = MyMoneyFile::instance(); // QList prices; // MyMoneySecurity security = file->baseCurrency(); // try { // if (acc.isInvest()) { // security = file->security(acc.currencyId()); // if (security.tradingCurrency() != file->baseCurrency().id()) { // MyMoneySecurity sec = file->security(security.tradingCurrency()); // prices += file->price(sec.id(), file->baseCurrency().id()); // } // } else if (acc.currencyId() != file->baseCurrency().id()) { // if (acc.currencyId() != file->baseCurrency().id()) { // security = file->security(acc.currencyId()); // prices += file->price(acc.currencyId(), file->baseCurrency().id()); // } // } // } catch (const MyMoneyException &e) { // qDebug() << Q_FUNC_INFO << " caught exception while adding " << acc.name() << "[" << acc.id() << "]: " << e.what(); // } // return prices; // } KForecastView *q_ptr; Ui::KForecastView *ui; bool m_needReload[MaxViewTabs]; /** * This member holds the load state of page */ bool m_needLoad; QTreeWidgetItem* m_totalItem; QTreeWidgetItem* m_assetItem; QTreeWidgetItem* m_liabilityItem; QTreeWidgetItem* m_incomeItem; QTreeWidgetItem* m_expenseItem; QLayout* m_chartLayout; QWidget *m_forecastChart; QScopedPointer m_fixedColumnView; QMap m_nameIdx; }; #endif diff --git a/kmymoney/plugins/views/reports/reportsview.cpp b/kmymoney/plugins/views/reports/reportsview.cpp index 98a651be4..5953fe67a 100644 --- a/kmymoney/plugins/views/reports/reportsview.cpp +++ b/kmymoney/plugins/views/reports/reportsview.cpp @@ -1,330 +1,330 @@ /*************************************************************************** reportsview.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 "reportsview.h" // ---------------------------------------------------------------------------- // QT Includes // ---------------------------------------------------------------------------- // KDE Includes #include #include // ---------------------------------------------------------------------------- // Project Includes #include "viewinterface.h" #include "kreportsview.h" #include "kreportchartview.h" #include "kmymoneysettings.h" #include "pivottable.h" #include "pivotgrid.h" #include "mymoneyfile.h" #include "mymoneysecurity.h" #include "mymoneyenums.h" #include "reportsviewenums.h" #define VIEW_LEDGER "ledger" ReportsView::ReportsView(QObject *parent, const QVariantList &args) : KMyMoneyPlugin::Plugin(parent, "reportsview"/*must be the same as X-KDE-PluginInfo-Name*/), m_view(nullptr) { Q_UNUSED(args) setComponentName("reportsview", i18n("Reports view")); // For information, announce that we have been loaded. qDebug("Plugins: reportsview loaded"); } ReportsView::~ReportsView() { qDebug("Plugins: reportsview unloaded"); } void ReportsView::plug() { m_view = new KReportsView; viewInterface()->addView(m_view, i18n("Reports"), View::Reports); } void ReportsView::unplug() { viewInterface()->removeView(View::Reports); } QVariant ReportsView::requestData(const QString &arg, uint type) { switch(type) { case eWidgetPlugin::WidgetType::NetWorthForecast: return QVariant::fromValue(netWorthForecast()); case eWidgetPlugin::WidgetType::NetWorthForecastWithArgs: return QVariant::fromValue(netWorthForecast(arg)); case eWidgetPlugin::WidgetType::Budget: return QVariant(budget()); default: return QVariant(); } } QWidget *ReportsView::netWorthForecast() const { MyMoneyReport reportCfg = MyMoneyReport( eMyMoney::Report::RowType::AssetLiability, static_cast(eMyMoney::Report::ColumnType::Months), eMyMoney::TransactionFilter::Date::UserDefined, // overridden by the setDateFilter() call below eMyMoney::Report::DetailLevel::Total, i18n("Net Worth Forecast"), i18n("Generated Report")); reportCfg.setChartByDefault(true); reportCfg.setChartCHGridLines(false); reportCfg.setChartSVGridLines(false); reportCfg.setChartDataLabels(false); reportCfg.setChartType(eMyMoney::Report::ChartType::Line); reportCfg.setIncludingSchedules(false); reportCfg.addAccountGroup(eMyMoney::Account::Type::Asset); reportCfg.addAccountGroup(eMyMoney::Account::Type::Liability); reportCfg.setColumnsAreDays(true); reportCfg.setConvertCurrency(true); reportCfg.setIncludingForecast(true); reportCfg.setDateFilter(QDate::currentDate(), QDate::currentDate().addDays(+ 90)); reports::PivotTable table(reportCfg); auto chartWidget = new reports::KReportChartView(nullptr); table.drawChart(*chartWidget); return chartWidget; } QWidget *ReportsView::netWorthForecast(const QString &arg) const { const QStringList liArgs = arg.split(';'); if (liArgs.count() != 4) return new QWidget(); eMyMoney::Report::DetailLevel detailLevel[4] = { eMyMoney::Report::DetailLevel::All, eMyMoney::Report::DetailLevel::Top, eMyMoney::Report::DetailLevel::Group, eMyMoney::Report::DetailLevel::Total }; MyMoneyReport reportCfg = MyMoneyReport( eMyMoney::Report::RowType::AssetLiability, static_cast(eMyMoney::Report::ColumnType::Months), eMyMoney::TransactionFilter::Date::UserDefined, // overridden by the setDateFilter() call below detailLevel[liArgs.at(0).toInt()], i18n("Net Worth Forecast"), i18n("Generated Report")); reportCfg.setChartByDefault(true); reportCfg.setChartCHGridLines(false); reportCfg.setChartSVGridLines(false); reportCfg.setChartType(eMyMoney::Report::ChartType::Line); reportCfg.setIncludingSchedules(false); reportCfg.setColumnsAreDays( true ); reportCfg.setChartDataLabels(false); reportCfg.setConvertCurrency(true); reportCfg.setIncludingForecast(true); reportCfg.setDateFilter(QDate::currentDate(), QDate::currentDate().addDays(liArgs.at(2).toLongLong())); reports::PivotTable table(reportCfg); auto forecastChart = new reports::KReportChartView(nullptr); forecastChart->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); table.drawChart(*forecastChart); // Adjust the size forecastChart->resize(liArgs.at(2).toInt() - 10, liArgs.at(3).toInt()); forecastChart->show(); forecastChart->update(); return forecastChart; } QString ReportsView::budget() const { const auto file = MyMoneyFile::instance(); QString html; if (file->countBudgets() == 0) { html += QString(""); html += QString("
%1
").arg(i18n("You have no budgets to display.")); html += QString(""); return html; } auto prec = MyMoneyMoney::denomToPrec(file->baseCurrency().smallestAccountFraction()); - auto isOverrun = false; + bool isOverrun = false; int i = 0; //config report just like "Monthly Budgeted vs Actual MyMoneyReport reportCfg = MyMoneyReport( eMyMoney::Report::RowType::BudgetActual, static_cast(eMyMoney::Report::ColumnType::Months), eMyMoney::TransactionFilter::Date::CurrentMonth, eMyMoney::Report::DetailLevel::All, i18n("Monthly Budgeted vs. Actual"), i18n("Generated Report")); reportCfg.setBudget("Any", true); reports::PivotTable table(reportCfg); reports::PivotGrid grid = table.grid(); //div header html += "
" + i18n("Budget") + "
\n
 
\n"; //display budget summary html += ""; html += ""; html += ""; html += ""; html += ""; html += ""; html += ""; html += QString(""); MyMoneyMoney totalBudgetValue = grid.m_total[reports::eBudget].m_total; MyMoneyMoney totalActualValue = grid.m_total[reports::eActual].m_total; MyMoneyMoney totalBudgetDiffValue = grid.m_total[reports::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); html += QString("").arg(showColoredAmount(totalBudgetAmount, totalBudgetValue.isNegative())); html += QString("").arg(showColoredAmount(totalActualAmount, totalActualValue.isNegative())); html += QString("").arg(showColoredAmount(totalBudgetDiffAmount, totalBudgetDiffValue.isNegative())); html += ""; html += "
"; html += i18n("Current Month Summary"); html += "
"; html += i18n("Budgeted"); html += ""; html += i18n("Actual"); html += ""; html += i18n("Difference"); html += "
%1%1%1
"; //budget overrun html += "
 
\n"; html += ""; html += ""; html += ""; html += ""; html += ""; html += ""; html += ""; html += ""; reports::PivotGrid::iterator it_outergroup = grid.begin(); while (it_outergroup != grid.end()) { i = 0; reports::PivotOuterGroup::iterator it_innergroup = (*it_outergroup).begin(); while (it_innergroup != (*it_outergroup).end()) { reports::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()[reports::eBudgetDiff].value(1).isNegative()) { //get report account to get the name later reports::ReportAccount rowname = it_row.key(); //write the outergroup if it is the first row of outergroup being shown if (i == 0) { html += ""; html += QString("").arg(MyMoneyAccount::accountTypeToString(rowname.accountType())); html += ""; } html += QString("").arg(i++ & 0x01 ? "even" : "odd"); //get values from grid MyMoneyMoney actualValue = it_row.value()[reports::eActual][1]; MyMoneyMoney budgetValue = it_row.value()[reports::eBudget][1]; MyMoneyMoney budgetDiffValue = it_row.value()[reports::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 html += QString(""; //show amounts html += QString("").arg(showColoredAmount(budgetAmount, budgetValue.isNegative())); html += QString("").arg(showColoredAmount(actualAmount, actualValue.isNegative())); html += QString("").arg(showColoredAmount(budgetDiffAmount, budgetDiffValue.isNegative())); 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) { html += QString::fromLatin1("").arg(((i++ & 1) == 1) ? QLatin1String("even") : QLatin1String("odd")); html += QString::fromLatin1("").arg(i18n("No Budget Categories have been overrun")); html += ""; } html += "
"; html += i18n("Budget Overruns"); html += "
"; html += i18n("Account"); html += ""; html += i18n("Budgeted"); html += ""; html += i18n("Actual"); html += ""; html += i18n("Difference"); html += "
%1
") + link(VIEW_LEDGER, QString("?id=%1").arg(rowname.id()), QString()) + rowname.name() + linkend() + "%1%1%1
%1
"; return html; } QString ReportsView::showColoredAmount(const QString &amount, bool isNegative) const { 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; } QString ReportsView::link(const QString& view, const QString& query, const QString& _title) const { QString titlePart; QString title(_title); if (!title.isEmpty()) titlePart = QString(" title=\"%1\"").arg(title.replace(QLatin1Char(' '), " ")); return QString("").arg(view, query, titlePart); } QString ReportsView::linkend() const { return QStringLiteral(""); } K_PLUGIN_FACTORY_WITH_JSON(ReportsViewFactory, "reportsview.json", registerPlugin();) #include "reportsview.moc" diff --git a/kmymoney/plugins/xmlhelper/xmlstoragehelper.cpp b/kmymoney/plugins/xmlhelper/xmlstoragehelper.cpp index b1d56526c..7a15385f3 100644 --- a/kmymoney/plugins/xmlhelper/xmlstoragehelper.cpp +++ b/kmymoney/plugins/xmlhelper/xmlstoragehelper.cpp @@ -1,1259 +1,1254 @@ /* * Copyright 2004-2006 Ace Jones * Copyright 2006 Darren Gould * Copyright 2007-2010 Alvaro Soliverez * Copyright 2017-2018 Łukasz Wojniłowicz * Copyright 2018 Michael Kiefer * Copyright 2019 Thomas Baumgart * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "xmlstoragehelper.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes #include "mymoneymoney.h" #include "mymoneybudget.h" #include "mymoneyreport.h" #include "mymoneytransactionfilter.h" #include "mymoneyenums.h" #include "mymoneyexception.h" namespace Element { enum class Report { Payee, Tag, Account, Text, Type, State, Number, Amount, Dates, Category, AccountGroup, Validity }; enum class Budget { Budget = 0, Account, Period }; } namespace Attribute { enum class Report { ID = 0, Group, Type, Name, Comment, ConvertCurrency, Favorite, SkipZero, DateLock, DataLock, MovingAverageDays, IncludesActuals, IncludesForecast, IncludesPrice, IncludesAveragePrice, IncludesMovingAverage, IncludesSchedules, IncludesTransfers, IncludesUnused, MixedTime, Investments, Budget, ShowRowTotals, ShowColumnTotals, Detail, ColumnsAreDays, ChartType, ChartCHGridLines, ChartSVGridLines, ChartDataLabels, ChartByDefault, LogYAxis, ChartLineWidth, ColumnType, RowType, DataRangeStart, DataRangeEnd, DataMajorTick, DataMinorTick, YLabelsPrecision, QueryColumns, Tax, Loans, HideTransactions, InvestmentSum, SettlementPeriod, ShowSTLTCapitalGains, TermsSeparator, Pattern, CaseSensitive, RegEx, InvertText, State, From, To, Validity, NegExpenses, // insert new entries above this line LastAttribute }; enum class Budget { ID = 0, Name, Start, Version, BudgetLevel, BudgetSubAccounts, Amount, // insert new entries above this line LastAttribute }; } namespace MyMoneyXmlContentHandler2 { enum class Node { Report, Budget }; QString nodeName(Node nodeID) { static const QHash nodeNames { {Node::Report, QStringLiteral("REPORT")}, {Node::Budget, QStringLiteral("BUDGET")} }; return nodeNames.value(nodeID); } uint qHash(const Node key, uint seed) { return ::qHash(static_cast(key), seed); } QString elementName(Element::Report elementID) { static const QMap elementNames { {Element::Report::Payee, QStringLiteral("PAYEE")}, {Element::Report::Tag, QStringLiteral("TAG")}, {Element::Report::Account, QStringLiteral("ACCOUNT")}, {Element::Report::Text, QStringLiteral("TEXT")}, {Element::Report::Type, QStringLiteral("TYPE")}, {Element::Report::State, QStringLiteral("STATE")}, {Element::Report::Number, QStringLiteral("NUMBER")}, {Element::Report::Amount, QStringLiteral("AMOUNT")}, {Element::Report::Dates, QStringLiteral("DATES")}, {Element::Report::Category, QStringLiteral("CATEGORY")}, {Element::Report::AccountGroup, QStringLiteral("ACCOUNTGROUP")}, {Element::Report::Validity, QStringLiteral("VALIDITY")} }; return elementNames.value(elementID); } QString attributeName(Attribute::Report attributeID) { static const QMap attributeNames { {Attribute::Report::ID, QStringLiteral("id")}, {Attribute::Report::Group, QStringLiteral("group")}, {Attribute::Report::Type, QStringLiteral("type")}, {Attribute::Report::Name, QStringLiteral("name")}, {Attribute::Report::Comment, QStringLiteral("comment")}, {Attribute::Report::ConvertCurrency, QStringLiteral("convertcurrency")}, {Attribute::Report::Favorite, QStringLiteral("favorite")}, {Attribute::Report::SkipZero, QStringLiteral("skipZero")}, {Attribute::Report::DateLock, QStringLiteral("datelock")}, {Attribute::Report::DataLock, QStringLiteral("datalock")}, {Attribute::Report::MovingAverageDays, QStringLiteral("movingaveragedays")}, {Attribute::Report::IncludesActuals, QStringLiteral("includesactuals")}, {Attribute::Report::IncludesForecast, QStringLiteral("includesforecast")}, {Attribute::Report::IncludesPrice, QStringLiteral("includesprice")}, {Attribute::Report::IncludesAveragePrice, QStringLiteral("includesaverageprice")}, {Attribute::Report::IncludesMovingAverage, QStringLiteral("includesmovingaverage")}, {Attribute::Report::IncludesSchedules, QStringLiteral("includeschedules")}, {Attribute::Report::IncludesTransfers, QStringLiteral("includestransfers")}, {Attribute::Report::IncludesUnused, QStringLiteral("includeunused")}, {Attribute::Report::MixedTime, QStringLiteral("mixedtime")}, {Attribute::Report::Investments, QStringLiteral("investments")}, {Attribute::Report::Budget, QStringLiteral("budget")}, {Attribute::Report::ShowRowTotals, QStringLiteral("showrowtotals")}, {Attribute::Report::ShowColumnTotals, QStringLiteral("showcolumntotals")}, {Attribute::Report::Detail, QStringLiteral("detail")}, {Attribute::Report::ColumnsAreDays, QStringLiteral("columnsaredays")}, {Attribute::Report::ChartType, QStringLiteral("charttype")}, {Attribute::Report::ChartCHGridLines, QStringLiteral("chartchgridlines")}, {Attribute::Report::ChartSVGridLines, QStringLiteral("chartsvgridlines")}, {Attribute::Report::ChartDataLabels, QStringLiteral("chartdatalabels")}, {Attribute::Report::ChartByDefault, QStringLiteral("chartbydefault")}, {Attribute::Report::LogYAxis, QStringLiteral("logYaxis")}, {Attribute::Report::ChartLineWidth, QStringLiteral("chartlinewidth")}, {Attribute::Report::ColumnType, QStringLiteral("columntype")}, {Attribute::Report::RowType, QStringLiteral("rowtype")}, {Attribute::Report::DataRangeStart, QStringLiteral("dataRangeStart")}, {Attribute::Report::DataRangeEnd, QStringLiteral("dataRangeEnd")}, {Attribute::Report::DataMajorTick, QStringLiteral("dataMajorTick")}, {Attribute::Report::DataMinorTick, QStringLiteral("dataMinorTick")}, {Attribute::Report::YLabelsPrecision, QStringLiteral("yLabelsPrecision")}, {Attribute::Report::QueryColumns, QStringLiteral("querycolumns")}, {Attribute::Report::Tax, QStringLiteral("tax")}, {Attribute::Report::Loans, QStringLiteral("loans")}, {Attribute::Report::HideTransactions, QStringLiteral("hidetransactions")}, {Attribute::Report::InvestmentSum, QStringLiteral("investmentsum")}, {Attribute::Report::SettlementPeriod, QStringLiteral("settlementperiod")}, {Attribute::Report::ShowSTLTCapitalGains, QStringLiteral("showSTLTCapitalGains")}, {Attribute::Report::TermsSeparator, QStringLiteral("tseparator")}, {Attribute::Report::Pattern, QStringLiteral("pattern")}, {Attribute::Report::CaseSensitive, QStringLiteral("casesensitive")}, {Attribute::Report::RegEx, QStringLiteral("regex")}, {Attribute::Report::InvertText, QStringLiteral("inverttext")}, {Attribute::Report::State, QStringLiteral("state")}, {Attribute::Report::From, QStringLiteral("from")}, {Attribute::Report::To, QStringLiteral("to")}, {Attribute::Report::Validity, QStringLiteral("validity")}, {Attribute::Report::NegExpenses, QStringLiteral("negexpenses")} }; return attributeNames.value(attributeID); } QString elementName(Element::Budget elementID) { static const QMap elementNames { {Element::Budget::Budget, QStringLiteral("BUDGET")}, {Element::Budget::Account, QStringLiteral("ACCOUNT")}, {Element::Budget::Period, QStringLiteral("PERIOD")} }; return elementNames.value(elementID); } QString attributeName(Attribute::Budget attributeID) { static const QMap attributeNames { {Attribute::Budget::ID, QStringLiteral("id")}, {Attribute::Budget::Name, QStringLiteral("name")}, {Attribute::Budget::Start, QStringLiteral("start")}, {Attribute::Budget::Version, QStringLiteral("version")}, {Attribute::Budget::BudgetLevel, QStringLiteral("budgetlevel")}, {Attribute::Budget::BudgetSubAccounts, QStringLiteral("budgetsubaccounts")}, {Attribute::Budget::Amount, QStringLiteral("amount")} }; return attributeNames.value(attributeID); } QHash rowTypesLUT() { static const QHash lut { {eMyMoney::Report::RowType::NoRows, QStringLiteral("none")}, {eMyMoney::Report::RowType::AssetLiability, QStringLiteral("assetliability")}, {eMyMoney::Report::RowType::ExpenseIncome, QStringLiteral("expenseincome")}, {eMyMoney::Report::RowType::Category, QStringLiteral("category")}, {eMyMoney::Report::RowType::TopCategory, QStringLiteral("topcategory")}, {eMyMoney::Report::RowType::Account, QStringLiteral("account")}, {eMyMoney::Report::RowType::Tag, QStringLiteral("tag")}, {eMyMoney::Report::RowType::Payee, QStringLiteral("payee")}, {eMyMoney::Report::RowType::Month, QStringLiteral("month")}, {eMyMoney::Report::RowType::Week, QStringLiteral("week")}, {eMyMoney::Report::RowType::TopAccount, QStringLiteral("topaccount")}, {eMyMoney::Report::RowType::AccountByTopAccount, QStringLiteral("topaccount-account")}, {eMyMoney::Report::RowType::EquityType, QStringLiteral("equitytype")}, {eMyMoney::Report::RowType::AccountType, QStringLiteral("accounttype")}, {eMyMoney::Report::RowType::Institution, QStringLiteral("institution")}, {eMyMoney::Report::RowType::Budget, QStringLiteral("budget")}, {eMyMoney::Report::RowType::BudgetActual, QStringLiteral("budgetactual")}, {eMyMoney::Report::RowType::Schedule, QStringLiteral("schedule")}, {eMyMoney::Report::RowType::AccountInfo, QStringLiteral("accountinfo")}, {eMyMoney::Report::RowType::AccountLoanInfo, QStringLiteral("accountloaninfo")}, {eMyMoney::Report::RowType::AccountReconcile, QStringLiteral("accountreconcile")}, {eMyMoney::Report::RowType::CashFlow, QStringLiteral("cashflow")}, }; return lut; } QString reportNames(eMyMoney::Report::RowType textID) { return rowTypesLUT().value(textID); } eMyMoney::Report::RowType stringToRowType(const QString &text) { return rowTypesLUT().key(text, eMyMoney::Report::RowType::Invalid); } QHash columTypesLUT() { static const QHash lut { {eMyMoney::Report::ColumnType::NoColumns, QStringLiteral("none")}, {eMyMoney::Report::ColumnType::Months, QStringLiteral("months")}, {eMyMoney::Report::ColumnType::BiMonths, QStringLiteral("bimonths")}, {eMyMoney::Report::ColumnType::Quarters, QStringLiteral("quarters")}, // {eMyMoney::Report::ColumnType::, QStringLiteral("4")} // {eMyMoney::Report::ColumnType::, QStringLiteral("5")} // {eMyMoney::Report::ColumnType::, QStringLiteral("6")} {eMyMoney::Report::ColumnType::Weeks, QStringLiteral("weeks")}, // {eMyMoney::Report::ColumnType::, QStringLiteral("8")} // {eMyMoney::Report::ColumnType::, QStringLiteral("9")} // {eMyMoney::Report::ColumnType::, QStringLiteral("10")} // {eMyMoney::Report::ColumnType::, QStringLiteral("11")} {eMyMoney::Report::ColumnType::Years, QStringLiteral("years")} }; return lut; } QString reportNames(eMyMoney::Report::ColumnType textID) { return columTypesLUT().value(textID); } eMyMoney::Report::ColumnType stringToColumnType(const QString &text) { return columTypesLUT().key(text, eMyMoney::Report::ColumnType::Invalid); } QHash queryColumnsLUT() { static const QHash lut { {eMyMoney::Report::QueryColumn::None, QStringLiteral("none")}, {eMyMoney::Report::QueryColumn::Number, QStringLiteral("number")}, {eMyMoney::Report::QueryColumn::Payee, QStringLiteral("payee")}, {eMyMoney::Report::QueryColumn::Category, QStringLiteral("category")}, {eMyMoney::Report::QueryColumn::Tag, QStringLiteral("tag")}, {eMyMoney::Report::QueryColumn::Memo, QStringLiteral("memo")}, {eMyMoney::Report::QueryColumn::Account, QStringLiteral("account")}, {eMyMoney::Report::QueryColumn::Reconciled, QStringLiteral("reconcileflag")}, {eMyMoney::Report::QueryColumn::Action, QStringLiteral("action")}, {eMyMoney::Report::QueryColumn::Shares, QStringLiteral("shares")}, {eMyMoney::Report::QueryColumn::Price, QStringLiteral("price")}, {eMyMoney::Report::QueryColumn::Performance, QStringLiteral("performance")}, {eMyMoney::Report::QueryColumn::Loan, QStringLiteral("loan")}, {eMyMoney::Report::QueryColumn::Balance, QStringLiteral("balance")}, {eMyMoney::Report::QueryColumn::CapitalGain, QStringLiteral("capitalgain")} }; return lut; } QString reportNamesForQC(eMyMoney::Report::QueryColumn textID) { return queryColumnsLUT().value(textID); } eMyMoney::Report::QueryColumn stringToQueryColumn(const QString &text) { return queryColumnsLUT().key(text, eMyMoney::Report::QueryColumn::End); } QHash detailLevelLUT() { static const QHash lut { {eMyMoney::Report::DetailLevel::None, QStringLiteral("none")}, {eMyMoney::Report::DetailLevel::All, QStringLiteral("all")}, {eMyMoney::Report::DetailLevel::Top, QStringLiteral("top")}, {eMyMoney::Report::DetailLevel::Group, QStringLiteral("group")}, {eMyMoney::Report::DetailLevel::Total, QStringLiteral("total")}, {eMyMoney::Report::DetailLevel::End, QStringLiteral("invalid")} }; return lut; } QString reportNames(eMyMoney::Report::DetailLevel textID) { return detailLevelLUT().value(textID); } eMyMoney::Report::DetailLevel stringToDetailLevel(const QString &text) { return detailLevelLUT().key(text, eMyMoney::Report::DetailLevel::End); } QHash chartTypeLUT() { static const QHash lut { {eMyMoney::Report::ChartType::None, QStringLiteral("none")}, {eMyMoney::Report::ChartType::Line, QStringLiteral("line")}, {eMyMoney::Report::ChartType::Bar, QStringLiteral("bar")}, {eMyMoney::Report::ChartType::Pie, QStringLiteral("pie")}, {eMyMoney::Report::ChartType::Ring, QStringLiteral("ring")}, {eMyMoney::Report::ChartType::StackedBar, QStringLiteral("stackedbar")} }; return lut; } QString reportNames(eMyMoney::Report::ChartType textID) { return chartTypeLUT().value(textID); } eMyMoney::Report::ChartType stringToChartType(const QString &text) { return chartTypeLUT().key(text, eMyMoney::Report::ChartType::End); } QHash typeAttributeLUT() { static const QHash lut { {0, QStringLiteral("all")}, {1, QStringLiteral("payments")}, {2, QStringLiteral("deposits")}, {3, QStringLiteral("transfers")}, {4, QStringLiteral("none")}, }; return lut; } QString typeAttributeToString(int textID) { return typeAttributeLUT().value(textID); } int stringToTypeAttribute(const QString &text) { return typeAttributeLUT().key(text, 4); } QHash stateAttributeLUT() { static const QHash lut { {0, QStringLiteral("all")}, {1, QStringLiteral("notreconciled")}, {2, QStringLiteral("cleared")}, {3, QStringLiteral("reconciled")}, {4, QStringLiteral("frozen")}, {5, QStringLiteral("none")} }; return lut; } QString stateAttributeToString(int textID) { return stateAttributeLUT().value(textID); } int stringToStateAttribute(const QString &text) { return stateAttributeLUT().key(text, 5); } QHash validityAttributeLUT() { static const QHash lut { {0, QStringLiteral("any")}, {1, QStringLiteral("valid")}, {2, QStringLiteral("invalid")}, }; return lut; } QString validityAttributeToString(int textID) { return validityAttributeLUT().value(textID); } int stringToValidityAttribute(const QString &text) { return validityAttributeLUT().key(text, 0); } QHash dateLockLUT() { static const QHash lut { {eMyMoney::TransactionFilter::Date::All, QStringLiteral("alldates")}, {eMyMoney::TransactionFilter::Date::AsOfToday, QStringLiteral("untiltoday")}, {eMyMoney::TransactionFilter::Date::CurrentMonth, QStringLiteral("currentmonth")}, {eMyMoney::TransactionFilter::Date::CurrentYear, QStringLiteral("currentyear")}, {eMyMoney::TransactionFilter::Date::MonthToDate, QStringLiteral("monthtodate")}, {eMyMoney::TransactionFilter::Date::YearToDate, QStringLiteral("yeartodate")}, {eMyMoney::TransactionFilter::Date::YearToMonth, QStringLiteral("yeartomonth")}, {eMyMoney::TransactionFilter::Date::LastMonth, QStringLiteral("lastmonth")}, {eMyMoney::TransactionFilter::Date::LastYear, QStringLiteral("lastyear")}, {eMyMoney::TransactionFilter::Date::Last7Days, QStringLiteral("last7days")}, {eMyMoney::TransactionFilter::Date::Last30Days, QStringLiteral("last30days")}, {eMyMoney::TransactionFilter::Date::Last3Months, QStringLiteral("last3months")}, {eMyMoney::TransactionFilter::Date::Last6Months, QStringLiteral("last6months")}, {eMyMoney::TransactionFilter::Date::Last12Months, QStringLiteral("last12months")}, {eMyMoney::TransactionFilter::Date::Next7Days, QStringLiteral("next7days")}, {eMyMoney::TransactionFilter::Date::Next30Days, QStringLiteral("next30days")}, {eMyMoney::TransactionFilter::Date::Next3Months, QStringLiteral("next3months")}, {eMyMoney::TransactionFilter::Date::Next6Months, QStringLiteral("next6months")}, {eMyMoney::TransactionFilter::Date::Next12Months, QStringLiteral("next12months")}, {eMyMoney::TransactionFilter::Date::UserDefined, QStringLiteral("userdefined")}, {eMyMoney::TransactionFilter::Date::Last3ToNext3Months, QStringLiteral("last3tonext3months")}, {eMyMoney::TransactionFilter::Date::Last11Months, QStringLiteral("last11Months")}, {eMyMoney::TransactionFilter::Date::CurrentQuarter, QStringLiteral("currentQuarter")}, {eMyMoney::TransactionFilter::Date::LastQuarter, QStringLiteral("lastQuarter")}, {eMyMoney::TransactionFilter::Date::NextQuarter, QStringLiteral("nextQuarter")}, {eMyMoney::TransactionFilter::Date::CurrentFiscalYear, QStringLiteral("currentFiscalYear")}, {eMyMoney::TransactionFilter::Date::LastFiscalYear, QStringLiteral("lastFiscalYear")}, {eMyMoney::TransactionFilter::Date::Today, QStringLiteral("today")}, {eMyMoney::TransactionFilter::Date::Next18Months, QStringLiteral("next18months")} }; return lut; } QString dateLockAttributeToString(eMyMoney::TransactionFilter::Date textID) { return dateLockLUT().value(textID); } eMyMoney::TransactionFilter::Date stringToDateLockAttribute(const QString &text) { return dateLockLUT().key(text, eMyMoney::TransactionFilter::Date::UserDefined); } QHash dataLockLUT() { static const QHash lut { {eMyMoney::Report::DataLock::Automatic, QStringLiteral("automatic")}, {eMyMoney::Report::DataLock::UserDefined, QStringLiteral("userdefined")} }; return lut; } QString reportNames(eMyMoney::Report::DataLock textID) { return dataLockLUT().value(textID); } eMyMoney::Report::DataLock stringToDataLockAttribute(const QString &text) { return dataLockLUT().key(text, eMyMoney::Report::DataLock::DataOptionCount); } QHash accountTypeAttributeLUT() { static const QHash lut { {eMyMoney::Account::Type::Unknown, QStringLiteral("unknown")}, {eMyMoney::Account::Type::Checkings, QStringLiteral("checkings")}, {eMyMoney::Account::Type::Savings, QStringLiteral("savings")}, {eMyMoney::Account::Type::Cash, QStringLiteral("cash")}, {eMyMoney::Account::Type::CreditCard, QStringLiteral("creditcard")}, {eMyMoney::Account::Type::Loan, QStringLiteral("loan")}, {eMyMoney::Account::Type::CertificateDep, QStringLiteral("certificatedep")}, {eMyMoney::Account::Type::Investment, QStringLiteral("investment")}, {eMyMoney::Account::Type::MoneyMarket, QStringLiteral("moneymarket")}, {eMyMoney::Account::Type::Asset, QStringLiteral("asset")}, {eMyMoney::Account::Type::Liability, QStringLiteral("liability")}, {eMyMoney::Account::Type::Currency, QStringLiteral("currency")}, {eMyMoney::Account::Type::Income, QStringLiteral("income")}, {eMyMoney::Account::Type::Expense, QStringLiteral("expense")}, {eMyMoney::Account::Type::AssetLoan, QStringLiteral("assetloan")}, {eMyMoney::Account::Type::Stock, QStringLiteral("stock")}, {eMyMoney::Account::Type::Equity, QStringLiteral("equity")}, }; return lut; } QString accountTypeAttributeToString(eMyMoney::Account::Type type) { return accountTypeAttributeLUT().value(type); } eMyMoney::Account::Type stringToAccountTypeAttribute(const QString &text) { return accountTypeAttributeLUT().key(text, eMyMoney::Account::Type::Unknown); } eMyMoney::Report::ReportType rowTypeToReportType(eMyMoney::Report::RowType rowType) { static const QHash reportTypes { {eMyMoney::Report::RowType::NoRows, eMyMoney::Report::ReportType::NoReport}, {eMyMoney::Report::RowType::AssetLiability, eMyMoney::Report::ReportType::PivotTable}, {eMyMoney::Report::RowType::ExpenseIncome, eMyMoney::Report::ReportType::PivotTable}, {eMyMoney::Report::RowType::Category, eMyMoney::Report::ReportType::QueryTable}, {eMyMoney::Report::RowType::TopCategory, eMyMoney::Report::ReportType::QueryTable}, {eMyMoney::Report::RowType::Account, eMyMoney::Report::ReportType::QueryTable}, {eMyMoney::Report::RowType::Tag, eMyMoney::Report::ReportType::QueryTable}, {eMyMoney::Report::RowType::Payee, eMyMoney::Report::ReportType::QueryTable}, {eMyMoney::Report::RowType::Month, eMyMoney::Report::ReportType::QueryTable}, {eMyMoney::Report::RowType::Week, eMyMoney::Report::ReportType::QueryTable}, {eMyMoney::Report::RowType::TopAccount, eMyMoney::Report::ReportType::QueryTable}, {eMyMoney::Report::RowType::AccountByTopAccount, eMyMoney::Report::ReportType::QueryTable}, {eMyMoney::Report::RowType::EquityType, eMyMoney::Report::ReportType::QueryTable}, {eMyMoney::Report::RowType::AccountType, eMyMoney::Report::ReportType::QueryTable}, {eMyMoney::Report::RowType::Institution, eMyMoney::Report::ReportType::QueryTable}, {eMyMoney::Report::RowType::Budget, eMyMoney::Report::ReportType::PivotTable}, {eMyMoney::Report::RowType::BudgetActual, eMyMoney::Report::ReportType::PivotTable}, {eMyMoney::Report::RowType::Schedule, eMyMoney::Report::ReportType::InfoTable}, {eMyMoney::Report::RowType::AccountInfo, eMyMoney::Report::ReportType::InfoTable}, {eMyMoney::Report::RowType::AccountLoanInfo, eMyMoney::Report::ReportType::InfoTable}, {eMyMoney::Report::RowType::AccountReconcile, eMyMoney::Report::ReportType::QueryTable}, {eMyMoney::Report::RowType::CashFlow, eMyMoney::Report::ReportType::QueryTable}, }; return reportTypes.value(rowType, eMyMoney::Report::ReportType::Invalid); } QHash budgetLevelLUT() { static const QHash lut { {eMyMoney::Budget::Level::None, QStringLiteral("none")}, {eMyMoney::Budget::Level::Monthly, QStringLiteral("monthly")}, {eMyMoney::Budget::Level::MonthByMonth, QStringLiteral("monthbymonth")}, {eMyMoney::Budget::Level::Yearly, QStringLiteral("yearly")}, {eMyMoney::Budget::Level::Max, QStringLiteral("invalid")}, }; return lut; } QString budgetNames(eMyMoney::Budget::Level textID) { return budgetLevelLUT().value(textID); } eMyMoney::Budget::Level stringToBudgetLevel(const QString &text) { return budgetLevelLUT().key(text, eMyMoney::Budget::Level::Max); } QHash budgetLevelsLUT() { static const QHash lut { {eMyMoney::Budget::Level::None, QStringLiteral("none")}, {eMyMoney::Budget::Level::Monthly, QStringLiteral("monthly")}, {eMyMoney::Budget::Level::MonthByMonth, QStringLiteral("monthbymonth")}, {eMyMoney::Budget::Level::Yearly, QStringLiteral("yearly")}, {eMyMoney::Budget::Level::Max, QStringLiteral("invalid")}, }; return lut; } QString budgetLevels(eMyMoney::Budget::Level textID) { return budgetLevelsLUT().value(textID); } void writeBaseXML(const QString &id, QDomDocument &document, QDomElement &el) { Q_UNUSED(document); el.setAttribute(QStringLiteral("id"), id); } MyMoneyReport readReport(const QDomElement &node) { if (nodeName(Node::Report) != node.tagName()) throw MYMONEYEXCEPTION_CSTRING("Node was not REPORT"); MyMoneyReport report(node.attribute(attributeName(Attribute::Report::ID))); // The goal of this reading method is 100% backward AND 100% forward // compatibility. Any report ever created with any version of KMyMoney // should be able to be loaded by this method (as long as it's one of the // report types supported in this version, of course) // read report's internals QString type = node.attribute(attributeName(Attribute::Report::Type)); if (type.startsWith(QLatin1String("pivottable"))) report.setReportType(eMyMoney::Report::ReportType::PivotTable); else if (type.startsWith(QLatin1String("querytable"))) report.setReportType(eMyMoney::Report::ReportType::QueryTable); else if (type.startsWith(QLatin1String("infotable"))) report.setReportType(eMyMoney::Report::ReportType::InfoTable); else throw MYMONEYEXCEPTION_CSTRING("Unknown report type"); report.setGroup(node.attribute(attributeName(Attribute::Report::Group))); report.clearTransactionFilter(); // read date tab QString datelockstr = node.attribute(attributeName(Attribute::Report::DateLock), "userdefined"); // Handle the pivot 1.2/query 1.1 case where the values were saved as // numbers bool ok = false; eMyMoney::TransactionFilter::Date dateLock = static_cast(datelockstr.toUInt(&ok)); if (!ok) { dateLock = stringToDateLockAttribute(datelockstr); } report.setDateFilter(dateLock); // read general tab report.setName(node.attribute(attributeName(Attribute::Report::Name))); report.setComment(node.attribute(attributeName(Attribute::Report::Comment), "Extremely old report")); report.setConvertCurrency(node.attribute(attributeName(Attribute::Report::ConvertCurrency), "1").toUInt()); report.setFavorite(node.attribute(attributeName(Attribute::Report::Favorite), "0").toUInt()); report.setSkipZero(node.attribute(attributeName(Attribute::Report::SkipZero), "0").toUInt()); const auto rowTypeFromXML = stringToRowType(node.attribute(attributeName(Attribute::Report::RowType))); if (report.reportType() == eMyMoney::Report::ReportType::PivotTable) { // read report's internals report.setIncludingBudgetActuals(node.attribute(attributeName(Attribute::Report::IncludesActuals), "0").toUInt()); report.setIncludingForecast(node.attribute(attributeName(Attribute::Report::IncludesForecast), "0").toUInt()); report.setIncludingPrice(node.attribute(attributeName(Attribute::Report::IncludesPrice), "0").toUInt()); report.setIncludingAveragePrice(node.attribute(attributeName(Attribute::Report::IncludesAveragePrice), "0").toUInt()); report.setMixedTime(node.attribute(attributeName(Attribute::Report::MixedTime), "0").toUInt()); report.setInvestmentsOnly(node.attribute(attributeName(Attribute::Report::Investments), "0").toUInt()); // read rows/columns tab if (node.hasAttribute(attributeName(Attribute::Report::Budget))) report.setBudget(node.attribute(attributeName(Attribute::Report::Budget))); if (rowTypeFromXML != eMyMoney::Report::RowType::Invalid) report.setRowType(rowTypeFromXML); else report.setRowType(eMyMoney::Report::RowType::ExpenseIncome); if (node.hasAttribute(attributeName(Attribute::Report::ShowRowTotals))) report.setShowingRowTotals(node.attribute(attributeName(Attribute::Report::ShowRowTotals)).toUInt()); else if (report.rowType() == eMyMoney::Report::RowType::ExpenseIncome) // for backward compatibility report.setShowingRowTotals(true); report.setShowingColumnTotals(node.attribute(attributeName(Attribute::Report::ShowColumnTotals), "1").toUInt()); //check for reports with older settings which didn't have the detail attribute const auto detailLevelFromXML = stringToDetailLevel(node.attribute(attributeName(Attribute::Report::Detail))); if (detailLevelFromXML != eMyMoney::Report::DetailLevel::End) report.setDetailLevel(detailLevelFromXML); else report.setDetailLevel(eMyMoney::Report::DetailLevel::All); report.setIncludingMovingAverage(node.attribute(attributeName(Attribute::Report::IncludesMovingAverage), "0").toUInt()); if (report.isIncludingMovingAverage()) report.setMovingAverageDays(node.attribute(attributeName(Attribute::Report::MovingAverageDays), "1").toUInt()); report.setIncludingSchedules(node.attribute(attributeName(Attribute::Report::IncludesSchedules), "0").toUInt()); report.setIncludingTransfers(node.attribute(attributeName(Attribute::Report::IncludesTransfers), "0").toUInt()); report.setIncludingUnusedAccounts(node.attribute(attributeName(Attribute::Report::IncludesUnused), "0").toUInt()); report.setColumnsAreDays(node.attribute(attributeName(Attribute::Report::ColumnsAreDays), "0").toUInt()); // read chart tab const auto chartTypeFromXML = stringToChartType(node.attribute(attributeName(Attribute::Report::ChartType))); if (chartTypeFromXML != eMyMoney::Report::ChartType::End) report.setChartType(chartTypeFromXML); else report.setChartType(eMyMoney::Report::ChartType::None); report.setChartCHGridLines(node.attribute(attributeName(Attribute::Report::ChartCHGridLines), "1").toUInt()); report.setChartSVGridLines(node.attribute(attributeName(Attribute::Report::ChartSVGridLines), "1").toUInt()); report.setChartDataLabels(node.attribute(attributeName(Attribute::Report::ChartDataLabels), "1").toUInt()); report.setChartByDefault(node.attribute(attributeName(Attribute::Report::ChartByDefault), "0").toUInt()); report.setLogYAxis(node.attribute(attributeName(Attribute::Report::LogYAxis), "0").toUInt()); report.setNegExpenses(node.attribute(attributeName(Attribute::Report::NegExpenses), "0").toUInt()); report.setChartLineWidth(node.attribute(attributeName(Attribute::Report::ChartLineWidth), QString(MyMoneyReport::lineWidth())).toUInt()); // read range tab const auto columnTypeFromXML = stringToColumnType(node.attribute(attributeName(Attribute::Report::ColumnType))); if (columnTypeFromXML != eMyMoney::Report::ColumnType::Invalid) report.setColumnType(columnTypeFromXML); else report.setColumnType(eMyMoney::Report::ColumnType::Months); const auto dataLockFromXML = stringToDataLockAttribute(node.attribute(attributeName(Attribute::Report::DataLock))); if (dataLockFromXML != eMyMoney::Report::DataLock::DataOptionCount) report.setDataFilter(dataLockFromXML); else report.setDataFilter(eMyMoney::Report::DataLock::Automatic); report.setDataRangeStart(node.attribute(attributeName(Attribute::Report::DataRangeStart), "0")); report.setDataRangeEnd(node.attribute(attributeName(Attribute::Report::DataRangeEnd), "0")); report.setDataMajorTick(node.attribute(attributeName(Attribute::Report::DataMajorTick), "0")); report.setDataMinorTick(node.attribute(attributeName(Attribute::Report::DataMinorTick), "0")); report.setYLabelsPrecision(node.attribute(attributeName(Attribute::Report::YLabelsPrecision), "2").toUInt()); } else if (report.reportType() == eMyMoney::Report::ReportType::QueryTable) { // read rows/columns tab if (rowTypeFromXML != eMyMoney::Report::RowType::Invalid) report.setRowType(rowTypeFromXML); else report.setRowType(eMyMoney::Report::RowType::Account); unsigned qc = 0; QStringList columns = node.attribute(attributeName(Attribute::Report::QueryColumns), "none").split(','); foreach (const auto column, columns) { const int queryColumnFromXML = stringToQueryColumn(column); if (queryColumnFromXML != eMyMoney::Report::QueryColumn::End) qc |= queryColumnFromXML; } report.setQueryColumns(static_cast(qc)); report.setTax(node.attribute(attributeName(Attribute::Report::Tax), "0").toUInt()); report.setInvestmentsOnly(node.attribute(attributeName(Attribute::Report::Investments), "0").toUInt()); report.setLoansOnly(node.attribute(attributeName(Attribute::Report::Loans), "0").toUInt()); report.setHideTransactions(node.attribute(attributeName(Attribute::Report::HideTransactions), "0").toUInt()); report.setShowingColumnTotals(node.attribute(attributeName(Attribute::Report::ShowColumnTotals), "1").toUInt()); const auto detailLevelFromXML = stringToDetailLevel(node.attribute(attributeName(Attribute::Report::Detail), "none")); if (detailLevelFromXML == eMyMoney::Report::DetailLevel::All) report.setDetailLevel(detailLevelFromXML); else report.setDetailLevel(eMyMoney::Report::DetailLevel::None); // read performance or capital gains tab if (report.queryColumns() & eMyMoney::Report::QueryColumn::Performance) report.setInvestmentSum(static_cast(node.attribute(attributeName(Attribute::Report::InvestmentSum), QString::number(static_cast(eMyMoney::Report::InvestmentSum::Period))).toInt())); // read capital gains tab if (report.queryColumns() & eMyMoney::Report::QueryColumn::CapitalGain) { report.setInvestmentSum(static_cast(node.attribute(attributeName(Attribute::Report::InvestmentSum), QString::number(static_cast(eMyMoney::Report::InvestmentSum::Sold))).toInt())); if (report.investmentSum() == eMyMoney::Report::InvestmentSum::Sold) { report.setShowSTLTCapitalGains(node.attribute(attributeName(Attribute::Report::ShowSTLTCapitalGains), "0").toUInt()); report.setSettlementPeriod(node.attribute(attributeName(Attribute::Report::SettlementPeriod), "3").toUInt()); report.setTermSeparator(QDate::fromString(node.attribute(attributeName(Attribute::Report::TermsSeparator), QDate::currentDate().addYears(-1).toString(Qt::ISODate)),Qt::ISODate)); } } } else if (report.reportType() == eMyMoney::Report::ReportType::InfoTable) { if (rowTypeFromXML != eMyMoney::Report::RowType::Invalid) report.setRowType(rowTypeFromXML); else report.setRowType(eMyMoney::Report::RowType::AccountInfo); if (node.hasAttribute(attributeName(Attribute::Report::ShowRowTotals))) report.setShowingRowTotals(node.attribute(attributeName(Attribute::Report::ShowRowTotals)).toUInt()); else report.setShowingRowTotals(true); } QDomNode child = node.firstChild(); while (!child.isNull() && child.isElement()) { QDomElement c = child.toElement(); if (elementName(Element::Report::Text) == c.tagName() && c.hasAttribute(attributeName(Attribute::Report::Pattern))) { report.setTextFilter(QRegExp(c.attribute(attributeName(Attribute::Report::Pattern)), c.attribute(attributeName(Attribute::Report::CaseSensitive), "1").toUInt() ? Qt::CaseSensitive : Qt::CaseInsensitive, c.attribute(attributeName(Attribute::Report::RegEx), "1").toUInt() ? QRegExp::Wildcard : QRegExp::RegExp), c.attribute(attributeName(Attribute::Report::InvertText), "0").toUInt()); } if (elementName(Element::Report::Type) == c.tagName() && c.hasAttribute(attributeName(Attribute::Report::Type))) { const auto reportType = stringToTypeAttribute(c.attribute(attributeName(Attribute::Report::Type))); if (reportType != -1) report.addType(reportType); } if (elementName(Element::Report::State) == c.tagName() && c.hasAttribute(attributeName(Attribute::Report::State))) { const auto state = stringToStateAttribute(c.attribute(attributeName(Attribute::Report::State))); if (state != -1) report.addState(state); } if (elementName(Element::Report::Validity) == c.tagName() && c.hasAttribute(attributeName(Attribute::Report::Validity))) { const auto validity = stringToValidityAttribute(c.attribute(attributeName(Attribute::Report::Validity))); if (validity != -1) report.addValidity(validity); } if (elementName(Element::Report::Number) == c.tagName()) report.setNumberFilter(c.attribute(attributeName(Attribute::Report::From)), c.attribute(attributeName(Attribute::Report::To))); if (elementName(Element::Report::Amount) == c.tagName()) report.setAmountFilter(MyMoneyMoney(c.attribute(attributeName(Attribute::Report::From), "0/100")), MyMoneyMoney(c.attribute(attributeName(Attribute::Report::To), "0/100"))); if (elementName(Element::Report::Dates) == c.tagName()) { QDate from, to; if (c.hasAttribute(attributeName(Attribute::Report::From))) from = QDate::fromString(c.attribute(attributeName(Attribute::Report::From)), Qt::ISODate); if (c.hasAttribute(attributeName(Attribute::Report::To))) to = QDate::fromString(c.attribute(attributeName(Attribute::Report::To)), Qt::ISODate); report.setDateFilter(from, to); } if (elementName(Element::Report::Payee) == c.tagName()) report.addPayee(c.attribute(attributeName(Attribute::Report::ID))); if (elementName(Element::Report::Tag) == c.tagName()) report.addTag(c.attribute(attributeName(Attribute::Report::ID))); if (elementName(Element::Report::Category) == c.tagName() && c.hasAttribute(attributeName(Attribute::Report::ID))) report.addCategory(c.attribute(attributeName(Attribute::Report::ID))); if (elementName(Element::Report::Account) == c.tagName() && c.hasAttribute(attributeName(Attribute::Report::ID))) report.addAccount(c.attribute(attributeName(Attribute::Report::ID))); #if 0 // account groups had a severe problem in versions 5.0.0 to 5.0.2. Therefor, we don't read them // in anymore and rebuild them internally. They are written to the file nevertheless to maintain // compatibility to older versions which rely on them. I left the old code for reference here // ipwizard - 2019-01-13 if (elementName(Element::Report::AccountGroup) == c.tagName() && c.hasAttribute(attributeName(Attribute::Report::Group))) { const auto groupType = stringToAccountTypeAttribute(c.attribute(attributeName(Attribute::Report::Group))); if (groupType != eMyMoney::Account::Type::Unknown) report.addAccountGroup(groupType); } #endif child = child.nextSibling(); } return report; } void writeReport(const MyMoneyReport &report, QDomDocument &document, QDomElement &parent) { auto el = document.createElement(nodeName(Node::Report)); // No matter what changes, be sure to have a 'type' attribute. Only change // the major type if it becomes impossible to maintain compatibility with // older versions of the program as new features are added to the reports. // Feel free to change the minor type every time a change is made here. // write report's internals if (report.reportType() == eMyMoney::Report::ReportType::PivotTable) el.setAttribute(attributeName(Attribute::Report::Type), "pivottable 1.15"); else if (report.reportType() == eMyMoney::Report::ReportType::QueryTable) el.setAttribute(attributeName(Attribute::Report::Type), "querytable 1.14"); else if (report.reportType() == eMyMoney::Report::ReportType::InfoTable) el.setAttribute(attributeName(Attribute::Report::Type), "infotable 1.0"); el.setAttribute(attributeName(Attribute::Report::Group), report.group()); el.setAttribute(attributeName(Attribute::Report::ID), report.id()); // write general tab - auto anonymous = false; - if (anonymous) { - el.setAttribute(attributeName(Attribute::Report::Name), report.id()); - el.setAttribute(attributeName(Attribute::Report::Comment), QString(report.comment()).fill('x')); - } else { - el.setAttribute(attributeName(Attribute::Report::Name), report.name()); - el.setAttribute(attributeName(Attribute::Report::Comment), report.comment()); - } + el.setAttribute(attributeName(Attribute::Report::Name), report.name()); + el.setAttribute(attributeName(Attribute::Report::Comment), report.comment()); + el.setAttribute(attributeName(Attribute::Report::ConvertCurrency), report.isConvertCurrency()); el.setAttribute(attributeName(Attribute::Report::Favorite), report.isFavorite()); el.setAttribute(attributeName(Attribute::Report::SkipZero), report.isSkippingZero()); el.setAttribute(attributeName(Attribute::Report::DateLock), dateLockAttributeToString(report.dateRange())); el.setAttribute(attributeName(Attribute::Report::RowType), reportNames(report.rowType())); if (report.reportType() == eMyMoney::Report::ReportType::PivotTable) { // write report's internals el.setAttribute(attributeName(Attribute::Report::IncludesActuals), report.isIncludingBudgetActuals()); el.setAttribute(attributeName(Attribute::Report::IncludesForecast), report.isIncludingForecast()); el.setAttribute(attributeName(Attribute::Report::IncludesPrice), report.isIncludingPrice()); el.setAttribute(attributeName(Attribute::Report::IncludesAveragePrice), report.isIncludingAveragePrice()); el.setAttribute(attributeName(Attribute::Report::MixedTime), report.isMixedTime()); el.setAttribute(attributeName(Attribute::Report::Investments), report.isInvestmentsOnly()); // it's setable in rows/columns tab of querytable, but here it is internal setting // write rows/columns tab if (!report.budget().isEmpty()) el.setAttribute(attributeName(Attribute::Report::Budget), report.budget()); el.setAttribute(attributeName(Attribute::Report::ShowRowTotals), report.isShowingRowTotals()); el.setAttribute(attributeName(Attribute::Report::ShowColumnTotals), report.isShowingColumnTotals()); el.setAttribute(attributeName(Attribute::Report::Detail), reportNames(report.detailLevel())); el.setAttribute(attributeName(Attribute::Report::IncludesMovingAverage), report.isIncludingMovingAverage()); if (report.isIncludingMovingAverage()) el.setAttribute(attributeName(Attribute::Report::MovingAverageDays), report.movingAverageDays()); el.setAttribute(attributeName(Attribute::Report::IncludesSchedules), report.isIncludingSchedules()); el.setAttribute(attributeName(Attribute::Report::IncludesTransfers), report.isIncludingTransfers()); el.setAttribute(attributeName(Attribute::Report::IncludesUnused), report.isIncludingUnusedAccounts()); el.setAttribute(attributeName(Attribute::Report::ColumnsAreDays), report.isColumnsAreDays()); el.setAttribute(attributeName(Attribute::Report::ChartType), reportNames(report.chartType())); el.setAttribute(attributeName(Attribute::Report::ChartCHGridLines), report.isChartCHGridLines()); el.setAttribute(attributeName(Attribute::Report::ChartSVGridLines), report.isChartSVGridLines()); el.setAttribute(attributeName(Attribute::Report::ChartDataLabels), report.isChartDataLabels()); el.setAttribute(attributeName(Attribute::Report::ChartByDefault), report.isChartByDefault()); el.setAttribute(attributeName(Attribute::Report::LogYAxis), report.isLogYAxis()); el.setAttribute(attributeName(Attribute::Report::NegExpenses), report.isNegExpenses()); el.setAttribute(attributeName(Attribute::Report::ChartLineWidth), report.chartLineWidth()); el.setAttribute(attributeName(Attribute::Report::ColumnType), reportNames(report.columnType())); el.setAttribute(attributeName(Attribute::Report::DataLock), reportNames(report.dataFilter())); el.setAttribute(attributeName(Attribute::Report::DataRangeStart), report.dataRangeStart()); el.setAttribute(attributeName(Attribute::Report::DataRangeEnd), report.dataRangeEnd()); el.setAttribute(attributeName(Attribute::Report::DataMajorTick), report.dataMajorTick()); el.setAttribute(attributeName(Attribute::Report::DataMinorTick), report.dataMinorTick()); el.setAttribute(attributeName(Attribute::Report::YLabelsPrecision), report.yLabelsPrecision()); } else if (report.reportType() == eMyMoney::Report::ReportType::QueryTable) { // write rows/columns tab QStringList columns; unsigned qc = report.queryColumns(); unsigned it_qc = eMyMoney::Report::QueryColumn::Begin; unsigned index = 1; while (it_qc != eMyMoney::Report::QueryColumn::End) { if (qc & it_qc) columns += reportNamesForQC(static_cast(it_qc)); it_qc *= 2; index++; } el.setAttribute(attributeName(Attribute::Report::QueryColumns), columns.join(",")); el.setAttribute(attributeName(Attribute::Report::Tax), report.isTax()); el.setAttribute(attributeName(Attribute::Report::Investments), report.isInvestmentsOnly()); el.setAttribute(attributeName(Attribute::Report::Loans), report.isLoansOnly()); el.setAttribute(attributeName(Attribute::Report::HideTransactions), report.isHideTransactions()); el.setAttribute(attributeName(Attribute::Report::ShowColumnTotals), report.isShowingColumnTotals()); el.setAttribute(attributeName(Attribute::Report::Detail), reportNames(report.detailLevel())); // write performance tab if (report.queryColumns() & eMyMoney::Report::QueryColumn::Performance || report.queryColumns() & eMyMoney::Report::QueryColumn::CapitalGain) el.setAttribute(attributeName(Attribute::Report::InvestmentSum), static_cast(report.investmentSum())); // write capital gains tab if (report.queryColumns() & eMyMoney::Report::QueryColumn::CapitalGain) { if (report.investmentSum() == eMyMoney::Report::InvestmentSum::Sold) { el.setAttribute(attributeName(Attribute::Report::SettlementPeriod), report.settlementPeriod()); el.setAttribute(attributeName(Attribute::Report::ShowSTLTCapitalGains), report.isShowingSTLTCapitalGains()); el.setAttribute(attributeName(Attribute::Report::TermsSeparator), report.termSeparator().toString(Qt::ISODate)); } } } else if (report.reportType() == eMyMoney::Report::ReportType::InfoTable) el.setAttribute(attributeName(Attribute::Report::ShowRowTotals), report.isShowingRowTotals()); // // Text Filter // QRegExp textfilter; if (report.textFilter(textfilter)) { QDomElement f = document.createElement(elementName(Element::Report::Text)); f.setAttribute(attributeName(Attribute::Report::Pattern), textfilter.pattern()); f.setAttribute(attributeName(Attribute::Report::CaseSensitive), (textfilter.caseSensitivity() == Qt::CaseSensitive) ? 1 : 0); f.setAttribute(attributeName(Attribute::Report::RegEx), (textfilter.patternSyntax() == QRegExp::Wildcard) ? 1 : 0); f.setAttribute(attributeName(Attribute::Report::InvertText), report.MyMoneyTransactionFilter::isInvertingText()); el.appendChild(f); } // // Type & State Filters // QList typelist; if (report.types(typelist) && ! typelist.empty()) { // iterate over payees, and add each one QList::const_iterator it_type = typelist.constBegin(); while (it_type != typelist.constEnd()) { QDomElement p = document.createElement(elementName(Element::Report::Type)); p.setAttribute(attributeName(Attribute::Report::Type), typeAttributeToString(*it_type)); el.appendChild(p); ++it_type; } } QList statelist; if (report.states(statelist) && ! statelist.empty()) { // iterate over payees, and add each one QList::const_iterator it_state = statelist.constBegin(); while (it_state != statelist.constEnd()) { QDomElement p = document.createElement(elementName(Element::Report::State)); p.setAttribute(attributeName(Attribute::Report::State), stateAttributeToString(*it_state)); el.appendChild(p); ++it_state; } } QList validitylist; if (report.validities(validitylist) && ! validitylist.empty()) { // iterate over payees, and add each one QList::const_iterator it_validity = validitylist.constBegin(); while (it_validity != validitylist.constEnd()) { QDomElement p = document.createElement(elementName(Element::Report::Validity)); p.setAttribute(attributeName(Attribute::Report::Validity), validityAttributeToString(*it_validity)); el.appendChild(p); ++it_validity; } } // // Number Filter // QString nrFrom, nrTo; if (report.numberFilter(nrFrom, nrTo)) { QDomElement f = document.createElement(elementName(Element::Report::Number)); f.setAttribute(attributeName(Attribute::Report::From), nrFrom); f.setAttribute(attributeName(Attribute::Report::To), nrTo); el.appendChild(f); } // // Amount Filter // MyMoneyMoney from, to; if (report.amountFilter(from, to)) { // bool getAmountFilter(MyMoneyMoney&,MyMoneyMoney&); QDomElement f = document.createElement(elementName(Element::Report::Amount)); f.setAttribute(attributeName(Attribute::Report::From), from.toString()); f.setAttribute(attributeName(Attribute::Report::To), to.toString()); el.appendChild(f); } // // Payees Filter // QStringList payeelist; if (report.payees(payeelist)) { if (payeelist.empty()) { QDomElement p = document.createElement(elementName(Element::Report::Payee)); el.appendChild(p); } else { // iterate over payees, and add each one QStringList::const_iterator it_payee = payeelist.constBegin(); while (it_payee != payeelist.constEnd()) { QDomElement p = document.createElement(elementName(Element::Report::Payee)); p.setAttribute(attributeName(Attribute::Report::ID), *it_payee); el.appendChild(p); ++it_payee; } } } // // Tags Filter // QStringList taglist; if (report.tags(taglist)) { if (taglist.empty()) { QDomElement p = document.createElement(elementName(Element::Report::Tag)); el.appendChild(p); } else { // iterate over tags, and add each one QStringList::const_iterator it_tag = taglist.constBegin(); while (it_tag != taglist.constEnd()) { QDomElement p = document.createElement(elementName(Element::Report::Tag)); p.setAttribute(attributeName(Attribute::Report::ID), *it_tag); el.appendChild(p); ++it_tag; } } } // // Account Groups Filter // QList accountgrouplist; if (report.accountGroups(accountgrouplist)) { // iterate over accounts, and add each one QList::const_iterator it_group = accountgrouplist.constBegin(); while (it_group != accountgrouplist.constEnd()) { QDomElement p = document.createElement(elementName(Element::Report::AccountGroup)); p.setAttribute(attributeName(Attribute::Report::Group), accountTypeAttributeToString(*it_group)); el.appendChild(p); ++it_group; } } // // Accounts Filter // QStringList accountlist; if (report.accounts(accountlist)) { // iterate over accounts, and add each one QStringList::const_iterator it_account = accountlist.constBegin(); while (it_account != accountlist.constEnd()) { QDomElement p = document.createElement(elementName(Element::Report::Account)); p.setAttribute(attributeName(Attribute::Report::ID), *it_account); el.appendChild(p); ++it_account; } } // // Categories Filter // accountlist.clear(); if (report.categories(accountlist)) { // iterate over accounts, and add each one QStringList::const_iterator it_account = accountlist.constBegin(); while (it_account != accountlist.constEnd()) { QDomElement p = document.createElement(elementName(Element::Report::Category)); p.setAttribute(attributeName(Attribute::Report::ID), *it_account); el.appendChild(p); ++it_account; } } // // Date Filter // if (report.dateRange() == eMyMoney::TransactionFilter::Date::UserDefined) { QDate dateFrom, dateTo; if (report.dateFilter(dateFrom, dateTo)) { QDomElement f = document.createElement(elementName(Element::Report::Dates)); if (dateFrom.isValid()) f.setAttribute(attributeName(Attribute::Report::From), dateFrom.toString(Qt::ISODate)); if (dateTo.isValid()) f.setAttribute(attributeName(Attribute::Report::To), dateTo.toString(Qt::ISODate)); el.appendChild(f); } } parent.appendChild(el); } MyMoneyBudget readBudget(const QDomElement &node) { if (nodeName(Node::Budget) != node.tagName()) throw MYMONEYEXCEPTION_CSTRING("Node was not BUDGET"); MyMoneyBudget budget(node.attribute(QStringLiteral("id"))); // The goal of this reading method is 100% backward AND 100% forward // compatibility. Any Budget ever created with any version of KMyMoney // should be able to be loaded by this method (as long as it's one of the // Budget types supported in this version, of course) budget.setName(node.attribute(attributeName(Attribute::Budget::Name))); budget.setBudgetStart(QDate::fromString(node.attribute(attributeName(Attribute::Budget::Start)), Qt::ISODate)); QDomNode child = node.firstChild(); while (!child.isNull() && child.isElement()) { QDomElement c = child.toElement(); MyMoneyBudget::AccountGroup account; if (elementName(Element::Budget::Account) == c.tagName()) { if (c.hasAttribute(attributeName(Attribute::Budget::ID))) account.setId(c.attribute(attributeName(Attribute::Budget::ID))); if (c.hasAttribute(attributeName(Attribute::Budget::BudgetLevel))) account.setBudgetLevel(stringToBudgetLevel(c.attribute(attributeName(Attribute::Budget::BudgetLevel)))); if (c.hasAttribute(attributeName(Attribute::Budget::BudgetSubAccounts))) account.setBudgetSubaccounts(c.attribute(attributeName(Attribute::Budget::BudgetSubAccounts)).toUInt()); } QDomNode period = c.firstChild(); while (!period.isNull() && period.isElement()) { QDomElement per = period.toElement(); MyMoneyBudget::PeriodGroup pGroup; if (elementName(Element::Budget::Period) == per.tagName() && per.hasAttribute(attributeName(Attribute::Budget::Amount)) && per.hasAttribute(attributeName(Attribute::Budget::Start))) { pGroup.setAmount(MyMoneyMoney(per.attribute(attributeName(Attribute::Budget::Amount)))); pGroup.setStartDate(QDate::fromString(per.attribute(attributeName(Attribute::Budget::Start)), Qt::ISODate)); account.addPeriod(pGroup.startDate(), pGroup); } period = period.nextSibling(); } budget.setAccount(account, account.id()); child = child.nextSibling(); } return budget; } const int BUDGET_VERSION = 2; void writeBudget(const MyMoneyBudget &budget, QDomDocument &document, QDomElement &parent) { auto el = document.createElement(nodeName(Node::Budget)); writeBaseXML(budget.id(), document, el); el.setAttribute(attributeName(Attribute::Budget::Name), budget.name()); el.setAttribute(attributeName(Attribute::Budget::Start), budget.budgetStart().toString(Qt::ISODate)); el.setAttribute(attributeName(Attribute::Budget::Version), BUDGET_VERSION); QMap::const_iterator it; auto accounts = budget.accountsMap(); for (it = accounts.cbegin(); it != accounts.cend(); ++it) { // only add the account if there is a budget entered // or it covers some sub accounts if (!(*it).balance().isZero() || (*it).budgetSubaccounts()) { QDomElement domAccount = document.createElement(elementName(Element::Budget::Account)); domAccount.setAttribute(attributeName(Attribute::Budget::ID), it.key()); domAccount.setAttribute(attributeName(Attribute::Budget::BudgetLevel), budgetLevels(it.value().budgetLevel())); domAccount.setAttribute(attributeName(Attribute::Budget::BudgetSubAccounts), it.value().budgetSubaccounts()); const QMap periods = it.value().getPeriods(); QMap::const_iterator it_per; for (it_per = periods.begin(); it_per != periods.end(); ++it_per) { if (!(*it_per).amount().isZero()) { QDomElement domPeriod = document.createElement(elementName(Element::Budget::Period)); domPeriod.setAttribute(attributeName(Attribute::Budget::Amount), (*it_per).amount().toString()); domPeriod.setAttribute(attributeName(Attribute::Budget::Start), (*it_per).startDate().toString(Qt::ISODate)); domAccount.appendChild(domPeriod); } } el.appendChild(domAccount); } } parent.appendChild(el); } } diff --git a/kmymoney/settings/kmymoneysettings_addons.cpp b/kmymoney/settings/kmymoneysettings_addons.cpp index 788b5029a..a3bbbe454 100644 --- a/kmymoney/settings/kmymoneysettings_addons.cpp +++ b/kmymoney/settings/kmymoneysettings_addons.cpp @@ -1,144 +1,143 @@ /*************************************************************************** kmymoneysettings_addons.cpp ------------------- copyright : (C) 2018 by Thomas Baumgart email : tbaumgart@kde.org ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ // ---------------------------------------------------------------------------- // QT Includes #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes QFont KMyMoneySettings::listCellFontEx() { if (useSystemFont()) { return QFontDatabase::systemFont(QFontDatabase::GeneralFont); } else { return listCellFont(); } } QFont KMyMoneySettings::listHeaderFontEx() { if (useSystemFont()) { QFont font = QFontDatabase::systemFont(QFontDatabase::GeneralFont); font.setBold(true); return font; } else { return listHeaderFont(); } } QColor KMyMoneySettings::schemeColor(const SchemeColor color) { switch(color) { case SchemeColor::ListBackground1: return KColorScheme (QPalette::Active, KColorScheme::View).background(KColorScheme::NormalBackground).color(); case SchemeColor::ListBackground2: return KColorScheme (QPalette::Active, KColorScheme::View).background(KColorScheme::AlternateBackground).color(); case SchemeColor::ListGrid: return KColorScheme (QPalette::Active, KColorScheme::View).foreground(KColorScheme::InactiveText).color(); case SchemeColor::ListHighlightText: return KColorScheme (QPalette::Active, KColorScheme::Selection).foreground(KColorScheme::NormalText).color(); case SchemeColor::ListHighlight: return KColorScheme (QPalette::Active, KColorScheme::Selection).background(KColorScheme::NormalBackground).color(); case SchemeColor::WindowText: return KColorScheme (QPalette::Active, KColorScheme::Window).foreground(KColorScheme::NormalText).color(); case SchemeColor::WindowBackground: return KColorScheme (QPalette::Active, KColorScheme::Window).background(KColorScheme::NormalBackground).color(); case SchemeColor::Positive: return KColorScheme (QPalette::Active, KColorScheme::View).foreground(KColorScheme::PositiveText).color(); case SchemeColor::Negative: return KColorScheme (QPalette::Active, KColorScheme::View).foreground(KColorScheme::NegativeText).color(); case SchemeColor::TransactionImported: if (useCustomColors()) return transactionImportedColor(); else return KColorScheme (QPalette::Active, KColorScheme::View).background(KColorScheme::LinkBackground).color(); case SchemeColor::TransactionMatched: if (useCustomColors()) return transactionMatchedColor(); else return KColorScheme (QPalette::Active, KColorScheme::View).background(KColorScheme::LinkBackground).color(); case SchemeColor::TransactionErroneous: if (useCustomColors()) return transactionErroneousColor(); else return KColorScheme (QPalette::Active, KColorScheme::View).foreground(KColorScheme::NegativeText).color(); case SchemeColor::FieldRequired: if (useCustomColors()) return fieldRequiredColor(); else return KColorScheme (QPalette::Active, KColorScheme::View).background(KColorScheme::NeutralBackground).color(); case SchemeColor::GroupMarker: if (useCustomColors()) return groupMarkerColor(); else return KColorScheme (QPalette::Active, KColorScheme::Selection).background(KColorScheme::LinkBackground).color(); case SchemeColor::MissingConversionRate: if (useCustomColors()) return missingConversionRateColor(); else return KColorScheme (QPalette::Active, KColorScheme::Complementary).foreground(KColorScheme::LinkText).color(); default: return QColor(); } } QStringList KMyMoneySettings::listOfItems() { bool prevValue = self()->useDefaults(true); QStringList all = itemList().split(',', QString::SkipEmptyParts); self()->useDefaults(prevValue); QStringList list = itemList().split(',', QString::SkipEmptyParts); // now add all from 'all' that are missing in 'list' QRegExp exp("-?(\\d+)"); QStringList::iterator it_s; for (it_s = all.begin(); it_s != all.end(); ++it_s) { - exp.indexIn(*it_s); - if (!list.contains(exp.cap(1)) && !list.contains(QString("-%1").arg(exp.cap(1)))) { + if ((exp.indexIn(*it_s) != -1) && !list.contains(exp.cap(1)) && !list.contains(QString("-%1").arg(exp.cap(1)))) { list << *it_s; } } return list; } int KMyMoneySettings::firstFiscalMonth() { return fiscalYearBegin() + 1; } int KMyMoneySettings::firstFiscalDay() { return fiscalYearBeginDay(); } QDate KMyMoneySettings::firstFiscalDate() { QDate date = QDate(QDate::currentDate().year(), firstFiscalMonth(), firstFiscalDay()); if (date > QDate::currentDate()) date = date.addYears(-1); return date; } diff --git a/kmymoney/views/kaccountsview.cpp b/kmymoney/views/kaccountsview.cpp index c2954cd4c..2cff8bfae 100644 --- a/kmymoney/views/kaccountsview.cpp +++ b/kmymoney/views/kaccountsview.cpp @@ -1,538 +1,542 @@ /*************************************************************************** kaccountsview.cpp ------------------- copyright : (C) 2007 by 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 "kaccountsview_p.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include // ---------------------------------------------------------------------------- // Project Includes #include "onlinejobadministration.h" #include "knewaccountwizard.h" #include "kmymoneyutils.h" #include "kmymoneysettings.h" #include "storageenums.h" #include "menuenums.h" using namespace Icons; KAccountsView::KAccountsView(QWidget *parent) : KMyMoneyAccountsViewBase(*new KAccountsViewPrivate(this), parent) { Q_D(KAccountsView); d->ui->setupUi(this); connect(pActions[eMenu::Action::NewAccount], &QAction::triggered, this, &KAccountsView::slotNewAccount); connect(pActions[eMenu::Action::EditAccount], &QAction::triggered, this, &KAccountsView::slotEditAccount); connect(pActions[eMenu::Action::DeleteAccount], &QAction::triggered, this, &KAccountsView::slotDeleteAccount); connect(pActions[eMenu::Action::CloseAccount], &QAction::triggered, this, &KAccountsView::slotCloseAccount); connect(pActions[eMenu::Action::ReopenAccount], &QAction::triggered, this, &KAccountsView::slotReopenAccount); connect(pActions[eMenu::Action::ChartAccountBalance], &QAction::triggered, this, &KAccountsView::slotChartAccountBalance); connect(pActions[eMenu::Action::MapOnlineAccount], &QAction::triggered, this, &KAccountsView::slotAccountMapOnline); connect(pActions[eMenu::Action::UnmapOnlineAccount], &QAction::triggered, this, &KAccountsView::slotAccountUnmapOnline); connect(pActions[eMenu::Action::UpdateAccount], &QAction::triggered, this, &KAccountsView::slotAccountUpdateOnline); connect(pActions[eMenu::Action::UpdateAllAccounts], &QAction::triggered, this, &KAccountsView::slotAccountUpdateOnlineAll); } KAccountsView::~KAccountsView() { } void KAccountsView::executeCustomAction(eView::Action action) { switch(action) { case eView::Action::Refresh: refresh(); break; case eView::Action::SetDefaultFocus: { Q_D(KAccountsView); QTimer::singleShot(0, d->ui->m_accountTree, SLOT(setFocus())); } break; default: break; } } void KAccountsView::refresh() { Q_D(KAccountsView); if (!isVisible()) { d->m_needsRefresh = true; return; } d->m_needsRefresh = false; // TODO: check why the invalidate is needed here d->m_proxyModel->invalidate(); d->m_proxyModel->setHideClosedAccounts(KMyMoneySettings::hideClosedAccounts()); d->m_proxyModel->setHideEquityAccounts(!KMyMoneySettings::expertMode()); if (KMyMoneySettings::showCategoriesInAccountsView()) { d->m_proxyModel->addAccountGroup(QVector {eMyMoney::Account::Type::Income, eMyMoney::Account::Type::Expense}); } else { d->m_proxyModel->removeAccountType(eMyMoney::Account::Type::Income); d->m_proxyModel->removeAccountType(eMyMoney::Account::Type::Expense); } // reinitialize the default state of the hidden categories label d->m_haveUnusedCategories = false; d->ui->m_hiddenCategories->hide(); // hides label d->m_proxyModel->setHideUnusedIncomeExpenseAccounts(KMyMoneySettings::hideUnusedCategory()); } void KAccountsView::showEvent(QShowEvent * event) { Q_D(KAccountsView); if (!d->m_proxyModel) d->init(); emit customActionRequested(View::Accounts, eView::Action::AboutToShow); if (d->m_needsRefresh) refresh(); // don't forget base class implementation QWidget::showEvent(event); } void KAccountsView::updateActions(const MyMoneyObject& obj) { Q_D(KAccountsView); const auto file = MyMoneyFile::instance(); if (typeid(obj) != typeid(MyMoneyAccount) && (obj.id().isEmpty() && d->m_currentAccount.id().isEmpty())) // do not disable actions that were already disabled) return; const auto& acc = static_cast(obj); const QVector actionsToBeDisabled { eMenu::Action::NewAccount, eMenu::Action::EditAccount, eMenu::Action::DeleteAccount, eMenu::Action::CloseAccount, eMenu::Action::ReopenAccount, eMenu::Action::ChartAccountBalance, eMenu::Action::UnmapOnlineAccount, eMenu::Action::MapOnlineAccount, eMenu::Action::UpdateAccount }; for (const auto& a : actionsToBeDisabled) pActions[a]->setEnabled(false); pActions[eMenu::Action::NewAccount]->setEnabled(true); pActions[eMenu::Action::UpdateAllAccounts]->setEnabled(KMyMoneyUtils::canUpdateAllAccounts()); if (acc.id().isEmpty()) { d->m_currentAccount = MyMoneyAccount(); return; } else if (file->isStandardAccount(acc.id())) { d->m_currentAccount = acc; return; } d->m_currentAccount = acc; switch (acc.accountGroup()) { case eMyMoney::Account::Type::Asset: case eMyMoney::Account::Type::Liability: case eMyMoney::Account::Type::Equity: { pActions[eMenu::Action::EditAccount]->setEnabled(true); pActions[eMenu::Action::DeleteAccount]->setEnabled(!file->isReferenced(acc)); auto b = acc.isClosed() ? true : false; pActions[eMenu::Action::ReopenAccount]->setEnabled(b); pActions[eMenu::Action::CloseAccount]->setEnabled(!b); if (!acc.isClosed()) { b = (d->canCloseAccount(acc) == KAccountsViewPrivate::AccountCanClose) ? true : false; pActions[eMenu::Action::CloseAccount]->setEnabled(b); d->hintCloseAccountAction(acc, pActions[eMenu::Action::CloseAccount]); } pActions[eMenu::Action::ChartAccountBalance]->setEnabled(true); if (d->m_currentAccount.hasOnlineMapping()) { pActions[eMenu::Action::UnmapOnlineAccount]->setEnabled(true); if (d->m_onlinePlugins) { // check if provider is available QMap::const_iterator it_p; it_p = d->m_onlinePlugins->constFind(d->m_currentAccount.onlineBankingSettings().value(QLatin1String("provider")).toLower()); if (it_p != d->m_onlinePlugins->constEnd()) { QStringList protocols; (*it_p)->protocols(protocols); if (protocols.count() > 0) { pActions[eMenu::Action::UpdateAccount]->setEnabled(true); } } } } else { pActions[eMenu::Action::MapOnlineAccount]->setEnabled(!acc.isClosed() && d->m_onlinePlugins && !d->m_onlinePlugins->isEmpty()); } break; } default: break; } +#if 0 + // It is unclear to me, what the following code should do + // as it just does nothing QBitArray skip((int)eStorage::Reference::Count); if (!d->m_currentAccount.id().isEmpty()) { if (!file->isStandardAccount(d->m_currentAccount.id())) { switch (d->m_currentAccount.accountGroup()) { case eMyMoney::Account::Type::Asset: case eMyMoney::Account::Type::Liability: case eMyMoney::Account::Type::Equity: break; default: break; } } } +#endif } /** * The view is notified that an unused income expense account has been hidden. */ void KAccountsView::slotUnusedIncomeExpenseAccountHidden() { Q_D(KAccountsView); d->m_haveUnusedCategories = true; d->ui->m_hiddenCategories->setVisible(d->m_haveUnusedCategories); } void KAccountsView::slotNetWorthChanged(const MyMoneyMoney &netWorth) { Q_D(KAccountsView); d->netBalProChanged(netWorth, d->ui->m_totalProfitsLabel, View::Accounts); } void KAccountsView::slotShowAccountMenu(const MyMoneyAccount& acc) { Q_UNUSED(acc); pMenus[eMenu::Menu::Account]->exec(QCursor::pos()); } void KAccountsView::slotSelectByObject(const MyMoneyObject& obj, eView::Intent intent) { switch(intent) { case eView::Intent::UpdateActions: updateActions(obj); break; case eView::Intent::OpenContextMenu: slotShowAccountMenu(static_cast(obj)); break; default: break; } } void KAccountsView::slotSelectByVariant(const QVariantList& variant, eView::Intent intent) { Q_D(KAccountsView); switch (intent) { case eView::Intent::UpdateNetWorth: if (variant.count() == 1) slotNetWorthChanged(variant.first().value()); break; case eView::Intent::SetOnlinePlugins: if (variant.count() == 1) d->m_onlinePlugins = static_cast*>(variant.first().value()); break; default: break; } } void KAccountsView::slotNewAccount() { MyMoneyAccount account; account.setOpeningDate(KMyMoneySettings::firstFiscalDate()); NewAccountWizard::Wizard::newAccount(account); } void KAccountsView::slotEditAccount() { Q_D(KAccountsView); switch (d->m_currentAccount.accountType()) { case eMyMoney::Account::Type::Loan: case eMyMoney::Account::Type::AssetLoan: d->editLoan(); break; default: d->editAccount(); break; } emit selectByObject(d->m_currentAccount, eView::Intent::None); } void KAccountsView::slotDeleteAccount() { Q_D(KAccountsView); if (d->m_currentAccount.id().isEmpty()) return; // need an account ID const auto file = MyMoneyFile::instance(); // can't delete standard accounts or account which still have transactions assigned if (file->isStandardAccount(d->m_currentAccount.id())) return; // check if the account is referenced by a transaction or schedule QBitArray skip((int)eStorage::Reference::Count); skip.fill(false); skip.setBit((int)eStorage::Reference::Account); skip.setBit((int)eStorage::Reference::Institution); skip.setBit((int)eStorage::Reference::Payee); skip.setBit((int)eStorage::Reference::Tag); skip.setBit((int)eStorage::Reference::Security); skip.setBit((int)eStorage::Reference::Currency); skip.setBit((int)eStorage::Reference::Price); if (file->isReferenced(d->m_currentAccount, skip)) return; MyMoneyFileTransaction ft; // retain the account name for a possible later usage in the error message box // since the account removal notifies the views the selected account can be changed // so we make sure by doing this that we display the correct name in the error message auto selectedAccountName = d->m_currentAccount.name(); try { file->removeAccount(d->m_currentAccount); d->m_currentAccount.clearId(); emit selectByObject(MyMoneyAccount(), eView::Intent::None); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::error(this, i18n("Unable to delete account '%1'. Cause: %2", selectedAccountName, QString::fromLatin1(e.what()))); } } void KAccountsView::slotCloseAccount() { Q_D(KAccountsView); MyMoneyFileTransaction ft; try { d->m_currentAccount.setClosed(true); MyMoneyFile::instance()->modifyAccount(d->m_currentAccount); emit selectByObject(d->m_currentAccount, eView::Intent::None); ft.commit(); if (KMyMoneySettings::hideClosedAccounts()) KMessageBox::information(this, i18n("You have closed this account. It remains in the system because you have transactions which still refer to it, but it is not shown in the views. You can make it visible again by going to the View menu and selecting Show all accounts or by deselecting the Do not show closed accounts setting."), i18n("Information"), "CloseAccountInfo"); } catch (const MyMoneyException &) { } } void KAccountsView::slotReopenAccount() { Q_D(KAccountsView); const auto file = MyMoneyFile::instance(); MyMoneyFileTransaction ft; try { auto& acc = d->m_currentAccount; while (acc.isClosed()) { acc.setClosed(false); file->modifyAccount(acc); acc = file->account(acc.parentAccountId()); } emit selectByObject(d->m_currentAccount, eView::Intent::None); ft.commit(); } catch (const MyMoneyException &) { } } void KAccountsView::slotChartAccountBalance() { Q_D(KAccountsView); if (!d->m_currentAccount.id().isEmpty()) { emit customActionRequested(View::Accounts, eView::Action::ShowBalanceChart); } } void KAccountsView::slotNewCategory() { Q_D(KAccountsView); KNewAccountDlg::newCategory(d->m_currentAccount, MyMoneyAccount()); } void KAccountsView::slotNewPayee(const QString& nameBase, QString& id) { KMyMoneyUtils::newPayee(nameBase, id); } void KAccountsView::slotAccountUnmapOnline() { Q_D(KAccountsView); // no account selected if (d->m_currentAccount.id().isEmpty()) return; // not a mapped account if (!d->m_currentAccount.hasOnlineMapping()) return; if (KMessageBox::warningYesNo(this, QString("%1").arg(i18n("Do you really want to remove the mapping of account %1 to an online account? Depending on the details of the online banking method used, this action cannot be reverted.", d->m_currentAccount.name())), i18n("Remove mapping to online account")) == KMessageBox::Yes) { MyMoneyFileTransaction ft; try { d->m_currentAccount.setOnlineBankingSettings(MyMoneyKeyValueContainer()); // Avoid showing an oline balance d->m_currentAccount.deletePair(QStringLiteral("lastStatementBalance")); // delete the kvp that is used in MyMoneyStatementReader too // we should really get rid of it, but since I don't know what it // is good for, I'll keep it around. (ipwizard) d->m_currentAccount.deletePair(QStringLiteral("StatementKey")); MyMoneyFile::instance()->modifyAccount(d->m_currentAccount); ft.commit(); // The mapping could disable the online task system onlineJobAdministration::instance()->updateOnlineTaskProperties(); } catch (const MyMoneyException &e) { KMessageBox::error(this, i18n("Unable to unmap account from online account: %1", QString::fromLatin1(e.what()))); } } updateActions(d->m_currentAccount); } void KAccountsView::slotAccountMapOnline() { Q_D(KAccountsView); // no account selected if (d->m_currentAccount.id().isEmpty()) return; // already an account mapped if (d->m_currentAccount.hasOnlineMapping()) return; // check if user tries to map a brokerageAccount if (d->m_currentAccount.name().contains(i18n(" (Brokerage)"))) { if (KMessageBox::warningContinueCancel(this, i18n("You try to map a brokerage account to an online account. This is usually not advisable. In general, the investment account should be mapped to the online account. Please cancel if you intended to map the investment account, continue otherwise"), i18n("Mapping brokerage account")) == KMessageBox::Cancel) { return; } } if (!d->m_onlinePlugins) return; // if we have more than one provider let the user select the current provider QString provider; QMap::const_iterator it_p; switch (d->m_onlinePlugins->count()) { case 0: break; case 1: provider = d->m_onlinePlugins->begin().key(); break; default: { QMenu popup(this); popup.setTitle(i18n("Select online banking plugin")); // Populate the pick list with all the provider for (it_p = d->m_onlinePlugins->constBegin(); it_p != d->m_onlinePlugins->constEnd(); ++it_p) { popup.addAction(it_p.key())->setData(it_p.key()); } QAction *item = popup.actions()[0]; if (item) { popup.setActiveAction(item); } // cancelled if ((item = popup.exec(QCursor::pos(), item)) == 0) { return; } provider = item->data().toString(); } break; } if (provider.isEmpty()) return; // find the provider it_p = d->m_onlinePlugins->constFind(provider.toLower()); if (it_p != d->m_onlinePlugins->constEnd()) { // plugin found, call it MyMoneyKeyValueContainer settings; if ((*it_p)->mapAccount(d->m_currentAccount, settings)) { settings["provider"] = provider.toLower(); MyMoneyAccount acc(d->m_currentAccount); acc.setOnlineBankingSettings(settings); MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->modifyAccount(acc); ft.commit(); // The mapping could enable the online task system onlineJobAdministration::instance()->updateOnlineTaskProperties(); } catch (const MyMoneyException &e) { KMessageBox::error(this, i18n("Unable to map account to online account: %1", QString::fromLatin1(e.what()))); } } } updateActions(d->m_currentAccount); } void KAccountsView::slotAccountUpdateOnlineAll() { Q_D(KAccountsView); QList accList; MyMoneyFile::instance()->accountList(accList); QList mappedAccList; Q_FOREACH(auto account, accList) { if (account.hasOnlineMapping()) mappedAccList += account; } d->accountsUpdateOnline(mappedAccList); } void KAccountsView::slotAccountUpdateOnline() { Q_D(KAccountsView); // no account selected if (d->m_currentAccount.id().isEmpty()) return; // no online account mapped if (!d->m_currentAccount.hasOnlineMapping()) return; d->accountsUpdateOnline(QList { d->m_currentAccount } ); } diff --git a/kmymoney/views/khomeview_p.h b/kmymoney/views/khomeview_p.h index fef8682eb..afa268a58 100644 --- a/kmymoney/views/khomeview_p.h +++ b/kmymoney/views/khomeview_p.h @@ -1,1855 +1,1855 @@ /*************************************************************************** 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 #include #ifdef ENABLE_WEBENGINE #include #else #include #include #endif // ---------------------------------------------------------------------------- // KDE Includes #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 "mymoneyreport.h" #include "mymoneymoney.h" #include "mymoneyforecast.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "icons.h" #include "kmymoneywebpage.h" #include "mymoneyschedule.h" #include "mymoneysecurity.h" #include "mymoneyexception.h" #include "kmymoneyplugin.h" #include "mymoneyenums.h" #include "menuenums.h" #include "plugins/views/reports/reportsviewenums.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; } 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) + m_currentPrinter(nullptr), + m_scrollBarPos(0) { } ~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); #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; + QString cellStatus, 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 = m_transactionStats[acc.id()][(int)Split::State::NotReconciled]; if (KMyMoneySettings::showCountOfClearedTransactions() || KMyMoneySettings::showCountOfNotReconciledTransactions()) countCleared = m_transactionStats[acc.id()][(int)Split::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).replace(QChar(' '), " "); 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) { const auto file = MyMoneyFile::instance(); 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()) { const auto curPrice = file->price(acc.tradingCurrencyId(), file->baseCurrency().id(), QDate::currentDate()); const auto curRate = curPrice.rate(file->baseCurrency().id()); auto baseValue = value * curRate; 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() { Q_Q(KHomeView); m_view->setZoomFactor(KMyMoneySettings::zoomFactor()); QList list; if (MyMoneyFile::instance()->storage()) { MyMoneyFile::instance()->accountList(list); } if (list.isEmpty()) { m_view->setHtml(KWelcomePage::welcomePage(), QUrl("file://")); } else { // preload transaction statistics m_transactionStats = MyMoneyFile::instance()->countTransactionsWithSpecificReconciliationState(); // keep current location on page m_scrollBarPos = 0; #ifndef ENABLE_WEBENGINE m_scrollBarPos = m_view->page()->mainFrame()->scrollBarValue(Qt::Vertical); #endif //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://")); #ifndef ENABLE_WEBENGINE if (m_scrollBarPos) { QMetaObject::invokeMethod(q, "slotAdjustScrollPos", Qt::QueuedConnection); } #endif } } void showNetWorthGraph() { Q_Q(KHomeView); // Adjust the size QSize netWorthGraphSize = q->size(); netWorthGraphSize -= QSize(80, 30); m_netWorthGraphLastValidSize = netWorthGraphSize; m_html += QString("
%1
\n
 
\n").arg(i18n("Net Worth Forecast")); m_html += QString(""); m_html += QString(""); if (const auto reportsPlugin = pPlugins.data.value(QStringLiteral("reportsview"), nullptr)) { const auto variantReport = reportsPlugin->requestData(QString(), eWidgetPlugin::WidgetType::NetWorthForecast); if (!variantReport.isNull()) { auto report = variantReport.value(); report->resize(m_netWorthGraphLastValidSize); m_html += QString("").arg(QPixmapToDataUri(report->grab())); delete report; } } else { m_html += QString("").arg(i18n("Enable reports plugin to see this chart.")); } m_html += QString(""); m_html += QString("
\"Networth\"
%1
"); } 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", 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); auto i = 0; auto colspan = 1; //get begin day auto 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 += ""; auto 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(""; qint64 dropZero = -1; //account dropped below zero qint64 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 (auto 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()) { const auto curPrice = file->price((*asset_it).tradingCurrencyId(), file->baseCurrency().id(), QDate::currentDate()); const auto curRate = curPrice.rate(file->baseCurrency().id()); auto baseValue = value * curRate; 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()) { const auto curPrice = file->price((*liabilities_it).tradingCurrencyId(), file->baseCurrency().id(), QDate::currentDate()); const auto curRate = curPrice.rate(file->baseCurrency().id()); auto baseValue = value * curRate; 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() { m_html += "
" + i18n("Budget") + "
\n
 
\n"; m_html += ""; if (const auto reportsPlugin = pPlugins.data.value(QStringLiteral("reportsview"), nullptr)) { const auto variantReport = reportsPlugin->requestData(QString(), eWidgetPlugin::WidgetType::Budget); if (!variantReport.isNull()) m_html.append(variantReport.toString()); } else { m_html += QString(""); m_html += QString("").arg(i18n("Enable reports plugin to see this chart.")); m_html += QString(""); } m_html += QString("
%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()) { auto repSplitAcc = file->account(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()) { const auto curPrice = file->price(repSplitAcc.tradingCurrencyId(), file->baseCurrency().id(), QDate::currentDate()); const auto curRate = curPrice.rate(file->baseCurrency().id()); value = (split.shares() * MyMoneyMoney::MINUS_ONE) * curRate; 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) { 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()) { 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()) { auto repSplitAcc = file->account((*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()) { const auto curPrice = file->price(repSplitAcc.tradingCurrencyId(), file->baseCurrency().id(), QDate::currentDate()); const auto curRate = curPrice.rate(file->baseCurrency().id()); value = value * curRate; 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()) { const auto curPrice = file->price((*account_it).tradingCurrencyId(), file->baseCurrency().id(), QDate::currentDate()); const auto curRate = curPrice.rate(file->baseCurrency().id()); auto baseValue = value * curRate; 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()) { const auto curPrice = file->price((*account_it).tradingCurrencyId(), file->baseCurrency().id(), QDate::currentDate()); const auto curRate = curPrice.rate(file->baseCurrency().id()); auto baseValue = value * curRate; 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; QMap< QString, QVector > m_transactionStats; /** * daily forecast balance of accounts */ QMap m_accountList; QPrinter *m_currentPrinter; int m_scrollBarPos; }; #endif diff --git a/kmymoney/views/kpayeesview.cpp b/kmymoney/views/kpayeesview.cpp index 19828f386..f5d77ae0c 100644 --- a/kmymoney/views/kpayeesview.cpp +++ b/kmymoney/views/kpayeesview.cpp @@ -1,721 +1,720 @@ /*************************************************************************** kpayeesview.cpp --------------- begin : Thu Jan 24 2002 copyright : (C) 2000-2002 by Michael Edwardes Javier Campos Morales Felix Rodriguez John C Thomas Baumgart Kevin Tambascio Andreas Nicolai (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 "kpayeesview_p.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include #include "ui_kpayeesview.h" #include "kmymoneyviewbase_p.h" #include "kpayeeidentifierview.h" #include "mymoneypayee.h" #include "mymoneyexception.h" #include "mymoneyfile.h" #include "mymoneyaccount.h" #include "mymoneymoney.h" #include "mymoneytransactionfilter.h" #include "kmymoneysettings.h" #include "models.h" #include "accountsmodel.h" #include "mymoneysecurity.h" #include "mymoneycontact.h" #include "mymoneyprice.h" #include "mymoneysplit.h" #include "mymoneytransaction.h" #include "icons/icons.h" #include "transaction.h" #include "widgetenums.h" #include "mymoneyenums.h" #include "modelenums.h" #include "menuenums.h" using namespace Icons; // *** KPayeesView Implementation *** KPayeesView::KPayeesView(QWidget *parent) : KMyMoneyViewBase(*new KPayeesViewPrivate(this), parent) { connect(pActions[eMenu::Action::NewPayee], &QAction::triggered, this, &KPayeesView::slotNewPayee); connect(pActions[eMenu::Action::RenamePayee], &QAction::triggered, this, &KPayeesView::slotRenamePayee); connect(pActions[eMenu::Action::DeletePayee], &QAction::triggered, this, &KPayeesView::slotDeletePayee); connect(pActions[eMenu::Action::MergePayee], &QAction::triggered, this, &KPayeesView::slotMergePayee); } KPayeesView::~KPayeesView() { } void KPayeesView::slotChooseDefaultAccount() { Q_D(KPayeesView); MyMoneyFile* file = MyMoneyFile::instance(); QMap account_count; KMyMoneyRegister::RegisterItem* item = d->ui->m_register->firstItem(); while (item) { //only walk through selectable items. eg. transactions and not group markers if (item->isSelectable()) { auto t = dynamic_cast(item); if (!t) return; MyMoneySplit s = t->transaction().splitByPayee(d->m_payee.id()); const MyMoneyAccount& acc = file->account(s.accountId()); - QString txt; if (s.action() != MyMoneySplit::actionName(eMyMoney::Split::Action::Amortization) && acc.accountType() != eMyMoney::Account::Type::AssetLoan && !file->isTransfer(t->transaction()) && t->transaction().splitCount() == 2) { MyMoneySplit s0 = t->transaction().splitByAccount(s.accountId(), false); if (account_count.contains(s0.accountId())) { account_count[s0.accountId()]++; } else { account_count[s0.accountId()] = 1; } } } item = item->nextItem(); } QMap::Iterator most_frequent, iter; most_frequent = account_count.begin(); for (iter = account_count.begin(); iter != account_count.end(); ++iter) { if (iter.value() > most_frequent.value()) { most_frequent = iter; } } if (most_frequent != account_count.end()) { d->ui->checkEnableDefaultCategory->setChecked(true); d->ui->comboDefaultCategory->setSelected(most_frequent.key()); d->setDirty(true); } } void KPayeesView::slotClosePayeeIdentifierSource() { Q_D(KPayeesView); if (!d->m_needLoad) d->ui->payeeIdentifiers->closeSource(); } void KPayeesView::slotSelectByVariant(const QVariantList& variant, eView::Intent intent) { switch (intent) { case eView::Intent::ShowPayee: if (variant.count() == 3) slotSelectPayeeAndTransaction(variant.at(0).toString(), variant.at(1).toString(), variant.at(2).toString()); break; default: break; } } void KPayeesView::slotStartRename(QListWidgetItem* item) { Q_D(KPayeesView); d->m_allowEditing = true; d->ui->m_payeesList->editItem(item); } // This variant is only called when a single payee is selected and renamed. void KPayeesView::slotRenameSinglePayee(QListWidgetItem* p) { Q_D(KPayeesView); //if there is no current item selected, exit if (d->m_allowEditing == false || !d->ui->m_payeesList->currentItem() || p != d->ui->m_payeesList->currentItem()) return; //qDebug() << "[KPayeesView::slotRenamePayee]"; // create a copy of the new name without appended whitespaces QString new_name = p->text(); if (d->m_payee.name() != new_name) { MyMoneyFileTransaction ft; try { // check if we already have a payee with the new name try { // this function call will throw an exception, if the payee // hasn't been found. MyMoneyFile::instance()->payeeByName(new_name); // the name already exists, ask the user whether he's sure to keep the name if (KMessageBox::questionYesNo(this, i18n("A payee with the name '%1' already exists. It is not advisable to have " "multiple payees with the same identification name. Are you sure you would like " "to rename the payee?", new_name)) != KMessageBox::Yes) { p->setText(d->m_payee.name()); return; } } catch (const MyMoneyException &) { // all ok, the name is unique } d->m_payee.setName(new_name); d->m_newName = new_name; MyMoneyFile::instance()->modifyPayee(d->m_payee); // the above call to modifyPayee will reload the view so // all references and pointers to the view have to be // re-established. // make sure, that the record is visible even if it moved // out of sight due to the rename operation d->ensurePayeeVisible(d->m_payee.id()); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to modify payee"), QString::fromLatin1(e.what())); } } else { p->setText(new_name); } } void KPayeesView::slotSelectPayee(QListWidgetItem* cur, QListWidgetItem* prev) { Q_D(KPayeesView); Q_UNUSED(cur); Q_UNUSED(prev); d->m_allowEditing = false; } void KPayeesView::slotSelectPayee() { Q_D(KPayeesView); // check if the content of a currently selected payee was modified // and ask to store the data if (d->isDirty()) { if (KMessageBox::questionYesNo(this, i18n("Do you want to save the changes for %1?", d->m_newName), i18n("Save changes")) == KMessageBox::Yes) { d->m_inSelection = true; slotUpdatePayee(); d->m_inSelection = false; } } // make sure we always clear the selected list when listing again d->m_selectedPayeesList.clear(); // loop over all payees and count the number of payees, also // obtain last selected payee d->selectedPayees(d->m_selectedPayeesList); updatePayeeActions(d->m_selectedPayeesList); emit selectObjects(d->m_selectedPayeesList); if (d->m_selectedPayeesList.isEmpty()) { d->ui->m_tabWidget->setEnabled(false); // disable tab widget d->ui->m_balanceLabel->hide(); d->ui->m_deleteButton->setEnabled(false); //disable delete, rename and merge buttons d->ui->m_renameButton->setEnabled(false); d->ui->m_mergeButton->setEnabled(false); d->clearItemData(); d->m_payee = MyMoneyPayee(); d->ui->m_syncAddressbook->setEnabled(false); return; // make sure we don't access an undefined payee } d->ui->m_deleteButton->setEnabled(true); //re-enable delete button d->ui->m_syncAddressbook->setEnabled(true); // if we have multiple payees selected, clear and disable the payee information if (d->m_selectedPayeesList.count() > 1) { d->ui->m_tabWidget->setEnabled(false); // disable tab widget d->ui->m_renameButton->setEnabled(false); // disable also the rename button d->ui->m_mergeButton->setEnabled(true); d->ui->m_balanceLabel->hide(); d->clearItemData(); } else { d->ui->m_mergeButton->setEnabled(false); d->ui->m_renameButton->setEnabled(true); } // otherwise we have just one selected, enable payee information widget d->ui->m_tabWidget->setEnabled(true); d->ui->m_balanceLabel->show(); // as of now we are updating only the last selected payee, and until // selection mode of the QListView has been changed to Extended, this // will also be the only selection and behave exactly as before - Andreas try { d->m_payee = d->m_selectedPayeesList[0]; d->m_newName = d->m_payee.name(); d->ui->addressEdit->setEnabled(true); d->ui->addressEdit->setText(d->m_payee.address()); d->ui->postcodeEdit->setEnabled(true); d->ui->postcodeEdit->setText(d->m_payee.postcode()); d->ui->telephoneEdit->setEnabled(true); d->ui->telephoneEdit->setText(d->m_payee.telephone()); d->ui->emailEdit->setEnabled(true); d->ui->emailEdit->setText(d->m_payee.email()); d->ui->notesEdit->setText(d->m_payee.notes()); QStringList keys; bool ignorecase = false; auto type = d->m_payee.matchData(ignorecase, keys); d->ui->matchTypeCombo->setCurrentIndex(d->ui->matchTypeCombo->findData(static_cast(type))); d->ui->matchKeyEditList->clear(); d->ui->matchKeyEditList->insertStringList(keys); d->ui->checkMatchIgnoreCase->setChecked(ignorecase); d->ui->checkEnableDefaultCategory->setChecked(d->m_payee.defaultAccountEnabled()); d->ui->comboDefaultCategory->setSelected(d->m_payee.defaultAccountId()); d->ui->payeeIdentifiers->setSource(d->m_payee); slotPayeeDataChanged(); d->showTransactions(); } catch (const MyMoneyException &e) { qDebug("exception during display of payee: %s", e.what()); d->ui->m_register->clear(); d->m_selectedPayeesList.clear(); d->m_payee = MyMoneyPayee(); } d->m_allowEditing = true; } void KPayeesView::slotKeyListChanged() { Q_D(KPayeesView); bool rc = false; bool ignorecase = false; QStringList keys; d->m_payee.matchData(ignorecase, keys); if (static_cast(d->ui->matchTypeCombo->currentData().toUInt()) == eMyMoney::Payee::MatchType::Key) { rc |= (keys != d->ui->matchKeyEditList->items()); } d->setDirty(rc); } void KPayeesView::slotPayeeDataChanged() { Q_D(KPayeesView); bool rc = false; if (d->ui->m_tabWidget->isEnabled()) { rc |= ((d->m_payee.email().isEmpty() != d->ui->emailEdit->text().isEmpty()) || (!d->ui->emailEdit->text().isEmpty() && d->m_payee.email() != d->ui->emailEdit->text())); rc |= ((d->m_payee.address().isEmpty() != d->ui->addressEdit->toPlainText().isEmpty()) || (!d->ui->addressEdit->toPlainText().isEmpty() && d->m_payee.address() != d->ui->addressEdit->toPlainText())); rc |= ((d->m_payee.postcode().isEmpty() != d->ui->postcodeEdit->text().isEmpty()) || (!d->ui->postcodeEdit->text().isEmpty() && d->m_payee.postcode() != d->ui->postcodeEdit->text())); rc |= ((d->m_payee.telephone().isEmpty() != d->ui->telephoneEdit->text().isEmpty()) || (!d->ui->telephoneEdit->text().isEmpty() && d->m_payee.telephone() != d->ui->telephoneEdit->text())); rc |= ((d->m_payee.name().isEmpty() != d->m_newName.isEmpty()) || (!d->m_newName.isEmpty() && d->m_payee.name() != d->m_newName)); rc |= ((d->m_payee.notes().isEmpty() != d->ui->notesEdit->toPlainText().isEmpty()) || (!d->ui->notesEdit->toPlainText().isEmpty() && d->m_payee.notes() != d->ui->notesEdit->toPlainText())); bool ignorecase = false; QStringList keys; auto type = d->m_payee.matchData(ignorecase, keys); rc |= (static_cast(type) != d->ui->matchTypeCombo->currentData().toUInt()); d->ui->checkMatchIgnoreCase->setEnabled(false); d->ui->matchKeyEditList->setEnabled(false); if (static_cast(d->ui->matchTypeCombo->currentData().toUInt()) != eMyMoney::Payee::MatchType::Disabled) { d->ui->checkMatchIgnoreCase->setEnabled(true); // if we turn matching on, we default to 'ignore case' // TODO maybe make the default a user option if (type == eMyMoney::Payee::MatchType::Disabled && static_cast(d->ui->matchTypeCombo->currentData().toUInt()) != eMyMoney::Payee::MatchType::Disabled) d->ui->checkMatchIgnoreCase->setChecked(true); rc |= (ignorecase != d->ui->checkMatchIgnoreCase->isChecked()); if (static_cast(d->ui->matchTypeCombo->currentData().toUInt()) == eMyMoney::Payee::MatchType::Key) { d->ui->matchKeyEditList->setEnabled(true); rc |= (keys != d->ui->matchKeyEditList->items()); } } rc |= (d->ui->checkEnableDefaultCategory->isChecked() != d->m_payee.defaultAccountEnabled()); if (d->ui->checkEnableDefaultCategory->isChecked()) { d->ui->comboDefaultCategory->setEnabled(true); d->ui->labelDefaultCategory->setEnabled(true); // this is only going to understand the first in the list of selected accounts if (d->ui->comboDefaultCategory->getSelected().isEmpty()) { rc |= !d->m_payee.defaultAccountId().isEmpty(); } else { QString temp = d->ui->comboDefaultCategory->getSelected(); rc |= (temp.isEmpty() != d->m_payee.defaultAccountId().isEmpty()) || (!d->m_payee.defaultAccountId().isEmpty() && temp != d->m_payee.defaultAccountId()); } } else { d->ui->comboDefaultCategory->setEnabled(false); d->ui->labelDefaultCategory->setEnabled(false); } rc |= (d->m_payee.payeeIdentifiers() != d->ui->payeeIdentifiers->identifiers()); } d->setDirty(rc); } void KPayeesView::slotUpdatePayee() { Q_D(KPayeesView); if (d->isDirty()) { MyMoneyFileTransaction ft; d->setDirty(false); try { d->m_payee.setName(d->m_newName); d->m_payee.setAddress(d->ui->addressEdit->toPlainText()); d->m_payee.setPostcode(d->ui->postcodeEdit->text()); d->m_payee.setTelephone(d->ui->telephoneEdit->text()); d->m_payee.setEmail(d->ui->emailEdit->text()); d->m_payee.setNotes(d->ui->notesEdit->toPlainText()); d->m_payee.setMatchData(static_cast(d->ui->matchTypeCombo->currentData().toUInt()), d->ui->checkMatchIgnoreCase->isChecked(), d->ui->matchKeyEditList->items()); d->m_payee.setDefaultAccountId(); d->m_payee.resetPayeeIdentifiers(d->ui->payeeIdentifiers->identifiers()); if (d->ui->checkEnableDefaultCategory->isChecked()) { QString temp; if (!d->ui->comboDefaultCategory->getSelected().isEmpty()) { temp = d->ui->comboDefaultCategory->getSelected(); d->m_payee.setDefaultAccountId(temp); } } MyMoneyFile::instance()->modifyPayee(d->m_payee); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to modify payee"), QString::fromLatin1(e.what())); } } } void KPayeesView::slotSyncAddressBook() { Q_D(KPayeesView); if (d->m_payeeRows.isEmpty()) { // empty list means no syncing is pending... foreach (auto item, d->ui->m_payeesList->selectedItems()) { d->m_payeeRows.append(d->ui->m_payeesList->row(item)); // ...so initialize one } d->ui->m_payeesList->clearSelection(); // otherwise slotSelectPayee will be run after every payee update // d->ui->m_syncAddressbook->setEnabled(false); // disallow concurrent syncs } if (d->m_payeeRows.count() <= d->m_payeeRow) { if (auto item = dynamic_cast(d->ui->m_payeesList->currentItem())) { // update ui if something is selected d->m_payee = item->payee(); d->ui->addressEdit->setText(d->m_payee.address()); d->ui->postcodeEdit->setText(d->m_payee.postcode()); d->ui->telephoneEdit->setText(d->m_payee.telephone()); } d->m_payeeRows.clear(); // that means end of sync d->m_payeeRow = 0; return; } if (auto item = dynamic_cast(d->ui->m_payeesList->item(d->m_payeeRows.at(d->m_payeeRow)))) d->m_payee = item->payee(); ++d->m_payeeRow; d->m_contact->fetchContact(d->m_payee.email()); // search for payee's data in addressbook and receive it in slotContactFetched } void KPayeesView::slotContactFetched(const ContactData &identity) { Q_D(KPayeesView); if (!identity.email.isEmpty()) { // empty e-mail means no identity fetched QString txt; if (!identity.street.isEmpty()) txt.append(identity.street + '\n'); if (!identity.locality.isEmpty()) { txt.append(identity.locality); if (!identity.postalCode.isEmpty()) txt.append(' ' + identity.postalCode + '\n'); else txt.append('\n'); } if (!identity.country.isEmpty()) txt.append(identity.country + '\n'); if (!txt.isEmpty() && d->m_payee.address().compare(txt) != 0) d->m_payee.setAddress(txt); if (!identity.postalCode.isEmpty() && d->m_payee.postcode().compare(identity.postalCode) != 0) d->m_payee.setPostcode(identity.postalCode); if (!identity.phoneNumber.isEmpty() && d->m_payee.telephone().compare(identity.phoneNumber) != 0) d->m_payee.setTelephone(identity.phoneNumber); MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->modifyPayee(d->m_payee); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Unable to modify payee"), QString::fromLatin1(e.what())); } } slotSyncAddressBook(); // process next payee } void KPayeesView::slotSendMail() { Q_D(KPayeesView); QRegularExpression re(".+@.+"); if (re.match(d->m_payee.email()).hasMatch()) QDesktopServices::openUrl(QUrl(QStringLiteral("mailto:?to=") + d->m_payee.email(), QUrl::TolerantMode)); } void KPayeesView::executeCustomAction(eView::Action action) { switch(action) { case eView::Action::Refresh: refresh(); break; case eView::Action::SetDefaultFocus: { Q_D(KPayeesView); QTimer::singleShot(0, d->m_searchWidget, SLOT(setFocus())); } break; case eView::Action::ClosePayeeIdentifierSource: slotClosePayeeIdentifierSource(); break; default: break; } } void KPayeesView::refresh() { Q_D(KPayeesView); if (isVisible()) { if (d->m_inSelection) { QTimer::singleShot(0, this, SLOT(refresh())); } else { d->loadPayees(); d->m_needsRefresh = false; } } else { d->m_needsRefresh = true; } } void KPayeesView::showEvent(QShowEvent* event) { if (MyMoneyFile::instance()->storageAttached()) { Q_D(KPayeesView); if (d->m_needLoad) d->init(); emit customActionRequested(View::Payees, eView::Action::AboutToShow); if (d->m_needsRefresh) refresh(); QList list; d->selectedPayees(list); emit selectObjects(list); } // don't forget base class implementation QWidget::showEvent(event); } void KPayeesView::updatePayeeActions(const QList &payees) { pActions[eMenu::Action::NewPayee]->setEnabled(true); const auto payeesCount = payees.count(); auto b = payeesCount == 1 ? true : false; pActions[eMenu::Action::RenamePayee]->setEnabled(b); b = payeesCount > 1 ? true : false; pActions[eMenu::Action::MergePayee]->setEnabled(b); b = payeesCount == 0 ? false : true; pActions[eMenu::Action::DeletePayee]->setEnabled(b); } void KPayeesView::slotSelectTransaction() { Q_D(KPayeesView); auto list = d->ui->m_register->selectedItems(); if (!list.isEmpty()) { const auto t = dynamic_cast(list[0]); if (t) emit selectByVariant(QVariantList {QVariant(t->split().accountId()), QVariant(t->transaction().id()) }, eView::Intent::ShowTransaction); } } void KPayeesView::slotSelectPayeeAndTransaction(const QString& payeeId, const QString& accountId, const QString& transactionId) { Q_D(KPayeesView); if (!isVisible()) return; try { // clear filter d->m_searchWidget->clear(); d->m_searchWidget->updateSearch(); // deselect all other selected items QList selectedItems = d->ui->m_payeesList->selectedItems(); QList::const_iterator payeesIt = selectedItems.constBegin(); while (payeesIt != selectedItems.constEnd()) { if (auto item = dynamic_cast(*payeesIt)) item->setSelected(false); ++payeesIt; } // find the payee in the list QListWidgetItem* it; for (int i = 0; i < d->ui->m_payeesList->count(); ++i) { it = d->ui->m_payeesList->item(i); auto item = dynamic_cast(it); if (item && item->payee().id() == payeeId) { d->ui->m_payeesList->scrollToItem(it, QAbstractItemView::PositionAtCenter); d->ui->m_payeesList->setCurrentItem(it); // active item and deselect all others d->ui->m_payeesList->setCurrentRow(i, QItemSelectionModel::ClearAndSelect); // and select it //make sure the payee selection is updated and transactions are updated accordingly slotSelectPayee(); KMyMoneyRegister::RegisterItem *registerItem = 0; for (i = 0; i < d->ui->m_register->rowCount(); ++i) { registerItem = d->ui->m_register->itemAtRow(i); if (auto t = dynamic_cast(registerItem)) { if (t->transaction().id() == transactionId && t->transaction().accountReferenced(accountId)) { d->ui->m_register->selectItem(registerItem); d->ui->m_register->ensureItemVisible(registerItem); break; } } } // quit out of outer for() loop break; } } } catch (const MyMoneyException &e) { qWarning("Unexpected exception in KPayeesView::slotSelectPayeeAndTransaction %s", e.what()); } } void KPayeesView::slotShowPayeesMenu(const QPoint& /*p*/) { Q_D(KPayeesView); if (dynamic_cast(d->ui->m_payeesList->currentItem())) { slotSelectPayee(); pMenus[eMenu::Menu::Payee]->exec(QCursor::pos()); } } void KPayeesView::slotHelp() { KHelpClient::invokeHelp("details.payees"); } void KPayeesView::slotChangeFilter(int index) { Q_D(KPayeesView); //update the filter type then reload the payees list d->m_payeeFilterType = index; d->loadPayees(); } void KPayeesView::slotNewPayee() { QString id; KMyMoneyUtils::newPayee(i18n("New Payee"), id); slotSelectPayeeAndTransaction(id); } void KPayeesView::slotRenamePayee() { Q_D(KPayeesView); if (d->ui->m_payeesList->currentItem() && d->ui->m_payeesList->selectedItems().count() == 1) { slotStartRename(d->ui->m_payeesList->currentItem()); } } void KPayeesView::slotDeletePayee() { Q_D(KPayeesView); if (d->m_selectedPayeesList.isEmpty()) return; // shouldn't happen // get confirmation from user QString prompt; if (d->m_selectedPayeesList.size() == 1) prompt = i18n("

Do you really want to remove the payee %1?

", d->m_selectedPayeesList.front().name()); else prompt = i18n("Do you really want to remove all selected payees?"); if (KMessageBox::questionYesNo(this, prompt, i18n("Remove Payee")) == KMessageBox::No) return; d->payeeReassign(KPayeeReassignDlg::TypeDelete); } void KPayeesView::slotMergePayee() { Q_D(KPayeesView); if (d->m_selectedPayeesList.size() < 1) return; // shouldn't happen if (KMessageBox::questionYesNo(this, i18n("

Do you really want to merge the selected payees?"), i18n("Merge Payees")) == KMessageBox::No) return; if (d->payeeReassign(KPayeeReassignDlg::TypeMerge)) // clean selection since we just deleted the selected payees d->m_selectedPayeesList.clear(); } diff --git a/kmymoney/views/kscheduledview_p.h b/kmymoney/views/kscheduledview_p.h index 11e3d6ae7..d2d1377c5 100644 --- a/kmymoney/views/kscheduledview_p.h +++ b/kmymoney/views/kscheduledview_p.h @@ -1,691 +1,687 @@ /*************************************************************************** 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 +#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; } - // make sure that the parent widget is currently visible to avoid - // positioning of the dialog in the upper left corner of the screen - QWidget* parent = q; - while (parent && !parent->isVisible()) { - parent = parent->parentWidget(); - } - + QWidget* parent = QApplication::activeWindow(); QPointer dlg = new KEnterScheduleDlg(parent, schedule); + qDebug() << "parent widget" << (void*) parent; 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()) { 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/newspliteditor.cpp b/kmymoney/views/newspliteditor.cpp index e8f4412f2..6ba378f4b 100644 --- a/kmymoney/views/newspliteditor.cpp +++ b/kmymoney/views/newspliteditor.cpp @@ -1,391 +1,389 @@ /*************************************************************************** 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); // useless call as reported by coverity scan } diff --git a/kmymoney/views/newtransactioneditor.cpp b/kmymoney/views/newtransactioneditor.cpp index b6730bcf8..286ffcc6a 100644 --- a/kmymoney/views/newtransactioneditor.cpp +++ b/kmymoney/views/newtransactioneditor.cpp @@ -1,729 +1,727 @@ /*************************************************************************** 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 #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 m_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 << m_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) == m_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: something 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->m_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->m_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->m_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/widgethintframe.cpp b/kmymoney/views/widgethintframe.cpp index e9bbfc977..3e82a8eb4 100644 --- a/kmymoney/views/widgethintframe.cpp +++ b/kmymoney/views/widgethintframe.cpp @@ -1,231 +1,236 @@ /* * Copyright 2015-2018 Thomas Baumgart * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "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) { } +WidgetHintFrameCollection::~WidgetHintFrameCollection() +{ + delete d; +} + void WidgetHintFrameCollection::addFrame(WidgetHintFrame* frame) { if(!d->frameList.contains(frame)) { connect(frame, &QObject::destroyed, this, &WidgetHintFrameCollection::frameDestroyed); connect(frame, &WidgetHintFrame::changed, this, [=] { QMetaObject::invokeMethod(this, "updateWidgets", Qt::QueuedConnection); }); 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::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 20f372a35..d273bf3cb 100644 --- a/kmymoney/views/widgethintframe.h +++ b/kmymoney/views/widgethintframe.h @@ -1,105 +1,106 @@ /* * Copyright 2015-2018 Thomas Baumgart * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef WIDGETHINTFRAME_H #define WIDGETHINTFRAME_H // ---------------------------------------------------------------------------- // QT Includes #include class QWidget; // ---------------------------------------------------------------------------- // KDE Includes // ---------------------------------------------------------------------------- // Project Includes class WidgetHintFrame : public QFrame { Q_OBJECT public: enum FrameStyle { Error = 0, Warning, Info }; Q_ENUM(FrameStyle) explicit WidgetHintFrame(QWidget* editWidget, FrameStyle style = Error, Qt::WindowFlags f = 0); ~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: bool eventFilter(QObject* o, QEvent* e) final override; Q_SIGNALS: void changed(); private: class Private; Private * const d; }; class WidgetHintFrameCollection : public QObject { Q_OBJECT public: explicit WidgetHintFrameCollection(QObject* parent = 0); + ~WidgetHintFrameCollection(); void addFrame(WidgetHintFrame* frame); void addWidget(QWidget* w); void removeWidget(QWidget* w); 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/amountedit.cpp b/kmymoney/widgets/amountedit.cpp index 37ebe3573..f38839094 100644 --- a/kmymoney/widgets/amountedit.cpp +++ b/kmymoney/widgets/amountedit.cpp @@ -1,438 +1,440 @@ /* * Copyright 2010-2017 Thomas Baumgart * Copyright 2017-2018 Łukasz Wojniłowicz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "amountedit.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include // ---------------------------------------------------------------------------- // Project Includes #include "amountvalidator.h" #include "kmymoneycalculator.h" #include "mymoneymoney.h" #include "mymoneysecurity.h" #include "icons/icons.h" using namespace Icons; class AmountEditPrivate { Q_DISABLE_COPY(AmountEditPrivate) Q_DECLARE_PUBLIC(AmountEdit) public: explicit AmountEditPrivate(AmountEdit* qq) : q_ptr(qq), m_calculatorFrame(nullptr), m_calculator(nullptr), m_calculatorButton(nullptr), m_prec(2), m_allowEmpty(false) { Q_Q(AmountEdit); m_calculatorFrame = new QFrame(q); m_calculatorFrame->setWindowFlags(Qt::Popup); m_calculatorFrame->setFrameStyle(QFrame::Panel | QFrame::Raised); m_calculatorFrame->setLineWidth(3); m_calculator = new KMyMoneyCalculator(m_calculatorFrame); m_calculatorFrame->hide(); } void init() { Q_Q(AmountEdit); // Yes, just a simple double validator ! auto validator = new AmountValidator(q); q->setValidator(validator); q->setAlignment(Qt::AlignRight | Qt::AlignVCenter); int height = q->sizeHint().height(); int btnSize = q->sizeHint().height() - 5; m_calculatorButton = new QToolButton(q); m_calculatorButton->setIcon(Icons::get(Icon::AccessoriesCalculator)); m_calculatorButton->setCursor(Qt::ArrowCursor); m_calculatorButton->setStyleSheet("QToolButton { border: none; padding: 2px}"); m_calculatorButton->setFixedSize(btnSize, btnSize); + m_calculatorButton->setFocusPolicy(Qt::ClickFocus); m_calculatorButton->show(); int frameWidth = q->style()->pixelMetric(QStyle::PM_DefaultFrameWidth); q->setStyleSheet(QString("QLineEdit { padding-right: %1px }") .arg(btnSize - frameWidth)); q->setMinimumHeight(height); q->connect(m_calculatorButton, &QAbstractButton::clicked, q, &AmountEdit::slotCalculatorOpen); KSharedConfig::Ptr kconfig = KSharedConfig::openConfig(); KConfigGroup grp = kconfig->group("General Options"); if (grp.readEntry("DontShowCalculatorButton", false) == true) q->setCalculatorButtonVisible(false); q->connect(q, &QLineEdit::textChanged, q, &AmountEdit::theTextChanged); q->connect(m_calculator, &KMyMoneyCalculator::signalResultAvailable, q, &AmountEdit::slotCalculatorResult); q->connect(m_calculator, &KMyMoneyCalculator::signalQuit, q, &AmountEdit::slotCalculatorClose); } /** * Internal helper function for value() and ensureFractionalPart(). */ void ensureFractionalPart(QString& s) const { s = MyMoneyMoney(s).formatMoney(QString(), m_prec, false); } /** * This method opens the calculator and replays the key * event pointed to by @p ev. If @p ev is 0, then no key * event is replayed. * * @param ev pointer to QKeyEvent that started the calculator. */ void calculatorOpen(QKeyEvent* k) { Q_Q(AmountEdit); m_calculator->setInitialValues(q->text(), k); auto h = m_calculatorFrame->height(); auto w = m_calculatorFrame->width(); // usually, the calculator widget is shown underneath the MoneyEdit widget // if it does not fit on the screen, we show it above this widget auto p = q->mapToGlobal(QPoint(0, 0)); if (p.y() + q->height() + h > QApplication::desktop()->height()) p.setY(p.y() - h); else p.setY(p.y() + q->height()); // usually, it is shown left aligned. If it does not fit, we align it // to the right edge of the widget if (p.x() + w > QApplication::desktop()->width()) p.setX(p.x() + q->width() - w); + m_calculatorFrame->show(); + QRect r = m_calculator->geometry(); r.moveTopLeft(p); m_calculatorFrame->setGeometry(r); - m_calculatorFrame->show(); m_calculator->setFocus(); } AmountEdit* q_ptr; QFrame* m_calculatorFrame; KMyMoneyCalculator* m_calculator; QToolButton* m_calculatorButton; int m_prec; bool m_allowEmpty; QString m_previousText; // keep track of what has been typed QString m_text; // keep track of what was the original value /** * This holds the number of precision to be used * when no other information (e.g. from account) * is available. * * @sa setStandardPrecision() */ static int standardPrecision; }; int AmountEditPrivate::standardPrecision = 2; AmountEdit::AmountEdit(QWidget *parent, const int prec) : QLineEdit(parent), d_ptr(new AmountEditPrivate(this)) { Q_D(AmountEdit); d->m_prec = prec; if (prec < -1 || prec > 20) { d->m_prec = AmountEditPrivate::standardPrecision; } d->init(); } AmountEdit::AmountEdit(const MyMoneySecurity& sec, QWidget *parent) : QLineEdit(parent), d_ptr(new AmountEditPrivate(this)) { Q_D(AmountEdit); d->m_prec = MyMoneyMoney::denomToPrec(sec.smallestAccountFraction()); d->init(); } AmountEdit::~AmountEdit() { Q_D(AmountEdit); delete d; } void AmountEdit::setStandardPrecision(int prec) { if (prec >= 0 && prec < 20) { AmountEditPrivate::standardPrecision = prec; } } void AmountEdit::resizeEvent(QResizeEvent* event) { Q_D(AmountEdit); Q_UNUSED(event); const int frameWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth); d->m_calculatorButton->move(width() - d->m_calculatorButton->width() - frameWidth - 2, 2); } void AmountEdit::focusOutEvent(QFocusEvent* event) { Q_D(AmountEdit); QLineEdit::focusOutEvent(event); // make sure we have a zero value in case the current text // is empty but this is not allowed if (text().isEmpty() && !d->m_allowEmpty) { QLineEdit::setText(QLatin1String("0")); } // make sure we have a fractional part if (!text().isEmpty()) ensureFractionalPart(); // in case the widget contains a different value we emit // the valueChanged signal if (MyMoneyMoney(text()) != MyMoneyMoney(d->m_text)) { emit valueChanged(text()); } } void AmountEdit::keyPressEvent(QKeyEvent* event) { Q_D(AmountEdit); switch(event->key()) { case Qt::Key_Plus: case Qt::Key_Minus: if (hasSelectedText()) { cut(); } if (text().length() == 0) { QLineEdit::keyPressEvent(event); break; } // in case of '-' we do not enter the calculator when // the current position is the beginning and there is // no '-' sign at the first position. if (event->key() == Qt::Key_Minus) { if (cursorPosition() == 0 && text()[0] != '-') { QLineEdit::keyPressEvent(event); break; } } // intentional fall through case Qt::Key_Slash: case Qt::Key_Asterisk: case Qt::Key_Percent: if (hasSelectedText()) { // remove the selected text cut(); } d->calculatorOpen(event); break; default: QLineEdit::keyPressEvent(event); break; } } void AmountEdit::setPrecision(const int prec) { Q_D(AmountEdit); if (prec >= -1 && prec <= 20) { if (prec != d->m_prec) { d->m_prec = prec; // update current display setValue(value()); } } } int AmountEdit::precision() const { Q_D(const AmountEdit); return d->m_prec; } bool AmountEdit::isValid() const { return !(text().isEmpty()); } QString AmountEdit::numericalText() const { return value().toString(); } MyMoneyMoney AmountEdit::value() const { Q_D(const AmountEdit); MyMoneyMoney money(text()); if (d->m_prec != -1) money = money.convert(MyMoneyMoney::precToDenom(d->m_prec)); return money; } void AmountEdit::setValue(const MyMoneyMoney& value) { Q_D(AmountEdit); // load the value into the widget but don't use thousandsSeparators setText(value.formatMoney(QString(), d->m_prec, false)); } void AmountEdit::setText(const QString& txt) { Q_D(AmountEdit); d->m_text = txt; if (isEnabled() && !txt.isEmpty()) d->ensureFractionalPart(d->m_text); QLineEdit::setText(d->m_text); #if 0 m_resetButton->setEnabled(false); #endif } void AmountEdit::resetText() { #if 0 Q_D(AmountEdit); setText(d->m_text); m_resetButton->setEnabled(false); #endif } void AmountEdit::theTextChanged(const QString & theText) { Q_D(AmountEdit); QLocale locale; QString dec = locale.groupSeparator(); QString l_text = theText; QString nsign, psign; nsign = locale.negativeSign(); psign = locale.positiveSign(); auto i = 0; if (isEnabled()) { QValidator::State state = validator()->validate(l_text, i); if (state == QValidator::Intermediate) { if (l_text.length() == 1) { if (l_text != dec && l_text != nsign && l_text != psign) state = QValidator::Invalid; } } if (state == QValidator::Invalid) QLineEdit::setText(d->m_previousText); else { d->m_previousText = l_text; emit validatedTextChanged(text()); } } } void AmountEdit::slotCalculatorOpen() { Q_D(AmountEdit); d->calculatorOpen(0); } void AmountEdit::slotCalculatorClose() { Q_D(AmountEdit); if (d->m_calculator != 0) { d->m_calculatorFrame->hide(); } } void AmountEdit::slotCalculatorResult() { Q_D(AmountEdit); slotCalculatorClose(); if (d->m_calculator != 0) { setText(d->m_calculator->result()); ensureFractionalPart(); #if 0 // I am not sure if getting a result from the calculator // is a good event to emit a value changed signal. We // should do this only on focusOutEvent() emit valueChanged(text()); d->m_text = text(); #endif } } void AmountEdit::setCalculatorButtonVisible(const bool show) { Q_D(AmountEdit); d->m_calculatorButton->setVisible(show); } void AmountEdit::setAllowEmpty(bool allowed) { Q_D(AmountEdit); d->m_allowEmpty = allowed; } bool AmountEdit::isEmptyAllowed() const { Q_D(const AmountEdit); return d->m_allowEmpty; } bool AmountEdit::isCalculatorButtonVisible() const { Q_D(const AmountEdit); return d->m_calculatorButton->isVisible(); } void AmountEdit::ensureFractionalPart() { Q_D(AmountEdit); QString s(text()); d->ensureFractionalPart(s); // by setting the text only when it's different then the one that it is already there // we preserve the edit widget's state (like the selection for example) during a // call to ensureFractionalPart() that does not change anything if (s != text()) QLineEdit::setText(s); } diff --git a/kmymoney/widgets/kmymoneydateinput.cpp b/kmymoney/widgets/kmymoneydateinput.cpp index 3d9a2bb40..6ecd2679d 100644 --- a/kmymoney/widgets/kmymoneydateinput.cpp +++ b/kmymoney/widgets/kmymoneydateinput.cpp @@ -1,373 +1,389 @@ /* * Copyright 2000-2003 Michael Edwardes * Copyright 2001 Felix Rodriguez * Copyright 2017-2018 Łukasz Wojniłowicz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "kmymoneydateinput.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "icons/icons.h" using namespace Icons; namespace { const int DATE_POPUP_TIMEOUT = 1500; const QDate INVALID_DATE = QDate(1800, 1, 1); } KMyMoney::OldDateEdit::OldDateEdit(const QDate& date, QWidget* parent) : QDateEdit(date, parent) { } void KMyMoney::OldDateEdit::keyPressEvent(QKeyEvent* k) { if ((lineEdit()->text().isEmpty() || lineEdit()->selectedText() == lineEdit()->text()) && QChar(k->key()).isDigit()) { // the line edit is empty which means that the date was cleared // or the whole text is selected and a digit character was entered // (the same meaning as clearing the date) - in this case set the date // to the current date and let the editor do the actual work setDate(QDate::currentDate()); setSelectedSection(QDateTimeEdit::DaySection); // start as when focused in if the date was cleared } QDateEdit::keyPressEvent(k); } void KMyMoney::OldDateEdit::focusInEvent(QFocusEvent * event) { QDateEdit::focusInEvent(event); setSelectedSection(QDateTimeEdit::DaySection); } bool KMyMoney::OldDateEdit::event(QEvent* e) { // make sure that we keep the current date setting of a KMyMoneyDateInput object // across the QDateEdit::event(FocusOutEvent) bool rc; KMyMoneyDateInput* p = dynamic_cast(parentWidget()); if (e->type() == QEvent::FocusOut && p) { QDate d = p->date(); rc = QDateEdit::event(e); if (d.isValid()) d = p->date(); p->loadDate(d); } else { rc = QDateEdit::event(e); } return rc; } bool KMyMoney::OldDateEdit::focusNextPrevChild(bool next) { Q_UNUSED(next) return true; } struct KMyMoneyDateInput::Private { QDateEdit *m_dateEdit; KDatePicker *m_datePicker; QDate m_date; QDate m_prevDate; Qt::AlignmentFlag m_qtalignment; QWidget *m_dateFrame; QPushButton *m_dateButton; KPassivePopup *m_datePopup; }; KMyMoneyDateInput::KMyMoneyDateInput(QWidget *parent, Qt::AlignmentFlag flags) : QWidget(parent), d(new Private) { d->m_qtalignment = flags; d->m_date = QDate::currentDate(); QHBoxLayout *dateInputLayout = new QHBoxLayout(this); dateInputLayout->setSpacing(0); dateInputLayout->setContentsMargins(0, 0, 0, 0); d->m_dateEdit = new KMyMoney::OldDateEdit(d->m_date, this); dateInputLayout->addWidget(d->m_dateEdit, 3); setFocusProxy(d->m_dateEdit); d->m_dateEdit->installEventFilter(this); // To get d->m_dateEdit's FocusIn/Out and some KeyPress events // we use INVALID_DATE as a special value for multi transaction editing d->m_dateEdit->setMinimumDate(INVALID_DATE); d->m_dateEdit->setSpecialValueText(QLatin1String(" ")); d->m_datePopup = new KPassivePopup(d->m_dateEdit); d->m_datePopup->setObjectName("datePopup"); d->m_datePopup->setTimeout(DATE_POPUP_TIMEOUT); d->m_datePopup->setView(new QLabel(QLocale().toString(d->m_date), d->m_datePopup)); d->m_dateFrame = new QWidget(this); dateInputLayout->addWidget(d->m_dateFrame); QVBoxLayout *dateFrameVBoxLayout = new QVBoxLayout(d->m_dateFrame); dateFrameVBoxLayout->setMargin(0); dateFrameVBoxLayout->setContentsMargins(0, 0, 0, 0); d->m_dateFrame->setWindowFlags(Qt::Popup); d->m_dateFrame->hide(); d->m_dateEdit->setDisplayFormat(QLocale().dateFormat(QLocale::ShortFormat)); d->m_datePicker = new KDatePicker(d->m_date, d->m_dateFrame); dateFrameVBoxLayout->addWidget(d->m_datePicker); // Let the date picker have a close button (Added in 3.1) d->m_datePicker->setCloseButton(true); // the next line is a try to add an icon to the button d->m_dateButton = new QPushButton(Icons::get(Icon::ViewCalendarDay), QString(), this); dateInputLayout->addWidget(d->m_dateButton); connect(d->m_dateButton, &QAbstractButton::clicked, this, &KMyMoneyDateInput::toggleDatePicker); connect(d->m_dateEdit, &QDateTimeEdit::dateChanged, this, &KMyMoneyDateInput::slotDateChosenRef); connect(d->m_datePicker, &KDatePicker::dateSelected, this, &KMyMoneyDateInput::slotDateChosen); connect(d->m_datePicker, &KDatePicker::dateEntered, this, &KMyMoneyDateInput::slotDateChosen); connect(d->m_datePicker, &KDatePicker::dateSelected, d->m_dateFrame, &QWidget::hide); } void KMyMoneyDateInput::markAsBadDate(bool bad, const QColor& color) { // the next line knows a bit about the internals of QAbstractSpinBox QLineEdit* le = d->m_dateEdit->findChild(); //krazy:exclude=qclasses if (le) { QPalette palette = this->palette(); le->setPalette(palette); if (bad) { palette.setColor(foregroundRole(), color); le->setPalette(palette); } } } void KMyMoneyDateInput::showEvent(QShowEvent* event) { // don't forget the standard behaviour ;-) QWidget::showEvent(event); // If the widget is shown, the size must be fixed a little later // to be appropriate. I saw this in some other places and the only // way to solve this problem is to postpone the setup of the size // to the time when the widget is on the screen. QTimer::singleShot(50, this, SLOT(fixSize())); } void KMyMoneyDateInput::fixSize() { // According to a hint in the documentation of KDatePicker::sizeHint() // 28 pixels should be added in each direction to obtain a better // display of the month button. I decided, (22,14) is good // enough and save some space on the screen (ipwizard) d->m_dateFrame->setFixedSize(d->m_datePicker->sizeHint() + QSize(22, 14)); } KMyMoneyDateInput::~KMyMoneyDateInput() { delete d->m_dateFrame; delete d->m_datePopup; delete d; } void KMyMoneyDateInput::toggleDatePicker() { int w = d->m_dateFrame->width(); int h = d->m_dateFrame->height(); if (d->m_dateFrame->isVisible()) { d->m_dateFrame->hide(); } else { QPoint tmpPoint = mapToGlobal(d->m_dateButton->geometry().bottomRight()); // usually, the datepicker widget is shown underneath the d->m_dateEdit widget // if it does not fit on the screen, we show it above this widget if (tmpPoint.y() + h > QApplication::desktop()->height()) { tmpPoint.setY(tmpPoint.y() - h - d->m_dateButton->height()); } if ((d->m_qtalignment == Qt::AlignRight && tmpPoint.x() + w <= QApplication::desktop()->width()) || (tmpPoint.x() - w < 0)) { d->m_dateFrame->setGeometry(tmpPoint.x() - width(), tmpPoint.y(), w, h); } else { tmpPoint.setX(tmpPoint.x() - w); d->m_dateFrame->setGeometry(tmpPoint.x(), tmpPoint.y(), w, h); } if (d->m_date.isValid() && d->m_date != INVALID_DATE) { d->m_datePicker->setDate(d->m_date); } else { d->m_datePicker->setDate(QDate::currentDate()); } d->m_dateFrame->show(); } } /** Overriding QWidget::keyPressEvent * * increments/decrements the date upon +/- or Up/Down key input * sets the date to current date when the 'T' key is pressed */ void KMyMoneyDateInput::keyPressEvent(QKeyEvent * k) { QKeySequence today(i18nc("Enter todays date into date input widget", "T")); + auto adjustDateSection = [&](int offset) { + switch(d->m_dateEdit->currentSection()) { + case QDateTimeEdit::DaySection: + slotDateChosen(d->m_date.addDays(offset)); + break; + case QDateTimeEdit::MonthSection: + slotDateChosen(d->m_date.addMonths(offset)); + break; + case QDateTimeEdit::YearSection: + slotDateChosen(d->m_date.addYears(offset)); + break; + default: + break; + } + }; + switch (k->key()) { case Qt::Key_Equal: case Qt::Key_Plus: - slotDateChosen(d->m_date.addDays(1)); + adjustDateSection(1); k->accept(); break; case Qt::Key_Minus: - slotDateChosen(d->m_date.addDays(-1)); + adjustDateSection(-1); k->accept(); break; default: if (today == QKeySequence(k->key()) || k->key() == Qt::Key_T) { slotDateChosen(QDate::currentDate()); k->accept(); } break; } k->ignore(); // signal that the key event was not handled } /** * This function receives all events that are sent to focusWidget(). * Some KeyPress events are intercepted and passed to keyPressEvent. * Otherwise they would be consumed by QDateEdit. */ bool KMyMoneyDateInput::eventFilter(QObject *, QEvent *e) { if (e->type() == QEvent::FocusIn) { #ifndef Q_OS_MAC d->m_datePopup->show(mapToGlobal(QPoint(0, height()))); #endif // select the date section, but we need to delay it a bit } else if (e->type() == QEvent::FocusOut) { #ifndef Q_OS_MAC d->m_datePopup->hide(); #endif } else if (e->type() == QEvent::KeyPress) { if (QKeyEvent *k = dynamic_cast(e)) { keyPressEvent(k); if (k->isAccepted()) return true; // signal that the key event was handled } } return false; // Don't filter the event } void KMyMoneyDateInput::slotDateChosenRef(const QDate& date) { if (date.isValid()) { emit dateChanged(date); d->m_date = date; #ifndef Q_OS_MAC QLabel *lbl = static_cast(d->m_datePopup->view()); lbl->setText(QLocale().toString(date)); lbl->adjustSize(); if (d->m_datePopup->isVisible() || hasFocus()) d->m_datePopup->show(mapToGlobal(QPoint(0, height()))); // Repaint #endif } } void KMyMoneyDateInput::slotDateChosen(QDate date) { if (date.isValid()) { // the next line implies a call to slotDateChosenRef() above d->m_dateEdit->setDate(date); } else { d->m_dateEdit->setDate(INVALID_DATE); } } QDate KMyMoneyDateInput::date() const { QDate rc = d->m_dateEdit->date(); if (rc == INVALID_DATE) rc = QDate(); return rc; } void KMyMoneyDateInput::setDate(QDate date) { slotDateChosen(date); } void KMyMoneyDateInput::loadDate(const QDate& date) { d->m_date = d->m_prevDate = date; blockSignals(true); slotDateChosen(date); blockSignals(false); } void KMyMoneyDateInput::resetDate() { setDate(d->m_prevDate); } void KMyMoneyDateInput::setMaximumDate(const QDate& max) { d->m_dateEdit->setMaximumDate(max); } QWidget* KMyMoneyDateInput::focusWidget() const { QWidget* w = d->m_dateEdit; while (w->focusProxy()) w = w->focusProxy(); return w; } /* void KMyMoneyDateInput::setRange(const QDate & min, const QDate & max) { d->m_dateEdit->setDateRange(min, max); } */ diff --git a/kmymoney/widgets/kmymoneyedit.cpp b/kmymoney/widgets/kmymoneyedit.cpp index 88aca61ab..d228a584d 100644 --- a/kmymoney/widgets/kmymoneyedit.cpp +++ b/kmymoney/widgets/kmymoneyedit.cpp @@ -1,596 +1,596 @@ /* * Copyright 2000-2002 Michael Edwardes * Copyright 2002-2017 Thomas Baumgart * Copyright 2017-2018 Łukasz Wojniłowicz * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "kmymoneyedit.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneymoneyvalidator.h" #include "kmymoneylineedit.h" #include "kmymoneycalculator.h" #include "mymoneymoney.h" #include "mymoneysecurity.h" #include "icons.h" using namespace Icons; // converted image from kde3.5.1/share/apps/kdevdesignerpart/pics/designer_resetproperty.png static const uchar resetButtonImage[] = { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x06, 0x08, 0x06, 0x00, 0x00, 0x00, 0x0F, 0x0E, 0x84, 0x76, 0x00, 0x00, 0x00, 0x06, 0x62, 0x4B, 0x47, 0x44, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xA0, 0xBD, 0xA7, 0x93, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, 0x0B, 0x13, 0x00, 0x00, 0x0B, 0x13, 0x01, 0x00, 0x9A, 0x9C, 0x18, 0x00, 0x00, 0x00, 0x07, 0x74, 0x49, 0x4D, 0x45, 0x07, 0xD6, 0x06, 0x10, 0x09, 0x36, 0x0C, 0x58, 0x91, 0x11, 0x7C, 0x00, 0x00, 0x00, 0x64, 0x49, 0x44, 0x41, 0x54, 0x78, 0xDA, 0x65, 0xC9, 0xA1, 0x0D, 0x02, 0x41, 0x18, 0x84, 0xD1, 0xF7, 0x5F, 0x13, 0x04, 0x9A, 0x39, 0x43, 0x68, 0x81, 0x02, 0x10, 0xB8, 0x13, 0x74, 0x80, 0xC1, 0x21, 0x76, 0x1D, 0xDD, 0xD0, 0x01, 0x65, 0x10, 0x34, 0x9A, 0x0C, 0x66, 0x83, 0x61, 0x92, 0x2F, 0x23, 0x5E, 0x25, 0x01, 0xBD, 0x6A, 0xC6, 0x1D, 0x9B, 0x25, 0x79, 0xC2, 0x34, 0xE0, 0x30, 0x00, 0x56, 0xBD, 0x6A, 0x0D, 0xD5, 0x38, 0xE1, 0xEA, 0x7F, 0xE7, 0x4A, 0xA2, 0x57, 0x1D, 0x71, 0xC1, 0x07, 0xBB, 0x81, 0x8F, 0x09, 0x96, 0xE4, 0x86, 0x3D, 0xDE, 0x78, 0x8D, 0x48, 0xF2, 0xAB, 0xB1, 0x1D, 0x9F, 0xC6, 0xFC, 0x05, 0x46, 0x68, 0x28, 0x6B, 0x58, 0xEE, 0x72, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82 }; class KMyMoneyEditPrivate { Q_DISABLE_COPY(KMyMoneyEditPrivate) Q_DECLARE_PUBLIC(KMyMoneyEdit) public: explicit KMyMoneyEditPrivate(KMyMoneyEdit *qq) : q_ptr(qq), m_calculator(nullptr), m_calculatorFrame(nullptr), m_edit(nullptr), m_calcButton(nullptr), m_resetButton(nullptr), m_prec(2), allowEmpty(true) { } ~KMyMoneyEditPrivate() { } /** * Force geometry update of a hidden widget, * see https://stackoverflow.com/a/3996525 for details * * @parem widget widget to force update */ void forceUpdate(QWidget *widget) { widget->setAttribute(Qt::WA_DontShowOnScreen); widget->show(); widget->updateGeometry(); widget->hide(); widget->setAttribute(Qt::WA_DontShowOnScreen, false); } void init() { Q_Q(KMyMoneyEdit); QHBoxLayout *editLayout = new QHBoxLayout(q); editLayout->setSpacing(0); editLayout->setContentsMargins(0, 0, 0, 0); allowEmpty = false; m_edit = new KMyMoneyLineEdit(q, true); m_edit->installEventFilter(q); + q->setFocusPolicy(m_edit->focusPolicy()); q->setFocusProxy(m_edit); editLayout->addWidget(m_edit); // Yes, just a simple double validator ! KMyMoneyMoneyValidator *validator = new KMyMoneyMoneyValidator(q); m_edit->setValidator(validator); m_edit->setAlignment(Qt::AlignRight | Qt::AlignVCenter); m_calculatorFrame = new QWidget; QVBoxLayout *calculatorFrameVBoxLayout = new QVBoxLayout(m_calculatorFrame); calculatorFrameVBoxLayout->setMargin(0); m_calculatorFrame->setWindowFlags(Qt::Popup); m_calculator = new KMyMoneyCalculator(m_calculatorFrame); calculatorFrameVBoxLayout->addWidget(m_calculator); forceUpdate(m_calculatorFrame); m_calcButton = new QPushButton(Icons::get(Icon::AccessoriesCalculator), QString(), q); m_calcButton->setFocusProxy(m_edit); editLayout->addWidget(m_calcButton); QPixmap pixmap; pixmap.loadFromData(resetButtonImage, sizeof(resetButtonImage), "PNG", 0); m_resetButton = new QPushButton(pixmap, QString(QString()), q); m_resetButton->setEnabled(false); m_resetButton->setFocusProxy(m_edit); editLayout->addWidget(m_resetButton); KSharedConfigPtr kconfig = KSharedConfig::openConfig(); KConfigGroup grp = kconfig->group("General Options"); if (grp.readEntry("DontShowCalculatorButton", false) == true) q->setCalculatorButtonVisible(false); q->connect(m_edit, &QLineEdit::textChanged, q, &KMyMoneyEdit::theTextChanged); q->connect(m_calculator, &KMyMoneyCalculator::signalResultAvailable, q, &KMyMoneyEdit::slotCalculatorResult); q->connect(m_calcButton, &QAbstractButton::clicked, q, &KMyMoneyEdit::slotCalculatorOpen); q->connect(m_resetButton, &QAbstractButton::clicked, q, &KMyMoneyEdit::resetText); } /** * This method ensures that the text version contains a * fractional part. */ void ensureFractionalPart() { QString s(m_edit->text()); ensureFractionalPart(s); // by setting the text only when it's different then the one that it is already there // we preserve the edit widget's state (like the selection for example) during a // call to ensureFractionalPart() that does not change anything if (s != m_edit->text()) m_edit->setText(s); } /** * Internal helper function for value() and ensureFractionalPart(). */ void ensureFractionalPart(QString& s) const { QString decimalSymbol = QLocale().decimalPoint(); if (decimalSymbol.isEmpty()) decimalSymbol = '.'; // If text contains no 'monetaryDecimalSymbol' then add it // followed by the required number of 0s if (!s.isEmpty()) { if (m_prec > 0) { if (!s.contains(decimalSymbol)) { s += decimalSymbol; for (auto i = 0; i < m_prec; ++i) s += '0'; } } else if (m_prec == 0) { while (s.contains(decimalSymbol)) { int pos = s.lastIndexOf(decimalSymbol); if (pos != -1) { s.truncate(pos); } } } else if (s.contains(decimalSymbol)) { // m_prec == -1 && fraction // no trailing zeroes while (s.endsWith('0')) { s.truncate(s.length() - 1); } // no trailing decimalSymbol if (s.endsWith(decimalSymbol)) s.truncate(s.length() - 1); } } } /** * This method opens the calculator and replays the key * event pointed to by @p ev. If @p ev is 0, then no key * event is replayed. * * @param ev pointer to QKeyEvent that started the calculator. */ void calculatorOpen(QKeyEvent* k) { Q_Q(KMyMoneyEdit); m_calculator->setInitialValues(m_edit->text(), k); int h = m_calculatorFrame->height(); int w = m_calculatorFrame->width(); // usually, the calculator widget is shown underneath the MoneyEdit widget // if it does not fit on the screen, we show it above this widget QPoint p = q->mapToGlobal(QPoint(0, 0)); if (p.y() + q->height() + h > QApplication::desktop()->height()) p.setY(p.y() - h); else p.setY(p.y() + q->height()); // usually, it is shown left aligned. If it does not fit, we align it // to the right edge of the widget if (p.x() + w > QApplication::desktop()->width()) p.setX(p.x() + q->width() - w); QRect r = m_calculator->geometry(); r.moveTopLeft(p); m_calculatorFrame->setGeometry(r); m_calculatorFrame->show(); m_calculator->setFocus(); } KMyMoneyEdit *q_ptr; QString previousText; // keep track of what has been typed QString m_text; // keep track of what was the original value KMyMoneyCalculator* m_calculator; QWidget* m_calculatorFrame; KMyMoneyLineEdit* m_edit; QPushButton* m_calcButton; QPushButton* m_resetButton; int m_prec; bool allowEmpty; /** * This holds the number of precision to be used * when no other information (e.g. from account) * is available. * * @sa setStandardPrecision() */ static int standardPrecision; }; int KMyMoneyEditPrivate::standardPrecision = 2; KMyMoneyEdit::KMyMoneyEdit(QWidget *parent, const int prec) : QWidget(parent), d_ptr(new KMyMoneyEditPrivate(this)) { Q_D(KMyMoneyEdit); d->m_prec = prec; if (prec < -1 || prec > 20) d->m_prec = KMyMoneyEditPrivate::standardPrecision; d->init(); } KMyMoneyEdit::KMyMoneyEdit(const MyMoneySecurity& sec, QWidget *parent) : QWidget(parent), d_ptr(new KMyMoneyEditPrivate(this)) { Q_D(KMyMoneyEdit); d->m_prec = MyMoneyMoney::denomToPrec(sec.smallestAccountFraction()); d->init(); } void KMyMoneyEdit::setStandardPrecision(int prec) { if (prec >= 0 && prec < 20) { KMyMoneyEditPrivate::standardPrecision = prec; } } void KMyMoneyEdit::setValidator(const QValidator* v) { Q_D(KMyMoneyEdit); d->m_edit->setValidator(v); } KMyMoneyEdit::~KMyMoneyEdit() { Q_D(KMyMoneyEdit); delete d; } KLineEdit* KMyMoneyEdit::lineedit() const { Q_D(const KMyMoneyEdit); return d->m_edit; } QString KMyMoneyEdit::text() const { return value().toString(); } void KMyMoneyEdit::setMinimumWidth(int w) { Q_D(KMyMoneyEdit); d->m_edit->setMinimumWidth(w); } void KMyMoneyEdit::setPrecision(const int prec) { Q_D(KMyMoneyEdit); if (prec >= -1 && prec <= 20) { if (prec != d->m_prec) { d->m_prec = prec; // update current display setValue(value()); } } } int KMyMoneyEdit::precision() const { Q_D(const KMyMoneyEdit); return d->m_prec; } bool KMyMoneyEdit::isValid() const { Q_D(const KMyMoneyEdit); return !(d->m_edit->text().isEmpty()); } MyMoneyMoney KMyMoneyEdit::value() const { Q_D(const KMyMoneyEdit); auto txt = d->m_edit->text(); d->ensureFractionalPart(txt); MyMoneyMoney money(txt); if (d->m_prec != -1) money = money.convert(MyMoneyMoney::precToDenom(d->m_prec)); return money; } void KMyMoneyEdit::setValue(const MyMoneyMoney& value) { Q_D(KMyMoneyEdit); // load the value into the widget but don't use thousandsSeparators auto txt = value.formatMoney(QString(), d->m_prec, false); loadText(txt); } void KMyMoneyEdit::loadText(const QString& txt) { Q_D(KMyMoneyEdit); d->m_edit->setText(txt); if (isEnabled() && !txt.isEmpty()) d->ensureFractionalPart(); d->m_text = d->m_edit->text(); d->m_resetButton->setEnabled(false); } void KMyMoneyEdit::clearText() { Q_D(KMyMoneyEdit); d->m_text.clear(); d->m_edit->setText(d->m_text); } void KMyMoneyEdit::setText(const QString& txt) { setValue(MyMoneyMoney(txt)); } void KMyMoneyEdit::resetText() { Q_D(KMyMoneyEdit); d->m_edit->setText(d->m_text); d->m_resetButton->setEnabled(false); } void KMyMoneyEdit::theTextChanged(const QString & theText) { Q_D(KMyMoneyEdit); QString txt = QLocale().decimalPoint(); QString l_text = theText; QString nsign, psign; #if 0 KLocale * l = KLocale::global(); if (l->negativeMonetarySignPosition() == KLocale::ParensAround || l->positiveMonetarySignPosition() == KLocale::ParensAround) { nsign = psign = '('; } else { nsign = l->negativeSign(); psign = l->positiveSign(); } #else nsign = "-"; psign = QString(); #endif auto i = 0; if (isEnabled()) { QValidator::State state = d->m_edit->validator()->validate(l_text, i); if (state == QValidator::Intermediate) { if (l_text.length() == 1) { if (l_text != txt && l_text != nsign && l_text != psign) state = QValidator::Invalid; } } if (state == QValidator::Invalid) d->m_edit->setText(d->previousText); else { d->previousText = l_text; emit textChanged(d->m_edit->text()); d->m_resetButton->setEnabled(true); } } } bool KMyMoneyEdit::eventFilter(QObject * /* o */ , QEvent *event) { Q_D(KMyMoneyEdit); auto rc = false; // we want to catch some keys that are usually handled by // the base class (e.g. '+', '-', etc.) if (event->type() == QEvent::KeyPress) { QKeyEvent *k = static_cast(event); rc = true; switch (k->key()) { case Qt::Key_Plus: case Qt::Key_Minus: if (d->m_edit->hasSelectedText()) { d->m_edit->cut(); } if (d->m_edit->text().length() == 0) { rc = false; break; } // in case of '-' we do not enter the calculator when // the current position is the beginning and there is // no '-' sign at the first position. if (k->key() == Qt::Key_Minus) { if (d->m_edit->cursorPosition() == 0 && d->m_edit->text()[0] != '-') { rc = false; break; } } // intentional fall through case Qt::Key_Slash: case Qt::Key_Asterisk: case Qt::Key_Percent: if (d->m_edit->hasSelectedText()) { // remove the selected text d->m_edit->cut(); } d->calculatorOpen(k); break; default: rc = false; break; } } else if (event->type() == QEvent::FocusOut) { if (!d->m_edit->text().isEmpty() || !d->allowEmpty) d->ensureFractionalPart(); if (MyMoneyMoney(d->m_edit->text()) != MyMoneyMoney(d->m_text) && !d->m_calculator->isVisible()) { emit valueChanged(d->m_edit->text()); } d->m_text = d->m_edit->text(); } return rc; } void KMyMoneyEdit::slotCalculatorOpen() { Q_D(KMyMoneyEdit); d->calculatorOpen(0); } void KMyMoneyEdit::slotCalculatorResult() { Q_D(KMyMoneyEdit); - QString result; if (d->m_calculator != 0) { d->m_calculatorFrame->hide(); d->m_edit->setText(d->m_calculator->result()); d->ensureFractionalPart(); emit valueChanged(d->m_edit->text()); d->m_text = d->m_edit->text(); } } QWidget* KMyMoneyEdit::focusWidget() const { Q_D(const KMyMoneyEdit); QWidget* w = d->m_edit; while (w->focusProxy()) w = w->focusProxy(); return w; } void KMyMoneyEdit::setCalculatorButtonVisible(const bool show) { Q_D(KMyMoneyEdit); d->m_calcButton->setVisible(show); } void KMyMoneyEdit::setResetButtonVisible(const bool show) { Q_D(KMyMoneyEdit); d->m_resetButton->setVisible(show); } void KMyMoneyEdit::setAllowEmpty(bool allowed) { Q_D(KMyMoneyEdit); d->allowEmpty = allowed; } bool KMyMoneyEdit::isCalculatorButtonVisible() const { Q_D(const KMyMoneyEdit); return d->m_calcButton->isVisible(); } bool KMyMoneyEdit::isResetButtonVisible() const { Q_D(const KMyMoneyEdit); return d->m_resetButton->isVisible(); } bool KMyMoneyEdit::isEmptyAllowed() const { Q_D(const KMyMoneyEdit); return d->allowEmpty; } void KMyMoneyEdit::setPlaceholderText(const QString& hint) const { Q_D(const KMyMoneyEdit); if (d->m_edit) d->m_edit->setPlaceholderText(hint); } bool KMyMoneyEdit::isReadOnly() const { Q_D(const KMyMoneyEdit); if (d->m_edit) return d->m_edit->isReadOnly(); return false; } void KMyMoneyEdit::setReadOnly(bool readOnly) { Q_D(KMyMoneyEdit); // we use the QLineEdit::setReadOnly() method directly to avoid // changing the background between readonly and read/write mode // as it is done by the KLineEdit code. if (d->m_edit) d->m_edit->QLineEdit::setReadOnly(readOnly); //krazy:exclude=qclasses } diff --git a/kmymoney/widgets/payeeidentifier/ibanbic/ibanbicitemdelegate.cpp b/kmymoney/widgets/payeeidentifier/ibanbic/ibanbicitemdelegate.cpp index 7a2058214..59e138f75 100644 --- a/kmymoney/widgets/payeeidentifier/ibanbic/ibanbicitemdelegate.cpp +++ b/kmymoney/widgets/payeeidentifier/ibanbic/ibanbicitemdelegate.cpp @@ -1,158 +1,157 @@ /* * Copyright 2014-2016 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 "ibanbicitemdelegate.h" #include #include #include #include #include "models/payeeidentifiercontainermodel.h" #include "ibanbicitemedit.h" ibanBicItemDelegate::ibanBicItemDelegate(QObject* parent, const QVariantList&) : QStyledItemDelegate(parent) { } /** @todo elide texts */ void ibanBicItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { QStyleOptionViewItem opt = option; initStyleOption(&opt, index); // Background QStyle *style = opt.widget ? opt.widget->style() : QApplication::style(); style->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, painter, opt.widget); const int margin = style->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1; const QRect textArea = QRect(opt.rect.x() + margin, opt.rect.y() + margin, opt.rect.width() - 2 * margin, opt.rect.height() - 2 * margin); // Do not paint text if the edit widget is shown const QAbstractItemView *view = qobject_cast(opt.widget); if (view && view->indexWidget(index)) return; // Get data payeeIdentifierTyped ibanBic = ibanBicByIndex(index); // Paint Bic painter->save(); const QFont smallFont = painter->font(); const QFontMetrics metrics(opt.font); const QFontMetrics smallMetrics(smallFont); const QRect bicRect = style->alignedRect((opt.direction == Qt::RightToLeft) ? Qt::LeftToRight : Qt::RightToLeft, Qt::AlignTop, QSize(textArea.width(), smallMetrics.lineSpacing()), QRect(textArea.left(), metrics.lineSpacing() + textArea.top(), textArea.width(), smallMetrics.lineSpacing()) ); painter->setFont(smallFont); style->drawItemText(painter, bicRect, Qt::AlignBottom | Qt::AlignRight, QApplication::palette(), true, ibanBic->storedBic(), opt.state & QStyle::State_Selected ? QPalette::HighlightedText : QPalette::Text); painter->restore(); // Paint Bank name painter->save(); const QRect nameRect = style->alignedRect(opt.direction, Qt::AlignTop, QSize(textArea.width(), smallMetrics.lineSpacing()), QRect(textArea.left(), metrics.lineSpacing() + textArea.top(), textArea.width(), smallMetrics.lineSpacing()) ); style->drawItemText(painter, nameRect, Qt::AlignBottom, QApplication::palette(), true, ibanBic->institutionName(), opt.state & QStyle::State_Selected ? QPalette::HighlightedText : QPalette::Text); painter->restore(); // Paint IBAN painter->save(); QFont normal = painter->font(); normal.setBold(true); painter->setFont(normal); const QRect ibanRect = style->alignedRect(opt.direction, Qt::AlignTop, QSize(textArea.width(), metrics.lineSpacing()), textArea); - const QString bic = index.model()->data(index, Qt::DisplayRole).toString(); style->drawItemText(painter, ibanRect, Qt::AlignTop, QApplication::palette(), true, ibanBic->paperformatIban(), opt.state & QStyle::State_Selected ? QPalette::HighlightedText : QPalette::Text); painter->restore(); // Paint type painter->save(); QRect typeRect = style->alignedRect(opt.direction, Qt::AlignTop | Qt::AlignRight, QSize(textArea.width() / 5, metrics.lineSpacing()), textArea); style->drawItemText(painter, typeRect, Qt::AlignTop | Qt::AlignRight, QApplication::palette(), true, i18n("IBAN & BIC"), opt.state & QStyle::State_Selected ? QPalette::HighlightedText : QPalette::Text); painter->restore(); } QSize ibanBicItemDelegate::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const { QStyleOptionViewItem opt = option; initStyleOption(&opt, index); // Test if current index is edited at the moment const QAbstractItemView *view = qobject_cast(opt.widget); if (view && view->indexWidget(index)) return view->indexWidget(index)->sizeHint(); QFontMetrics metrics(option.font); const QStyle *style = opt.widget ? opt.widget->style() : QApplication::style(); const int margin = style->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1; // A bic has maximal 11 characters, an IBAN 32 return QSize((32 + 11)*metrics.width(QLatin1Char('X')) + 3*margin, 3*metrics.lineSpacing() + metrics.leading() + 2*margin); } QWidget* ibanBicItemDelegate::createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const { Q_UNUSED(option); ibanBicItemEdit* edit = new ibanBicItemEdit(parent); connect(edit, SIGNAL(commitData(QWidget*)), this, SIGNAL(commitData(QWidget*))); connect(edit, SIGNAL(closeEditor(QWidget*)), this, SIGNAL(closeEditor(QWidget*))); emit sizeHintChanged(index); return edit; } void ibanBicItemDelegate::setEditorData(QWidget* editor, const QModelIndex& index) const { payeeIdentifierTyped ibanBic = ibanBicByIndex(index); ibanBicItemEdit* ibanEditor = qobject_cast< ibanBicItemEdit* >(editor); Q_CHECK_PTR(ibanEditor); ibanEditor->setIdentifier(ibanBic); } void ibanBicItemDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const { Q_CHECK_PTR(editor); Q_CHECK_PTR(model); Q_ASSERT(index.isValid()); ibanBicItemEdit* ibanEditor = qobject_cast< ibanBicItemEdit* >(editor); Q_CHECK_PTR(ibanEditor); model->setData(index, QVariant::fromValue(ibanEditor->identifier()), payeeIdentifierContainerModel::payeeIdentifier); } void ibanBicItemDelegate::updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option, const QModelIndex& index) const { Q_UNUSED(index); editor->setGeometry(option.rect); } /** * Internal helper to directly convert the QVariant into the correct pointer type. */ payeeIdentifierTyped ibanBicItemDelegate::ibanBicByIndex(const QModelIndex& index) const { payeeIdentifierTyped ibanBic{ index.model()->data(index, payeeIdentifierContainerModel::payeeIdentifier).value() }; Q_ASSERT(!ibanBic.isNull()); return ibanBic; } diff --git a/kmymoney/widgets/payeeidentifier/nationalaccount/nationalaccountdelegate.cpp b/kmymoney/widgets/payeeidentifier/nationalaccount/nationalaccountdelegate.cpp index 5c4b1fe5b..71aadf299 100644 --- a/kmymoney/widgets/payeeidentifier/nationalaccount/nationalaccountdelegate.cpp +++ b/kmymoney/widgets/payeeidentifier/nationalaccount/nationalaccountdelegate.cpp @@ -1,161 +1,160 @@ /* * Copyright 2014-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 "nationalaccountdelegate.h" #include #include #include #include #include "models/payeeidentifiercontainermodel.h" #include "nationalaccountedit.h" nationalAccountDelegate::nationalAccountDelegate(QObject* parent, const QVariantList&) : QStyledItemDelegate(parent) { } /** @todo elide texts */ void nationalAccountDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { QStyleOptionViewItem opt = option; initStyleOption(&opt, index); // Background QStyle *style = opt.widget ? opt.widget->style() : QApplication::style(); style->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, painter, opt.widget); const int margin = style->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1; const QRect textArea = QRect(opt.rect.x() + margin, opt.rect.y() + margin, opt.rect.width() - 2 * margin, opt.rect.height() - 2 * margin); // Do not paint text if the edit widget is shown const QAbstractItemView *view = qobject_cast(opt.widget); if (view && view->indexWidget(index)) return; // Get data payeeIdentifierTyped ident = identByIndex(index); // Paint bank code painter->save(); const QFont smallFont = painter->font(); const QFontMetrics metrics(opt.font); const QFontMetrics smallMetrics(smallFont); const QRect bicRect = style->alignedRect(opt.direction, Qt::AlignTop, QSize(textArea.width(), smallMetrics.lineSpacing()), QRect(textArea.left(), metrics.lineSpacing() + textArea.top(), textArea.width(), smallMetrics.lineSpacing()) ); painter->setFont(smallFont); style->drawItemText(painter, bicRect, Qt::AlignBottom, QApplication::palette(), true, ident->bankCode(), opt.state & QStyle::State_Selected ? QPalette::HighlightedText : QPalette::Text); painter->restore(); // Paint bank name painter->save(); const QRect nameRect = style->alignedRect(opt.direction, Qt::AlignTop, QSize(textArea.width(), smallMetrics.lineSpacing()), QRect(textArea.left(), metrics.lineSpacing() + smallMetrics.lineSpacing() + textArea.top(), textArea.width(), smallMetrics.lineSpacing()) ); style->drawItemText(painter, nameRect, Qt::AlignBottom, QApplication::palette(), true, ident->bankName(), opt.state & QStyle::State_Selected ? QPalette::HighlightedText : QPalette::Text); painter->restore(); // Paint account number painter->save(); QFont normal = painter->font(); normal.setBold(true); painter->setFont(normal); const QRect ibanRect = style->alignedRect(opt.direction, Qt::AlignTop, QSize(textArea.width(), metrics.lineSpacing()), textArea); - const QString bic = index.model()->data(index, Qt::DisplayRole).toString(); style->drawItemText(painter, ibanRect, Qt::AlignTop, QApplication::palette(), true, ident->accountNumber(), opt.state & QStyle::State_Selected ? QPalette::HighlightedText : QPalette::Text); painter->restore(); // Paint type painter->save(); QRect typeRect = style->alignedRect(opt.direction, Qt::AlignTop | Qt::AlignRight, QSize(textArea.width() / 5, metrics.lineSpacing()), textArea); style->drawItemText(painter, typeRect, Qt::AlignTop | Qt::AlignRight, QApplication::palette(), true, i18n("National Account"), opt.state & QStyle::State_Selected ? QPalette::HighlightedText : QPalette::Text); painter->restore(); } QSize nationalAccountDelegate::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const { QStyleOptionViewItem opt = option; initStyleOption(&opt, index); // QStyle::State_Editing is never set (seems to be a bug in Qt)! This code is here only because it was written already const QAbstractItemView *view = qobject_cast(opt.widget); if (view && view->indexWidget(index)) return view->indexWidget(index)->sizeHint(); QFontMetrics metrics(option.font); const QStyle *style = opt.widget ? opt.widget->style() : QApplication::style(); const int margin = style->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1; // An iban has maximal 32 characters, so national accounts should be shorter than 28 return QSize((28)*metrics.width(QLatin1Char('X')) + 2*margin, 3*metrics.lineSpacing() + metrics.leading() + 2*margin); } QWidget* nationalAccountDelegate::createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const { Q_UNUSED(option); nationalAccountEdit* edit = new nationalAccountEdit(parent); connect(edit, SIGNAL(commitData(QWidget*)), this, SIGNAL(commitData(QWidget*))); connect(edit, SIGNAL(closeEditor(QWidget*)), this, SIGNAL(closeEditor(QWidget*))); emit sizeHintChanged(index); return edit; } void nationalAccountDelegate::setEditorData(QWidget* editor, const QModelIndex& index) const { nationalAccountEdit* nationalEditor = qobject_cast< nationalAccountEdit* >(editor); Q_CHECK_PTR(nationalEditor); nationalEditor->setIdentifier(identByIndex(index)); } void nationalAccountDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const { Q_CHECK_PTR(editor); Q_CHECK_PTR(model); Q_ASSERT(index.isValid()); nationalAccountEdit* nationalEditor = qobject_cast< nationalAccountEdit* >(editor); Q_CHECK_PTR(nationalEditor); payeeIdentifierTyped ident = identByIndex(index); ident->setAccountNumber(nationalEditor->accountNumber()); ident->setBankCode(nationalEditor->institutionCode()); model->setData(index, QVariant::fromValue(ident), payeeIdentifierContainerModel::payeeIdentifier); } void nationalAccountDelegate::updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option, const QModelIndex& index) const { Q_UNUSED(index); editor->setGeometry(option.rect); } /** * Internal helper to directly convert the QVariant into the correct pointer type. */ payeeIdentifierTyped nationalAccountDelegate::identByIndex(const QModelIndex& index) const { payeeIdentifierTyped ident = payeeIdentifierTyped( index.model()->data(index, payeeIdentifierContainerModel::payeeIdentifier).value() ); Q_ASSERT(!ident.isNull()); return ident; }