1 /*************************************************************************** 2 kgloballedgerview_p.h - description 3 ------------------- 4 begin : Wed Jul 26 2006 5 copyright : (C) 2006 by Thomas Baumgart 6 email : Thomas Baumgart <ipwizard@users.sourceforge.net> 7 (C) 2017 by Łukasz Wojniłowicz <lukasz.wojnilowicz@gmail.com> 8 ***************************************************************************/ 9 10 /*************************************************************************** 11 * * 12 * This program is free software; you can redistribute it and/or modify * 13 * it under the terms of the GNU General Public License as published by * 14 * the Free Software Foundation; either version 2 of the License, or * 15 * (at your option) any later version. * 16 * * 17 ***************************************************************************/ 18 19 #ifndef KGLOBALLEDGERVIEW_P_H 20 #define KGLOBALLEDGERVIEW_P_H 21 22 #include "kgloballedgerview.h" 23 24 // ---------------------------------------------------------------------------- 25 // QT Includes 26 27 #include <QFrame> 28 #include <QHBoxLayout> 29 #include <QList> 30 #include <QLabel> 31 #include <QEvent> 32 #include <QVBoxLayout> 33 #include <QHeaderView> 34 #include <QToolTip> 35 #include <QMenu> 36 #include <QWidgetAction> 37 38 // ---------------------------------------------------------------------------- 39 // KDE Includes 40 41 #include <KLocalizedString> 42 #include <KMessageBox> 43 #include <KToolBar> 44 #include <KPassivePopup> 45 46 // ---------------------------------------------------------------------------- 47 // Project Includes 48 49 #include "kmymoneyviewbase_p.h" 50 #include "kendingbalancedlg.h" 51 #include "kfindtransactiondlg.h" 52 #include "kmymoneyaccountselector.h" 53 #include "kmymoneyutils.h" 54 #include "mymoneyexception.h" 55 #include "mymoneymoney.h" 56 #include "mymoneyaccount.h" 57 #include "mymoneyfile.h" 58 #include "kmymoneyaccountcombo.h" 59 #include "kbalancewarning.h" 60 #include "transactionmatcher.h" 61 #include "tabbar.h" 62 #include "register.h" 63 #include "transactioneditor.h" 64 #include "selectedtransactions.h" 65 #include "kmymoneysettings.h" 66 #include "registersearchline.h" 67 #include "scheduledtransaction.h" 68 #include "accountsmodel.h" 69 #include "models.h" 70 #include "mymoneyprice.h" 71 #include "mymoneyschedule.h" 72 #include "mymoneysecurity.h" 73 #include "mymoneytransaction.h" 74 #include "mymoneytransactionfilter.h" 75 #include "mymoneysplit.h" 76 #include "mymoneypayee.h" 77 #include "mymoneytracer.h" 78 #include "transaction.h" 79 #include "transactionform.h" 80 #include "fancydategroupmarkers.h" 81 #include "widgetenums.h" 82 #include "mymoneyenums.h" 83 #include "modelenums.h" 84 #include "menuenums.h" 85 86 #include <config-kmymoney.h> 87 #ifdef KMM_DEBUG 88 #include "mymoneyutils.h" 89 #endif 90 91 using namespace eMenu; 92 using namespace eMyMoney; 93 94 /** 95 * helper class implementing an event filter to detect mouse button press 96 * events on widgets outside a given set of widgets. This is used internally 97 * to detect when to leave the edit mode. 98 */ 99 class MousePressFilter : public QObject 100 { 101 Q_OBJECT 102 public: 103 explicit MousePressFilter(QWidget* parent = nullptr) : QObject(parent)104 QObject(parent), 105 m_lastMousePressEvent(0), 106 m_filterActive(true) 107 { 108 } 109 110 /** 111 * Add widget @p w to the list of possible parent objects. See eventFilter() how 112 * they will be used. 113 */ addWidget(QWidget * w)114 void addWidget(QWidget* w) 115 { 116 m_parents.append(w); 117 } 118 119 public Q_SLOTS: 120 /** 121 * This slot allows to activate/deactivate the filter. By default the 122 * filter is active. 123 * 124 * @param state Allows to activate (@a true) or deactivate (@a false) the filter 125 */ 126 void setFilterActive(bool state = true) 127 { 128 m_filterActive = state; 129 } 130 131 /** 132 * This slot allows to activate/deactivate the filter. By default the 133 * filter is active. 134 * 135 * @param state Allows to deactivate (@a true) or activate (@a false) the filter 136 */ 137 void setFilterDeactive(bool state = false) { 138 setFilterActive(!state); 139 } 140 141 protected: 142 /** 143 * This method checks if the widget @p child is a child of 144 * the widget @p parent and returns either @a true or @a false. 145 * 146 * @param child pointer to child widget 147 * @param parent pointer to parent widget 148 * @retval true @p child points to widget which has @p parent as parent or grand-parent 149 * @retval false @p child points to a widget which is not related to @p parent 150 */ isChildOf(QWidget * child,QWidget * parent)151 bool isChildOf(QWidget* child, QWidget* parent) 152 { 153 // QDialogs cannot be detected directly, but it can be assumed, 154 // that events on a widget that do not have a parent widget within 155 // our application are dialogs. 156 if (!child->parentWidget()) 157 return true; 158 159 while (child) { 160 // if we are a child of the given parent, we have a match 161 if (child == parent) 162 return true; 163 // if we are at the application level, we don't have a match 164 if (child->inherits("KMyMoneyApp")) 165 return false; 166 // If one of the ancestors is a KPassivePopup or a KDialog or a popup widget then 167 // it's as if it is a child of our own because these widgets could 168 // appear during transaction entry (message boxes, completer widgets) 169 if (dynamic_cast<KPassivePopup*>(child) || 170 ((child->windowFlags() & Qt::Popup) && /*child != kmymoney*/ 171 !child->parentWidget())) // has no parent, then it must be top-level window 172 return true; 173 child = child->parentWidget(); 174 } 175 return false; 176 } 177 178 /** 179 * Reimplemented from base class. Sends out the mousePressedOnExternalWidget() signal 180 * if object @p o points to an object which is not a child widget of any added previously 181 * using the addWidget() method. The signal is sent out only once for each event @p e. 182 * 183 * @param o pointer to QObject 184 * @param e pointer to QEvent 185 * @return always returns @a false 186 */ eventFilter(QObject * o,QEvent * e)187 bool eventFilter(QObject* o, QEvent* e) final override 188 { 189 if (m_filterActive) { 190 if (e->type() == QEvent::MouseButtonPress && !m_lastMousePressEvent) { 191 QWidget* w = qobject_cast<QWidget*>(o); 192 if (!w) { 193 return QObject::eventFilter(o, e); 194 } 195 QList<QWidget*>::const_iterator it_w; 196 for (it_w = m_parents.constBegin(); it_w != m_parents.constEnd(); ++it_w) { 197 if (isChildOf(w, (*it_w))) { 198 m_lastMousePressEvent = e; 199 break; 200 } 201 } 202 if (it_w == m_parents.constEnd()) { 203 m_lastMousePressEvent = e; 204 bool rc = false; 205 emit mousePressedOnExternalWidget(rc); 206 } 207 } 208 209 if (e->type() != QEvent::MouseButtonPress) { 210 m_lastMousePressEvent = 0; 211 } 212 } 213 return false; 214 } 215 216 Q_SIGNALS: 217 void mousePressedOnExternalWidget(bool&); 218 219 private: 220 QList<QWidget*> m_parents; 221 QEvent* m_lastMousePressEvent; 222 bool m_filterActive; 223 }; 224 225 class KGlobalLedgerViewPrivate : public KMyMoneyViewBasePrivate 226 { Q_DECLARE_PUBLIC(KGlobalLedgerView)227 Q_DECLARE_PUBLIC(KGlobalLedgerView) 228 229 public: 230 explicit KGlobalLedgerViewPrivate(KGlobalLedgerView *qq) : 231 q_ptr(qq), 232 m_mousePressFilter(0), 233 m_registerSearchLine(0), 234 m_precision(2), 235 m_recursion(false), 236 m_showDetails(false), 237 m_action(eWidgets::eRegister::Action::None), 238 m_filterProxyModel(0), 239 m_accountComboBox(0), 240 m_balanceIsApproximated(false), 241 m_toolbarFrame(nullptr), 242 m_registerFrame(nullptr), 243 m_buttonFrame(nullptr), 244 m_formFrame(nullptr), 245 m_summaryFrame(nullptr), 246 m_register(nullptr), 247 m_buttonbar(nullptr), 248 m_leftSummaryLabel(nullptr), 249 m_centerSummaryLabel(nullptr), 250 m_rightSummaryLabel(nullptr), 251 m_form(nullptr), 252 m_needLoad(true), 253 m_newAccountLoaded(true), 254 m_inEditMode(false), 255 m_transactionEditor(nullptr), 256 m_balanceWarning(nullptr), 257 m_moveToAccountSelector(nullptr), 258 m_endingBalanceDlg(nullptr), 259 m_searchDlg(nullptr) 260 { 261 } 262 ~KGlobalLedgerViewPrivate()263 ~KGlobalLedgerViewPrivate() 264 { 265 delete m_moveToAccountSelector; 266 delete m_endingBalanceDlg; 267 delete m_searchDlg; 268 } 269 init()270 void init() 271 { 272 Q_Q(KGlobalLedgerView); 273 m_needLoad = false; 274 auto vbox = new QVBoxLayout(q); 275 q->setLayout(vbox); 276 vbox->setSpacing(6); 277 vbox->setMargin(0); 278 279 m_mousePressFilter = new MousePressFilter((QWidget*)q); 280 m_action = eWidgets::eRegister::Action::None; 281 282 // the proxy filter model 283 m_filterProxyModel = new AccountNamesFilterProxyModel(q); 284 m_filterProxyModel->addAccountGroup(QVector<eMyMoney::Account::Type> {eMyMoney::Account::Type::Asset, eMyMoney::Account::Type::Liability, eMyMoney::Account::Type::Equity}); 285 auto const model = Models::instance()->accountsModel(); 286 m_filterProxyModel->setSourceModel(model); 287 m_filterProxyModel->setSourceColumns(model->getColumns()); 288 m_filterProxyModel->sort((int)eAccountsModel::Column::Account); 289 290 // create the toolbar frame at the top of the view 291 m_toolbarFrame = new QFrame(); 292 QHBoxLayout* toolbarLayout = new QHBoxLayout(m_toolbarFrame); 293 toolbarLayout->setContentsMargins(0, 0, 0, 0); 294 toolbarLayout->setSpacing(6); 295 296 // the account selector widget 297 m_accountComboBox = new KMyMoneyAccountCombo(); 298 m_accountComboBox->setModel(m_filterProxyModel); 299 toolbarLayout->addWidget(m_accountComboBox); 300 301 vbox->addWidget(m_toolbarFrame); 302 toolbarLayout->setStretchFactor(m_accountComboBox, 60); 303 // create the register frame 304 m_registerFrame = new QFrame(); 305 QVBoxLayout* registerFrameLayout = new QVBoxLayout(m_registerFrame); 306 registerFrameLayout->setContentsMargins(0, 0, 0, 0); 307 registerFrameLayout->setSpacing(0); 308 vbox->addWidget(m_registerFrame); 309 vbox->setStretchFactor(m_registerFrame, 2); 310 m_register = new KMyMoneyRegister::Register(m_registerFrame); 311 m_register->setUsedWithEditor(true); 312 registerFrameLayout->addWidget(m_register); 313 m_register->installEventFilter(q); 314 q->connect(m_register, &KMyMoneyRegister::Register::openContextMenu, q, &KGlobalLedgerView::slotTransactionsContextMenuRequested); 315 q->connect(m_register, &KMyMoneyRegister::Register::transactionsSelected, q, &KGlobalLedgerView::slotUpdateSummaryLine); 316 q->connect(m_register->horizontalHeader(), &QWidget::customContextMenuRequested, q, &KGlobalLedgerView::slotSortOptions); 317 q->connect(m_register, &KMyMoneyRegister::Register::reconcileStateColumnClicked, q, &KGlobalLedgerView::slotToggleTransactionMark); 318 319 // insert search line widget 320 321 m_registerSearchLine = new KMyMoneyRegister::RegisterSearchLineWidget(m_register, m_toolbarFrame); 322 toolbarLayout->addWidget(m_registerSearchLine); 323 toolbarLayout->setStretchFactor(m_registerSearchLine, 100); 324 // create the summary frame 325 m_summaryFrame = new QFrame(); 326 QHBoxLayout* summaryFrameLayout = new QHBoxLayout(m_summaryFrame); 327 summaryFrameLayout->setContentsMargins(0, 0, 0, 0); 328 summaryFrameLayout->setSpacing(0); 329 m_leftSummaryLabel = new QLabel(m_summaryFrame); 330 m_centerSummaryLabel = new QLabel(m_summaryFrame); 331 m_rightSummaryLabel = new QLabel(m_summaryFrame); 332 summaryFrameLayout->addWidget(m_leftSummaryLabel); 333 QSpacerItem* spacer = new QSpacerItem(20, 1, QSizePolicy::Expanding, QSizePolicy::Minimum); 334 summaryFrameLayout->addItem(spacer); 335 summaryFrameLayout->addWidget(m_centerSummaryLabel); 336 spacer = new QSpacerItem(20, 1, QSizePolicy::Expanding, QSizePolicy::Minimum); 337 summaryFrameLayout->addItem(spacer); 338 summaryFrameLayout->addWidget(m_rightSummaryLabel); 339 vbox->addWidget(m_summaryFrame); 340 341 // create the button frame 342 m_buttonFrame = new QFrame(q); 343 QVBoxLayout* buttonLayout = new QVBoxLayout(m_buttonFrame); 344 buttonLayout->setContentsMargins(0, 0, 0, 0); 345 buttonLayout->setSpacing(0); 346 vbox->addWidget(m_buttonFrame); 347 m_buttonbar = new KToolBar(m_buttonFrame, 0, true); 348 m_buttonbar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); 349 buttonLayout->addWidget(m_buttonbar); 350 351 m_buttonbar->addAction(pActions[eMenu::Action::NewTransaction]); 352 m_buttonbar->addAction(pActions[eMenu::Action::DeleteTransaction]); 353 m_buttonbar->addAction(pActions[eMenu::Action::EditTransaction]); 354 m_buttonbar->addAction(pActions[eMenu::Action::EnterTransaction]); 355 m_buttonbar->addAction(pActions[eMenu::Action::CancelTransaction]); 356 m_buttonbar->addAction(pActions[eMenu::Action::AcceptTransaction]); 357 m_buttonbar->addAction(pActions[eMenu::Action::MatchTransaction]); 358 359 // create the transaction form frame 360 m_formFrame = new QFrame(q); 361 QVBoxLayout* frameLayout = new QVBoxLayout(m_formFrame); 362 frameLayout->setContentsMargins(5, 5, 5, 5); 363 frameLayout->setSpacing(0); 364 m_form = new KMyMoneyTransactionForm::TransactionForm(m_formFrame); 365 frameLayout->addWidget(m_form->getTabBar(m_formFrame)); 366 frameLayout->addWidget(m_form); 367 m_formFrame->setFrameShape(QFrame::Panel); 368 m_formFrame->setFrameShadow(QFrame::Raised); 369 vbox->addWidget(m_formFrame); 370 371 q->connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KGlobalLedgerView::refresh); 372 q->connect(MyMoneyFile::instance(), &MyMoneyFile::dataChanged, q, &KGlobalLedgerView::slotUpdateMoveToAccountMenu); 373 q->connect(m_register, static_cast<void (KMyMoneyRegister::Register::*)(KMyMoneyRegister::Transaction *)>(&KMyMoneyRegister::Register::focusChanged), m_form, &KMyMoneyTransactionForm::TransactionForm::slotSetTransaction); 374 q->connect(m_register, static_cast<void (KMyMoneyRegister::Register::*)()>(&KMyMoneyRegister::Register::focusChanged), q, &KGlobalLedgerView::updateLedgerActionsInternal); 375 // q->connect(m_accountComboBox, &KMyMoneyAccountCombo::accountSelected, q, &KGlobalLedgerView::slotAccountSelected); 376 q->connect(m_accountComboBox, &KMyMoneyAccountCombo::accountSelected, q, static_cast<void (KGlobalLedgerView::*)(const QString&)>(&KGlobalLedgerView::slotSelectAccount)); 377 q->connect(m_accountComboBox, &KMyMoneyAccountCombo::accountSelected, q, &KGlobalLedgerView::slotUpdateMoveToAccountMenu); 378 q->connect(m_register, &KMyMoneyRegister::Register::transactionsSelected, q, &KGlobalLedgerView::slotTransactionsSelected); 379 q->connect(m_register, &KMyMoneyRegister::Register::transactionsSelected, q, &KGlobalLedgerView::slotUpdateMoveToAccountMenu); 380 q->connect(m_register, &KMyMoneyRegister::Register::editTransaction, q, &KGlobalLedgerView::slotEditTransaction); 381 q->connect(m_register, &KMyMoneyRegister::Register::emptyItemSelected, q, &KGlobalLedgerView::slotNewTransaction); 382 q->connect(m_register, &KMyMoneyRegister::Register::aboutToSelectItem, q, &KGlobalLedgerView::slotAboutToSelectItem); 383 q->connect(m_mousePressFilter, &MousePressFilter::mousePressedOnExternalWidget, q, &KGlobalLedgerView::slotCancelOrEnterTransactions); 384 385 q->connect(m_form, &KMyMoneyTransactionForm::TransactionForm::newTransaction, q, static_cast<void (KGlobalLedgerView::*)(eWidgets::eRegister::Action)>(&KGlobalLedgerView::slotNewTransactionForm)); 386 387 // setup mouse press filter 388 m_mousePressFilter->addWidget(m_formFrame); 389 m_mousePressFilter->addWidget(m_buttonFrame); 390 m_mousePressFilter->addWidget(m_summaryFrame); 391 m_mousePressFilter->addWidget(m_registerFrame); 392 393 m_tooltipPosn = QPoint(); 394 } 395 396 /** 397 * This method reloads the account selection combo box of the 398 * view with all asset and liability accounts from the engine. 399 * If the account id of the current account held in @p m_accountId is 400 * empty or if the referenced account does not exist in the engine, 401 * the first account found in the list will be made the current account. 402 */ loadAccounts()403 void loadAccounts() 404 { 405 const auto file = MyMoneyFile::instance(); 406 407 // check if the current account still exists and make it the 408 // current account 409 if (!m_lastSelectedAccountID.isEmpty()) { 410 try { 411 m_currentAccount = file->account(m_lastSelectedAccountID); 412 } catch (const MyMoneyException &) { 413 m_lastSelectedAccountID.clear(); 414 m_currentAccount = MyMoneyAccount(); 415 m_accountComboBox->setSelected(QString()); 416 } 417 } 418 419 // TODO: check why the invalidate is needed here 420 m_filterProxyModel->invalidate(); 421 m_filterProxyModel->sort((int)eAccountsModel::Column::Account); 422 m_filterProxyModel->setHideClosedAccounts(KMyMoneySettings::hideClosedAccounts() && !KMyMoneySettings::showAllAccounts()); 423 m_filterProxyModel->setHideEquityAccounts(!KMyMoneySettings::expertMode()); 424 m_accountComboBox->expandAll(); 425 426 if (m_currentAccount.id().isEmpty()) { 427 // find the first favorite account 428 QModelIndexList list = m_filterProxyModel->match(m_filterProxyModel->index(0, 0), 429 (int)eAccountsModel::Role::Favorite, 430 QVariant(true), 431 1, 432 Qt::MatchFlags(Qt::MatchExactly | Qt::MatchCaseSensitive | Qt::MatchRecursive)); 433 if (list.count() > 0) { 434 QVariant accountId = list.front().data((int)eAccountsModel::Role::ID); 435 if (accountId.isValid()) { 436 m_currentAccount = file->account(accountId.toString()); 437 } 438 } 439 440 if (m_currentAccount.id().isEmpty()) { 441 // there are no favorite accounts find any account 442 list = m_filterProxyModel->match(m_filterProxyModel->index(0, 0), 443 Qt::DisplayRole, 444 QVariant(QString("*")), 445 -1, 446 Qt::MatchFlags(Qt::MatchWildcard | Qt::MatchRecursive)); 447 for (QModelIndexList::ConstIterator it = list.constBegin(); it != list.constEnd(); ++it) { 448 if (!it->parent().isValid()) 449 continue; // skip the top level accounts 450 QVariant accountId = (*it).data((int)eAccountsModel::Role::ID); 451 if (accountId.isValid()) { 452 MyMoneyAccount a = file->account(accountId.toString()); 453 if (!a.isInvest() && !a.isClosed()) { 454 m_currentAccount = a; 455 break; 456 } 457 } 458 } 459 } 460 } 461 462 if (!m_currentAccount.id().isEmpty()) { 463 m_accountComboBox->setSelected(m_currentAccount.id()); 464 try { 465 m_precision = MyMoneyMoney::denomToPrec(m_currentAccount.fraction()); 466 } catch (const MyMoneyException &) { 467 qDebug("Security %s for account %s not found", qPrintable(m_currentAccount.currencyId()), qPrintable(m_currentAccount.name())); 468 m_precision = 2; 469 } 470 } 471 } 472 473 /** 474 * This method clears the register, form, transaction list. See @sa m_register, 475 * @sa m_transactionList 476 */ clear()477 void clear() 478 { 479 // clear current register contents 480 m_register->clear(); 481 482 // setup header font 483 QFont font = KMyMoneySettings::listHeaderFontEx(); 484 QFontMetrics fm(font); 485 int height = fm.lineSpacing() + 6; 486 m_register->horizontalHeader()->setMinimumHeight(height); 487 m_register->horizontalHeader()->setMaximumHeight(height); 488 m_register->horizontalHeader()->setFont(font); 489 490 // setup cell font 491 font = KMyMoneySettings::listCellFontEx(); 492 m_register->setFont(font); 493 494 // clear the form 495 m_form->clear(); 496 497 // the selected transactions list 498 m_transactionList.clear(); 499 500 // and the selected account in the combo box 501 m_accountComboBox->setSelected(QString()); 502 503 // fraction defaults to two digits 504 m_precision = 2; 505 } 506 loadView()507 void loadView() 508 { 509 MYMONEYTRACER(tracer); 510 Q_Q(KGlobalLedgerView); 511 512 // setup form visibility 513 m_formFrame->setVisible(KMyMoneySettings::transactionForm()); 514 515 // no account selected 516 // emit q->objectSelected(MyMoneyAccount()); 517 // no transaction selected 518 KMyMoneyRegister::SelectedTransactions list; 519 emit q->selectByVariant(QVariantList {QVariant::fromValue(list)}, eView::Intent::SelectRegisterTransactions); 520 521 QMap<QString, bool> isSelected; 522 QString focusItemId; 523 QString backUpFocusItemId; // in case the focus item is removed 524 QString anchorItemId; 525 QString backUpAnchorItemId; // in case the anchor item is removed 526 527 if (!m_newAccountLoaded) { 528 // remember the current selected transactions 529 KMyMoneyRegister::RegisterItem* item = m_register->firstItem(); 530 for (; item; item = item->nextItem()) { 531 if (item->isSelected()) { 532 isSelected[item->id()] = true; 533 } 534 } 535 // remember the item that has the focus 536 storeId(m_register->focusItem(), focusItemId, backUpFocusItemId); 537 // and the one that has the selection anchor 538 storeId(m_register->anchorItem(), anchorItemId, backUpAnchorItemId); 539 } else { 540 m_registerSearchLine->searchLine()->clear(); 541 } 542 543 // clear the current contents ... 544 clear(); 545 546 // ... load the combobox widget and select current account ... 547 loadAccounts(); 548 549 // ... setup the register columns ... 550 m_register->setupRegister(m_currentAccount); 551 552 // ... setup the form ... 553 m_form->setupForm(m_currentAccount); 554 555 if (m_currentAccount.id().isEmpty()) { 556 // if we don't have an account we bail out 557 q->setEnabled(false); 558 return; 559 } 560 q->setEnabled(true); 561 562 m_register->setUpdatesEnabled(false); 563 564 // ... and recreate it 565 KMyMoneyRegister::RegisterItem* focusItem = 0; 566 KMyMoneyRegister::RegisterItem* anchorItem = 0; 567 QMap<QString, MyMoneyMoney> actBalance, clearedBalance, futureBalance; 568 QMap<QString, MyMoneyMoney>::iterator it_b; 569 try { 570 // setup the filter to select the transactions we want to display 571 // and update the sort order 572 QString sortOrder; 573 QString key; 574 QDate reconciliationDate = m_reconciliationDate; 575 576 MyMoneyTransactionFilter filter(m_currentAccount.id()); 577 // if it's an investment account, we also take care of 578 // the sub-accounts (stock accounts) 579 if (m_currentAccount.accountType() == eMyMoney::Account::Type::Investment) 580 filter.addAccount(m_currentAccount.accountList()); 581 582 if (isReconciliationAccount()) { 583 key = "kmm-sort-reconcile"; 584 sortOrder = KMyMoneySettings::sortReconcileView(); 585 filter.addState((int)eMyMoney::TransactionFilter::State::NotReconciled); 586 filter.addState((int)eMyMoney::TransactionFilter::State::Cleared); 587 } else { 588 filter.setDateFilter(KMyMoneySettings::startDate().date(), QDate()); 589 key = "kmm-sort-std"; 590 sortOrder = KMyMoneySettings::sortNormalView(); 591 if (KMyMoneySettings::hideReconciledTransactions() 592 && !m_currentAccount.isIncomeExpense()) { 593 filter.addState((int)eMyMoney::TransactionFilter::State::NotReconciled); 594 filter.addState((int)eMyMoney::TransactionFilter::State::Cleared); 595 } 596 } 597 filter.setReportAllSplits(true); 598 599 // check if we have an account override of the sort order 600 if (!m_currentAccount.value(key).isEmpty()) 601 sortOrder = m_currentAccount.value(key); 602 603 // setup sort order 604 m_register->setSortOrder(sortOrder); 605 606 // retrieve the list from the engine 607 MyMoneyFile::instance()->transactionList(m_transactionList, filter); 608 609 emit q->slotStatusProgress(0, m_transactionList.count()); 610 611 // create the elements for the register 612 QList<QPair<MyMoneyTransaction, MyMoneySplit> >::const_iterator it; 613 QMap<QString, int>uniqueMap; 614 int i = 0; 615 for (it = m_transactionList.constBegin(); it != m_transactionList.constEnd(); ++it) { 616 uniqueMap[(*it).first.id()]++; 617 KMyMoneyRegister::Transaction* t = KMyMoneyRegister::Register::transactionFactory(m_register, (*it).first, (*it).second, uniqueMap[(*it).first.id()]); 618 actBalance[t->split().accountId()] = MyMoneyMoney(); 619 emit q->slotStatusProgress(++i, 0); 620 // if we're in reconciliation and the state is cleared, we 621 // force the item to show in dimmed intensity to get a visual focus 622 // on those items, that we need to work on 623 if (isReconciliationAccount() && (*it).second.reconcileFlag() == eMyMoney::Split::State::Cleared) { 624 t->setReducedIntensity(true); 625 } 626 } 627 628 // create dummy entries for the scheduled transactions if sorted by postdate 629 int period = KMyMoneySettings::schedulePreview(); 630 if (m_register->primarySortKey() == eWidgets::SortField::PostDate) { 631 // show scheduled transactions which have a scheduled postdate 632 // within the next 'period' days. In reconciliation mode, the 633 // period starts on the statement date. 634 QDate endDate = QDate::currentDate().addDays(period); 635 if (isReconciliationAccount()) 636 endDate = reconciliationDate.addDays(period); 637 QList<MyMoneySchedule> scheduleList = MyMoneyFile::instance()->scheduleList(m_currentAccount.id()); 638 while (!scheduleList.isEmpty()) { 639 MyMoneySchedule& s = scheduleList.first(); 640 for (;;) { 641 if (s.isFinished() || s.adjustedNextDueDate() > endDate) { 642 break; 643 } 644 645 MyMoneyTransaction t(s.id(), KMyMoneyUtils::scheduledTransaction(s)); 646 if (s.isOverdue() && !KMyMoneySettings::showPlannedScheduleDates()) { 647 // if the transaction is scheduled and overdue, it can't 648 // certainly be posted in the past. So we take today's date 649 // as the alternative 650 t.setPostDate(s.adjustedDate(QDate::currentDate(), s.weekendOption())); 651 } else { 652 t.setPostDate(s.adjustedNextDueDate()); 653 } 654 foreach (const auto split, t.splits()) { 655 if (split.accountId() == m_currentAccount.id()) { 656 new KMyMoneyRegister::StdTransactionScheduled(m_register, t, split, uniqueMap[t.id()]); 657 } 658 } 659 // keep track of this payment locally (not in the engine) 660 if (s.isOverdue() && !KMyMoneySettings::showPlannedScheduleDates()) { 661 s.setLastPayment(QDate::currentDate()); 662 } else { 663 s.setLastPayment(s.nextDueDate()); 664 } 665 666 // if this is a one time schedule, we can bail out here as we're done 667 if (s.occurrence() == eMyMoney::Schedule::Occurrence::Once) 668 break; 669 670 // for all others, we check if the next payment date is still 'in range' 671 QDate nextDueDate = s.nextPayment(s.nextDueDate()); 672 if (nextDueDate.isValid()) { 673 s.setNextDueDate(nextDueDate); 674 } else { 675 break; 676 } 677 } 678 scheduleList.pop_front(); 679 } 680 } 681 682 // add the group markers 683 m_register->addGroupMarkers(); 684 685 // sort the transactions according to the sort setting 686 m_register->sortItems(); 687 688 // remove trailing and adjacent markers 689 m_register->removeUnwantedGroupMarkers(); 690 691 // add special markers for reconciliation now so that they do not get 692 // removed by m_register->removeUnwantedGroupMarkers(). Needs resorting 693 // of items but that's ok. 694 695 KMyMoneyRegister::StatementGroupMarker* statement = 0; 696 KMyMoneyRegister::StatementGroupMarker* dStatement = 0; 697 KMyMoneyRegister::StatementGroupMarker* pStatement = 0; 698 699 if (isReconciliationAccount()) { 700 switch (m_register->primarySortKey()) { 701 case eWidgets::SortField::PostDate: 702 statement = new KMyMoneyRegister::StatementGroupMarker(m_register, eWidgets::eRegister::CashFlowDirection::Deposit, reconciliationDate, i18n("Statement Details")); 703 m_register->sortItems(); 704 break; 705 case eWidgets::SortField::Type: 706 dStatement = new KMyMoneyRegister::StatementGroupMarker(m_register, eWidgets::eRegister::CashFlowDirection::Deposit, reconciliationDate, i18n("Statement Deposit Details")); 707 pStatement = new KMyMoneyRegister::StatementGroupMarker(m_register, eWidgets::eRegister::CashFlowDirection::Payment, reconciliationDate, i18n("Statement Payment Details")); 708 m_register->sortItems(); 709 break; 710 default: 711 break; 712 } 713 } 714 715 // we need at least the balance for the account we currently show 716 actBalance[m_currentAccount.id()] = MyMoneyMoney(); 717 718 if (m_currentAccount.accountType() == eMyMoney::Account::Type::Investment) 719 foreach (const auto accountID, m_currentAccount.accountList()) 720 actBalance[accountID] = MyMoneyMoney(); 721 722 // determine balances (actual, cleared). We do this by getting the actual 723 // balance of all entered transactions from the engine and walk the list 724 // of transactions backward. Also re-select a transaction if it was 725 // selected before and setup the focus item. 726 727 MyMoneyMoney factor(1, 1); 728 if (m_currentAccount.accountGroup() == eMyMoney::Account::Type::Liability 729 || m_currentAccount.accountGroup() == eMyMoney::Account::Type::Equity) 730 factor = -factor; 731 732 QMap<QString, int> deposits; 733 QMap<QString, int> payments; 734 QMap<QString, MyMoneyMoney> depositAmount; 735 QMap<QString, MyMoneyMoney> paymentAmount; 736 for (it_b = actBalance.begin(); it_b != actBalance.end(); ++it_b) { 737 MyMoneyMoney balance = MyMoneyFile::instance()->balance(it_b.key()); 738 balance = balance * factor; 739 clearedBalance[it_b.key()] = 740 futureBalance[it_b.key()] = 741 (*it_b) = balance; 742 deposits[it_b.key()] = payments[it_b.key()] = 0; 743 depositAmount[it_b.key()] = MyMoneyMoney(); 744 paymentAmount[it_b.key()] = MyMoneyMoney(); 745 } 746 747 tracer.printf("total balance of %s = %s", qPrintable(m_currentAccount.name()), qPrintable(actBalance[m_currentAccount.id()].formatMoney("", 2))); 748 tracer.printf("future balance of %s = %s", qPrintable(m_currentAccount.name()), qPrintable(futureBalance[m_currentAccount.id()].formatMoney("", 2))); 749 tracer.printf("cleared balance of %s = %s", qPrintable(m_currentAccount.name()), qPrintable(clearedBalance[m_currentAccount.id()].formatMoney("", 2))); 750 751 KMyMoneyRegister::RegisterItem* p = m_register->lastItem(); 752 focusItem = 0; 753 754 // take care of possibly trailing scheduled transactions (bump up the future balance) 755 while (p) { 756 if (p->isSelectable()) { 757 KMyMoneyRegister::Transaction* t = dynamic_cast<KMyMoneyRegister::Transaction*>(p); 758 if (t && t->isScheduled()) { 759 MyMoneyMoney balance = futureBalance[t->split().accountId()]; 760 const MyMoneySplit& split = t->split(); 761 // if this split is a stock split, we can't just add the amount of shares 762 if (t->transaction().isStockSplit()) { 763 balance = balance * split.shares(); 764 } else { 765 balance += split.shares() * factor; 766 } 767 futureBalance[split.accountId()] = balance; 768 } else if (t && !focusItem) 769 focusItem = p; 770 } 771 p = p->prevItem(); 772 } 773 774 p = m_register->lastItem(); 775 while (p) { 776 KMyMoneyRegister::Transaction* t = dynamic_cast<KMyMoneyRegister::Transaction*>(p); 777 if (t) { 778 if (isSelected.contains(t->id())) 779 t->setSelected(true); 780 781 matchItemById(&focusItem, t, focusItemId, backUpFocusItemId); 782 matchItemById(&anchorItem, t, anchorItemId, backUpAnchorItemId); 783 784 const MyMoneySplit& split = t->split(); 785 MyMoneyMoney balance = futureBalance[split.accountId()]; 786 t->setBalance(balance); 787 788 // if this split is a stock split, we can't just add the amount of shares 789 if (t->transaction().isStockSplit()) { 790 balance /= split.shares(); 791 } else { 792 balance -= split.shares() * factor; 793 } 794 795 if (!t->isScheduled()) { 796 if (isReconciliationAccount() && t->transaction().postDate() <= reconciliationDate && split.reconcileFlag() == eMyMoney::Split::State::Cleared) { 797 if (split.shares().isNegative()) { 798 payments[split.accountId()]++; 799 paymentAmount[split.accountId()] += split.shares(); 800 } else { 801 deposits[split.accountId()]++; 802 depositAmount[split.accountId()] += split.shares(); 803 } 804 } 805 806 if (t->transaction().postDate() > QDate::currentDate()) { 807 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))); 808 actBalance[split.accountId()] -= split.shares() * factor; 809 } 810 } 811 futureBalance[split.accountId()] = balance; 812 } 813 p = p->prevItem(); 814 } 815 816 clearedBalance[m_currentAccount.id()] = MyMoneyFile::instance()->clearedBalance(m_currentAccount.id(), reconciliationDate); 817 818 tracer.printf("total balance of %s = %s", qPrintable(m_currentAccount.name()), qPrintable(actBalance[m_currentAccount.id()].formatMoney("", 2))); 819 tracer.printf("future balance of %s = %s", qPrintable(m_currentAccount.name()), qPrintable(futureBalance[m_currentAccount.id()].formatMoney("", 2))); 820 tracer.printf("cleared balance of %s = %s", qPrintable(m_currentAccount.name()), qPrintable(clearedBalance[m_currentAccount.id()].formatMoney("", 2))); 821 822 // update statement information 823 if (statement) { 824 const QString aboutDeposits = i18np("%1 deposit (%2)", "%1 deposits (%2)", 825 deposits[m_currentAccount.id()], depositAmount[m_currentAccount.id()].abs().formatMoney(m_currentAccount.fraction())); 826 const QString aboutCharges = i18np("%1 charge (%2)", "%1 charges (%2)", 827 deposits[m_currentAccount.id()], depositAmount[m_currentAccount.id()].abs().formatMoney(m_currentAccount.fraction())); 828 const QString aboutPayments = i18np("%1 payment (%2)", "%1 payments (%2)", 829 payments[m_currentAccount.id()], paymentAmount[m_currentAccount.id()].abs().formatMoney(m_currentAccount.fraction())); 830 831 statement->setText(i18nc("%1 is a string, e.g. 7 deposits; %2 is a string, e.g. 4 payments", "%1, %2", 832 m_currentAccount.accountType() == eMyMoney::Account::Type::CreditCard ? aboutCharges : aboutDeposits, 833 aboutPayments)); 834 } 835 if (pStatement) { 836 pStatement->setText(i18np("%1 payment (%2)", "%1 payments (%2)", payments[m_currentAccount.id()] 837 , paymentAmount[m_currentAccount.id()].abs().formatMoney(m_currentAccount.fraction()))); 838 } 839 if (dStatement) { 840 dStatement->setText(i18np("%1 deposit (%2)", "%1 deposits (%2)", deposits[m_currentAccount.id()] 841 , depositAmount[m_currentAccount.id()].abs().formatMoney(m_currentAccount.fraction()))); 842 } 843 844 // add a last empty entry for new transactions 845 // leave some information about the current account 846 MyMoneySplit split; 847 split.setReconcileFlag(eMyMoney::Split::State::NotReconciled); 848 // make sure to use the value specified in the option during reconciliation 849 if (isReconciliationAccount()) 850 split.setReconcileFlag(static_cast<eMyMoney::Split::State>(KMyMoneySettings::defaultReconciliationState())); 851 MyMoneyTransaction emptyTransaction; 852 emptyTransaction.setCommodity(m_currentAccount.currencyId()); 853 KMyMoneyRegister::Register::transactionFactory(m_register, emptyTransaction, split, 0); 854 855 m_register->updateRegister(true); 856 857 if (focusItem) { 858 // in case we have some selected items we just set the focus item 859 // in other cases, we make the focusitem also the selected item 860 if (anchorItem && (anchorItem != focusItem)) { 861 m_register->setFocusItem(focusItem); 862 m_register->setAnchorItem(anchorItem); 863 } else 864 m_register->selectItem(focusItem, true); 865 } else { 866 // just use the empty line at the end if nothing else exists in the ledger 867 p = m_register->lastItem(); 868 m_register->setFocusItem(p); 869 m_register->selectItem(p); 870 focusItem = p; 871 } 872 873 updateSummaryLine(actBalance, clearedBalance); 874 emit q->slotStatusProgress(-1, -1); 875 876 } catch (const MyMoneyException &) { 877 m_currentAccount = MyMoneyAccount(); 878 clear(); 879 } 880 881 m_showDetails = KMyMoneySettings::showRegisterDetailed(); 882 883 // and tell everyone what's selected 884 emit q->selectByObject(m_currentAccount, eView::Intent::None); 885 KMyMoneyRegister::SelectedTransactions actualSelection(m_register); 886 emit q->selectByVariant(QVariantList {QVariant::fromValue(actualSelection)}, eView::Intent::SelectRegisterTransactions); 887 } 888 selectTransaction(const QString & id)889 void selectTransaction(const QString& id) 890 { 891 if (!id.isEmpty()) { 892 KMyMoneyRegister::RegisterItem* p = m_register->lastItem(); 893 while (p) { 894 KMyMoneyRegister::Transaction* t = dynamic_cast<KMyMoneyRegister::Transaction*>(p); 895 if (t) { 896 if (t->transaction().id() == id) { 897 m_register->selectItem(t); 898 m_register->ensureItemVisible(t); 899 break; 900 } 901 } 902 p = p->prevItem(); 903 } 904 } 905 } 906 907 /** 908 * @brief selects transactions for processing with slots 909 * @param list of transactions 910 * @return false if only schedule is to be selected 911 */ selectTransactions(const KMyMoneyRegister::SelectedTransactions list)912 bool selectTransactions(const KMyMoneyRegister::SelectedTransactions list) 913 { 914 Q_Q(KGlobalLedgerView); 915 // list can either contain a list of transactions or a single selected scheduled transaction 916 // in the latter case, the transaction id is actually the one of the schedule. In order 917 // to differentiate between the two, we just ask for the schedule. If we don't find one - because 918 // we passed the id of a real transaction - then we know that fact. We use the schedule here, 919 // because the list of schedules is kept in a cache by MyMoneyFile. This way, we save some trips 920 // to the backend which we would have to do if we check for the transaction. 921 m_selectedTransactions.clear(); 922 auto sch = MyMoneySchedule(); 923 auto ret = true; 924 925 m_accountGoto.clear(); 926 m_payeeGoto.clear(); 927 if (!list.isEmpty() && !list.first().isScheduled()) { 928 m_selectedTransactions = list; 929 if (list.count() == 1) { 930 const MyMoneySplit& sp = m_selectedTransactions[0].split(); 931 if (!sp.payeeId().isEmpty()) { 932 try { 933 auto payee = MyMoneyFile::instance()->payee(sp.payeeId()); 934 if (!payee.name().isEmpty()) { 935 m_payeeGoto = payee.id(); 936 auto name = payee.name(); 937 name.replace(QRegExp("&(?!&)"), "&&"); 938 pActions[Action::GoToPayee]->setText(i18n("Go to '%1'", name)); 939 } 940 } catch (const MyMoneyException &) { 941 } 942 } 943 try { 944 const auto& t = m_selectedTransactions[0].transaction(); 945 // search the first non-income/non-expense account and use it for the 'goto account' 946 const auto& selectedTransactionSplit = m_selectedTransactions[0].split(); 947 foreach (const auto split, t.splits()) { 948 if (split.id() != selectedTransactionSplit.id()) { 949 auto acc = MyMoneyFile::instance()->account(split.accountId()); 950 if (!acc.isIncomeExpense()) { 951 // for stock accounts we show the portfolio account 952 if (acc.isInvest()) { 953 acc = MyMoneyFile::instance()->account(acc.parentAccountId()); 954 } 955 m_accountGoto = acc.id(); 956 auto name = acc.name(); 957 name.replace(QRegExp("&(?!&)"), "&&"); 958 pActions[Action::GoToAccount]->setText(i18n("Go to '%1'", name)); 959 break; 960 } 961 } 962 } 963 } catch (const MyMoneyException &) { 964 } 965 } 966 } else if (!list.isEmpty()) { 967 sch = MyMoneyFile::instance()->schedule(list.first().scheduleId()); 968 m_selectedTransactions.append(list.first()); 969 ret = false; 970 } 971 972 emit q->selectByObject(sch, eView::Intent::None); 973 974 // make sure, we show some neutral menu entry if we don't have an object 975 if (m_payeeGoto.isEmpty()) 976 pActions[Action::GoToPayee]->setText(i18n("Go to payee")); 977 if (m_accountGoto.isEmpty()) 978 pActions[Action::GoToAccount]->setText(i18n("Go to account")); 979 return ret; 980 } 981 982 /** 983 * Returns @a true if setReconciliationAccount() has been called for 984 * the current loaded account. 985 * 986 * @retval true current account is in reconciliation mode 987 * @retval false current account is not in reconciliation mode 988 */ isReconciliationAccount()989 bool isReconciliationAccount() const 990 { 991 return m_currentAccount.id() == m_reconciliationAccount.id(); 992 } 993 994 /** 995 * Updates the values on the summary line beneath the register with 996 * the given values. The contents shown differs between reconciliation 997 * mode and normal mode. 998 * 999 * @param actBalance map of account indexed values to be used as actual balance 1000 * @param clearedBalance map of account indexed values to be used as cleared balance 1001 */ updateSummaryLine(const QMap<QString,MyMoneyMoney> & actBalance,const QMap<QString,MyMoneyMoney> & clearedBalance)1002 void updateSummaryLine(const QMap<QString, MyMoneyMoney>& actBalance, const QMap<QString, MyMoneyMoney>& clearedBalance) 1003 { 1004 Q_Q(KGlobalLedgerView); 1005 const auto file = MyMoneyFile::instance(); 1006 m_leftSummaryLabel->show(); 1007 m_centerSummaryLabel->show(); 1008 m_rightSummaryLabel->show(); 1009 1010 if (isReconciliationAccount()) { 1011 if (m_currentAccount.accountType() != eMyMoney::Account::Type::Investment) { 1012 m_leftSummaryLabel->setText(i18n("Statement: %1", m_endingBalance.formatMoney("", m_precision))); 1013 m_centerSummaryLabel->setText(i18nc("Cleared balance", "Cleared: %1", clearedBalance[m_currentAccount.id()].formatMoney("", m_precision))); 1014 m_totalBalance = clearedBalance[m_currentAccount.id()] - m_endingBalance; 1015 } 1016 } else { 1017 // update summary line in normal mode 1018 QDate reconcileDate = m_currentAccount.lastReconciliationDate(); 1019 1020 if (reconcileDate.isValid()) { 1021 m_leftSummaryLabel->setText(i18n("Last reconciled: %1", QLocale().toString(reconcileDate, QLocale::ShortFormat))); 1022 } else { 1023 m_leftSummaryLabel->setText(i18n("Never reconciled")); 1024 } 1025 1026 QPalette palette = m_rightSummaryLabel->palette(); 1027 palette.setColor(m_rightSummaryLabel->foregroundRole(), m_leftSummaryLabel->palette().color(q->foregroundRole())); 1028 if (m_currentAccount.accountType() != eMyMoney::Account::Type::Investment) { 1029 m_centerSummaryLabel->setText(i18nc("Cleared balance", "Cleared: %1", clearedBalance[m_currentAccount.id()].formatMoney("", m_precision))); 1030 m_totalBalance = actBalance[m_currentAccount.id()]; 1031 } else { 1032 m_centerSummaryLabel->hide(); 1033 MyMoneyMoney balance; 1034 MyMoneySecurity base = file->baseCurrency(); 1035 QMap<QString, MyMoneyMoney>::const_iterator it_b; 1036 // reset the approximated flag 1037 m_balanceIsApproximated = false; 1038 for (it_b = actBalance.begin(); it_b != actBalance.end(); ++it_b) { 1039 MyMoneyAccount stock = file->account(it_b.key()); 1040 QString currencyId = stock.currencyId(); 1041 MyMoneySecurity sec = file->security(currencyId); 1042 MyMoneyMoney rate(1, 1); 1043 1044 if (stock.isInvest()) { 1045 currencyId = sec.tradingCurrency(); 1046 const MyMoneyPrice &priceInfo = file->price(sec.id(), currencyId); 1047 m_balanceIsApproximated |= !priceInfo.isValid(); 1048 rate = priceInfo.rate(sec.tradingCurrency()); 1049 } 1050 1051 if (currencyId != base.id()) { 1052 const MyMoneyPrice &priceInfo = file->price(sec.tradingCurrency(), base.id()); 1053 m_balanceIsApproximated |= !priceInfo.isValid(); 1054 rate = (rate * priceInfo.rate(base.id())).convertPrecision(sec.pricePrecision()); 1055 } 1056 balance += ((*it_b) * rate).convert(base.smallestAccountFraction()); 1057 } 1058 m_totalBalance = balance; 1059 } 1060 m_rightSummaryLabel->setPalette(palette); 1061 } 1062 // determine the number of selected transactions 1063 KMyMoneyRegister::SelectedTransactions selection; 1064 m_register->selectedTransactions(selection); 1065 q->slotUpdateSummaryLine(selection); 1066 } 1067 1068 /** 1069 * setup the default action according to the current account type 1070 */ setupDefaultAction()1071 void setupDefaultAction() 1072 { 1073 switch (m_currentAccount.accountType()) { 1074 case eMyMoney::Account::Type::Asset: 1075 case eMyMoney::Account::Type::AssetLoan: 1076 case eMyMoney::Account::Type::Savings: 1077 m_action = eWidgets::eRegister::Action::Deposit; 1078 break; 1079 default: 1080 m_action = eWidgets::eRegister::Action::Withdrawal; 1081 break; 1082 } 1083 } 1084 1085 // used to store the id of an item and the id of an immediate unselected sibling storeId(KMyMoneyRegister::RegisterItem * item,QString & id,QString & backupId)1086 void storeId(KMyMoneyRegister::RegisterItem *item, QString &id, QString &backupId) { 1087 if (item) { 1088 // the id of the item 1089 id = item->id(); 1090 // the id of the item's previous/next unselected item 1091 for (KMyMoneyRegister::RegisterItem *it = item->prevItem(); it != 0 && backupId.isEmpty(); it = it->prevItem()) { 1092 if (!it->isSelected()) { 1093 backupId = it->id(); 1094 } 1095 } 1096 // if we didn't found previous unselected items search trough the next items 1097 for (KMyMoneyRegister::RegisterItem *it = item->nextItem(); it != 0 && backupId.isEmpty(); it = it->nextItem()) { 1098 if (!it->isSelected()) { 1099 backupId = it->id(); 1100 } 1101 } 1102 } 1103 } 1104 1105 // use to match an item by it's id or a backup id which has a lower precedence matchItemById(KMyMoneyRegister::RegisterItem ** item,KMyMoneyRegister::Transaction * t,QString & id,QString & backupId)1106 void matchItemById(KMyMoneyRegister::RegisterItem **item, KMyMoneyRegister::Transaction* t, QString &id, QString &backupId) { 1107 if (!backupId.isEmpty() && t->id() == backupId) 1108 *item = t; 1109 if (!id.isEmpty() && t->id() == id) { 1110 // we found the real thing there's no need for the backup anymore 1111 backupId.clear(); 1112 *item = t; 1113 } 1114 } 1115 canProcessTransactions(const KMyMoneyRegister::SelectedTransactions & list,QString & tooltip)1116 bool canProcessTransactions(const KMyMoneyRegister::SelectedTransactions& list, QString& tooltip) const 1117 { 1118 if (m_register->focusItem() == 0) 1119 return false; 1120 1121 bool rc = true; 1122 if (list.warnLevel() == KMyMoneyRegister::SelectedTransaction::OneAccountClosed) { 1123 // scan all splits for the first closed account 1124 QString closedAccount; 1125 foreach(const auto selectedTransaction, list) { 1126 foreach(const auto split, selectedTransaction.transaction().splits()) { 1127 const auto id = split.accountId(); 1128 const auto acc = MyMoneyFile::instance()->account(id); 1129 if (acc.isClosed()) { 1130 closedAccount = acc.name(); 1131 // we're done 1132 rc = false; 1133 break; 1134 } 1135 } 1136 if(!rc) 1137 break; 1138 } 1139 tooltip = i18n("Cannot process transactions in account %1, which is closed.", closedAccount); 1140 showTooltip(tooltip); 1141 return false; 1142 } 1143 1144 if (!m_register->focusItem()->isSelected()) { 1145 tooltip = i18n("Cannot process transaction with focus if it is not selected."); 1146 showTooltip(tooltip); 1147 return false; 1148 } 1149 tooltip.clear(); 1150 return !list.isEmpty(); 1151 } 1152 showTooltip(const QString msg)1153 void showTooltip(const QString msg) const 1154 { 1155 QToolTip::showText(m_tooltipPosn, msg); 1156 } 1157 createNewTransaction()1158 bool createNewTransaction() 1159 { 1160 Q_Q(KGlobalLedgerView); 1161 auto rc = false; 1162 QString txt; 1163 if (q->canCreateTransactions(txt)) { 1164 rc = q->selectEmptyTransaction(); 1165 } 1166 return rc; 1167 } 1168 startEdit(const KMyMoneyRegister::SelectedTransactions & list)1169 TransactionEditor* startEdit(const KMyMoneyRegister::SelectedTransactions& list) 1170 { 1171 Q_Q(KGlobalLedgerView); 1172 TransactionEditor* editor = 0; 1173 QString txt; 1174 if (q->canEditTransactions(list, txt) || q->canCreateTransactions(txt)) { 1175 editor = q->startEdit(list); 1176 } 1177 return editor; 1178 } 1179 doDeleteTransactions()1180 void doDeleteTransactions() 1181 { 1182 Q_Q(KGlobalLedgerView); 1183 KMyMoneyRegister::SelectedTransactions list = m_selectedTransactions; 1184 KMyMoneyRegister::SelectedTransactions::iterator it_t; 1185 int cnt = list.count(); 1186 int i = 0; 1187 emit q->slotStatusProgress(0, cnt); 1188 MyMoneyFileTransaction ft; 1189 const auto file = MyMoneyFile::instance(); 1190 try { 1191 it_t = list.begin(); 1192 while (it_t != list.end()) { 1193 const auto accountId = (*it_t).split().accountId(); 1194 const auto deletedNum = (*it_t).split().number(); 1195 const auto transactionId = (*it_t).transaction().id(); 1196 // only remove those transactions that do not reference a closed account 1197 if (!file->referencesClosedAccount((*it_t).transaction())) { 1198 file->removeTransaction((*it_t).transaction()); 1199 // remove all those references in the list of selected transactions 1200 // that refer to the same transaction we just removed so that we 1201 // will not be caught by an exception later on (see bko #285310) 1202 while (it_t != list.end()) { 1203 if (transactionId == (*it_t).transaction().id()) { 1204 it_t = list.erase(it_t); 1205 i++; // bump count of deleted transactions 1206 } else { 1207 ++it_t; 1208 } 1209 } 1210 1211 } else { 1212 list.erase(it_t); 1213 } 1214 it_t = list.begin(); 1215 1216 // need to ensure "nextCheckNumber" is still correct 1217 auto acc = file->account(accountId); 1218 1219 // the "lastNumberUsed" might have been the txn number deleted 1220 // so adjust it 1221 if (deletedNum == acc.value("lastNumberUsed")) { 1222 // decrement deletedNum and set new "lastNumberUsed" 1223 QString num = KMyMoneyUtils::getAdjacentNumber(deletedNum, -1); 1224 acc.setValue("lastNumberUsed", num); 1225 file->modifyAccount(acc); 1226 } 1227 1228 emit q->slotStatusProgress(i++, 0); 1229 } 1230 ft.commit(); 1231 1232 } catch (const MyMoneyException &e) { 1233 KMessageBox::detailedSorry(q, i18n("Unable to delete transaction(s)"), e.what()); 1234 } 1235 emit q->slotStatusProgress(-1, -1); 1236 } 1237 deleteTransactionEditor()1238 void deleteTransactionEditor() 1239 { 1240 // make sure, we don't use the transaction editor pointer 1241 // anymore from now on 1242 auto p = m_transactionEditor; 1243 m_transactionEditor = nullptr; 1244 delete p; 1245 } 1246 transactionUnmatch()1247 void transactionUnmatch() 1248 { 1249 Q_Q(KGlobalLedgerView); 1250 KMyMoneyRegister::SelectedTransactions::const_iterator it; 1251 MyMoneyFileTransaction ft; 1252 try { 1253 for (it = m_selectedTransactions.constBegin(); it != m_selectedTransactions.constEnd(); ++it) { 1254 if ((*it).split().isMatched()) { 1255 TransactionMatcher matcher(m_currentAccount); 1256 matcher.unmatch((*it).transaction(), (*it).split()); 1257 } 1258 } 1259 ft.commit(); 1260 1261 } catch (const MyMoneyException &e) { 1262 KMessageBox::detailedSorry(q, i18n("Unable to unmatch the selected transactions"), e.what()); 1263 } 1264 } 1265 transactionMatch()1266 void transactionMatch() 1267 { 1268 Q_Q(KGlobalLedgerView); 1269 if (m_selectedTransactions.count() != 2) 1270 return; 1271 1272 MyMoneyTransaction startMatchTransaction; 1273 MyMoneyTransaction endMatchTransaction; 1274 MyMoneySplit startSplit; 1275 MyMoneySplit endSplit; 1276 1277 KMyMoneyRegister::SelectedTransactions::const_iterator it; 1278 KMyMoneyRegister::SelectedTransactions toBeDeleted; 1279 for (it = m_selectedTransactions.constBegin(); it != m_selectedTransactions.constEnd(); ++it) { 1280 if ((*it).transaction().isImported()) { 1281 if (endMatchTransaction.id().isEmpty()) { 1282 endMatchTransaction = (*it).transaction(); 1283 endSplit = (*it).split(); 1284 toBeDeleted << *it; 1285 } else { 1286 //This is a second imported transaction, we still want to merge 1287 startMatchTransaction = (*it).transaction(); 1288 startSplit = (*it).split(); 1289 } 1290 } else if (!(*it).split().isMatched()) { 1291 if (startMatchTransaction.id().isEmpty()) { 1292 startMatchTransaction = (*it).transaction(); 1293 startSplit = (*it).split(); 1294 } else { 1295 endMatchTransaction = (*it).transaction(); 1296 endSplit = (*it).split(); 1297 toBeDeleted << *it; 1298 } 1299 } 1300 } 1301 1302 #if 0 1303 KMergeTransactionsDlg dlg(m_selectedAccount); 1304 dlg.addTransaction(startMatchTransaction); 1305 dlg.addTransaction(endMatchTransaction); 1306 if (dlg.exec() == QDialog::Accepted) 1307 #endif 1308 { 1309 MyMoneyFileTransaction ft; 1310 try { 1311 if (startMatchTransaction.id().isEmpty()) 1312 throw MYMONEYEXCEPTION(QString::fromLatin1("No manually entered transaction selected for matching")); 1313 if (endMatchTransaction.id().isEmpty()) 1314 throw MYMONEYEXCEPTION(QString::fromLatin1("No imported transaction selected for matching")); 1315 1316 TransactionMatcher matcher(m_currentAccount); 1317 matcher.match(startMatchTransaction, startSplit, endMatchTransaction, endSplit, true); 1318 ft.commit(); 1319 } catch (const MyMoneyException &e) { 1320 KMessageBox::detailedSorry(q, i18n("Unable to match the selected transactions"), e.what()); 1321 } 1322 } 1323 } 1324 1325 /** 1326 * Mark the selected transactions as provided by @a flag. If 1327 * flag is @a MyMoneySplit::Unknown, the future state depends 1328 * on the current stat of the split's flag according to the 1329 * following table: 1330 * 1331 * - NotReconciled --> Cleared 1332 * - Cleared --> Reconciled 1333 * - Reconciled --> NotReconciled 1334 */ markTransaction(eMyMoney::Split::State flag)1335 void markTransaction(eMyMoney::Split::State flag) 1336 { 1337 Q_Q(KGlobalLedgerView); 1338 auto list = m_selectedTransactions; 1339 KMyMoneyRegister::SelectedTransactions::const_iterator it_t; 1340 auto cnt = list.count(); 1341 auto i = 0; 1342 emit q->slotStatusProgress(0, cnt); 1343 MyMoneyFileTransaction ft; 1344 try { 1345 for (it_t = list.constBegin(); it_t != list.constEnd(); ++it_t) { 1346 // turn on signals before we modify the last entry in the list 1347 cnt--; 1348 MyMoneyFile::instance()->blockSignals(cnt != 0); 1349 1350 // get a fresh copy 1351 auto t = MyMoneyFile::instance()->transaction((*it_t).transaction().id()); 1352 auto sp = t.splitById((*it_t).split().id()); 1353 if (sp.reconcileFlag() != flag) { 1354 if (flag == eMyMoney::Split::State::Unknown) { 1355 if (m_reconciliationAccount.id().isEmpty()) { 1356 // in normal mode we cycle through all states 1357 switch (sp.reconcileFlag()) { 1358 case eMyMoney::Split::State::NotReconciled: 1359 sp.setReconcileFlag(eMyMoney::Split::State::Cleared); 1360 break; 1361 case eMyMoney::Split::State::Cleared: 1362 sp.setReconcileFlag(eMyMoney::Split::State::Reconciled); 1363 break; 1364 case eMyMoney::Split::State::Reconciled: 1365 sp.setReconcileFlag(eMyMoney::Split::State::NotReconciled); 1366 break; 1367 default: 1368 break; 1369 } 1370 } else { 1371 // in reconciliation mode we skip the reconciled state 1372 switch (sp.reconcileFlag()) { 1373 case eMyMoney::Split::State::NotReconciled: 1374 sp.setReconcileFlag(eMyMoney::Split::State::Cleared); 1375 break; 1376 case eMyMoney::Split::State::Cleared: 1377 sp.setReconcileFlag(eMyMoney::Split::State::NotReconciled); 1378 break; 1379 default: 1380 break; 1381 } 1382 } 1383 } else { 1384 sp.setReconcileFlag(flag); 1385 } 1386 1387 t.modifySplit(sp); 1388 MyMoneyFile::instance()->modifyTransaction(t); 1389 } 1390 emit q->slotStatusProgress(i++, 0); 1391 } 1392 emit q->slotStatusProgress(-1, -1); 1393 ft.commit(); 1394 } catch (const MyMoneyException &e) { 1395 KMessageBox::detailedSorry(q, i18n("Unable to modify transaction"), e.what()); 1396 } 1397 } 1398 1399 // move a stock transaction from one investment account to another moveInvestmentTransaction(const QString &,const QString & toId,const MyMoneyTransaction & tx)1400 void moveInvestmentTransaction(const QString& /*fromId*/, 1401 const QString& toId, 1402 const MyMoneyTransaction& tx) 1403 { 1404 MyMoneyAccount toInvAcc = MyMoneyFile::instance()->account(toId); 1405 MyMoneyTransaction t(tx); 1406 // first determine which stock we are dealing with. 1407 // fortunately, investment transactions have only one stock involved 1408 QString stockAccountId; 1409 QString stockSecurityId; 1410 MyMoneySplit s; 1411 foreach (const auto split, t.splits()) { 1412 stockAccountId = split.accountId(); 1413 stockSecurityId = 1414 MyMoneyFile::instance()->account(stockAccountId).currencyId(); 1415 if (!MyMoneyFile::instance()->security(stockSecurityId).isCurrency()) { 1416 s = split; 1417 break; 1418 } 1419 } 1420 // Now check the target investment account to see if it 1421 // contains a stock with this id 1422 QString newStockAccountId; 1423 foreach (const auto sAccount, toInvAcc.accountList()) { 1424 if (MyMoneyFile::instance()->account(sAccount).currencyId() == 1425 stockSecurityId) { 1426 newStockAccountId = sAccount; 1427 break; 1428 } 1429 } 1430 // if it doesn't exist, we need to add it as a copy of the old one 1431 // no 'copyAccount()' function?? 1432 if (newStockAccountId.isEmpty()) { 1433 MyMoneyAccount stockAccount = 1434 MyMoneyFile::instance()->account(stockAccountId); 1435 MyMoneyAccount newStock; 1436 newStock.setName(stockAccount.name()); 1437 newStock.setNumber(stockAccount.number()); 1438 newStock.setDescription(stockAccount.description()); 1439 newStock.setInstitutionId(stockAccount.institutionId()); 1440 newStock.setOpeningDate(stockAccount.openingDate()); 1441 newStock.setAccountType(stockAccount.accountType()); 1442 newStock.setCurrencyId(stockAccount.currencyId()); 1443 newStock.setClosed(stockAccount.isClosed()); 1444 MyMoneyFile::instance()->addAccount(newStock, toInvAcc); 1445 newStockAccountId = newStock.id(); 1446 } 1447 // now update the split and the transaction 1448 s.setAccountId(newStockAccountId); 1449 t.modifySplit(s); 1450 MyMoneyFile::instance()->modifyTransaction(t); 1451 } 1452 createTransactionMoveMenu()1453 void createTransactionMoveMenu() 1454 { 1455 Q_Q(KGlobalLedgerView); 1456 if (!m_moveToAccountSelector) { 1457 auto menu = pMenus[eMenu::Menu::MoveTransaction]; 1458 if (menu ) { 1459 auto accountSelectorAction = new QWidgetAction(menu); 1460 m_moveToAccountSelector = new KMyMoneyAccountSelector(menu, 0, false); 1461 m_moveToAccountSelector->setObjectName("transaction_move_menu_selector"); 1462 accountSelectorAction->setDefaultWidget(m_moveToAccountSelector); 1463 menu->addAction(accountSelectorAction); 1464 q->connect(m_moveToAccountSelector, &QObject::destroyed, q, &KGlobalLedgerView::slotObjectDestroyed); 1465 q->connect(m_moveToAccountSelector, &KMyMoneySelector::itemSelected, q, &KGlobalLedgerView::slotMoveToAccount); 1466 } 1467 } 1468 } 1469 automaticReconciliation(const MyMoneyAccount & account,const QList<QPair<MyMoneyTransaction,MyMoneySplit>> & transactions,const MyMoneyMoney & amount)1470 QList<QPair<MyMoneyTransaction, MyMoneySplit> > automaticReconciliation(const MyMoneyAccount &account, 1471 const QList<QPair<MyMoneyTransaction, MyMoneySplit> > &transactions, 1472 const MyMoneyMoney &amount) 1473 { 1474 Q_Q(KGlobalLedgerView); 1475 static const int NR_OF_STEPS_LIMIT = 60000; 1476 static const int PROGRESSBAR_STEPS = 1000; 1477 QList<QPair<MyMoneyTransaction, MyMoneySplit> > result = transactions; 1478 1479 // optimize the most common case - all transactions should be cleared 1480 QListIterator<QPair<MyMoneyTransaction, MyMoneySplit> > itTransactionSplitResult(result); 1481 MyMoneyMoney transactionsBalance; 1482 while (itTransactionSplitResult.hasNext()) { 1483 const QPair<MyMoneyTransaction, MyMoneySplit> &transactionSplit = itTransactionSplitResult.next(); 1484 transactionsBalance += transactionSplit.second.shares(); 1485 } 1486 if (amount == transactionsBalance) { 1487 result = transactions; 1488 return result; 1489 } 1490 1491 // only one transaction is uncleared 1492 itTransactionSplitResult.toFront(); 1493 int index = 0; 1494 while (itTransactionSplitResult.hasNext()) { 1495 const QPair<MyMoneyTransaction, MyMoneySplit> &transactionSplit = itTransactionSplitResult.next(); 1496 if (transactionsBalance - transactionSplit.second.shares() == amount) { 1497 result.removeAt(index); 1498 return result; 1499 } 1500 index++; 1501 } 1502 1503 // more than one transaction is uncleared - apply the algorithm 1504 result.clear(); 1505 1506 const auto& security = MyMoneyFile::instance()->security(account.currencyId()); 1507 double precision = 0.1 / account.fraction(security); 1508 1509 QList<MyMoneyMoney> sumList; 1510 sumList << MyMoneyMoney(); 1511 1512 QMap<MyMoneyMoney, QList<QPair<QString, QString> > > sumToComponentsMap; 1513 1514 struct restoreStatusMsgHelper { 1515 restoreStatusMsgHelper(KGlobalLedgerView* qq) 1516 : q(qq) {} 1517 1518 ~restoreStatusMsgHelper() 1519 { 1520 q->slotStatusMsg(QString()); 1521 q->slotStatusProgress(-1, -1); 1522 } 1523 KGlobalLedgerView* q; 1524 } restoreHelper(q); 1525 1526 q->slotStatusMsg(i18n("Running automatic reconciliation")); 1527 q->slotStatusProgress(0, NR_OF_STEPS_LIMIT); 1528 1529 // compute the possible matches 1530 QListIterator<QPair<MyMoneyTransaction, MyMoneySplit> > it_ts(transactions); 1531 while (it_ts.hasNext()) { 1532 const QPair<MyMoneyTransaction, MyMoneySplit> &transactionSplit = it_ts.next(); 1533 QListIterator<MyMoneyMoney> itSum(sumList); 1534 QList<MyMoneyMoney> tempList; 1535 while (itSum.hasNext()) { 1536 const MyMoneyMoney &sum = itSum.next(); 1537 QList<QPair<QString, QString> > splitIds; 1538 splitIds << qMakePair<QString, QString>(transactionSplit.first.id(), transactionSplit.second.id()); 1539 if (sumToComponentsMap.contains(sum)) { 1540 if (sumToComponentsMap.value(sum).contains(qMakePair<QString, QString>(transactionSplit.first.id(), transactionSplit.second.id()))) { 1541 continue; 1542 } 1543 splitIds.append(sumToComponentsMap.value(sum)); 1544 } 1545 tempList << transactionSplit.second.shares() + sum; 1546 sumToComponentsMap[transactionSplit.second.shares() + sum] = splitIds; 1547 int size = sumToComponentsMap.size(); 1548 if (size % PROGRESSBAR_STEPS == 0) { 1549 q->slotStatusProgress(size, 0); 1550 } 1551 if (size > NR_OF_STEPS_LIMIT) { 1552 return result; // it's taking too much resources abort the algorithm 1553 } 1554 } 1555 QList<MyMoneyMoney> unionList; 1556 unionList.append(tempList); 1557 unionList.append(sumList); 1558 qSort(unionList); 1559 sumList.clear(); 1560 MyMoneyMoney smallestSumFromUnion = unionList.first(); 1561 sumList.append(smallestSumFromUnion); 1562 QListIterator<MyMoneyMoney> itUnion(unionList); 1563 while (itUnion.hasNext()) { 1564 MyMoneyMoney sumFromUnion = itUnion.next(); 1565 if (smallestSumFromUnion < MyMoneyMoney(1 - precision / transactions.size())*sumFromUnion) { 1566 smallestSumFromUnion = sumFromUnion; 1567 sumList.append(sumFromUnion); 1568 } 1569 } 1570 } 1571 1572 q->slotStatusProgress(NR_OF_STEPS_LIMIT / PROGRESSBAR_STEPS, 0); 1573 if (sumToComponentsMap.contains(amount)) { 1574 QListIterator<QPair<MyMoneyTransaction, MyMoneySplit> > itTransactionSplit(transactions); 1575 while (itTransactionSplit.hasNext()) { 1576 const QPair<MyMoneyTransaction, MyMoneySplit> &transactionSplit = itTransactionSplit.next(); 1577 const QList<QPair<QString, QString> > &splitIds = sumToComponentsMap.value(amount); 1578 if (splitIds.contains(qMakePair<QString, QString>(transactionSplit.first.id(), transactionSplit.second.id()))) { 1579 result.append(transactionSplit); 1580 } 1581 } 1582 } 1583 1584 #ifdef KMM_DEBUG 1585 qDebug("For the amount %s a number of %d possible sums where computed from the set of %d transactions: ", 1586 qPrintable(MyMoneyUtils::formatMoney(amount, security)), sumToComponentsMap.size(), transactions.size()); 1587 #endif 1588 1589 return result; 1590 } 1591 1592 KGlobalLedgerView *q_ptr; 1593 MousePressFilter *m_mousePressFilter; 1594 KMyMoneyRegister::RegisterSearchLineWidget* m_registerSearchLine; 1595 // QString m_reconciliationAccount; 1596 QDate m_reconciliationDate; 1597 MyMoneyMoney m_endingBalance; 1598 int m_precision; 1599 bool m_recursion; 1600 bool m_showDetails; 1601 eWidgets::eRegister::Action m_action; 1602 1603 // models 1604 AccountNamesFilterProxyModel *m_filterProxyModel; 1605 1606 // widgets 1607 KMyMoneyAccountCombo* m_accountComboBox; 1608 1609 MyMoneyMoney m_totalBalance; 1610 bool m_balanceIsApproximated; 1611 // frames 1612 QFrame* m_toolbarFrame; 1613 QFrame* m_registerFrame; 1614 QFrame* m_buttonFrame; 1615 QFrame* m_formFrame; 1616 QFrame* m_summaryFrame; 1617 1618 // widgets 1619 KMyMoneyRegister::Register* m_register; 1620 KToolBar* m_buttonbar; 1621 1622 /** 1623 * This member holds the currently selected account 1624 */ 1625 MyMoneyAccount m_currentAccount; 1626 QString m_lastSelectedAccountID; 1627 1628 MyMoneyAccount m_reconciliationAccount; 1629 1630 /** 1631 * This member holds the transaction list 1632 */ 1633 QList<QPair<MyMoneyTransaction, MyMoneySplit> > m_transactionList; 1634 1635 QLabel* m_leftSummaryLabel; 1636 QLabel* m_centerSummaryLabel; 1637 QLabel* m_rightSummaryLabel; 1638 1639 KMyMoneyTransactionForm::TransactionForm* m_form; 1640 1641 /** 1642 * This member holds the load state of page 1643 */ 1644 bool m_needLoad; 1645 1646 bool m_newAccountLoaded; 1647 bool m_inEditMode; 1648 1649 QWidgetList m_tabOrderWidgets; 1650 QPoint m_tooltipPosn; 1651 KMyMoneyRegister::SelectedTransactions m_selectedTransactions; 1652 /** 1653 * This member keeps the date that was used as the last posting date. 1654 * It will be updated whenever the user modifies the post date 1655 * and is used to preset the posting date when new transactions are created. 1656 * This member is initialised to the current date when the program is started. 1657 */ 1658 static QDate m_lastPostDate; 1659 // pointer to the current transaction editor 1660 QPointer<TransactionEditor> m_transactionEditor; 1661 1662 // id's that need to be remembered 1663 QString m_accountGoto, m_payeeGoto; 1664 QString m_lastPayeeEnteredId; 1665 QScopedPointer<KBalanceWarning> m_balanceWarning; 1666 KMyMoneyAccountSelector* m_moveToAccountSelector; 1667 1668 // Reconciliation dialog 1669 KEndingBalanceDlg* m_endingBalanceDlg; 1670 KFindTransactionDlg* m_searchDlg; 1671 }; 1672 1673 #endif 1674