diff --git a/kmymoney/views/kgloballedgerview.cpp b/kmymoney/views/kgloballedgerview.cpp index f70908600..56a1764fa 100644 --- a/kmymoney/views/kgloballedgerview.cpp +++ b/kmymoney/views/kgloballedgerview.cpp @@ -1,1968 +1,1971 @@ /*************************************************************************** kgloballedgerview.cpp - description ------------------- begin : Wed Jul 26 2006 copyright : (C) 2006 by Thomas Baumgart email : Thomas Baumgart (C) 2017 by Łukasz Wojniłowicz ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "kgloballedgerview_p.h" #include // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "mymoneyaccount.h" #include "mymoneyfile.h" #include "kmymoneyaccountcombo.h" #include "kmymoneypayeecombo.h" #include "keditscheduledlg.h" #include "kendingbalancedlg.h" #include "register.h" #include "transactioneditor.h" #include "selectedtransactions.h" #include "kmymoneyglobalsettings.h" #include "registersearchline.h" #include "kfindtransactiondlg.h" #include "kmymoneysettings.h" #include "accountsmodel.h" #include "models.h" #include "mymoneyschedule.h" #include "mymoneysecurity.h" #include "mymoneytransaction.h" #include "mymoneytransactionfilter.h" #include "mymoneysplit.h" #include "transaction.h" #include "transactionform.h" #include "widgetenums.h" #include "mymoneyenums.h" #include "menuenums.h" using namespace eMenu; QDate KGlobalLedgerViewPrivate::m_lastPostDate; KGlobalLedgerView::KGlobalLedgerView(QWidget *parent) : KMyMoneyViewBase(*new KGlobalLedgerViewPrivate(this), parent) { typedef void(KGlobalLedgerView::*KGlobalLedgerViewFunc)(); const QHash actionConnections { {Action::NewTransaction, &KGlobalLedgerView::slotNewTransaction}, {Action::EditTransaction, &KGlobalLedgerView::slotEditTransaction}, {Action::DeleteTransaction, &KGlobalLedgerView::slotDeleteTransaction}, {Action::DuplicateTransaction, &KGlobalLedgerView::slotDuplicateTransaction}, {Action::EnterTransaction, &KGlobalLedgerView::slotEnterTransaction}, {Action::AcceptTransaction, &KGlobalLedgerView::slotAcceptTransaction}, {Action::CancelTransaction, &KGlobalLedgerView::slotCancelTransaction}, {Action::EditSplits, &KGlobalLedgerView::slotEditSplits}, {Action::CopySplits, &KGlobalLedgerView::slotCopySplits}, {Action::GoToPayee, &KGlobalLedgerView::slotGoToPayee}, {Action::GoToAccount, &KGlobalLedgerView::slotGoToAccount}, {Action::MatchTransaction, &KGlobalLedgerView::slotMatchTransactions}, {Action::CombineTransactions, &KGlobalLedgerView::slotCombineTransactions}, {Action::ToggleReconciliationFlag, &KGlobalLedgerView::slotToggleReconciliationFlag}, {Action::MarkCleared, &KGlobalLedgerView::slotMarkCleared}, {Action::MarkReconciled, &KGlobalLedgerView::slotMarkReconciled}, {Action::MarkNotReconciled, &KGlobalLedgerView::slotMarkNotReconciled}, {Action::SelectAllTransactions, &KGlobalLedgerView::slotSelectAllTransactions}, {Action::NewScheduledTransaction, &KGlobalLedgerView::slotCreateScheduledTransaction}, {Action::AssignTransactionsNumber, &KGlobalLedgerView::slotAssignNumber}, {Action::StartReconciliation, &KGlobalLedgerView::slotStartReconciliation}, {Action::FinishReconciliation, &KGlobalLedgerView::slotFinishReconciliation}, {Action::PostponeReconciliation, &KGlobalLedgerView::slotPostponeReconciliation}, {Action::OpenAccount, &KGlobalLedgerView::slotOpenAccount} }; for (auto a = actionConnections.cbegin(); a != actionConnections.cend(); ++a) connect(pActions[a.key()], &QAction::triggered, this, a.value()); Q_D(KGlobalLedgerView); d->m_balanceWarning.reset(new KBalanceWarning(this)); } KGlobalLedgerView::~KGlobalLedgerView() { } void KGlobalLedgerView::setDefaultFocus() { Q_D(KGlobalLedgerView); QTimer::singleShot(0, d->m_registerSearchLine->searchLine(), SLOT(setFocus())); } void KGlobalLedgerView::refresh() { Q_D(KGlobalLedgerView); if (isVisible()) { if (!d->m_inEditMode) { setUpdatesEnabled(false); d->loadView(); setUpdatesEnabled(true); d->m_needsRefresh = false; // force a new account if the current one is empty d->m_newAccountLoaded = d->m_currentAccount.id().isEmpty(); } } else { d->m_needsRefresh = true; } } void KGlobalLedgerView::showEvent(QShowEvent* event) { Q_D(KGlobalLedgerView); if (d->m_needLoad) d->init(); emit aboutToShow(View::Ledgers); if (d->m_needsRefresh) { if (!d->m_inEditMode) { setUpdatesEnabled(false); d->loadView(); setUpdatesEnabled(true); d->m_needsRefresh = false; d->m_newAccountLoaded = false; } } else { - slotSelectAccount(d->m_accountComboBox->getSelected()); + if (!d->m_lastSelectedAccountID.isEmpty()) { + try { + const auto acc = MyMoneyFile::instance()->account(d->m_lastSelectedAccountID); + slotSelectAccount(acc.id()); + } catch (const MyMoneyException &) { + d->m_lastSelectedAccountID.clear(); // account is invalid + } + } else { + slotSelectAccount(d->m_accountComboBox->getSelected()); + } // emit objectSelected(d->m_currentAccount); KMyMoneyRegister::SelectedTransactions list(d->m_register); emit transactionsSelected(list); } // don't forget base class implementation QWidget::showEvent(event); } void KGlobalLedgerView::updateActions(const MyMoneyObject& obj) { Q_D(KGlobalLedgerView); // 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 { Action::StartReconciliation, Action::FinishReconciliation, Action::PostponeReconciliation, Action::OpenAccount, Action::NewTransaction }; for (const auto& a : actionsToBeDisabled) pActions[a]->setEnabled(false); auto b = acc.isClosed() ? false : true; pMenus[Menu::MoveTransaction]->setEnabled(b); QString tooltip; pActions[Action::NewTransaction]->setEnabled(canCreateTransactions(tooltip)); pActions[Action::NewTransaction]->setToolTip(tooltip); const auto file = MyMoneyFile::instance(); if (!acc.id().isEmpty() && !file->isStandardAccount(acc.id())) { switch (acc.accountGroup()) { case eMyMoney::Account::Type::Asset: case eMyMoney::Account::Type::Liability: case eMyMoney::Account::Type::Equity: pActions[Action::OpenAccount]->setEnabled(true); if (acc.accountGroup() != eMyMoney::Account::Type::Equity) { if (d->m_reconciliationAccount.id().isEmpty()) { pActions[Action::StartReconciliation]->setEnabled(true); pActions[Action::StartReconciliation]->setToolTip(i18n("Reconcile")); } else { auto tip = i18n("Reconcile - disabled because you are currently reconciling %1", d->m_reconciliationAccount.name()); pActions[Action::StartReconciliation]->setToolTip(tip); if (!d->m_transactionEditor) { pActions[Action::FinishReconciliation]->setEnabled(acc.id() == d->m_reconciliationAccount.id()); pActions[Action::PostponeReconciliation]->setEnabled(acc.id() == d->m_reconciliationAccount.id()); } } } break; case eMyMoney::Account::Type::Income : case eMyMoney::Account::Type::Expense : pActions[Action::OpenAccount]->setEnabled(true); break; default: break; } } d->m_currentAccount = acc; // slotSelectAccount(acc); } void KGlobalLedgerView::updateLedgerActions(const KMyMoneyRegister::SelectedTransactions& list) { Q_D(KGlobalLedgerView); d->selectTransactions(list); updateLedgerActionsInternal(); } void KGlobalLedgerView::updateLedgerActionsInternal() { Q_D(KGlobalLedgerView); const QVector actionsToBeDisabled { Action::EditTransaction, Action::EditSplits, Action::EnterTransaction, Action::CancelTransaction, Action::DeleteTransaction, Action::MatchTransaction, Action::AcceptTransaction, Action::DuplicateTransaction, Action::ToggleReconciliationFlag, Action::MarkCleared, Action::GoToAccount, Action::GoToPayee, Action::AssignTransactionsNumber, Action::NewScheduledTransaction, Action::CombineTransactions, Action::SelectAllTransactions, Action::CopySplits, }; for (const auto& a : actionsToBeDisabled) pActions[a]->setEnabled(false); const auto file = MyMoneyFile::instance(); pActions[Action::MatchTransaction]->setText(i18nc("Button text for match transaction", "Match")); // pActions[Action::TransactionNew]->setToolTip(i18n("Create a new transaction")); pMenus[Menu::MoveTransaction]->setEnabled(false); pMenus[Menu::MarkTransaction]->setEnabled(false); pMenus[Menu::MarkTransactionContext]->setEnabled(false); pActions[Action::SelectAllTransactions]->setEnabled(true); if (!d->m_selectedTransactions.isEmpty() && !d->m_selectedTransactions.first().isScheduled()) { // enable 'delete transaction' only if at least one of the // selected transactions does not reference a closed account bool enable = false; KMyMoneyRegister::SelectedTransactions::const_iterator it_t; for (it_t = d->m_selectedTransactions.constBegin(); (enable == false) && (it_t != d->m_selectedTransactions.constEnd()); ++it_t) { enable = !(*it_t).transaction().id().isEmpty() && !file->referencesClosedAccount((*it_t).transaction()); } pActions[Action::DeleteTransaction]->setEnabled(enable); if (!d->m_transactionEditor) { QString tooltip = i18n("Duplicate the current selected transactions"); pActions[Action::DuplicateTransaction]->setEnabled(canDuplicateTransactions(d->m_selectedTransactions, tooltip) && !d->m_selectedTransactions[0].transaction().id().isEmpty()); pActions[Action::DuplicateTransaction]->setToolTip(tooltip); if (canEditTransactions(d->m_selectedTransactions, tooltip)) { pActions[Action::EditTransaction]->setEnabled(true); // editing splits is allowed only if we have one transaction selected if (d->m_selectedTransactions.count() == 1) { pActions[Action::EditSplits]->setEnabled(true); } if (d->m_currentAccount.isAssetLiability() && d->m_currentAccount.accountType() != eMyMoney::Account::Type::Investment) { pActions[Action::NewScheduledTransaction]->setEnabled(d->m_selectedTransactions.count() == 1); } } pActions[Action::EditTransaction]->setToolTip(tooltip); if (!d->m_currentAccount.isClosed()) pMenus[Menu::MoveTransaction]->setEnabled(true); pMenus[Menu::MarkTransaction]->setEnabled(true); pMenus[Menu::MarkTransactionContext]->setEnabled(true); // Allow marking the transaction if at least one is selected pActions[Action::MarkCleared]->setEnabled(true); pActions[Action::MarkReconciled]->setEnabled(true); pActions[Action::MarkNotReconciled]->setEnabled(true); pActions[Action::ToggleReconciliationFlag]->setEnabled(true); if (!d->m_accountGoto.isEmpty()) pActions[Action::GoToAccount]->setEnabled(true); if (!d->m_payeeGoto.isEmpty()) pActions[Action::GoToPayee]->setEnabled(true); // Matching is enabled as soon as one regular and one imported transaction is selected int matchedCount = 0; int importedCount = 0; KMyMoneyRegister::SelectedTransactions::const_iterator it; for (it = d->m_selectedTransactions.constBegin(); it != d->m_selectedTransactions.constEnd(); ++it) { if ((*it).transaction().isImported()) ++importedCount; if ((*it).split().isMatched()) ++matchedCount; } if (d->m_selectedTransactions.count() == 2 /* && pActions[Action::TransactionEdit]->isEnabled() */) { pActions[Action::MatchTransaction]->setEnabled(true); } if (importedCount != 0 || matchedCount != 0) pActions[Action::AcceptTransaction]->setEnabled(true); if (matchedCount != 0) { pActions[Action::MatchTransaction]->setEnabled(true); pActions[Action::MatchTransaction]->setText(i18nc("Button text for unmatch transaction", "Unmatch")); pActions[Action::MatchTransaction]->setIcon(QIcon("process-stop")); } if (d->m_selectedTransactions.count() > 1) { pActions[Action::CombineTransactions]->setEnabled(true); } if (d->m_selectedTransactions.count() >= 2) { int singleSplitTransactions = 0; int multipleSplitTransactions = 0; foreach (const KMyMoneyRegister::SelectedTransaction& st, d->m_selectedTransactions) { switch (st.transaction().splitCount()) { case 0: break; case 1: singleSplitTransactions++; break; default: multipleSplitTransactions++; break; } } if (singleSplitTransactions > 0 && multipleSplitTransactions == 1) { pActions[Action::CopySplits]->setEnabled(true); } } if (d->m_selectedTransactions.count() >= 2) { int singleSplitTransactions = 0; int multipleSplitTransactions = 0; foreach(const KMyMoneyRegister::SelectedTransaction& st, d->m_selectedTransactions) { switch(st.transaction().splitCount()) { case 0: break; case 1: singleSplitTransactions++; break; default: multipleSplitTransactions++; break; } } if(singleSplitTransactions > 0 && multipleSplitTransactions == 1) { pActions[Action::CopySplits]->setEnabled(true); } } } else { pActions[Action::AssignTransactionsNumber]->setEnabled(d->m_transactionEditor->canAssignNumber()); pActions[Action::NewTransaction]->setEnabled(false); pActions[Action::DeleteTransaction]->setEnabled(false); QString reason; pActions[Action::EnterTransaction]->setEnabled(d->m_transactionEditor->isComplete(reason)); //FIXME: Port to KDE4 // the next line somehow worked in KDE3 but does not have // any influence under KDE4 /// Works for me when 'reason' is set. Allan pActions[Action::EnterTransaction]->setToolTip(reason); pActions[Action::CancelTransaction]->setEnabled(true); } } } void KGlobalLedgerView::slotAboutToSelectItem(KMyMoneyRegister::RegisterItem* item, bool& okToSelect) { Q_UNUSED(item); slotCancelOrEnterTransactions(okToSelect); } void KGlobalLedgerView::slotUpdateSummaryLine(const KMyMoneyRegister::SelectedTransactions& selection) { Q_D(KGlobalLedgerView); if (selection.count() > 1) { MyMoneyMoney balance; foreach (const KMyMoneyRegister::SelectedTransaction& t, selection) { if (!t.isScheduled()) { balance += t.split().shares(); } } d->m_rightSummaryLabel->setText(QString("%1: %2").arg(QChar(0x2211), balance.formatMoney("", d->m_precision))); } else { if (d->isReconciliationAccount()) { d->m_rightSummaryLabel->setText(i18n("Difference: %1", d->m_totalBalance.formatMoney("", d->m_precision))); } else { if (d->m_currentAccount.accountType() != eMyMoney::Account::Type::Investment) { d->m_rightSummaryLabel->setText(i18n("Balance: %1", d->m_totalBalance.formatMoney("", d->m_precision))); bool showNegative = d->m_totalBalance.isNegative(); if (d->m_currentAccount.accountGroup() == eMyMoney::Account::Type::Liability && !d->m_totalBalance.isZero()) showNegative = !showNegative; if (showNegative) { QPalette palette = d->m_rightSummaryLabel->palette(); palette.setColor(d->m_rightSummaryLabel->foregroundRole(), KMyMoneyGlobalSettings::schemeColor(SchemeColor::Negative)); d->m_rightSummaryLabel->setPalette(palette); } } else { d->m_rightSummaryLabel->setText(i18n("Investment value: %1%2", d->m_balanceIsApproximated ? "~" : "", d->m_totalBalance.formatMoney(MyMoneyFile::instance()->baseCurrency().tradingSymbol(), d->m_precision))); } } } } void KGlobalLedgerView::resizeEvent(QResizeEvent* ev) { Q_D(KGlobalLedgerView); if (d->m_needLoad) d->init(); d->m_register->resize((int)eWidgets::eTransaction::Column::Detail); d->m_form->resize((int)eWidgets::eTransactionForm::Column::Value1); KMyMoneyViewBase::resizeEvent(ev); } void KGlobalLedgerView::slotSetReconcileAccount(const MyMoneyAccount& acc, const QDate& reconciliationDate, const MyMoneyMoney& endingBalance) { Q_D(KGlobalLedgerView); if(d->m_needLoad) d->init(); if (d->m_reconciliationAccount.id() != acc.id()) { // make sure the account is selected if (!acc.id().isEmpty()) slotSelectAccount(acc.id()); d->m_reconciliationAccount = acc; d->m_reconciliationDate = reconciliationDate; d->m_endingBalance = endingBalance; if (acc.accountGroup() == eMyMoney::Account::Type::Liability) d->m_endingBalance = -endingBalance; d->m_newAccountLoaded = true; if (acc.id().isEmpty()) { d->m_buttonbar->removeAction(pActions[Action::PostponeReconciliation]); d->m_buttonbar->removeAction(pActions[Action::FinishReconciliation]); } else { d->m_buttonbar->addAction(pActions[Action::PostponeReconciliation]); d->m_buttonbar->addAction(pActions[Action::FinishReconciliation]); // when we start reconciliation, we need to reload the view // because no data has been changed. When postponing or finishing // reconciliation, the data change in the engine takes care of updateing // the view. refresh(); } } } void KGlobalLedgerView::slotSetReconcileAccount(const MyMoneyAccount& acc, const QDate& reconciliationDate) { slotSetReconcileAccount(acc, reconciliationDate, MyMoneyMoney()); } void KGlobalLedgerView::slotSetReconcileAccount(const MyMoneyAccount& acc) { slotSetReconcileAccount(acc, QDate(), MyMoneyMoney()); } void KGlobalLedgerView::slotSetReconcileAccount() { slotSetReconcileAccount(MyMoneyAccount(), QDate(), MyMoneyMoney()); } void KGlobalLedgerView::slotShowTransactionMenu(const MyMoneySplit& sp) { Q_UNUSED(sp) pMenus[Menu::Transaction]->exec(QCursor::pos()); } void KGlobalLedgerView::slotContinueReconciliation(eView::Schedules::Requester req) { // check if request has been made because of reconciliation if (req != eView::Schedules::Requester::Reconciliation) return; Q_D(KGlobalLedgerView); const auto file = MyMoneyFile::instance(); MyMoneyAccount account; try { account = file->account(d->m_currentAccount.id()); // get rid of previous run. delete d->m_endingBalanceDlg; d->m_endingBalanceDlg = new KEndingBalanceDlg(account, this); if (account.isAssetLiability()) { if (d->m_endingBalanceDlg->exec() == QDialog::Accepted) { if (KMyMoneyGlobalSettings::autoReconciliation()) { MyMoneyMoney startBalance = d->m_endingBalanceDlg->previousBalance(); MyMoneyMoney endBalance = d->m_endingBalanceDlg->endingBalance(); QDate endDate = d->m_endingBalanceDlg->statementDate(); QList > transactionList; MyMoneyTransactionFilter filter(account.id()); filter.addState((int)eMyMoney::TransactionFilter::State::Cleared); filter.addState((int)eMyMoney::TransactionFilter::State::NotReconciled); filter.setDateFilter(QDate(), endDate); filter.setConsiderCategory(false); filter.setReportAllSplits(true); file->transactionList(transactionList, filter); QList > result = d->automaticReconciliation(account, transactionList, endBalance - startBalance); if (!result.empty()) { QString message = i18n("KMyMoney has detected transactions matching your reconciliation data.\nWould you like KMyMoney to clear these transactions for you?"); if (KMessageBox::questionYesNo(this, message, i18n("Automatic reconciliation"), KStandardGuiItem::yes(), KStandardGuiItem::no(), "AcceptAutomaticReconciliation") == KMessageBox::Yes) { // mark the transactions cleared KMyMoneyRegister::SelectedTransactions oldSelection = d->m_selectedTransactions; d->m_selectedTransactions.clear(); QListIterator > itTransactionSplitResult(result); while (itTransactionSplitResult.hasNext()) { const QPair &transactionSplit = itTransactionSplitResult.next(); d->m_selectedTransactions.append(KMyMoneyRegister::SelectedTransaction(transactionSplit.first, transactionSplit.second, QString())); } // mark all transactions in d->m_selectedTransactions as 'Cleared' d->markTransaction(eMyMoney::Split::State::Cleared); d->m_selectedTransactions = oldSelection; } } } if (!file->isStandardAccount(account.id()) && account.isAssetLiability()) { emit switchViewRequested(View::Ledgers); Models::instance()->accountsModel()->slotReconcileAccount(account, d->m_endingBalanceDlg->statementDate(), d->m_endingBalanceDlg->endingBalance()); slotSetReconcileAccount(account, d->m_endingBalanceDlg->statementDate(), d->m_endingBalanceDlg->endingBalance()); // check if the user requests us to create interest // or charge transactions. auto ti = d->m_endingBalanceDlg->interestTransaction(); auto tc = d->m_endingBalanceDlg->chargeTransaction(); MyMoneyFileTransaction ft; try { if (ti != MyMoneyTransaction()) { MyMoneyFile::instance()->addTransaction(ti); } if (tc != MyMoneyTransaction()) { MyMoneyFile::instance()->addTransaction(tc); } ft.commit(); } catch (const MyMoneyException &e) { qWarning("interest transaction not stored: '%s'", qPrintable(e.what())); } // reload the account object as it might have changed in the meantime d->m_reconciliationAccount = file->account(account.id()); updateActions(d->m_currentAccount); updateLedgerActionsInternal(); // slotUpdateActions(); } } } } catch (const MyMoneyException &) { } } void KGlobalLedgerView::slotLedgerSelected(const QString& _accId, const QString& transaction) { auto acc = MyMoneyFile::instance()->account(_accId); QString accId(_accId); switch (acc.accountType()) { case Account::Type::Stock: // if a stock account is selected, we show the // the corresponding parent (investment) account acc = MyMoneyFile::instance()->account(acc.parentAccountId()); accId = acc.id(); // intentional fall through case Account::Type::Checkings: case Account::Type::Savings: case Account::Type::Cash: case Account::Type::CreditCard: case Account::Type::Loan: case Account::Type::Asset: case Account::Type::Liability: case Account::Type::AssetLoan: case Account::Type::Income: case Account::Type::Expense: case Account::Type::Investment: case Account::Type::Equity: if (!isVisible()) emit switchViewRequested(View::Ledgers); slotSelectAccount(accId, transaction); break; case Account::Type::CertificateDep: case Account::Type::MoneyMarket: case Account::Type::Currency: qDebug("No ledger view available for account type %d", (int)acc.accountType()); break; default: qDebug("Unknown account type %d in KMyMoneyView::slotLedgerSelected", (int)acc.accountType()); break; } } void KGlobalLedgerView::slotSelectAccount(const MyMoneyObject& obj) { Q_D(KGlobalLedgerView); if (typeid(obj) != typeid(MyMoneyAccount)) return/* false */; - if (d->m_recursion) - return /* false */; - - d->m_recursion = true; - const auto& acc = dynamic_cast(obj); - /*bool rc =*/ slotSelectAccount(acc.id(), QString()); - d->m_recursion = false; -// return rc; + d->m_lastSelectedAccountID = obj.id(); } void KGlobalLedgerView::slotSelectAccount(const QString& id) { slotSelectAccount(id, QString()); } bool KGlobalLedgerView::slotSelectAccount(const QString& id, const QString& transactionId) { Q_D(KGlobalLedgerView); auto rc = true; if (!id.isEmpty()) { if (d->m_currentAccount.id() != id) { try { d->m_currentAccount = MyMoneyFile::instance()->account(id); // if a stock account is selected, we show the // the corresponding parent (investment) account if (d->m_currentAccount.isInvest()) { d->m_currentAccount = MyMoneyFile::instance()->account(d->m_currentAccount.parentAccountId()); } + d->m_lastSelectedAccountID = d->m_currentAccount.id(); d->m_newAccountLoaded = true; refresh(); } catch (const MyMoneyException &) { qDebug("Unable to retrieve account %s", qPrintable(id)); rc = false; } } else { // we need to refresh m_account.m_accountList, a child could have been deleted d->m_currentAccount = MyMoneyFile::instance()->account(id); emit objectSelected(d->m_currentAccount); } d->selectTransaction(transactionId); } return rc; } bool KGlobalLedgerView::selectEmptyTransaction() { Q_D(KGlobalLedgerView); bool rc = false; if (!d->m_inEditMode) { // in case we don't know the type of transaction to be created, // have at least one selected transaction and the id of // this transaction is not empty, we take it as template for the // transaction to be created KMyMoneyRegister::SelectedTransactions list(d->m_register); if ((d->m_action == eWidgets::eRegister::Action::None) && (!list.isEmpty()) && (!list[0].transaction().id().isEmpty())) { // the new transaction to be created will have the same type // as the one that currently has the focus KMyMoneyRegister::Transaction* t = dynamic_cast(d->m_register->focusItem()); if (t) d->m_action = t->actionType(); d->m_register->clearSelection(); } // if we still don't have an idea which type of transaction // to create, we use the default. if (d->m_action == eWidgets::eRegister::Action::None) { d->setupDefaultAction(); } d->m_register->selectItem(d->m_register->lastItem()); d->m_register->updateRegister(); rc = true; } return rc; } TransactionEditor* KGlobalLedgerView::startEdit(const KMyMoneyRegister::SelectedTransactions& list) { Q_D(KGlobalLedgerView); // we use the warnlevel to keep track, if we have to warn the // user that some or all splits have been reconciled or if the // user cannot modify the transaction if at least one split // has the status frozen. The following value are used: // // 0 - no sweat, user can modify // 1 - user should be warned that at least one split has been reconciled // already // 2 - user will be informed, that this transaction cannot be changed anymore int warnLevel = list.warnLevel(); Q_ASSERT(warnLevel < 2); // otherwise the edit action should not be enabled switch (warnLevel) { case 0: break; case 1: if (KMessageBox::warningContinueCancel(this, i18n( "At least one split of the selected transactions has been reconciled. " "Do you wish to continue to edit the transactions anyway?" ), i18n("Transaction already reconciled"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), "EditReconciledTransaction") == KMessageBox::Cancel) { warnLevel = 2; } break; case 2: KMessageBox::sorry(this, i18n("At least one split of the selected transactions has been frozen. " "Editing the transactions is therefore prohibited."), i18n("Transaction already frozen")); break; case 3: KMessageBox::sorry(this, i18n("At least one split of the selected transaction references an account that has been closed. " "Editing the transactions is therefore prohibited."), i18n("Account closed")); break; } if (warnLevel > 1) return 0; TransactionEditor* editor = 0; KMyMoneyRegister::Transaction* item = dynamic_cast(d->m_register->focusItem()); if (item) { // in case the current focus item is not selected, we move the focus to the first selected transaction if (!item->isSelected()) { KMyMoneyRegister::RegisterItem* p; for (p = d->m_register->firstItem(); p; p = p->nextItem()) { KMyMoneyRegister::Transaction* t = dynamic_cast(p); if (t && t->isSelected()) { d->m_register->setFocusItem(t); item = t; break; } } } // decide, if we edit in the register or in the form TransactionEditorContainer* parent; if (d->m_formFrame->isVisible()) parent = d->m_form; else { parent = d->m_register; } editor = item->createEditor(parent, list, KGlobalLedgerViewPrivate::m_lastPostDate); // check that we use the same transaction commodity in all selected transactions // if not, we need to update this in the editor's list. The user can also bail out // of this operation which means that we have to stop editing here. if (editor) { if (!editor->fixTransactionCommodity(d->m_currentAccount)) { // if the user wants to quit, we need to destroy the editor // and bail out delete editor; editor = 0; } } if (editor) { if (parent == d->m_register) { // make sure, the height of the table is correct d->m_register->updateRegister(KMyMoneyGlobalSettings::ledgerLens() | !KMyMoneyGlobalSettings::transactionForm()); } d->m_inEditMode = true; connect(editor, &TransactionEditor::transactionDataSufficient, pActions[Action::EnterTransaction], &QAction::setEnabled); connect(editor, &TransactionEditor::returnPressed, pActions[Action::EnterTransaction], &QAction::trigger); connect(editor, &TransactionEditor::escapePressed, pActions[Action::CancelTransaction], &QAction::trigger); connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, editor, &TransactionEditor::slotReloadEditWidgets); connect(editor, &TransactionEditor::finishEdit, this, &KGlobalLedgerView::slotLeaveEditMode); connect(editor, &TransactionEditor::objectCreation, d->m_mousePressFilter, &MousePressFilter::setFilterDeactive); connect(editor, &TransactionEditor::assignNumber, this, &KGlobalLedgerView::slotAssignNumber); connect(editor, &TransactionEditor::lastPostDateUsed, this, &KGlobalLedgerView::slotKeepPostDate); // create the widgets, place them in the parent and load them with data // setup tab order d->m_tabOrderWidgets.clear(); editor->setup(d->m_tabOrderWidgets, d->m_currentAccount, d->m_action); Q_ASSERT(!d->m_tabOrderWidgets.isEmpty()); // install event filter in all taborder widgets QWidgetList::const_iterator it_w = d->m_tabOrderWidgets.constBegin(); for (; it_w != d->m_tabOrderWidgets.constEnd(); ++it_w) { (*it_w)->installEventFilter(this); } // Install a filter that checks if a mouse press happened outside // of one of our own widgets. qApp->installEventFilter(d->m_mousePressFilter); // Check if the editor has some preference on where to set the focus // If not, set the focus to the first widget in the tab order QWidget* focusWidget = editor->firstWidget(); if (!focusWidget) focusWidget = d->m_tabOrderWidgets.first(); // for some reason, this only works reliably if delayed a bit QTimer::singleShot(10, focusWidget, SLOT(setFocus())); // preset to 'I have no idea which type to create' for the next round. d->m_action = eWidgets::eRegister::Action::None; } } return editor; } void KGlobalLedgerView::slotTransactionsContextMenuRequested() { Q_D(KGlobalLedgerView); auto transactions = d->m_selectedTransactions; updateLedgerActionsInternal(); // emit transactionsSelected(d->m_selectedTransactions); // that should select MyMoneySchedule in KScheduledView if (!transactions.isEmpty() && transactions.first().isScheduled()) emit contextMenuRequested(MyMoneyFile::instance()->schedule(transactions.first().scheduleId())); else emit transactionsContextMenuRequested(d->m_selectedTransactions); } void KGlobalLedgerView::slotLeaveEditMode(const KMyMoneyRegister::SelectedTransactions& list) { Q_D(KGlobalLedgerView); d->m_inEditMode = false; qApp->removeEventFilter(d->m_mousePressFilter); // a possible focusOut event may have removed the focus, so we // install it back again. d->m_register->focusItem()->setFocus(true); // if we come back from editing a new item, we make sure that // we always select the very last known transaction entry no // matter if the transaction has been created or not. if (list.count() && list[0].transaction().id().isEmpty()) { // block signals to prevent some infinite loops that might occur here. d->m_register->blockSignals(true); d->m_register->clearSelection(); KMyMoneyRegister::RegisterItem* p = d->m_register->lastItem(); if (p && p->prevItem()) p = p->prevItem(); d->m_register->selectItem(p); d->m_register->updateRegister(true); d->m_register->blockSignals(false); // we need to update the form manually as sending signals was blocked KMyMoneyRegister::Transaction* t = dynamic_cast(p); if (t) d->m_form->slotSetTransaction(t); } else { if (!KMyMoneySettings::transactionForm()) { // update the row height of the transactions because it might differ between viewing/editing mode when not using the transaction form d->m_register->blockSignals(true); d->m_register->updateRegister(true); d->m_register->blockSignals(false); } } d->m_needsRefresh = true; // TODO: Why transaction in view doesn't update without this? if (d->m_needsRefresh) refresh(); d->m_register->setFocus(); } bool KGlobalLedgerView::focusNextPrevChild(bool next) { Q_D(KGlobalLedgerView); bool rc = false; // qDebug("KGlobalLedgerView::focusNextPrevChild(editmode=%s)", m_inEditMode ? "true" : "false"); if (d->m_inEditMode) { QWidget *w = 0; w = qApp->focusWidget(); // qDebug("w = %p", w); int currentWidgetIndex = d->m_tabOrderWidgets.indexOf(w); while (w && currentWidgetIndex == -1) { // qDebug("'%s' not in list, use parent", qPrintable(w->objectName())); w = w->parentWidget(); currentWidgetIndex = d->m_tabOrderWidgets.indexOf(w); } if (currentWidgetIndex != -1) { // if(w) qDebug("tab order is at '%s'", qPrintable(w->objectName())); currentWidgetIndex += next ? 1 : -1; if (currentWidgetIndex < 0) currentWidgetIndex = d->m_tabOrderWidgets.size() - 1; else if (currentWidgetIndex >= d->m_tabOrderWidgets.size()) currentWidgetIndex = 0; w = d->m_tabOrderWidgets[currentWidgetIndex]; // qDebug("currentWidgetIndex = %d, w = %p", currentWidgetIndex, w); if (((w->focusPolicy() & Qt::TabFocus) == Qt::TabFocus) && w->isVisible() && w->isEnabled()) { // qDebug("Selecting '%s' (%p) as focus", qPrintable(w->objectName()), w); w->setFocus(); rc = true; } } } else rc = KMyMoneyViewBase::focusNextPrevChild(next); return rc; } bool KGlobalLedgerView::eventFilter(QObject* o, QEvent* e) { Q_D(KGlobalLedgerView); bool rc = false; // Need to capture mouse position here as QEvent::ToolTip is too slow d->m_tooltipPosn = QCursor::pos(); if (e->type() == QEvent::KeyPress) { if (d->m_inEditMode) { // qDebug("object = %s, key = %d", o->className(), k->key()); if (o == d->m_register) { // we hide all key press events from the register // while editing a transaction rc = true; } } } if (!rc) rc = KMyMoneyViewBase::eventFilter(o, e); return rc; } void KGlobalLedgerView::slotSortOptions() { Q_D(KGlobalLedgerView); QPointer dlg = new KSortOptionDlg(this); QString key; QString sortOrder, def; if (d->isReconciliationAccount()) { key = "kmm-sort-reconcile"; def = KMyMoneyGlobalSettings::sortReconcileView(); } else { key = "kmm-sort-std"; def = KMyMoneyGlobalSettings::sortNormalView(); } // check if we have an account override of the sort order if (!d->m_currentAccount.value(key).isEmpty()) sortOrder = d->m_currentAccount.value(key); QString oldOrder = sortOrder; dlg->setSortOption(sortOrder, def); if (dlg->exec() == QDialog::Accepted) { if (dlg != 0) { sortOrder = dlg->sortOption(); if (sortOrder != oldOrder) { if (sortOrder.isEmpty()) { d->m_currentAccount.deletePair(key); } else { d->m_currentAccount.setValue(key, sortOrder); } MyMoneyFileTransaction ft; try { MyMoneyFile::instance()->modifyAccount(d->m_currentAccount); ft.commit(); } catch (const MyMoneyException &e) { qDebug("Unable to update sort order for account '%s': %s", qPrintable(d->m_currentAccount.name()), qPrintable(e.what())); } } } } delete dlg; } void KGlobalLedgerView::slotToggleTransactionMark(KMyMoneyRegister::Transaction* /* t */) { Q_D(KGlobalLedgerView); if (!d->m_inEditMode) { slotToggleReconciliationFlag(); } } void KGlobalLedgerView::slotKeepPostDate(const QDate& date) { KGlobalLedgerViewPrivate::m_lastPostDate = date; } QString KGlobalLedgerView::accountId() const { Q_D(const KGlobalLedgerView); return d->m_currentAccount.id(); } bool KGlobalLedgerView::canCreateTransactions(QString& tooltip) const { Q_D(const KGlobalLedgerView); bool rc = true; // we can only create transactions in the ledger view so // we check that this is the active page if(!isVisible()) { tooltip = i18n("Creating transactions can only be performed in the ledger view"); rc = false; } if (d->m_currentAccount.id().isEmpty()) { tooltip = i18n("Cannot create transactions when no account is selected."); rc = false; } if (d->m_currentAccount.accountGroup() == eMyMoney::Account::Type::Income || d->m_currentAccount.accountGroup() == eMyMoney::Account::Type::Expense) { tooltip = i18n("Cannot create transactions in the context of a category."); d->showTooltip(tooltip); rc = false; } if (d->m_currentAccount.isClosed()) { tooltip = i18n("Cannot create transactions in a closed account."); d->showTooltip(tooltip); rc = false; } return rc; } bool KGlobalLedgerView::canModifyTransactions(const KMyMoneyRegister::SelectedTransactions& list, QString& tooltip) const { Q_D(const KGlobalLedgerView); return d->canProcessTransactions(list, tooltip) && list.canModify(); } bool KGlobalLedgerView::canDuplicateTransactions(const KMyMoneyRegister::SelectedTransactions& list, QString& tooltip) const { Q_D(const KGlobalLedgerView); return d->canProcessTransactions(list, tooltip) && list.canDuplicate(); } bool KGlobalLedgerView::canEditTransactions(const KMyMoneyRegister::SelectedTransactions& list, QString& tooltip) const { Q_D(const KGlobalLedgerView); // check if we can edit the list of transactions. We can edit, if // // a) no mix of standard and investment transactions exist // b) if a split transaction is selected, this is the only selection // c) none of the splits is frozen // d) the transaction having the current focus is selected // check for d) if (!d->canProcessTransactions(list, tooltip)) return false; // check for c) if (list.warnLevel() == 2) { tooltip = i18n("Cannot edit transactions with frozen splits."); d->showTooltip(tooltip); return false; } bool rc = true; int investmentTransactions = 0; int normalTransactions = 0; if (d->m_currentAccount.accountGroup() == eMyMoney::Account::Type::Income || d->m_currentAccount.accountGroup() == eMyMoney::Account::Type::Expense) { tooltip = i18n("Cannot edit transactions in the context of a category."); d->showTooltip(tooltip); rc = false; } if (d->m_currentAccount.isClosed()) { tooltip = i18n("Cannot create or edit any transactions in Account %1 as it is closed", d->m_currentAccount.name()); d->showTooltip(tooltip); rc = false; } KMyMoneyRegister::SelectedTransactions::const_iterator it_t; QString action; for (it_t = list.begin(); rc && it_t != list.end(); ++it_t) { if ((*it_t).transaction().id().isEmpty()) { tooltip.clear(); rc = false; continue; } if (KMyMoneyUtils::transactionType((*it_t).transaction()) == KMyMoneyUtils::InvestmentTransaction) { if (action.isEmpty()) { action = (*it_t).split().action(); continue; } if (action == (*it_t).split().action()) { continue; } else { tooltip = (i18n("Cannot edit mixed investment action/type transactions together.")); d->showTooltip(tooltip); rc = false; break; } } if (KMyMoneyUtils::transactionType((*it_t).transaction()) == KMyMoneyUtils::InvestmentTransaction) ++investmentTransactions; else ++normalTransactions; // check for a) if (investmentTransactions != 0 && normalTransactions != 0) { tooltip = i18n("Cannot edit investment transactions and non-investment transactions together."); d->showTooltip(tooltip); rc = false; break; } // check for b) but only for normalTransactions if ((*it_t).transaction().splitCount() > 2 && normalTransactions != 0) { if (list.count() > 1) { tooltip = i18n("Cannot edit multiple split transactions at once."); d->showTooltip(tooltip); rc = false; break; } } } // check for multiple transactions being selected in an investment account // we do not allow editing in this case: https://bugs.kde.org/show_bug.cgi?id=240816 // later on, we might allow to edit investment transactions of the same type /// Can now disable the following check. /* if (rc == true && investmentTransactions > 1) { tooltip = i18n("Cannot edit multiple investment transactions at once"); rc = false; }*/ // now check that we have the correct account type for investment transactions if (rc == true && investmentTransactions != 0) { if (d->m_currentAccount.accountType() != eMyMoney::Account::Type::Investment) { tooltip = i18n("Cannot edit investment transactions in the context of this account."); rc = false; } } return rc; } void KGlobalLedgerView::slotMoveToAccount(const QString& id) { Q_D(KGlobalLedgerView); // close the menu, if it is still open if (pMenus[Menu::Transaction]->isVisible()) pMenus[Menu::Transaction]->close(); if (!d->m_selectedTransactions.isEmpty()) { MyMoneyFileTransaction ft; try { foreach (const auto selection, d->m_selectedTransactions) { if (d->m_currentAccount.accountType() == eMyMoney::Account::Type::Investment) { d->moveInvestmentTransaction(d->m_currentAccount.id(), id, selection.transaction()); } else { auto changed = false; auto t = selection.transaction(); foreach (const auto split, selection.transaction().splits()) { if (split.accountId() == d->m_currentAccount.id()) { MyMoneySplit s = split; s.setAccountId(id); t.modifySplit(s); changed = true; } } if (changed) { MyMoneyFile::instance()->modifyTransaction(t); } } } ft.commit(); } catch (const MyMoneyException &) { } } } void KGlobalLedgerView::slotUpdateMoveToAccountMenu() { Q_D(KGlobalLedgerView); d->createTransactionMoveMenu(); // in case we were not able to create the selector, we // better get out of here. Anything else would cause // a crash later on (accountSet.load) if (!d->m_moveToAccountSelector) return; if (!d->m_currentAccount.id().isEmpty()) { AccountSet accountSet; if (d->m_currentAccount.accountType() == eMyMoney::Account::Type::Investment) { accountSet.addAccountType(eMyMoney::Account::Type::Investment); } else if (d->m_currentAccount.isAssetLiability()) { accountSet.addAccountType(eMyMoney::Account::Type::Checkings); accountSet.addAccountType(eMyMoney::Account::Type::Savings); accountSet.addAccountType(eMyMoney::Account::Type::Cash); accountSet.addAccountType(eMyMoney::Account::Type::AssetLoan); accountSet.addAccountType(eMyMoney::Account::Type::CertificateDep); accountSet.addAccountType(eMyMoney::Account::Type::MoneyMarket); accountSet.addAccountType(eMyMoney::Account::Type::Asset); accountSet.addAccountType(eMyMoney::Account::Type::Currency); accountSet.addAccountType(eMyMoney::Account::Type::CreditCard); accountSet.addAccountType(eMyMoney::Account::Type::Loan); accountSet.addAccountType(eMyMoney::Account::Type::Liability); } else if (d->m_currentAccount.isIncomeExpense()) { accountSet.addAccountType(eMyMoney::Account::Type::Income); accountSet.addAccountType(eMyMoney::Account::Type::Expense); } accountSet.load(d->m_moveToAccountSelector); // remove those accounts that we currently reference foreach (const auto selection, d->m_selectedTransactions) { foreach (const auto split, selection.transaction().splits()) { d->m_moveToAccountSelector->removeItem(split.accountId()); } } // remove those accounts from the list that are denominated // in a different currency auto list = d->m_moveToAccountSelector->accountList(); QList::const_iterator it_a; for (it_a = list.constBegin(); it_a != list.constEnd(); ++it_a) { auto acc = MyMoneyFile::instance()->account(*it_a); if (acc.currencyId() != d->m_currentAccount.currencyId()) d->m_moveToAccountSelector->removeItem((*it_a)); } } } void KGlobalLedgerView::slotObjectDestroyed(QObject* o) { Q_D(KGlobalLedgerView); if (o == d->m_moveToAccountSelector) { d->m_moveToAccountSelector = nullptr; } } void KGlobalLedgerView::slotCancelOrEnterTransactions(bool& okToSelect) { Q_D(KGlobalLedgerView); static bool oneTime = false; if (!oneTime) { oneTime = true; auto dontShowAgain = "CancelOrEditTransaction"; // qDebug("KMyMoneyApp::slotCancelOrEndEdit"); if (d->m_transactionEditor) { if (KMyMoneyGlobalSettings::focusChangeIsEnter() && pActions[Action::EnterTransaction]->isEnabled()) { slotEnterTransaction(); if (d->m_transactionEditor) { // if at this stage the editor is still there that means that entering the transaction was cancelled // for example by pressing cancel on the exchange rate editor so we must stay in edit mode okToSelect = false; } } else { // okToSelect is preset to true if a cancel of the dialog is useful and false if it is not int rc; KGuiItem noGuiItem = KStandardGuiItem::save(); KGuiItem yesGuiItem = KStandardGuiItem::discard(); KGuiItem cancelGuiItem = KStandardGuiItem::cont(); // if the transaction can't be entered make sure that it can't be entered by pressing no either if (!pActions[Action::EnterTransaction]->isEnabled()) { noGuiItem.setEnabled(false); noGuiItem.setToolTip(pActions[Action::EnterTransaction]->toolTip()); } if (okToSelect == true) { rc = KMessageBox::warningYesNoCancel(this, i18n("

Please select what you want to do: discard the changes, save the changes or continue to edit the transaction.

You can also set an option to save the transaction automatically when e.g. selecting another transaction.

"), i18n("End transaction edit"), yesGuiItem, noGuiItem, cancelGuiItem, dontShowAgain); } else { rc = KMessageBox::warningYesNo(this, i18n("

Please select what you want to do: discard the changes, save the changes or continue to edit the transaction.

You can also set an option to save the transaction automatically when e.g. selecting another transaction.

"), i18n("End transaction edit"), yesGuiItem, noGuiItem, dontShowAgain); } switch (rc) { case KMessageBox::Yes: slotCancelTransaction(); break; case KMessageBox::No: slotEnterTransaction(); // make sure that we'll see this message the next time no matter // if the user has chosen the 'Don't show again' checkbox KMessageBox::enableMessage(dontShowAgain); if (d->m_transactionEditor) { // if at this stage the editor is still there that means that entering the transaction was cancelled // for example by pressing cancel on the exchange rate editor so we must stay in edit mode okToSelect = false; } break; case KMessageBox::Cancel: // make sure that we'll see this message the next time no matter // if the user has chosen the 'Don't show again' checkbox KMessageBox::enableMessage(dontShowAgain); okToSelect = false; break; } } } oneTime = false; } } void KGlobalLedgerView::slotNewSchedule(const MyMoneyTransaction& _t, eMyMoney::Schedule::Occurrence occurrence) { KEditScheduleDlg::newSchedule(_t, occurrence); } void KGlobalLedgerView::slotNewTransactionForm(eWidgets::eRegister::Action id) { Q_D(KGlobalLedgerView); if (!d->m_inEditMode) { d->m_action = id; // since we jump here via code, we have to make sure to react only // if the action is enabled if (pActions[Action::NewTransaction]->isEnabled()) { if (d->createNewTransaction()) { d->m_transactionEditor = d->startEdit(d->m_selectedTransactions); if (d->m_transactionEditor) { KMyMoneyMVCCombo::setSubstringSearchForChildren(this/*d->m_myMoneyView*/, !KMyMoneySettings::stringMatchFromStart()); KMyMoneyPayeeCombo* payeeEdit = dynamic_cast(d->m_transactionEditor->haveWidget("payee")); if (payeeEdit && !d->m_lastPayeeEnteredId.isEmpty()) { // in case we entered a new transaction before and used a payee, // we reuse it here. Save the text to the edit widget, select it // so that hitting any character will start entering another payee. payeeEdit->setSelectedItem(d->m_lastPayeeEnteredId); payeeEdit->lineEdit()->selectAll(); } if (d->m_transactionEditor) { connect(d->m_transactionEditor.data(), &TransactionEditor::statusProgress, this, &KGlobalLedgerView::statusProgress); connect(d->m_transactionEditor.data(), &TransactionEditor::statusMsg, this, &KGlobalLedgerView::statusMsg); connect(d->m_transactionEditor.data(), &TransactionEditor::scheduleTransaction, this, &KGlobalLedgerView::slotNewSchedule); } updateLedgerActionsInternal(); // emit transactionsSelected(d->m_selectedTransactions); } } } } } void KGlobalLedgerView::slotNewTransaction() { slotNewTransactionForm(eWidgets::eRegister::Action::None); } void KGlobalLedgerView::slotEditTransaction() { Q_D(KGlobalLedgerView); // qDebug("KMyMoneyApp::slotTransactionsEdit()"); // since we jump here via code, we have to make sure to react only // if the action is enabled if (pActions[Action::EditTransaction]->isEnabled()) { // as soon as we edit a transaction, we don't remember the last payee entered d->m_lastPayeeEnteredId.clear(); d->m_transactionEditor = d->startEdit(d->m_selectedTransactions); KMyMoneyMVCCombo::setSubstringSearchForChildren(this/*d->m_myMoneyView*/, !KMyMoneySettings::stringMatchFromStart()); updateLedgerActionsInternal(); } } void KGlobalLedgerView::slotDeleteTransaction() { Q_D(KGlobalLedgerView); // since we may jump here via code, we have to make sure to react only // if the action is enabled if (!pActions[Action::DeleteTransaction]->isEnabled()) return; if (d->m_selectedTransactions.isEmpty()) return; if (d->m_selectedTransactions.warnLevel() == 1) { if (KMessageBox::warningContinueCancel(this, i18n("At least one split of the selected transactions has been reconciled. " "Do you wish to delete the transactions anyway?"), i18n("Transaction already reconciled")) == KMessageBox::Cancel) return; } auto msg = i18np("Do you really want to delete the selected transaction?", "Do you really want to delete all %1 selected transactions?", d->m_selectedTransactions.count()); if (KMessageBox::questionYesNo(this, msg, i18n("Delete transaction")) == KMessageBox::Yes) { //KMSTATUS(i18n("Deleting transactions")); d->doDeleteTransactions(); } } void KGlobalLedgerView::slotDuplicateTransaction() { Q_D(KGlobalLedgerView); // since we may jump here via code, we have to make sure to react only // if the action is enabled if (pActions[Action::DuplicateTransaction]->isEnabled()) { KMyMoneyRegister::SelectedTransactions selectionList = d->m_selectedTransactions; KMyMoneyRegister::SelectedTransactions::iterator it_t; int i = 0; int cnt = d->m_selectedTransactions.count(); // KMSTATUS(i18n("Duplicating transactions")); emit statusProgress(0, cnt); MyMoneyFileTransaction ft; MyMoneyTransaction lt; try { foreach (const auto selection, selectionList) { auto t = selection.transaction(); // wipe out any reconciliation information for (auto& split : t.splits()) { split.setReconcileFlag(eMyMoney::Split::State::NotReconciled); split.setReconcileDate(QDate()); split.setBankID(QString()); } // clear invalid data t.setEntryDate(QDate()); t.clearId(); // and set the post date to today t.setPostDate(QDate::currentDate()); MyMoneyFile::instance()->addTransaction(t); lt = t; emit statusProgress(i++, 0); } ft.commit(); // select the new transaction in the ledger if (!d->m_currentAccount.id().isEmpty()) slotLedgerSelected(d->m_currentAccount.id(), lt.id()); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Error"), i18n("Unable to duplicate transaction(s): %1, thrown in %2:%3", e.what(), e.file(), e.line())); } // switch off the progress bar emit statusProgress(-1, -1); } } void KGlobalLedgerView::slotEnterTransaction() { Q_D(KGlobalLedgerView); // since we jump here via code, we have to make sure to react only // if the action is enabled if (pActions[Action::EnterTransaction]->isEnabled()) { // disable the action while we process it to make sure it's processed only once since // d->m_transactionEditor->enterTransactions(newId) will run QCoreApplication::processEvents // we could end up here twice which will cause a crash slotUpdateActions() will enable the action again pActions[Action::EnterTransaction]->setEnabled(false); if (d->m_transactionEditor) { QString accountId = d->m_currentAccount.id(); QString newId; connect(d->m_transactionEditor.data(), &TransactionEditor::balanceWarning, d->m_balanceWarning.data(), &KBalanceWarning::slotShowMessage); if (d->m_transactionEditor->enterTransactions(newId)) { KMyMoneyPayeeCombo* payeeEdit = dynamic_cast(d->m_transactionEditor->haveWidget("payee")); if (payeeEdit && !newId.isEmpty()) { d->m_lastPayeeEnteredId = payeeEdit->selectedItem(); } d->deleteTransactionEditor(); } if (!newId.isEmpty()) { slotLedgerSelected(accountId, newId); } } updateLedgerActionsInternal(); } } void KGlobalLedgerView::slotAcceptTransaction() { Q_D(KGlobalLedgerView); KMyMoneyRegister::SelectedTransactions list = d->m_selectedTransactions; KMyMoneyRegister::SelectedTransactions::const_iterator it_t; int cnt = list.count(); int i = 0; emit statusProgress(0, cnt); MyMoneyFileTransaction ft; try { for (it_t = list.constBegin(); it_t != list.constEnd(); ++it_t) { // reload transaction in case it got changed during the course of this loop MyMoneyTransaction t = MyMoneyFile::instance()->transaction((*it_t).transaction().id()); if (t.isImported()) { t.setImported(false); if (!d->m_currentAccount.id().isEmpty()) { foreach (const auto split, t.splits()) { if (split.accountId() == d->m_currentAccount.id()) { if (split.reconcileFlag() == eMyMoney::Split::State::NotReconciled) { MyMoneySplit s = split; s.setReconcileFlag(eMyMoney::Split::State::Cleared); t.modifySplit(s); } } } } MyMoneyFile::instance()->modifyTransaction(t); } if ((*it_t).split().isMatched()) { // reload split in case it got changed during the course of this loop MyMoneySplit s = t.splitById((*it_t).split().id()); TransactionMatcher matcher(d->m_currentAccount); matcher.accept(t, s); } emit statusProgress(i++, 0); } emit statusProgress(-1, -1); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Error"), i18n("Unable to accept transaction: %1, thrown in %2:%3", e.what(), e.file(), e.line())); } } void KGlobalLedgerView::slotCancelTransaction() { Q_D(KGlobalLedgerView); // since we jump here via code, we have to make sure to react only // if the action is enabled if (pActions[Action::CancelTransaction]->isEnabled()) { // make sure, we block the enter function pActions[Action::EnterTransaction]->setEnabled(false); // qDebug("KMyMoneyApp::slotTransactionsCancel"); d->deleteTransactionEditor(); emit transactionsSelected(d->m_selectedTransactions); } } void KGlobalLedgerView::slotEditSplits() { Q_D(KGlobalLedgerView); // since we jump here via code, we have to make sure to react only // if the action is enabled if (pActions[Action::EditSplits]->isEnabled()) { // as soon as we edit a transaction, we don't remember the last payee entered d->m_lastPayeeEnteredId.clear(); d->m_transactionEditor = d->startEdit(d->m_selectedTransactions); emit transactionsSelected(d->m_selectedTransactions); if (d->m_transactionEditor) { KMyMoneyMVCCombo::setSubstringSearchForChildren(this/*d->m_myMoneyView*/, !KMyMoneySettings::stringMatchFromStart()); if (d->m_transactionEditor->slotEditSplits() == QDialog::Accepted) { MyMoneyFileTransaction ft; try { QString id; connect(d->m_transactionEditor.data(), &TransactionEditor::balanceWarning, d->m_balanceWarning.data(), &KBalanceWarning::slotShowMessage); d->m_transactionEditor->enterTransactions(id); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(this, i18n("Error"), i18n("Unable to modify transaction: %1, thrown in %2:%3", e.what(), e.file(), e.line())); } } } d->deleteTransactionEditor(); emit transactionsSelected(d->m_selectedTransactions); } } void KGlobalLedgerView::slotCopySplits() { Q_D(KGlobalLedgerView); const auto file = MyMoneyFile::instance(); if (d->m_selectedTransactions.count() >= 2) { int singleSplitTransactions = 0; int multipleSplitTransactions = 0; KMyMoneyRegister::SelectedTransaction selectedSourceTransaction; foreach (const auto& st, d->m_selectedTransactions) { switch (st.transaction().splitCount()) { case 0: break; case 1: singleSplitTransactions++; break; default: selectedSourceTransaction = st; multipleSplitTransactions++; break; } } if (singleSplitTransactions > 0 && multipleSplitTransactions == 1) { MyMoneyFileTransaction ft; try { const auto& sourceTransaction = selectedSourceTransaction.transaction(); const auto& sourceSplit = selectedSourceTransaction.split(); foreach (const KMyMoneyRegister::SelectedTransaction& st, d->m_selectedTransactions) { auto t = st.transaction(); // don't process the source transaction if (sourceTransaction.id() == t.id()) { continue; } const auto& baseSplit = st.split(); if (t.splitCount() == 1) { foreach (const auto& split, sourceTransaction.splits()) { // Don't copy the source split, as we already have that // as part of the destination transaction if (split.id() == sourceSplit.id()) { continue; } MyMoneySplit sp(split); // clear the ID and reconciliation state sp.clearId(); sp.setReconcileFlag(eMyMoney::Split::State::NotReconciled); sp.setReconcileDate(QDate()); // in case it is a simple transaction consisting of two splits, // we can adjust the share and value part of the second split we // just created. We need to keep a possible price in mind in case // of different currencies if (sourceTransaction.splitCount() == 2) { sp.setValue(-baseSplit.value()); sp.setShares(-(baseSplit.shares() * baseSplit.price())); } t.addSplit(sp); } file->modifyTransaction(t); } } ft.commit(); } catch (const MyMoneyException &) { qDebug() << "transactionCopySplits() failed"; } } } } void KGlobalLedgerView::slotGoToPayee() { Q_D(KGlobalLedgerView); if (!d->m_payeeGoto.isEmpty()) { try { QString transactionId; if (d->m_selectedTransactions.count() == 1) { transactionId = d->m_selectedTransactions[0].transaction().id(); } // make sure to pass copies, as d->myMoneyView->slotPayeeSelected() overrides // d->m_payeeGoto and d->m_currentAccount while calling slotUpdateActions() QString payeeId = d->m_payeeGoto; QString accountId = d->m_currentAccount.id(); emit openPayeeRequested(payeeId, accountId, transactionId); } catch (const MyMoneyException &) { } } } void KGlobalLedgerView::slotGoToAccount() { Q_D(KGlobalLedgerView); if (!d->m_accountGoto.isEmpty()) { try { QString transactionId; if (d->m_selectedTransactions.count() == 1) { transactionId = d->m_selectedTransactions[0].transaction().id(); } // make sure to pass a copy, as d->myMoneyView->slotLedgerSelected() overrides // d->m_accountGoto while calling slotUpdateActions() slotLedgerSelected(d->m_accountGoto, transactionId); } catch (const MyMoneyException &) { } } } void KGlobalLedgerView::slotMatchTransactions() { Q_D(KGlobalLedgerView); // if the menu action is retrieved it can contain an '&' character for the accelerator causing the comparison to fail if not removed QString transactionActionText = pActions[Action::MatchTransaction]->text(); transactionActionText.remove('&'); if (transactionActionText == i18nc("Button text for match transaction", "Match")) d->transactionMatch(); else d->transactionUnmatch(); } void KGlobalLedgerView::slotCombineTransactions() { qDebug("slotTransactionCombine() not implemented yet"); } void KGlobalLedgerView::slotToggleReconciliationFlag() { Q_D(KGlobalLedgerView); d->markTransaction(eMyMoney::Split::State::Unknown); } void KGlobalLedgerView::slotMarkCleared() { Q_D(KGlobalLedgerView); d->markTransaction(eMyMoney::Split::State::Cleared); } void KGlobalLedgerView::slotMarkReconciled() { Q_D(KGlobalLedgerView); d->markTransaction(eMyMoney::Split::State::Reconciled); } void KGlobalLedgerView::slotMarkNotReconciled() { Q_D(KGlobalLedgerView); d->markTransaction(eMyMoney::Split::State::NotReconciled); } void KGlobalLedgerView::slotSelectAllTransactions() { Q_D(KGlobalLedgerView); if(d->m_needLoad) d->init(); d->m_register->clearSelection(); KMyMoneyRegister::RegisterItem* p = d->m_register->firstItem(); while (p) { KMyMoneyRegister::Transaction* t = dynamic_cast(p); if (t) { if (t->isVisible() && t->isSelectable() && !t->isScheduled() && !t->id().isEmpty()) { t->setSelected(true); } } p = p->nextItem(); } // this is here only to re-paint the items without selecting anything because the data (including the selection) is not really held in the model right now d->m_register->selectAll(); // inform everyone else about the selected items KMyMoneyRegister::SelectedTransactions list(d->m_register); emit transactionsSelected(list); } void KGlobalLedgerView::slotCreateScheduledTransaction() { Q_D(KGlobalLedgerView); if (d->m_selectedTransactions.count() == 1) { // make sure to have the current selected split as first split in the schedule MyMoneyTransaction t = d->m_selectedTransactions[0].transaction(); MyMoneySplit s = d->m_selectedTransactions[0].split(); QString splitId = s.id(); s.clearId(); s.setReconcileFlag(eMyMoney::Split::State::NotReconciled); s.setReconcileDate(QDate()); t.removeSplits(); t.addSplit(s); foreach (const auto split, d->m_selectedTransactions[0].transaction().splits()) { if (split.id() != splitId) { MyMoneySplit s0 = split; s0.clearId(); s0.setReconcileFlag(eMyMoney::Split::State::NotReconciled); s0.setReconcileDate(QDate()); t.addSplit(s0); } } KEditScheduleDlg::newSchedule(t, eMyMoney::Schedule::Occurrence::Monthly); } } void KGlobalLedgerView::slotAssignNumber() { Q_D(KGlobalLedgerView); if (d->m_transactionEditor) d->m_transactionEditor->assignNextNumber(); } void KGlobalLedgerView::slotStartReconciliation() { Q_D(KGlobalLedgerView); // we cannot reconcile standard accounts if (!MyMoneyFile::instance()->isStandardAccount(d->m_currentAccount.id())) emit enterOverdueSchedulesRequested(d->m_currentAccount, eView::Schedules::Requester::Reconciliation); // asynchronous call to KScheduledView::slotEnterOverdueSchedules is made here // after that all activity should be continued in KGlobalLedgerView::slotContinueReconciliation() } void KGlobalLedgerView::slotFinishReconciliation() { Q_D(KGlobalLedgerView); const auto file = MyMoneyFile::instance(); if (!d->m_reconciliationAccount.id().isEmpty()) { // retrieve list of all transactions that are not reconciled or cleared QList > transactionList; MyMoneyTransactionFilter filter(d->m_reconciliationAccount.id()); filter.addState((int)eMyMoney::TransactionFilter::State::Cleared); filter.addState((int)eMyMoney::TransactionFilter::State::NotReconciled); filter.setDateFilter(QDate(), d->m_endingBalanceDlg->statementDate()); filter.setConsiderCategory(false); filter.setReportAllSplits(true); file->transactionList(transactionList, filter); auto balance = MyMoneyFile::instance()->balance(d->m_reconciliationAccount.id(), d->m_endingBalanceDlg->statementDate()); MyMoneyMoney actBalance, clearedBalance; actBalance = clearedBalance = balance; // walk the list of transactions to figure out the balance(s) QList >::const_iterator it; for (it = transactionList.constBegin(); it != transactionList.constEnd(); ++it) { if ((*it).second.reconcileFlag() == eMyMoney::Split::State::NotReconciled) { clearedBalance -= (*it).second.shares(); } } if (d->m_endingBalanceDlg->endingBalance() != clearedBalance) { auto message = i18n("You are about to finish the reconciliation of this account with a difference between your bank statement and the transactions marked as cleared.\n" "Are you sure you want to finish the reconciliation?"); if (KMessageBox::questionYesNo(this, message, i18n("Confirm end of reconciliation"), KStandardGuiItem::yes(), KStandardGuiItem::no()) == KMessageBox::No) return; } MyMoneyFileTransaction ft; // refresh object d->m_reconciliationAccount = file->account(d->m_reconciliationAccount.id()); // Turn off reconciliation mode // Models::instance()->accountsModel()->slotReconcileAccount(MyMoneyAccount(), QDate(), MyMoneyMoney()); // slotSetReconcileAccount(MyMoneyAccount(), QDate(), MyMoneyMoney()); // d->m_myMoneyView->finishReconciliation(d->m_reconciliationAccount); // only update the last statement balance here, if we haven't a newer one due // to download of online statements. if (d->m_reconciliationAccount.value("lastImportedTransactionDate").isEmpty() || QDate::fromString(d->m_reconciliationAccount.value("lastImportedTransactionDate"), Qt::ISODate) < d->m_endingBalanceDlg->statementDate()) { d->m_reconciliationAccount.setValue("lastStatementBalance", d->m_endingBalanceDlg->endingBalance().toString()); // in case we override the last statement balance here, we have to make sure // that we don't show the online balance anymore, as it might be different d->m_reconciliationAccount.deletePair("lastImportedTransactionDate"); } d->m_reconciliationAccount.setLastReconciliationDate(d->m_endingBalanceDlg->statementDate()); // keep a record of this reconciliation d->m_reconciliationAccount.addReconciliation(d->m_endingBalanceDlg->statementDate(), d->m_endingBalanceDlg->endingBalance()); d->m_reconciliationAccount.deletePair("lastReconciledBalance"); d->m_reconciliationAccount.deletePair("statementBalance"); d->m_reconciliationAccount.deletePair("statementDate"); try { // update the account data file->modifyAccount(d->m_reconciliationAccount); /* // collect the list of cleared splits for this account filter.clear(); filter.addAccount(d->m_reconciliationAccount.id()); filter.addState(eMyMoney::TransactionFilter::Cleared); filter.setConsiderCategory(false); filter.setReportAllSplits(true); file->transactionList(transactionList, filter); */ // walk the list of transactions/splits and mark the cleared ones as reconciled QList >::iterator it; for (it = transactionList.begin(); it != transactionList.end(); ++it) { MyMoneySplit sp = (*it).second; // skip the ones that are not marked cleared if (sp.reconcileFlag() != eMyMoney::Split::State::Cleared) continue; // always retrieve a fresh copy of the transaction because we // might have changed it already with another split MyMoneyTransaction t = file->transaction((*it).first.id()); sp.setReconcileFlag(eMyMoney::Split::State::Reconciled); sp.setReconcileDate(d->m_endingBalanceDlg->statementDate()); t.modifySplit(sp); // update the engine ... file->modifyTransaction(t); // ... and the list (*it) = qMakePair(t, sp); } ft.commit(); // reload account data from engine as the data might have changed in the meantime d->m_reconciliationAccount = file->account(d->m_reconciliationAccount.id()); emit accountReconciled(d->m_reconciliationAccount, d->m_endingBalanceDlg->statementDate(), d->m_endingBalanceDlg->previousBalance(), d->m_endingBalanceDlg->endingBalance(), transactionList); } catch (const MyMoneyException &) { qDebug("Unexpected exception when setting cleared to reconcile"); } // Turn off reconciliation mode Models::instance()->accountsModel()->slotReconcileAccount(MyMoneyAccount(), QDate(), MyMoneyMoney()); slotSetReconcileAccount(MyMoneyAccount(), QDate(), MyMoneyMoney()); } // Turn off reconciliation mode d->m_reconciliationAccount = MyMoneyAccount(); updateActions(d->m_currentAccount); updateLedgerActionsInternal(); d->loadView(); // slotUpdateActions(); } void KGlobalLedgerView::slotPostponeReconciliation() { Q_D(KGlobalLedgerView); MyMoneyFileTransaction ft; const auto file = MyMoneyFile::instance(); if (!d->m_reconciliationAccount.id().isEmpty()) { // refresh object d->m_reconciliationAccount = file->account(d->m_reconciliationAccount.id()); // Turn off reconciliation mode // Models::instance()->accountsModel()->slotReconcileAccount(MyMoneyAccount(), QDate(), MyMoneyMoney()); // slotSetReconcileAccount(MyMoneyAccount(), QDate(), MyMoneyMoney()); // d->m_myMoneyView->finishReconciliation(d->m_reconciliationAccount); d->m_reconciliationAccount.setValue("lastReconciledBalance", d->m_endingBalanceDlg->previousBalance().toString()); d->m_reconciliationAccount.setValue("statementBalance", d->m_endingBalanceDlg->endingBalance().toString()); d->m_reconciliationAccount.setValue("statementDate", d->m_endingBalanceDlg->statementDate().toString(Qt::ISODate)); try { file->modifyAccount(d->m_reconciliationAccount); ft.commit(); d->m_reconciliationAccount = MyMoneyAccount(); updateActions(d->m_currentAccount); updateLedgerActionsInternal(); // slotUpdateActions(); } catch (const MyMoneyException &) { qDebug("Unexpected exception when setting last reconcile info into account"); ft.rollback(); d->m_reconciliationAccount = file->account(d->m_reconciliationAccount.id()); } // Turn off reconciliation mode Models::instance()->accountsModel()->slotReconcileAccount(MyMoneyAccount(), QDate(), MyMoneyMoney()); slotSetReconcileAccount(MyMoneyAccount(), QDate(), MyMoneyMoney()); d->loadView(); } } void KGlobalLedgerView::slotOpenAccount() { Q_D(KGlobalLedgerView); if (!MyMoneyFile::instance()->isStandardAccount(d->m_currentAccount.id())) slotLedgerSelected(d->m_currentAccount.id(), QString()); } diff --git a/kmymoney/views/kgloballedgerview_p.h b/kmymoney/views/kgloballedgerview_p.h index 037ba5964..a2022105d 100644 --- a/kmymoney/views/kgloballedgerview_p.h +++ b/kmymoney/views/kgloballedgerview_p.h @@ -1,1631 +1,1632 @@ /*************************************************************************** kgloballedgerview_p.h - description ------------------- begin : Wed Jul 26 2006 copyright : (C) 2006 by Thomas Baumgart email : Thomas Baumgart (C) 2017 by Łukasz Wojniłowicz ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #ifndef KGLOBALLEDGERVIEW_P_H #define KGLOBALLEDGERVIEW_P_H #include "kgloballedgerview.h" // ---------------------------------------------------------------------------- // QT Includes #include #include #include #include #include #include #include #include #include #include // ---------------------------------------------------------------------------- // KDE Includes #include #include #include #include // ---------------------------------------------------------------------------- // Project Includes #include "kmymoneyviewbase_p.h" #include "kendingbalancedlg.h" #include "kmymoneyaccountselector.h" #include "kmymoneyutils.h" #include "mymoneyexception.h" #include "mymoneymoney.h" #include "mymoneyaccount.h" #include "mymoneyfile.h" #include "kmymoneyaccountcombo.h" #include "kbalancewarning.h" #include "transactionmatcher.h" #include "tabbar.h" #include "register.h" #include "transactioneditor.h" #include "selectedtransactions.h" #include "kmymoneyglobalsettings.h" #include "registersearchline.h" #include "scheduledtransaction.h" #include "accountsmodel.h" #include "models.h" #include "mymoneyprice.h" #include "mymoneyschedule.h" #include "mymoneysecurity.h" #include "mymoneytransaction.h" #include "mymoneytransactionfilter.h" #include "mymoneysplit.h" #include "mymoneypayee.h" #include "mymoneytracer.h" #include "transaction.h" #include "transactionform.h" #include "fancydategroupmarkers.h" #include "widgetenums.h" #include "mymoneyenums.h" #include "modelenums.h" #include "menuenums.h" #include #ifdef KMM_DEBUG #include "mymoneyutils.h" #endif using namespace eMenu; using namespace eMyMoney; /** * helper class implementing an event filter to detect mouse button press * events on widgets outside a given set of widgets. This is used internally * to detect when to leave the edit mode. */ class MousePressFilter : public QObject { Q_OBJECT public: explicit MousePressFilter(QWidget* parent = nullptr) : QObject(parent), m_lastMousePressEvent(0), m_filterActive(true) { } /** * Add widget @p w to the list of possible parent objects. See eventFilter() how * they will be used. */ void addWidget(QWidget* w) { m_parents.append(w); } public Q_SLOTS: /** * This slot allows to activate/deactivate the filter. By default the * filter is active. * * @param state Allows to activate (@a true) or deactivate (@a false) the filter */ void setFilterActive(bool state = true) { m_filterActive = state; } /** * This slot allows to activate/deactivate the filter. By default the * filter is active. * * @param state Allows to deactivate (@a true) or activate (@a false) the filter */ void setFilterDeactive(bool state = false) { setFilterActive(!state); } protected: /** * This method checks if the widget @p child is a child of * the widget @p parent and returns either @a true or @a false. * * @param child pointer to child widget * @param parent pointer to parent widget * @retval true @p child points to widget which has @p parent as parent or grand-parent * @retval false @p child points to a widget which is not related to @p parent */ bool isChildOf(QWidget* child, QWidget* parent) { // QDialogs cannot be detected directly, but it can be assumed, // that events on a widget that do not have a parent widget within // our application are dialogs. if (!child->parentWidget()) return true; while (child) { // if we are a child of the given parent, we have a match if (child == parent) return true; // if we are at the application level, we don't have a match if (child->inherits("KMyMoneyApp")) return false; // If one of the ancestors is a KPassivePopup or a KDialog or a popup widget then // it's as if it is a child of our own because these widgets could // appear during transaction entry (message boxes, completer widgets) if (dynamic_cast(child) || ((child->windowFlags() & Qt::Popup) && /*child != kmymoney*/ !child->parentWidget())) // has no parent, then it must be top-level window return true; child = child->parentWidget(); } return false; } /** * Reimplemented from base class. Sends out the mousePressedOnExternalWidget() signal * if object @p o points to an object which is not a child widget of any added previously * using the addWidget() method. The signal is sent out only once for each event @p e. * * @param o pointer to QObject * @param e pointer to QEvent * @return always returns @a false */ bool eventFilter(QObject* o, QEvent* e) { if (m_filterActive) { if (e->type() == QEvent::MouseButtonPress && !m_lastMousePressEvent) { QWidget* w = qobject_cast(o); if (!w) { return QObject::eventFilter(o, e); } QList::const_iterator it_w; for (it_w = m_parents.constBegin(); it_w != m_parents.constEnd(); ++it_w) { if (isChildOf(w, (*it_w))) { m_lastMousePressEvent = e; break; } } if (it_w == m_parents.constEnd()) { m_lastMousePressEvent = e; bool rc = false; emit mousePressedOnExternalWidget(rc); } } if (e->type() != QEvent::MouseButtonPress) { m_lastMousePressEvent = 0; } } return false; } Q_SIGNALS: void mousePressedOnExternalWidget(bool&); private: QList m_parents; QEvent* m_lastMousePressEvent; bool m_filterActive; }; class KGlobalLedgerViewPrivate : public KMyMoneyViewBasePrivate { Q_DECLARE_PUBLIC(KGlobalLedgerView) public: explicit KGlobalLedgerViewPrivate(KGlobalLedgerView *qq) : q_ptr(qq), m_mousePressFilter(0), m_registerSearchLine(0), m_recursion(false), m_showDetails(false), m_filterProxyModel(0), m_accountComboBox(0), m_balanceIsApproximated(false), m_needLoad(true), m_newAccountLoaded(true), m_inEditMode(false), m_transactionEditor(nullptr), m_balanceWarning(nullptr), m_moveToAccountSelector(nullptr), m_endingBalanceDlg(nullptr) { } ~KGlobalLedgerViewPrivate() { delete m_moveToAccountSelector; delete m_endingBalanceDlg; } void init() { Q_Q(KGlobalLedgerView); m_needLoad = false; auto vbox = new QVBoxLayout(q); q->setLayout(vbox); vbox->setSpacing(6); vbox->setMargin(0); m_mousePressFilter = new MousePressFilter((QWidget*)q); m_action = eWidgets::eRegister::Action::None; // the proxy filter model m_filterProxyModel = new AccountNamesFilterProxyModel(q); m_filterProxyModel->addAccountGroup(QVector {eMyMoney::Account::Type::Asset, eMyMoney::Account::Type::Liability, eMyMoney::Account::Type::Equity}); auto const model = Models::instance()->accountsModel(); m_filterProxyModel->setSourceModel(model); m_filterProxyModel->setSourceColumns(model->getColumns()); m_filterProxyModel->sort((int)eAccountsModel::Column::Account); // create the toolbar frame at the top of the view m_toolbarFrame = new QFrame(); QHBoxLayout* toolbarLayout = new QHBoxLayout(m_toolbarFrame); toolbarLayout->setContentsMargins(0, 0, 0, 0); toolbarLayout->setSpacing(6); // the account selector widget m_accountComboBox = new KMyMoneyAccountCombo(); m_accountComboBox->setModel(m_filterProxyModel); toolbarLayout->addWidget(m_accountComboBox); vbox->addWidget(m_toolbarFrame); toolbarLayout->setStretchFactor(m_accountComboBox, 60); // create the register frame m_registerFrame = new QFrame(); QVBoxLayout* registerFrameLayout = new QVBoxLayout(m_registerFrame); registerFrameLayout->setContentsMargins(0, 0, 0, 0); registerFrameLayout->setSpacing(0); vbox->addWidget(m_registerFrame); vbox->setStretchFactor(m_registerFrame, 2); m_register = new KMyMoneyRegister::Register(m_registerFrame); m_register->setUsedWithEditor(true); registerFrameLayout->addWidget(m_register); m_register->installEventFilter(q); q->connect(m_register, &KMyMoneyRegister::Register::openContextMenu, q, &KGlobalLedgerView::slotTransactionsContextMenuRequested); q->connect(m_register, &KMyMoneyRegister::Register::transactionsSelected, q, &KGlobalLedgerView::slotUpdateSummaryLine); q->connect(m_register->horizontalHeader(), &QWidget::customContextMenuRequested, q, &KGlobalLedgerView::slotSortOptions); q->connect(m_register, &KMyMoneyRegister::Register::reconcileStateColumnClicked, q, &KGlobalLedgerView::slotToggleTransactionMark); // insert search line widget m_registerSearchLine = new KMyMoneyRegister::RegisterSearchLineWidget(m_register, m_toolbarFrame); toolbarLayout->addWidget(m_registerSearchLine); toolbarLayout->setStretchFactor(m_registerSearchLine, 100); // create the summary frame m_summaryFrame = new QFrame(); QHBoxLayout* summaryFrameLayout = new QHBoxLayout(m_summaryFrame); summaryFrameLayout->setContentsMargins(0, 0, 0, 0); summaryFrameLayout->setSpacing(0); m_leftSummaryLabel = new QLabel(m_summaryFrame); m_centerSummaryLabel = new QLabel(m_summaryFrame); m_rightSummaryLabel = new QLabel(m_summaryFrame); summaryFrameLayout->addWidget(m_leftSummaryLabel); QSpacerItem* spacer = new QSpacerItem(20, 1, QSizePolicy::Expanding, QSizePolicy::Minimum); summaryFrameLayout->addItem(spacer); summaryFrameLayout->addWidget(m_centerSummaryLabel); spacer = new QSpacerItem(20, 1, QSizePolicy::Expanding, QSizePolicy::Minimum); summaryFrameLayout->addItem(spacer); summaryFrameLayout->addWidget(m_rightSummaryLabel); vbox->addWidget(m_summaryFrame); // create the button frame m_buttonFrame = new QFrame(q); QVBoxLayout* buttonLayout = new QVBoxLayout(m_buttonFrame); buttonLayout->setContentsMargins(0, 0, 0, 0); buttonLayout->setSpacing(0); vbox->addWidget(m_buttonFrame); m_buttonbar = new KToolBar(m_buttonFrame, 0, true); m_buttonbar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); buttonLayout->addWidget(m_buttonbar); m_buttonbar->addAction(pActions[eMenu::Action::NewTransaction]); m_buttonbar->addAction(pActions[eMenu::Action::DeleteTransaction]); m_buttonbar->addAction(pActions[eMenu::Action::EditTransaction]); m_buttonbar->addAction(pActions[eMenu::Action::EnterTransaction]); m_buttonbar->addAction(pActions[eMenu::Action::CancelTransaction]); m_buttonbar->addAction(pActions[eMenu::Action::AcceptTransaction]); m_buttonbar->addAction(pActions[eMenu::Action::MatchTransaction]); // create the transaction form frame m_formFrame = new QFrame(q); QVBoxLayout* frameLayout = new QVBoxLayout(m_formFrame); frameLayout->setContentsMargins(5, 5, 5, 5); frameLayout->setSpacing(0); m_form = new KMyMoneyTransactionForm::TransactionForm(m_formFrame); frameLayout->addWidget(m_form->getTabBar(m_formFrame)); frameLayout->addWidget(m_form); m_formFrame->setFrameShape(QFrame::Panel); m_formFrame->setFrameShadow(QFrame::Raised); vbox->addWidget(m_formFrame); q->connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KGlobalLedgerView::refresh); q->connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KGlobalLedgerView::slotUpdateMoveToAccountMenu); q->connect(m_register, static_cast(&KMyMoneyRegister::Register::focusChanged), m_form, &KMyMoneyTransactionForm::TransactionForm::slotSetTransaction); q->connect(m_register, static_cast(&KMyMoneyRegister::Register::focusChanged), q, &KGlobalLedgerView::updateLedgerActionsInternal); // q->connect(m_accountComboBox, &KMyMoneyAccountCombo::accountSelected, q, &KGlobalLedgerView::slotAccountSelected); q->connect(m_accountComboBox, &KMyMoneyAccountCombo::accountSelected, q, static_cast(&KGlobalLedgerView::slotSelectAccount)); q->connect(m_accountComboBox, &KMyMoneyAccountCombo::accountSelected, q, &KGlobalLedgerView::slotUpdateMoveToAccountMenu); q->connect(m_register, &KMyMoneyRegister::Register::transactionsSelected, q, &KGlobalLedgerView::transactionsSelected); q->connect(m_register, &KMyMoneyRegister::Register::transactionsSelected, q, &KGlobalLedgerView::slotUpdateMoveToAccountMenu); q->connect(m_register, &KMyMoneyRegister::Register::editTransaction, q, &KGlobalLedgerView::slotEditTransaction); q->connect(m_register, &KMyMoneyRegister::Register::emptyItemSelected, q, &KGlobalLedgerView::slotNewTransaction); q->connect(m_register, &KMyMoneyRegister::Register::aboutToSelectItem, q, &KGlobalLedgerView::slotAboutToSelectItem); q->connect(m_mousePressFilter, &MousePressFilter::mousePressedOnExternalWidget, q, &KGlobalLedgerView::slotCancelOrEnterTransactions); q->connect(m_form, &KMyMoneyTransactionForm::TransactionForm::newTransaction, q, static_cast(&KGlobalLedgerView::slotNewTransactionForm)); // setup mouse press filter m_mousePressFilter->addWidget(m_formFrame); m_mousePressFilter->addWidget(m_buttonFrame); m_mousePressFilter->addWidget(m_summaryFrame); m_mousePressFilter->addWidget(m_registerFrame); m_tooltipPosn = QPoint(); } /** * This method reloads the account selection combo box of the * view with all asset and liability accounts from the engine. * If the account id of the current account held in @p m_accountId is * empty or if the referenced account does not exist in the engine, * the first account found in the list will be made the current account. */ void loadAccounts() { const auto file = MyMoneyFile::instance(); auto const model = Models::instance()->accountsModel(); // check if the current account still exists and make it the // current account if (!m_currentAccount.id().isEmpty()) { try { m_currentAccount = file->account(m_currentAccount.id()); } catch (const MyMoneyException &) { m_currentAccount = MyMoneyAccount(); return; } } // TODO: check why the invalidate is needed here m_filterProxyModel->invalidate(); m_filterProxyModel->sort((int)eAccountsModel::Column::Account); m_filterProxyModel->setHideClosedAccounts(KMyMoneyGlobalSettings::hideClosedAccounts() && !KMyMoneyGlobalSettings::showAllAccounts()); m_filterProxyModel->setHideEquityAccounts(!KMyMoneyGlobalSettings::expertMode()); m_accountComboBox->expandAll(); if (m_currentAccount.id().isEmpty()) { // find the first favorite account QModelIndexList list = model->match(model->index(0, 0), (int)eAccountsModel::Role::Favorite, QVariant(true), 1, Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive | Qt::MatchRecursive)); if (list.count() > 0) { QVariant accountId = list.front().data((int)eAccountsModel::Role::ID); if (accountId.isValid()) { m_currentAccount = file->account(accountId.toString()); } } if (m_currentAccount.id().isEmpty()) { // there are no favorite accounts find any account QModelIndexList list = model->match(model->index(0, 0), Qt::DisplayRole, QVariant(QString("*")), -1, Qt::MatchFlags(Qt::MatchWildcard | Qt::MatchRecursive)); for (QModelIndexList::ConstIterator it = list.constBegin(); it != list.constEnd(); ++it) { if (!it->parent().isValid()) continue; // skip the top level accounts QVariant accountId = (*it).data((int)eAccountsModel::Role::ID); if (accountId.isValid()) { MyMoneyAccount a = file->account(accountId.toString()); if (!a.isInvest()) { m_currentAccount = a; break; } } } } } if (!m_currentAccount.id().isEmpty()) { m_accountComboBox->setSelected(m_currentAccount.id()); try { m_precision = MyMoneyMoney::denomToPrec(m_currentAccount.fraction()); } catch (const MyMoneyException &) { qDebug("Security %s for account %s not found", qPrintable(m_currentAccount.currencyId()), qPrintable(m_currentAccount.name())); m_precision = 2; } } } /** * This method clears the register, form, transaction list. See @sa m_register, * @sa m_transactionList */ void clear() { // clear current register contents m_register->clear(); // setup header font QFont font = KMyMoneyGlobalSettings::listHeaderFont(); QFontMetrics fm(font); int height = fm.lineSpacing() + 6; m_register->horizontalHeader()->setMinimumHeight(height); m_register->horizontalHeader()->setMaximumHeight(height); m_register->horizontalHeader()->setFont(font); // setup cell font font = KMyMoneyGlobalSettings::listCellFont(); m_register->setFont(font); // clear the form m_form->clear(); // the selected transactions list m_transactionList.clear(); // and the selected account in the combo box m_accountComboBox->setSelected(QString()); // fraction defaults to two digits m_precision = 2; } void loadView() { MYMONEYTRACER(tracer); Q_Q(KGlobalLedgerView); // setup form visibility m_formFrame->setVisible(KMyMoneyGlobalSettings::transactionForm()); // no account selected // emit q->objectSelected(MyMoneyAccount()); // no transaction selected KMyMoneyRegister::SelectedTransactions list; emit q->transactionsSelected(list); QMap isSelected; QString focusItemId; QString backUpFocusItemId; // in case the focus item is removed QString anchorItemId; QString backUpAnchorItemId; // in case the anchor item is removed if (!m_newAccountLoaded) { // remember the current selected transactions KMyMoneyRegister::RegisterItem* item = m_register->firstItem(); for (; item; item = item->nextItem()) { if (item->isSelected()) { isSelected[item->id()] = true; } } // remember the item that has the focus storeId(m_register->focusItem(), focusItemId, backUpFocusItemId); // and the one that has the selection anchor storeId(m_register->anchorItem(), anchorItemId, backUpAnchorItemId); } else { m_registerSearchLine->searchLine()->reset(); } // clear the current contents ... clear(); // ... load the combobox widget and select current account ... loadAccounts(); // ... setup the register columns ... m_register->setupRegister(m_currentAccount); // ... setup the form ... m_form->setupForm(m_currentAccount); if (m_currentAccount.id().isEmpty()) { // if we don't have an account we bail out q->setEnabled(false); return; } q->setEnabled(true); m_register->setUpdatesEnabled(false); // ... and recreate it KMyMoneyRegister::RegisterItem* focusItem = 0; KMyMoneyRegister::RegisterItem* anchorItem = 0; QMap actBalance, clearedBalance, futureBalance; QMap::iterator it_b; try { // setup the filter to select the transactions we want to display // and update the sort order QString sortOrder; QString key; QDate reconciliationDate = m_reconciliationDate; MyMoneyTransactionFilter filter(m_currentAccount.id()); // if it's an investment account, we also take care of // the sub-accounts (stock accounts) if (m_currentAccount.accountType() == eMyMoney::Account::Type::Investment) filter.addAccount(m_currentAccount.accountList()); if (isReconciliationAccount()) { key = "kmm-sort-reconcile"; sortOrder = KMyMoneyGlobalSettings::sortReconcileView(); filter.addState((int)eMyMoney::TransactionFilter::State::NotReconciled); filter.addState((int)eMyMoney::TransactionFilter::State::Cleared); } else { filter.setDateFilter(KMyMoneyGlobalSettings::startDate().date(), QDate()); key = "kmm-sort-std"; sortOrder = KMyMoneyGlobalSettings::sortNormalView(); if (KMyMoneyGlobalSettings::hideReconciledTransactions() && !m_currentAccount.isIncomeExpense()) { filter.addState((int)eMyMoney::TransactionFilter::State::NotReconciled); filter.addState((int)eMyMoney::TransactionFilter::State::Cleared); } } filter.setReportAllSplits(true); // check if we have an account override of the sort order if (!m_currentAccount.value(key).isEmpty()) sortOrder = m_currentAccount.value(key); // setup sort order m_register->setSortOrder(sortOrder); // retrieve the list from the engine MyMoneyFile::instance()->transactionList(m_transactionList, filter); emit q->statusProgress(0, m_transactionList.count()); // create the elements for the register QList >::const_iterator it; QMapuniqueMap; int i = 0; for (it = m_transactionList.constBegin(); it != m_transactionList.constEnd(); ++it) { uniqueMap[(*it).first.id()]++; KMyMoneyRegister::Transaction* t = KMyMoneyRegister::Register::transactionFactory(m_register, (*it).first, (*it).second, uniqueMap[(*it).first.id()]); actBalance[t->split().accountId()] = MyMoneyMoney(); emit q->statusProgress(++i, 0); // if we're in reconciliation and the state is cleared, we // force the item to show in dimmed intensity to get a visual focus // on those items, that we need to work on if (isReconciliationAccount() && (*it).second.reconcileFlag() == eMyMoney::Split::State::Cleared) { t->setReducedIntensity(true); } } // create dummy entries for the scheduled transactions if sorted by postdate int period = KMyMoneyGlobalSettings::schedulePreview(); if (m_register->primarySortKey() == eWidgets::SortField::PostDate) { // show scheduled transactions which have a scheduled postdate // within the next 'period' days. In reconciliation mode, the // period starts on the statement date. QDate endDate = QDate::currentDate().addDays(period); if (isReconciliationAccount()) endDate = reconciliationDate.addDays(period); QList scheduleList = MyMoneyFile::instance()->scheduleList(m_currentAccount.id()); while (!scheduleList.isEmpty()) { MyMoneySchedule& s = scheduleList.first(); for (;;) { if (s.isFinished() || s.adjustedNextDueDate() > endDate) { break; } MyMoneyTransaction t(s.id(), KMyMoneyUtils::scheduledTransaction(s)); // if the transaction is scheduled and overdue, it can't // certainly be posted in the past. So we take today's date // as the alternative if (s.isOverdue()) { t.setPostDate(s.adjustedDate(QDate::currentDate(), s.weekendOption())); } else { t.setPostDate(s.adjustedNextDueDate()); } foreach (const auto split, t.splits()) { if (split.accountId() == m_currentAccount.id()) { new KMyMoneyRegister::StdTransactionScheduled(m_register, t, split, uniqueMap[t.id()]); } } // keep track of this payment locally (not in the engine) if (s.isOverdue()) { s.setLastPayment(QDate::currentDate()); } else { s.setLastPayment(s.nextDueDate()); } // if this is a one time schedule, we can bail out here as we're done if (s.occurrence() == eMyMoney::Schedule::Occurrence::Once) break; // for all others, we check if the next payment date is still 'in range' QDate nextDueDate = s.nextPayment(s.nextDueDate()); if (nextDueDate.isValid()) { s.setNextDueDate(nextDueDate); } else { break; } } scheduleList.pop_front(); } } // add the group markers m_register->addGroupMarkers(); // sort the transactions according to the sort setting m_register->sortItems(); // remove trailing and adjacent markers m_register->removeUnwantedGroupMarkers(); // add special markers for reconciliation now so that they do not get // removed by m_register->removeUnwantedGroupMarkers(). Needs resorting // of items but that's ok. KMyMoneyRegister::StatementGroupMarker* statement = 0; KMyMoneyRegister::StatementGroupMarker* dStatement = 0; KMyMoneyRegister::StatementGroupMarker* pStatement = 0; if (isReconciliationAccount()) { switch (m_register->primarySortKey()) { case eWidgets::SortField::PostDate: statement = new KMyMoneyRegister::StatementGroupMarker(m_register, eWidgets::eRegister::CashFlowDirection::Deposit, reconciliationDate, i18n("Statement Details")); m_register->sortItems(); break; case eWidgets::SortField::Type: dStatement = new KMyMoneyRegister::StatementGroupMarker(m_register, eWidgets::eRegister::CashFlowDirection::Deposit, reconciliationDate, i18n("Statement Deposit Details")); pStatement = new KMyMoneyRegister::StatementGroupMarker(m_register, eWidgets::eRegister::CashFlowDirection::Payment, reconciliationDate, i18n("Statement Payment Details")); m_register->sortItems(); break; default: break; } } // we need at least the balance for the account we currently show actBalance[m_currentAccount.id()] = MyMoneyMoney(); if (m_currentAccount.accountType() == eMyMoney::Account::Type::Investment) foreach (const auto accountID, m_currentAccount.accountList()) actBalance[accountID] = MyMoneyMoney(); // determine balances (actual, cleared). We do this by getting the actual // balance of all entered transactions from the engine and walk the list // of transactions backward. Also re-select a transaction if it was // selected before and setup the focus item. MyMoneyMoney factor(1, 1); if (m_currentAccount.accountGroup() == eMyMoney::Account::Type::Liability || m_currentAccount.accountGroup() == eMyMoney::Account::Type::Equity) factor = -factor; QMap deposits; QMap payments; QMap depositAmount; QMap paymentAmount; for (it_b = actBalance.begin(); it_b != actBalance.end(); ++it_b) { MyMoneyMoney balance = MyMoneyFile::instance()->balance(it_b.key()); balance = balance * factor; clearedBalance[it_b.key()] = futureBalance[it_b.key()] = (*it_b) = balance; deposits[it_b.key()] = payments[it_b.key()] = 0; depositAmount[it_b.key()] = MyMoneyMoney(); paymentAmount[it_b.key()] = MyMoneyMoney(); } tracer.printf("total balance of %s = %s", qPrintable(m_currentAccount.name()), qPrintable(actBalance[m_currentAccount.id()].formatMoney("", 2))); tracer.printf("future balance of %s = %s", qPrintable(m_currentAccount.name()), qPrintable(futureBalance[m_currentAccount.id()].formatMoney("", 2))); tracer.printf("cleared balance of %s = %s", qPrintable(m_currentAccount.name()), qPrintable(clearedBalance[m_currentAccount.id()].formatMoney("", 2))); KMyMoneyRegister::RegisterItem* p = m_register->lastItem(); focusItem = 0; // take care of possibly trailing scheduled transactions (bump up the future balance) while (p) { if (p->isSelectable()) { KMyMoneyRegister::Transaction* t = dynamic_cast(p); if (t && t->isScheduled()) { MyMoneyMoney balance = futureBalance[t->split().accountId()]; const MyMoneySplit& split = t->split(); // if this split is a stock split, we can't just add the amount of shares if (t->transaction().isStockSplit()) { balance = balance * split.shares(); } else { balance += split.shares() * factor; } futureBalance[split.accountId()] = balance; } else if (t && !focusItem) focusItem = p; } p = p->prevItem(); } p = m_register->lastItem(); while (p) { KMyMoneyRegister::Transaction* t = dynamic_cast(p); if (t) { if (isSelected.contains(t->id())) t->setSelected(true); matchItemById(&focusItem, t, focusItemId, backUpFocusItemId); matchItemById(&anchorItem, t, anchorItemId, backUpAnchorItemId); const MyMoneySplit& split = t->split(); MyMoneyMoney balance = futureBalance[split.accountId()]; t->setBalance(balance); // if this split is a stock split, we can't just add the amount of shares if (t->transaction().isStockSplit()) { balance /= split.shares(); } else { balance -= split.shares() * factor; } if (!t->isScheduled()) { if (isReconciliationAccount() && t->transaction().postDate() <= reconciliationDate && split.reconcileFlag() == eMyMoney::Split::State::Cleared) { if (split.shares().isNegative()) { payments[split.accountId()]++; paymentAmount[split.accountId()] += split.shares(); } else { deposits[split.accountId()]++; depositAmount[split.accountId()] += split.shares(); } } if (t->transaction().postDate() > QDate::currentDate()) { tracer.printf("Reducing actual balance by %s because %s/%s(%s) is in the future", qPrintable((split.shares() * factor).formatMoney("", 2)), qPrintable(t->transaction().id()), qPrintable(split.id()), qPrintable(t->transaction().postDate().toString(Qt::ISODate))); actBalance[split.accountId()] -= split.shares() * factor; } } futureBalance[split.accountId()] = balance; } p = p->prevItem(); } clearedBalance[m_currentAccount.id()] = MyMoneyFile::instance()->clearedBalance(m_currentAccount.id(), reconciliationDate); tracer.printf("total balance of %s = %s", qPrintable(m_currentAccount.name()), qPrintable(actBalance[m_currentAccount.id()].formatMoney("", 2))); tracer.printf("future balance of %s = %s", qPrintable(m_currentAccount.name()), qPrintable(futureBalance[m_currentAccount.id()].formatMoney("", 2))); tracer.printf("cleared balance of %s = %s", qPrintable(m_currentAccount.name()), qPrintable(clearedBalance[m_currentAccount.id()].formatMoney("", 2))); // update statement information if (statement) { const QString aboutDeposits = i18np("%1 deposit (%2)", "%1 deposits (%2)", deposits[m_currentAccount.id()], depositAmount[m_currentAccount.id()].abs().formatMoney(m_currentAccount.fraction())); const QString aboutPayments = i18np("%1 payment (%2)", "%1 payments (%2)", payments[m_currentAccount.id()], paymentAmount[m_currentAccount.id()].abs().formatMoney(m_currentAccount.fraction())); statement->setText(i18nc("%1 is a string, e.g. 7 deposits; %2 is a string, e.g. 4 payments", "%1, %2", aboutDeposits, aboutPayments)); } if (pStatement) { pStatement->setText(i18np("%1 payment (%2)", "%1 payments (%2)", payments[m_currentAccount.id()] , paymentAmount[m_currentAccount.id()].abs().formatMoney(m_currentAccount.fraction()))); } if (dStatement) { dStatement->setText(i18np("%1 deposit (%2)", "%1 deposits (%2)", deposits[m_currentAccount.id()] , depositAmount[m_currentAccount.id()].abs().formatMoney(m_currentAccount.fraction()))); } // add a last empty entry for new transactions // leave some information about the current account MyMoneySplit split; split.setReconcileFlag(eMyMoney::Split::State::NotReconciled); // make sure to use the value specified in the option during reconciliation if (isReconciliationAccount()) split.setReconcileFlag(static_cast(KMyMoneyGlobalSettings::defaultReconciliationState())); KMyMoneyRegister::Register::transactionFactory(m_register, MyMoneyTransaction(), split, 0); m_register->updateRegister(true); if (focusItem) { // in case we have some selected items we just set the focus item // in other cases, we make the focusitem also the selected item if (anchorItem && (anchorItem != focusItem)) { m_register->setFocusItem(focusItem); m_register->setAnchorItem(anchorItem); } else m_register->selectItem(focusItem, true); } else { // just use the empty line at the end if nothing else exists in the ledger p = m_register->lastItem(); m_register->setFocusItem(p); m_register->selectItem(p); focusItem = p; } updateSummaryLine(actBalance, clearedBalance); emit q->statusProgress(-1, -1); } catch (const MyMoneyException &) { m_currentAccount = MyMoneyAccount(); clear(); } m_showDetails = KMyMoneyGlobalSettings::showRegisterDetailed(); // and tell everyone what's selected emit q->objectSelected(m_currentAccount); KMyMoneyRegister::SelectedTransactions actualSelection(m_register); emit q->transactionsSelected(actualSelection); } void selectTransaction(const QString& id) { if (!id.isEmpty()) { KMyMoneyRegister::RegisterItem* p = m_register->lastItem(); while (p) { KMyMoneyRegister::Transaction* t = dynamic_cast(p); if (t) { if (t->transaction().id() == id) { m_register->selectItem(t); m_register->ensureItemVisible(t); break; } } p = p->prevItem(); } } } /** * @brief selects transactions for processing with slots * @param list of transactions * @return false if only schedule is to be selected */ bool selectTransactions(const KMyMoneyRegister::SelectedTransactions& list) { Q_Q(KGlobalLedgerView); // list can either contain a list of transactions or a single selected scheduled transaction // in the latter case, the transaction id is actually the one of the schedule. In order // to differentiate between the two, we just ask for the schedule. If we don't find one - because // we passed the id of a real transaction - then we know that fact. We use the schedule here, // because the list of schedules is kept in a cache by MyMoneyFile. This way, we save some trips // to the backend which we would have to do if we check for the transaction. m_selectedTransactions.clear(); auto sch = MyMoneySchedule(); auto ret = true; m_accountGoto.clear(); m_payeeGoto.clear(); if (!list.isEmpty() && !list.first().isScheduled()) { m_selectedTransactions = list; if (list.count() == 1) { const MyMoneySplit& sp = m_selectedTransactions[0].split(); if (!sp.payeeId().isEmpty()) { try { auto payee = MyMoneyFile::instance()->payee(sp.payeeId()); if (!payee.name().isEmpty()) { m_payeeGoto = payee.id(); auto name = payee.name(); name.replace(QRegExp("&(?!&)"), "&&"); pActions[Action::GoToPayee]->setText(i18n("Go to '%1'", name)); } } catch (const MyMoneyException &) { } } try { const auto& t = m_selectedTransactions[0].transaction(); // search the first non-income/non-expense accunt and use it for the 'goto account' const auto& sp = m_selectedTransactions[0].split(); foreach (const auto split, t.splits()) { if (split.id() != sp.id()) { auto acc = MyMoneyFile::instance()->account(split.accountId()); if (!acc.isIncomeExpense()) { // for stock accounts we show the portfolio account if (acc.isInvest()) { acc = MyMoneyFile::instance()->account(acc.parentAccountId()); } m_accountGoto = acc.id(); auto name = acc.name(); name.replace(QRegExp("&(?!&)"), "&&"); pActions[Action::GoToAccount]->setText(i18n("Go to '%1'", name)); break; } } } } catch (const MyMoneyException &) { } } } else if (!list.isEmpty()) { sch = MyMoneyFile::instance()->schedule(list.first().scheduleId()); m_selectedTransactions.append(list.first()); ret = false; } emit q->objectSelected(sch); // make sure, we show some neutral menu entry if we don't have an object if (m_payeeGoto.isEmpty()) pActions[Action::GoToPayee]->setText(i18n("Go to payee")); if (m_accountGoto.isEmpty()) pActions[Action::GoToAccount]->setText(i18n("Go to account")); return ret; } /** * Returns @a true if setReconciliationAccount() has been called for * the current loaded account. * * @retval true current account is in reconciliation mode * @retval false current account is not in reconciliation mode */ bool isReconciliationAccount() const { return m_currentAccount.id() == m_reconciliationAccount.id(); } /** * Updates the values on the summary line beneath the register with * the given values. The contents shown differs between reconciliation * mode and normal mode. * * @param actBalance map of account indexed values to be used as actual balance * @param clearedBalance map of account indexed values to be used as cleared balance */ void updateSummaryLine(const QMap& actBalance, const QMap& clearedBalance) { Q_Q(KGlobalLedgerView); const auto file = MyMoneyFile::instance(); m_leftSummaryLabel->show(); m_centerSummaryLabel->show(); m_rightSummaryLabel->show(); if (isReconciliationAccount()) { if (m_currentAccount.accountType() != eMyMoney::Account::Type::Investment) { m_leftSummaryLabel->setText(i18n("Statement: %1", m_endingBalance.formatMoney("", m_precision))); m_centerSummaryLabel->setText(i18nc("Cleared balance", "Cleared: %1", clearedBalance[m_currentAccount.id()].formatMoney("", m_precision))); m_totalBalance = clearedBalance[m_currentAccount.id()] - m_endingBalance; } } else { // update summary line in normal mode QDate reconcileDate = m_currentAccount.lastReconciliationDate(); if (reconcileDate.isValid()) { m_leftSummaryLabel->setText(i18n("Last reconciled: %1", QLocale().toString(reconcileDate, QLocale::ShortFormat))); } else { m_leftSummaryLabel->setText(i18n("Never reconciled")); } QPalette palette = m_rightSummaryLabel->palette(); palette.setColor(m_rightSummaryLabel->foregroundRole(), m_leftSummaryLabel->palette().color(q->foregroundRole())); if (m_currentAccount.accountType() != eMyMoney::Account::Type::Investment) { m_centerSummaryLabel->setText(i18nc("Cleared balance", "Cleared: %1", clearedBalance[m_currentAccount.id()].formatMoney("", m_precision))); m_totalBalance = actBalance[m_currentAccount.id()]; } else { m_centerSummaryLabel->hide(); MyMoneyMoney balance; MyMoneySecurity base = file->baseCurrency(); QMap::const_iterator it_b; // reset the approximated flag m_balanceIsApproximated = false; for (it_b = actBalance.begin(); it_b != actBalance.end(); ++it_b) { MyMoneyAccount stock = file->account(it_b.key()); QString currencyId = stock.currencyId(); MyMoneySecurity sec = file->security(currencyId); MyMoneyMoney rate(1, 1); if (stock.isInvest()) { currencyId = sec.tradingCurrency(); const MyMoneyPrice &priceInfo = file->price(sec.id(), currencyId); m_balanceIsApproximated |= !priceInfo.isValid(); rate = priceInfo.rate(sec.tradingCurrency()); } if (currencyId != base.id()) { const MyMoneyPrice &priceInfo = file->price(sec.tradingCurrency(), base.id()); m_balanceIsApproximated |= !priceInfo.isValid(); rate = (rate * priceInfo.rate(base.id())).convertPrecision(sec.pricePrecision()); } balance += ((*it_b) * rate).convert(base.smallestAccountFraction()); } m_totalBalance = balance; } m_rightSummaryLabel->setPalette(palette); } // determine the number of selected transactions KMyMoneyRegister::SelectedTransactions selection; m_register->selectedTransactions(selection); q->slotUpdateSummaryLine(selection); } /** * setup the default action according to the current account type */ void setupDefaultAction() { switch (m_currentAccount.accountType()) { case eMyMoney::Account::Type::Asset: case eMyMoney::Account::Type::AssetLoan: case eMyMoney::Account::Type::Savings: m_action = eWidgets::eRegister::Action::Deposit; break; default: m_action = eWidgets::eRegister::Action::Withdrawal; break; } } // used to store the id of an item and the id of an immeadiate unselected sibling void storeId(KMyMoneyRegister::RegisterItem *item, QString &id, QString &backupId) { if (item) { // the id of the item id = item->id(); // the id of the item's previous/next unselected item for (KMyMoneyRegister::RegisterItem *it = item->prevItem(); it != 0 && backupId.isEmpty(); it = it->prevItem()) { if (!it->isSelected()) { backupId = it->id(); } } // if we didn't found previous unselected items search trough the next items for (KMyMoneyRegister::RegisterItem *it = item->nextItem(); it != 0 && backupId.isEmpty(); it = it->nextItem()) { if (!it->isSelected()) { backupId = it->id(); } } } } // use to match an item by it's id or a backup id which has a lower precedence void matchItemById(KMyMoneyRegister::RegisterItem **item, KMyMoneyRegister::Transaction* t, QString &id, QString &backupId) { if (!backupId.isEmpty() && t->id() == backupId) *item = t; if (!id.isEmpty() && t->id() == id) { // we found the real thing there's no need for the backup anymore backupId.clear(); *item = t; } } bool canProcessTransactions(const KMyMoneyRegister::SelectedTransactions& list, QString& tooltip) const { MyMoneyAccount acc; QString closedAccount; if (m_register->focusItem() == 0) return false; bool rc = true; if (list.warnLevel() == 3) { //Closed account somewhere KMyMoneyRegister::SelectedTransactions::const_iterator it_t; for (it_t = list.begin(); rc && it_t != list.end(); ++it_t) { QList splitList = (*it_t).transaction().splits(); QString id = splitList.first().accountId(); acc = MyMoneyFile::instance()->account(id); if (!acc.isClosed()) { //wrong split, try other id = splitList.last().accountId(); acc = MyMoneyFile::instance()->account(id); } closedAccount = acc.name(); break; } tooltip = i18n("Cannot process transactions in account %1, which is closed.", closedAccount); showTooltip(tooltip); return false; } if (!m_register->focusItem()->isSelected()) { tooltip = i18n("Cannot process transaction with focus if it is not selected."); showTooltip(tooltip); return false; } tooltip.clear(); return !list.isEmpty(); } void showTooltip(const QString msg) const { QToolTip::showText(m_tooltipPosn, msg); } bool createNewTransaction() { Q_Q(KGlobalLedgerView); auto rc = false; QString txt; if (q->canCreateTransactions(txt)) { rc = q->selectEmptyTransaction(); } return rc; } TransactionEditor* startEdit(const KMyMoneyRegister::SelectedTransactions& list) { Q_Q(KGlobalLedgerView); TransactionEditor* editor = 0; QString txt; if (q->canEditTransactions(list, txt) || q->canCreateTransactions(txt)) { editor = q->startEdit(list); } return editor; } void doDeleteTransactions() { Q_Q(KGlobalLedgerView); KMyMoneyRegister::SelectedTransactions list = m_selectedTransactions; KMyMoneyRegister::SelectedTransactions::iterator it_t; int cnt = list.count(); int i = 0; emit q->statusProgress(0, cnt); MyMoneyFileTransaction ft; const auto file = MyMoneyFile::instance(); try { it_t = list.begin(); while (it_t != list.end()) { // only remove those transactions that do not reference a closed account if (!file->referencesClosedAccount((*it_t).transaction())) { file->removeTransaction((*it_t).transaction()); // remove all those references in the list of selected transactions // that refer to the same transaction we just removed so that we // will not be caught by an exception later on (see bko #285310) KMyMoneyRegister::SelectedTransactions::iterator it_td = it_t; ++it_td; while (it_td != list.end()) { if ((*it_t).transaction().id() == (*it_td).transaction().id()) { it_td = list.erase(it_td); i++; // bump count of deleted transactions } else { ++it_td; } } } // need to ensure "nextCheckNumber" is still correct auto acc = file->account((*it_t).split().accountId()); // the "lastNumberUsed" might have been the txn number deleted // so adjust it QString deletedNum = (*it_t).split().number(); // decrement deletedNum and set new "lastNumberUsed" QString num = KMyMoneyUtils::getAdjacentNumber(deletedNum, -1); acc.setValue("lastNumberUsed", num); file->modifyAccount(acc); list.erase(it_t); it_t = list.begin(); emit q->statusProgress(i++, 0); } ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(q, i18n("Error"), i18n("Unable to delete transaction(s): %1, thrown in %2:%3", e.what(), e.file(), e.line())); } emit q->statusProgress(-1, -1); } void deleteTransactionEditor() { // make sure, we don't use the transaction editor pointer // anymore from now on auto p = m_transactionEditor; m_transactionEditor = nullptr; delete p; } void transactionUnmatch() { Q_Q(KGlobalLedgerView); KMyMoneyRegister::SelectedTransactions::const_iterator it; MyMoneyFileTransaction ft; try { for (it = m_selectedTransactions.constBegin(); it != m_selectedTransactions.constEnd(); ++it) { if ((*it).split().isMatched()) { TransactionMatcher matcher(m_currentAccount); matcher.unmatch((*it).transaction(), (*it).split()); } } ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(q, i18n("Unable to unmatch the selected transactions"), e.what()); } } void transactionMatch() { Q_Q(KGlobalLedgerView); if (m_selectedTransactions.count() != 2) return; MyMoneyTransaction startMatchTransaction; MyMoneyTransaction endMatchTransaction; MyMoneySplit startSplit; MyMoneySplit endSplit; KMyMoneyRegister::SelectedTransactions::const_iterator it; KMyMoneyRegister::SelectedTransactions toBeDeleted; for (it = m_selectedTransactions.constBegin(); it != m_selectedTransactions.constEnd(); ++it) { if ((*it).transaction().isImported()) { if (endMatchTransaction.id().isEmpty()) { endMatchTransaction = (*it).transaction(); endSplit = (*it).split(); toBeDeleted << *it; } else { //This is a second imported transaction, we still want to merge startMatchTransaction = (*it).transaction(); startSplit = (*it).split(); } } else if (!(*it).split().isMatched()) { if (startMatchTransaction.id().isEmpty()) { startMatchTransaction = (*it).transaction(); startSplit = (*it).split(); } else { endMatchTransaction = (*it).transaction(); endSplit = (*it).split(); toBeDeleted << *it; } } } #if 0 KMergeTransactionsDlg dlg(m_selectedAccount); dlg.addTransaction(startMatchTransaction); dlg.addTransaction(endMatchTransaction); if (dlg.exec() == QDialog::Accepted) #endif { MyMoneyFileTransaction ft; try { if (startMatchTransaction.id().isEmpty()) throw MYMONEYEXCEPTION(i18n("No manually entered transaction selected for matching")); if (endMatchTransaction.id().isEmpty()) throw MYMONEYEXCEPTION(i18n("No imported transaction selected for matching")); TransactionMatcher matcher(m_currentAccount); matcher.match(startMatchTransaction, startSplit, endMatchTransaction, endSplit, true); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(q, i18n("Unable to match the selected transactions"), e.what()); } } } /** * Mark the selected transactions as provided by @a flag. If * flag is @a MyMoneySplit::Unknown, the future state depends * on the current stat of the split's flag accoring to the * following table: * * - NotReconciled --> Cleared * - Cleared --> Reconciled * - Reconciled --> NotReconciled */ void markTransaction(eMyMoney::Split::State flag) { Q_Q(KGlobalLedgerView); auto list = m_selectedTransactions; KMyMoneyRegister::SelectedTransactions::const_iterator it_t; auto cnt = list.count(); auto i = 0; emit q->statusProgress(0, cnt); MyMoneyFileTransaction ft; try { for (it_t = list.constBegin(); it_t != list.constEnd(); ++it_t) { // turn on signals before we modify the last entry in the list cnt--; MyMoneyFile::instance()->blockSignals(cnt != 0); // get a fresh copy auto t = MyMoneyFile::instance()->transaction((*it_t).transaction().id()); auto sp = t.splitById((*it_t).split().id()); if (sp.reconcileFlag() != flag) { if (flag == eMyMoney::Split::State::Unknown) { if (m_reconciliationAccount.id().isEmpty()) { // in normal mode we cycle through all states switch (sp.reconcileFlag()) { case eMyMoney::Split::State::NotReconciled: sp.setReconcileFlag(eMyMoney::Split::State::Cleared); break; case eMyMoney::Split::State::Cleared: sp.setReconcileFlag(eMyMoney::Split::State::Reconciled); break; case eMyMoney::Split::State::Reconciled: sp.setReconcileFlag(eMyMoney::Split::State::NotReconciled); break; default: break; } } else { // in reconciliation mode we skip the reconciled state switch (sp.reconcileFlag()) { case eMyMoney::Split::State::NotReconciled: sp.setReconcileFlag(eMyMoney::Split::State::Cleared); break; case eMyMoney::Split::State::Cleared: sp.setReconcileFlag(eMyMoney::Split::State::NotReconciled); break; default: break; } } } else { sp.setReconcileFlag(flag); } t.modifySplit(sp); MyMoneyFile::instance()->modifyTransaction(t); } emit q->statusProgress(i++, 0); } emit q->statusProgress(-1, -1); ft.commit(); } catch (const MyMoneyException &e) { KMessageBox::detailedSorry(q, i18n("Error"), i18n("Unable to modify transaction: %1, thrown in %2:%3", e.what(), e.file(), e.line())); } } // move a stock transaction from one investment account to another void 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 createTransactionMoveMenu() { Q_Q(KGlobalLedgerView); if (!m_moveToAccountSelector) { auto menu = pMenus[eMenu::Menu::MoveTransaction]; if (menu ) { auto accountSelectorAction = new QWidgetAction(menu); m_moveToAccountSelector = new KMyMoneyAccountSelector(menu, 0, false); m_moveToAccountSelector->setObjectName("transaction_move_menu_selector"); accountSelectorAction->setDefaultWidget(m_moveToAccountSelector); menu->addAction(accountSelectorAction); q->connect(m_moveToAccountSelector, &QObject::destroyed, q, &KGlobalLedgerView::slotObjectDestroyed); q->connect(m_moveToAccountSelector, &KMyMoneySelector::itemSelected, q, &KGlobalLedgerView::slotMoveToAccount); } } } QList > automaticReconciliation(const MyMoneyAccount &account, const QList > &transactions, const MyMoneyMoney &amount) { Q_Q(KGlobalLedgerView); static const int NR_OF_STEPS_LIMIT = 300000; static const int PROGRESSBAR_STEPS = 1000; QList > result = transactions; // KMSTATUS(i18n("Running automatic reconciliation")); auto progressBarIndex = 0; q->statusProgress(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->statusProgress(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->statusProgress(progressBarIndex++, 0); // more than one transaction is uncleared - apply the algorithm result.clear(); const auto& 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->statusProgress(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->statusProgress(NR_OF_STEPS_LIMIT / PROGRESSBAR_STEPS, 0); if (sumToComponentsMap.contains(amount)) { QListIterator > itTransactionSplit(transactions); while (itTransactionSplit.hasNext()) { const QPair &transactionSplit = itTransactionSplit.next(); const QList > &splitIds = sumToComponentsMap.value(amount); if (splitIds.contains(qMakePair(transactionSplit.first.id(), transactionSplit.second.id()))) { result.append(transactionSplit); } } } #ifdef KMM_DEBUG qDebug("For the amount %s a number of %d possible sums where computed from the set of %d transactions: ", qPrintable(MyMoneyUtils::formatMoney(amount, security)), sumToComponentsMap.size(), transactions.size()); #endif q->statusProgress(-1, -1); return result; } KGlobalLedgerView *q_ptr; MousePressFilter *m_mousePressFilter; KMyMoneyRegister::RegisterSearchLineWidget* m_registerSearchLine; // QString m_reconciliationAccount; QDate m_reconciliationDate; MyMoneyMoney m_endingBalance; int m_precision; bool m_recursion; bool m_showDetails; eWidgets::eRegister::Action m_action; // models AccountNamesFilterProxyModel *m_filterProxyModel; // widgets KMyMoneyAccountCombo* m_accountComboBox; MyMoneyMoney m_totalBalance; bool m_balanceIsApproximated; // frames QFrame* m_toolbarFrame; QFrame* m_registerFrame; QFrame* m_buttonFrame; QFrame* m_formFrame; QFrame* m_summaryFrame; // widgets KMyMoneyRegister::Register* m_register; KToolBar* m_buttonbar; /** * This member holds the currently selected account */ MyMoneyAccount m_currentAccount; + QString m_lastSelectedAccountID; MyMoneyAccount m_reconciliationAccount; /** * This member holds the transaction list */ QList > m_transactionList; QLabel* m_leftSummaryLabel; QLabel* m_centerSummaryLabel; QLabel* m_rightSummaryLabel; KMyMoneyTransactionForm::TransactionForm* m_form; /** * This member holds the load state of page */ bool m_needLoad; bool m_newAccountLoaded; bool m_inEditMode; QWidgetList m_tabOrderWidgets; QPoint m_tooltipPosn; KMyMoneyRegister::SelectedTransactions m_selectedTransactions; /** * This member keeps the date that was used as the last posting date. * It will be updated whenever the user modifies the post date * and is used to preset the posting date when new transactions are created. * This member is initialised to the current date when the program is started. */ static QDate m_lastPostDate; // pointer to the current transaction editor QPointer m_transactionEditor; // id's that need to be remembered QString m_accountGoto, m_payeeGoto; QString m_lastPayeeEnteredId; QScopedPointer m_balanceWarning; KMyMoneyAccountSelector* m_moveToAccountSelector; // Reconciliation dialog KEndingBalanceDlg* m_endingBalanceDlg; }; #endif