File indexing completed on 2024-05-12 16:43:43
0001 /* 0002 SPDX-FileCopyrightText: 2000-2002 Michael Edwardes <mte@users.sourceforge.net> 0003 SPDX-FileCopyrightText: 2000-2002 Javier Campos Morales <javi_c@users.sourceforge.net> 0004 SPDX-FileCopyrightText: 2000-2002 Felix Rodriguez <frodriguez@users.sourceforge.net> 0005 SPDX-FileCopyrightText: 2000-2002 John C <thetacoturtle@users.sourceforge.net> 0006 SPDX-FileCopyrightText: 2000-2002 Thomas Baumgart <ipwizard@users.sourceforge.net> 0007 SPDX-FileCopyrightText: 2000-2002 Kevin Tambascio <ktambascio@users.sourceforge.net> 0008 SPDX-FileCopyrightText: 2017 Łukasz Wojniłowicz <lukasz.wojnilowicz@gmail.com> 0009 SPDX-License-Identifier: GPL-2.0-or-later 0010 */ 0011 0012 #include "kcategoriesview_p.h" 0013 0014 #include <typeinfo> 0015 0016 // ---------------------------------------------------------------------------- 0017 // QT Includes 0018 0019 #include <QTimer> 0020 #include <QBitArray> 0021 #include <QMenu> 0022 0023 // ---------------------------------------------------------------------------- 0024 // KDE Includes 0025 0026 #include <KMessageBox> 0027 0028 // ---------------------------------------------------------------------------- 0029 // Project Includes 0030 0031 #include "mymoneyexception.h" 0032 #include "kmymoneysettings.h" 0033 #include "knewaccountdlg.h" 0034 #include "kcategoryreassigndlg.h" 0035 #include "mymoneyschedule.h" 0036 #include "mymoneybudget.h" 0037 #include "mymoneytransaction.h" 0038 #include "mymoneytransactionfilter.h" 0039 #include "mymoneyenums.h" 0040 #include "storageenums.h" 0041 #include "menuenums.h" 0042 0043 using namespace Icons; 0044 0045 KCategoriesView::KCategoriesView(QWidget *parent) : 0046 KMyMoneyAccountsViewBase(*new KCategoriesViewPrivate(this), parent) 0047 { 0048 Q_D(KCategoriesView); 0049 d->ui->setupUi(this); 0050 0051 connect(pActions[eMenu::Action::NewCategory], &QAction::triggered, this, &KCategoriesView::slotNewCategory); 0052 connect(pActions[eMenu::Action::EditCategory], &QAction::triggered, this, &KCategoriesView::slotEditCategory); 0053 connect(pActions[eMenu::Action::DeleteCategory], &QAction::triggered, this, &KCategoriesView::slotDeleteCategory); 0054 } 0055 0056 KCategoriesView::~KCategoriesView() 0057 { 0058 } 0059 0060 void KCategoriesView::executeCustomAction(eView::Action action) 0061 { 0062 Q_D(KCategoriesView); 0063 switch(action) { 0064 case eView::Action::Refresh: 0065 refresh(); 0066 break; 0067 0068 case eView::Action::SetDefaultFocus: 0069 QTimer::singleShot(0, d->ui->m_accountTree, SLOT(setFocus())); 0070 break; 0071 0072 default: 0073 break; 0074 } 0075 } 0076 0077 void KCategoriesView::refresh() 0078 { 0079 Q_D(KCategoriesView); 0080 if (!isVisible()) { 0081 d->m_needsRefresh = true; 0082 return; 0083 } 0084 d->m_needsRefresh = false; 0085 0086 d->m_proxyModel->invalidate(); 0087 d->m_proxyModel->setHideClosedAccounts(!KMyMoneySettings::showAllAccounts()); 0088 0089 // reinitialize the default state of the hidden categories label 0090 d->m_haveUnusedCategories = false; 0091 d->ui->m_hiddenCategories->hide(); 0092 d->m_proxyModel->setHideUnusedIncomeExpenseAccounts(KMyMoneySettings::hideUnusedCategory()); 0093 } 0094 0095 void KCategoriesView::showEvent(QShowEvent * event) 0096 { 0097 Q_D(KCategoriesView); 0098 if (!d->m_proxyModel) 0099 d->init(); 0100 0101 emit customActionRequested(View::Categories, eView::Action::AboutToShow); 0102 0103 if (d->m_needsRefresh) 0104 refresh(); 0105 0106 // don't forget base class implementation 0107 QWidget::showEvent(event); 0108 } 0109 0110 void KCategoriesView::updateActions(const MyMoneyObject& obj) 0111 { 0112 Q_D(KCategoriesView); 0113 if (typeid(obj) != typeid(MyMoneyAccount) && 0114 (obj.id().isEmpty() && d->m_currentCategory.id().isEmpty())) // do not disable actions that were already disabled)) 0115 return; 0116 0117 const auto& acc = static_cast<const MyMoneyAccount&>(obj); 0118 0119 if (d->m_currentCategory.id().isEmpty() && acc.id().isEmpty()) 0120 return; 0121 0122 switch (acc.accountType()) { 0123 case eMyMoney::Account::Type::Income: 0124 case eMyMoney::Account::Type::Expense: 0125 { 0126 const auto file = MyMoneyFile::instance(); 0127 auto b = file->isStandardAccount(acc.id()) ? false : true; 0128 pActions[eMenu::Action::EditCategory]->setEnabled(b); 0129 // enable delete action, if category/account itself is not referenced 0130 // by any object except accounts, because we want to allow 0131 // deleting of sub-categories. Also, we allow transactions, schedules and budgets 0132 // to be present because we can re-assign them during the delete process 0133 QBitArray skip((int)eStorage::Reference::Count); 0134 skip.fill(false); 0135 skip.setBit((int)eStorage::Reference::Transaction); 0136 skip.setBit((int)eStorage::Reference::Account); 0137 skip.setBit((int)eStorage::Reference::Schedule); 0138 skip.setBit((int)eStorage::Reference::Budget); 0139 0140 pActions[eMenu::Action::DeleteCategory]->setEnabled(b && !file->isReferenced(acc, skip)); 0141 d->m_currentCategory = acc; 0142 break; 0143 } 0144 default: 0145 pActions[eMenu::Action::EditCategory]->setEnabled(false); 0146 pActions[eMenu::Action::DeleteCategory]->setEnabled(false); 0147 d->m_currentCategory = MyMoneyAccount(); 0148 break; 0149 } 0150 } 0151 0152 /** 0153 * The view is notified that an unused income expense account has been hidden. 0154 */ 0155 void KCategoriesView::slotUnusedIncomeExpenseAccountHidden() 0156 { 0157 Q_D(KCategoriesView); 0158 d->m_haveUnusedCategories = true; 0159 d->ui->m_hiddenCategories->setVisible(d->m_haveUnusedCategories); 0160 } 0161 0162 void KCategoriesView::slotProfitChanged(const MyMoneyMoney &profit) 0163 { 0164 Q_D(KCategoriesView); 0165 d->netBalProChanged(profit, d->ui->m_totalProfitsLabel, View::Categories); 0166 } 0167 0168 void KCategoriesView::slotShowCategoriesMenu(const MyMoneyAccount& acc) 0169 { 0170 Q_UNUSED(acc); 0171 pMenus[eMenu::Menu::Category]->exec(QCursor::pos()); 0172 } 0173 0174 void KCategoriesView::slotSelectByObject(const MyMoneyObject& obj, eView::Intent intent) 0175 { 0176 switch(intent) { 0177 case eView::Intent::UpdateActions: 0178 updateActions(obj); 0179 break; 0180 0181 case eView::Intent::OpenContextMenu: 0182 slotShowCategoriesMenu(static_cast<const MyMoneyAccount&>(obj)); 0183 break; 0184 0185 default: 0186 break; 0187 } 0188 } 0189 0190 void KCategoriesView::slotSelectByVariant(const QVariantList& variant, eView::Intent intent) 0191 { 0192 switch (intent) { 0193 case eView::Intent::UpdateProfit: 0194 if (variant.count() == 1) 0195 slotProfitChanged(variant.first().value<MyMoneyMoney>()); 0196 break; 0197 default: 0198 break; 0199 } 0200 } 0201 0202 void KCategoriesView::slotNewCategory() 0203 { 0204 Q_D(KCategoriesView); 0205 MyMoneyAccount parent; 0206 MyMoneyAccount account; 0207 0208 // Preselect the parent account by looking at the current selected account/category 0209 if (!d->m_currentCategory.id().isEmpty() && 0210 d->m_currentCategory.isIncomeExpense()) { 0211 try { 0212 parent = MyMoneyFile::instance()->account(d->m_currentCategory.id()); 0213 } catch (const MyMoneyException &) { 0214 } 0215 } 0216 0217 KNewAccountDlg::createCategory(account, parent); 0218 } 0219 0220 void KCategoriesView::slotEditCategory() 0221 { 0222 Q_D(KCategoriesView); 0223 if (d->m_currentCategory.id().isEmpty()) 0224 return; 0225 0226 const auto file = MyMoneyFile::instance(); 0227 if (file->isStandardAccount(d->m_currentCategory.id())) 0228 return; 0229 0230 switch (d->m_currentCategory.accountType()) { 0231 case eMyMoney::Account::Type::Income: 0232 case eMyMoney::Account::Type::Expense: 0233 break; 0234 default: 0235 return; 0236 } 0237 0238 // set a status message so that the application can't be closed until the editing is done 0239 // slotStatusMsg(caption); 0240 0241 QPointer<KNewAccountDlg> dlg = 0242 new KNewAccountDlg(d->m_currentCategory, true, true, 0, i18n("Edit category '%1'", d->m_currentCategory.name())); 0243 0244 dlg->setOpeningBalanceShown(false); 0245 dlg->setOpeningDateShown(false); 0246 0247 if (dlg && dlg->exec() == QDialog::Accepted) { 0248 try { 0249 MyMoneyFileTransaction ft; 0250 0251 auto account = dlg->account(); 0252 auto parent = dlg->parentAccount(); 0253 0254 // we need to modify first, as reparent would override all other changes 0255 file->modifyAccount(account); 0256 if (account.parentAccountId() != parent.id()) 0257 file->reparentAccount(account, parent); 0258 0259 ft.commit(); 0260 0261 // reload the account object as it might have changed in the meantime 0262 emit selectByObject(account, eView::Intent::None); 0263 0264 } catch (const MyMoneyException &e) { 0265 KMessageBox::error(this, i18n("Unable to modify category '%1'. Cause: %2", d->m_currentCategory.name(), QString::fromLatin1(e.what()))); 0266 } 0267 } 0268 0269 delete dlg; 0270 // ready(); 0271 } 0272 0273 void KCategoriesView::slotDeleteCategory() 0274 { 0275 Q_D(KCategoriesView); 0276 if (d->m_currentCategory.id().isEmpty()) 0277 return; // need an account ID 0278 0279 const auto file = MyMoneyFile::instance(); 0280 // can't delete standard accounts or account which still have transactions assigned 0281 if (file->isStandardAccount(d->m_currentCategory.id())) 0282 return; 0283 0284 // check if the account is referenced by a transaction or schedule 0285 QBitArray skip((int)eStorage::Reference::Count); 0286 skip.fill(false); 0287 skip.setBit((int)eStorage::Reference::Account); 0288 skip.setBit((int)eStorage::Reference::Institution); 0289 skip.setBit((int)eStorage::Reference::Payee); 0290 skip.setBit((int)eStorage::Reference::Tag); 0291 skip.setBit((int)eStorage::Reference::Security); 0292 skip.setBit((int)eStorage::Reference::Currency); 0293 skip.setBit((int)eStorage::Reference::Price); 0294 const auto hasReference = file->isReferenced(d->m_currentCategory, skip); 0295 0296 // if we get here and still have transactions referencing the account, we 0297 // need to check with the user to possibly re-assign them to a different account 0298 auto needAskUser = true; 0299 0300 MyMoneyFileTransaction ft; 0301 0302 if (hasReference) { 0303 // show transaction reassignment dialog 0304 0305 needAskUser = false; 0306 auto dlg = new KCategoryReassignDlg(this); 0307 auto categoryId = dlg->show(d->m_currentCategory); 0308 delete dlg; // and kill the dialog 0309 if (categoryId.isEmpty()) 0310 return; // the user aborted the dialog, so let's abort as well 0311 0312 auto newCategory = file->account(categoryId); 0313 try { 0314 { 0315 // KMSTATUS(i18n("Adjusting transactions...")); 0316 /* 0317 d->m_currentCategory.id() is the old id, categoryId the new one 0318 Now search all transactions and schedules that reference d->m_currentCategory.id() 0319 and replace that with categoryId. 0320 */ 0321 // get the list of all transactions that reference the old account 0322 MyMoneyTransactionFilter filter(d->m_currentCategory.id()); 0323 filter.setReportAllSplits(false); 0324 QList<MyMoneyTransaction> tlist; 0325 QList<MyMoneyTransaction>::iterator it_t; 0326 file->transactionList(tlist, filter); 0327 0328 // slotStatusProgressBar(0, tlist.count()); 0329 // int cnt = 0; 0330 for (it_t = tlist.begin(); it_t != tlist.end(); ++it_t) { 0331 // slotStatusProgressBar(++cnt, 0); 0332 MyMoneyTransaction t = (*it_t); 0333 if (t.replaceId(categoryId, d->m_currentCategory.id())) 0334 file->modifyTransaction(t); 0335 } 0336 // slotStatusProgressBar(tlist.count(), 0); 0337 } 0338 // now fix all schedules 0339 { 0340 // KMSTATUS(i18n("Adjusting scheduled transactions...")); 0341 QList<MyMoneySchedule> slist = file->scheduleList(d->m_currentCategory.id()); 0342 QList<MyMoneySchedule>::iterator it_s; 0343 0344 // int cnt = 0; 0345 // slotStatusProgressBar(0, slist.count()); 0346 for (it_s = slist.begin(); it_s != slist.end(); ++it_s) { 0347 // slotStatusProgressBar(++cnt, 0); 0348 MyMoneySchedule sch = (*it_s); 0349 if (sch.replaceId(categoryId, d->m_currentCategory.id())) { 0350 file->modifySchedule(sch); 0351 } 0352 } 0353 // slotStatusProgressBar(slist.count(), 0); 0354 } 0355 // now fix all budgets 0356 { 0357 // KMSTATUS(i18n("Adjusting budgets...")); 0358 QList<MyMoneyBudget> blist = file->budgetList(); 0359 QList<MyMoneyBudget>::const_iterator it_b; 0360 for (it_b = blist.constBegin(); it_b != blist.constEnd(); ++it_b) { 0361 if ((*it_b).hasReferenceTo(d->m_currentCategory.id())) { 0362 MyMoneyBudget b = (*it_b); 0363 MyMoneyBudget::AccountGroup fromBudget = b.account(d->m_currentCategory.id()); 0364 MyMoneyBudget::AccountGroup toBudget = b.account(categoryId); 0365 toBudget += fromBudget; 0366 b.setAccount(toBudget, categoryId); 0367 b.removeReference(d->m_currentCategory.id()); 0368 file->modifyBudget(b); 0369 0370 } 0371 } 0372 // slotStatusProgressBar(blist.count(), 0); 0373 } 0374 } catch (MyMoneyException &e) { 0375 KMessageBox::error(this, i18n("Unable to exchange category <b>%1</b> with category <b>%2</b>. Reason: %3", d->m_currentCategory.name(), newCategory.name(), QString::fromLatin1(e.what()))); 0376 // slotStatusProgressBar(-1, -1); 0377 return; 0378 } 0379 // slotStatusProgressBar(-1, -1); 0380 } 0381 0382 // retain the account name for a possible later usage in the error message box 0383 // since the account removal notifies the views the selected account can be changed 0384 // so we make sure by doing this that we display the correct name in the error message 0385 auto selectedAccountName = d->m_currentCategory.name(); 0386 0387 // at this point, we must not have a reference to the account 0388 // to be deleted anymore 0389 // special handling for categories to allow deleting of empty subcategories 0390 { 0391 // open a compound statement here to be able to declare variables 0392 // which would otherwise not work within a case label. 0393 0394 // case A - only a single, unused category without subcats selected 0395 if (d->m_currentCategory.accountList().isEmpty()) { 0396 if (!needAskUser || (KMessageBox::questionYesNo(this, i18n("<qt>Do you really want to delete category <b>%1</b>?</qt>", selectedAccountName)) == KMessageBox::Yes)) { 0397 try { 0398 file->removeAccount(d->m_currentCategory); 0399 d->m_currentCategory.clearId(); 0400 emit selectByObject(d->m_currentCategory, eView::Intent::None); 0401 ft.commit(); 0402 } catch (const MyMoneyException &e) { 0403 KMessageBox::error(this, i18n("<qt>Unable to delete category <b>%1</b>. Cause: %2</qt>", selectedAccountName, QString::fromLatin1(e.what()))); 0404 } 0405 } 0406 return; 0407 } 0408 // case B - we have some subcategories, maybe the user does not want to 0409 // delete them all, but just the category itself? 0410 auto parentAccount = file->account(d->m_currentCategory.parentAccountId()); 0411 0412 QStringList accountsToReparent; 0413 int result = KMessageBox::questionYesNoCancel(this, 0414 i18n("<qt>Do you want to delete category <b>%1</b> with all its sub-categories or only " 0415 "the category itself? If you only delete the category itself, all its sub-categories " 0416 "will be made sub-categories of <b>%2</b>.</qt>", selectedAccountName, parentAccount.name()), 0417 QString(), 0418 KGuiItem(i18n("Delete all")), 0419 KGuiItem(i18n("Just the category"))); 0420 if (result == KMessageBox::Cancel) 0421 return; // cancel pressed? ok, no delete then... 0422 // "No" means "Just the category" and that means we need to reparent all subaccounts 0423 bool need_confirmation = false; 0424 // case C - User only wants to delete the category itself 0425 if (result == KMessageBox::No) 0426 accountsToReparent = d->m_currentCategory.accountList(); 0427 else { 0428 // case D - User wants to delete all subcategories, now check all subcats of 0429 // d->m_currentCategory and remember all that cannot be deleted and 0430 // must be "reparented" 0431 foreach (const auto accountID, d->m_currentCategory.accountList()) { 0432 // reparent account if a transaction is assigned 0433 if (file->transactionCount(accountID) != 0) 0434 accountsToReparent.push_back(accountID); 0435 else if (!file->account(accountID).accountList().isEmpty()) { 0436 // or if we have at least one sub-account that is used for transactions 0437 if (!file->hasOnlyUnusedAccounts(file->account(accountID).accountList())) { 0438 accountsToReparent.push_back(accountID); 0439 //qDebug() << "subaccount not empty"; 0440 } 0441 } 0442 } 0443 if (!accountsToReparent.isEmpty()) 0444 need_confirmation = true; 0445 } 0446 if (!accountsToReparent.isEmpty() && need_confirmation) { 0447 if (KMessageBox::questionYesNo(this, i18n("<p>Some sub-categories of category <b>%1</b> cannot " 0448 "be deleted, because they are still used. They will be made sub-categories of <b>%2</b>. Proceed?</p>", selectedAccountName, parentAccount.name())) != KMessageBox::Yes) { 0449 return; // user gets wet feet... 0450 } 0451 } 0452 // all good, now first reparent selected sub-categories 0453 try { 0454 auto parent = file->account(d->m_currentCategory.parentAccountId()); 0455 for (QStringList::const_iterator it = accountsToReparent.constBegin(); it != accountsToReparent.constEnd(); ++it) { 0456 auto child = file->account(*it); 0457 file->reparentAccount(child, parent); 0458 } 0459 // reload the account because the sub-account list might have changed 0460 d->m_currentCategory = file->account(d->m_currentCategory.id()); 0461 // now recursively delete remaining sub-categories 0462 file->removeAccountList(d->m_currentCategory.accountList()); 0463 // don't forget to update d->m_currentCategory, because we still have a copy of 0464 // the old account list, which is no longer valid 0465 d->m_currentCategory = file->account(d->m_currentCategory.id()); 0466 } catch (const MyMoneyException &e) { 0467 KMessageBox::error(this, i18n("<qt>Unable to delete a sub-category of category <b>%1</b>. Reason: %2</qt>", selectedAccountName, QString::fromLatin1(e.what()))); 0468 return; 0469 } 0470 } 0471 // the category/account is deleted after the switch 0472 0473 0474 try { 0475 file->removeAccount(d->m_currentCategory); 0476 d->m_currentCategory.clearId(); 0477 emit selectByObject(MyMoneyAccount(), eView::Intent::None); 0478 ft.commit(); 0479 } catch (const MyMoneyException &e) { 0480 KMessageBox::error(this, i18n("Unable to delete category '%1'. Cause: %2", selectedAccountName, QString::fromLatin1(e.what()))); 0481 } 0482 } 0483